Skip to content

novelty: placebo-gated added, noise floor, atom-clustered CI, context framing - #418

Open
napetrov wants to merge 2 commits into
intel:mainfrom
napetrov:feat/novelty-review-controls
Open

napetrov wants to merge 2 commits into
intel:mainfrom
napetrov:feat/novelty-review-controls

Conversation

@napetrov

@napetrov napetrov commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

novelty: placebo-gated added, noise floor, atom-clustered CI, context framing

Problem: a review of the novelty study asked four things the 2x2 could not answer: is "added" above the placebo, above run-to-run noise, how wide is the CI when paraphrases of one atom are correlated, and is placebo abstention an artefact of the "Reference material" wording.

Change:

  • new dokimi/novelty/controls.py: placebo_gated, noise_floor, clustered_ci (atom resampling), control_fields
  • paired_outcomes: ci95_added is atom-clustered (ci_cluster: "atom"); adds added_over_placebo when placebo arm ran; adds noise_floor / added_net_of_noise / degraded_within_noise when the new no_artifact_repeat arm ran (asked with draw=1 so the response cache gives a fresh sample)
  • run_measurement(framing=reference|neutral); AnalyzeConfig.context_framing recorded in run config; CLI --context-framing on analyze/probe; preambles named *_PROMPT so prompts_sha256 covers them
  • shuffle_text docstring no longer cites the biased study result
  • estimate: repeat arm not priced as context-bearing
    Evidence: tests/test_novelty_controls.py; reviewer rerun numbers in docs.

Local review: two self-review passes (design lens, finder and verifier with probes); the blocking parity finding is fixed. mypy, bandit and zizmor did not run locally, and some Windows-only test failures are the same on main.

🤖 Generated with Claude Code

napetrov and others added 2 commits November 5, 2026 21:36
…text framing

Adds the controls a reviewer asked of the novelty 2x2:
- added_over_placebo when the placebo arm ran
- no_artifact_repeat arm (new cache draw) -> noise_floor, added_net_of_noise,
  degraded_within_noise
- ci95_added resamples atoms (ci_cluster: atom)
- --context-framing reference|neutral on novelty analyze/probe

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bo-as-with arm, docs)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@napetrov
napetrov requested a review from a team as a code owner November 6, 2026 06:04
Copilot AI balanced review requested due to automatic review settings November 6, 2026 06:05
@exai-bot

exai-bot commented Nov 6, 2026 •

Copy link
Copy Markdown

🔍 OpenCodeReview found 3 issue(s) in this PR.

CI at `b3d97e2`: 3 failing, 1 waiting for approval, 8 passing, 4 skipped
Check Status Assessment
Build docs (strict) failing at step mkdocs build Caused by this PR. The newly added noise-floor sublist uses two-space indentation without a preceding blank line, producing a list-depth warning that aborts strict MkDocs.
Static checks (ruff, schema, zizmor, licenses, pre-commit, bandit, secrets, changelog, schema com... failing at step License header check Caused by this PR. License checking explicitly rejects both newly added Python files because they lack the required copyright/SPDX header. Add the same two-line header to controls.py as well as the test file fixed below.
mypy failing at step CLI snapshot is current Caused by this PR. The CLI snapshot step, not a reported type error, fails because the new --context-framing option appears in two command surfaces but the committed snapshot was not updated. Run make cli-snapshot in dokimi/ and commit the generated result.
DCO waiting for approval Caused by this PR. Both branch commits, including the reviewed commit, lack Signed-off-by trailers; this is a commit-metadata issue rather than a source edit.

By severity: 3 medium

Severity Category Location Finding
medium bug controls.py:122-123 Must-fix — Impact: these derived metrics can report noise-adjusted gains or dismiss degradation using unrelated probe populations.
medium bug measure.py:446 Must-fix — Impact: novelty analyze silently loses all new control metrics and writes atom-clustered intervals without their identifying...
medium bug novelty.py:230 Must-fix — Impact: saved probe results cannot distinguish the reference and neutral framing experiments, undermining comparison of the pl...
Review details
  • Commit reviewed: b3d97e2 against merge base 0e993b9
  • Coverage: 8 files in 2 groups
  • Engine: OpenCodeReview v1.13.5-exai.3.d7112371, us.openai.gpt-6.1-sol
  • Usage: 539k tokens (386k from cache), 2m12s
  • Tool calls: 35 (18 file_read, 7 code_search, 5 ci_log_search, 3 code_comment, 1 ci_status, 1 file_read_diff)
  • Novelty review controls implementation, tests, and documentation: 7 files
  • Growth baseline update: 1 file

arms=_arms(args.arms), concurrency=args.concurrency,
allow_self_judge=args.allow_self_judge, seed=args.seed,
placebo_text=placebo_text,
placebo_text=placebo_text, framing=args.context_framing,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix — Impact: saved probe results cannot distinguish the reference and neutral framing experiments, undermining comparison of the placebo-abstention control. Trigger: run novelty probe with --context-framing neutral and save its output. Evidence: framing is passed only to measurement; build_probe_record() writes no framing/config field, and the saved runs contain questions and responses, not the context preamble. Thus neither the output nor its artifact digest identifies the chosen framing. Persist the framing in the probe record (and test both choices). Workaround: use analyze, which records neutral framing, or track probe flags externally. Severity justification: medium because this loses experiment provenance for the newly supported probe control, without affecting unrelated measurements.

Comment on lines +122 to +123
fields["added_net_of_noise"] = round(rates["added"] - floor["added"], 4)
fields["degraded_within_noise"] = rates["degraded"] <= floor["degraded"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix — Impact: these derived metrics can report noise-adjusted gains or dismiss degradation using unrelated probe populations. Trigger: asymmetric full/repeat arm errors. Evidence: paired_outcomes intersects baseline with full, whereas noise_floor independently intersects baseline with repeat. For two initially wrong probes, let full answer a correctly and error on b, while repeat errors on a and answers b wrongly: this reports added_net_of_noise=1.0 despite having no successfully answered three-arm comparison. The same population mismatch affects degraded_within_noise. Compute both compared rates on the common baseline/full/repeat intersection, and omit the derived metrics when that intersection is empty; retain the standalone floor if useful. Workaround: use these derived fields only when all three arms have the same usable probes. Severity justification: medium because this is a bounded but material measurement correctness failure. Add an asymmetric-error regression test; the current tests cover only complete populations.

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.

Agreed, and the empty case is worse: when every full answer errors, rates are the 0.0 placeholders and the output still says added_net_of_noise: -0.1, degraded_within_noise: true (n_paired 0, floor 0.1/0.05). That is a pass for a degradation nobody measured. Computing both on the common baseline/full/repeat probes and omitting the two fields when that set is empty fixes both cases.

for atom in per_atom.values() for n in atom]),
# Atoms are resampled: the paraphrases of one atom are not independent probes.
"ci95_added": clustered_ci(added_by_atom),
"ci_cluster": "atom",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix — Impact: novelty analyze silently loses all new control metrics and writes atom-clustered intervals without their identifying marker. Trigger: any analyze run (including --arms no_artifact,full,placebo,no_artifact_repeat). Evidence: analyze() passes the measurement to report.build_novelty_artifact(), whose paired projection (report.py lines 100–110) copies only the old seven fields, dropping ci_cluster, added_over_placebo, noise_floor, added_net_of_noise, and degraded_within_noise. This also makes the documented rule that records without ci_cluster have probe-level intervals incorrect for new analyze outputs. Update that projection to preserve the new fields and test the persisted analyze artifact, not only paired_outcomes. Workaround: use the probe output or inspect paired_outcomes directly. Severity: medium because the advertised controls are absent from the main command's saved results, while the underlying measurement still runs.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

2 open issues
What changed in this PR

This PR adds reviewer-requested control metrics and prompt framing options to the novelty measurement pipeline, improving interpretability of the added signal by separating placebo/context effects, run-to-run noise, and correlated paraphrase uncertainty.

