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 000000000..ee4cf6274 --- /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 ceec0a447..92aaae659 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 5a812e4bf..78332cabd 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 ec4fa01e6..c158cbbdd 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 ce2e3799a..93ad0a401 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"))