[FIX] The wake digest read an anchor case its producer was never told to emit

The digest read `PULLING THREAD` and `LITERAL QUESTION` with a case-sensitive
`str.find`. /wrap-up names these fields in prose — "The pulling thread",
"**Pulling thread:**" — and has never mandated a case. The reader was demanding
a shape its own producer had no instruction to write, so the wrap wrote
correctly and the digest reported DEGRADED.

Censused by running the real function over all 195 wrap files rather than by
grep: 102 threads and 103 questions unreadable, of which 86 and 84 were present
in the body in the wrong case. The remaining 16 and 19 are wraps that never
wrote the field. After the fix: 16 and 19. Predicted and achieved agree.

Second defect, distinct and found only because the first was traced to its
class: the label terminator was "first colon within 40 characters", a proxy for
"same line". When an aside pushed the colon past the window the function
returned THE LABEL, and with the paragraph cut assuming label and content share
a paragraph, a question written below its heading was invisible. The 2026-08-22
wrap hit both defects at once. The terminator is now the first colon on the
anchor's own line, and a label that ends its line takes the paragraph below.

Case-insensitivity alone would have let a narrative "…as the pulling thread
showed…" outrank the field it describes, so a match in label position now wins
over an earlier mention. Four checks added (71 -> 75), including that negative
control and a positive control proving it cannot pass vacuously.

Not changed: file selection (ledgers were already excluded correctly, line 124),
frontmatter stripping, and the verbatim-never-summarised contract. The ladder's
"61 checks" is stale — it was 71 before this commit — and stays stale: the
ladder is frozen under REVIEWED-123 and this is not an exemption.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JQKeKY9T9d95KpvHwwok8T
This commit is contained in:
David F Glidden
2026-08-23 09:55:21 +02:00
co-authored by Claude Opus 5
parent 028365dbd7
commit 5a350c7059
+64 -8
View File
@@ -100,16 +100,50 @@ def extract_anchor(text, anchor, limit=900):
paraphrases the steward's own words is the lossy extraction this whole paraphrases the steward's own words is the lossy extraction this whole
architecture is designed to avoid. architecture is designed to avoid.
Handles both wrap shapes: `**ANCHOR: content**` (label and content share the Matching is CASE-INSENSITIVE. /wrap-up names these fields in prose ("The
bold run) and `**ANCHOR for next-Claude:** content` (label closes first).""" pulling thread", "**Pulling thread:**") and has never mandated a case, so a
case-sensitive reader demands of the wrap a shape its own producer was never
told to emit. Censused 2026-08-23 by running this function over all 195 wrap
files: 86 threads and 84 questions sat in the body, correctly written, and
were unreadable for that reason alone — including the last three wraps.
Shapes handled (the four the wraps actually emit):
**ANCHOR:** content label closes first
**ANCHOR: content** label and content share the bold run
**ANCHOR for next-Claude:** content label runs long
**ANCHOR** *(aside)*:\n\ncontent content in the NEXT paragraph
The label ends at the first ':' ON THE ANCHOR'S OWN LINE. The old 40-char
window was a proxy for "same line"; when an aside pushed the colon past it the
function returned THE LABEL and the wake inherited a heading in place of the
question (2026-08-22).
Where the anchor appears more than once, a match in LABEL POSITION wins over an
earlier narrative mention — case-insensitivity would otherwise let "…as the
pulling thread showed…" outrank the field it is describing.
"""
text = strip_frontmatter(text) text = strip_frontmatter(text)
i = text.find(anchor) cands = list(re.finditer(re.escape(anchor), text, re.I))
if i < 0: if not cands:
return None return None
s = text[i + len(anchor): i + len(anchor) + limit + 120]
c = s.find(":") def in_label_position(m):
if 0 <= c <= 40: # remainder of the label, not content bol = text.rfind("\n", 0, m.start()) + 1
s = s[c + 1:] pre = re.sub(r"(?i)\b(the|a|our)\b", "", text[bol:m.start()])
return pre.strip(" *#->\t") == ""
m = next((c for c in cands if in_label_position(c)), cands[0])
s = text[m.end():]
line, nl, rest = s.partition("\n")
c = line.find(":")
if c >= 0: # remainder of the label, not content
line = line[c + 1:]
if not line.replace("*", "").strip(): # label ended the line — content is below
s = rest.lstrip("\n")
else:
s = line + nl + rest
cut = re.search(r"\n\s*\n", s) cut = re.search(r"\n\s*\n", s)
if cut: if cut:
s = s[: cut.start()] s = s[: cut.start()]
@@ -895,6 +929,28 @@ def selftest():
extract_anchor("**A:** " + "w " * 200, "A", limit=50).endswith("…[truncated]")) extract_anchor("**A:** " + "w " * 200, "A", limit=50).endswith("…[truncated]"))
chk("extract_anchor returns None when the anchor is absent", chk("extract_anchor returns None when the anchor is absent",
extract_anchor("no anchor here", "PULLING THREAD") is None) extract_anchor("no anchor here", "PULLING THREAD") is None)
# --- 2026-08-23 [FIX]: the three shapes the last three wraps emitted, none of
# which the case-sensitive/40-char version could read. Each earned from a real
# miss, not invented: 86 threads + 84 questions across 195 wraps.
chk("extract_anchor matches case-insensitively (**The pulling thread: …**)",
extract_anchor("**The pulling thread: the beacon, 12:00Z. Run ONCE.**\n\nnext",
"PULLING THREAD")
== "the beacon, 12:00Z. Run ONCE.")
chk("extract_anchor takes the NEXT paragraph when the label ends the line",
extract_anchor("**Literal question for next-Claude** *(checkable, and the "
"REVIEWED half is not my corpus)*:\n\nHas the rate risen?\n\nz",
"LITERAL QUESTION")
== "Has the rate risen?")
chk("extract_anchor prefers a LABEL over an earlier narrative mention"
" [negative control — case-insensitivity must not let prose outrank the field]",
extract_anchor("Yesterday the pulling thread went slack.\n\n"
"**Pulling thread:** the beacon.\n\nz", "PULLING THREAD")
== "the beacon.")
chk("extract_anchor still finds a narrative-only mention when no label exists"
" [positive control — proves the check above is not passing vacuously]",
extract_anchor("Yesterday the pulling thread: went slack.\n\nz",
"PULLING THREAD")
== "went slack.")
MEMDIR = os.path.realpath(MEM) MEMDIR = os.path.realpath(MEM)
TOUCH = "~/_Dev/studium-engine/docs/the-chamber-touchstone.md" TOUCH = "~/_Dev/studium-engine/docs/the-chamber-touchstone.md"
chk("classify_pointers accepts an in-directory relative pointer", chk("classify_pointers accepts an in-directory relative pointer",