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:
1 parent
c46d7dde33
commit
2f32c23976
2 files changed
+80
-21
No files matched your search
@@ -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`.
|
||||||
+74
-21
@@ -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 not m:
|
if m:
|
||||||
row["classes"].append("NAME_MALFORMED")
|
y, mo, d, row["recipient"] = m.groups()
|
||||||
rows.append(row)
|
legacy = True
|
||||||
continue
|
else:
|
||||||
y, mo, d, row["recipient"] = m.groups()
|
m = INTERVIEW_RE.match(name)
|
||||||
legacy = True
|
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)
|
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)
|
||||||
|
|||||||
Reference in new issue
Block a user