Skip to content

drift/01: the twin-parity ledger lint - #23

Merged
bhelm merged 2 commits into
drift/metafrom
drift/01-twin-ledger
Aug 18, 2026
Merged

bhelm merged 2 commits into
drift/metafrom
drift/01-twin-ledger

Conversation

@bhelm

@bhelm bhelm commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Plan step 1 of #17: the twin-parity ledger — a stdlib-Python lint in the doc_comment_lint.py idiom (local command first, baseline file, exit 0/1, CI as fork convenience) that puts a machine behind the parity discipline docs/CROSS_PLATFORM.md promises.

Five machine-checked rules

  1. Declared twin references must resolve on the other side (scanned repo-wide incl. app targets; qualified owners verified, //-comment annotations included). Catches the sdnnIndex class on introduction day.
  2. Test-only wiring is an error: a declaration with test callsites but no production callsite (owner/arity-aware counting). Catches the [P1] N-08: Production resting-HR path has no artifact gate — a single-sample bin can set daily RHR #10 class that a direct output comparison structurally cannot see.
  3. Constant pairs (camelCase ↔ SNAKE_CASE, owner-disambiguated) must agree value-for-value, with full constant-expression evaluation — 48 * 3_600 compares as 172800, not 48; unparseable initializers surface as findings, never silently skipped.
  4. Duplicate/callsite counters ratchet against the baseline (dayString 12, resting-HR paths 2, Android-UI Pearson 4 at bootstrap).
  5. One-sided files/functions/computed properties/constants need a twin-map entry; unmapped is red on both sides, deliberate non-twins are explicit map entries.

Inventory spans the five mirrored package pairs (StrandAnalytics, StrandImport, WhoopStore, WhoopProtocol, OuraProtocol ↔ analytics, ingest, data, protocol, oura); deliberate exclusions live in the map's scope block. Kotlin parsing handles extension receivers with generics, trailing commas, and getter properties. Baseline identities survive line shifts but are value-fingerprinted — a known constant drift that drifts again is a new finding.

What it already found

Beyond mechanizing the known items: two previously unreported constant drifts — SleepStagerV2 padLo/padHi are 330/390 in Swift vs 360/420 in Kotlin (evidence for the fund table in #17; fix PR to follow separately).

Verification

  • Current tree: exit 0 against the 263-finding baseline in ~14 s; byte-identical output across runs.
  • Without baseline: 267 findings, including every must-find (sdnnIndex dead reference, test-only resting-HR on both platforms, 4 Pearson copies, padLo/padHi).
  • 24 unittest probes: every rule fires on synthetic fixtures, plus baseline semantics (known suppressed / new red / disappeared tolerated / counter regression red / value change red) and the parser edge forms above.
  • doc_comment_lint.py untouched and green.

Review

Two independent Codex reviews: the first (verdict red) found real parser and enforcement gaps — CI path filters missing the scanned sources, inventory scope excluding WhoopProtocol, first-literal-only constant parsing, trailing-comma arity, extension receivers, computed properties, name-only callsite counting, comment-form dead-twin scanning, baseline identity without value fingerprint, missing baseline-semantics tests. All eleven were fixed; the focused re-review of those fixes returned green with zero findings.

Part of the drift-control plan in #17.

🤖 Generated with Claude Code

bhelm and others added 2 commits August 18, 2026 03:14
Tools/parity_ledger.py, stdlib-only, in the doc_comment_lint idiom
(local command, baseline file, exit 0/1). Five machine-checked rules
over the mirrored Swift/Kotlin trees: declared twin references must
resolve on the other side (scanned repo-wide, including app targets);
a declaration with test callsites but no production callsite is an
error; camelCase<->SNAKE_CASE constant pairs must agree value-for-value
(full constant-expression evaluation — 48 * 3_600 is compared as
172800, not 48; unparseable initializers surface as findings, never
silently skipped); duplicate/callsite counters ratchet against the
baseline; one-sided files, functions, computed properties and constants
need a twin-map entry.

Inventory spans the five mirrored package pairs (StrandAnalytics,
StrandImport, WhoopStore, WhoopProtocol, OuraProtocol <-> analytics,
ingest, data, protocol, oura); deliberate exclusions are recorded in
the map's scope block. Kotlin parsing handles extension receivers with
generics, trailing commas, and getter properties; ambiguous constant
names are disambiguated via mapped owners or reported, never dropped.
Baseline identities are line-shift-stable but value-fingerprinted: a
known constant drift that drifts AGAIN counts as a new finding.

The checked-in twin map is bootstrapped from the declared twin
annotations in the tree (357 references, 340 resolving); same-name
matches are recorded as suggestions only. The 263-finding baseline
captures today's debt, including the sdnnIndex dead twin reference, the
test-only hardened resting-HR path on both platforms (#10), 4 duplicate
Android-UI Pearson implementations, 12 dayString implementations — and
two previously unknown constant drifts (SleepStagerV2 padLo/padHi:
Swift 330/390 vs Kotlin 360/420).

Current tree: exit 0 against the baseline in ~14 s, byte-identical
across runs; 24 unittest probes cover every rule plus baseline
semantics (known finding suppressed, new finding red, disappeared
finding tolerated, counter regression red, value change red).

Part of the drift-control plan in #17 (step 1).

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Path filters cover the ledger's own files AND every scanned source
tree, so a PR touching only Swift/Kotlin twin sources runs the ledger.
Discovery-based unittest run picks up new test files without editing
the workflow.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant