mirror of
https://github.com/tradecatlabs/vibe-coding-cn.git
synced 2026-08-13 19:08:05 +00:00
ci: update actions and strengthen docs gates
This commit is contained in:
+134
-33
@@ -9,42 +9,77 @@ from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DOC_READMES: dict[Path, list[str]] = {
|
||||
Path("docs/getting-started/README.md"): [
|
||||
"vibe-coding-experience",
|
||||
"learning-map",
|
||||
"network-environment",
|
||||
"cli-setup",
|
||||
"development-environment",
|
||||
],
|
||||
Path("docs/concepts/README.md"): [
|
||||
"concept-problem-solving",
|
||||
"concept-glue-coding",
|
||||
"concept-system-building",
|
||||
"concept-development-paradigms",
|
||||
"concept-language-layers",
|
||||
"concept-recursive-self-optimizing-system",
|
||||
],
|
||||
Path("docs/philosophy/README.md"): [
|
||||
"philosophy-thinking-models",
|
||||
"philosophy-compositional-description-model",
|
||||
"philosophy-programming-dao",
|
||||
"philosophy-methodology-toolbox",
|
||||
],
|
||||
Path("docs/references/README.md"): [
|
||||
"reference-engineering-practice",
|
||||
"reference-technology-stack",
|
||||
],
|
||||
Path("docs/research/README.md"): [
|
||||
"research-harness-engineering",
|
||||
],
|
||||
}
|
||||
TAXONOMY = ROOT / "metadata/taxonomy.yml"
|
||||
SKIP_PARTS = {".git", ".history", "node_modules"}
|
||||
SKIP_PREFIXES = [
|
||||
Path(".github/wiki"),
|
||||
Path("tools/external"),
|
||||
]
|
||||
ANCHOR_PATTERN = re.compile(r"<a\s+id=[\"']([^\"']+)[\"']")
|
||||
SUMMARY_LINE = "<summary><strong>完整细粒度目录(点击展开/收起)</strong></summary>"
|
||||
|
||||
|
||||
def strip_quotes(value: str) -> str:
|
||||
return value.strip().strip("\"'")
|
||||
|
||||
|
||||
def taxonomy_sections() -> dict[str, dict[str, str]]:
|
||||
sections: dict[str, dict[str, str]] = {}
|
||||
current: str | None = None
|
||||
in_sections = False
|
||||
|
||||
for line in TAXONOMY.read_text(encoding="utf-8").splitlines():
|
||||
if line == "sections:":
|
||||
in_sections = True
|
||||
continue
|
||||
if in_sections and line and not line.startswith(" "):
|
||||
break
|
||||
if not in_sections:
|
||||
continue
|
||||
|
||||
section_match = re.match(r"^ ([A-Za-z0-9_-]+):\s*$", line)
|
||||
if section_match:
|
||||
current = section_match.group(1)
|
||||
sections[current] = {}
|
||||
continue
|
||||
field_match = re.match(r"^ (path|entry|agent_guide):\s*(.+?)\s*$", line)
|
||||
if current and field_match:
|
||||
sections[current][field_match.group(1)] = strip_quotes(field_match.group(2))
|
||||
|
||||
return sections
|
||||
|
||||
|
||||
def taxonomy_document_paths() -> list[str]:
|
||||
paths: list[str] = []
|
||||
in_documents = False
|
||||
|
||||
for line in TAXONOMY.read_text(encoding="utf-8").splitlines():
|
||||
if line == "documents:":
|
||||
in_documents = True
|
||||
continue
|
||||
if in_documents and line and not line.startswith(" "):
|
||||
break
|
||||
if not in_documents:
|
||||
continue
|
||||
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("- path:"):
|
||||
_, value = stripped.split(":", 1)
|
||||
paths.append(strip_quotes(value))
|
||||
|
||||
return paths
|
||||
|
||||
|
||||
def doc_readmes_from_taxonomy() -> dict[Path, list[str]]:
|
||||
readmes: dict[Path, list[str]] = {}
|
||||
|
||||
for target in taxonomy_document_paths():
|
||||
path_part, _, anchor = target.partition("#")
|
||||
if not anchor or not path_part.startswith("docs/") or not path_part.endswith("/README.md"):
|
||||
continue
|
||||
readmes.setdefault(Path(path_part), []).append(anchor)
|
||||
|
||||
return readmes
|
||||
|
||||
|
||||
def strip_fenced_code(text: str) -> str:
|
||||
@@ -118,12 +153,72 @@ def check_linear_readme(path: Path, expected_anchors: list[str]) -> list[str]:
|
||||
errors.append(f"{rel}:{line}: main anchor '{anchor}' is out of expected section order")
|
||||
previous_line = line
|
||||
|
||||
toc_boundary = min((anchor_lines.get(anchor, len(text.splitlines()) + 1) for anchor in expected_anchors), default=0)
|
||||
toc_text = "\n".join(text.splitlines()[:toc_boundary])
|
||||
toc_text = active_toc_text(text)
|
||||
if not toc_text:
|
||||
errors.append(f"{rel}: missing active collapsible fine-grained TOC block")
|
||||
|
||||
for anchor in expected_anchors:
|
||||
if f"#{anchor}" not in toc_text:
|
||||
errors.append(f"{rel}: top navigation or fine-grained TOC does not link to '#{anchor}'")
|
||||
|
||||
required_prefixes = tuple(f"{anchor}-" for anchor in expected_anchors)
|
||||
for anchor in anchor_lines:
|
||||
if anchor in expected_anchors or anchor.startswith(required_prefixes):
|
||||
if f"#{anchor}" not in toc_text:
|
||||
errors.append(f"{rel}: fine-grained TOC does not link to manual anchor '#{anchor}'")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def active_toc_text(text: str) -> str:
|
||||
in_fence = False
|
||||
fence_marker = ""
|
||||
collecting = False
|
||||
collected: list[str] = []
|
||||
|
||||
for line in text.splitlines():
|
||||
stripped = line.lstrip()
|
||||
if stripped.startswith(("```", "~~~")):
|
||||
marker = stripped[:3]
|
||||
if not in_fence:
|
||||
in_fence = True
|
||||
fence_marker = marker
|
||||
elif marker == fence_marker:
|
||||
in_fence = False
|
||||
fence_marker = ""
|
||||
|
||||
if in_fence:
|
||||
continue
|
||||
if SUMMARY_LINE in line:
|
||||
collecting = True
|
||||
if collecting:
|
||||
collected.append(line)
|
||||
if collecting and line.strip() == "</details>":
|
||||
break
|
||||
|
||||
return "\n".join(collected)
|
||||
|
||||
|
||||
def check_docs_index() -> list[str]:
|
||||
errors: list[str] = []
|
||||
docs_index = ROOT / "docs/README.md"
|
||||
rel = docs_index.relative_to(ROOT)
|
||||
|
||||
if not docs_index.exists():
|
||||
return [f"{rel}: missing docs index"]
|
||||
|
||||
text = docs_index.read_text(encoding="utf-8", errors="ignore")
|
||||
for name, fields in taxonomy_sections().items():
|
||||
for field in ("path", "entry", "agent_guide"):
|
||||
value = fields.get(field)
|
||||
if not value:
|
||||
errors.append(f"metadata/taxonomy.yml: section '{name}' missing '{field}'")
|
||||
continue
|
||||
docs_relative = value.removeprefix("docs/")
|
||||
candidates = {value, f"./{docs_relative}", docs_relative}
|
||||
if not any(candidate in text for candidate in candidates):
|
||||
errors.append(f"{rel}: missing taxonomy {field} reference for '{name}': {value}")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@@ -137,13 +232,19 @@ def main() -> int:
|
||||
checked_files += 1
|
||||
errors.extend(duplicate_manual_anchors(markdown_file))
|
||||
|
||||
for rel_path, expected_anchors in DOC_READMES.items():
|
||||
doc_readmes = doc_readmes_from_taxonomy()
|
||||
if not doc_readmes:
|
||||
errors.append("metadata/taxonomy.yml: no docs README anchors found under documents")
|
||||
|
||||
for rel_path, expected_anchors in doc_readmes.items():
|
||||
path = ROOT / rel_path
|
||||
if not path.exists():
|
||||
errors.append(f"{rel_path}: missing docs README")
|
||||
continue
|
||||
errors.extend(check_linear_readme(path, expected_anchors))
|
||||
|
||||
errors.extend(check_docs_index())
|
||||
|
||||
if errors:
|
||||
print("DOC_STRUCTURE_ERRORS")
|
||||
for error in errors:
|
||||
|
||||
Reference in New Issue
Block a user