Files
gbrain/docs/proposals/temporal-contradiction-probe.md
T
dd1cc121d8 v0.35.3.1 feat(eval): temporal-aware contradiction probe + verdict enum (#1052)
* rfc: temporal axis for contradiction probe

Field report on residual HIGH findings from gbrain eval suspected-contradictions
and proposal for a 4-phase fix (Phase 1 = judge prompt + verdict enum is the
recommended starting point).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(eval): pass effective_date to judge prompt; bump PROMPT_VERSION

Lane A1 of the temporal-contradiction-probe wave. Threads page-level
effective_date through the search projection into the contradiction judge so
the LLM can reason about supersession instead of treating every dated pair as
a contradiction.

Changes:
- SearchResult interface adds optional effective_date + effective_date_source
  fields; rowToSearchResult populates them from the row data with date-only
  YYYY-MM-DD normalization (handles both postgres.js Date and PGLite string).
- 8 SELECT projection sites (3 in postgres-engine, 5 in pglite-engine) now
  carry p.effective_date + p.effective_date_source through their inner CTEs
  and outer SELECTs so search results expose the field on both engines.
- PairMember (eval-contradictions/types.ts) gets the two fields as required
  (string | null) so the type forces every constructor to think about temporal
  anchoring. Runner's searchResultToMember + takeToMember handle the
  normalization; takes inherit the chunk's page-level date.
- buildJudgePrompt emits `Statement A (from: YYYY-MM-DD)` when effective_date
  is non-null, else `(date unknown)`. Prompt instructions explain the tag so
  the model knows what to do with it.
- PROMPT_VERSION bumps '1' → '2'. Cache-key tuple shape unchanged; old rows
  miss naturally on first run against the new prompt.

Test fixtures in 5 files updated to include the new required fields. All 205
eval-contradictions unit tests + 101 search-related tests pass. Typecheck
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(eval): replace contradicts:boolean with verdict:enum (6 members)

Lane A2 of the temporal-contradiction-probe wave. Expands the judge's
classification vocabulary from a binary contradicts:bool to a six-member
verdict enum so the probe can distinguish "this changed" from "this is wrong".

Verdict taxonomy:
  no_contradiction       — drop from findings
  contradiction          — genuine conflict at same point in time
  temporal_supersession  — newer claim updates/replaces older; not an error
  temporal_regression    — metric/status went backwards over time (signal)
  temporal_evolution     — legitimate change, neither supersession nor regression
  negation_artifact      — judge misread an explicit negation

Changes:
- types.ts: Verdict union (6 members); Severity gains 'info'; ResolutionKind
  extended with temporal_supersede, flag_for_review, log_timeline_change;
  JudgeVerdict.contradicts → verdict; ContradictionFinding now carries verdict;
  ProbeReport adds queries_with_any_finding + verdict_breakdown (additive).
- judge.ts: parseResolutionKind + parseVerdict guards; normalizeVerdict reads
  the new field and applies the C1 confidence floor only to verdict='contradiction'
  (the new verdicts are informational classifications, no floor). Prompt rubric
  rewritten to ask for verdict + extended severity scale.
- severity-classify.ts: 'info' joins the rank with value 0; defaultSeverityForVerdict
  maps each verdict to its baseline severity (D7 — supersession=info, regression=high,
  etc.). parseSeverity gains a fallback param so consumers can override 'low' default.
- auto-supersession.ts: classifyResolution + renderResolutionCommand handle the
  three new resolution kinds. Probe still NEVER auto-mutates — the new kinds
  render paste-ready commands or informational lines.
- cache.ts: isJudgeVerdict shape check matches the new verdict field; old v1
  rows fail the guard and treat as misses.
- runner.ts: emit predicate at cache-hit and judge-success branches changes
  from `verdict.contradicts` to `verdict.verdict !== 'no_contradiction'`.
  Without this, the new verdicts vanish from the report. Added per-verdict
  tally + queriesWithAnyFinding alongside the strict queriesWithContradiction.
- trends.ts: latest run verdict breakdown surfaces in the trend chart.

Test fixtures updated across 8 test files. All 210 eval-contradictions unit
tests pass. Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(eval): relax date-filter rule 3 when both sides dated

Lane B of the temporal-contradiction-probe wave. The v1 date pre-filter
skipped pairs whose chunk-text-extracted dates differed by >30 days as a
cost-saving heuristic. That heuristic silently killed exactly the cases the
new verdict taxonomy exists to surface — role transitions across years
(e.g. a 2017 historical record vs. a 2025 current state), MRR claims years
apart, status changes recorded over time.

Lane A1+A2 made temporal supersession explicit and cheap to classify. The
filter no longer needs to skip these pairs; the judge can label them.

Changes:
- date-filter.ts: shouldSkipForDateMismatch accepts optional effectiveDateA
  and effectiveDateB. When BOTH are non-null, returns skip=false with the new
  'both_have_effective_date' reason — the judge will see the dates via the
  (from: YYYY-MM-DD) prompt tag from Lane A1. Other rules (same-paragraph
  dual-date override, missing-date fallback) preserved verbatim and still
  run first.
- runner.ts: threads pair.{a,b}.effective_date into the date-filter call.
  Pairs that previously vanished into the skip bucket now reach the judge.

Tests (R1 IRON RULE regression suite, 6 new cases):
- both sides effective_date → not skipped
- both sides effective_date overrides >30d chunk-text rule
- rule 1 (same-paragraph dual-date) still wins over effective_date relaxation
- rule 2 (missing chunk dates) still applies when effective_date partially present
- undefined effective_dates fall through to v1 behavior (back-compat)
- empty-string effective_date treated as missing (only real dates enable the relaxation)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): cost-estimate prompt + --budget-usd + Haiku routing

