[HARDENING] governance-mcp: keyword search — the jurist can discover an item whose id it does not know (PENDING-86 d)

Steward-authorized 2026-08-05, completing the (a)+(d) pair the jurist asked for.

The failure this closes is NOT "cannot read item X" — (a) fixed that. It is
"cannot DISCOVER item X whose id it does not already know": the 2026-07-29 case
where a ruling demanded an outcome REVIEWED-74 had settled four days earlier, in
a file the jurist could read but had no reason to open. Keyed retrieval cannot
serve that; only search can.

`governance_search(query, limit)` over PENDING / PENDING-archive / REVIEWED.
Result unit is the ITEM, boundaries from wd.item_spans — no second definition of
"an item" (the 2026-07-28 bug that hid twenty). Results name ids to hand to
governance_item, so the two tools compose.

Three deliberate properties:

  - Terms are ANDed, and that is DISCLOSED on every result. A silently
    conjunctive matcher is exactly how recall dies as a question lengthens —
    found in the engine yesterday (PENDING-97, "what does levi mean by the gray
    zone" -> 0 over ten real matches). The same shape is not being rebuilt here
    unannounced.
  - A miss is a legible empty: it states the corpus, the item count scanned, the
    terms, and the match mode, and says outright that a longer query narrows
    fast. Silence discloses its own blindness (PENDING-96's discipline, applied
    to a new instrument on the day it was ruled).
  - Ranked by exact-phrase then raw term-count, labelled as a term COUNT and not
    a relevance score — it is a field this code actually computes.

Plus a query-INDEPENDENT structural pass: an item header hidden by leading
whitespace is invisible to item_spans, so it can never appear in results and its
absence reads as a genuine miss. Such headers are now reported beside the
results. An earlier draft flagged any uncovered matching line and drowned the
signal in each file's preamble — which is how a warning stops being read.

That pass earned itself immediately: REVIEWED-11, REVIEWED-12 and REVIEWED-74
were all indented and therefore unreachable by governance_item. REVIEWED-74 is
precisely the ruling the jurist could not find, so its failure was
over-determined — it did not know the id, AND the id would not have worked.
Steward unindented all three (REVIEWED.md is his file, not the executor's, per
Constitutional Constraint 1); items visible 78 -> 81, hidden headers now zero.

Selftest 35 -> 44 controls, 0 fail, including a negative control that goes red
if a header is ever hidden again. Live stdio round-trip confirms six tools and a
correct search result.

⚠ Requires a Claude.app restart to expose the new tool.

Refs PENDING-86 (d), PENDING-82, PENDING-96, PENDING-97.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AB3Kryoy6b1pm2Nz1DYdLh
This commit is contained in:
David F Glidden
2026-08-05 21:27:18 +02:00
co-authored by Claude Opus 5
parent b97e77aebe
commit 673823961c
+139
View File
@@ -36,8 +36,10 @@ Provenance: 2026-07-28, on PENDING-81's MCP leg. Sibling of wake-digest.py and
governance-drift-check.py; imports the first. governance-drift-check.py; imports the first.
""" """
import importlib.util import importlib.util
import inspect
import json import json
import os import os
import re
import subprocess import subprocess
import sys import sys
import time import time
@@ -160,6 +162,107 @@ def t_read(args):
+ "\n".join(chunk) + more) + "\n".join(chunk) + more)
SEARCH_KEYS = ("pending", "pending-archive", "reviewed")
def t_search(args):
"""Keyword search across the authorization corpus — PENDING-86 option (d).
The failure this exists for is NOT 'the jurist cannot read item X'. It is
'the jurist cannot DISCOVER item X whose id it does not already know' — the
2026-07-29 case where a ruling demanded an outcome REVIEWED-74 had settled
four days earlier, in a file the jurist could read but had no reason to open.
Keyed retrieval cannot fix that; only search can.
Three deliberate properties:
* The result unit is the ITEM, because the jurist's next move is
governance_item(id). Boundaries come from wd.item_spans — no second
definition of 'an item' (the 2026-07-28 bug).
* Terms are ANDed, and that is disclosed, because a silently conjunctive
matcher is exactly how recall dies as a question lengthens (the engine's
PENDING-97, found 2026-08-04). A zero-result search says what it
searched and how, so silence is legible rather than bare.
* A raw-text pass runs BESIDE the item pass and reports matches lying
outside every span item_spans can see — an indented header, a region
the parser drops. Search reveals the hole instead of papering it.
"""
q = (args.get("query") or "").strip()
if not q:
return "ERROR: query is required, e.g. 'order attestation' or 'two-column'."
terms = [t for t in q.lower().split() if t]
lim = max(1, min(int(args.get("limit") or 12), 50))
hits, scanned, orphan_lines = [], 0, []
for key in SEARCH_KEYS:
text = read(FILES[key][0])
if text is None:
continue
fname = os.path.basename(FILES[key][0])
lines = text.split("\n")
covered = set()
for head, start, end in wd.item_spans(text):
scanned += 1
covered.update(range(start, end))
body = "\n".join(lines[start - 1:end - 1])
low = body.lower()
if not all(t in low for t in terms):
continue
n = sum(low.count(t) for t in terms)
phrase = q.lower() in low
snippet = ""
for ln in body.split("\n"):
if any(t in ln.lower() for t in terms):
snippet = " ".join(ln.split())[:200]
break
hits.append((phrase, n, head, fname, start, snippet))
# Structural health pass, deliberately INDEPENDENT of the query: an item header
# hidden by leading whitespace is invisible to item_spans, so it can never appear
# in results and its absence would look like a genuine miss. Query-independent
# because the jurist needs to know the index is incomplete whatever it searched.
# Narrow by construction — an earlier draft flagged any uncovered matching line
# and drowned the signal in each file's preamble, which is exactly how a warning
# stops being read.
infence = False
for i, ln in enumerate(lines, 1):
if ln.lstrip().startswith("```"):
infence = not infence
continue
if infence or i in covered:
continue
m = re.match(r"^\s+## ((?:PENDING|REVIEWED|COMPLETED)-\S+)", ln)
if m:
orphan_lines.append((fname, i, " ".join(ln.split())[:160]))
hits.sort(key=lambda h: (not h[0], -h[1]))
mode = ("exact phrase, else all-terms-present" if len(terms) > 1
else "single term, substring")
o = [f"[governance_search {q!r} — {len(hits)} matching item(s) of {scanned} scanned "
f"across {', '.join(SEARCH_KEYS)}]",
f"match mode: ALL {len(terms)} term(s) must appear in the same item "
f"({mode}); ranked by exact-phrase first, then raw term-count — "
f"term-count is a computed field, not a relevance score.", ""]
if not hits:
o.append("NO ITEM MATCHED. This is a legible empty, not a claim that the corpus "
"is silent on the subject: the matcher is CONJUNCTIVE, so a longer query "
"narrows fast. Retry with fewer or more common terms before concluding "
"nothing is on file.")
for phrase, n, head, fname, start, snip in hits[:lim]:
o.append(f"{'★ ' if phrase else ' '}{head} [{fname}:{start}] {n} hit(s)")
if snip:
o.append(f" … {snip}")
if len(hits) > lim:
o.append(f"\n[… {len(hits) - lim} more — raise limit (max 50)]")
if orphan_lines:
o.append("\n⚠ MATCHES OUTSIDE ANY ITEM THE PARSER CAN SEE — a header hidden by "
"leading whitespace, or a region item_spans drops. governance_item() "
"cannot retrieve these by id; they are reported here so the gap is "
"visible rather than silent:")
for fname, i, ln in orphan_lines[:10]:
o.append(f" {fname}:{i} {ln}")
o.append("\nNext: governance_item(id) for the verbatim body of any hit above.")
return "\n".join(o)
def t_drift(_args): def t_drift(_args):
"""Full drift-check output. Reports its own non-verification rather than a clean bill.""" """Full drift-check output. Reports its own non-verification rather than a clean bill."""
out = wd.sh([sys.executable, wd.DRIFT], timeout=30) out = wd.sh([sys.executable, wd.DRIFT], timeout=30)
@@ -207,6 +310,17 @@ TOOLS = [
"offset": {"type": "integer", "description": "0-based first line (default 0)."}, "offset": {"type": "integer", "description": "0-based first line (default 0)."},
"limit": {"type": "integer", "description": "Lines to return, max 2000 (default 400)."}}, "limit": {"type": "integer", "description": "Lines to return, max 2000 (default 400)."}},
"required": ["file"]}), "required": ["file"]}),
("governance_search", t_search,
"Keyword search across PENDING.md, PENDING-archive.md and REVIEWED.md. Use this to "
"DISCOVER a relevant prior item or ruling whose id you do not already know — the case "
"governance_item cannot serve, because it needs the id up front. Terms are ANDed "
"within a single item; results name ids to pass to governance_item.",
{"type": "object", "properties": {
"query": {"type": "string",
"description": "Words that must all appear in the same item, e.g. "
"'order attestation' or 'two-column'. Not a regex."},
"limit": {"type": "integer", "description": "Max items to list, max 50 (default 12)."}},
"required": ["query"]}),
("drift_report", t_drift, ("drift_report", t_drift,
"Full governance-drift-check output: claims in CLAUDE.md the substrate contradicts. " "Full governance-drift-check output: claims in CLAUDE.md the substrate contradicts. "
"Detection only — correcting doctrine requires steward authorization.", "Detection only — correcting doctrine requires steward authorization.",
@@ -386,6 +500,31 @@ def selftest():
chk("a first-page read lands in the SUPERSEDED region [negative control: proves the " chk("a first-page read lands in the SUPERSEDED region [negative control: proves the "
"trap is real, not hypothetical]", "trap is real, not hypothetical]",
"(obsoleted)" in t_read({"file": "chamber-spec", "limit": 300})) "(obsoleted)" in t_read({"file": "chamber-spec", "limit": 300}))
print("\nPENDING-86 (d) — discovery without knowing the id:")
_s = t_search({"query": "added-side fabrication"})
chk("search finds REVIEWED-74 from CONTENT alone [the 2026-07-29 failure: the jurist "
"demanded an outcome this ruling had settled four days earlier, and could not find it]",
"REVIEWED-74" in _s)
chk("search spans the archive too, not just open items",
"PENDING-archive.md" in _s)
chk("results carry ids to hand to governance_item [the two tools compose]",
"governance_item(id)" in _s)
_e = t_search({"query": "zzzq nonexistent phrase"})
chk("a miss is a LEGIBLE empty, not a bare one [it discloses the conjunctive matcher, "
"the failure shape that killed engine recall — PENDING-97]",
"0 matching item(s)" in _e and "CONJUNCTIVE" in _e)
chk("the match mode is stated on EVERY result, hit or miss [positive control for the above]",
"match mode:" in _s and "match mode:" in _e)
chk("empty query refused rather than matching everything",
t_search({"query": " "}).startswith("ERROR: query is required"))
chk("search takes no path and no file key — corpus is a fixed tuple",
set(SEARCH_KEYS).issubset(FILES) and len(SEARCH_KEYS) == 3)
chk("no second definition of 'an item' — search reuses wd.item_spans [the 07-28 bug]",
"wd.item_spans" in inspect.getsource(t_search))
chk("orphan pass reports NOTHING today [the three indented headers were unhidden "
"2026-08-05; this control goes red if a header is ever hidden again]",
"MATCHES OUTSIDE ANY ITEM" not in t_search({"query": "the"}))
chk("repo_activity refuses an unlisted repo", chk("repo_activity refuses an unlisted repo",
t_repo({"repo": "/etc"}).startswith("ERROR: unknown repo")) t_repo({"repo": "/etc"}).startswith("ERROR: unknown repo"))
chk("repo_activity ACCEPTS a listed repo [positive control]", chk("repo_activity ACCEPTS a listed repo [positive control]",