Changes:

  • Introduces placebo gating, a baseline repeat “noise floor”, and atom-clustered bootstrap confidence intervals.
  • Extends measurement/analyze/probe flows with --context-framing reference|neutral, recording framing in run config and prompt hashing.
  • Updates docs/changelog and adds a focused test suite for the new controls.
File Description
dokimi/​tests/​test_novelty_controls.py Adds tests covering placebo gating, noise floor, clustered CI, and neutral framing validation.
dokimi/​scripts/​growth_baseline.json Updates growth baseline metadata to reflect novelty command growth from new flags/arm.
dokimi/​dokimi/​novelty/​measure.py Adds repeat arm + framing support; switches CI to atom-clustered; integrates control field reporting.
dokimi/​dokimi/​novelty/​controls.py Implements placebo-gated added, noise floor, and atom-clustered bootstrap CI helpers.
dokimi/​dokimi/​novelty/​analyze.py Records context_framing in config and passes it through to measurement.
dokimi/​dokimi/​commands/​novelty.py Adds --context-framing; adjusts cost estimation for the repeat arm.
dokimi/​docs/​skill-novelty.md Documents placebo gating, noise floor outputs, clustered CI semantics, and framing usage.
dokimi/​changelog.d/​20261105_120000_napetrov_novelty-review-controls.md Changelog entry for controls, clustered CI, framing, and prompt hash changes.

Review effort: Lite

