From 5a350c7059cb83c714d5e1f3231657b2a12c0ccd Mon Sep 17 00:00:00 2001 From: David F Glidden Date: Sun, 23 Aug 2026 09:55:21 +0200 Subject: [PATCH] [FIX] The wake digest read an anchor case its producer was never told to emit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01JQKeKY9T9d95KpvHwwok8T --- scripts/wake-digest.py | 72 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 8 deletions(-) diff --git a/scripts/wake-digest.py b/scripts/wake-digest.py index 85b8d0b..4aea6b4 100755 --- a/scripts/wake-digest.py +++ b/scripts/wake-digest.py @@ -100,16 +100,50 @@ def extract_anchor(text, anchor, limit=900): paraphrases the steward's own words is the lossy extraction this whole architecture is designed to avoid. - Handles both wrap shapes: `**ANCHOR: content**` (label and content share the - bold run) and `**ANCHOR for next-Claude:** content` (label closes first).""" + Matching is CASE-INSENSITIVE. /wrap-up names these fields in prose ("The + 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) - i = text.find(anchor) - if i < 0: + cands = list(re.finditer(re.escape(anchor), text, re.I)) + if not cands: return None - s = text[i + len(anchor): i + len(anchor) + limit + 120] - c = s.find(":") - if 0 <= c <= 40: # remainder of the label, not content - s = s[c + 1:] + + def in_label_position(m): + bol = text.rfind("\n", 0, m.start()) + 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) if cut: s = s[: cut.start()] @@ -895,6 +929,28 @@ def selftest(): extract_anchor("**A:** " + "w " * 200, "A", limit=50).endswith("…[truncated]")) chk("extract_anchor returns None when the anchor is absent", 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) TOUCH = "~/_Dev/studium-engine/docs/the-chamber-touchstone.md" chk("classify_pointers accepts an in-directory relative pointer",