Confine the ledger's epochs to the question that recorded them
A question's evidence epoch and declaration describe that question and end with it, so `begin_question` clears them. Held past it, they let the last question's citations ground the next one and hold a horizon its own declarations cannot pass, and they force every question's message count to be comparable to counts taken over a history that a host may since have rebuilt: a thread reconstructed without a run that never finished shifts every later position, and the next question was refused where answering it is correct. Identities still separate one question from the next. Occurrences outlive the question that recorded them and carry the identities that cited them, which the capsule is grouped and ordered by, so reuse merges two questions into one group and a lower identity renders later evidence as earlier.
This commit is contained in:
parent
11b1bfbc94
commit
97a29d168f
2 changed files with 75 additions and 12 deletions
|
|
@ -65,14 +65,15 @@ class CapabilityEvidenceRecord(BaseModel):
|
|||
declaration: CitationDeclaration | None = None
|
||||
|
||||
def _reject_regression(self, count: int, what: str) -> None:
|
||||
"""Refuse a message count below one already recorded.
|
||||
"""Refuse a message count below one already recorded in this question.
|
||||
|
||||
Identities and epochs are both message counts, and every comparison
|
||||
between them assumes the conversation only grows. One capability
|
||||
truncating or reordering the history breaks that, and each way of
|
||||
recording it has to refuse the same way: an unchecked evidence outcome
|
||||
freezes every later declaration as stale, while an unchecked declaration
|
||||
replaces a newer one with an older one and revives the answer it grounded.
|
||||
between them assumes the conversation only grows while a question is
|
||||
being answered. One capability truncating or reordering the history
|
||||
breaks that, and each way of recording it has to refuse the same way: an
|
||||
unchecked evidence outcome freezes every later declaration as stale, while
|
||||
an unchecked declaration replaces a newer one with an older one and
|
||||
revives the answer it grounded.
|
||||
"""
|
||||
recorded = max(
|
||||
self.question or 0,
|
||||
|
|
@ -87,9 +88,30 @@ class CapabilityEvidenceRecord(BaseModel):
|
|||
)
|
||||
|
||||
def begin_question(self, identity: int) -> None:
|
||||
"""Take the identity of a question that has just arrived."""
|
||||
self._reject_regression(identity, "A question")
|
||||
"""Take the identity of a question that has just arrived.
|
||||
|
||||
The evidence epoch and the declaration describe the question that has
|
||||
just ended, and outlive their meaning the moment the next one begins:
|
||||
kept, they let the last question's citations ground this one, and hold a
|
||||
horizon this question's own declarations cannot pass. Clearing them
|
||||
confines those comparisons to one question, so a host whose stored
|
||||
history shifted between two of them is answered rather than refused.
|
||||
|
||||
Identities themselves must still separate one question from the next.
|
||||
Occurrences outlive the question that recorded them and carry the
|
||||
identities that cited them, which compaction groups and orders the capsule
|
||||
by: a reused identity merges two questions into one group, and a lower one
|
||||
renders later evidence as though it were cited earlier.
|
||||
"""
|
||||
if self.question is not None and identity <= self.question:
|
||||
raise ValueError(
|
||||
f"A question at message count {identity} is not past question "
|
||||
f"{self.question}, which is already answered: identities separate "
|
||||
"one question from the next and are compared as recency."
|
||||
)
|
||||
self.question = identity
|
||||
self.latest_evidence_epoch = 0
|
||||
self.declaration = None
|
||||
|
||||
def note_evidence(self, epoch: int) -> None:
|
||||
"""Record that the model has seen an evidence outcome.
|
||||
|
|
|
|||
|
|
@ -47,14 +47,13 @@ def test_refs_make_it_grounded_and_no_refs_make_it_ungrounded():
|
|||
|
||||
|
||||
def test_an_earlier_questions_declaration_is_never_current():
|
||||
"""Epochs outlive a question, so the epoch alone would inherit it."""
|
||||
"""A later question inherits nothing: it has declared nothing yet."""
|
||||
record = CapabilityEvidenceRecord(question=2)
|
||||
record.declare([rag_ref()], epoch=3)
|
||||
assert citation_status([record], question=2) == "grounded"
|
||||
|
||||
record.begin_question(8)
|
||||
|
||||
assert record.declaration is not None
|
||||
assert citation_status([record], question=8) == "missing"
|
||||
|
||||
|
||||
|
|
@ -193,9 +192,51 @@ def test_evidence_cannot_predate_a_recorded_declaration():
|
|||
record.note_evidence(3)
|
||||
|
||||
|
||||
def test_a_question_cannot_start_before_what_is_already_recorded():
|
||||
def test_a_question_starts_behind_the_epochs_of_the_one_before_it():
|
||||
"""A question's identity is the history it arrives on, not a continuation.
|
||||
|
||||
Epochs are compared only within the question that recorded them, so a host
|
||||
whose stored history shifted between two questions is answered rather than
|
||||
refused.
|
||||
"""
|
||||
record = CapabilityEvidenceRecord(question=0)
|
||||
record.note_evidence(9)
|
||||
|
||||
with pytest.raises(ValueError, match="append-only"):
|
||||
record.begin_question(4)
|
||||
|
||||
assert record.question == 4
|
||||
|
||||
|
||||
def test_a_question_cannot_reuse_the_identity_of_the_one_before_it():
|
||||
"""Occurrences outlive their question and are ordered by identity.
|
||||
|
||||
Two questions sharing one identity merge into a single capsule group, and a
|
||||
lower one is rendered as though its evidence were cited earlier.
|
||||
"""
|
||||
record = CapabilityEvidenceRecord(question=4)
|
||||
|
||||
with pytest.raises(ValueError, match="already answered"):
|
||||
record.begin_question(4)
|
||||
|
||||
with pytest.raises(ValueError, match="already answered"):
|
||||
record.begin_question(3)
|
||||
|
||||
assert record.question == 4
|
||||
|
||||
|
||||
def test_a_question_starts_clear_of_the_one_before_it():
|
||||
"""Evidence and declarations describe a single question and end with it.
|
||||
|
||||
Carrying either into the next question makes it answerable by the last
|
||||
question's citations, and freezes its own declarations behind an epoch no
|
||||
message in it can reach.
|
||||
"""
|
||||
record = CapabilityEvidenceRecord(question=4)
|
||||
record.note_evidence(6)
|
||||
record.declare([rag_ref()], epoch=7)
|
||||
|
||||
record.begin_question(9)
|
||||
|
||||
assert record.latest_evidence_epoch == 0
|
||||
assert record.declaration is None
|
||||
assert citation_status([record], question=9) == "missing"
|
||||
|
|
|
|||
Loading…
Reference in a new issue