Merged
feat: 先合入索引摘要生成器并规范中文导航 #67
zhanghz1Add on Sep 28
feat: 先合入索引摘要生成器并规范中文导航 #67
Merged
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 | ) |
| 40 | from governance.contracts.checks import Document, RunContext, body # noqa: E402 | 40 | from governance.contracts.checks import Document, RunContext, body # noqa: E402 |
| 41 | +from governance.contracts.indexes import summarize_subdir # noqa: E402 | ||
| 41 | 42 | ||
| 42 | 43 | ||
| 43 | INGEST = ROOT / ".agents/skills/knowledge-ingest/scripts/knowledge_ingest.py" | 44 | INGEST = 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 | + | ||
| 1828 | if __name__ == "__main__": | 1967 | if __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, description | 48 | 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 | + | ||
| 51 | def summarize_subdir(directory: Path) -> str: | 60 | def 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 | ||
| 63 | def section_title(directory: Path) -> str: | 105 | def 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` 是新增索引条目的默认文案;没有批量改写历史索引。 | ||