Skip to content

Add Wollack-style omega answer-copying statistic - #259

Merged
seonghobae merged 6 commits into
mainfrom
seonghobae-wollack-omega
Jul 26, 2026
Merged

Add Wollack-style omega answer-copying statistic#259
seonghobae merged 6 commits into
mainfrom
seonghobae-wollack-omega

Conversation

@seonghobae

@seonghobae seonghobae commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Implements the Wollack-style omega answer-copying statistic as fast_mlsirm.wollack_omega (Rust core mlsirm_core::security::wollack_omega, PyO3 py_wollack_omega). Stacked on #258 (DIMTEST).

Contract

  • h = count of items where copier and source chose the same option.
  • p_i = P_i[source_i] — the COPIER's model-implied probability of the SOURCE's observed option (caller supplies fitted option probabilities, e.g. from a nominal response model).
  • omega = (h - sum p_i) / sqrt(sum p_i (1 - p_i)), one-sided upper-tail normal p-value.
  • Scope (adversarial spec-verify REDUCED-SCOPE APPROVED): omega only — no g2/GBT/K-index, no continuity correction, no missing responses, no NRM fitting inside the function.

Citation governance

  • READ: CRAN CopyDetect R sources (similarity1.r/similarity2.r) and the independent aberrance package (compute_OMG) — formula verified against both.
  • NOT READ: Wollack (1997, APM 21(4), 307-320) itself (access blocked); cited only as implemented by those sources.
  • Documented conflict: CopyDetect's printed docs show (E-h)/sqrt(V) but both source files compute (h-E)/sqrt(V) with an upper-tail p; the source convention is implemented.

