Skip to content

feat: derive curation outcomes instead of paying the judge to guess them - #304

Merged
Barsoomx merged 8 commits into
masterfrom
feat/curation-outcome-derivation
Jul 29, 2026
Merged

feat: derive curation outcomes instead of paying the judge to guess them#304
Barsoomx merged 8 commits into
masterfrom
feat/curation-outcome-derivation

Conversation

@Barsoomx

Copy link
Copy Markdown
Owner

What

The curation judge no longer chooses an outcome. It reports a relation per shortlist
target; the code derives the outcome from those relations plus deterministic facts.

judge_policy_denied becomes impossible by construction.

Why

_apply_evidence_policy admits a model-chosen outcome only when it satisfies predicates
over facts the model cannot fully observe — including latest_evidence_at ordering, which
is not in the prompt at all. The admissible set is computable before the call, yet we
asked the model to guess into it and paid for every guess.

Measured on the dogfood stand:

signal value
judge_policy_denied in 6h 1380
denial site (3h sample, n=345) 100% the terminal if not ok
curation calls / decisions, 3d 931 / 289 — 69% of paid inference discarded
curation share of all spend, 24h $3.64 of $5.44
revise_memory / supersede_memory / open_conflict, all time 0 / 0 / 0
candidate_decision works in terminal_failure 2918

The denial is deterministic — the same candidate against the same corpus is denied
identically forever — but it is classified PROVIDER_TRANSIENT, so every denial retries
and buys another discarded call.

Two things the measurements refuted

Brute-forcing the policy over every (outcome, relation, target, applicability,
temporal_order) tuple for 30 stuck works, with zero judge calls:

candidate tier      : supported x30      (never corroborated, never none)
comparison_complete : True x30
reachable outcomes  : 16/30  merge | publish | reject/redundant
                      14/30  the same plus open_conflict
  1. The corpus is fully embedded. The embedding cliff is not the current cause; the
    feasibility precheck here is insurance against a regression, not the main saving.

  2. revise_memory and supersede_memory are structurally unreachable. They need
    candidate_tier == 'corroborated', and every distillation candidate comes from exactly
    one window (4243 of 4243), which the independence union-find collapses to one group:

    curation decisions by evidence_tier: supported -> 1333, corroborated -> 0
    

    Two of six outcomes are dead code for the only candidate producer that exists. Whether
    single-window multi-observation evidence should corroborate is a separate design
    question and is not changed here.

How

curation_derivation.py is pure and total. Eligibility rules mirror _apply_evidence_policy
conjunct for conjunct. The ladder, in priority order:

  1. open_conflict — a contradiction blocks every other action
  2. supersede_memory
  3. revise_memory
  4. merge_evidence
  5. reject_candidate / redundant — a redundant target means publishing would duplicate
  6. publish_new
  7. reject_candidate / unsupported

Ties within a rung resolve to shortlist order, which is already part of manifest_hash.

The safety claim is that the derivation is a strict restriction of the old policy:
_apply_evidence_policy is kept and still runs on the derived verdict, as an assertion.
The property tests prove it never fires — exhaustively over the lattice (relations ×
candidate tier × target tier × comparison_complete × conflict × visibility × precedence ×
applicability, for one- and two-target shortlists), not by sampling.

Also proven exhaustively:

  • never mutate cross-visibility, under open conflict, or without both tiers supported
  • comparison_complete=True ⟹ derivation is total (never returns "no decision")
  • therefore "no decision" ⟹ the corpus was incomplete, so curation_infeasible is
    INFRASTRUCTURE_TRANSIENT: it retries with backoff, for free, and self-heals

JudgeCurationCandidate.execute computes the feasible set before resolving a policy or
building a prompt, and raises without calling the provider when it is empty.

This slice changes only how the same response bytes are interpreted — no prompt or wire
change. Fields the model still emits but we now ignore get trimmed in slice 2, which is
where the token saving lands.

Eval

The frozen corpus is a CI gate (backend.yml). Exactly one existing case changed meaning:
fault-policy-004 expected a provider fault from a policy denial, which can no longer
happen. It is replaced by a genuinely-still-faulting case (fault-comparisons, a comparison
outside the shortlist manifest), and the semantic content it was testing becomes a new
safe_supersession case: a zero-evidence candidate claiming supersession must derive
reject_candidate and must never be destructive. No threshold was weakened.

The corpus as committed could not see the new decision surface. Every case had 0 or 1
shortlist entries ({1: 86, 0: 35}) while production shortlists carry 4 ({4: 30} across
30 sampled works), so cross-target rung ordering — the entire behaviour the ladder
introduces — had zero coverage. An earlier draft of this PR wrongly named the eval as the
guard for the last risk below. A multi_target_ladder bucket (minimum 5) closes it, and
every case pins what the LADDER derives against a model that said something else:

