Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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

-
32 changes: 23 additions & 9 deletions scripts/agent-issue-index.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 18 additions & 1 deletion scripts/check-agent-record.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,19 +587,36 @@ 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)
if len(state_matches) != 1:
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)
Expand Down
75 changes: 75 additions & 0 deletions tests/scripts/test_agent_issue_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
46 changes: 46 additions & 0 deletions tests/scripts/test_agent_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand Down
Loading