§1 — Problem & goal

The downstream SN↔DD unit-mismatch axis (followup f-ddhu-004) compares each standard name's declared unit against the unit its Data-Dictionary source facet declares, and flags disagreements. A large fraction of what it flags is not a physical disagreement — it is the same unit written in a different token order or symbol spelling on the two sides. Those false positives bury the handful of genuine dimensional defects and make the axis untrustworthy for curation.

This plan closes the axis in three moves:

  1. Ordering normalization — make the DD-side and SN-side unit strings flow through the same pint parser and the same canonical formatter before they are compared, so ordering-only and spelling-only differences collapse. The user's directive: "both the DD and the SN should use the same pint unit parser; this defines the order — this check is reconcilable in code." §2 confirms it is.
  2. Prune the stale ion_charge finding — remove the mismatch finding that is an artifact of the ordering bug (or of an already-landed rename) rather than a real defect.
  3. Triage the genuine residual — review the small set of names whose declared unit truly disagrees with the DD after normalization, and correct each at source.

Scope split. This repo (ISN) owns the unit models and the three unit formatters — it is where the canonical form is defined. The mismatch axis itself runs downstream in the imas-codex catalog pipeline, which consumes ISN's canonical form. This plan delivers a single ISN-side canonical-unit function and reconciles the formatters onto it; the downstream axis then compares canonical strings on both sides.

§2 — Check & confirm (verdict: reconcilable in code)

Confirmed against the live source. There are three independent unit-formatting paths in this repo, and they do not agree:

Empirical run of all three paths over sample units (input token order deliberately scrambled):

input DD "U" (unsorted, long) SN ~F (sorted, short) regex canonicalize
s^-1.mmeter.second^-1.0m.s^-1m.s^-1
m.s^-1meter.second^-1m.s^-1m.s^-1
m^-3.kgkilogram.meter^-3.0kg.m^-3kg.m^-3
m^-1.keVkiloelectron_volt.meter^-1.0keV.m^-1keV.m^-1
V.ssecond.voltV.sV.s
kg.m^2.s^-3.A^-1ampere^-1.kilogram.meter^2.0.second^-3.0A^-1.kg.m^2.s^-3A^-1.kg.m^2.s^-3

Three findings from the run:

  1. The DD "U" formatter is the entire source of the spurious mismatches. For the identical physical unit it emits meter.second^-1.0 where the SN side stores m.s^-1 — a guaranteed string mismatch on every compound unit.
  2. "U" also has an order-dependent float-exponent bug. m.s^-1 (already canonical) formats as second^-1, but s^-1.m (reordered) formats as second^-1.0 — the same unit yields different strings depending on the author's token order, because pint re-parsing yields float exponents that "U" renders verbatim (~F guards this with int(exp); "U" does not).
  3. ~F and the regex canonicalizer agree on every sample — good — but they are two separate implementations of the ordering. They agree today only because authored short symbols happen to equal pint's short symbols; a unit alias or prefix pint spells differently would silently diverge.

Verdict: the user's claim holds — the check is reconcilable in code. Both sides already have pint. Route both the DD unit string and the SN unit string through pint.Unit(...) and format both with the same sorted short-symbol formatter (~F); the ordering-only and spelling-only mismatches vanish and only genuine dimensional differences survive. The ~F formatter is already correct; the "U" formatter is the outlier to retire or fix, and the regex canonicalizer should fold onto the same single ordering authority.

One unit, three code paths — reconcile both sides on pint ~F author / DD input s^-1.m (any order) DD "U" formatter · units/__init__.py meter.second^-1.0 long names, float exp, unsorted SN ~F formatter · __init__.py m.s^-1 short, sorted, int exp — canonical regex canonicalize · models.py m.s^-1 agrees today, separate impl (not pint) Fix — one ordering authority: route DD unit and SN unit through the same pint.Unit(...) + ~F, then compare strings → ordering/spelling mismatches vanish; only real defects remain.
One physical unit, three code paths. The DD "U" formatter (red) diverges by long names, float exponents, and no sort; ~F (solid green) is the canonical short-sorted form; the regex canonicalizer (dashed green) agrees today but is a second ordering implementation. Reconciliation routes both compared sides through pint ~F.

