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-<recipient>-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).
This commit is contained in:
volt committed 2026-10-19 23:16:42 +00:00
1 parent c46d7dde33
commit 2f32c23976
2 files changed
+75 -16

No files matched your search

+6
View File
@@ -5,3 +5,9 @@ addresses and `<N>` 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, 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 Mercury, Zephyrus, Cassian, Gemini, Argus). Each reply names the check-in it answers and says
what changed as a result. what changed as a result.
One artifact in this directory is not a reply: a reported interview is filed as
`YYYY-MM-DD-to-<recipient>-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`.
+69 -16
View File
@@ -22,9 +22,10 @@ WHAT IT MEASURES (read-only; the only write path is --json)
file says about itself. Seven classes are graded: file says about itself. Seven classes are graded:
NAME_MALFORMED the filename is neither the numbered contract form nor NAME_MALFORMED the filename is neither the numbered contract form nor
the one unnumbered legacy form -- the row is skipped either of the two unnumbered forms (the legacy file,
by the header rules, because there is no name to check the reported interview) -- the row is skipped by the
it against. header rules, because there is no name to check it
against.
BAD_DATE the date half is not a real calendar date. BAD_DATE the date half is not a real calendar date.
NO_HEADER no `# ` heading in the first --head-lines lines. NO_HEADER no `# ` heading in the first --head-lines lines.
UNREADABLE the file is not UTF-8 text, so no heading can be read 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 FAULT: "the next free integer in that recipient's own
series" cannot produce the same integer twice. 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 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 `advisories: N` count; HOLE rides the `series:` line instead, because a hole
belongs to a recipient's series and not to any one file: belongs to a recipient's series and not to any one file:
LEGACY_UNNUMBERED the one legacy form (no `-<N>`). Its number is implicit LEGACY_UNNUMBERED the one legacy form (no `-<N>`). Its number is implicit
in its position, so numbering rules skip it. 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 NO_RECIPIENT_IN_HEADING a heading that names no addressee at all (the
early "`# Reply 14 -- Q31 ...`" house style). Nothing early "`# Reply 14 -- Q31 ...`" house style). Nothing
contradicts the filename; the heading is silent. 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. The ledger names the tolerance, and the row stays visible in every report.
SELFTEST 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 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 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 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, bytes), the advisory classes stay advisory and exit 0 (legacy form, interview
heading that names nobody), the prefix match accepts a hyphenated handle, form, hole, heading that names nobody), the prefix match accepts a hyphenated
README.md is not a row, and both rc 2 paths (missing directory, file where a handle, the interview form still fails on a wrong addressee or an impossible
directory belongs) answer 2. Four cells grade this docstring's own census -- 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 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 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. 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$") 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$") 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_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_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) 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", FAULTS = ("NAME_MALFORMED", "BAD_DATE", "NO_HEADER", "UNREADABLE",
"HEADER_RECIPIENT", "HEADER_NUMBER", "DUPLICATE_NUMBER") "HEADER_RECIPIENT", "HEADER_NUMBER", "DUPLICATE_NUMBER")
ADVISORIES = ("LEGACY_UNNUMBERED", "NO_RECIPIENT_IN_HEADING", ADVISORIES = ("LEGACY_UNNUMBERED", "INTERVIEW_UNNUMBERED",
"NO_NUMBER_IN_HEADING", "HOLE") "NO_RECIPIENT_IN_HEADING", "NO_NUMBER_IN_HEADING", "HOLE")
WORD_NUMBERS = {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6, WORD_NUMBERS = {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6,
"seven": 7, "eight": 8, "nine": 9, "ten": 10, "eleven": 11, "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, faults, advisories, series, holes) for one replies directory.
rows: one dict per file, in sorted filename order, each with `file`, rows: one dict per file, in sorted filename order, each with `file`,
`date`, `recipient`, `number` (None for the legacy form), `heading`, and `date`, `recipient`, `number` (None on both unnumbered forms), `heading`,
`classes` (a list). Nothing is written. and `classes` (a list). Nothing is written.
""" """
rows = [] rows = []
for name in sorted(os.listdir(dirpath)): 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, row = {"file": name, "date": None, "recipient": None, "number": None,
"heading": None, "classes": []} "heading": None, "classes": []}
m = NAME_RE.match(name) m = NAME_RE.match(name)
legacy = False legacy = interview = False
if m: if m:
y, mo, d, row["recipient"], num = m.groups() y, mo, d, row["recipient"], num = m.groups()
row["number"] = int(num) row["number"] = int(num)
else: else:
m = LEGACY_RE.match(name) m = LEGACY_RE.match(name)
if m:
y, mo, d, row["recipient"] = m.groups()
legacy = True
else:
m = INTERVIEW_RE.match(name)
if not m: if not m:
row["classes"].append("NAME_MALFORMED") row["classes"].append("NAME_MALFORMED")
rows.append(row) rows.append(row)
continue continue
y, mo, d, row["recipient"] = m.groups() y, mo, d, row["recipient"] = m.groups()
legacy = True interview = True
row["date"] = "%s-%s-%s" % (y, mo, d) row["date"] = "%s-%s-%s" % (y, mo, d)
if not _real_date(y, mo, d): if not _real_date(y, mo, d):
row["classes"].append("BAD_DATE") row["classes"].append("BAD_DATE")
if legacy: if legacy:
row["classes"].append("LEGACY_UNNUMBERED") row["classes"].append("LEGACY_UNNUMBERED")
if interview:
row["classes"].append("INTERVIEW_UNNUMBERED")
heading = read_heading(path, head_lines) heading = read_heading(path, head_lines)
unreadable = heading is False unreadable = heading is False
row["heading"] = heading if isinstance(heading, str) else None 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"]) classes(d, "2026-09-24-to-zephyrus.md") == ["LEGACY_UNNUMBERED"])
cell("legacy form exits 0", rc(["--dir", d]) == 0) 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 # 8 a real-calendar check, not a shape check
d = os.path.join(root, "baddate") d = os.path.join(root, "baddate")
os.makedirs(d) os.makedirs(d)