Comment on lines 84 to +87
n_arms = len(_arms(args.arms))
estimate["expected_usd"] = _estimate_usd(
args, n_tokens, estimate["estimate"]["by_role"], settings.tier,
context_share=(n_arms - 1) / n_arms,
context_share=(n_arms - 1 - ("no_artifact_repeat" in args.arms)) / n_arms,
Comment on lines +35 to +44
clusters = [values for values in per_atom.values() if values]
if len(clusters) < 5:
return None
rng = random.Random(f"novelty-ci-{seed}")
n = len(clusters)
means = []
for _ in range(iterations):
picked = [clusters[rng.randrange(n)] for _ in range(n)]
total = sum(len(c) for c in picked)
means.append(sum(sum(c) for c in picked) / total)

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.

Not worth the extra code: clustered_ci over 300 atoms of 3 paraphrases takes about 30 ms, and runs probe far fewer atoms.

@vlad-perevezentsev vlad-perevezentsev 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.

Review at b3d97e2. The controls are the right ones and the repeat arm really gets a new cache draw, but the new fields are not ready to read yet: novelty analyze drops them from its artifact, and two of them turn "not measured" into a number or a pass. CI is red for reasons from this PR; the details are in the line comments.

Agreed with @exai-bot: the analyze artifact loses the control fields and ci_cluster; novelty probe does not record the framing; the noise fields mix probe sets, and I added the empty case in its thread.

Agreed with @copilot-pull-request-reviewer on parsing --arms once; on precomputing cluster sums I disagree and replied in its thread.

What I ran, at b3d97e2:

  • make ci-local --offline: 15,796 passed, 3 failed, 63 skipped. The CLI snapshot, the flag registry (--context-framing) and the growth rationale fail in the tests; the strict docs build fails on the noise-floor list. All four come from this PR.
  • A repro of paired_outcomes with the full arm all errored, and with a placebo that overlaps no other arm: the output shows degraded_within_noise: true and added_over_placebo.rate: 0.0 with n_paired: 0.

Checked, no issues

  • Repeat arm: draw=1 reaches the default transport and so the response cache key; reversal probes and reversal_resisted stay on the with-artifact arm only.
  • Cost estimate: the repeat arm is priced without the artifact, which is right.
  • Schema: novelty.v1 allows extra keys in paired, so adding the control fields there needs no new version.
  • Framing: reference gives the same prompt as before; neutral is in run_config only when set, so older reference cells keep the same config.

{
"files": {
"dokimi/commands/novelty.py": {"lines": 1021, "rationale": "estimate-only projection and W1 routing cost output (#164) from main, plus the #172 routing field, --all-vs-all, tied/unscored caveat and blind-skill warning, --all-vs-all with --repeats (noise and cost block); routing output in commands/novelty_routing_report.py; file split tracked separately"},
"dokimi/commands/novelty.py": {"lines": 1026, "rationale": "--context-framing and the no_artifact_repeat arm help (review controls); estimate-only projection and W1 routing cost output (#164) from main, plus the #172 routing field, --all-vs-all, tied/unscored caveat and blind-skill warning, --all-vs-all with --repeats (noise and cost block); routing output in commands/novelty_routing_report.py; file split tracked separately"},

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.

CI fails on this branch for five reasons, all from this PR.

  • docs/reference/cli-interface.json is stale: run make cli-snapshot.
  • test_every_spelling_on_the_tree_is_canonical_or_allowlisted: --context-framing is not in cli_flags.py nor in tests/data/cli_flags_allowlist.txt.
  • test_growth_ratchet: this rationale is over 300 characters.
  • strict mkdocs: the nested list under Noise floor in skill-novelty.md needs 4-space indent and a blank line before it.
  • controls.py and test_novelty_controls.py have no SPDX header.

return {
"n_paired": n,
"count": count,
"rate": round(count / n, 4) if n else 0.0,

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.

added_over_placebo reports rate: 0.0 when no probe was answered in all three arms.

With full errored on the probes the placebo answered (or the reverse), paired_outcomes returns {"n_paired": 0, "count": 0, "rate": 0.0, "ci95": None}, which reads as "no gain beats the placebo". Nothing was measured. noise_floor already returns None for this case.

Suggestion: "rate": round(count / n, 4) if n else None (or omit the key), and a test with no three-arm overlap.

Comment on lines +568 to +570
A probe asks for one fact, so it cannot punish a scrambled document: expect this arm
near ``full``. The study's shuffled arm scored higher, but it injected more files
than ``full`` did, so that result does not carry over to this arm.

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.

The PR retracts the shuffled-arm claim here, but users still see it elsewhere.

The order_independence gate message (gate/novelty_gate.py:635, "+0.022 (CI -0.027 to +0.072)"), the _order_independent docstring (report.py:189, "four of five models") and skill-novelty.md ("Investment in prose order") still cite it. The removed text also dropped the 71-task behavioural retest, which is a separate result.

Suggestion: say which part no longer holds, and update the gate message and the docs to match.


### Changed

- **`ci95_added` resamples atoms rather than probes** (`ci_cluster: "atom"`). Paraphrases of one atom are correlated, so the old interval was too narrow.

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.

The clustered interval needs 5 atoms where the old one needed 5 probes, and the fragment does not say so.

A run of 4 atoms with 3 paraphrases each used to get ci95_added; now it gets null. The new intervals are also wider, so ci95_added from older cells is not comparable with new ones.

Suggestion: add both to this entry, so a reader knows what to re-run.

framing is recorded in the run config and is covered by `prompts_sha256`.

```bash
python cli.py novelty analyze --artifact skill:s --model M --arms no_artifact,full,placebo,no_artifact_repeat --placebo-text other-domain.md --context-framing neutral

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.

Nit: The line breaks of this command were lost (--model M --arms ... --context-framing). Put it on separate lines with \ continuations, like the other examples.

@antonwolfy antonwolfy 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.

Not ready to merge. The controls are the right ones, but the new fields are not yet safe to read, and CI is red for reasons from this PR. Two test gaps are inline.

I agree with @vlad-perevezentsev's points on the novelty analyze artifact dropping ci_cluster and the control fields (report.py:100-110), the mixed probe populations in the noise fields, added_over_placebo reporting rate: 0.0 for n_paired: 0, the shuffled-arm claim still cited in the gate message and docs, the changelog's missing 5-atom minimum, and the broken example line in the docs.

@vlad-perevezentsev's points on the CI failures hold at this head. The CLI snapshot is stale, --context-framing is neither in the flag registry nor the allowlist, the growth rationale in growth_baseline.json exceeds 300 characters, the strict docs build fails on the noise-floor list, and controls.py and test_novelty_controls.py have no SPDX header.

@napetrov's point that d1a87df and b3d97e2 have no Signed-off-by waits for the author; the DCO check stays red until then.

What I ran, on a Linux review host at b3d97e2:

  • make ci-local: 27 passed, 3 failed, 7 skipped. The failures are the CLI snapshot, the strict docs build and Run tests. In the test run, 15,792 passed. The failures from this PR are the snapshot test, the flag registry test and the growth ratchet. Two audit tests timed out and one hook test took 2.0 s against a 1 s limit; none of them touches the files this PR changes.
  • A mutation run over controls.py, measure.py and analyze.py: 30 mutants, 15 killed by the new tests alone and 16 with the neighbouring novelty tests. Survivors are in the inline comments.

Open bot points I confirm:

  • I agree with exai-bot that novelty analyze drops the new control fields and ci_cluster from the saved artifact. New evidence: report.py:100-110 copies only added, redundant, degraded, still_missing, n_paired, n_unpaired_dropped and ci95_added.
  • I agree with exai-bot that noise_floor and paired_outcomes use different probe populations, so added_net_of_noise and degraded_within_noise can come from unrelated probes. Both should use the common baseline, full and repeat set and be omitted when it is empty. This is from reading controls.py; I did not rerun the empty-set repro.
  • I agree with exai-bot that novelty probe does not record --context-framing, so a saved probe result cannot say which framing produced it.

Checked, no issues

  • Repeat arm: draw=1 reaches LLMClient.text, the transport and the cache key. Draw 0 leaves the key unchanged, so old cache entries still hit.
  • Reference framing: the Reference material: prompt is byte-identical to before, and run_config gains a key only for neutral.
  • Old bootstrap: _bootstrap_ci has no remaining callers, and random is still used by shuffle_text.
Bot points I checked that no longer apply at this head (2)
  • Copilot's point on the args.arms substring test does not apply: no_artifact_repeat is not contained in any other arm name, so the test gives the same answer as list membership.
  • Copilot's point on precomputing cluster sums does not apply: clustered_ci over 300 atoms of 3 paraphrases takes about 30 ms, and the result would be identical.

answerer=answerer, judge=judge, arms=config.arms,
concurrency=config.concurrency, allow_self_judge=config.allow_self_judge,
placebo_text=config.placebo_text, seed=config.seed,
placebo_text=config.placebo_text, seed=config.seed, framing=config.context_framing,

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.

No test pins that analyze passes the framing to the measurement or records it in the run config.

Mutation check: removing , framing=config.context_framing here, and separately changing framing = {} if config.context_framing == "reference" else {...} (analyze.py:85) to if True, both pass the new tests and the 357 neighbouring novelty tests (analyze, measure, cli, report, gate). With the first mutant, novelty analyze --context-framing neutral silently runs the reference prompt. With the second, cells with different framings look comparable. The flag exists only for the placebo-abstention control.

Suggestion: add a test that runs analyze with context_framing="neutral" and a recording transport, asserts the prompt contains the neutral preamble, and asserts run_config holds context_framing: "neutral" while the default run has no such key.


def _usable(runs, arm: str) -> dict:
return {_key(r): r for r in runs
if r.arm == arm and r.probe_kind != "reversal" and not r.errored}

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.

The guards and the interval bounds that decide reported measurements are untested.

Mutation check, each passing the full new test file and the neighbouring novelty tests:

  • _usable no longer excluding errored runs (controls.py:24): errored placebo and repeat answers then count as wrong and inflate added_over_placebo.count and noise_floor.added.
  • Dropping the with_arm == PLACEBO_ARM guard (controls.py:58).
  • Flipping degraded <= floor in degraded_within_noise: the test data has degraded == floor degraded, so only equality is pinned.
  • Swapping the floor's ci95_added flips to right-then-wrong.
  • Changing the < 5 cluster threshold to < 8, the lo/hi percentile indices, the seed, or the cluster weighting. The tests only check "wider than flat" and None for very few clusters.

Suggestion: add cases with an errored probe in the placebo and repeat arms, a case with degraded strictly below the floor, and one exact ci95 for a fixed small input, plus a 5-cluster case that must return an interval.

This branch has not been deployed

No deployments
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.

5 participants