Skip to content

Add fail-closed cross-platform parity governance - #1534

Merged
ryanbr merged 19 commits into
ryanbr:mainfrom
bhelm:codex/harness-governance-pr1
Sep 11, 2026
Merged

ryanbr merged 19 commits into
ryanbr:mainfrom
bhelm:codex/harness-governance-pr1

Conversation

@bhelm

@bhelm bhelm commented Aug 21, 2026 •

Copy link
Copy Markdown

Why

Swift/Kotlin parity debt was tracked informally, so regenerating a snapshot could hide a newly one-sided implementation. This PR adds a product-free, fail-closed static governance foundation for cross-platform review.

What this PR contains

  • deterministic stdlib scanner, compact reviewed authority/baseline, and a single manually reviewed typed disposition registry
  • monotonic policy: proven improvements and obsolete dispositions warn only; new/reintroduced debt and removed/retargeted twins block
  • guarded refresh: regeneration computes state but cannot approve new debt or create dispositions
  • exact-base proof pinned to an immutable commit; missing or shallow bases fail closed
  • operation-local immutable source snapshots and nested Tools/tests/** workflow coverage

Scope is 17 files (+5,772/-16): governance/workflow files plus two doc-comment twin claims that stopped resolving on current main (SleepStager+Trace.swift, WhoopBleClient.kt); no product logic. Current authority: 141 function pairs, 270 baselined findings, 768/768 references resolved, and 657 constant pairs.

Verification

  • 103/103 parity-governance tests (own path-filtered workflow; the core Tools suite is unchanged)
  • ledger green: no new findings
  • ratchet against current origin/main: errors=0
  • clean diff and guarded regeneration on the rebased tree
  • Rebased onto main at 7055def; authority regenerated through the guarded refresh.
  • Known baselined divergence recorded for the maintainer's decision: SleepStagerV2 padLo/padHi (Swift 330/390, Kotlin 360/420).

Deliberate boundary

This is the foundation, executable demo, and self-test layer. Even if merged, it does not enforce parity on product-source changes. PR2 adds the portable case layer, PR3 adds corpora and native runners, and only PR4 enables product-source CI enforcement after the intermediate layers have demonstrated reliable behavior.

@ryanbr

ryanbr commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Reviewed as a draft — no merge decision, and I've respected the stacking (#1531 first). I ran the tool rather than reading it, including trying to defeat it.

It works, and I checked that adversarially

Stale twin reference. I renamed evaluateWithTrainingLoad on the Swift side so the Kotlin comment declaring it pointed at nothing:

android/app/src/main/java/com/noop/analytics/ReadinessTrainingLoad.kt:9:
  dead-twin-reference: Swift target ReadinessEngine.evaluateWithTrainingLoad does not resolve

File, line, and target. That is the message a contributor can act on.

New one-sided declaration, with the authority regenerated to hide it. This is the claim I most wanted to test, since it's the one that decides whether the whole thing is worth having. I added a Kotlin-only function, hit the ledger failure, then ran the remedy your own doc gives — --bootstrap-map --write-baseline — and the ledger went green with the debt still sitting there.

The ratchet was not fooled:

ERROR: Tools/parity_twin_map.json: derived inventory changed without an exact issue-bound
  authority change: add-unpaired-function kotlin
  android/app/src/main/java/com/noop/analytics/SkinTempDisplay.kt::aFreshlyInventedOneSidedHelper/1#1

Exact kind, language, file, symbol — after both checked files were regenerated. "Regenerating both checked files cannot hide new debt" is a real property, not a claim. That is the part worth building on.

What I'd want before it leaves draft

1. Nothing runs the ratchet. No workflow is touched, and neither script is referenced anywhere in .github/workflows/. The doc says "CI should omit --offline", which implies a caller that doesn't exist yet. As it stands, merging this adds a gate that is never invoked — the tests prove the tool works, but nothing applies it to the repo. If that's deliberate phasing, worth saying so explicitly in the description, because "fail-closed governance" reads as enforcement.

2. The CI floor isn't raised, and this is your own failure mode. tools-python.yml asserts >= 45 tests, with a comment saying "Raise the floor when suites are added; do not lower it to make a red run pass." The Tools suite goes 88 → 137 with this branch (the 137 in your description is the total discovered, not the 49 you add — worth stating precisely since it's offered as evidence). Leave the floor at 45 and someone can later delete Tools/tests/__init__.py, lose all 49 governance tests to discovery, and CI stays green at 88. That is exactly the false-green class this PR exists to remove, in the PR's own wiring. The tests do get discovered automatically today — I confirmed that — so this is the one line needed to protect them.

3. The ledger's diagnostic for new debt is undiagnosable. The stale-twin message is excellent; this one is not:

FAIL compact baseline drift in finding twin-map-authority-drift|functions
  has no reviewed compact-baseline disposition

No file, no symbol, no kind. --no-baseline doesn't name it either — I checked, expecting that to be the escape hatch. So a contributor who adds a Kotlin helper gets a failure they cannot act on, and the only remedy the doc offers is the regenerate command, which locally absorbs the debt and moves the failure to a ratchet they may not run. The ratchet already computes the exact string needed (add-unpaired-function kotlin <file>::<symbol>); surfacing that from the ledger too would close the gap.

Cost, stated neutrally

2,373-line scanner, 567-line ratchet, 1,531 lines of tests, and the Tools suite goes from 3s to 55s. Stdlib-only with no new dependency, which is the right call for this repo. Whether that maintenance surface is worth the drift protection is the maintainer's call, not mine — I'm recording the number rather than arguing it either way.

Two things I checked and found clean: the tool touches no product code paths, and the JSON authority really is 65 lines rather than a generated inventory dump.

@bhelm

bhelm commented Aug 22, 2026

Copy link
Copy Markdown
Author

Thanks for the adversarial review — all three pre-ready items are now addressed in the draft:

  1. Phased enforcement is explicit. “Fail-closed” now describes the tools when invoked. PR1 does not invoke the ledger or ratchet from a workflow; repository enforcement remains planned for the final stack PR with native execution and path-filtered wiring.
  2. Discovery is protected. The existing generic Tools Python test floor is raised from 45 to 138, its comment names parity governance as the third discovered suite, and the PR evidence now says 138 total Tools tests rather than implying they are all newly added governance tests.
  3. New debt is actionable locally. A branch/worktree-added one-sided helper now reports the exact rule, language, file, symbol, and arity before the ratchet. The regression was captured RED with only the opaque compact-authority drift, then GREEN with add-unpaired-function kotlin …::aFreshlyInventedOneSidedHelper/1#1.

Verification on the amended candidate: 138/138 Tools tests, ledger green, offline ratchet errors=0, and independent delta review P0=0/P1=0.

This remains a Draft stacked on #1531. After that dependency merges, it still needs the planned rebase, authority regeneration, exact discovery-floor refresh against the new base, and final review before it can be considered ready.

@bhelm
bhelm force-pushed the codex/harness-governance-pr1 branch from d580b22 to a4c68ee Compare August 22, 2026 11:11
@bhelm

bhelm commented Aug 22, 2026

Copy link
Copy Markdown
Author

Follow-up on the failed refresh CI: GitHub tested the stacked merge ref after main advanced, so the checked authority still described the old source tree. The six failures were stale authority/baseline evidence, not native/product failures.

The draft is now rebuilt from current main (49c97ca4, including #1535), plus the refreshed comment-only #1531 dependency, then PR1. Authority and baseline were regenerated from that exact tree. Because #1535 landed the Android sdnnIndex twin, the former bhelm#18 bootstrap exemption became stale and was removed; its one-off bootstrap path was removed as well. No new debt was waived, and the discovery guard was strengthened rather than weakened: its floor is now 167, matching full discovery on this stack.

Final local evidence: 167/167 Tools tests; 219 baselined findings; 424/424 declared references; 73 function pairs and 590 constant pairs; offline and online ratchets errors=0; byte-identical regeneration; scope/diff checks green; independent delta review P0=0/P1=0.

This remains a Draft. It still depends on #1531 and will be rebased/regenerated once that PR merges; parity-gate CI invocation remains deliberately reserved for the final stack PR.

@ryanbr

ryanbr commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Re-reviewed at a4c68eed. All three items are addressed — I verified each rather than taking the summary — and re-running my adversarial tests turned up one new thing that matters more than any of them.

The three, confirmed

  1. Phased enforcement is now stated rather than implied. Still no workflow invokes the ledger or ratchet, but that is now a declared property of PR1 instead of something a reader has to discover, which was the actual complaint.
  2. Discovery floor raised to 167, and I checked the number rather than the diff: python3 -m unittest discover -p "test_*.py" from Tools/ collects exactly 167. The floor now sits at the real count, so losing a suite fails loudly.
  3. New debt is diagnosable. The old opaque line is replaced by an Actionable source drift: block naming rule, language, file, line and symbol. That was the fix I most wanted.

Also verified on the pristine tree: 219 baselined, 424/424 declared references resolved, ledger exit 0 — matching your numbers, on a tree rebuilt over #1535.

New: the governance suite is flaky here

tests.test_parity_ledger (58 tests), same command, same unchanged tree, back to back:

isolated run 1: OK
isolated run 2: FAILED (failures=1)
isolated run 3: FAILED (failures=2)

Across five more runs the failing set rotates rather than repeating — drawn from about five tests:

run 1: test_attached_reference_retarget_to_existing_f, test_retargeted_claims_cannot_hide_behind_stal
run 2: test_disappeared_baseline_finding_requires_rev
run 3: test_changed_value_of_baselined_mismatch_is_a_, test_retargeted_claims_cannot_hide_behind_stal
run 4: test_attached_reference_retarget_to_existing_f, test_changed_value_of_baselined_mismatch_is_a_
run 5: test_attached_reference_retarget_to_existing_f, test_retargeted_claims_cannot_hide_behind_stal

Full-discovery runs showed it too (1 failure, then 3). What I ruled out: not hash randomisation — PYTHONHASHSEED=0 repeated still gave 1, 1, then 2 failures — and not filesystem scan order, since _paths already returns sorted(found). I suspected the @lru_cache on _read_cached keyed by (path, mtime_ns, size) going stale when a test rewrites a temp file of identical size inside one mtime tick, but disabling it did not settle the suite, so I am naming that as an unconfirmed suspect rather than a diagnosis.

Your CI is 15/15 green and you report 167/167 locally, so this is environment-sensitive — I am on WSL2, and coarse mtime granularity there is a plausible ingredient. I am not claiming your evidence is wrong; I am reporting that the suite does not reproduce green on a second machine.

It matters more than a normal flake because of what this tool is for. A gate whose own tests fail one run in two teaches exactly the habit the PR exists to break: re-run until green. Worth pinning before the enforcement layer lands, since after that a flaky suite blocks merges at random.

The tool itself is deterministic — three runs of the ledger over the same modified tree produced 1104 findings each time with an identical first entry. So this is the tests, not the scanner, which is the better of the two.

Signal-to-noise on the thing being reported

Re-running my one-sided-function test: the format is fixed, but adding one Kotlin helper produces 1,104 actionable ledger lines, with the one I caused at position 590. The first line names ActivityEstimate.swift, a file the contributor never touched. The ratchet still names the exact new symbol, but now emits three errors where it emitted one, and the first two are the unrelated file and a generic authority mismatch.

So the message is actionable and the finding is present; what is missing is filtering the new debt from the tree's existing one-sided inventory. Given the ratchet is the real gate and it does still name the right symbol, I would treat this as polish rather than a blocker — but a contributor's first encounter with this tool will be that wall of output.

Nothing here changes my read that the ratchet's core property is real and worth having.

@bhelm

bhelm commented Aug 22, 2026

Copy link
Copy Markdown
Author

Im waiting on #1531 before addressing the new findings, so i can un-draft this PR.

@bhelm
bhelm force-pushed the codex/harness-governance-pr1 branch from a4c68ee to bbf9dd3 Compare August 22, 2026 16:28
@bhelm

bhelm commented Aug 22, 2026

Copy link
Copy Markdown
Author

Confirmed and fixed on the refreshed draft. The WSL/coarse-mtime suspicion was correct, but the stale identity existed at four layers: _read_cached, _parse_functions_cached, _parse_properties_cached, and _parse_constants_cached, all keyed by path + size + mtime. Disabling only the read cache therefore left stale parsed declarations/constants behind.

The regression is deterministic now: tests rewrite the same resolved path with the same byte size and restore the identical mtime_ns, then require a fresh function/twin-retarget result and a fresh constant-value result. A third regression runs the complete top-level scan and proves every scanned fixture file is read exactly once; it failed on the previous head because _call_sites reread both files.

The fix removes all persistent file-metadata caches. One top-level scan owns an operation-local immutable source snapshot, including masks and parse results, and releases it afterward; direct parse APIs still read fresh on every call. No ctime substitute was introduced.

Evidence on frozen head bbf9dd39: the focused regressions passed 100 consecutive runs; full Tools discovery passed 170/170; ledger, offline ratchet, online ratchet, deterministic bootstrap, py_compile, and diff checks passed; independent frozen-delta review reports P0=0/P1=0. A normal ledger run measured 22.71s / 192 MB RSS versus 23.35s / 197 MB on the parent. Full discovery is slower without cross-test persistent caches (150s on the final loaded-host run); I am recording that as a P2 CI-time trade-off rather than hiding it or restoring stale global state.

The draft still deliberately does not invoke the parity gate from CI; that remains final-stack scope.

@ryanbr

ryanbr commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Reviewed, @bhelm. #1531 is merged (5c899ea3), so the dependency is cleared and this is unblocked for the rebase you described.

Since this is stdlib Python, I could actually run it rather than read it — which is rare for this repo, and worth doing given the flakiness reported earlier.

The tooling works

Run against the branch's own tree:

  • 170 tests discovered, 169 pass
  • 216 Swift + 189 Kotlin files scanned; 424/424 declared twin references resolved
  • 590 constant pairs, 219 baselined findings

Pointed at today's main with the branch's authority, it resolves 432/432 and correctly fails closed on genuinely-undisposed debt (add-unpaired-function … ActivityEstimate.estimate/4 has no reviewed compact-baseline disposition) rather than silently accepting it. That is the fail-closed claim behaving as advertised against a base it has never seen, which is the strongest evidence in the PR's favour.

The earlier flakiness looks fixed

I re-tested it directly. The one failure is deterministic — 5/5 runs, byte-identical output, not the rotating failure reported before. Whatever the per-scan snapshot change did, it held.

One real blocker: the frozen authority is already stale against its own tip

test_checked_in_inventory_and_baseline_match_current_sources fails on bbf9dd39 with a clean tree:

AssertionError: [] != ['test-only-callsite|Packages/StrandAnalytics']

The baseline records 33 test-only callsites in Packages/StrandAnalytics; the scan finds 32. The debt went down by one and the exact-match authority no longer describes the tree it ships with. Regeneration fixes it — flagging it because the PR body reports this suite as green, so it likely drifted after that verification.

A design question worth answering in PARITY_GOVERNANCE.md

The ratchet fails on a debt decrease as well as an increase — 33 → 32 is what broke it. That is defensible for an exact authority, but it means a contributor who removes parity debt gets a red build and no obvious hint that regeneration is the fix. Worth one sentence in the doc saying so, or the first person to improve something will assume they broke the gate.

The rebase is larger than a refresh

The branch is 104 files and −3,670 lines behind main. Merging as-is would revert work landed today:

Would be lost From
disableLiveHR() teardown #1550
setHcVo2MaxAsked permission prompt #1554
ReportCompletenessContractTest rename #1564
banisterEffort, logMapDenominator #1561/#1562/#1563
RescoreBackgroundScheduler #1557

Two of the ten comment files also still differ from main — OuraLiveSource.kt and DataSourcesScreen.kt — because both gained unrelated changes since. Everything else in that set now matches exactly.

One useful confirmation for the rebase: your discovery floor of 170 still holds. Today's main has 103 Tools tests (I added 15 in #1555), plus your 82 parity tests = 185.

Runtime

The Tools suite took ~390–400 s here for 170 tests, against the ~150 s in your notes. Machine-dependent, but it roughly triples that job. You already flagged it as a transparent P2 trade-off for dropping the persistent cache, and I think that is the right call — I would rather a slow gate than a stale one — but the maintainer should know the CI-minute cost before it is wired up.

Summary

Design and execution look sound, and the fail-closed behaviour is real. The blockers are both staleness, not correctness: rebase onto current main, regenerate the authority, and re-run. Happy to re-review once it leaves draft.

@ryanbr
ryanbr marked this pull request as ready for review August 23, 2026 09:58
@ryanbr

ryanbr commented Aug 23, 2026

Copy link
Copy Markdown
Owner

@bhelm — draft flag is off and the checks read green, but that green is stale, so please rebase and regenerate before this lands.

Why the tick is not evidence

The check runs on this head completed 2026-08-22T16:34Z. GitHub validates the merge result, so they reflect this branch against yesterday's main — before roughly a dozen PRs merged today, including #1550, #1554, #1557, #1561, #1562, #1563 and #1564.

What the real merge does

I merged your branch onto today's main locally. It is conflict-free, and your harness then fails exactly as designed:

add-unpaired-function|swift|
  Packages/OuraProtocol/Sources/OuraProtocol/ActivityEstimate.swift::estimate/4#1
  has no reviewed compact-baseline disposition

That is the tool doing its job — declarations landed today that the frozen authority has never seen, refused rather than silently absorbed.

The consequence is what makes it blocking: tools-python.yml triggers on push to main for Tools/*.py, and this merge touches those paths. It would go red on main the moment it lands, and stay red for every later PR that touches Tools.

Separately, and independent of the rebase: on your current tip the same acceptance test already fails, deterministically (5/5 runs, identical output) — the baseline records 33 test-only-callsite findings for Packages/StrandAnalytics and the scan finds 32. The debt went down by one after your verification run.

The fix converges, and it is one command

I ran your own bootstrap on the merge result:

python3 Tools/parity_ledger.py --bootstrap-map --write-baseline

→ WROTE parity_twin_map.json (74 derived function pairs), WROTE parity_ledger_baseline.json (217 known findings), then the acceptance test returns OK. The committed-authority delta is 7 lines across the two JSON files.

I deliberately did not push that. Regenerating the frozen authority is a judgement call that belongs to you — for a tool whose entire claim is that the authority is reviewed rather than auto-accepted, having the maintainer's reviewer regenerate it on your behalf would undercut the point.

What still stands from the earlier review

Everything positive, and it is worth repeating because none of the above is a design criticism:

  • 170 tests discovered, 169 pass; 216 Swift + 189 Kotlin files scanned
  • 424/424 declared references resolved on your tree, and 432/432 when pointed at today's main — it copes with a base it has never seen
  • The flakiness is fixed: the failure is deterministic and byte-identical across runs, not the rotating one reported before
  • Pointing it at an unseen base and watching it refuse undisposed debt is the most convincing evidence in the PR

One small thing you can stop worrying about: your discovery floor of 170 still holds after rebase. Today's main has 103 Tools tests (#1555 added 15), plus your 82 = 185.

And one doc request from the earlier review, still open: the ratchet fails on a debt decrease as well as an increase — 33 → 32 is what broke your tip. Worth a sentence in PARITY_GOVERNANCE.md, or the first contributor who removes parity debt will read the red build as their own breakage.

Rebase, regenerate, push, and I will re-review promptly.

@bhelm
bhelm force-pushed the codex/harness-governance-pr1 branch from bbf9dd3 to 256af7f Compare August 23, 2026 13:10
@bhelm

bhelm commented Aug 23, 2026

Copy link
Copy Markdown
Author

Rebased conflict-free onto current main (b858bc97); #1531 is absorbed and only 10 governance/workflow files remain.
Decision implemented: proven debt reductions and obsolete exemptions warn only; new/reintroduced debt and removed/retargeted twins still block, including lower-count replacements.
Direct-ledger bases are pinned once to an immutable SHA and missing/shallow refs fail closed; nested Tools/tests/** changes now trigger the workflow.
Authority: 75 function pairs, 220 findings, 437/437 refs, 591 constants; floors: 103 core + 90 governance.
Verified both suites, ledger, online/offline ratchets, deterministic regeneration, and identical head/merge trees.

@ryanbr

ryanbr commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Ran this three ways to see how it behaves against a moving main. Short version: the harness is sound and your branch is green on its own — what it needs is a regeneration at merge time, plus a decision about which side of the fail-closed line additions belong on.

Measurements (cd Tools && python3 -m unittest discover -s .)

tree result
your head 256af7fb3 alone 193 tests, OK
merged with main @ cecc78f4b 193 tests, 6 failures — all RepositoryBaselineTests
same merge, after regeneration 193 tests, OK

Your base b858bc973 is 21 commits behind main, and GitHub still reports this PR CLEAN because it does not re-run checks when the base advances. The green tick is stale rather than wrong. Since tools-python.yml runs this suite, merging as-is turns main red — and being fail-closed, it would then block everyone else until someone regenerated.

The regeneration is small, and that is the interesting part. python3 Tools/parity_ledger.py --bootstrap-map --write-baseline on the merged tree gives an 8-line diff:

function_pairs       75   ->   77
file_pairs           34   ->   35
unpaired_files      347   ->  346
unpaired_functions 3530   -> 3544
constants          1678   -> 1679

plus one accepted finding — test-only-callsite | Packages/StrandAnalytics — moving 35 → 34.

Read as behaviour rather than churn: those 21 commits added two twin pairs, paired one more file, and took parity debt down by one. The harness tracked all of it correctly, and the diff is legible enough to review by eye. That is a better result than I expected going in.

Two things before this can land:

1. Regenerate on top of current main and push, so the merge is green at the moment it happens. Anything that merges in between will need it again — which leads to the second point.

2. The merge protocol, which is the real design question. Every merge that adds a function invalidates the baseline, so a fail-closed gate risks spending most of its life blocking work that is entirely correct. The asymmetry worth weighing: a removed or retargeted twin is a genuine parity regression and should fail the build; a newly added one-sided function is usually just new code whose twin has not been written yet, and failing on that makes every unrelated PR pay for someone else's addition. If additions warned — or updated the counters in place — while only removals and retargets blocked, the gate would fire on exactly what it exists to catch and stay quiet otherwise.

Interested in your view on that, since you have thought about the fail-closed stance more than I have. Enabling "require branches to be up to date before merging" on the repo would also make the staleness visible rather than silent, which is worth doing regardless of how this lands.

None of this is a criticism of the approach — the harness does what it claims. It is the protocol around it that needs settling.

@bhelm
bhelm force-pushed the codex/harness-governance-pr1 branch from 256af7f to 4cb1faa Compare August 24, 2026 16:03
@bhelm

bhelm commented Aug 24, 2026

Copy link
Copy Markdown
Author

Thanks — my take is a proposal, not a prescription; as maintainer, the final policy call is yours. The main project risk I see is accidental Android/iOS divergence, especially when a contributor does not realize that a shared change needs both sides. “Parity Guard” might be a useful concise name for that intent.

I would therefore make shared bug fixes and final shared features parity-required, while allowing one-sided experimental work or genuinely platform-specific behavior only through an explicit typed disposition. Improvements and obsolete dispositions should warn, not block; regeneration should compute the new state but never silently approve new debt.

The branch is now rebased and implements that model, with 203 tests, a green ledger, and errors=0 against current main. This remains only the foundation/demo/self-test layer even if merged — product-source CI enforcement is intentionally deferred to PR4, after PR2/PR3 validate the intermediate layers.

@bhelm
bhelm force-pushed the codex/harness-governance-pr1 branch from 4cb1faa to 333665e Compare September 1, 2026 00:07
@bhelm

bhelm commented Sep 1, 2026

Copy link
Copy Markdown
Author

Rebased onto current main and refreshed the governance authority in c7578748f.

The rebase did exactly what this PR is meant to do: it refused to refresh while ten newly encountered twin declarations were unresolved. I fixed those before regenerating any snapshots.

Ten hard scan errors found after the rebase

These were declaration/scanner-contract problems, not demonstrated Android/iOS runtime divergences:

  • Swift package-qualified top-level twins were not understood by the scanner. This affected declarations introduced in #1626, #1628, and #1634. The scanner now understands the Swift package module as a valid owner.
  • Kotlin primary-constructor properties were not attributed to their containing type. This made LiveState.historyReady from #1753 look dead. Constructor properties are now resolved with their class owner.
  • Three source annotations were stale or underspecified:
    • pct from #1688 was ambiguous among five Kotlin functions; it now names HrvAnalyzer.pct.
    • the LocalDate.now().toEpochDay() implementation note from #1757 was prose about a library expression, not an app-owned twin declaration; it is no longer phrased as one.
    • deviceInventoryLines from #1633 still named its old Swift owner; it now names DeviceInventory.deviceInventoryLines.

Both scanner gaps have focused red-before/green-after tests.

New inherited parity debt since the previous snapshot

Current main also contains 19 additional exact findings compared with this PR's previous reviewed authority. These are now recorded rather than hidden:

  • sparse-sleep bridge test-only surfaces: #1755
  • multi-device motion/session helpers: #1730
  • raw-IMU unpack helper: #1709 — one of ours
  • HR fingerprint helpers: #1734 and #1620
  • stable serial-identity helpers: #1455
  • alarm-readback classification helpers: #1707
  • hypnogram hole classifier: #1764
  • MG type-43/51 raw-data helpers and one lexical constant: #1765
  • sleep-debt lexical constant: #1741

“Finding” here means the conservative lexical guard sees a test-only surface or a mirrored expression it cannot prove equal; it does not by itself prove a user-visible platform mismatch. Each identity is nevertheless retained in the reviewed baseline, remains visible, and cannot be replaced or increased silently.

Verification on the refreshed head:

  • python3 Tools/parity_ledger.py: pass, no new findings
  • 581 declared twin references; 581 resolved
  • 239 exact inherited findings baselined
  • full parity-governance suite: 102 tests pass
  • both new scanner tests fail before their fixes and pass afterward
  • the Tools workflow remains unfiltered on every PR, while core and governance test-count floors stay independent

This is also a concrete example of why landing the guard matters: these declarations accumulated while the guard was still outside main, so they were only reviewed together when this long-running PR was rebased.

@bhelm
bhelm force-pushed the codex/harness-governance-pr1 branch from c757874 to 55eacad Compare September 1, 2026 00:50
@bhelm

bhelm commented Sep 1, 2026

Copy link
Copy Markdown
Author

Follow-up on the remaining red Android check:

The failing test is not introduced by this PR. I reproduced the exact failure on a detached, unchanged current origin/main (8c9fca0f9):

PushCoordinatorTest.capabilitiesPreventEveryUnsupportedStreamSnapshotAndPost
expected: [journal]
actual:   []

The test came from #1631. It stores a journal row dated 2026-08-18 but constructs PushCoordinator without injecting the test clock, so production LocalDate.now() is used. As of 2026-09-01 that row has moved outside the 14-day mutable window and no journal batch is emitted. The same focused Gradle test fails both on this PR and directly on current main.

For completeness on provenance: the parity-debt list above intentionally includes our own work as well—#1709 is the source of the inherited raw-IMU unpack findings. No contributor or PR was exempted from attribution.

All checks owned by this PR, including linux-capture / the parity-governance suite, are green. The only remaining red check is this independently reproduced main-branch test regression.

@ryanbr

ryanbr commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Reviewed this properly — merged it onto current main locally and ran the tooling. The design is sound and it demonstrably works. Two things needed before it can land, and one number worth a decision.

It proved itself on live code

Running the ledger against the merged tree produced two findings, both on one line — mine, written the day before:

PreSleepHeartRateFeedback.kt:239: dead-twin-reference:
  Swift target subtractingReportingOverflow does not resolve

I'd written "The Long twin of Swift's subtractingReportingOverflow". That's stdlib on FixedWidthInteger, not a declaration here — the exact mistake this PR fixes in AICoach.swift. Two of us reached for that phrasing independently. Fixed in #1794 rather than baselined, so it doesn't get laundered into accepted debt.

I also checked your DeviceInventory.kt correction: deviceInventoryLines really does live in Strand/System/DeviceInventory.swift, not DebugDataDiagnostics. The doc pointed at the wrong Swift type and the tool caught it. That's a real answer to "is 3352 lines of tooling proportionate".

The failing check is not yours

build-and-test failed on PushCoordinatorTest.capabilitiesPreventEveryUnsupportedStreamSnapshotAndPost. That's a wall-clock time bomb that broke main at midnight UTC on 1 Sep — your run was at 00:50Z, the fix (#1786) merged at 02:33Z. Inherited, not yours. A rebase clears it.

What's needed

Rebase onto current main and regenerate both JSON files. You're 16 commits behind, and on the merged tree the acceptance suite fails 6 of 102 — all RepositoryBaseline*, the tests asserting the checked-in baseline matches current sources. That's the tool behaving exactly as documented ("editing a baseline without matching current sources fails closed"), not a defect. The ledger reports ~1299 findings on that tree, which is the accepted-debt set failing to match a moved tree rather than 1299 new problems.

Worth doing after #1794 lands so the dead-twin line isn't captured into the regenerated baseline.

One number for @ryanbr to decide

linux-capture goes from ~21s to 6m48s, on every PR — tools-python.yml has no path filter:

PR linux-capture
#1793 (docs-only) 21s
#1792 22s
#1534 6m48s

The ledger itself is 68s / ~526 MB; the rest is the acceptance suite (284s locally). A 19× increase on the cheapest always-on gate is a real change to contributor feedback time. Options: path-filter the parity suite to Tools/** plus the source roots, or split it into its own workflow so it doesn't sit in front of every docs typo. Not a blocker, but it should be a deliberate choice rather than a side effect.

On the design

The scoping is honest and I checked it against the diff: the doc says CI enforcement is deferred to a later stack PR, and the workflow change does only run the tests — no blocking parity gate on contributors yet. Splitting the core and parity suites with independent floors, because "a single total lets growth in one suite hide accidental deletion in the other", is the right instinct.

The parts that address what actually goes wrong in parity audits — selector labels disambiguating overloads, "name-only suggestions are not authority" — are the parts I'd have worried about, since naming divergence between the two layers (VALID_MIN_BPM vs validMinBpm, DetectedSleep vs SleepSession) is exactly what defeats a grep.

@ryanbr

ryanbr commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Clarifying my own figure above, because if you try to reproduce it you'll see something different — and the reason is worth knowing.

The ledger halts on scan errors before it ever evaluates the baseline. On the merged tree today:

state output
dead-twin reference present FAIL 2 parity ledger scan error(s) — zero mentions of the baseline
after #1794 clears it 38 baseline mentions, 1299 add-unpaired-* findings

So both numbers are real, at different stages. My "~1299 findings" only appears after the two scan errors are fixed; run it before and you get 2 errors and no baseline information at all.

That makes the sequencing advice stronger than I put it: it isn't just tidier to land #1794 first, it's that you cannot see your baseline's state while any scan error exists. One dead reference anywhere in scope hides whether the baseline is current.

Worth considering whether that's the behaviour you want. Fail-fast on scan errors is defensible — a claim that doesn't resolve means the inventory is untrustworthy, so comparing against it would be comparing to noise. But the operator cost is that "my baseline is stale" and "someone wrote a bad twin reference" produce completely different output, and only the second is visible. A one-line summary at the end — "baseline not evaluated: N scan errors" — would make the masking explicit without changing the fail-closed semantics.

Also worth recording: I checked the 1299 against the source. Whoop5RawOptical.kt, Whoop5Variant.kt and ActivityEstimate.swift have zero commits since your branch point, so those findings are the accepted-debt groups failing to match a moved tree, not new one-sided declarations. Your baseline has 25 groups covering that debt, and the identity-set hashing means unrelated movement elsewhere invalidates a whole group. That's the design working, but it does mean a regeneration is needed roughly whenever anything in scope moves — which is the ongoing cost to weigh alongside the CI time.

ryanbr added a commit that referenced this pull request Sep 1, 2026
I wrote this in #1785:

    /** `a - b`, or null when it would overflow. The Long twin of Swift's
        `subtractingReportingOverflow`. */

