Skip to content

feat(analysis): exact counts and rationals with an explicit numeric policy - #49

Merged
hyperpolymath merged 3 commits into
mainfrom
feat/numeric-policy-foundation
Sep 22, 2026
Merged

hyperpolymath merged 3 commits into
mainfrom
feat/numeric-policy-foundation

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Refs #1 — the numeric-integrity half of the validated statistics layer, as a first
foundation. No method is added, and nothing existing changes behaviour: the
default :ordinary mode is Float64 throughout, so saved analyses are untouched
(criterion: "verify existing scientific behaviour remains unchanged when the new
analysis layer is not selected").

What it does

Keeps three claims apart rather than letting one representation carry all three —
exact (integer counts, rationals), approximate (any float, including
arbitrary precision
), rounded (display) — and refuses the conversions where the
alternative is a wrong number that looks right:

  • 1//0. Julia answers 1//0 for a non-zero numerator: a non-finite Rational
    that flows into charts looking like a number. A zero denominator is refused before
    the division, and a sample of zero reads reports nothing — an unsuccessful state
    the presentation layer must render as such, not 0.0.
  • Counts as Float64 beyond 2^53. Such a value can hold a value but cannot prove
    which integer it was, and its history at an earlier boundary (R, CSV, a JavaScript
    client) cannot be inspected. Refused rather than trusted.
  • Overflow — checked sums raise CountOverflowError unless the caller asks for
    :widen; a total that wraps is worse than a failure because nothing downstream can
    detect it.
  • Unbounded rational growth — a stated max_denominator_bits budget, with
    ResourceLimitError when exhausted.
  • NaN/Inf at the JSON boundary, which JSON cannot carry.

Concurrency

BigFloat precision is a process global in Julia, not task-local, so
with_precision serialises callers (the same trade the R runtime already makes) and
reads the achieved precision inside the scope. Six concurrent tasks at six
precisions are asserted to each observe their own, default restored afterwards.

Contracts published

docs/statistics/numeric-contracts.md: the JSON rules (integers beyond 2^53 and
rationals travel as strings, because a JSON number in a browser is a Float64), CSV,
R and DuckDB boundaries, the mode-downgrade refusal, and the provenance fingerprint
that makes two runs comparable. docs/statistics/method-catalogue-v1-draft.md drafts
the bounded method catalogue for approval — #1 requires it before methods are
implemented, and approval is the owner's call, not this PR's.

Verification

144 tests against the real module: known-answer, adversarial (overflow, all-zero
tables, denominator exhaustion, non-finite input), metamorphic (proportions of a table
sum to exactly 1 across 50 random tables), concurrency, and negative controls that
assert the lossy alternatives really are lossy — e.g. that
Float64(2^53 + 1) != 2^53 + 1, so the refusal at that boundary is not a formality.
scripts/check-spdx.sh, check-format.sh, check-lint.sh, check-blob-hygiene.sh
and config/ci/lint_source.jl all clean.

What this does not do

It does not make any method exact, and it claims no accuracy: precision is not
accuracy, and measurement error, sampling uncertainty and model misspecification are
untouched. The remaining #1 criteria — the catalogue's approval, method-level
known-answer and simulation validation, independent statistical review — stay open, and
the symbolic gate (#2) stays blocked.

The live-R guard asked a different question from the record it stood in
front of: it probed an empty package list, its consumer probed the four
in R_PACKAGES, so on any checkout where R answers but the packages are
absent -- every bare checkout -- the guard reported ready and the
consumer threw outside any try, erroring the testset before its first
@test (#43). The @info branch could not fire for the case it was
written for.

provenance.jl now answers the question once. RProbeStatus carries a
status (:ok, :packages_missing, :r_unreachable, :no_lockfile) beside
the record, and r_probe_status performs the single probe whose record
the suite then asserts on, so guard and consumer cannot diverge. The
failure's component names travel as a ProbeFailure field rather than as
prose, so classification is a field read, and probe_r collects every
absent package instead of stopping at the first: four missing packages
report four.

The skip is loud and named: the testset is titled with the status and
the reason -- "r - not probed (packages_missing): ... dada2, Biostrings,
ShortRead, vegan" -- and recorded with @test_skip, so it lands in the
summary's own Broken column rather than in an @info that scrolls past.
On CI, where ci.yml installs R and restores all 79 packages from
renv.lock, the same block is a hard assertion: a skip there is a
provisioning regression, not a bare checkout.

The negative control is pure and runs everywhere the live probe skips:
:r_unreachable, :packages_missing and :no_lockfile are asserted to be
distinguishable in both symbol and text. Verified against the real
module on a box with R 4.5.0 and none of the four packages: the
:packages_missing path is exercised live, names all four, and records
Broken rather than passing or erroring.

Closes #43
Issue #15 asks for the Milestone 2 record to be audited. Re-ran the
claims rather than reading them back, and two of them no longer
described the repository:

- the ">10% regression gate fails CI" claim, for the frontend script
  and for each bench/*.jl. The gate was built, then deliberately
  replaced by an informational comparison: ci.yml records two
  consecutive runs of identical benchmark code producing -16%..+52%
  deltas, because the workloads import no application code and so
  measure the runner. bench/table_loading/benchmark.jl prints NOTE and
  warns instead of failing. Corrected, with the reasoning kept.
- the test-suite figures: 586 frontend tests across 16 files are now
  604 (599 pass, 5 todo, 0 fail, 3368 expects) and 27 Julia unit files
  / 6830 lines are now 30 / 8310. Corrected as dated audit notes beside
  the original snapshot rather than overwritten, so the record shows
  both what was measured then and what holds now.

Verified and left as recorded: coverage 40.23% funcs / 47.50% lines
(claim 40/47), all five benchmark categories plus the comprehensive
runner and analysis_config, the seven frontend workloads one-for-one,
Codecov residue gone and gitar absent, CI runtime 14-29 min (claim
15-20). The project board is recorded as unverifiable with the
credentials this audit had, not assumed.

docs/audit/milestone-2-close-out.md carries the claim-by-claim
evidence and the reproducible commands behind each verdict.

Closes #15
…olicy

First foundation for the validated statistics layer (#1): the numeric
integrity half, which the methods half builds on. No method is added
here and nothing existing changes behaviour -- :ordinary, the default
mode, is Float64 throughout and leaves saved analyses untouched.

The module keeps three claims apart instead of letting one
representation carry all of them: exact (integer counts, rationals),
approximate (any float, including arbitrary precision), rounded
(display). Higher precision is not exactness, and the contract says so.

What it refuses, each because the alternative is a wrong number that
looks right:

- `1//0`. Julia answers `1//0` for a non-zero numerator -- a
  non-finite Rational that renders as a number -- so a zero denominator
  is refused before the division, and a sample of zero reads reports
  `nothing` rather than a proportion of 0.0.
- Counts arriving as Float64 beyond 2^53, which can hold a value but
  cannot prove which integer it was, and whose history at an earlier
  boundary (R, CSV, a JavaScript client) cannot be inspected.
- Overflow: checked sums raise CountOverflowError unless the caller
  asks for :widen, rather than wrapping to a wrong total.
- Unbounded rational growth, which is a resource decision and now
  raises ResourceLimitError against a stated budget.
- NaN/Inf at the JSON boundary, which has no representation for them.

Concurrency: BigFloat precision is a process global, not a task-local,
so with_precision serialises callers and reads the achieved precision
inside the scope. Six concurrent tasks at six precisions are asserted
to each observe their own, with the default restored afterwards.

Boundaries are published in docs/statistics/numeric-contracts.md: the
JSON rules (integers beyond 2^53 and rationals travel as strings,
because a JSON number in a browser is a Float64), CSV, R, DuckDB, and
the provenance fingerprint that makes two runs comparable. The bounded
method catalogue is drafted for approval in the same directory, since
#1 requires it before methods are implemented. .gitignore whitelists
docs/statistics/ the same way its sibling documentation directories are
whitelisted.

Tests: 144, run under the real module. Known-answer, adversarial
(overflow, all-zero tables, denominator exhaustion, non-finite input),
metamorphic (proportions of a table sum to exactly 1 across 50 random
tables), concurrency, and negative controls that assert the lossy
alternatives really are lossy.

Refs #1
@coderabbitai

coderabbitai Bot commented Sep 22, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 53 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 100541a5-11db-4bc3-9b9a-6d91dde1a9a4

📥 Commits

Reviewing files that changed from the base of the PR and between 22d228f and 99efa78.

📒 Files selected for processing (11)
  • .gitignore
  • docs/audit/milestone-2-close-out.md
  • docs/milestones/02-baseline-tests-benchmarks.md
  • docs/statistics/method-catalogue-v1-draft.md
  • docs/statistics/numeric-contracts.md
  • src/MetaManifold.jl
  • src/analysis/numeric_policy.jl
  • src/core/provenance.jl
  • test/runtests.jl
  • test/unit/test_numeric_policy.jl
  • test/unit/test_provenance.jl

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

@hyperpolymath
hyperpolymath merged commit e1856af into main Sep 22, 2026
4 checks passed
@hyperpolymath
hyperpolymath deleted the feat/numeric-policy-foundation branch September 22, 2026 08:56
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