From 2777eebe99040e1ab6baf01872d7c6ec7ca4acdf Mon Sep 17 00:00:00 2001 From: Kevin Abraham Date: Wed, 28 Oct 2026 14:07:21 -0400 Subject: [PATCH] fix(pipeline): resolve gap paths to real files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit detect_gaps.py copied docs.json page IDs, which have no extension, into the path of thin_page, missing_description and apiref_narrative_page gaps. The later steps expect a file: - generate.py read REPO_ROOT / path, got FileNotFoundError (swallowed by load_file, so the model saw an empty page) or, when a folder shares the page's name, IsADirectoryError (the run crashed) - the draft was written as docs--x--y with no .mdx, so evaluate.py found no .mdx files and the run failed Add util.page_file() to resolve a page ID to its .mdx/.md file and use it where the detector emits those three gap types. Detection is unchanged; only the path value differs. Add a stdlib test for the helper. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- pipeline/detect_gaps.py | 8 ++++-- pipeline/test_page_file.py | 59 ++++++++++++++++++++++++++++++++++++++ pipeline/util.py | 15 ++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 pipeline/test_page_file.py diff --git a/pipeline/detect_gaps.py b/pipeline/detect_gaps.py index f2e909b0..e54b5f0c 100644 --- a/pipeline/detect_gaps.py +++ b/pipeline/detect_gaps.py @@ -30,6 +30,8 @@ import yaml +from util import page_file + REPO_ROOT = Path(__file__).resolve().parent.parent STANDARDS_PATH = REPO_ROOT / "standards" / "ia.yaml" @@ -318,7 +320,7 @@ def detect_gaps(docs_json, endpoints, families, section=None, force=False): if fm is None: continue # file missing; separate concern if not is_openapi_page(fm): - gaps.append(gap("apiref_narrative_page", "medium", None, path=p, + gaps.append(gap("apiref_narrative_page", "medium", None, path=page_file(p) or p, desc=f"{p} sits in the API Reference tab but is not an endpoint page (narrative belongs in a product tab or Home)")) # --- Ungrouped OpenAPI tags ------------------------------------------- # @@ -334,10 +336,10 @@ def detect_gaps(docs_json, endpoints, families, section=None, force=False): if fm is None: continue if not fm.get("description"): - gaps.append(gap("missing_description", "low", None, path=p, + gaps.append(gap("missing_description", "low", None, path=page_file(p) or p, desc=f"{p} has no frontmatter description")) if len(strip_frontmatter(content).split()) < 100: - gaps.append(gap("thin_page", "medium", None, path=p, + gaps.append(gap("thin_page", "medium", None, path=page_file(p) or p, desc=f"{p} has under 100 words")) return gaps diff --git a/pipeline/test_page_file.py b/pipeline/test_page_file.py new file mode 100644 index 00000000..d37a3a49 --- /dev/null +++ b/pipeline/test_page_file.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Tests for resolving a docs.json page ID to its file. + + python pipeline/test_page_file.py + +Stdlib only: each test builds a throwaway docs tree in a temp directory. +""" + +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from util import page_file # noqa: E402 + + +class PageFileTest(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.addCleanup(self._tmp.cleanup) + self.root = Path(self._tmp.name) + + def touch(self, rel): + path = self.root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("---\ntitle: t\n---\n", encoding="utf-8") + + def test_resolves_an_mdx_page(self): + self.touch("docs/a.mdx") + self.assertEqual(page_file("docs/a", self.root), "docs/a.mdx") + + def test_falls_back_to_md(self): + self.touch("docs/a.md") + self.assertEqual(page_file("docs/a", self.root), "docs/a.md") + + def test_prefers_mdx_over_md(self): + self.touch("docs/a.mdx") + self.touch("docs/a.md") + self.assertEqual(page_file("docs/a", self.root), "docs/a.mdx") + + def test_a_page_and_a_folder_with_the_same_name_resolve_to_the_page(self): + # docs/resources/breaking-changes-change-notices.mdx sits next to a folder of + # the same name; the folder must never be mistaken for the page. + self.touch("docs/a.mdx") + self.touch("docs/a/child.mdx") + self.assertEqual(page_file("docs/a", self.root), "docs/a.mdx") + + def test_a_folder_without_a_page_resolves_to_nothing(self): + self.touch("docs/a/child.mdx") + self.assertIsNone(page_file("docs/a", self.root)) + + def test_a_missing_page_resolves_to_nothing(self): + self.assertIsNone(page_file("docs/missing", self.root)) + + +if __name__ == "__main__": + unittest.main() diff --git a/pipeline/util.py b/pipeline/util.py index c93769de..2bbe9294 100644 --- a/pipeline/util.py +++ b/pipeline/util.py @@ -38,6 +38,21 @@ def load_text(path): return "" +def page_file(page_id, repo_root=REPO_ROOT): + """Repo-relative path of the file behind a docs.json page ID, or None. + + docs.json lists pages without an extension (docs/resources/foo). A gap that + carries that ID points at no file, or at a folder that happens to share the + page's name, so resolve it to the real .mdx/.md file before any step reads or + writes it. + """ + for ext in (".mdx", ".md"): + candidate = f"{page_id}{ext}" + if (Path(repo_root) / candidate).is_file(): + return candidate + return None + + OUTPUT_RULES = """## Output Format - Output ONLY the .mdx file content. No commentary, no explanation, no markdown fences.