Lane C of the temporal-contradiction-probe wave. Three layers of cost
guardrail, all stacked:

(a) cost-estimate prompt at probe-run-time. Before the runner spends any
    tokens after a PROMPT_VERSION change, eval-suspected-contradictions
    reads the most recent persisted prompt_version from
    eval_contradictions_runs and compares. When they differ:
      - TTY: prints an upper-bound estimate + Ctrl-C window (default 10s,
        override via GBRAIN_PROBE_PROMPT_GRACE_SECONDS).
      - non-TTY: prints the estimate + auto-proceeds (autopilot path).
      - --yes override or GBRAIN_NO_PROBE_PROMPT=1: skip entirely.
    Mirrors the v0.32.7 runPostUpgradeReembedPrompt pattern.

(b) --budget-usd N hard cap (pre-existing; PreFlightBudgetError surfaces
    when the estimate alone exceeds the cap, and CostTracker halts the
    run mid-flight when cumulative cost exceeds it). Documented in the
    help text alongside (a).

(c) Judge model now routes through resolveModel() with configKey
    'models.eval.contradictions_judge', tier 'utility' (Haiku-class
    default), and env var GBRAIN_CONTRADICTIONS_JUDGE_MODEL. The legacy
    --judge CLI flag still wins as the highest-precedence override.
    Doctor's model touchpoint registry (src/commands/models.ts:50) carries
    the new key so `gbrain models` and `gbrain models doctor` surface it.

Also in this lane:
- CLI: --severity accepts 'info' (the new Severity member from Lane A2).
- CLI: --severity output shows [verdict] tag alongside slug pairs so
  operators distinguish genuine contradictions from temporal classifications.
- Human summary: prints the new queries_with_any_finding metric and the
  per-verdict breakdown table.
- Help text: explains the cost-prompt + budget-cap + model-routing
  interactions in one paragraph.

New tests (9 cases on the cost-prompt helper):
- --yes override skips
- GBRAIN_NO_PROBE_PROMPT=1 skips
- prompt_version unchanged → skips
- non-TTY auto-proceeds with stderr note
- TTY proceeds after grace
- TTY aborts on Ctrl-C
- fresh brain (no prior runs) fires the prompt
- GBRAIN_PROBE_PROMPT_GRACE_SECONDS override honored
- estimate banner contains query count + judge model + dollar amount

All 225 eval-contradictions tests + 25 model-config tests pass. Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(eval): R4/R5/R6 IRON-RULE regressions for the verdict-enum wave

Lane D of the temporal-contradiction-probe wave. The Lanes A1/A2/B/C lanes
landed the behavior; this lane pins the regressions that protect the wave
against future drift.

R4 (runner emit predicate): five new tests, one per non-no_contradiction
verdict, prove the runner.ts emit rule surfaces each one as a finding with
the correct verdict tag, and that:
  - queries_with_contradiction (Wilson-CI denominator) ONLY counts verdict
    ='contradiction' — the strict metric is preserved
  - queries_with_any_finding counts every non-no_contradiction verdict
  - verdict_breakdown tallies correctly
Plus one negative case: verdict='no_contradiction' produces zero findings.
Without R4, a future runner refactor could collapse the new verdicts back
to /dev/null and the report would silently shrink.

R5 (cache key shape): direct shape assertion on buildCacheKey output. The
key tuple is exactly 5 fields (chunk_a_hash, chunk_b_hash, model_id,
prompt_version, truncation_policy). Adding a 6th field would silently break
every operator's brain (no migration path).

