#!/usr/bin/env python3 """ Reduction arm — tile a document into taggable units, and gate every claim about it. WHY THIS EXISTS Control Kernel v1.0 (frozen 2026-08-02, sha256 67c9b870…) requires that every sentence of a control document carry exactly one tag. "Every sentence" is only meaningful relative to a declared splitter, so the splitter is part of the record — §3.1 of the kernel says so explicitly. The reduction arm exists to FALSIFY the kernel, not to ratify it. It runs before the synthetic arm because a generated corpus can only confirm whatever the kernel already believes. THE TILING INVARIANT Spans TILE the document: concatenating every span in order reproduces the source byte-for-byte. Nothing is dropped, nothing is silently normalised. This is what makes non-destructive quarantine checkable rather than promised — laundering a document means changing it, and a change that preserves the tiling must appear in the ledger. Assertive spans need a tag. Structural spans (blank lines, fences, table rows, list bullets) do not, and are marked so the distinction is visible rather than implicit. USAGE ./reduce.py split → doc.units.jsonl (+ tiling gate) ./reduce.py check → kernel §3 checks """ from __future__ import annotations import argparse import hashlib import json import re import sys from pathlib import Path # Bumped whenever unit boundaries could change. A tag file is only valid against # the splitter version that produced its units. # 1.1.0 — three defects found by contact with a real jurist ruling, not by review: # (a) a `##` line INSIDE a fenced block was kinded `heading` and made taggable, # because heading was tested before code. Quoted content is not structure. # (b) `---` rules were `block` and taggable. A horizontal rule is not a sentence. # (c) a `?` inside a quotation split a sentence mid-clause, yielding a FRAGMENT # ("…asserts to be true?" | "alone — is less safe…"). Tagging a fragment is # meaningless, so it must not be produced. # 1.2.0 — two further defects, again found by contact rather than review, this # time on a package rather than a ruling: # (d) a numbered marker ("**1.", "2.") was read as a sentence end, orphaning the # marker as a fragment and decapitating the sentence after it. # (e) YAML frontmatter was treated as flowing prose and shredded mid-key # (`…quoted verbatim below." status: "DRAFT.`). Frontmatter is line-oriented. # It stays TAGGABLE — it carries real assertions about the document, and # excluding it would quietly shrink the quarantine in the author's favour. SPLITTER_VERSION = "1.2.0" # The kernel this tool enforces. It was left at v1.0's hash after v1.1 was # frozen, so every run record printed a provenance line that named the wrong # governing document — the tool asserting a fact about itself that the substrate # contradicted, which is the class this whole programme exists to catch. KERNEL_VERSION = "1.1" KERNEL_SHA256 = "d4b48db23612b30ff66e26b6235065a3c2f3c9be19d750dafc97e80a1329974d" KERNEL_FILE = "CONTROL-KERNEL-v1.1.md" TAGS = {"D", "Q", "A", "N", "X"} # `!` is NOT a kernel tag. It is the reduction's record that a unit cannot be # typed under Kernel v1.0 and must therefore leave the sound remainder. Quarantine # is non-destructive: the unit stays in the units file and in the census, with its # reason, so what was removed is inspectable rather than silently absent. QUARANTINE = "!" QUARANTINE_REASONS = { # First-person report of an act performed outside the document. Cannot be # demonstrated in-document by construction ("I read ~/CLAUDE.md in full"). "TESTIMONY", # A determination constituted by being uttered, not by being argued # ("Design gate PASSED", "AFFIRMED", "Keep both clauses"). "PERFORMATIVE", # Faithfully derived from a §1 axiom source but not verbatim, so it cannot be # `Q`; and not argued in-document, so it cannot be `D`. "PARAPHRASE", # A factual claim about the world or another document, not traceable to a §1 # source at all. "UNSOURCED-FACT", # Quotation of a party that is not a §1 axiom source. "UNSOURCED-QUOTE", # §2c — more than one primitive in a single sentence, unsplittable without # editing the source, which the reduction arm may not do silently. "BLEND", # Rests on a quarantined or `A` unit, so §2's transitivity clause forbids `D`. "INHERITED", } # Kernel §3.3 — a control document may not collect its caveats into a section. # Widened after a FALSE PASS on a real package: "Part VII — Disconfirming # evidence, which the steward specifically asked to be carried" is a collected # limitations section in §2a's sense — trial 03 showed the model skipping exactly # that section wholesale — and the original pattern did not match it because it # never says "limitations". # # THIS CHECK IS A SCREEN, NOT A DECISION. No pattern can decide whether a section # collects the author's own caveats; a section titled "Part VII" alone would defeat # any wordlist. §2a compliance therefore belongs in Kernel §4's judgement residue, # and a pass here means only that the obvious namings were absent. FORBIDDEN_HEADING_RE = re.compile( r"limitation|caveat|assumption|what this does not|open question" r"|disconfirming|evidence against|weakness|objection|counter-?argument" r"|self-?critique|known (?:issue|gap|problem)|shortcoming|scope boundary", re.IGNORECASE, ) # Abbreviations after which a period does NOT end a sentence. Deliberately short: # every entry is a judgement about English, and this list is part of the trusted # base in the same way the tags are. ABBREVIATIONS = { "e.g", "i.e", "cf", "vs", "etc", "al", "no", "vol", "pp", "ch", "Mr", "Mrs", "Ms", "Dr", "St", "Prof", "Fig", "approx", } _SENT_END = re.compile(r"([.!?])([\"'’”\)\]]*)(\s+)") def sha256(text: str) -> str: return hashlib.sha256(text.encode("utf-8")).hexdigest() def _is_abbrev(text: str, dot_index: int) -> bool: """True if the period at dot_index closes a known abbreviation.""" start = dot_index while start > 0 and (text[start - 1].isalnum() or text[start - 1] == "."): start -= 1 return text[start:dot_index].rstrip(".") in ABBREVIATIONS def _is_enumerator(text: str, dot_index: int) -> bool: """ True if the period at dot_index closes a numbered marker such as `1.` or `**2.` rather than a sentence. Deliberately narrow: at most two digits, and nothing before them on the line except markdown emphasis or whitespace. A bare numeric token is NOT enough — "…formalized in 2026. The next…" is a real boundary and must stay one. """ start = dot_index while start > 0 and text[start - 1].isdigit(): start -= 1 digits = text[start:dot_index] if not (1 <= len(digits) <= 2): return False line_start = text.rfind("\n", 0, start) + 1 return text[line_start:start].strip(" \t*_>#") == "" def split_prose(block: str, offset: int) -> list[tuple[int, int]]: """ Split a prose block into sentence spans as (start, end) absolute offsets. Spans are CONTIGUOUS and cover the block exactly — trailing whitespace stays attached to the sentence it follows, so the tiling invariant holds without a separate whitespace span per gap. """ spans: list[tuple[int, int]] = [] cursor = 0 for m in _SENT_END.finditer(block): dot = m.start(1) if block[dot] == "." and (_is_abbrev(block, dot) or _is_enumerator(block, dot)): continue end = m.end() # include the closing punctuation and the following space # A sentence-ending mark inside a quotation is usually not the end of the # sentence: `collapse to "does it change what X asserts?" alone — is less # safe` is one sentence, and splitting it produced a fragment. English # sentences do not open in lower case, so the following character decides. if end < len(block) and block[end].islower(): continue spans.append((offset + cursor, offset + end)) cursor = end if cursor < len(block): spans.append((offset + cursor, offset + len(block))) return spans def split_spans(text: str) -> list[dict]: """ Tile `text` into spans. Guarantees sum(spans) == text, byte for byte. Line-oriented, because Markdown structure is line-oriented: headings, list items, table rows, blank lines and fenced code are decided per line, and only paragraph prose is split into sentences. """ spans: list[dict] = [] pos = 0 in_fence = False lines = text.splitlines(keepends=True) # YAML frontmatter: a `---` on the very first line opens it, the next `---` # closes it. Line-oriented, so it must not flow into the prose splitter. fm_end = -1 if lines and lines[0].strip() == "---": for i in range(1, len(lines)): if lines[i].strip() == "---": fm_end = i break para: list[str] = [] para_start = 0 def flush_para() -> None: nonlocal para, para_start if not para: return block = "".join(para) for s, e in split_prose(block, para_start): spans.append({"kind": "prose", "start": s, "end": e}) para = [] for lineno, line in enumerate(lines): stripped = line.strip() if 0 < lineno < fm_end: flush_para() spans.append({"kind": "frontmatter", "start": pos, "end": pos + len(line)}) pos += len(line) continue fence = stripped.startswith("```") structural = ( fence or in_fence or not stripped or stripped.startswith("#") or stripped.startswith("|") or stripped.startswith(">") or re.match(r"^\s*([-*+]|\d+\.)\s", line) is not None or stripped.startswith("---") or stripped.startswith("