diff --git a/.agents/issues/POLICY-ISSUE-INTAKE/ISSUE-LOCAL-01M3NH23E78PEXCJF4HW1XHQQ3.md b/.agents/issues/POLICY-ISSUE-INTAKE/ISSUE-LOCAL-01M3NH23E78PEXCJF4HW1XHQQ3.md new file mode 100644 index 000000000..156429915 --- /dev/null +++ b/.agents/issues/POLICY-ISSUE-INTAKE/ISSUE-LOCAL-01M3NH23E78PEXCJF4HW1XHQQ3.md @@ -0,0 +1,19 @@ +ID: ISSUE-LOCAL-01M3NH23E78PEXCJF4HW1XHQQ3 +Title: frozen-evidence byte equality and record link resolution cannot both hold +Row: POLICY-ISSUE-INTAKE +State: OPEN +Kind: bug +GitHub: - +Mirror: PENDING +Availability: FULL +Created: 2026-09-28 +Updated: 2026-09-28 +Closed: - + +## Problem + +An _intake record's Problem must quote the frozen archive line byte for byte, but check-agent-record's check_links requires every link in a record to resolve from the record's own directory. The archive lives at .agents/completed/issue-index.md, one level under .agents, so a link to a spec is spelled ../specs/x.md there; the record that QUOTES the row lives at .agents/issues//, two levels down, so the same link must be spelled ../../specs/x.md from there. One string cannot satisfy both. Commit e3539d994 re-pointed the ISSUE-GH-1033 quote to the record-relative spelling to fix the dangling link, which broke the byte comparison: first divergence at column 2191 of archive line 350, archive 2237 chars against evidence 2240. Measured over all 831 records that carry a Frozen archive evidence block, 350 are byte-equal, 438 differ from the cited line ONLY by a relative link rebase, and 43 cite a line the archive no longer has (24 past its 886 lines after the delete and re-add, 19 naming a row that moved). Restoring the archive spelling makes check-links red instead. Fix: compare the quote against the cited line with each side's relative link targets resolved to the file they denote, so the check asks whether the quote IS the archived row rather than which directory holds the quote. + +## Resolution + +- diff --git a/scripts/issue_records.py b/scripts/issue_records.py index 544f34632..fe8bd52d7 100755 --- a/scripts/issue_records.py +++ b/scripts/issue_records.py @@ -13,6 +13,7 @@ from dataclasses import dataclass from datetime import date from pathlib import Path +import posixpath import re from typing import TypeAlias @@ -38,11 +39,21 @@ _LOCAL_ID = re.compile(r"ISSUE-LOCAL-([0-7][0-9A-HJKMNP-TV-Z]{25})\Z") _ROW_ID = re.compile(r"[A-Z0-9][A-Za-z0-9_.-]*\Z") _GITHUB_NUMBER = re.compile(r"[1-9][0-9]*\Z") +# The one frozen archive an intake record's Problem must quote, in +# repository-relative form. The record that HOLDS a quote lives deeper than +# the file the quote was cut from, which is why the comparison below +# resolves relative link targets instead of comparing their spelling: the +# same row is `../specs/x.md` in the archive and `../../specs/x.md` in the +# record, and both gates (this one and check-links) must be satisfiable at +# once. +FROZEN_ARCHIVE_RELPATH = ".agents/completed/issue-index.md" _INTAKE_PROBLEM = re.compile( - r"Archive: `\.agents/completed/issue-index\.md:([1-9][0-9]*)`\n\n" + r"Archive: `" + re.escape(FROZEN_ARCHIVE_RELPATH) + r":([1-9][0-9]*)`\n\n" r"### Frozen archive evidence\n\n" r"> ([^\n]+)\Z" ) +_LINK_TARGET = re.compile(r"(\[[^\]]*\]\()([^)]+)(\))") +_REMOTE_TARGET = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*:") class IssueRecordError(ValueError): @@ -303,11 +314,61 @@ def intake_archive_evidence(record: IssueRecord) -> tuple[int, str] | None: return int(match.group(1)), match.group(2) +def _resolve_relative_links(line: str, base: str) -> str: + """Rewrite every relative Markdown link target to the file it denotes. + + A relative link resolves against the file that QUOTES it, so one frozen + row cannot keep one spelling in the archive (`.agents/completed/`) and in + the record that quotes it (`.agents/issues//`). Resolving both + sides to the repository-relative path they point at is what lets the + evidence comparison ask the question it means to ask -- is this the + archived row? -- instead of which directory holds the quote. + + Remote (`https:`, `mailto:`, ...) and root-absolute (`/...`) targets are + left exactly as written: only a rebase of a relative target is + comparable, so a swapped remote URL, a truncated link, or any other + difference still fails the comparison byte for byte. + """ + + def rewrite(match: re.Match[str]) -> str: + raw = match.group(2) + lead = raw[: len(raw) - len(raw.lstrip())] + trail = raw[len(raw.rstrip()) :] + target = raw.strip() + angled = target.startswith("<") and target.endswith(">") + if angled: + target = target[1:-1] + path, separator, fragment = target.partition("#") + if ( + not path + or path.startswith(("/", "?")) + or _REMOTE_TARGET.match(path) is not None + ): + return match.group(0) + resolved = posixpath.normpath(posixpath.join(base, path)) + rendered = f"{resolved}{separator}{fragment}" + if angled: + rendered = f"<{rendered}>" + return f"{match.group(1)}{lead}{rendered}{trail}{match.group(3)}" + + return _LINK_TARGET.sub(rewrite, line) + + def _archive_evidence_matches_source( evidence: tuple[int, str], frozen_archive: bytes | None, + record_base: str = "", ) -> bool: - """Require exact UTF-8 evidence bytes at the declared one-based source line.""" + """Require the declared line to be the quote, modulo relative link rebase. + + Byte equality is tried first and answers almost every record: an archive + that has not moved and a record that copied the line verbatim need + nothing resolved. Only when the bytes differ is the quote compared with + each side's relative link targets resolved, which admits exactly the + spelling a MOVE forces (`../specs/x.md` vs `../../specs/x.md`) and + nothing else. `record_base` is the record's repository-relative + directory; with no base to resolve against, only byte equality passes. + """ if frozen_archive is None: return False @@ -315,7 +376,18 @@ def _archive_evidence_matches_source( source_lines = frozen_archive.split(b"\n") if line_number > len(source_lines): return False - return source_lines[line_number - 1] == archived_line.encode("utf-8") + source_line = source_lines[line_number - 1] + if source_line == archived_line.encode("utf-8"): + return True + if not record_base: + return False + try: + decoded = source_line.decode("utf-8") + except UnicodeDecodeError: + return False + return _resolve_relative_links( + decoded, posixpath.dirname(FROZEN_ARCHIVE_RELPATH) + ) == _resolve_relative_links(archived_line, record_base) def _archive_row_owner(line: str, github: int | None) -> str | None: @@ -342,13 +414,20 @@ def _archive_row_owner(line: str, github: int | None) -> str | None: def valid_intake_archive_evidence( record: IssueRecord, frozen_archive: bytes | None = None, + record_base: str = "", ) -> tuple[int, str] | None: - """Return evidence only when exact source bytes identify ownerless self.""" + """Return evidence only when the source line identifies ownerless self. + + `record_base` is the record's repository-relative directory, so a quote + whose links were re-pointed to resolve from there still matches the line + it was cut from; see `_resolve_relative_links`. + """ evidence = intake_archive_evidence(record) if evidence is None or not _archive_evidence_matches_source( evidence, frozen_archive, + record_base, ): return None _, line = evidence @@ -455,7 +534,7 @@ def validate_issue_record( errors.append(f"{name} must be UNKNOWN for Availability METADATA_ONLY") if normalize_body(record.resolution).strip() != "-": errors.append("Resolution must be - for Availability METADATA_ONLY") - archive = ".agents/completed/issue-index.md" + archive = FROZEN_ARCHIVE_RELPATH exact_number = ( record.github is not None and re.search(rf"(? str: + """An intake Problem whose quote carries a relative link, archive-spelled. + + The archive lives at `.agents/completed/issue-index.md`, so a link to a + spec is `../specs/...` there. The record that QUOTES the row lives two + levels down, and the same link is `../../specs/...` from there -- the + spelling #3348 is about. + """ + archived = ( + f"| [#{number}](https://github.com/mudler/vllm.cpp/issues/{number}) " + f"| — | Spec [example]({spec}) | bug |" + ) + return ( + f"Archive: `.agents/completed/issue-index.md:{line}`\n\n" + "### Frozen archive evidence\n\n" + f"> {archived}" + ) + + +def linked_frozen_archive_source(number: int = 77, spec: str = "../specs/example.md") -> bytes: + archived = ( + f"| [#{number}](https://github.com/mudler/vllm.cpp/issues/{number}) " + f"| — | Spec [example]({spec}) | bug |" + ) + return ( + "# Issue index\n\nFrozen archive\n" + "| Issue | Row | Title | Kind |\n" + "|---:|---|---|---|\n" + f"{archived}\n" + ).encode() + + def parse(text: str | None = None) -> records.IssueRecord: return records.parse_issue_text(text if text is not None else issue_text()) @@ -543,6 +579,83 @@ def test_intake_evidence_must_equal_the_declared_frozen_source_line( frozen_archive=frozen_archive, ) + def test_intake_quote_may_rebase_a_relative_link_to_resolve_from_the_record( + self, tmp_path: Path + ) -> None: + """The record-relative spelling of a link is the SAME link (#3348). + + `check-links` requires every link in a record to resolve from the + record's own directory; the frozen-evidence comparison required the + quote to be byte-identical to the archive line it was cut from. One + string could not satisfy both, because the archive is one level under + `.agents/` and the record two. A quote whose ONLY difference is a + relative link resolving to the same file is accepted. + """ + record = self.intake(problem=linked_intake_problem(spec="../../specs/example.md")) + validate( + tmp_path, + record, + owner="_intake", + rows={"ROW-A"}, + owed=(), + frozen_archive=linked_frozen_archive_source(), + ) + + @pytest.mark.parametrize( + ("spec", "archive_spec"), + [ + # a rebase to a DIFFERENT file is still drift + ("../../specs/other.md", "../specs/example.md"), + ("../../specs/example.md#frag", "../specs/example.md"), + # a remote target is never rebased, so a swap must fail byte for byte + ("https://example.com/example.md", "../specs/example.md"), + # a root-absolute target is never rebased either + ("/specs/example.md", "../specs/example.md"), + ], + ) + def test_intake_quote_may_rebase_nothing_but_a_relative_link( + self, tmp_path: Path, spec: str, archive_spec: str + ) -> None: + record = self.intake(problem=linked_intake_problem(spec=spec)) + with pytest.raises(records.IssueRecordError, match="frozen archive source"): + validate( + tmp_path, + record, + owner="_intake", + rows={"ROW-A"}, + owed=(), + frozen_archive=linked_frozen_archive_source(spec=archive_spec), + ) + + def test_a_rebased_quote_needs_a_record_base_to_be_compared(self) -> None: + """Without the record's directory, only byte equality may pass. + + The public helper keeps the old strict behaviour for a caller that + cannot say where the quote lives, rather than guessing a base and + admitting a drift it cannot see. + """ + record = records.IssueRecord( + id="ISSUE-GH-77", + title="Archived 77", + row=None, + state="UNKNOWN", + kind="bug", + github=77, + mirror="MISSING", + availability="METADATA_ONLY", + created="UNKNOWN", + updated="UNKNOWN", + closed="UNKNOWN", + problem=linked_intake_problem(spec="../../specs/example.md"), + resolution="-", + ) + assert records.valid_intake_archive_evidence( + record, linked_frozen_archive_source() + ) is None + assert records.valid_intake_archive_evidence( + record, linked_frozen_archive_source(), ".agents/issues/_intake" + ) is not None + def test_full_and_local_records_cannot_use_intake(self, tmp_path: Path) -> None: for record in ( parse(),