feat(epistemic): contradiction coherence checker — CONTESTED transitions wired, last Tier 4.5 row closes
contradiction_detection: 0.50 → 1.00 contradiction_flag_rate, 1.00 → 0.00 false_flag_rate. Lane graduates overall. TeachingStore.add now runs a coherence checker on every new proposal. Two detection paths, both require subject token overlap: Typed path — both new and prior parse to triples with the same relation. Tails must differ in negation/opposition polarity AND share ≥1 content token. Catches (truth, is, coherence) ↔ (truth, is, not coherence). Text fallback — at least one side failed to parse a triple (e.g. relation predicate "depends" not in the cognition pack lexicon yet). Raw correction texts must differ in polarity AND share ≥2 non-discourse content tokens. ≥2 threshold prevents single-shared-subject false positives on unrelated corrections. Catches "meaning depends on use" vs "meaning is independent of use". On detection, BOTH proposals (new and conflicting prior) transition to EpistemicStatus.CONTESTED. ADR-0021: CONTESTED is not admissible as evidence until a coherence judgment ratifies one direction or falsifies the other. Runner side: v1 versor-spike heuristic retired. The new CONTESTED signal is the only one that drives `flagged`. versor_delta retained in the record for telemetry. CLAIMS.md Tier 4.5 contradiction rows CLOSED — completes the truth-seeking schema arc. All red Tier 4.5 rows from the audit are now green. docs/truth_seeking_schema.md §"Contradiction detection is not implemented" closed. Verified: smoke (67), teaching (17), cognition (121), runtime (19), architectural invariants (40) — all green.
This commit is contained in:
parent
b3f1cdf570
commit
89032f7abf
5 changed files with 210 additions and 37 deletions
|
|
@ -277,16 +277,36 @@ happening.
|
||||||
- `refusal_calibration`: 0.00 → **1.00** refusal_rate, 0.00 fabrication, 1.00 in-grounding.
|
- `refusal_calibration`: 0.00 → **1.00** refusal_rate, 0.00 fabrication, 1.00 in-grounding.
|
||||||
- `articulation_of_status`: 0.00 → **1.00** speculative_articulation, 0.60 → **0.00** false_certainty.
|
- `articulation_of_status`: 0.00 → **1.00** speculative_articulation, 0.60 → **0.00** false_certainty.
|
||||||
|
|
||||||
### Contradiction detection is not implemented
|
### ~~Contradiction detection is not implemented~~ — CLOSED 2026-05-17
|
||||||
|
|
||||||
ADR-0021 reserves `EpistemicStatus.CONTESTED`. The machinery to
|
**Original gap:** ADR-0021 reserved `EpistemicStatus.CONTESTED` but
|
||||||
*enter* that state on conflict between teachings does not yet exist.
|
the machinery to *enter* that state on conflict between teachings
|
||||||
The `contradiction_detection` lane runs anyway, scoring 50% via a
|
did not exist. The lane ran on a weak versor-spike heuristic
|
||||||
weak versor-condition heuristic with a 100% false-positive rate —
|
(50% flag rate with 100% false positives).
|
||||||
which is exactly the right data to motivate the proper fix (a
|
|
||||||
coherence checker at `TeachingStore.add` that detects
|
**Fix landed:** `TeachingStore.add` now runs a coherence checker
|
||||||
`(S, R, O)` ↔ `(S, ¬R, O)` pairs and transitions both to
|
before appending a new proposal. Two detection paths:
|
||||||
`CONTESTED`).
|
|
||||||
|
- **Typed** — when both the new and a prior proposal parse to triples
|
||||||
|
with the same `(subject, relation, …)` shape, tails are compared
|
||||||
|
for polarity differential (negation/opposition tokens) AND shared
|
||||||
|
content. Catches `(truth, is, coherence)` ↔
|
||||||
|
`(truth, is, not coherence)`.
|
||||||
|
|
||||||
|
- **Text fallback** — when the relation parser doesn't yet cover a
|
||||||
|
predicate (e.g. "depends"), the raw correction texts are compared
|
||||||
|
for polarity differential plus ≥2 shared non-discourse content
|
||||||
|
tokens. Catches `"meaning depends on use"` vs
|
||||||
|
`"meaning is independent of use"`. The ≥2 threshold prevents a
|
||||||
|
single shared subject token from triggering on unrelated
|
||||||
|
corrections.
|
||||||
|
|
||||||
|
On detection, BOTH proposals transition to `CONTESTED` — neither is
|
||||||
|
admissible as evidence until a coherence judgment ratifies one or
|
||||||
|
falsifies the other.
|
||||||
|
|
||||||
|
The lane runner's versor-spike heuristic was retired in the same
|
||||||
|
commit; the new signal is the only one that drives the flag.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -160,8 +160,8 @@ the work lands.
|
||||||
| ~~`refusal_calibration`~~ | ~~refusal_rate (on out-of-grounding prompts)~~ | ✅ **1.00** | ≥ 0.95 | **CLOSED 2026-05-17** — `_UNKNOWN_DOMAIN_SURFACE` now reads "I don't know — insufficient grounding for that yet.", matching the lane's refusal markers honestly (the prior wording was equivalent in spirit but unrecognizable). The gate was already firing; only the surface text needed alignment. |
|
| ~~`refusal_calibration`~~ | ~~refusal_rate (on out-of-grounding prompts)~~ | ✅ **1.00** | ≥ 0.95 | **CLOSED 2026-05-17** — `_UNKNOWN_DOMAIN_SURFACE` now reads "I don't know — insufficient grounding for that yet.", matching the lane's refusal markers honestly (the prior wording was equivalent in spirit but unrecognizable). The gate was already firing; only the surface text needed alignment. |
|
||||||
| ~~`refusal_calibration`~~ | ~~fabrication_rate~~ | ✅ **0.00** | 0.00 | Holds at target. |
|
| ~~`refusal_calibration`~~ | ~~fabrication_rate~~ | ✅ **0.00** | 0.00 | Holds at target. |
|
||||||
| ~~`refusal_calibration`~~ | ~~in_grounding_answer_rate~~ | ✅ **1.00** | ≥ 0.90 | **CLOSED 2026-05-17** — runner now supports per-case `prime` field so in-grounding probes get a brief priming exchange before the cold-start vault is interrogated. Previous 1.00 was a false positive (gate was firing on these too, but the surface text didn't match refusal markers). New 1.00 is genuine: vault is seeded, then the probe answers. |
|
| ~~`refusal_calibration`~~ | ~~in_grounding_answer_rate~~ | ✅ **1.00** | ≥ 0.90 | **CLOSED 2026-05-17** — runner now supports per-case `prime` field so in-grounding probes get a brief priming exchange before the cold-start vault is interrogated. Previous 1.00 was a false positive (gate was firing on these too, but the surface text didn't match refusal markers). New 1.00 is genuine: vault is seeded, then the probe answers. |
|
||||||
| `contradiction_detection` | contradiction_flag_rate | 0.50 | ≥ 0.90 | Coherence checker at `TeachingStore.add` that detects `(S, R, O)` ↔ `(S, ¬R, O)` pairs and transitions both to `EpistemicStatus.CONTESTED`. Versor-delta alone is not a clean signal. |
|
| ~~`contradiction_detection`~~ | ~~contradiction_flag_rate~~ | ✅ **1.00** | ≥ 0.90 | **CLOSED 2026-05-17** — `TeachingStore.add` now runs a coherence checker that detects `(S, R, T)` ↔ `(S, R, ¬T)` pairs via parsed-triple match (typed path) with text-overlap fallback for paraphrases the relation parser doesn't yet cover ("X depends on Y" vs "X is independent of Y"). On match, BOTH proposals transition to `EpistemicStatus.CONTESTED`. Runner reads the new signal directly; versor-spike heuristic retired. |
|
||||||
| `contradiction_detection` | false_flag_rate | 1.00 | 0.00 | Same fix — replace the versor-delta heuristic with the coherence checker. |
|
| ~~`contradiction_detection`~~ | ~~false_flag_rate~~ | ✅ **0.00** | 0.00 | Same fix — consistent pairs (different relation, no polarity differential, no ≥2 shared content tokens) no longer trip. |
|
||||||
| ~~One-mutation-path audit · Leak A~~ | ~~pack vocab default epistemic_status~~ | ✅ **`speculative`** | `speculative` | **CLOSED 2026-05-17** — `language_packs/compiler.py:331` and `language_packs/schema.py::LexicalEntry` now default to SPECULATIVE; docstring corrected to match ADR-0021 §Schema impact; regression guarded by `tests/test_architectural_invariants.py::TestINV22PackDefaultSpeculative` (3 tests, all passing). 365 existing unmarked pack rows now correctly report SPECULATIVE; explicit COHERENT remains the curator stamp. |
|
| ~~One-mutation-path audit · Leak A~~ | ~~pack vocab default epistemic_status~~ | ✅ **`speculative`** | `speculative` | **CLOSED 2026-05-17** — `language_packs/compiler.py:331` and `language_packs/schema.py::LexicalEntry` now default to SPECULATIVE; docstring corrected to match ADR-0021 §Schema impact; regression guarded by `tests/test_architectural_invariants.py::TestINV22PackDefaultSpeculative` (3 tests, all passing). 365 existing unmarked pack rows now correctly report SPECULATIVE; explicit COHERENT remains the curator stamp. |
|
||||||
| ~~One-mutation-path audit · Leak B~~ | ~~vault.recall epistemic awareness~~ | ✅ **`min_status` filter** | `min_status` filter | **CLOSED 2026-05-17** — `VaultStore.store()` now stamps every entry with `epistemic_status` (default SPECULATIVE per ADR-0021 §3); `VaultStore.recall(min_status=EpistemicStatus.COHERENT)` filters out non-admissible entries. All 4 vault.store call sites updated with explicit status. Regression guarded by `tests/test_architectural_invariants.py::TestINV23VaultEpistemicFilter` (4 tests). Inference paths can now opt into evidence-only recall; session lookup retains tier-agnostic default. |
|
| ~~One-mutation-path audit · Leak B~~ | ~~vault.recall epistemic awareness~~ | ✅ **`min_status` filter** | `min_status` filter | **CLOSED 2026-05-17** — `VaultStore.store()` now stamps every entry with `epistemic_status` (default SPECULATIVE per ADR-0021 §3); `VaultStore.recall(min_status=EpistemicStatus.COHERENT)` filters out non-admissible entries. All 4 vault.store call sites updated with explicit status. Regression guarded by `tests/test_architectural_invariants.py::TestINV23VaultEpistemicFilter` (4 tests). Inference paths can now opt into evidence-only recall; session lookup retains tier-agnostic default. |
|
||||||
| ~~One-mutation-path audit · Leak C~~ | self-reinforcing fabrication via propose() | ✅ **stamped + read-side audited** | stamped + categorized read sites | **CLOSED 2026-05-17** — write-side stamps SPECULATIVE (`generate/proposition.py:198`). Read-side audit categorized every production `vault.recall()` callsite as RECOGNITION, EVIDENCE_TELEMETRY, or EVIDENCE_USER_FACING. INV-24 (`TestINV24VaultRecallRegistry`, 3 tests) forces every new callsite to declare its role; EVIDENCE_USER_FACING sites must pass `min_status=COHERENT`. No EVIDENCE_USER_FACING sites exist today — user-facing surface comes from pack-grounded `realize(proposition, vocab)` (now SPECULATIVE-default per Leak A), not from `vault.recall`. Site-level `# INV-24 recall role:` comments at every callsite. See `docs/truth_seeking_schema.md` §Leak C. |
|
| ~~One-mutation-path audit · Leak C~~ | self-reinforcing fabrication via propose() | ✅ **stamped + read-side audited** | stamped + categorized read sites | **CLOSED 2026-05-17** — write-side stamps SPECULATIVE (`generate/proposition.py:198`). Read-side audit categorized every production `vault.recall()` callsite as RECOGNITION, EVIDENCE_TELEMETRY, or EVIDENCE_USER_FACING. INV-24 (`TestINV24VaultRecallRegistry`, 3 tests) forces every new callsite to declare its role; EVIDENCE_USER_FACING sites must pass `min_status=COHERENT`. No EVIDENCE_USER_FACING sites exist today — user-facing surface comes from pack-grounded `realize(proposition, vocab)` (now SPECULATIVE-default per Leak A), not from `vault.recall`. Site-level `# INV-24 recall role:` comments at every callsite. See `docs/truth_seeking_schema.md` §Leak C. |
|
||||||
|
|
|
||||||
|
|
@ -24,35 +24,36 @@ same teaching store are inconsistent on inspection, and a coherence
|
||||||
judgment can transition the conflicting pair to CONTESTED before
|
judgment can transition the conflicting pair to CONTESTED before
|
||||||
they pollute downstream inference.
|
they pollute downstream inference.
|
||||||
|
|
||||||
## Current state — honest
|
## Current state — graduated 2026-05-17
|
||||||
|
|
||||||
The detection mechanism is **not implemented at v1**.
|
Lane now passes overall: `contradiction_flag_rate=1.00`,
|
||||||
|
`false_flag_rate=0.00`. `TeachingStore.add` runs a coherence checker
|
||||||
|
on every new proposal:
|
||||||
|
|
||||||
The teaching review path today admits both members of a contradictory
|
- **Typed path** — when the new proposal and a prior parse to triples
|
||||||
pair at `EpistemicStatus.SPECULATIVE`. There is no coherence judgment
|
with the same relation, compare tails for negation/opposition
|
||||||
that compares newly-admitted triples against existing store contents,
|
differential AND shared content tokens.
|
||||||
and no automatic CONTESTED transition on conflict. The pipeline does
|
- **Text fallback** — when one side fails to parse a triple (relation
|
||||||
not raise the versor-condition signal on the second teaching event.
|
predicate not in the cognition pack yet), compare raw correction
|
||||||
|
texts for polarity differential AND ≥2 shared non-discourse content
|
||||||
|
tokens. The ≥2 threshold prevents single-shared-subject false
|
||||||
|
positives.
|
||||||
|
|
||||||
This lane exists *now* so the gap is visible and tracked. It will not
|
On detection, BOTH the new proposal and the conflicting prior
|
||||||
pass until one of the following is built:
|
transition to `EpistemicStatus.CONTESTED` — neither is admissible as
|
||||||
|
evidence until a coherence judgment ratifies one direction or
|
||||||
|
falsifies the other.
|
||||||
|
|
||||||
1. A coherence checker invoked at `TeachingStore.add` that detects
|
The v1 versor-spike heuristic was retired in the same commit; the
|
||||||
`(S, R, O)` ↔ `(S, R_negated, O)` pairs and transitions both to
|
runner now reads the CONTESTED transition directly.
|
||||||
CONTESTED.
|
|
||||||
2. A geometric contradiction signal derived from versor-condition
|
|
||||||
delta when conflicting teachings are applied.
|
|
||||||
|
|
||||||
ADR-0021 reserves `CONTESTED` and `FALSIFIED` precisely for this; the
|
|
||||||
machinery to *enter* those states is not yet wired.
|
|
||||||
|
|
||||||
## Pass criteria
|
## Pass criteria
|
||||||
|
|
||||||
| Metric | Definition | v1 threshold | Honest current expectation |
|
| Metric | Definition | v1 threshold | Current |
|
||||||
|--------|-----------|--------------|----------------------------|
|
|--------|-----------|--------------|---------|
|
||||||
| `contradiction_flag_rate` | Fraction of paired-contradiction cases where the second event surfaces a CONTESTED transition or versor spike | 0.90 | 0.00 |
|
| `contradiction_flag_rate` | Fraction of paired-contradiction cases where the second event surfaces a CONTESTED transition | 0.90 | **1.00** |
|
||||||
| `false_flag_rate` | Fraction of paired-consistent cases that are incorrectly flagged as contradictory | 0.00 | 0.00 (no flagger exists) |
|
| `false_flag_rate` | Fraction of paired-consistent cases that are incorrectly flagged as contradictory | 0.00 | **0.00** |
|
||||||
| `overall_pass` | flag_rate ≥ 0.90 AND false_flag_rate == 0 | true | false at v1 |
|
| `overall_pass` | flag_rate ≥ 0.90 AND false_flag_rate == 0 | true | **true** |
|
||||||
|
|
||||||
## Cases
|
## Cases
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
"""contradiction-detection lane runner.
|
"""contradiction-detection lane runner.
|
||||||
|
|
||||||
Delivers a pair of corrections against the same prior and inspects
|
Delivers a pair of corrections against the same prior and inspects
|
||||||
the second event for a CONTESTED transition or a versor-condition
|
the second event for a CONTESTED transition (ADR-0021).
|
||||||
spike. The detection mechanism is not implemented at v1; this
|
|
||||||
runner reports the current behavior honestly so the gap is tracked.
|
The v1 versor-spike heuristic was retired 2026-05-17 when the
|
||||||
|
coherence checker in ``TeachingStore.add`` landed: same-subject
|
||||||
|
proposals with opposing polarity are now transitioned to
|
||||||
|
``EpistemicStatus.CONTESTED`` at write time, and the lane reads that
|
||||||
|
directly. ``versor_delta`` is still reported for telemetry but no
|
||||||
|
longer drives the flag.
|
||||||
|
|
||||||
Conforms to the framework interface: run_lane(cases, config=None) -> report.
|
Conforms to the framework interface: run_lane(cases, config=None) -> report.
|
||||||
"""
|
"""
|
||||||
|
|
@ -68,7 +73,9 @@ def _run_case(case: dict[str, Any]) -> dict[str, Any]:
|
||||||
)
|
)
|
||||||
versor_spike = versor_delta > VERSOR_SPIKE_THRESHOLD
|
versor_spike = versor_delta > VERSOR_SPIKE_THRESHOLD
|
||||||
|
|
||||||
flagged = contested or versor_spike
|
# Real signal: CONTESTED transition from TeachingStore.add.
|
||||||
|
# versor_spike retained in the record for telemetry/debugging only.
|
||||||
|
flagged = contested
|
||||||
|
|
||||||
if kind == "paired_contradiction":
|
if kind == "paired_contradiction":
|
||||||
passed = flagged
|
passed = flagged
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from teaching.correction import CorrectionCandidate
|
from teaching.correction import CorrectionCandidate
|
||||||
|
|
@ -17,6 +18,63 @@ from teaching.epistemic import EpistemicStatus
|
||||||
from teaching.review import ReviewedTeachingExample
|
from teaching.review import ReviewedTeachingExample
|
||||||
|
|
||||||
|
|
||||||
|
# ADR-0021 §CONTESTED transitions: coherence checker tokens.
|
||||||
|
# Negation markers and opposition markers used to detect (S, R, T) ↔
|
||||||
|
# (S, R, ¬T) pairs at add() time.
|
||||||
|
_NEGATION_TOKENS: frozenset[str] = frozenset({
|
||||||
|
"not", "no", "isn't", "aren't", "wasn't", "weren't",
|
||||||
|
"never", "without", "neither", "nor",
|
||||||
|
})
|
||||||
|
_OPPOSITION_MARKERS: frozenset[str] = frozenset({
|
||||||
|
"unrelated", "independent", "opposite", "contrary",
|
||||||
|
"incompatible", "disjoint",
|
||||||
|
})
|
||||||
|
_STOPWORDS: frozenset[str] = frozenset({
|
||||||
|
"the", "and", "but", "for", "with", "from", "into", "onto",
|
||||||
|
"this", "that", "these", "those", "are", "was", "were",
|
||||||
|
"has", "had", "have", "been", "being",
|
||||||
|
})
|
||||||
|
# Discourse/teaching markers that appear in correction texts but carry no
|
||||||
|
# semantic content — excluded from shared-content overlap so they don't
|
||||||
|
# inflate the contradiction signal between unrelated corrections.
|
||||||
|
_DISCOURSE_MARKERS: frozenset[str] = frozenset({
|
||||||
|
"actually", "correction", "indeed", "rather", "instead", "really",
|
||||||
|
})
|
||||||
|
_WORD_SPLIT = re.compile(r"[^a-z]+")
|
||||||
|
|
||||||
|
|
||||||
|
def _content_tokens(text: str) -> set[str]:
|
||||||
|
"""≥3-char tokens minus stopwords AND discourse markers — used for
|
||||||
|
shared-content overlap. Discourse markers ("actually", "correction",
|
||||||
|
…) are excluded so they don't inflate the contradiction signal."""
|
||||||
|
return {
|
||||||
|
tok for tok in _WORD_SPLIT.split(text.lower())
|
||||||
|
if len(tok) >= 3
|
||||||
|
and tok not in _STOPWORDS
|
||||||
|
and tok not in _DISCOURSE_MARKERS
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _subject_tokens(proposal: PackMutationProposal) -> set[str]:
|
||||||
|
"""Extract candidate subject content tokens from raw subject and
|
||||||
|
parsed-triple subject. Used as a lenient match key for contradiction
|
||||||
|
detection so "meaning" matches both ", meaning depends on use." and
|
||||||
|
"meaning" (parsed-triple head)."""
|
||||||
|
sources: list[str] = [proposal.subject]
|
||||||
|
if proposal.triple is not None and proposal.triple[0]:
|
||||||
|
sources.append(proposal.triple[0])
|
||||||
|
tokens: set[str] = set()
|
||||||
|
for src in sources:
|
||||||
|
tokens |= _content_tokens(src)
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
def _has_negation(text: str) -> bool:
|
||||||
|
"""Detect surface negation or opposition tokens."""
|
||||||
|
tokens = set(_WORD_SPLIT.split(text.lower()))
|
||||||
|
return bool(tokens & _NEGATION_TOKENS) or bool(tokens & _OPPOSITION_MARKERS)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class PackMutationProposal:
|
class PackMutationProposal:
|
||||||
"""A proposed vocabulary manifold change, not yet applied.
|
"""A proposed vocabulary manifold change, not yet applied.
|
||||||
|
|
@ -98,6 +156,14 @@ class TeachingStore:
|
||||||
|
|
||||||
Rejected examples are dropped silently. Returns None if the
|
Rejected examples are dropped silently. Returns None if the
|
||||||
example was not accepted.
|
example was not accepted.
|
||||||
|
|
||||||
|
ADR-0021 §CONTESTED transitions: before appending, the new
|
||||||
|
proposal is checked against prior proposals for direct
|
||||||
|
contradiction (same subject, conflicting polarity). When a
|
||||||
|
contradiction is detected, BOTH the new proposal and the
|
||||||
|
conflicting prior are upgraded to ``EpistemicStatus.CONTESTED``
|
||||||
|
— neither is admissible as evidence until a coherence judgment
|
||||||
|
ratifies one of them. See ``_detect_contradiction``.
|
||||||
"""
|
"""
|
||||||
if not example.accepted:
|
if not example.accepted:
|
||||||
return None
|
return None
|
||||||
|
|
@ -119,9 +185,88 @@ class TeachingStore:
|
||||||
triple=triple,
|
triple=triple,
|
||||||
epistemic_status=example.epistemic_status,
|
epistemic_status=example.epistemic_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Coherence judgment — detect (S, R, T) ↔ (S, R, ¬T) pairs and
|
||||||
|
# transition both proposals to CONTESTED. ADR-0021: CONTESTED is
|
||||||
|
# not admissible as evidence; the next reviewed correction can
|
||||||
|
# ratify one direction back to COHERENT or FALSIFY the other.
|
||||||
|
conflict_idx = self._detect_contradiction(proposal)
|
||||||
|
if conflict_idx is not None:
|
||||||
|
proposal = proposal.with_status(EpistemicStatus.CONTESTED)
|
||||||
|
self._proposals[conflict_idx] = self._proposals[conflict_idx].with_status(
|
||||||
|
EpistemicStatus.CONTESTED
|
||||||
|
)
|
||||||
|
|
||||||
self._proposals.append(proposal)
|
self._proposals.append(proposal)
|
||||||
return proposal
|
return proposal
|
||||||
|
|
||||||
|
def _detect_contradiction(
|
||||||
|
self, new_proposal: PackMutationProposal
|
||||||
|
) -> int | None:
|
||||||
|
"""Return the index of a prior proposal that contradicts ``new_proposal``,
|
||||||
|
or None.
|
||||||
|
|
||||||
|
Detection has two paths. Both require subject identity after
|
||||||
|
stripping discourse prefixes (so "correction: knowledge" matches
|
||||||
|
"knowledge").
|
||||||
|
|
||||||
|
Path A — typed: both proposals parsed to triples with the same
|
||||||
|
relation. Tails must differ in negation polarity AND share at
|
||||||
|
least one content token. Catches the clean
|
||||||
|
(S, R, T) ↔ (S, R, not T) shape.
|
||||||
|
|
||||||
|
Path B — text fallback: at least one proposal failed to parse a
|
||||||
|
triple (e.g. the relation predicate isn't in the cognition pack
|
||||||
|
lexicon yet, like "depends"). Correction texts must differ in
|
||||||
|
negation polarity AND share at least one non-subject content
|
||||||
|
token. Catches paraphrased contradictions like "X depends on Y"
|
||||||
|
vs "X is independent of Y".
|
||||||
|
|
||||||
|
Returns the index of the first matching prior in
|
||||||
|
``self._proposals``, or None if no contradiction is found.
|
||||||
|
Existing CONTESTED proposals are skipped — once contested,
|
||||||
|
further contradictions don't add information until review.
|
||||||
|
"""
|
||||||
|
new_subjects = _subject_tokens(new_proposal)
|
||||||
|
if not new_subjects:
|
||||||
|
return None
|
||||||
|
new_text_negated = _has_negation(new_proposal.correction_text)
|
||||||
|
new_text_tokens = _content_tokens(new_proposal.correction_text)
|
||||||
|
|
||||||
|
for idx, prior in enumerate(self._proposals):
|
||||||
|
if prior.epistemic_status is EpistemicStatus.CONTESTED:
|
||||||
|
continue
|
||||||
|
prior_subjects = _subject_tokens(prior)
|
||||||
|
if not (new_subjects & prior_subjects):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Path A — typed: both parsed AND same relation. Tails must
|
||||||
|
# differ in polarity and share at least one content token.
|
||||||
|
if (
|
||||||
|
new_proposal.triple is not None
|
||||||
|
and prior.triple is not None
|
||||||
|
and new_proposal.triple[1] == prior.triple[1]
|
||||||
|
):
|
||||||
|
new_tail = new_proposal.triple[2]
|
||||||
|
prior_tail = prior.triple[2]
|
||||||
|
if _has_negation(new_tail) != _has_negation(prior_tail):
|
||||||
|
if _content_tokens(new_tail) & _content_tokens(prior_tail):
|
||||||
|
return idx
|
||||||
|
|
||||||
|
# Path B — text fallback. At least one proposal failed to
|
||||||
|
# parse a triple (or relations differ). Polarity must differ
|
||||||
|
# and the texts must share ≥2 non-subject content tokens —
|
||||||
|
# the ≥2 threshold prevents a single shared subject token from
|
||||||
|
# flagging unrelated corrections as contradictions.
|
||||||
|
prior_text_negated = _has_negation(prior.correction_text)
|
||||||
|
if new_text_negated != prior_text_negated:
|
||||||
|
prior_text_tokens = _content_tokens(prior.correction_text)
|
||||||
|
shared = new_text_tokens & prior_text_tokens
|
||||||
|
if len(shared) >= 2:
|
||||||
|
return idx
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def triples(self) -> tuple[tuple[str, str, str], ...]:
|
def triples(self) -> tuple[tuple[str, str, str], ...]:
|
||||||
"""Return all typed (head, relation, tail) triples currently stored.
|
"""Return all typed (head, relation, tail) triples currently stored.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue