idle-draft: strip :line-range suffixes before citation existence check
First pilot rejected valid citations like SPEC.md:1-80 — the form the research template itself mandates. 5 new test cases (83 total). Claude-Session: https://claude.ai/code/session_01YQDoWNM7XPPii28khFWoMc
This commit is contained in:
@@ -92,6 +92,7 @@ RETRYABLE_STDERR_MARKERS = (
|
|||||||
|
|
||||||
ITEM_FILENAME_RE = re.compile(r"^(\d+)-(.+)\.overview\.md$")
|
ITEM_FILENAME_RE = re.compile(r"^(\d+)-(.+)\.overview\.md$")
|
||||||
ABS_PATH_RE = re.compile(r"/home/[^\s`)\]\"'>,;]+")
|
ABS_PATH_RE = re.compile(r"/home/[^\s`)\]\"'>,;]+")
|
||||||
|
LINE_RANGE_SUFFIX_RE = re.compile(r"^(.*/[^/:]+):\d+(?:-\d+)?$")
|
||||||
|
|
||||||
SCRIPT_PATH = Path(__file__).resolve()
|
SCRIPT_PATH = Path(__file__).resolve()
|
||||||
REPO_SELF = SCRIPT_PATH.parent.parent # small-scripts repo (holds data/idle-draft)
|
REPO_SELF = SCRIPT_PATH.parent.parent # small-scripts repo (holds data/idle-draft)
|
||||||
@@ -721,6 +722,16 @@ def extract_absolute_paths(text: str) -> list[str]:
|
|||||||
return ABS_PATH_RE.findall(text)
|
return ABS_PATH_RE.findall(text)
|
||||||
|
|
||||||
|
|
||||||
|
def strip_line_range(p: str) -> str:
|
||||||
|
"""Strip a trailing ':<digits>' or ':<digits>-<digits>' citation line-range suffix.
|
||||||
|
|
||||||
|
Only strips when the remainder still looks like a path (has a filename
|
||||||
|
segment before the colon); a plain path with no suffix is unchanged.
|
||||||
|
"""
|
||||||
|
m = LINE_RANGE_SUFFIX_RE.match(p)
|
||||||
|
return m.group(1) if m else p
|
||||||
|
|
||||||
|
|
||||||
def validate_output(work_type: str, text: str) -> tuple[bool, str]:
|
def validate_output(work_type: str, text: str) -> tuple[bool, str]:
|
||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
return False, "output is empty"
|
return False, "output is empty"
|
||||||
@@ -729,7 +740,7 @@ def validate_output(work_type: str, text: str) -> tuple[bool, str]:
|
|||||||
if not first_line.startswith(prefix):
|
if not first_line.startswith(prefix):
|
||||||
return False, f"first non-blank line does not start with '{prefix}': {first_line!r}"
|
return False, f"first non-blank line does not start with '{prefix}': {first_line!r}"
|
||||||
if work_type == "research":
|
if work_type == "research":
|
||||||
dead = [p for p in extract_absolute_paths(text) if not Path(p.rstrip(".,;:")).exists()]
|
dead = [p for p in extract_absolute_paths(text) if not Path(strip_line_range(p.rstrip(".,;:"))).exists()]
|
||||||
if dead:
|
if dead:
|
||||||
return False, f"dead cited path(s): {dead}"
|
return False, f"dead cited path(s): {dead}"
|
||||||
return True, "ok"
|
return True, "ok"
|
||||||
|
|||||||
@@ -247,7 +247,11 @@ For the selected `(item_or_dossier, work_type, provider)`:
|
|||||||
7. Validation (on exit 0, before promotion): the temp output file must be non-empty and
|
7. Validation (on exit 0, before promotion): the temp output file must be non-empty and
|
||||||
its first non-blank line must be a top-level Markdown heading (`# ...`). For
|
its first non-blank line must be a top-level Markdown heading (`# ...`). For
|
||||||
`research` work type specifically: every absolute path matching `/home/\S+` cited in
|
`research` work type specifically: every absolute path matching `/home/\S+` cited in
|
||||||
the file must exist on disk (`Path.exists()`); any dead path fails validation.
|
the file must exist on disk (`Path.exists()`); any dead path fails validation. Citations
|
||||||
|
may carry a trailing line-range suffix in `path:120-145` or `path:120` form (per the
|
||||||
|
research prompt template's required citation format); this suffix is stripped — via
|
||||||
|
`re.sub(r":\d+(-\d+)?$", ...)`, only when the remainder still looks like a path — before
|
||||||
|
the existence check, so a valid ranged citation is not flagged as a dead path.
|
||||||
8. On validation pass: `os.replace()` the temp file to the canonical path
|
8. On validation pass: `os.replace()` the temp file to the canonical path
|
||||||
(`<repo>/<dossier>/<NN-slug>.<work_type>.md`, or append to
|
(`<repo>/<dossier>/<NN-slug>.<work_type>.md`, or append to
|
||||||
`<dossier>/TOPIC-PROPOSALS.md` for `topic_ideas`) — atomic, never a partial file
|
`<dossier>/TOPIC-PROPOSALS.md` for `topic_ideas`) — atomic, never a partial file
|
||||||
|
|||||||
@@ -409,6 +409,26 @@ bad_text = "# Research: topic\n\nSee /home/nonexistent-user/definitely-not-here.
|
|||||||
ok, why = m.validate_output("research", bad_text)
|
ok, why = m.validate_output("research", bad_text)
|
||||||
check("citation validation: rejects research output citing a dead path", ok is False, why)
|
check("citation validation: rejects research output citing a dead path", ok is False, why)
|
||||||
|
|
||||||
|
ranged_good_text = f"# Research: topic\n\nSee {real_path}:12-18 for detail.\n"
|
||||||
|
ok, why = m.validate_output("research", ranged_good_text)
|
||||||
|
check("citation validation: accepts existing path cited with a :12-18 line range", ok is True, why)
|
||||||
|
|
||||||
|
bare_good_text = f"# Research: topic\n\nSee {real_path} for detail.\n"
|
||||||
|
ok, why = m.validate_output("research", bare_good_text)
|
||||||
|
check("citation validation: accepts existing path cited bare (no line range)", ok is True, why)
|
||||||
|
|
||||||
|
ranged_bad_text = "# Research: topic\n\nSee /home/nonexistent-user/definitely-not-here.md:1-20 for detail.\n"
|
||||||
|
ok, why = m.validate_output("research", ranged_bad_text)
|
||||||
|
check("citation validation: rejects nonexistent path cited with a :1-20 line range", ok is False, why)
|
||||||
|
|
||||||
|
bare_bad_text = "# Research: topic\n\nSee /home/nonexistent-user/definitely-not-here.md for detail.\n"
|
||||||
|
ok, why = m.validate_output("research", bare_bad_text)
|
||||||
|
check("citation validation: rejects nonexistent path cited bare (no line range)", ok is False, why)
|
||||||
|
|
||||||
|
noisy_text = f"# Research: topic\n\nSee ({real_path}), also `{real_path}`, and {real_path}, again.\n"
|
||||||
|
ok, why = m.validate_output("research", noisy_text)
|
||||||
|
check("citation validation: accepts existing path with trailing comma/backtick noise", ok is True, why)
|
||||||
|
|
||||||
ok, why = m.validate_output("research", "")
|
ok, why = m.validate_output("research", "")
|
||||||
check("citation validation: rejects empty output", ok is False, why)
|
check("citation validation: rejects empty output", ok is False, why)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user