Merged
feat: 先合入索引摘要生成器并规范中文导航 #67
feat: 先合入索引摘要生成器并规范中文导航 #67
Merged
zhanghz1Add on Sep 28
Total: 4 files changed+209-4
@@ -57,6 +57,7 @@ Schema、Profile、Registry、Contract、Spec、Skill 或测试若互相矛盾
57- 文件名使用小写 `snake_case`,自由主题的集合目录使用复数;Registry 控制的 Operator 源码分类保持其注册 ID。Registry 中注册的所有 domain 根目录必须保留 `index.md`;只有 `ops` 和 `model` 预建注册 route,其他领域的下一层目录随首张真实知识创建。57- 文件名使用小写 `snake_case`,自由主题的集合目录使用复数;Registry 控制的 Operator 源码分类保持其注册 ID。Registry 中注册的所有 domain 根目录必须保留 `index.md`;只有 `ops` 和 `model` 预建注册 route,其他领域的下一层目录随首张真实知识创建。
58- Operator 和 Runbook 只能使用 Registry 中声明的附加层级;`concepts`、`apis`、`examples`、`guides` 和 `optimizations` 可增加小写 `snake_case` 主题层级,其他路径 Profile 默认直接放卡片。58- Operator 和 Runbook 只能使用 Registry 中声明的附加层级;`concepts`、`apis`、`examples`、`guides` 和 `optimizations` 可增加小写 `snake_case` 主题层级,其他路径 Profile 默认直接放卡片。
59- `index.md` 只存在于 `knowledge/`。每个目录都必须有一个索引,完整且无重复地列出直接子目录和直接知识卡,并为每项提供摘要;domain 根和 `ops`/`model` 预建 route 可只有 index,且不表示已有知识覆盖。59- `index.md` 只存在于 `knowledge/`。每个目录都必须有一个索引,完整且无重复地列出直接子目录和直接知识卡,并为每项提供摘要;domain 根和 `ops`/`model` 预建 route 可只有 index,且不表示已有知识覆盖。
60+- `index.md` 供人阅读:本次新增或修改的一级标题、导航项名称和摘要应以中文表述,API 名、芯片型号、代码符号和报错原文等必要技术标识保留原文。知识卡的 `title`、`description` 会成为索引新增条目的默认文案,新增或修改卡片时应写成中文可读的标题和摘要;确需保留英文原题时,在索引的显示名称和摘要中补足中文说明。
60- 只有 `knowledge/index.md` 声明 `okf_version: "0.2"`,其他索引不带 Frontmatter,所有索引均不参与知识检索。61- 只有 `knowledge/index.md` 声明 `okf_version: "0.2"`,其他索引不带 Frontmatter,所有索引均不参与知识检索。
61- Concept 间和索引中的本地 Markdown 链接必须解析到 `knowledge/` Bundle 内的现有文件;知识卡可另行链接 Registry 登记的固定提交本地源码,源码链接只用于来源导航。图谱按需从现有卡片链接生成,只包含 Concept 节点和有向 `links_to` 边,API 与算子关联也使用普通链接;Index 和源码不进入图谱。新增或更新知识不要求构图、关系候选、Agent 关系判定或缓存,也不维护专用关系块。图谱 JSON 和可视化只写入 ignored 的 `artifacts/graphs/`,构图不修改知识卡。62- Concept 间和索引中的本地 Markdown 链接必须解析到 `knowledge/` Bundle 内的现有文件;知识卡可另行链接 Registry 登记的固定提交本地源码,源码链接只用于来源导航。图谱按需从现有卡片链接生成,只包含 Concept 节点和有向 `links_to` 边,API 与算子关联也使用普通链接;Index 和源码不进入图谱。新增或更新知识不要求构图、关系候选、Agent 关系判定或缓存,也不维护专用关系块。图谱 JSON 和可视化只写入 ignored 的 `artifacts/graphs/`,构图不修改知识卡。
62- API→Operator 的现有链接作为普通正文保留。需要发现新的引用时,可按需扫描算子卡声明的固定提交源码并输出引用建议;不自动回写知识卡,不因没有建议或未运行扫描阻断入库。源码调用证据仍须区分语法直接调用与运行时可达性;图谱本身只消费最终正文中的链接,不扫描源码。63- API→Operator 的现有链接作为普通正文保留。需要发现新的引用时,可按需扫描算子卡声明的固定提交源码并输出引用建议;不自动回写知识卡,不因没有建议或未运行扫描阻断入库。源码调用证据仍须区分语法直接调用与运行时可达性;图谱本身只消费最终正文中的链接,不扫描源码。
@@ -38,6 +38,7 @@ from governance.contracts.checks.platforms import ( # noqa: E402
38 fixed_archive_platforms,38 fixed_archive_platforms,
39)39)
40from governance.contracts.checks import Document, RunContext, body # noqa: E40240from governance.contracts.checks import Document, RunContext, body # noqa: E402
41+from governance.contracts.indexes import summarize_subdir # noqa: E402
41 42 
42 43 
43INGEST = ROOT / ".agents/skills/knowledge-ingest/scripts/knowledge_ingest.py"44INGEST = ROOT / ".agents/skills/knowledge-ingest/scripts/knowledge_ingest.py"
@@ -1825,5 +1826,143 @@ This isolated fixture verifies strict source navigation.
1825 self.assertIn("source.navigation", result.stdout)1826 self.assertIn("source.navigation", result.stdout)
1826 1827 
1827 1828 
1829+class IndexSummaryTests(unittest.TestCase):
1830+ def write_card(self, root: Path, name: str, title: str, description: str) -> None:
1831+ (root / name).write_text(
1832+ f"---\ntitle: {title}\ndescription: {description}\n---\n\n# {title}\n",
1833+ encoding="utf-8",
1834+ )
1835+ 
1836+ def test_overview_card_description_leads_the_summary(self) -> None:
1837+ with tempfile.TemporaryDirectory() as temporary:
1838+ directory = Path(temporary) / "hybrid"
1839+ directory.mkdir()
1840+ self.write_card(
1841+ directory,
1842+ "overview.md",
1843+ "混合编程概述",
1844+ "以SIMD为主、SIMT为辅的混合编程模型。",
1845+ )
1846+ self.write_card(directory, "a.md", "抽象硬件架构", "架构描述。")
1847+ self.write_card(directory, "b.md", "核函数与VF函数", "核函数描述。")
1848+ self.write_card(directory, "c.md", "内存层级", "内存描述。")
1849+ self.write_card(directory, "d.md", "编程示例", "示例描述。")
1850+ self.assertEqual(
1851+ summarize_subdir(directory),
1852+ "以SIMD为主、SIMT为辅的混合编程模型。"
1853+ "(共 5 张知识卡:抽象硬件架构、核函数与VF函数、内存层级 等)",
1854+ )
1855+ 
1856+ def test_directory_named_card_takes_precedence_over_overview(self) -> None:
1857+ with tempfile.TemporaryDirectory() as temporary:
1858+ directory = Path(temporary) / "matrix_compute"
1859+ directory.mkdir()
1860+ self.write_card(
1861+ directory,
1862+ "matrix_compute.md",
1863+ "矩阵计算",
1864+ "矩阵计算由数据搬入、mmad计算和结果搬出组成。",
1865+ )
1866+ self.write_card(directory, "overview.md", "概述", "详细描述请参考LINK。")
1867+ self.write_card(directory, "aux.md", "辅助配置", "辅助描述。")
1868+ self.assertEqual(
1869+ summarize_subdir(directory),
1870+ "矩阵计算由数据搬入、mmad计算和结果搬出组成。"
1871+ "(共 3 张知识卡:辅助配置、概述)",
1872+ )
1873+ 
1874+ def test_collection_summary_without_overview_card(self) -> None:
1875+ with tempfile.TemporaryDirectory() as temporary:
1876+ directory = Path(temporary) / "family"
1877+ directory.mkdir()
1878+ self.write_card(directory, "a.md", "算子甲", "甲描述。")
1879+ self.write_card(directory, "b.md", "算子乙", "乙描述。")
1880+ self.assertEqual(
1881+ summarize_subdir(directory),
1882+ "共 2 张知识卡(如 算子甲、算子乙)。",
1883+ )
1884+ 
1885+ def test_single_card_directory_uses_its_description(self) -> None:
1886+ with tempfile.TemporaryDirectory() as temporary:
1887+ directory = Path(temporary) / "solo"
1888+ directory.mkdir()
1889+ self.write_card(directory, "a.md", "唯一卡", "唯一描述。")
1890+ self.assertEqual(summarize_subdir(directory), "唯一描述。")
1891+ 
1892+ def test_recursive_count_includes_subdirectory_cards(self) -> None:
1893+ with tempfile.TemporaryDirectory() as temporary:
1894+ directory = Path(temporary) / "mixed"
1895+ directory.mkdir()
1896+ self.write_card(directory, "a.md", "直接卡", "直接描述。")
1897+ nested = directory / "sub"
1898+ nested.mkdir()
1899+ self.write_card(nested, "x.md", "嵌套卡一", "嵌套描述一。")
1900+ self.write_card(nested, "y.md", "嵌套卡二", "嵌套描述二。")
1901+ self.assertEqual(
1902+ summarize_subdir(directory),
1903+ "共 3 张知识卡(如 直接卡 等)。",
1904+ )
1905+ 
1906+ def test_overview_with_only_nested_cards_reports_distribution(self) -> None:
1907+ with tempfile.TemporaryDirectory() as temporary:
1908+ directory = Path(temporary) / "guide"
1909+ directory.mkdir()
1910+ self.write_card(
1911+ directory, "overview.md", "指南概述", "本指南覆盖编译全流程。"
1912+ )
1913+ nested = directory / "chapter"
1914+ nested.mkdir()
1915+ self.write_card(nested, "x.md", "章节卡", "章节描述。")
1916+ self.assertEqual(
1917+ summarize_subdir(directory),
1918+ "本指南覆盖编译全流程。(共 2 张知识卡,分布在 1 个子目录)",
1919+ )
1920+ 
1921+ def test_nested_only_directory_reports_subdirectory_distribution(self) -> None:
1922+ with tempfile.TemporaryDirectory() as temporary:
1923+ directory = Path(temporary) / "hub"
1924+ directory.mkdir()
1925+ for name in ("sub1", "sub2"):
1926+ nested = directory / name
1927+ nested.mkdir()
1928+ self.write_card(nested, "x.md", f"{name}卡", f"{name}描述。")
1929+ self.assertEqual(
1930+ summarize_subdir(directory),
1931+ "共 2 张知识卡,分布在 2 个子目录(sub1、sub2)。",
1932+ )
1933+ 
1934+ def test_deep_nesting_counts_first_level_subdirectories_only(self) -> None:
1935+ with tempfile.TemporaryDirectory() as temporary:
1936+ directory = Path(temporary) / "hub"
1937+ directory.mkdir()
1938+ first = directory / "a"
1939+ (first / "b").mkdir(parents=True)
1940+ second = directory / "c"
1941+ second.mkdir()
1942+ self.write_card(first, "y.md", "一级卡", "一级描述。")
1943+ self.write_card(first / "b", "x.md", "二级卡", "二级描述。")
1944+ self.write_card(second, "z.md", "另一卡", "另一描述。")
1945+ self.assertEqual(
1946+ summarize_subdir(directory),
1947+ "共 3 张知识卡,分布在 2 个子目录(a、c)。",
1948+ )
1949+ 
1950+ def test_deep_nesting_with_overview_counts_first_level_only(self) -> None:
1951+ with tempfile.TemporaryDirectory() as temporary:
1952+ directory = Path(temporary) / "guide"
1953+ directory.mkdir()
1954+ self.write_card(
1955+ directory, "overview.md", "指南概述", "本指南覆盖编译全流程。"
1956+ )
1957+ nested = directory / "chapter"
1958+ (nested / "section").mkdir(parents=True)
1959+ self.write_card(nested, "x.md", "章节卡", "章节描述。")
1960+ self.write_card(nested / "section", "y.md", "小节卡", "小节描述。")
1961+ self.assertEqual(
1962+ summarize_subdir(directory),
1963+ "本指南覆盖编译全流程。(共 3 张知识卡,分布在 1 个子目录)",
1964+ )
1965+ 
1966+ 
1828if __name__ == "__main__":1967if __name__ == "__main__":
1829 unittest.main()1968 unittest.main()
@@ -48,16 +48,58 @@ def read_card_description(card_path: Path) -> tuple[str, str]:
48 return title, description48 return title, description
49 49 
50 50 
51+def _summary_card(directory: Path) -> Path | None:
52+ """Return the directory's curated overview card when one exists."""
53+ candidates = (directory / f"{directory.name}.md", directory / "overview.md")
54+ for candidate in candidates:
55+ if candidate.name != "index.md" and candidate.is_file():
56+ return candidate
57+ return None
58+ 
59+ 
51def summarize_subdir(directory: Path) -> str:60def summarize_subdir(directory: Path) -> str:
61+ """Lead with a curated overview card, then describe the collection itself."""
52 direct_cards = sorted(62 direct_cards = sorted(
53 path for path in directory.glob("*.md") if path.name != "index.md"63 path for path in directory.glob("*.md") if path.name != "index.md"
54 )64 )
55- candidates = direct_cards or sorted(65+ descendants = sorted(
56 path for path in directory.rglob("*.md") if path.name != "index.md"66 path for path in directory.rglob("*.md") if path.name != "index.md"
57 )67 )
58- if candidates:68+ count = len(descendants)
59- return read_card_description(candidates[0])[1]69+ if count == 0:
60- return f"{directory.name.replace('_', ' ')} 导航。"70+ return f"{directory.name.replace('_', ' ')} 导航。"
71+ if count == 1 and direct_cards:
72+ return read_card_description(direct_cards[0])[1]
73+ child_dirs = sorted(
74+ {
75+ path.relative_to(directory).parts[0]
76+ for path in descendants
77+ if path.parent != directory
78+ }
79+ )
80+ if direct_cards:
81+ summary_card = _summary_card(directory)
82+ example_paths = [
83+ path for path in direct_cards if path != summary_card
84+ ][:3]
85+ examples = [read_card_description(path)[0] for path in example_paths]
86+ if summary_card is not None:
87+ description = read_card_description(summary_card)[1]
88+ if examples:
89+ more = " 等" if count > len(examples) + 1 else ""
90+ return (
91+ f"{description}(共 {count} 张知识卡:"
92+ f"{'、'.join(examples)}{more})"
93+ )
94+ spread = f",分布在 {len(child_dirs)} 个子目录" if child_dirs else ""
95+ return f"{description}(共 {count} 张知识卡{spread})"
96+ more = " 等" if count > len(examples) else ""
97+ return f"共 {count} 张知识卡(如 {'、'.join(examples)}{more})。"
98+ shown = "、".join(child_dirs[:3])
99+ more = " 等" if len(child_dirs) > 3 else ""
100+ return (
101+ f"共 {count} 张知识卡,分布在 {len(child_dirs)} 个子目录({shown}{more})。"
102+ )
61 103 
62 104 
63def section_title(directory: Path) -> str:105def section_title(directory: Path) -> str:
@@ -0,0 +1,23 @@
1+# 2026-09-28
2+ 
3+## [07:24] governance | 先合入索引摘要生成器与回归测试
4+ 
5+### Summary
6+ 
7+从 PR #58 提取 `governance/contracts/indexes.py` 和 `evals/test_governance.py` 的索引摘要改动,作为两步索引迁移的第一步;本次不提交 85 个再生成的 `index.md`,也不修改 `.gitcode/`。
8+ 
9+### Details
10+ 
11+- 新生成器优先使用目录概述卡,并按一级子目录统计知识卡分布;9 项定向单测通过,代码与 PR #58 对应文件一致。
12+- 在当前旧索引上运行 `sync-indexes --check`:662 个索引中 85 个 stale。这是已接受的短暂窗口,后续索引 PR 需立即合入 85 个再生成结果,使 master 恢复自洽。
13+ 
14+## [03:39] docs | 明确索引导航的中文主语言
15+ 
16+### Summary
17+ 
18+在 `AGENTS.md` 规定本次新增或修改的索引标题、导航项名称和摘要以中文表述。
19+ 
20+### Details
21+ 
22+- API 名、芯片型号、代码符号和报错原文等必要技术标识保留原文。
23+- 说明知识卡的 `title` 和 `description` 是新增索引条目的默认文案;没有批量改写历史索引。