subtractingReportingOverflow is Swift STDLIB on FixedWidthInteger, not a
declaration in this repository, so the reference resolves to nothing. A reader
chasing it finds no such function here, and neither does an audit.

Found by the parity ledger in #1534, which reported it as a dead-twin-reference
within minutes of being run against a merged tree - and that same PR fixes the
identical mistake in AICoach.swift, where "the twin of Kotlin's
LocalDate.now().toEpochDay()" became "Kotlin computes the same value with".
Two of us reached for that phrasing independently, which says it is easy to
reach for rather than a one-off slip.

Reworded to say what is true: Apple expresses the same contract with that
method, and it is stdlib rather than a twin. The file's PARITY note a few lines
up already had this right - it contrasts Math.subtractExact with
subtractingReportingOverflow without claiming a pairing - so this brings one
line into line with the rest of the file.

Fixed rather than baselined, deliberately. Accepting it as known debt would
launder a defect I introduced into the accepted set, which is the opposite of
what a ledger is for.

Verified against #1534's ledger that this specific finding is gone: two findings
on PreSleepHeartRateFeedback.kt:239 before, none after. It also unblocks that
PR's rebase - the ledger halts on scan errors before it ever evaluates the
baseline, so while this line stood, its baseline state could not be seen at all.

Comment only; no behaviour change.
@ryanbr

