Read a short subject's only number as its reading

A two-character Chinese subject — 温度, 室温, 设定, 预算 — is the common
way to state a reading, and the rename rule was swallowing its number:
「温度 22 度」 became a thing called 温度22 with nothing left to report, so
the lane recorded no conflict at all and no later memory could disagree
with it. A bare number written onto the subject is a name only while the
memory still has another number waiting behind it; with nothing after it,
it is the reading. Measured across 33 pairs: 6 change — five missed
conflicts now recorded, plus one new disclosed boundary (「Room 3 is big」
and 「Room 4 is big」 read as one quantity that moved, because a label
cannot tell an identifier from a reading). Everything else is byte-equal.

Also corrected: a memory that is only a number does have a subject — the
number itself — so 「22 度」 and 「24 度」 record nothing because they name
two different things, not because the store found nothing to compare.

The smoke script now refuses an answer from a server that predates the
memory tiers: it requires recall_count in the remember response, a field
the pre-p2-3 Go build never emits.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 11:44:29 -04:00
1 parent 8bed319848
commit 11e31eb7e3
4 files changed
+73 -14

No files matched your search

+21 -5
View File
@@ -225,7 +225,9 @@ def numeric_values(text: str) -> StatedNumbers:
differently but lead with the same thing, and it is that shared label which makes the pair one
disagreement about one quantity rather than two unrelated counts. A number written onto the
subject itself is part of the object's name, not a quantity — 「Room 3」 and 「Room 4」 are two
rooms — so it joins the subject instead of being recorded as a value.
rooms — but only while the memory still has another number waiting to be its reading. A number
with nothing after it is the reading itself: 「温度 22 度」 states a temperature, and renaming the
thermostat after it would leave the memory with no quantity left to compare.
"""
try:
tokens = fts_terms(text)
@@ -257,20 +259,34 @@ def _subject_and_rename_slot(tokens: list[str]) -> tuple[str, int]:
A memory that opens with a number is about that number: 「2026 saw 3 releases」 counts releases
for 2026, so 2026 names the thing and 3 measures it. An English memory usually opens with an
article, and a subject behind one is still the subject: 「The router forwards 220 packets」 leads
with its router, not with 「the」. Only a number written onto the subject renames it; one
standing past a word is a value, which is why 「The rent is 2200 dollars」 can move. A CJK run can
with its router, not with 「the」. Only a number written onto the subject renames it, and only
when the memory still has a later number to report: 「Room 3 holds 22 degrees」 names a room and
then reads it, while 「温度 22 度」 has nothing behind the 22 but the unit. A number standing past
a word is a value either way, which is why 「The rent is 2200 dollars」 can move. A CJK run can
fuse the subject and what it reports into one token, and the number after that is a value too:
「恒温器调到 24 度」 adjusts a thermostat, it does not name a thing called 「恒温器调到 24」.
"""
for index, token in enumerate(tokens):
if _NUMBER.fullmatch(token.strip(_PUNCTUATION)):
stated = _NUMBER.fullmatch(token.strip(_PUNCTUATION))
if stated:
return token.strip(_PUNCTUATION), -1
head = _label(token, first=True)
if head:
return head, -1 if _label(token) != head else index + 1
renames = _label(token) == head and _numbers_follow(tokens, index + 2)
return head, index + 1 if renames else -1
return "", -1
def _numbers_follow(tokens: list[str], start: int) -> bool:
"""Whether the memory still has a bare number to state after the slot in question.
A name is only worth taking when something else is left to be the reading. Without this test the
shortest Chinese phrasings — 「温度 22 度」, 「预算 5000 元」 — fold their only number into the
subject and the memory reports no quantity at all, so no later memory can ever disagree with it.
"""
return any(_NUMBER.fullmatch(token.strip(_PUNCTUATION)) for token in tokens[start:])
def quantity_conflict(older_text: str, newer_text: str) -> QuantityConflict | None:
"""The quantity two memories state differently, but only while they lead with the same subject.
+42 -3
View File
@@ -198,12 +198,12 @@ def test_the_same_subject_measuring_two_things_still_reads_as_one_quantity(tmp_p
)
def test_a_memory_that_leads_with_a_number_has_no_subject_to_conflict_on(tmp_path) -> None:
def test_two_memories_that_are_only_a_number_do_not_name_the_same_thing(tmp_path) -> None:
service, _ = _lane(tmp_path, ["22 度", "24 度"])
assert service.conflicts("wife") == [], (
"the number is all the memory has to name itself with, so the store has no subject it could "
"call the two one thing and records nothing"
"each number becomes its own subject — 「22」 and 「24」 — so these are two different things "
"to the store, not one quantity that moved; it finds a subject, it just is not the same one"
)
@@ -279,3 +279,42 @@ def test_a_letter_qualifier_does_not_rename_the_room(tmp_path) -> None:
"room whose reading moved — the pair is recorded for the holder to settle rather than "
"silently resolved, and this is the boundary the rule does not cross"
)
def test_a_short_chinese_reading_is_a_value_and_not_part_of_a_name(tmp_path) -> None:
service, _ = _lane(tmp_path, ["温度 22 度", "温度 24 度"])
found = service.conflicts("wife")
assert [conflict.terms for conflict in found] == [["温度"]], (
"「温度 22 度」 has nothing behind the 22 but the unit, so the 22 is the reading and not the "
"name of a thing called 温度22 — renaming it would leave the memory stating no quantity at "
"all, and the most common Chinese phrasing would never be noticed as an update"
)
assert [(conflict.older_value, conflict.newer_value) for conflict in found] == [("22", "24")]
def test_a_budget_that_changed_is_recorded(tmp_path) -> None:
service, _ = _lane(tmp_path, ["预算 5000 元", "预算 8000 元"])
found = service.conflicts("wife")
assert [(conflict.terms, conflict.older_value, conflict.newer_value) for conflict in found] == [
(["预算"], "5000", "8000")
], "a two-character subject is the common case, not an edge case: the number after it moves"
def test_a_latin_reading_with_nothing_after_it_is_a_value_too(tmp_path) -> None:
service, _ = _lane(tmp_path, ["The thermostat 22 degrees", "The thermostat 24 degrees"])
assert len(service.conflicts("wife")) == 1, (
"the same rule has to hold in both scripts or it is a rule about Chinese, not about memories"
)
def test_an_identifier_with_no_reading_after_it_reads_as_a_quantity(tmp_path) -> None:
service, _ = _lane(tmp_path, ["Room 3 is big", "Room 4 is big"])
assert len(service.conflicts("wife")) == 1, (
"a label cannot tell an identifier from a reading: with no second number in the sentence the "
"3 and 4 are compared as one quantity that moved, so this pair is recorded and the holder "
"settles it — the boundary the rule does not cross, and the price of not hiding a disagreement"
)