case model said ladder derives
ladder-rung-order-000 reject/redundant on A merge_evidence on B — rung 4 outranks rung 5
ladder-conflict-first-001 merge on B open_conflict on A
ladder-cross-visibility-002 merge on a cross-visibility target publish_new, no target
ladder-tie-order-003 merge on B merge on A — shortlist order
ladder-unsupported-duplicate-004 reject/redundant on a tier-none target publish_new

All twelve thresholds stay at their bound with these added.

Risks

  • A false equivalent no longer overwrites — because fix: make merge_evidence attach evidence instead of replacing content #303 landed first. Derivation
    removes the pressure to misreport an outcome, not the ability to misjudge a relation.
    merge_evidence used to route through _execute_candidate_revision and set
    memory.title/memory.body from the candidate: a content replacement wearing the name
    "merge". It fired 69 times all-time only because most attempts were denied; this change
    puts it at rung 4, reachable for every supported candidate, against a backlog of 5,824
    mostly-duplicate proposed candidates. Shipping this alone would have knowingly amplified a
    destructive path, so fix: make merge_evidence attach evidence instead of replacing content #303 made MERGE evidence-only first and is already in master. Both
    deploy together.

  • The revision signal degrades to a duplicate. Since candidates are always supported,
    candidate_revises can never reach its rung and falls through to publish_new. The old
    gate admitted exactly this state, so the restriction invariant holds and nothing is
    overwritten, but the corpus accumulates near-duplicates instead of revisions.
    suppressed_identity_relations is logged to measure it.

  • open_conflict is structurally unreachable, so contradictions publish silently.
    _conflict_eligible requires ¬deterministic_precedence — the two latest_evidence_at
    equal or missing — and in production both are always populated and essentially never
    equal. This mirrors the old gate exactly, so the unreachability is pre-existing, but the
    consequence changes: a mutually_incompatible used to be denied and terminalise, writing
    nothing; now it falls through to publish_new. Widening the rung would break the
    restriction invariant, so it is deliberately not done here. Tracked as B-008.

  • applicability is top-level in v1 but the ladder is per-target. The conservative
    direction is safe; the unsafe one (model said same about target A, ladder acts on B) is
    narrow because the ladder picks the strongest identity relation, usually the model's own
    choice. Slice 2 moves it into the comparison object.

  • judge_cross_visibility_denied becomes unreachable from the parse path. The ladder
    never selects a cross-visibility target for a mutation rung, so that terminal
    INVALID_INPUT stop cannot fire. On redrive, works that previously died there now derive
    publish_new and create a memory — a data-creating change applied to an existing backlog,
    which is why the redrive is batched with the outcome mix checked between batches.

  • Derivation may act more destructively than the model asked. The model's own outcome
    is ignored, so an identity relation on a target it did not select now drives the outcome
    ladder-rung-order-000 is exactly that case. Post-fix: make merge_evidence attach evidence instead of replacing content #303 this is no longer a content
    overwrite; it is evidence attached to a memory the model itself called equivalent. It is
    now a pinned expectation rather than an emergent accident.

Redrive protocol

write_exact_memory_projection looks the document up by memory_version_id, so every write
outcome creates a new version, a new RetrievalDocument, and therefore one with
embedding_pgvector NULL until its embedding work lands — while corpus_fully_embedded is
False if any current document in the authorized corpus is unembedded. Every successful
decision briefly sets comparison_complete=False project-wide. Invisible at low volume;
under a mass redrive it would stay false continuously, starving the publish_new rung and
making derive_decision return None after the call was paid for. The ~2918 terminal
works are therefore redriven in bounded batches, draining embeddings between them.

@Barsoomx

Copy link
Copy Markdown
Owner Author

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@codecov-commenter

codecov-commenter commented Jul 29, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

Affected Assets, Files, and Routes:

view changes for bundle: engram-frontend-client-array-push

Assets Changed:

Asset Name Size Change Total Size Change (%)
server/middleware-*.js 2.72kB 3.21kB 548.89% ⚠️
server/middleware-*.js -2.72kB 495 bytes -84.59%
static/yNEP1WCpBnKGkybef25nZ/_buildManifest.js (New) 1.51kB 1.51kB 100.0% 🚀
static/yNEP1WCpBnKGkybef25nZ/_ssgManifest.js (New) 77 bytes 77 bytes 100.0% 🚀
static/nJfhUpqTZsXO0sPS-*.js (Deleted) -77 bytes 0 bytes -100.0% 🗑️
static/nJfhUpqTZsXO0sPS-*.js (Deleted) -1.51kB 0 bytes -100.0% 🗑️

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.09%. Comparing base (ee7c594) to head (476bdf0).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #304      +/-   ##
==========================================
+ Coverage   91.05%   91.09%   +0.04%     
==========================================
  Files         222      223       +1     
  Lines       23201    23344     +143     
==========================================
+ Hits        21126    21266     +140     
- Misses       2075     2078       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@Barsoomx
Barsoomx merged commit e6b104f into master Jul 29, 2026
14 checks passed
@Barsoomx
Barsoomx deleted the feat/curation-outcome-derivation branch July 29, 2026 17:37
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