§3 — Reconciliation design ✓ landed 2026-07-22

Added canonical_unit(s) (pint parse + ~F, integer exponents, ASCII short symbols) as the single ordering/spelling authority, exported from imas_standard_names; retired the divergent "U" formatter (no in-repo consumer); folded the _canonicalize_unit_order regex onto canonical_unit() and demoted the grammar check to a CI stability test. A catalog scan caught the §2 spelling-drift trap live — authored m.ohm renders pint m.Ω — fixed by ASCII-mapping pint's non-ASCII glyphs (Ω→ohm, µ→u, °→deg) before sorting; all 20 catalog units now agree exactly (0 divergences). Ordering/spelling variants normalize equal and the float-exponent .0 artifact is gone. 347 unit-affected tests green. Full record: §3 landed.

§4 — Curation: prune stale + triage genuine ✓ landed 2026-07-23

Re-checked all four pre-reconciliation candidates against the live catalog, normalising both sides through canonical_unit(). 1 finding pruned (stale), 3 recorded as intentional, 0 genuine defects — no sn edit required. The ion_charge finding (effective_thermal_ion_charge_state_energy_velocity_due_to_convection) is pruned as stale: it is a convective velocity, so m.s^-1 is correct, and it has no DD source facet — the flag tripped on the embedded energy token plus the retired "U" ordering artifact. The two energy parents (effective_particle_energy, perturbed_particle_energy) are intentionally unit-less normalised-coordinate parents (dimensionless quantity in their children); particle_mass is an intentional generalisation of the DD's normalised mass (source_unit=1) to the physical concept, with the DD-faithful dimensionless value carried by normalized_particle_mass. All four are validation_status=valid. Downstream imas-codex axis still needs re-pointing at canonical_unit() (follow-on). Full record: §4 landed.

§ Decisions

How to make DD and SN share one canonical unit form?

The "U" formatter is the sole source of the spurious mismatches (long names, order-dependent float exponents, unsorted). Retire it and make the already-correct ~F formatter (short symbols, sorted, integer exponents) the single source of truth, exposed as canonical_unit(s) . No in-repo caller of "U" was found; its consumers must be re-pointed at canonical_unit() before removal.

Should _canonicalize_unit_order be reimplemented on pint, or kept as regex with a parity test?

Fold _canonicalize_unit_order into the pint-based canonical_unit() so ordering has exactly one authority (pint parse + ~F ). The regex is not retired outright — it is demoted to a unit-test stability assertion on canonical_unit() output: a test asserts the canonical string matches the dot-exponent grammar (sorted short tokens, integer exponents, no .0 artifact) so a pint version bump or symbol-spelling change fails CI rather than silently drifting the catalog's canonical form.

Is the ion_charge (effective_thermal_ion_charge_state_energy_velocity_due_to_convection) finding stale (prune) or genuine (triage)?

Prune — stale. effective_thermal_ion_charge_state_energy_velocity_due_to_convection is a convective VELOCITY (head noun velocity_due_to_convection); its m.s^-1 unit is dimensionally correct and it carries NO DD source facet (source_unit=None) for the axis to disagree with. The flag was a false positive — a token-level check tripping on the embedded energy/charge_state tokens in a velocity name, compounded by the now-retired "U" ordering artifact. Live-catalog re-check: validation_status=valid. Confirms the user's read.

§ Followups

§3 — Unify unit formatting on one pint-defined canonical form

Implement the single ordering authority so DD-side and SN-side unit strings reconcile. Add canonical_unit(s) (pint parse + ~F), reconcile the "U" formatter, and add the parity/round-trip tests that prove ordering-only mismatches collapse. §2 confirmed the mechanism and the "U" float-exponent bug; this followup lands the fix. §4 curation (prune stale ion_charge + triage genuine) is a separate followup that runs after this lands, because the residual list is only trustworthy once normalization is in place.