Evidence

  • Pinned independent Python oracle (10 items x 5 options): h=6, E=3.31, V=1.8839, omega=1.9598523632230238, p=0.02500652442931299 — asserted at 1e-12 (p at 5e-7, crate erfc |err|<1.2e-7). Every assert reads crate OmegaResult fields.
  • Structural invariant: appending one item changes E by exactly p and V by exactly p(1-p) (kills stride/accumulation bugs).
  • Error paths: lengths, n_options, option range (both vectors), row finiteness/nonnegativity/sum-to-1, zero variance.
  • 500-rep Monte Carlo (#[ignore]): empirical size < 0.10 at alpha=0.05 under the null; power > 0.8 under 40% copying — PASSED.
  • 3 EXECUTED mutation kills: (M1) divide by V instead of sqrt(V) -> FAILED; (M2) probability looked up at the copier's option -> FAILED; (M3) two-sided p -> FAILED. Restored green (3 passed + MC).
  • Python wrapper validates BEFORE casts (complex/string/fractional/range rejection); 4 pytest cases green.

Adversarial impl-review

  • Round 1 (3efcf4f): 2 findings — (MAJOR) Python wrapper accepted boolean index arrays, now rejected pre-cast with regression test; (MINOR) aberrance citation claimed unverified authors/year, now cites only package + read source files. Fixed in 18144fc.
  • Round 2 (18144fc): 1 finding — (MINOR) Rust module doc lacked the CopyDetect docs-vs-source sign-conflict disclosure present on the other surfaces. Fixed in 73ec82d. Reviewer independently recomputed the pinned oracle (digit-for-digit match) and re-ran all tests green.
  • Status: CLEAN.

Base automatically changed from seonghobae-dimtest to seonghobae-ci-classify July 26, 2026 09:05
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@seonghobae, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5258deb5-beb4-4a9b-bbd7-8a1d25418012

📥 Commits

Reviewing files that changed from the base of the PR and between 11e81fa and c20a2f2.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • crates/fast-mlsirm-py/src/lib.rs
  • crates/mlsirm-core/src/detect.rs
  • crates/mlsirm-core/src/lib.rs
  • crates/mlsirm-core/src/security.rs
  • python/fast_mlsirm/__init__.py
  • python/fast_mlsirm/detect.py
  • python/fast_mlsirm/security.py
  • tests/test_paper_features.py
  • tests/unit/data/dimtest_fixture.txt
  • tests/unit/detect_tests.rs
  • tests/unit/security_tests.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch seonghobae-wollack-omega

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

@seonghobae
seonghobae force-pushed the seonghobae-ci-classify branch from dc6f32e to d0a3573 Compare July 26, 2026 09:43

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

OpenCode cannot approve yet because required coverage evidence did not pass.

Review outcome

1. HIGH .github/workflows/opencode-review.yml:1 - Coverage evidence did not prove required test/docstring evidence

  • Problem: The required coverage-evidence job result was failure, so OpenCode cannot establish approval sufficiency for this head.

  • Root cause: Automated approval is only valid when the same-head coverage-evidence job proves supported repository test suites passed and configured docstring gates passed or were advisory, or reports not applicable because no supported source files or package manifests exist. Missing, failed, skipped, unavailable, or unsupported-tooling test evidence is a blocker.

  • Fix: Install or configure the repository test/docstring evidence tooling when source files or package manifests exist, rerun the current-head coverage-evidence job, and approve only after it reports success with required evidence or explicit no-source not-applicable evidence.

  • Regression test: Keep the approval branch checking needs.coverage-evidence.result == success before posting APPROVE, and publish REQUEST_CHANGES when coverage-evidence blocker states such as cancelled, skipped, failed, unsupported-tooling, or below-100 evidence are present.

  • Result: REQUEST_CHANGES

  • Reason: coverage-evidence result was failure, so required test/docstring evidence was not proven for current head 0bced3c4a1300fcf0c5f7177fb1579fc903468b5.

  • Head SHA: 0bced3c4a1300fcf0c5f7177fb1579fc903468b5

  • Workflow run: 30197002761

  • Workflow attempt: 1

Coverage evidence

Coverage evidence job did not run or did not publish coverage evidence.

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file (10 files)"]
  S1 --> I1["repository behavior"]
  I1 --> Conflict["Merge conflict blocks this path"]
  Conflict --> V1["required checks"]
  Evidence --> S2["Test (5 files)"]
  S2 --> I2["regression suite"]
  I2 --> Conflict["Merge conflict blocks this path"]
  Conflict --> V2["targeted test run"]
Loading

@opencode-agent

Copy link
Copy Markdown
Contributor

OpenCode Review Overview

  • Head SHA: 0bced3c4a1300fcf0c5f7177fb1579fc903468b5
  • Workflow run: 30197002761
  • Workflow attempt: 1
  • Gate result: REQUEST_CHANGES (approval step)

Pull request overview

OpenCode cannot approve yet because required coverage evidence did not pass.

Review outcome

1. HIGH .github/workflows/opencode-review.yml:1 - Coverage evidence did not prove required test/docstring evidence

  • Problem: The required coverage-evidence job result was failure, so OpenCode cannot establish approval sufficiency for this head.

  • Root cause: Automated approval is only valid when the same-head coverage-evidence job proves supported repository test suites passed and configured docstring gates passed or were advisory, or reports not applicable because no supported source files or package manifests exist. Missing, failed, skipped, unavailable, or unsupported-tooling test evidence is a blocker.

  • Fix: Install or configure the repository test/docstring evidence tooling when source files or package manifests exist, rerun the current-head coverage-evidence job, and approve only after it reports success with required evidence or explicit no-source not-applicable evidence.

  • Regression test: Keep the approval branch checking needs.coverage-evidence.result == success before posting APPROVE, and publish REQUEST_CHANGES when coverage-evidence blocker states such as cancelled, skipped, failed, unsupported-tooling, or below-100 evidence are present.

  • Result: REQUEST_CHANGES

  • Reason: coverage-evidence result was failure, so required test/docstring evidence was not proven for current head 0bced3c4a1300fcf0c5f7177fb1579fc903468b5.

  • Head SHA: 0bced3c4a1300fcf0c5f7177fb1579fc903468b5

  • Workflow run: 30197002761

  • Workflow attempt: 1

Coverage evidence

Coverage evidence job did not run or did not publish coverage evidence.

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file (10 files)"]
  S1 --> I1["repository behavior"]
  I1 --> Conflict["Merge conflict blocks this path"]
  Conflict --> V1["required checks"]
  Evidence --> S2["Test (5 files)"]
  S2 --> I2["regression suite"]
  I2 --> Conflict["Merge conflict blocks this path"]
  Conflict --> V2["targeted test run"]
Loading

Merge Conflict Guidance

  • Current merge state: DIRTY
  • Base branch: seonghobae-ci-classify
  • Head branch: seonghobae-wollack-omega
  • Fix direction: merge or rebase origin/seonghobae-ci-classify into seonghobae-wollack-omega, resolve conflict markers in the changed files, rerun the focused checks, then push the same branch.
  • Repair commands:
gh pr checkout 259 --repo ContextualWisdomLab/fast-mlsirm
git fetch origin seonghobae-ci-classify
git merge --no-ff origin/seonghobae-ci-classify  # or: git rebase origin/seonghobae-ci-classify
git status --short
# resolve files, then git add <resolved-files>
# merge path: git commit
# rebase path: git rebase --continue
git push origin HEAD:seonghobae-wollack-omega
# rebase path only: git push --force-with-lease origin HEAD:seonghobae-wollack-omega

Base automatically changed from seonghobae-ci-classify to main July 26, 2026 09:50
@seonghobae
seonghobae force-pushed the seonghobae-wollack-omega branch from 0bced3c to d15abdc Compare July 26, 2026 09:51
seonghobae and others added 6 commits July 26, 2026 18:52
…ensionality test

Implements the original (non-bootstrap) DIMTEST with caller-supplied
confirmatory AT1/AT2 partitions, PT raw-score grouping (J_k >= 20),
ML variance denominators, the Nandakumar & Stout (1992/1993) refined
bias-correction denominator S_k, T_L/T_B statistics, and one-sided
p-value. Formulas transcribed from the inspected ERIC ED351383
technical report (pre-publication of Nandakumar & Stout, 1993, JEBS).

- Rust core in mlsirm-core detect.rs with pinned 17-digit oracle
  (independent Python reference), exact-null bits fixture, error
  paths, and 500-rep Monte Carlo size/power test (#[ignore]).
- 5/5 mutation kills executed (sign swap, J_k-1 denominator, dropped
  bias correction, Jmin relaxation, two-sided p).
- PyO3 binding py_dimtest + thin Python wrapper with validate-before-
  cast input checks; exports and pytest coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…group oracle anchors

- Python wrapper now rejects non-numeric (e.g. string) response and index
  arrays via dtype-kind checks BEFORE any astype coercion, with regression
  tests for string responses and string indices.
- Rust: factor dimtest grouping (dimtest_pt_groups) and per-group
  computation (dimtest_group_diag) out of dimtest_stat, and add a test
  pinning all 8x2 per-group intermediates (J_k, mean, sigma_k^2,
  sigma_U,k^2, mu4_k, delta4_k, S_k^2, contribution) for AT1 and AT2
  against the independent oracle at 1e-12, closing the compensating-error
  audit gap.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Implements the standardized answer-similarity index omega for a suspected
copier-source pair: h counts identical observed options, p_i is the
copier's model-implied probability of the source's observed option,
omega = (h - sum p_i)/sqrt(sum p_i (1-p_i)) with a one-sided upper-tail
normal p-value.

Citation governance: formula verified against two independently READ
implementations (CRAN CopyDetect similarity1.r/similarity2.r and the
aberrance package compute_OMG); Wollack (1997) itself NOT read (access
blocked) and cited only as implemented by those sources. CopyDetect's
printed docs flip the sign but both source files use (h-E)/sqrt(V); the
source convention is implemented. Scope: omega only - no g2/GBT/K-index,
no continuity correction, no missing responses.

Rust core mlsirm_core::security::wollack_omega with full input
validation; PyO3 py_wollack_omega; Python wrapper fast_mlsirm.
wollack_omega with pre-cast dtype validation (complex/string/fractional
rejection). Tests: pinned independent oracle at 1e-12 (p at 5e-7 via
crate erfc), structural single-item-extension invariant, error paths,
500-rep Monte Carlo size/power (#[ignore]); 3 executed mutation kills
(V-vs-sqrt(V), copier-prob lookup, two-sided p).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ance authorship

Impl-review round 1 found: (MAJOR) the Python wrapper silently accepted
boolean response arrays as option indices 0/1 — bool dtype is now
rejected before any cast, with a regression test; (MINOR) the aberrance
package citation claimed authors/year not established from the sources
actually read — the citation now names only the package and the read
source files.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Review round 2 (MINOR): the sign-conflict disclosure existed in the PyO3
docstring, Python docstring, and CHANGELOG but not in the Rust module
documentation. All four surfaces now carry it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Add K-index answer-copying statistic (CopyDetect-faithful port)

Implement the K-index of matching incorrect answers exactly as in the
CRAN CopyDetect package's internal k() (R/similarity1.r, READ;
corroborated by R/similarity2.r): number-incorrect subgroup keyed on the
copier's score (including the copier and, when scores match, the source
- CopyDetect convention, source exclusion NOT applied), p =
mean(emp_agg)/ws, and the binomial upper tail P(Bin(ws, p) >= m)
computed with a coefficient-free f64 term recurrence (no factorial
overflow, smaller-tail summation, [0,1] clamp).

Holland (1996, ETS RR-96-07) and Sotaridona & Meijer (2002, JEM 39(2))
were NOT read; the statistic is cited only as implemented by CopyDetect.
Sotaridona & Meijer (2001, RR-01-07, ERIC ED467373) was read for
background corroboration of the binomial framework only. The aberrance
package was checked and contains no K-index.

Rust core mlsirm_core::security::k_index + KIndexResult; PyO3
py_k_index; Python fast_mlsirm.k_index with pre-cast validation
(complex/bool/non-binary rejected, ws==0 degenerate error). Tests:
pinned oracle (independent exact-binomial Python oracle, all 7 result
fields), subgroup-membership invariants (copier always included; source
included when its score matches), degenerate branches (m==0 -> K=1,
p==1 -> K=1), error paths, and a 500-rep Monte Carlo (#[ignore])
checking null size < 0.10 and power > 0.5 under 90% full-response
copying. Three executed mutation kills documented: tail flip, p
denominator wc-for-ws, subgroup keyed on ws.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Fix binomial tail underflow in k_index at extreme p / large n

The linear-space term recurrence started from (1-p)^n, which underflows
to zero for cases like n=1000, p=0.99 (K wrongly returned 0 instead of
0.58304080330109709). Sum the upper-tail terms directly in log space
(log-term recurrence + max-shifted exponential sum): no complement
subtraction, no cancellation, no underflow. Regression test pins the
extreme-p case against an exact fractions.Fraction reference computed
on the exact binary value of 0.99, plus the moderate oracle case.

Found by adversarial implementation review.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@seonghobae
seonghobae force-pushed the seonghobae-wollack-omega branch from d15abdc to c20a2f2 Compare July 26, 2026 09:52
@seonghobae
seonghobae dismissed opencode-agent[bot]’s stale review July 26, 2026 09:59

Dismissing stale bot request after branch rebuild and successful required checks.

@seonghobae
seonghobae merged commit 8c49774 into main Jul 26, 2026
33 checks passed
@seonghobae
seonghobae deleted the seonghobae-wollack-omega branch July 26, 2026 09:59
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