R6 (contradiction severity unchanged): four tests on normalizeVerdict pin
the legacy semantics — judge-supplied severity wins (whether 'high' or
'low'), and on garbage severity input the fallback is 'medium' (per
defaultSeverityForVerdict('contradiction')) NOT 'low'. The contradiction
verdict's severity must never default to 'low', which would silently mask
genuine conflicts as cosmetic naming issues. The temporal_regression case
is included for parity (garbage → 'high' since regressions are real
investor red flags).

236 eval-contradictions tests pass (211 + 6 R4 + 1 R5 + 4 R6 + 9 cost-prompt
from Lane C).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ci): privacy lint for docs/proposals/*.md

Captures the residual TODO from the temporal-contradiction-probe wave's
plan: prevent the bug class where an RFC lands in docs/proposals/ with
PII that should never appear in a public technical artifact. The
original RFC had to be scrubbed at force-push time (Step 0); this lint
catches the same patterns at CI time so the next one can't slip through.

Sibling to scripts/check-privacy.sh:
- check-privacy.sh: bans the literal "Wintermute" repo-wide.
- check-proposal-pii.sh: focuses on docs/proposals/*.md and the OTHER
  PII classes — personal-relationship vocabulary, private repo refs.

Design contract: the denylist names PATTERNS, not real people. Naming
specific real names (deceased relatives, therapist first names,
dealflow contacts) inside this script would leak PII into the repo
just by appearing here. The structural patterns below catch the
SURROUNDING vocabulary that always accompanies such content in
personal RFC prose. Trade-off: a future RFC that names a real person
without any contextual markers won't be caught — accepted as residual
risk handled by human review.

Patterns flagged in docs/proposals/*.md:
- garrytan/brain (private repo reference)
- trial separation, permanent separation
- couples session, couples therapist
- divorce attorney(s)
- grandmother's funeral, aunt's funeral
- wintermute (also caught by check-privacy.sh; listed here for
  proposal-scoped clarity)

Bare common words (separation, funeral) are NOT banned — only the
combined personal-context phrases. "Separation of concerns" and other
software vocabulary survives.

Wired into:
- `bun run verify` (gates every push)
- `bun run check:all`
- `bun run check:proposal-pii` (standalone)

Tests: 15 cases in test/scripts/check-proposal-pii.test.ts.
- Each pattern flagged when present, plus exit-code + stderr signal.
- Two negative cases (separation-of-concerns, funeral metaphor) prove
  the lint doesn't false-positive on legitimate software prose.
- No-proposals-dir → exit 0 (not a failure).
- Multi-hit case proves all patterns surface together with a summary
  count.
- The two test fixtures that name "Wintermute" / "WINTERMUTE" as
  sentinel literals are allowlisted in check-test-real-names.sh per
  the same meta-rule-enforcement exception as check-privacy.sh itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(privacy): allowlist new privacy-guard files in check-privacy.sh

check-privacy.sh bans the literal Wintermute repo-wide. The two new files
from the v0.34 privacy lint (scripts/check-proposal-pii.sh and its test)
necessarily name the token to do their job. Same meta-rule-enforcement
exception as scripts/check-privacy.sh itself, scripts/check-test-real-names.sh,
test/recency-decay.test.ts, and the existing entries — describing what
the rule forbids requires naming it.

Without this allowlist, `bun run verify` fails on check:privacy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v0.35.1.0)

Temporal-contradiction-probe wave — Phase 1 of the RFC at
docs/proposals/temporal-contradiction-probe.md.

Headline: the contradiction probe now classifies pairs into a 6-member
verdict enum (no_contradiction, contradiction, temporal_supersession,
temporal_regression, temporal_evolution, negation_artifact) and sees the
page-level effective_date for each chunk via a (from: YYYY-MM-DD) tag in
the prompt. The pre-judge date filter no longer skips dated wide-gap pairs,
so the role-transition class (e.g. a 2017 historical record vs. a 2025
current state) reaches the judge and gets classified as
temporal_supersession instead of vanishing into the skip bucket.

PROMPT_VERSION bumped 1 → 2 (cache fully invalidated). Three-layer cost
guardrail: TTY-only cost-estimate prompt with Ctrl-C window, --budget-usd
hard cap, Haiku-tier routing via new models.eval.contradictions_judge
config key.

Also adds a CI privacy lint (scripts/check-proposal-pii.sh) wired into
bun run verify that catches PII patterns in docs/proposals/*.md so future
RFCs can't ship with personal-context vocabulary the way this wave's
source RFC did at draft time.

Phases 2-4 deferred to follow-up RFCs per the plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: garrytan-agents <garrytan-agents@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 08:32:03 -07:00

9.5 KiB
Raw Blame History

Proposal: Temporal Axis for Contradiction Probe

Status: Report / RFC Date: 2026-05-14 Context: A large production run of gbrain eval suspected-contradictions surfaced ~115 HIGH findings. Walking through them by hand exposed a structural limitation in the probe.

The Problem

The contradiction probe (gbrain eval suspected-contradictions) treats all claims as timeless. When two chunks make conflicting statements, the judge flags a contradiction regardless of whether both statements were true at their respective points in time.

This worked fine when the brain was mostly static wiki pages. It breaks now that the brain contains:

  • Conversation transcripts with claims that were true when spoken
  • Meeting pages capturing what people said on specific dates
  • Takes that evolve (a founder's ARR claim in January vs. July)
  • Status records that supersede each other (a state moves from "trial" to "confirmed")

The probe can't distinguish "this changed" from "this is wrong."

Bug-class examples (synthetic placeholders)

1. Temporal Evolution (False Positive)

Finding: HIGH
  A: [daily/transcripts/2026/2026-04-28] "status: trial"
  B: [meetings/2026-05-07-session] "status: confirmed"
  Axis: Whether status is trial or confirmed

Both are correct as of their respective dates. April 28: trial. May 7: confirmed. The probe flags this because it has no concept of "this claim was valid from X until Y." The May 7 record didn't make the April 28 transcript wrong; it recorded a change.

2. Negation Parsing (False Positive)

Finding: HIGH
  A: [people/alice-example] "person traveled to city-a for alice-example's event — NOT bob-example's event"
  B: [meetings/2026-05-11-context] mentions of bob-example's event in city-b
  Axis: Whose event the city-a trip was for

The disambiguation fact contains "NOT bob-example's event" as an explicit negation. The judge reads "bob-example's event" as a positive claim and flags it against the alice-example context. The data is correct; the probe can't parse negation.

3. Role Changes (True Positive That Needs Time Awareness)

Finding: HIGH
  A: [sources/notes/2017-03-28] advisor-example: "Partner, venture-firm-a"
  B: [people/advisor-example] advisor-example: "Senior Policy Advisor, gov-org-b"

Both true at their respective times. 2017: partner at venture-firm-a. 2025: gov-org-b advisor. The current probe correctly flags this as a contradiction, but the resolution should be "superseded by time" not "one side is wrong." The 2017 note isn't wrong; it's a historical record.

Scenario #1: Founder Tracking (the big one)

This is the use case that makes a time axis transformative rather than incremental.

The brain holds hundreds of company pages and thousands of meeting pages. Founders make claims:

  • "We're at $50K MRR" (January OH)
  • "We hit $200K MRR" (April OH)
  • "We're at $150K MRR" (July OH — what happened?)

Today the probe would flag January vs. April as a contradiction. The real signal is April vs. July: a claimed metric went backwards. That's not a data quality issue; that's intelligence.

What a time-aware probe could surface:

Claim trajectory tracking:

Company: Acme Corp
  2026-01: "$50K MRR" (source: OH transcript)
  2026-04: "$200K MRR" (source: OH transcript)
  2026-07: "$150K MRR" (source: OH transcript) ← REGRESSION DETECTED
  2026-07: "$2M ARR" (source: investor update) ← INCONSISTENT WITH MRR

Prediction vs. outcome:

Founder: Jane Doe (Acme Corp)
  2026-01: "We'll hit $1M ARR by June" (source: batch kickoff)
  2026-06: Actual ARR: $400K (source: investor update)
  → Prediction accuracy: 40%
  → Pattern: consistently 2-3x optimistic on timeline

Narrative consistency:

Founder: John Smith (WidgetCo)
  2026-01: "Our moat is proprietary data" (source: interview)
  2026-03: "We're pivoting to an API-first model" (source: OH)
  2026-06: "Our moat is network effects" (source: Demo Day)
  → Moat narrative changed 3x in 6 months — flag for review

This isn't adversarial. It's the kind of pattern an experienced operator notices intuitively across hundreds of conversations. GBrain can make it systematic.

Scenario #2: Event Disambiguation

Two distinct events within a short window can conflate during ingestion because the probe has no temporal frame to say "event A is a different event from event B."

Time-aware facts would store (synthetic placeholders):

fact: "alice-example milestone" valid_from: 2026-04-15 valid_until: 2026-04-15
fact: "alice-example event in city-a" valid_from: 2026-04-17 valid_until: 2026-04-19
fact: "bob-example milestone" valid_from: 2026-05-04 valid_until: 2026-05-04
fact: "bob-example event in city-b" valid_from: 2026-05-12 valid_until: 2026-05-12

The probe should recognize these as two distinct events with non-overlapping time windows, not as contradictions about "whose event."

Scenario #3: Role and Status Changes

People change roles. Companies change status. The brain records history. Synthetic examples representative of the cases observed in production:

  • advisor-example: venture-firm-a partner (2019) → gov-org-b advisor (2025)
  • investor-example: fund-a partner → fund-b CEO (2023)
  • agent-fork: provider restriction event (2026-04-04) ≠ shutdown
  • fund-c: "interesting fund" (early) → "declined" (later) → "losing confidence" (latest)

All of these are correct historical records. The probe should classify them as temporal supersession rather than contradiction.

Scenario #4: Decision Tracking

Multi-step decisions that supersede earlier framings example (synthetic):

2026-04-24: "status: trial" (initial framing)
2026-04-25: "status: in progress" (confirmed, no longer "trial")
2026-05-07: "status: finalized" (session record)
2026-05-11: follow-up actions taken

Each step supersedes the previous. A time-aware probe would show the evolution chain rather than flagging each pair as a contradiction.

What Exists Today

The probe already has some temporal infrastructure:

  1. date-filter.tsshouldSkipForDateMismatch() pre-filters pairs, but only checks whether dates are "too far apart" (a coarse heuristic). It doesn't reason about which claim is newer or whether one supersedes the other.

  2. auto-supersession.ts — proposes resolution commands, checks since_date on takes. But this is post-hoc (after the judge flags a contradiction). The judge itself doesn't see dates.

  3. Facts table has valid_from and valid_until columns. These exist but are sparsely populated and not used by the probe.

  4. Takes table has since_date. Also sparsely populated.

What Would Need to Change

Phase 1: Judge prompt enhancement (smallest change, biggest impact)

Pass the source dates to the judge. The current judge prompt shows two text chunks and asks "are these contradictory?" If it also showed:

Statement A (from: 2026-04-28):
  "status: trial"

Statement B (from: 2026-05-07):
  "status: confirmed"

The judge could output a temporal_supersession verdict instead of contradiction. New verdict taxonomy:

  • no_contradiction — statements are compatible
  • contradiction — genuinely conflicting claims at the same point in time
  • temporal_supersession — newer claim updates/replaces older claim (not an error)
  • temporal_regression — a metric or status went backwards (potential signal)
  • temporal_evolution — legitimate change over time, neither supersession nor regression
  • negation_artifact — one side contains an explicit negation the judge misread

Phase 2: Claim trajectory view (new command)

gbrain eval trajectory "Acme Corp MRR"
gbrain eval trajectory "advisor-example role"
gbrain eval trajectory "deal-x status"

Pull all time-stamped claims about an entity+attribute, sort chronologically, detect:

  • Regressions (metric went down)
  • Contradictions within the same time window
  • Prediction vs. outcome gaps
  • Narrative drift (moat story changed 3x)

Phase 3: Automatic valid_from/valid_until population

During extract_facts, infer temporal bounds from source context:

  • Meeting page dated 2026-04-28 → claims valid_from 2026-04-28
  • Takes from transcripts → valid_from = transcript date
  • Imported notes → valid_from = note date
  • Entity pages with no date → valid_from = page created date (weakest signal)

Phase 4: Founder scorecard

For founders specifically, a temporal probe could generate:

  • Claim accuracy score — what they predicted vs. what happened
  • Consistency score — how stable their narrative is over time
  • Growth trajectory — whether the numbers are actually moving
  • Red flag detector — metrics going backwards, story changing, timeline slipping

Recommendation

Start with Phase 1. The judge prompt change is small. It immediately eliminates the temporal false positives (which were a majority of the residual HIGH findings in the production audit) and gives the probe a new vocabulary for time-aware reasoning.

Phase 2 (trajectory view) is the one that would change how operators use the brain for founder evaluation. Worth scoping as a standalone feature.

Phases 34 are downstream and can wait.

Appendix: Production probe stats (2026-05-14)

  • ~107K pages, ~257K chunks
  • Previous run: ~115 HIGH findings across 50 queries
  • After manual resolution: ~25 residual findings
  • Of those ~25: roughly two-thirds temporal false positives, the remainder probe artifacts (self-contradiction, negation parsing)
  • 0 genuine data contradictions remained on the queries tested
  • Fresh targeted probe on a representative entity-role query: 0 contradictions (was 14+ before fixes)