ryanbr commented Sep 1, 2026

Copy link
Copy Markdown
Owner

#1794 is merged, so the scan error is gone from main and this PR's baseline state is visible again.

Rebase order that should get you green:

  1. rebase onto current main (f481377 + analytics: do not claim a twin with a Swift stdlib method #1794) — that also clears the build-and-test failure, which was the wall-clock time bomb in PushCoordinatorTest, not yours
  2. regenerate parity_ledger_baseline.json and parity_twin_map.json against the rebased tree
  3. the six RepositoryBaseline* acceptance failures should go with it — they're the tests asserting the checked-in baseline matches current sources, which is the tool behaving as documented

The dead-twin line your ledger found is fixed rather than baselined, so it won't be captured into the regenerated set.

Still worth a decision from @ryanbr before this lands: linux-capture goes from ~21s to 6m48s on every PR, since tools-python.yml has no path filter. Path-filtering the parity suite, or splitting it into its own workflow, would keep that off the front of a docs typo.

@bhelm
bhelm force-pushed the codex/harness-governance-pr1 branch from 55eacad to 72f4d3c Compare September 1, 2026 20:37
@bhelm

bhelm commented Sep 1, 2026

Copy link
Copy Markdown
Author

Rebased onto current main (d6d84cc52) after #1794 and refreshed both derived snapshots through the guarded flow. The branch is now 0 commits behind main.

I also took the suggested scan-error UX cleanup: after listing concrete scan errors, the ledger now ends with Baseline not evaluated: N scan errors., with a focused red-before/green-after regression and an operator note in PARITY_GOVERNANCE.md.

Verified on final head 72f4d3ca6:

  • guarded refresh passed and a second refresh produced a byte-identical diff
  • ledger green: 242 baselined findings; 587/587 declared references resolved; 612 constant pairs
  • offline and online ratchets: errors=0
  • parity-governance suite: 102/102 pass
  • core Tools suite: 108/108 pass
  • py_compile and git diff --check pass
  • independent discovery floors raised to the observed counts (108 core / 102 governance)

The Tools workflow remains deliberately unfiltered, matching the current acceptance contract; I did not silently turn the runtime trade-off into a path-filtering policy change in this refresh.

@bhelm

bhelm commented Sep 1, 2026

Copy link
Copy Markdown
Author

Follow-up on the CI-runtime decision: I have now split the parity-governance suite into its own path-filtered workflow on b0d15577d.

  • the fast core Tools workflow remains unfiltered on every PR
  • the 102-test parity suite runs for governance implementation/metadata changes and for the scanner's complete Swift/Kotlin reference scope (Packages/**/*.swift, Strand*, NOOPWatch*, and android/**/*.kt)
  • unrelated docs and assets no longer pay the multi-minute parity-suite cost
  • the split and broad reference-scope coverage are protected by an acceptance test
  • PARITY_GOVERNANCE.md now documents the distinction between path-filtered self-tests and the still-deferred blocking ledger/ratchet invocation

Full parity-governance discovery remains green: 102/102.

bhelm and others added 17 commits September 10, 2026 11:46
The add-unpaired-function diagnostic decided "new" from added diff lines, so a
pre-existing declaration whose line was rewritten was reported as newly
unpaired. It now compares the base and current declaration inventories
(platform, owner, name, arity); authority drift only fails closed on removals
and newly unpaired identities, additions and debt reductions pass.

Red test: the rewritten existing helper was reported; green after the change,
a genuinely new one-sided function is still reported. 67 ledger tests and the
full Tools suite pass; the PR 1979 reproduction goes from exit 1 to exit 0.
Two attached twin claims stopped resolving on current main: the SleepStager
trace claimed a bare round1 that now matches twelve Kotlin functions, and the
BLE client claimed BLEManager.didConnect where the Swift method is
centralManager(_:didConnect:). Qualify both; no logic changes.
Regenerated through the guarded refresh against main 7055def: 466 files,
141 function pairs, 657 constant pairs, 270 baselined findings. Ledger green,
ratchet errors=0 against the rebase base.
main now path-filters tools-python.yml to Tools/** plus itself, so the
acceptance test asserting an unfiltered core workflow described a state that
no longer exists. Assert instead that the core filter covers every governance
tool path and that the governance workflow stays filtered to its own files.
Discovery floor raised to the measured 103 tests.
@bhelm
bhelm force-pushed the codex/harness-governance-pr1 branch from 2bd1f4f to c8e3cbf Compare September 10, 2026 09:52
@bhelm

bhelm commented Sep 10, 2026

Copy link
Copy Markdown
Author

The failed parity-governance run was another moved-base race, not a scanner regression: GitHub built the merge ref after main advanced again, so the checked snapshots no longer matched the merge tree (Packages/StrandImport and android/ingest baseline groups; authority file count 478 → 482).

I have rebased once more onto the new current main and ran the guarded refresh as the final change. This refresh modified only parity_ledger_baseline.json and parity_twin_map.json.

Verified before push:

  • the two previously failing RepositoryBaseline* checks pass
  • core Tools: 109/109; parity governance: 103/103
  • ledger green: 288 baselined findings, no new findings; 806/806 twin references resolved
  • offline and online ratchets: errors=0
  • py_compile and git diff --check pass
  • merge-base is exactly the current main

No product or comment changes were needed in this round.

@ryanbr
ryanbr merged commit 86368eb into ryanbr:main Sep 11, 2026
19 checks passed
ryanbr added a commit that referenced this pull request Sep 11, 2026
PR #1534 landed with snapshots derived from its own branch base, which sat 26
commits behind main. On main the ratchet re-derives the authority from the base
tree and compares it against the checked-in manifest (parity_ratchet.py:438), so
the stale pin failed the push-triggered gate: 482 files against 484 actual, plus
test-only-callsite drift in Packages/StrandAnalytics and Packages/WhoopStore.

The guarded refresh cannot repair that state by design. It rejects with "base
authority cannot be reproduced with the current derivation; migration required",
because the checked-in authority never matched a derivation of any tree that
also carries it. Regenerated from main's own tree instead.

No new debt was accepted: known findings stay at 288, with StrandAnalytics 42 to
41 and WhoopStore 36 to 37 netting out. The authority manifest tracks only the
two source files main gained (484 files, 4282 functions, 662 constant pairs).
103 of 103 parity-governance tests pass.
ryanbr added a commit that referenced this pull request Sep 15, 2026
A one-sided PROPERTY could never be dispositioned, so once one existed the
authority refresh refused permanently. Lift Log added two, which is how it
surfaced.

_required_v3_exemptions built its kind strings with name[:-1], so
unpaired_properties stemmed to add-unpaired-propertie, while
_validate_dispositions accepts add-unpaired-property. The two never meet:
the stemmed spelling is rejected by validation, and the accepted spelling never
matches the requirement, which is compared as an exact (kind, identity) key. The
debt could be neither waived nor cleared. Only the -ies plural breaks; files,
functions and constants survive the strip, and the removal side is fine because
"pairs" strips to "pair".

It went unnoticed since #1534 because the path was unreachable. No test declared
a one-sided property and none existed in the tree, so nothing ever generated the
bad kind.

Replaces the strip with explicit singulars and adds a test asserting the
invariant rather than the spelling: every kind the ratchet can REQUIRE must be a
kind a disposition may DECLARE. It builds a real disposition per required kind
and runs it through the validator, so a future set name that stems badly fails
immediately. Reverting the fix fails it on the exact string.

Also wires that test into the governance job, which runs an explicit module list
rather than discovery: a new file under Tools/tests is matched by the workflow's
paths filter, so it would have TRIGGERED the job while never being collected by
it. The collected-count floor moves 113 to 115 in step, because the three
existing modules collect exactly 113 and the floor tracks the real total.

Verified locally, since this PR's own parity-governance cannot report on it: the
exact CI block runs, the continuations assemble, the count extraction yields 115
and clears the floor. Three modules give 113 and four give 115, with the same two
failures either way.

Merged with parity-governance red. That failure is the stale checked-in authority
described in #2229, reproduces identically on unmodified main, and is inherited
by any pull request touching Tools/parity_*. Nothing here can clear it, and while
it persists the check cannot validate this change either, since GitHub runs the
step under bash -e and aborts before the floor check on a failing suite.
@bhelm
bhelm deleted the codex/harness-governance-pr1 branch September 15, 2026 22:35
ryanbr pushed a commit that referenced this pull request Sep 21, 2026
…lidation

Adds the portable behavioural-test contract that the static inventory gate (#1534) does not provide: cases carrying shared inputs and literal expected results, deterministic expansion, an authoritative shard and operation registry, exact split and remerge, and input-bound result artifacts.

A platform validates its own artifact against the shared expectations without building the other platform or waiting for its output; a later comparison takes both result files and checks their agreement. Both sides are checked against the expected results as well, so two implementations returning the same wrong answer cannot pass merely by agreeing. The expectations in `Tools/parity_case_specs/core_reference.json` are hand-written literals rather than values emitted by the Python reference runner, which is what makes that property real. Keep it that way in later specs: generating them would remove the property silently while every test still passed.

Each case hash binds its inputs, identity, function, shard, expected result and suite version. Output metadata must match those cases and the git revision the caller requires, so a stale or mixed revision, a changed case, malformed output or a missing result fails closed. Revision metadata is a consistency contract, not proof of which code ran.

Scope is deliberate and stated by the author: the only operation exercised is `Portable.clamp/3`, so this is contract and scaffolding whose value arrives with the native runners and the first real migration. Python agreement is not evidence of Swift or Kotlin product parity, and no native build or enforcement is coupled in here.

The governance discovery floor moves 122 to 159 alongside `tests.test_parity_harness`, and `Tools/parity_case_specs/**` joins both workflow path filters and the acceptance test's baseline list. Verified by merging onto current main in a clean worktree and running CI's own command: 159 tests, OK.

Contributed by @bhelm
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