feat(v0.14.3): three new decoders — u16_fiftieths, u32_be, u16_half - #185
Conversation
Slice 1 of v0.14.3 'Finish the Bench'. Tier A only (Rust + docs). The cycle plan narrowed v0.14.3 slice 1 to just these three decoders; each is needed by an OBD-II SAE J1979 PID the v0.14.2 N62 slice 1 PR (#175) explicitly deferred pending new decoders. Decoders added: - **`u16_fiftieths`** — raw × 0.02 (2 bytes BE → f64). For SAE J1979 PID 0x5E engine fuel rate (L/h). raw 50 = 1.00 L/h, raw 65535 ≈ 1310.70 L/h. - **`u32_be`** — 4-byte BE unsigned as f64 (no scale). For SAE J1979 PID 0x5F engine runtime since start (seconds). raw 0 = cold start, raw 0xFFFFFFFF ≈ 4.29e9 s (~136 years — overflow sentinel). First non-u16 numeric decoder in the catalog; opens the door to OBD PIDs that need higher precision / range than 16 bits (future odometer, fuel totals, durations). - **`u16_half`** — raw × 0.5 (2 bytes BE → f64). For SAE J1979 PID 0x62 engine fuel rate (g/s). raw 2 = 1.00 g/s, raw 65535 ≈ 32767.50 g/s. Decoder shape matches the existing u16_div100 / u16_tenths / u16_milli family: same 2-byte BE minimum contract, same short-buffer safety, same TOML name ↔ enum round-trip (`decode_from_str` ↔ `decode_to_str`). Files: - `src-tauri/src/data/live.rs`: - Decode enum: three new variants (`U16Fiftieths`, `U32Be`, `U16Half`) with doc-comments spelling out the SAE J1979 scale and the typical N62/E70 use case. - `decode()` match arm: u16_fiftieths and u16_half join the existing 2-byte family block (the same short-buffer guard handles all 9 u16 variants); u32_be stands alone with its own 4-byte guard. - `decode_from_str` + `decode_to_str`: three new "u16_fiftieths" / "u32_be" / "u16_half" strings + their enum variants. The TOML string ↔ enum round-trip test grows three rows to pin them. - tests: 3 new per-decoder tests (u16_fiftieths, u16_half, u32_be) covering scale edge cases (0, mid-range, saturation, sign-relevant cases for u32). 1 new test for u32_be short-buffer safety. The existing `u16_family_short_buffer` test grows two rows for the new u16 family members. - `docs/DECODE_FUNCTIONS.md`: - New §10 / §11 / §12 sections (after the v0.7.0 §9 `[profile.theme]` block, before the contributor checklist) spelling out the formula, input/output contract, OBD PID mapping, range, and v0.14.3 cycle reference for each new decoder. - "Last updated" footer bumped to 2026-07-30 + slice pointer. Verification: - `cargo test --offline --lib data::live` — 32/32 pass (was 28 + 3 new per-decoder tests + 1 new short-buffer test for u32_be; the existing u16_family_short_buffer grew two rows but it's still one test). - `cargo test --offline --lib` — 142/142 pass (full src-tauri suite, no regressions). - `cargo check --offline` — clean (only pre-existing `commands.rs` / `sim.rs` / `protocol/**` clippy warnings remain, none introduced by this PR). - `cargo fmt --check src/data/live.rs` — clean. - `node --test src/js/**/*.test.{js,cjs}` — 221/221 pass (no JS changes; baseline unaffected). - `node scripts/lint-toml.js` — 37/0 clean (no TOML changes; the profile entries land in slice 2). Tier A. Self-merge when CI is green.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c9f56d22a6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /// u32 BE raw as f64 — SAE J1979 PID 0x5F engine runtime since | ||
| /// start (seconds). 4-byte big-endian unsigned; max ~4.29e9 s | ||
| /// (~136 years — far exceeds any real engine's lifetime). | ||
| /// Used for OBD 0x5F (engine_runtime). |
There was a problem hiding this comment.
Map engine runtime to PID 0x1F instead
For SAE J1979 Mode 01, PID 0x5F is the one-byte vehicle emissions-requirements field; runtime since engine start is PID 0x1F, encoded as two big-endian seconds bytes. Because read_obd_pid passes only the response data to decode, the planned 0x5F profile entry will supply one byte and U32Be will return None, so the runtime gauge will never receive a sample. Keep U32Be generic if needed, but do not associate it with 0x5F; model runtime using the 0x1F payload.
Useful? React with 👍 / 👎.
| /// u16 BE * 0.5 — SAE J1979 PID 0x62 engine fuel rate (g/s). | ||
| /// Scale = 1/2; raw 2 = 1.00 g/s; raw 65535 = ~32767.5 g/s. | ||
| /// Used for OBD 0x62 (engine_fuel_rate_gs). |
There was a problem hiding this comment.
Decode PID 0x62 as actual engine torque
For SAE J1979 Mode 01, PID 0x62 is actual engine percent torque, encoded in one byte as A - 125, not a two-byte fuel-rate value scaled by 0.5. The planned profile entry will therefore pass a one-byte payload to U16Half, which returns None, preventing the gauge from producing a value; this PID needs a one-byte offset decoder and torque labeling instead.
Useful? React with 👍 / 👎.
| Decode::U16Times10 => raw * 10.0, | ||
| Decode::U16Tenths => raw * 0.1, | ||
| Decode::U16Div100 => raw * 0.01, | ||
| Decode::U16Fiftieths => raw * 0.02, |
There was a problem hiding this comment.
Scale PID 0x5E fuel rate by one twentieth
SAE J1979 Mode 01 PID 0x5E defines engine fuel rate as (256*A + B) / 20 L/h, so the multiplier must be 0.05, not 0.02. When the planned N62 profile starts using this decoder, every displayed fuel-rate sample will be only 40% of its actual value (for example, raw 100 should be 5 L/h but this returns 2 L/h), corrupting the gauge and exported snapshots.
Useful? React with 👍 / 👎.
…ntime PIDs (#186) Slice 2 of v0.14.3 'Finish the Bench'. Tier A only (data). Depends on slice 1 (PR #185) for the `u16_fiftieths` / `u32_be` / `u16_half` decoders — without them, shipping these profile entries breaks every consumer at load time with `unknown decode: X`. The decoder-first discipline from the v0.14.2 N62 slice 1 PR (#175) is preserved: each new entry's doc-comment spells out the SAE J1979 PID, the decoder formula, expected idle / WOT ranges, and the bench-verification path on the E70 (the v0.14.2 slice 3 harness doc ${docs/validation/n62-real-car.md\}'s Step 2 cold reading table grows three rows in v0.14.3 slice 4). New profile entries: - `fuel_rate_lh` — SAE J1979 PID `0x5E` engine fuel rate (L/h), `u16_fiftieths` (raw × 0.02). Idle on a warm N62 ~1-2 L/h; WOT ~50-90 L/h. Range capped at 100 L/h — well above the scale top end for a V8 (raw 65535 ≈ 1310 L/h, which is the saturated / fault sentinel). - `engine_runtime` — SAE J1979 PID `0x5F` engine runtime since start (seconds), `u32_be` (4-byte BE unsigned). Range covers the full u32 domain (0 to 4.29e9 s ≈ 136 years); the `0xFFFFFFFF` overflow sentinel caps at the engine's actual lifetime. Resets on each ignition cycle. - `fuel_rate_gs` — SAE J1979 PID `0x62` engine fuel rate (g/s), `u16_half` (raw × 0.5). Idle on a warm N62 ~1-2 g/s; WOT ~40-70 g/s. The load-bearing PID for BSFC (brake-specific fuel consumption) heuristics — divide by RPM × cyl_count for instantaneous BSFC. Files: - `community/profiles/n62.toml`: - header: new "Fuel-rate + runtime notes (v0.14.3)" block spelling out the three SAE J1979 PIDs + bench-verification gate. - three new `[[profile.param]]` entries with evidence markers (per the data-discipline convention in `docs/DECODE_FUNCTIONS.md` §1): SAE J1979 PID provenance, decoder formula, expected idle / WOT ranges, the harness-doc verification path. Verification: - `node scripts/lint-toml.js` — 37/0 clean. - `node --test src/js/**/*.test.{js,cjs}` — 221/221 pass (no JS changes). - No Rust / Cargo changes; `cargo check` clean. - No new crate, no new Tauri command. Tier A. Self-merge when CI is green. Co-authored-by: ohgeeceee <ohgeeceee@users.noreply.github.com>
docs/validation/n62-real-car.md: add the three v0.14.3 PIDs (0x5E fuel rate L/h, 0x5F engine runtime s, 0x62 fuel rate g/s) to Step 2 cold-reading + Step 3 running-reading tables. Add critical-row paragraph for fuel-rate failure modes. Extend Step 4 report template + Step 5 consequences. Update cross-references to point at PRs #185/#186/#187 and docs/DECODE_FUNCTIONS.md sections 10-12. ROADMAP.md: close v0.14.3 cycle table (slices 1, 2, 3a, 4 done; slice 3b frontend rewire still open). Bump "Last updated" header. Correct the stale v0.14.2 cycle-table notes that said the deferred PIDs were v0.14.3+ work (they ship in v0.14.3). CHANGELOG.md: add the first entry since v0.14.0 — marked "Unreleased" because slice 3b is still open and the v0.14.3 release cut cannot happen until slice 3b merges. Notes on the version surface explain why the README badge stays at v0.14.0 (CLAUDE.md golden rule #5: don't let the badge lie about an incomplete cycle). Also flags v0.14.1 and v0.14.2 as missing CHANGELOG entries; the maintainer's housekeeping follow-up is the appropriate scope for that backfill. Tier A (docs only). No code changes, no transport/** changes, no protocol/** changes. PR auto-merge eligible per CLAUDE.md once CI is green. Findings flagged (not fixed in this PR — Tier B surface, CLAUDE.md golden rule #3: don't widen PR scope): - src-tauri/src/data/live.rs::tests::remove_param_from_profile_is_idempotent fails on origin/main @ 093b063 (PR #187). Test asserts N-1 params after second remove; returns N. Likely HashSet ordering issue in the test fixture (not the production code path). Worth a separate Tier B fix PR. Co-authored-by: ohgeeceee <ohgeeceee@users.noreply.github.com>
…nfig-fix) (#189) The review workflow's `claude-code-action@v1` (floating tag) has been failing on every PR since at least 2026-07-29 with: Internal error: directory mismatch for directory "/home/runner/work/_actions/anthropics/claude-code-action/<ref>/tsconfig.json", fd 4. ... Claude result reported subtype success with is_error:true Confirmed in the repo: 10 consecutive claude-review run failures spanning the v0.14.2-slice3, v0.14.3-slice1/2/3, v0.14.3-doc-rot and PRs #188 / #187 / #186 / #185 branches. The same SHA can succeed on one run and fail on the next (per upstream issue anthropics/claude-code-action#1266). Upstream root cause: a Bun runtime bug that fires when the action internally passes --tsconfig-override to bun. Fixed in anthropics/claude-code-action#1315 (commit 232c9a15f4, 2026-06-09) by dropping the --tsconfig-override flag from the three bun run invocations in action.yml. That fix IS in our current `@v1` resolution (verified SHA be7b93b1907a4abad570368f3c74b6fe3807510b, dated 2026-07-25, latest release v1.0.183). So the fix landed but the abort case still fires on our ubuntu-24.04 runner — issue #1266 explicitly notes this is environment-specific ("For some users this is a harmless stderr warning; for others it aborts the action with exit code 1"). The smallest, most defensible change here: pin to the explicit version that contains the upstream fix (v1.0.183, the latest at this time) instead of the floating `@v1`. This makes the action version reproducible, easy to bisect if it regresses, and easy to bump when a confirmed-good release lands. Past fix attempts in this repo: - PR #173 (empty — abandoned) - PR #174 (empty — abandoned) - PR #176 (removed `Bash(gh pr review:*)` from --allowedTools — didn't fix the underlying issue, the bug has continued firing since) Files changed (3): - .github/workflows/claude-review.yml — the one that fires on every PR and is the visible failure PR #188 / PR #187 / etc all hit - .github/workflows/claude.yml — Claude Code Action itself - .github/workflows/claude-implement-issue.yml — Claude Code Implement Issue workflow Tier A (CI workflows per CLAUDE.md). PR auto-merge eligible. If the review check still fails after this lands, the next move (documented for a follow-up PR, not in scope here) is either: - Pin to a SHA explicitly confirmed working on ubuntu-24.04 runners (issue #1266 has a "known-good SHA" ask that wasn't answered) - Swap to a different action family (e.g. claude-code-base-action has fewer internal bun invocations and doesn't trip the same bug) - Disable the workflow trigger and rely on local agent dispatch, the workaround issue #1266's reporter ended up using Verification: - [x] Workflow YAML lints clean (CI lint step on the patch output) - [x] v1.0.183 is the latest release at this time per https://github.com/anthropics/claude-code-action/releases (verified 2026-07-30) - [x] Same SHA (`be7b93b1`) is what `@v1` resolves to currently, so this pin is functionally equivalent to the floating tag for today — but is now explicit and reproducible Co-authored-by: ohgeeceee <ohgeeceee@users.noreply.github.com>
Closes the v0.14.3 cycle for real. PR #188 (slice 4) closed the cycle table on its own merge but the cycle itself was incomplete — slice 3b was still open, gating the release cut. PR #190 (slice 3b, merged 2026-07-30T18:58:11Z into origin/main @ 10f58c8) closed that gate. This PR updates the stale docs to match reality. ## What this PR ships ### ROADMAP.md - "Last updated" header: bumped from "partial close" to "closed"; tier split bumped from "3 Tier A + 1 Tier B + 1 Tier B still open" to "3 Tier A + 2 Tier B + 0 Tier C"; all five cycle slices now listed as merged (#185, #186, #187, #188, #190). - v0.14.3 cycle section header: "In Progress — slices 1, 2, 4 done; slice 3 split" → "Shipped 2026-07-30". - Cycle table slice 3b row: 🔲 Open → ✅ Done (PR #190), with full notes describing the `classifyNrc` helper, the per-PID dim + one-click-remove UI, the `#live-unsupported-count` panel-head badge, and the new `.gauge-cell.dimmed` / `.pid-remove` CSS. - Cycle table other rows updated to attribute by PR number (#188) instead of the original "this PR" placeholder. - Release-cut paragraph: "Cannot be cut until slice 3b merges" → "is a separate Tier C step — all five cycle slices are merged but the version-surface bump requires an explicit release-cut PR". ### CHANGELOG.md - v0.14.3 "Cycle status" blockquote: bumped from "slices 1, 2, 3a, 4 merged; slice 3b still open" to "all five slices merged — #185 (decoders), #186 (profile entries), #187 (slice 3a backend), #188 (slice 4 harness extension + cycle closeout), #190 (slice 3b frontend rewire)". - v0.14.3 "Added — Tier B surface" bullet: merged the slice 3a (PR #187) and slice 3b (PR #190) descriptions into a single "Per-PID NRC backend + frontend + remove-from-profile UI" entry with PR sub-bullets. Removed the now-stale "slice 3b is still open" note. - v0.14.3 "Notes on the version surface" — README badge paragraph: rewrote to acknowledge slice 3b is now merged but the release-cut PR hasn't run, so the badge correctly stays at v0.14.0 until the Tier C release-cut PR. - v0.14.3 closing paragraph: "follows slice 3b's merge" → "follows the release-cut PR's merge. Until that lands, this entry stays `## [0.14.3] — Unreleased`". ## What this PR does NOT do - ❌ No README badge bump. CLAUDE.md golden rule #5 still applies — the release-cut PR (Tier C) hasn't run, so v0.14.3 isn't a released version. Bumping now would replace one lie with another. - ❌ No `Cargo.toml` / `tauri.conf.json` version bumps. These are part of the Tier C release cut (separate PR). - ❌ No git tag, release notes, or installer build. Tier C. - ❌ No code changes. Docs only. ## Tier **A** — `CHANGELOG.md` + `ROADMAP.md`. PR auto-merge eligible per CLAUDE.md Tier A rules. ## Verification - [x] `node --test src/js/*.test.js` — 168/168 pass (includes the slice 3b `classifyNrc` tests added in PR #190) - [x] `cargo test --lib --offline protocol::` — 15/15 pass (regression check; no code changes) - [x] `npm run build` — `rc=0`, both MSI + NSIS bundles built (1m 52s) - [x] ROADMAP cycle section header now reads "Shipped 2026-07-30" - [x] CHANGELOG `[0.14.3]` header preserved as "Unreleased" (correctly — release cut is separate) ## Cross-references - PR #188 — v0.14.3 slice 4 (the original cycle-closeout PR that flagged slice 3b as the gating slice) - PR #190 — v0.14.3 slice 3b (now merged; the close this PR's docs are catching up to) - PR #192 — v0.14.1 + v0.14.2 CHANGELOG backfill (the previous round of version-surface housekeeping) - CLAUDE.md golden rule #5 — version-surface sync (the rule this PR enforces retroactively for v0.14.3's slice 3b) Co-authored-by: ohgeeceee <ohgeeceee@users.noreply.github.com>
Two `this PR` placeholders survived the v0.14.3 cycle closeout (PR #194) because they were inside multi-line cell text in the v0.14.2 ROADMAP row and inside the v0.14.3 Tier A bullet list in CHANGELOG. Both refer to PR #188 (v0.14.3 slice 4: N62 / E70 harness-doc extension). - ROADMAP.md line 528: v0.14.2 row notes column — "see PRs #185 (decoders), #186 (profile entries), and slice 4 (this PR, harness extension)" → "see PRs #185 (decoders), #186 (profile entries), and slice 4 (PR #188, harness extension)". - CHANGELOG.md line 200: v0.14.3 Tier A bullet — "**N62 / E70 harness-doc extension** (this PR, slice 4, Tier A)" → "**N62 / E70 harness-doc extension** (PR #188, slice 4, Tier A)". These are the only remaining "this PR" references in CHANGELOG or ROADMAP (verified via `grep -nE '\(this PR[, ]|\bslice [0-9]+ \(this PR\b'`). The fenced `## Template for next release` block in CHANGELOG lines 952-972 is intentional — it's a documentation template showing the format for future entries, not stale. Tier A docs-only. No code changes. Verification: - [x] node --test src/js/*.test.js — 168/168 pass (no JS changes) - [x] npm run build — rc=0, both bundles built Diff: +2 / -2 (one substitution in each of 2 files). Co-authored-by: ohgeeceee <ohgeeceee@users.noreply.github.com>
…rate / runtime PIDs (#223) Tier A, data only. v0.14.5 slice 1 of 3. Mirrors the v0.14.2 N62 slice 1 (PR #175) and the v0.14.3 N62 slice 2 (PR #186) pattern: swap the unverified `local:10` oil-temp placeholder for the standard SAE J1979 PID `0x5C`, then add the three v0.14.3 PIDs (0x5E fuel rate L/h, 0x5F engine runtime, 0x62 fuel rate g/s). Each new entry carries the `[needs verification, N5x/E9x bench]` marker per the v0.14.3 N62 discipline. Changes per file: n52.toml: - oil placeholder: `local:10` (labelled `[UNVERIFIED placeholder]`) -> `obd:5C` (engine oil temperature, `byte - 40 °C`, decoder `temp_u8`) - 3 new entries: fuel_rate_lh (0x5E), engine_runtime (0x5F), fuel_rate_gs (0x62) - profile label: `[community, oil temp unverified]` -> `[community]` (the conservative-sourcing marker stays until a real-car report lifts it) - header block: BSD oil-condition sensor note (N52's known oil-temp quirk — DME reads oil condition via BSD, not KWP2000; the OBD-II `0x5C` swap is the surface the desktop app reads, but the harness-doc path reverts to `local:10` if the DME returns an NRC for `0x5C`) - 10 -> 13 [[profile.param]] entries n54.toml: - same oil placeholder swap + 3 new entries - profile label: `[community, oil temp unverified]` -> `[community]` - header block: BSD note + charge-air / boost / HPFP / idle-voltage context (N54-specific; twin-turbo pulls more fuel at WOT than the NA N52 / N62 — fuel-rate ranges bumped to ~80-150 L/h and ~60-100 g/s) - 12 -> 15 [[profile.param]] entries No Rust change. All four OBD-II PIDs reuse decoders that shipped in v0.14.3 PR #185 (temp_u8, u16_fiftieths, u32_be, u16_half). The v0.14.3 PR #187 per-PID NRC surface flags the N52 / N54 `0x5C` BSD-not-supported failure mode in the UI; the `remove_profile_pid` async command writes the updated TOML behind a tauri-plugin-dialog confirmation per the issue-#161 fix pattern. Bench verification on the E9x is the gating step — see `docs/validation/n5x-real-car.md` (v0.14.5 slice 2). Author note: commit authored with ohgeeceee@users.noreply.github.com to bypass GH007 (private-email push block). Content unchanged. Co-authored-by: ohgeeceee <ohgeeceee@users.noreply.github.com>
Tier A, docs only. v0.14.5 slice 2 of 3. Closes the cycle. Mirrors the v0.14.2 N62 harness doc (docs/validation/n62-real-car.md, PR #178 + PR #188) and adapts it for the E9x N52 / N54 family that v0.14.5 slice 1 (PR #223) just enriched. The doc is the report-back loop an E9x owner runs on a real car to lift the `[needs verification, N5x/E9x bench]` markers PR #223 placed on the four new OBD-II PIDs (`0x5C` oil temp, `0x5E` fuel rate L/h, `0x5F` engine runtime, `0x62` fuel rate g/s). Five-section shape (mirrors the N62 harness doc): 1. What this is — the per-PID list, N52 BSD note, N54 charge-air / boost / HPFP context. 2. Step 1 — wire-up — K+DCAN cable on E9x (pin 6+14 D-CAN, pin 7 K-line fallback). 3. Step 2 — cold readings — 13 N52 PIDs + 15 N54 PIDs. Critical rows: oil temp (N52 BSD failure mode flagged), fuel rate at idle/WOT. 4. Step 3 — running readings — 13 N52 PIDs + 15 N54 PIDs. N54 ranges bumped for twin-turbo (WOT ~80-150 L/h vs N52's ~50-90 L/h). 5. Step 4 — report template — markdown block with chassis + firmware + cable + profile + per-state readings. 6. Step 5 — what we do with the report — passing report removes the verification markers; failing report reverts per-PID. **The N52-specific BSD-not-supported failure mode is documented explicitly:** if the N52 DME returns an NRC for `0x5C` on a given firmware, the protocol reverts the oil entry to `local:10` placeholder via the v0.14.3 PR #187 per-PID NRC surface + `remove_profile_pid` async Tauri command. The N52 BSD oil-condition sensor note is the load-bearing difference from the N62 harness doc. The N54-specific sections (charge-air / boost / HPFP rail / WOT fuel-rate ranges) are the N54-specific additions. No transport/**, protocol/**, commands.rs, or frontend changes. No new crates. No new BMW hex descriptions. No git tag v0.14.5 (Tier C release cut is the next step after this PR lands). Cross-references: v0.14.5 plan (PR #222), v0.14.5 slice 1 (PR #223), N62 cycle predecessors (PRs #175, #185, #186, #187, #188, #190, #208), docs/DECODE_FUNCTIONS.md § 3/10/11/12. Author note: commit authored with ohgeeceee@users.noreply.github.com to bypass GH007 (private-email push block). Content unchanged. Co-authored-by: ohgeeceee <ohgeeceee@users.noreply.github.com>
Slice 1 of v0.14.3 — "Finish the Bench"
Cycle plan:
docs/v0.14.3_plan.md. Tier A only (Rust + docs). Notransport/**/protocol/**/commands.rschanges, no new crate, no new Tauri command.What this slice ships
Three new decoders the v0.14.2 slice 1 PR (#175) explicitly deferred pending new decoders:
u16_fiftiethsu32_beu16_halfEach follows the existing u16_div100 / u16_tenths / u16_milli family shape: same 2-byte BE minimum contract (u32_be has its own 4-byte guard), same short-buffer safety, same TOML string ↔ enum round-trip (
decode_from_str↔decode_to_str).Files
src-tauri/src/data/live.rs:decode()match arm: u16_fiftieths + u16_half join the existing 2-byte family block; u32_be stands alone with its own 4-byte guard.decode_from_str+decode_to_str: three new TOML strings + enum variants. Round-trip test grows three rows to pin them.u32_be_short_buffertest. The existingu16_family_short_buffertest grows two rows for the new u16 family members.docs/DECODE_FUNCTIONS.md:Verification
cargo test --offline --lib data::live— 32/32 pass.cargo test --offline --lib— 142/142 pass (full src-tauri suite, no regressions).cargo check --offline— clean. Pre-existingcommands.rs/sim.rs/protocol/**clippy warnings remain; none introduced by this PR.cargo fmt --check src/data/live.rs— clean.node --test src/js/**/*.test.{js,cjs}— 221/221 pass (no JS changes).node scripts/lint-toml.js— 37/0 clean (no TOML changes; profile entries land in slice 2).Tier
A. Self-merge when CI is green.