From 2f32c239763c5c919ffa83ec01c006d0ad6dc379 Mon Sep 17 00:00:00 2001 From: Volt Date: Mon, 19 Oct 2026 23:16:42 +0000 Subject: [PATCH] tools (volt): reply_check learns the reported-interview form, so the gate's reply-headings leg is green again (41 -> 49 cells) Timmy's c46d7dd filed replies/2026-10-19-to-zephyrus-interview.md, whose name carries no number by construction. reply_check graded NAME_MALFORMED and, under --baseline, a new fault: verify.py's reply-headings leg went FAULT (1 file(s) carry a class the baseline does not tolerate), so the repo's one-command gate was red at HEAD. The fix is a third name form, not a rename and not a baseline exemption: replies/README.md's numbered contract is about replies, and an interview is not one. INTERVIEW_RE accepts YYYY-MM-DD-to--interview.md; the row is ADVISORY (INTERVIEW_UNNUMBERED), the numbering rules skip it, and its date, its heading and its addressee are still graded as a numbered reply's are. A misspelled slug stays NAME_MALFORMED. 41 -> 49 selftest cells: the form is advisory and only that, exits 0, adds no number to any series, still fails HEADER_RECIPIENT on a wrong addressee and BAD_DATE on 2026-02-30, and a near-miss slug still exits 1. Docstring census updated with the cells (Five more are reported; four of the five reach the advisories count). --repo . --baseline tools/reply_baseline.tsv: 196 file(s), 0 new fault(s), 1 known-faulting, rc 0. Bare run rc 1 with faults: 1, advisories: 20 (194 + 6). --- replies/README.md | 6 +++ tools/reply_check.py | 95 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 80 insertions(+), 21 deletions(-) diff --git a/replies/README.md b/replies/README.md index 3a11946..40261a2 100644 --- a/replies/README.md +++ b/replies/README.md @@ -5,3 +5,9 @@ addresses and `` is the next free integer in that recipient's own series. The inside the file, and the commit carries them. Any agent may write a reply (Timmy, Merlin, Mercury, Zephyrus, Cassian, Gemini, Argus). Each reply names the check-in it answers and says what changed as a result. + +One artifact in this directory is not a reply: a reported interview is filed as +`YYYY-MM-DD-to--interview.md`, with `-interview` where the number goes. It has no +number by construction, so `tools/reply_check.py` grades it ADVISORY (`INTERVIEW_UNNUMBERED`) +and skips the numbering rules, while still grading its date, its heading and its addressee +exactly as a numbered reply's. Any other non-numeric suffix is still `NAME_MALFORMED`. diff --git a/tools/reply_check.py b/tools/reply_check.py index f473395..ee68807 100644 --- a/tools/reply_check.py +++ b/tools/reply_check.py @@ -22,9 +22,10 @@ WHAT IT MEASURES (read-only; the only write path is --json) file says about itself. Seven classes are graded: NAME_MALFORMED the filename is neither the numbered contract form nor - the one unnumbered legacy form -- the row is skipped - by the header rules, because there is no name to check - it against. + either of the two unnumbered forms (the legacy file, + the reported interview) -- the row is skipped by the + header rules, because there is no name to check it + against. BAD_DATE the date half is not a real calendar date. NO_HEADER no `# ` heading in the first --head-lines lines. UNREADABLE the file is not UTF-8 text, so no heading can be read @@ -39,14 +40,19 @@ WHAT IT MEASURES (read-only; the only write path is --json) FAULT: "the next free integer in that recipient's own series" cannot produce the same integer twice. - Four more are reported and printed, and are ADVISORY by construction -- each + Five more are reported and printed, and are ADVISORY by construction -- each is a stated consequence of history that predates the naming rule, not a - defect this tool can attribute to a writer. Three of the four reach the + defect this tool can attribute to a writer. Four of the five reach the `advisories: N` count; HOLE rides the `series:` line instead, because a hole belongs to a recipient's series and not to any one file: LEGACY_UNNUMBERED the one legacy form (no `-`). Its number is implicit in its position, so numbering rules skip it. + INTERVIEW_UNNUMBERED the reported-interview form, `-interview` in the + number's place. It is not a reply and carries no + number by construction, so the numbering rules skip it + -- but its date, its heading and its addressee are + still graded exactly as a numbered reply's are. NO_RECIPIENT_IN_HEADING a heading that names no addressee at all (the early "`# Reply 14 -- Q31 ...`" house style). Nothing contradicts the filename; the heading is silent. @@ -88,14 +94,16 @@ BASELINE -- the archive's one fault, stated instead of hidden The ledger names the tolerance, and the row stays visible in every report. SELFTEST - `--selftest` grades 41 cells, building every fixture in a fresh temporary + `--selftest` grades 49 cells, building every fixture in a fresh temporary directory: each fault class fires on a fixture that carries it and only the classes that belong there (a wrong addressee, a wrong number, a duplicate number, an impossible date, a missing heading, a malformed name, non-UTF-8 - bytes), the advisory classes stay advisory and exit 0 (legacy form, hole, - heading that names nobody), the prefix match accepts a hyphenated handle, - README.md is not a row, and both rc 2 paths (missing directory, file where a - directory belongs) answer 2. Four cells grade this docstring's own census -- + bytes), the advisory classes stay advisory and exit 0 (legacy form, interview + form, hole, heading that names nobody), the prefix match accepts a hyphenated + handle, the interview form still fails on a wrong addressee or an impossible + date and a misspelled `-interview` is still NAME_MALFORMED, README.md is not + a row, and both rc 2 paths (missing directory, file where a directory + belongs) answer 2. Four cells grade this docstring's own census -- the two spelled class counts against FAULTS/ADVISORIES, every graded class named here, and the pre-fix words refused on a fixture. A rule that stops firing and a rule that fires everywhere both fail a cell. @@ -113,6 +121,7 @@ import tempfile NAME_RE = re.compile(r"^(\d{4})-(\d{2})-(\d{2})-to-([a-z][a-z0-9]*)-(\d{2,})\.md$") LEGACY_RE = re.compile(r"^(\d{4})-(\d{2})-(\d{2})-to-([a-z][a-z0-9]*)\.md$") +INTERVIEW_RE = re.compile(r"^(\d{4})-(\d{2})-(\d{2})-to-([a-z][a-z0-9]*)-interview\.md$") HEADING_TO = re.compile(r"\bto\s+([A-Za-z][A-Za-z0-9-]*)") HEADING_ARROW = re.compile(r"(?:→|->)\s*([A-Za-z][A-Za-z0-9-]*)") HEADING_NUM = re.compile(r"\breply\s+(\d{1,4})\b", re.IGNORECASE) @@ -120,8 +129,8 @@ ROSTER_HANDLE_RE = re.compile(r"^[a-z][a-z0-9]*$") FAULTS = ("NAME_MALFORMED", "BAD_DATE", "NO_HEADER", "UNREADABLE", "HEADER_RECIPIENT", "HEADER_NUMBER", "DUPLICATE_NUMBER") -ADVISORIES = ("LEGACY_UNNUMBERED", "NO_RECIPIENT_IN_HEADING", - "NO_NUMBER_IN_HEADING", "HOLE") +ADVISORIES = ("LEGACY_UNNUMBERED", "INTERVIEW_UNNUMBERED", + "NO_RECIPIENT_IN_HEADING", "NO_NUMBER_IN_HEADING", "HOLE") WORD_NUMBERS = {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10, "eleven": 11, @@ -277,8 +286,8 @@ def scan(dirpath, head_lines=10, roster=()): """(rows, faults, advisories, series, holes) for one replies directory. rows: one dict per file, in sorted filename order, each with `file`, - `date`, `recipient`, `number` (None for the legacy form), `heading`, and - `classes` (a list). Nothing is written. + `date`, `recipient`, `number` (None on both unnumbered forms), `heading`, + and `classes` (a list). Nothing is written. """ rows = [] for name in sorted(os.listdir(dirpath)): @@ -290,23 +299,30 @@ def scan(dirpath, head_lines=10, roster=()): row = {"file": name, "date": None, "recipient": None, "number": None, "heading": None, "classes": []} m = NAME_RE.match(name) - legacy = False + legacy = interview = False if m: y, mo, d, row["recipient"], num = m.groups() row["number"] = int(num) else: m = LEGACY_RE.match(name) - if not m: - row["classes"].append("NAME_MALFORMED") - rows.append(row) - continue - y, mo, d, row["recipient"] = m.groups() - legacy = True + if m: + y, mo, d, row["recipient"] = m.groups() + legacy = True + else: + m = INTERVIEW_RE.match(name) + if not m: + row["classes"].append("NAME_MALFORMED") + rows.append(row) + continue + y, mo, d, row["recipient"] = m.groups() + interview = True row["date"] = "%s-%s-%s" % (y, mo, d) if not _real_date(y, mo, d): row["classes"].append("BAD_DATE") if legacy: row["classes"].append("LEGACY_UNNUMBERED") + if interview: + row["classes"].append("INTERVIEW_UNNUMBERED") heading = read_heading(path, head_lines) unreadable = heading is False row["heading"] = heading if isinstance(heading, str) else None @@ -588,6 +604,43 @@ def selftest(): classes(d, "2026-09-24-to-zephyrus.md") == ["LEGACY_UNNUMBERED"]) cell("legacy form exits 0", rc(["--dir", d]) == 0) + # 7b the reported-interview form: no number by construction, so the + # numbering rules skip it -- but everything a name can still contradict + # (its date, its addressee) is graded exactly as on a numbered reply. + d = os.path.join(root, "interview") + os.makedirs(d) + _write(d, "2026-10-19-to-zephyrus-interview.md", + "# Interview to zephyrus -- 2026-10-19\n\nbody\n") + cell("interview form is advisory, and only that", + classes(d, "2026-10-19-to-zephyrus-interview.md") == ["INTERVIEW_UNNUMBERED"]) + cell("interview form exits 0", rc(["--dir", d]) == 0) + _, _, _, series_i, holes_i = scan(d) + cell("interview form contributes no number to any series", + series_i == {} and holes_i == {}) + + d = os.path.join(root, "interview-wrongparty") + os.makedirs(d) + _write(d, "2026-10-19-to-zephyrus-interview.md", + "# Interview to Merlin -- 2026-10-19\n") + cell("interview heading still grades its addressee", + classes(d, "2026-10-19-to-zephyrus-interview.md") == + ["INTERVIEW_UNNUMBERED", "HEADER_RECIPIENT"]) + cell("interview with the wrong addressee exits 1", rc(["--dir", d]) == 1) + + d = os.path.join(root, "interview-baddate") + os.makedirs(d) + _write(d, "2026-02-30-to-zephyrus-interview.md", "# Interview to zephyrus\n") + cell("interview form still grades its date", + classes(d, "2026-02-30-to-zephyrus-interview.md") == + ["BAD_DATE", "INTERVIEW_UNNUMBERED"]) + + d = os.path.join(root, "interview-nearmiss") + os.makedirs(d) + _write(d, "2026-10-19-to-zephyrus-inteview.md", "# Interview to zephyrus\n") + cell("a misspelled interview slug is still NAME_MALFORMED", + classes(d, "2026-10-19-to-zephyrus-inteview.md") == ["NAME_MALFORMED"]) + cell("a misspelled interview slug exits 1", rc(["--dir", d]) == 1) + # 8 a real-calendar check, not a shape check d = os.path.join(root, "baddate") os.makedirs(d)