From 8a1c21223931aba9f417fa4a754fd837fd056e8a Mon Sep 17 00:00:00 2001 From: Yoav Date: Mon, 28 Sep 2026 22:27:32 -0400 Subject: [PATCH] fix(GATE-ISSUE-INDEX-TABLE-SHAPE): record checkers report every failure in one run, not the first Both record validators stopped at the first invalid input, so one run revealed one defect and each fix exposed the next only after another run. The ORPHAN-MODEL-ROWS repair met exactly this: three separate defects (two one-cell-short rows and an ownerless SPIKE row) hid behind one another, and the tool output never named more than the first. The loader's first file alphabetically is an _intake record nobody is editing, which made the pattern worse: the error a maintainer saw rarely belonged to the row they touched. Two changes, one per validator. scripts/agent-issue-index.py load_local_files now collects every record that fails parse or validation and raises one IssueRecordError naming every file with its reason, instead of raising at the first. A file that fails to read joins the same list. The record collection is only returned when every file is valid, so callers guarded by the exception see no new empty-collection state. check-agent-record.py parse_claim_rows now REPORTS a malformed row and still parses it, instead of dropping it: the ratchet counts what is on disk, duplicate detection sees the duplicate, and the per-row contract checks can add their own findings to the same run. A malformed row parses with an empty state so no state-conditional contract fires on it; the reported defects stay the shape ones. Both consumers of parse_claim_rows stay correct: audit-live-rows and check-gate-commands collect parse errors and raise before classifying rows, so their censuses only become more complete, and claim-view already rejects on any collected error. Tests: LoadLocalFilesReportsEveryInvalidFile in tests/scripts/test_agent_issue_index.py holds the two-files-one-run contract and the clean-load path. Two cases in tests/scripts/test_agent_record.py hold that a malformed row no longer corrupts the ratchet count and that a malformed duplicate is reported as both malformed AND duplicate in one run. MEASURED on this tree: check-agent-record rc=0 (ENGINE=179 MODEL=384 QUANT=87 KERNEL=60 BACKEND=90); test_agent_issue_index 13/13; test_audit_live_rows 58/58; claim-view --check rc=0. On a clean checkout of d15b1cc09 the test_agent_record suite fails identically (26 failures, 32 errors, same case set), so this change introduces no regression there; those are the deleted-helper class the RECORD-ANCHOR-UNWIRED branch repairs. ISSUE-LOCAL-01M3NC14GE995V9E6F7GTYSQJ3 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:codebuff/buffy [freebuff] --- .../ISSUE-LOCAL-01M3NC14GE995V9E6F7GTYSQJ3.md | 19 +++++ scripts/agent-issue-index.py | 32 +++++--- scripts/check-agent-record.py | 19 ++++- tests/scripts/test_agent_issue_index.py | 75 +++++++++++++++++++ tests/scripts/test_agent_record.py | 46 ++++++++++++ 5 files changed, 181 insertions(+), 10 deletions(-) create mode 100644 .agents/issues/GATE-ISSUE-INDEX-TABLE-SHAPE/ISSUE-LOCAL-01M3NC14GE995V9E6F7GTYSQJ3.md diff --git a/.agents/issues/GATE-ISSUE-INDEX-TABLE-SHAPE/ISSUE-LOCAL-01M3NC14GE995V9E6F7GTYSQJ3.md b/.agents/issues/GATE-ISSUE-INDEX-TABLE-SHAPE/ISSUE-LOCAL-01M3NC14GE995V9E6F7GTYSQJ3.md new file mode 100644 index 0000000000..ee4cf62740 --- /dev/null +++ b/.agents/issues/GATE-ISSUE-INDEX-TABLE-SHAPE/ISSUE-LOCAL-01M3NC14GE995V9E6F7GTYSQJ3.md @@ -0,0 +1,19 @@ +ID: ISSUE-LOCAL-01M3NC14GE995V9E6F7GTYSQJ3 +Title: record checkers stop at the first invalid input and hide the rest +Row: GATE-ISSUE-INDEX-TABLE-SHAPE +State: OPEN +Kind: bug +GitHub: - +Mirror: PENDING +Availability: FULL +Created: 2026-09-28 +Updated: 2026-09-28 +Closed: - + +## Problem + +Both record validators abort on the first invalid input, so one run reveals only one defect and each fix exposes the next only after a new run (this masked three separate defects behind each other in the ORPHAN-MODEL-ROWS repair). scripts/agent-issue-index.py load_local_files raises IssueRecordError at the first file whose record fails validate_issue_record, so ten broken issue files report one error and --check shows only that one. scripts/check-agent-record.py parse_claim_rows drops any row whose cell count or state cell is malformed (bare continue), which both corrupts the matrix counts AND silently removes the row from every downstream contract check (owner/claim, anchors, spec) - a row with one missing cell reports a pipe-count error and nothing else, exactly the MODEL-DSV41 shape. + +## Resolution + +- diff --git a/scripts/agent-issue-index.py b/scripts/agent-issue-index.py index ceec0a4474..92aaae6592 100755 --- a/scripts/agent-issue-index.py +++ b/scripts/agent-issue-index.py @@ -65,23 +65,37 @@ def load_local_files( owed: issue_records.OwedLookup | None = None, frozen_archive: bytes | None = None, ) -> list[issue_records.IssueRecord]: - """Parse and validate every canonical issue file without network access.""" + """Parse and validate every canonical issue file without network access. + + Every invalid file is reported in one run. Stopping at the first failure + is what let three separate defects hide behind one another in the + ORPHAN-MODEL-ROWS repair (ISSUE-LOCAL-01M3NC14GE995V9E6F7GTYSQJ3): each + fix exposed the next only after another run, and the first failure was + always an _intake record nobody was editing. + """ effective_rows = issue_records.canonical_rows(ROOT) if rows is None else rows effective_owed = issue_records.owed_issue_counts(ROOT) if owed is None else owed if frozen_archive is None: frozen_archive = FROZEN_ARCHIVE.read_bytes() records: list[issue_records.IssueRecord] = [] + failures: list[str] = [] for path in sorted(issues_root.glob("**/*.md")): - record = issue_records.parse_issue_file(path) - issue_records.validate_issue_record( - record, - path, - effective_rows, - effective_owed, - frozen_archive=frozen_archive, - ) + try: + record = issue_records.parse_issue_file(path) + issue_records.validate_issue_record( + record, + path, + effective_rows, + effective_owed, + frozen_archive=frozen_archive, + ) + except (OSError, issue_records.IssueRecordError) as error: + failures.append(f"{path}: {error}") + continue records.append(record) + if failures: + raise issue_records.IssueRecordError("; ".join(failures)) issue_records.validate_issue_collection(records) return records diff --git a/scripts/check-agent-record.py b/scripts/check-agent-record.py index 5a812e4bf2..78332cabd6 100644 --- a/scripts/check-agent-record.py +++ b/scripts/check-agent-record.py @@ -587,12 +587,20 @@ def parse_claim_rows(path: Path, errors: list[str]) -> list[ClaimRow]: item_id = cells[0].strip().strip("`") if not ID_RE.fullmatch(item_id): continue + # A row that fails its shape check is REPORTED and still parsed, never + # dropped: dropping it hid every downstream contract defect behind the + # shape error and corrupted the matrix ratchet counts, so each fix + # exposed the next defect only after another run (three defects masked + # this way in the ORPHAN-MODEL-ROWS repair; + # ISSUE-LOCAL-01M3NC14GE995V9E6F7GTYSQJ3). The one error list reports + # the shape and the contracts in the same run. + malformed = False if len(cells) != len(header): errors.append( f"{path.relative_to(ROOT)}:{line_no}: {item_id} has {len(cells)} cells; " f"header has {len(header)}" ) - continue + malformed = True state_index = field_index(header, "state") state_cell = cells[state_index] if state_index is not None else "" state_matches = STATE_RE.findall(state_cell) @@ -600,6 +608,15 @@ def parse_claim_rows(path: Path, errors: list[str]) -> list[ClaimRow]: errors.append( f"{path.relative_to(ROOT)}:{line_no}: {item_id} must have exactly one canonical state" ) + malformed = True + if malformed: + # Parsed with an empty state so the ratchet, duplicate detection + # and the summary rollups still see the row; no state-conditional + # contract can fire on the empty state, so the reported defects + # stay the shape ones. + rows.append( + ClaimRow(path, line_no, item_id, state_matches[0] if state_matches else "", header, tuple(cells), line) + ) continue rows.append( ClaimRow(path, line_no, item_id, state_matches[0], header, tuple(cells), line) diff --git a/tests/scripts/test_agent_issue_index.py b/tests/scripts/test_agent_issue_index.py index ec4fa01e68..c158cbbddf 100644 --- a/tests/scripts/test_agent_issue_index.py +++ b/tests/scripts/test_agent_issue_index.py @@ -326,5 +326,80 @@ def test_local_render_preserves_backslash_pipe_parity_in_every_free_form_cell(se ) +class LoadLocalFilesReportsEveryInvalidFile(unittest.TestCase): + """One run of the loader names EVERY invalid record, not just the first. + + The loader used to raise on the first file whose record failed, so a run + revealed one defect and each fix exposed the next only after another + run; the first file alphabetically was always an _intake record nobody + was editing. Three separate defects hid behind one another exactly this + way during the ORPHAN-MODEL-ROWS repair + (ISSUE-LOCAL-01M3NC14GE995V9E6F7GTYSQJ3). + """ + + def setUp(self) -> None: + self.mod = load_module() + + def record(self, number: int, title: str) -> records.IssueRecord: + return records.IssueRecord( + id=f"ISSUE-GH-{number}", + title=title, + row="ROW-A", + state="OPEN", + kind="bug", + github=number, + mirror="SYNCED", + availability="FULL", + created="2026-08-01", + updated="2026-08-01", + closed="-", + problem="Evidence.", + resolution="-", + ) + + def test_two_broken_files_are_both_named_in_one_run(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + issues_root = Path(temporary) / ".agents" / "issues" + row_dir = issues_root / "ROW-A" + for number, corrupt in ((7, "## Wrong"), (8, None)): + path = row_dir / f"ISSUE-GH-{number}.md" + path.parent.mkdir(parents=True, exist_ok=True) + if corrupt is None: + text = records.render_issue_record(self.record(number, "fine")) + # Break the path-owner contract only: point the record at + # a row its directory does not carry. + text = text.replace("Row: ROW-A", "Row: ROW-B") + path.write_text(text, encoding="utf-8") + else: + path.write_text(corrupt, encoding="utf-8") + with self.assertRaises(records.IssueRecordError) as caught: + self.mod.load_local_files( + issues_root, rows={"ROW-A"}, owed=set(), frozen_archive=b"" + ) + message = str(caught.exception) + self.assertIn("ISSUE-GH-7.md", message) + self.assertIn("ISSUE-GH-8.md", message) + self.assertIn("missing the exact ## Problem heading", message) + self.assertIn("path row 'ROW-A' must equal Row field ROW-B", message) + + def test_a_clean_collection_still_loads_and_validates_duplicates(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + issues_root = Path(temporary) / ".agents" / "issues" + row_dir = issues_root / "ROW-A" + row_dir.mkdir(parents=True) + for number in (7, 8): + (row_dir / f"ISSUE-GH-{number}.md").write_text( + records.render_issue_record(self.record(number, "fine")), + encoding="utf-8", + ) + records_loaded = self.mod.load_local_files( + issues_root, rows={"ROW-A"}, owed=set(), frozen_archive=b"" + ) + self.assertEqual( + [record.id for record in records_loaded], + ["ISSUE-GH-7", "ISSUE-GH-8"], + ) + + if __name__ == "__main__": unittest.main() diff --git a/tests/scripts/test_agent_record.py b/tests/scripts/test_agent_record.py index ce2e3799ac..93ad0a401e 100644 --- a/tests/scripts/test_agent_record.py +++ b/tests/scripts/test_agent_record.py @@ -2151,6 +2151,52 @@ def test_malformed_matrix_row_still_fails_for_its_own_reason(self) -> None: errors = self._check_kernel_source(source.replace(template, malformed, 1)) require(errors, r"KERNEL-CPU-A76-Q8-DOT has 7 cells; header has 8") + def test_malformed_row_is_kept_so_the_ratchet_does_not_corrupt(self) -> None: + """A dropped row silently moved the ratchet and hid the next defect. + + The ORPHAN-MODEL-ROWS repair hit exactly this: two one-cell-short + rows were dropped from the parse, so the ratchet counted a bogus + total AND every downstream contract check on those rows never ran; + each fix exposed the next defect only after another run + (ISSUE-LOCAL-01M3NC14GE995V9E6F7GTYSQJ3). The malformed row must + still be counted -- no ratchet error may appear, or the shape error + and the count error fight over which defect gets reported. + """ + source = (ROOT / ".agents/kernel-matrix.md").read_text(encoding="utf-8") + template = next( + line for line in source.splitlines() + if line.startswith("| `KERNEL-CPU-A76-Q8-DOT` |") + ) + malformed = template.rsplit(" | ", 1)[0] + " |" + errors = self._check_kernel_source(source.replace(template, malformed, 1)) + require(errors, r"KERNEL-CPU-A76-Q8-DOT has 7 cells; header has 8") + self.assertFalse( + any(re.search(r"KERNEL rows; expected", error) for error in errors), + "dropping the malformed row corrupted the ratchet count", + ) + + def test_malformed_row_is_still_seen_downstream(self) -> None: + """A malformed duplicate is reported as both malformed AND duplicate. + + Under the drop rule the shape error fired and the duplicate was + invisible: parse_claim_rows discarded the row before duplicate + detection ever saw it. One run must name both, plus the ratchet the + extra row now breaks -- three findings, one run, no re-run ladder. + """ + expected = agent_record.MATRICES["KERNEL"][1] + source = (ROOT / ".agents/kernel-matrix.md").read_text(encoding="utf-8") + template = next( + line for line in source.splitlines() + if line.startswith("| `KERNEL-CPU-A76-Q8-DOT` |") + ) + malformed = template.rsplit(" | ", 1)[0] + " |" + errors = self._check_kernel_source( + source.replace(template, template + "\n" + malformed, 1) + ) + require(errors, r"KERNEL-CPU-A76-Q8-DOT has 7 cells; header has 8") + require(errors, r"duplicate ID KERNEL-CPU-A76-Q8-DOT") + require(errors, rf"{expected + 1} KERNEL rows; expected {expected}") + def test_retired_history_payload_is_self_validating(self) -> None: self._assert_history_integrity(self.HISTORY.read_text(encoding="utf-8"))