Project: imas-standard-names
Plan:    sn-dd-unit-curation (http://localhost:8765/imas-standard-names/sn-dd-unit-curation.html)
Section: §3
Tier:    opus

Context
  The SN↔DD unit-mismatch axis flags ordering-only and spelling-only false
  positives. §2 confirmed the cause: three divergent unit-formatting paths —
  the DD "U" formatter emits long names + float exponents + unsorted order,
  while the SN ~F formatter and a separate regex canonicalizer both produce
  the sorted short form. Unify everything on one pint-defined canonical form.

State to read  (CODE / FILES — not the plan itself)
  imas_standard_names/units/__init__.py    ("U" formatter — the outlier)
  imas_standard_names/__init__.py:104-131  ("F" / format_unit_udunits_dot_exponent — the correct one)
  imas_standard_names/models.py:324-380,633-671  (_canonicalize_unit_order, formatted_unit, scalar validator)
  tests/  (existing unit tests to extend)

Scope locks / constraints  (non-decision)
  - pint is already a dep (pyproject: pint>=0.24.4,<0.25.0). No new deps.
  - Do NOT rewrite stored catalog units — they already store the sorted short
    form. This change only affects the DD-side comparison string + retiring "U".
  - Before removing "U", grep all consumers of it and of the units/__init__.py
    module registry; there was no in-repo caller found, but confirm.
  - The mismatch axis itself lives downstream in imas-codex; this followup only
    delivers the ISN-side canonical_unit() helper it will import.

Done-when  (both decisions are LOCKED: retire "U"; fold regex into pint; single source of truth)
  1. canonical_unit(s) added (pint.Unit + {u:~F}, exponents coerced to int) —
     the single source-of-truth formatter.
  2. The "U" formatter is REMOVED from units/__init__.py after confirming no
     consumers (re-point any found at canonical_unit() first).
  3. StandardNameScalarEntry's unit validator calls canonical_unit() instead of
     the hand-rolled _canonicalize_unit_order regex (one ordering authority).
  4. Regex STABILITY test: asserts canonical_unit() output matches the
     dot-exponent grammar (sorted short tokens, integer exponents, no ".0"
     artifact) over the full catalog unit set — guards against pint drift.
  5. Round-trip test: ordering/spelling variants of one unit normalize equal
     (e.g. "s^-1.m" == "m.s^-1"); the float-exponent artifact is gone.
  6. Full test suite green.
  7. Followup written into the plan for §4 curation; this followup resolved.

§3 landed — canonical_unit() is the single pint-defined ordering/spelling authority; "U" formatter retired (no consumer); regex folded onto pint + demoted to a CI stability test. Caught the §2 spelling-drift trap live (m.ohm -> pint m.Ω) and fixed it by ASCII-mapping pint glyphs before sorting — all 20 catalog units agree exactly (0 divergences). 347 unit-affected tests green. See archive/sn-dd-unit-curation-s3-landed.html.

§4 — Curation: prune stale ion_charge + triage genuine residuals

Normalization (§3) has landed, so the residual mismatch list is finally trustworthy. Re-check effective_thermal_ion_charge_state_energy_velocity_due_to_convection against the live catalog + DD facet — the user's read is the ion_charge finding is stale (an artifact of the retired "U" ordering bug); prune it if confirmed. Then triage each name that still mismatches after canonical_unit() normalization, correcting at source via sn edit. Depends on the downstream imas-codex axis being re-pointed at canonical_unit() so the residual list reflects the reconciled comparison.

Project: imas-standard-names
Plan:    sn-dd-unit-curation (http://localhost:8765/imas-standard-names/sn-dd-unit-curation.html)
Section: §4
Tier:    opus

Context
  §3 landed the ISN-side canonical_unit() authority (pint parse + ~F, ASCII
  short symbols). Ordering-only and spelling-only false positives now collapse
  under canonical_unit(sn) == canonical_unit(dd). With normalization in place the
  residual SN<->DD unit-mismatch list is finally trustworthy and small; §4 prunes
  the stale finding and triages each genuine residual.

State to read
  GET /plan/imas-standard-names/sn-dd-unit-curation   (decisions, followups, status)
  imas_standard_names/__init__.py  (canonical_unit — the helper the axis imports)
  The pre-reconciliation candidate list (from the model-selection plan):
    effective_particle_energy, particle_mass, perturbed_particle_energy,
    effective_thermal_ion_charge_state_energy_velocity_due_to_convection

Pre-req / constraint
  - The downstream mismatch axis lives in imas-codex and must import
    canonical_unit() from ISN before the residual list is regenerated. Confirm
    the axis is re-pointed (or regenerate the list applying canonical_unit() to
    both sides yourself) BEFORE triaging — a list built on the old "U" form is
    not trustworthy.
  - All graph edits go through `sn edit` / `sn run` — NEVER raw Cypher
    (project convention; the classifier blocks it anyway).

Open decision to resolve (do NOT pre-decide in code)
  ion-charge-finding: is effective_thermal_ion_charge_state_energy_velocity_due_to_convection
  stale (prune) or a genuine dimensional disagreement (triage)? Lock this
  decision with the re-checked evidence.

Done-when
  1. The residual list is regenerated with both sides normalized through
     canonical_unit(); ordering/spelling-only entries are gone.
  2. The ion_charge finding is adjudicated against the live catalog + DD facet;
     the ion-charge-finding decision is locked with rationale (prune or keep).
  3. Each genuine residual is triaged: DD facet unit vs declared unit compared,
     the correct one chosen, corrected at source via `sn edit`. Intentional
     dimensionless-vs-dimensional cases (derived operator forms) are recorded
     with their reason rather than 'fixed'.
  4. Plan updated: §4 collapsed with the per-name verdicts; this followup
     resolved; plan status set to shipped/done if §4 is the last section.

§4 landed — all four candidates re-checked against the live catalog with canonical_unit() normalising both sides. 1 pruned (ion_charge — stale convective velocity, m.s^-1 correct, no DD facet), 3 recorded as intentional (effective_particle_energy + perturbed_particle_energy unit-less normalised-coordinate parents; particle_mass an intentional generalisation of DD normalised mass source_unit=1, dimensionless value in child normalized_particle_mass). 0 genuine defects → no sn edit required; all four validation_status=valid. ion-charge-finding decision locked = prune-stale. See archive/sn-dd-unit-curation-s4-landed.html.

Re-point the downstream imas-codex mismatch axis at canonical_unit() and suppress the ion_charge false positive

ISN §3/§4 are complete: canonical_unit() is the single ordering/spelling authority and the ISN-side candidate list is adjudicated. The downstream SN↔DD unit-mismatch axis in imas-codex does not yet import canonical_unit() (grep confirms no hits). Re-point it so both sides normalise through the ISN helper, add a suppression/override so the pruned ion_charge velocity false positive cannot re-surface, and regenerate the axis over the full catalog to confirm the residual set carries no ordering/spelling artifacts. This work is imas-codex-scoped, not ISN.

Project: imas-codex
Plan:    (SN↔DD unit reconciliation — consumer side; see ISN plan sn-dd-unit-curation)
Tier:    opus

Context
  ISN landed canonical_unit() (imas_standard_names.canonical_unit) as the single pint-defined
  unit ordering/spelling authority, and adjudicated the pre-reconciliation candidate list
  (ion_charge pruned as stale; energy/mass parents recorded as intentional). The imas-codex
  SN↔DD unit-mismatch axis still uses its own unit handling and has NOT been re-pointed at the
  ISN helper.

State to read
  imas_standard_names/__init__.py            (canonical_unit — import this)
  imas_codex/units/                          (normalize_unit_symbol — current imas-codex path)
  imas_codex/standard_names/                 (where the SN↔DD unit comparison axis runs)
  ISN plan archive/sn-dd-unit-curation-s4-landed.html  (per-name verdicts + rationale)

Done-when
  1. The mismatch axis compares canonical_unit(sn_unit) == canonical_unit(dd_facet_unit) on both
     sides (import ISN's canonical_unit; retire any divergent local formatter).
  2. A suppression/override records the ion_charge velocity finding as adjudicated-stale so it
     cannot re-surface (effective_thermal_ion_charge_state_energy_velocity_due_to_convection —
     a convective velocity, m.s^-1 correct, no DD facet).
  3. The axis is regenerated over the full catalog; the residual set is confirmed free of
     ordering/spelling-only artifacts, and any NEW genuine residual is triaged via `sn edit`
     (never raw Cypher).
  4. Tests green; followup written; this followup resolved.

Landed (imas-codex). The SN↔DD unit-mismatch axis now normalises both sides through ISN canonical_unit (via imas_codex/units/dd_unit_exceptions.units_agree) and — critically — reads the LIVE HAS_STANDARD_NAME edges, not the denormalised source_paths scalar (which stranded phantom mismatches), excluding terminal superseded/exhausted/contested names. ion_charge handled: charge-number e→1 is a curated DD-unit-bug glob, and the pruned velocity finding carries no DD facet so the edge axis never sees it. Full-catalog regen confirmed ordering/spelling/stale-scalar/dead-name/DD-bug/equivalence false positives all gone. Genuine residual (all catalog-origin, so reset-to-extracted applies once source_filter='dd' is dropped) triaged graph-side: mis-attachments reset+detached (power_of_wave_beam psi, rotation_frequency Hz, total_neutron_flux_due_to_fusion power, poloidal_flux over-merge), 3 wrong-unit catalog names reset (etendue m.sr, particle_distribution '1', + dead distribution_amplitude), 38 multi-HAS_UNIT-edge corruptions deduped to the canonical unit, 4 missing HAS_UNIT edges backfilled, 3 neutral-flux edges reconciled to their W.m^-2 property, poloidal_flux detached from f_df_dpsi (product_of_poloidal_current preserved). All 6 tests/graph/test_sn_unit_integrity.py green; referential-integrity + data-quality suites green (97 passed). imas-codex commit 7d9648e7 (canonical_unit re-point + dd_unit_exceptions.yaml + loader + drop obsolete backfill script). A parallel hardening pass added StandardName edge-integrity invariants + a HAS_UNIT self-heal writer fix (imas-codex 30ba5053 root cause: _write_standard_name_edges MERGE'd HAS_UNIT without dropping an existing different-unit edge).

SN↔DD attachment consistency — reconcile the source_paths scalar + close edge-gaps

Hardening the SN↔DD edge fabric (imas-codex tests/graph/test_sn_edge_integrity.py) surfaced a broad pre-existing consistency issue distinct from the unit axis: ~25 accepted StandardNames whose denormalised source_paths scalar diverges from their live HAS_STANDARD_NAME/PRODUCED_NAME edges. The divergence is MIXED-direction — some are scalar-stale (edges are truth, e.g. area_of_flux_surface scalar lists grid/surface but edges are grid/area) and some are edge-GAPS (scalar is right but attachment edges are missing, e.g. atomic_number has 8 z_n paths in the scalar but only 1 HAS_STANDARD_NAME edge). A blind scalar:=edges reconcile fixes the stale class but MASKS the edge-gap class. Needs: (1) reconcile the scalar to the live edges under the edges-are-truth convention, and (2) treat attachment COMPLETENESS (missing HAS_STANDARD_NAME edges for paths an accepted SN should cover) as a separate coverage concern/test, not silently dropped. Also fold in the writer root-cause for the scalar drift if found. imas-codex-scoped.

Project: imas-codex
Plan:    (SN↔DD attachment consistency — see ISN plan sn-dd-unit-curation f-sdu-004)
Tier:    opus

Context
  The unit axis (f-sdu-003) is landed and green. Hardening the edge fabric added
  tests/graph/test_sn_edge_integrity.py, whose test_source_paths_scalar_consistent_with_edges
  fails on ~25 accepted SNs: the source_paths scalar lists dd:/signals: paths not backed by any
  live HAS_STANDARD_NAME/PRODUCED_NAME edge. The divergence is MIXED-direction (verified by
  sampling):
    - scalar-stale: edges are truth, scalar has residue (area_of_flux_surface: scalar grid/surface,
      edges grid/area; electron_temperature: scalar/edge langmuir t_e subsets differ).
    - edge-gap: scalar is right, HAS_STANDARD_NAME edges are missing (atomic_number: 8 z_n in
      scalar, 1 edge; area_of_flux_surface: 12 scalar vs 19 edges but different paths).

State to read
  tests/graph/test_sn_edge_integrity.py (the failing invariant + the 3 passing ones)
  imas_codex/standard_names/graph_ops.py (SN edge + source_paths writers; the HAS_UNIT self-heal
    landed here — mirror it for source_paths / HAS_STANDARD_NAME if drift originates there)
  imas_codex/standard_names/ (attachment / provenance_lifecycle / consolidation — where
    source_paths and HAS_STANDARD_NAME are (re)written)
  agents/schema-reference.md (StandardName.source_paths, HAS_STANDARD_NAME, PRODUCED_NAME)

Done-when
  1. Reconcile source_paths := the live edge paths graph-wide (edges-are-truth); the scalar
     becomes a faithful mirror. Do it as an inline migration (repl/graph shell), not a repair script.
  2. Attachment COMPLETENESS is handled as its OWN concern: identify accepted SNs whose scalar (or
     physics) implies DD paths they lack a HAS_STANDARD_NAME edge for (edge-gap, e.g. atomic_number
     → the other 7 z_n paths); decide whether to backfill the edges (via the sanctioned attach path,
     not raw Cypher for name/docs) or record as a coverage gap — do NOT let the scalar reconcile
     silently erase them.
  3. Root-cause the source_paths drift in the writer(s) and fix so the scalar cannot desync on
     prune/refine/attach (mirror the HAS_UNIT self-heal pattern).
  4. test_source_paths_scalar_consistent_with_edges green; edge-gap coverage tracked or closed;
     tests green; this followup resolved.

Scalar drift root-caused + fixed, and the scalar reconciled graph-wide. ROOT CAUSE (confirmed by reading every write/mutation site): sn.source_paths is a compose-time snapshot written once (graph_ops.py:2684, coalesce(new,existing)) and reconciled NOWHERE — persist_refined_name migrates edges but not the scalar, the prune/sever path deletes edges but not the scalar, and reconcile_standard_name_sources only touches FROM_DD_PATH edges. DURABLE FIX (imas-codex b955da37): reconcile_standard_name_source_paths() materializes sn.source_paths := sorted distinct union of 'dd:'+imas.id (HAS_STANDARD_NAME) ∪ non-derived PRODUCED_NAME src.id (preserving derived: entries), scoped to non-terminal names, idempotent; wired into the post-drain reconcile in loop.py so it can't desync again after any future refine/prune/remap. Plus the HAS_UNIT self-heal writer fix (b955da37's sibling 30ba5053). DATA: reconciled graph-wide to steady state (reconcile now returns 0). 28 integrity tests green (edge-integrity 4, unit-integrity 6, loader 14, source_paths-reconcile 4). Attachment COMPLETENESS (backfilling missing HAS_STANDARD_NAME edges for DD-eligible, unit-agreeing paths an accepted SN should cover — e.g. atomic_number → all z_n) is deliberately NOT folded into the scalar-consistency invariant (it would flap on legitimate compose history); re-scoped to f-sdu-005. Note: an eager scalar materialize (Step B) ran before the two-step recommendation, so pure scalar-only edge-gap hints with neither edge (e.g. atomic_number's 7 extra z_n) dropped from the scalar; not a real loss — f-sdu-005 backfills those edges from DD-side eligibility + units_agree, not the stale scalar.

SN↔DD attachment completeness — backfill missing HAS_STANDARD_NAME edges

Distinct from the now-closed scalar-consistency concern (f-sdu-004): the SN↔DD attachment fabric is INCOMPLETE — some accepted StandardNames lack a HAS_STANDARD_NAME edge to DD paths they should cover. Diagnosis (edge-hardening pass): ~65 accepted-SN scalar/produced paths point to an existing, SN-eligible DD node whose unit AGREES with the SN, yet carry no attachment edge (e.g. area_of_flux_surface m^2 ← .../grid/surface; electron_temperature eV ← langmuir_probes/.../t_e; poloidal_angle_of_measurement_position rad ← .../position/theta; atomic_number ← all z_n). These are real coverage gaps, not junk. Backfill the missing edges from DD-side eligibility + units_agree() gating (route the z_n→atomic_number 'e'/'1' quirk through the dd_unit_exceptions charge-number rule), via the sanctioned attach path — never raw Cypher for name/docs. Once edges land, the durable source_paths reconcile (b955da37) picks them into the scalar automatically. Consider a coverage gate/test so future gaps surface. imas-codex-scoped, DATA + possibly attach-path code.

Project: imas-codex
Plan:    (SN↔DD attachment completeness — see ISN plan sn-dd-unit-curation f-sdu-005)
Tier:    opus

Context
  f-sdu-003 (unit axis) and f-sdu-004 (source_paths scalar drift root-cause + durable
  reconcile_standard_name_source_paths, imas-codex b955da37) are landed and green. Remaining:
  the attachment fabric is INCOMPLETE — accepted SNs lack HAS_STANDARD_NAME edges to DD paths
  they should cover. The edge-hardening pass classified ~65 such edge-gaps (existing DD node,
  SN-eligible node_category, unit agrees) plus ~483 remapped entries (correctly dropped by the
  scalar reconcile).

State to read
  tests/graph/test_sn_edge_integrity.py (the invariants; note test_dd_attachment_unit_agrees_with_name
    is scoped to DD paths that HAVE a unit edge)
  imas_codex/standard_names/graph_ops.py (reconcile_standard_name_source_paths + the attach path)
  imas_codex/units/dd_unit_exceptions.py (units_agree — gate every backfill through it)
  imas_codex/core/node_categories.py (SN_SOURCE_CATEGORIES — eligibility)

  Classification query (regenerate the worklists):
    MATCH (sn:StandardName {name_stage:'accepted'})
    WHERE sn.source_paths IS NOT NULL AND size(sn.source_paths)>0
    OPTIONAL MATCH (imas:IMASNode)-[:HAS_STANDARD_NAME]->(sn)
    WITH sn, collect(DISTINCT 'dd:'+imas.id) AS hsn
    OPTIONAL MATCH (src:StandardNameSource)-[:PRODUCED_NAME]->(sn)
    WHERE src.source_type='dd'
    WITH sn, hsn, collect(DISTINCT src.id) AS prod
    WITH sn, [p IN (hsn+prod) WHERE p IS NOT NULL] AS edgep
    MATCH (src2:StandardNameSource)-[:PRODUCED_NAME]->(sn)
    WHERE src2.source_type='dd' AND NOT ('dd:'+src2.source_id IN hsn)
    MATCH (n:IMASNode {id: src2.source_id})
    RETURN sn.id AS name, src2.source_id AS dd_path, n.node_category AS cat, n.unit AS dd_unit
    ORDER BY name

Done-when
  1. For each PRODUCED_NAME dd path lacking a HAS_STANDARD_NAME edge where node is SN-eligible AND
     units_agree(sn.unit, dd_unit, dd_path): create the (imas)-[:HAS_STANDARD_NAME]->(sn) edge via
     the sanctioned attach path. Unit-disagreeing / ineligible ones: record, don't attach.
  2. Re-run reconcile_standard_name_source_paths so scalars pick up the new edges (or let the next
     sn run post-drain do it).
  3. Optional: add a coverage gate that surfaces future SN-eligible unit-agreeing PRODUCED-but-
     unattached paths.
  4. Tests green; this followup resolved.
Dated correction (2026-07-29): the §4 'intentional unit-less' verdict is challenged by the lead and two of the three names it dispositioned are now GHOSTS — perturbed_particle_energy and particle_mass no longer exist in the graph (removed by an unledgered bulk delete; only normalized_particle_mass survives). The surviving effective_particle_energy (unit=None, base energy = inherently dimensional) is re-adjudicated under D-parent-units in imas-codex sn-graph-wide-integrity §6b: the dimensionless role belongs to the normalized_* child, so a parent named 'energy' reading as unit-less is not accepted as intentional. This comment supersedes the §4 disposition for those names; the shipped record is retained as history, not rewritten.