docs(flows): DoD obligatorio con user-facing surface + abrir issues 0100-0103 (taxonomia, frontmatter migration, dev_console, work dashboard)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"""Generate <app>_modules_generated.cpp from app.md uses_modules + modules/*/module.md.
|
||||
|
||||
Stand-alone — no dependencies beyond PyYAML. Invoked from CMake at configure time.
|
||||
Reads YAML frontmatter directly (no registry.db dependency, no Go binary).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def _read_frontmatter(md_path: Path) -> dict:
|
||||
if not md_path.exists():
|
||||
return {}
|
||||
text = md_path.read_text(encoding="utf-8")
|
||||
if not text.startswith("---\n") and not text.startswith("---\r\n"):
|
||||
return {}
|
||||
end = text.find("\n---", 4)
|
||||
if end < 0:
|
||||
return {}
|
||||
raw = text[4:end]
|
||||
try:
|
||||
return yaml.safe_load(raw) or {}
|
||||
except yaml.YAMLError:
|
||||
return {}
|
||||
|
||||
|
||||
def _escape_c_string(s: str) -> str:
|
||||
out = []
|
||||
for ch in s or "":
|
||||
if ch == "\\":
|
||||
out.append("\\\\")
|
||||
elif ch == '"':
|
||||
out.append('\\"')
|
||||
elif ch == "\n":
|
||||
out.append("\\n")
|
||||
elif ch == "\r":
|
||||
out.append("\\r")
|
||||
elif ch == "\t":
|
||||
out.append("\\t")
|
||||
elif ord(ch) < 32:
|
||||
out.append(f"\\x{ord(ch):02x}")
|
||||
else:
|
||||
out.append(ch)
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def _resolve_module(modules_root: Path, mod_id: str) -> Optional[dict]:
|
||||
"""mod_id is e.g. `data_table_cpp`. Lookup module.md by name (strip _<lang>)."""
|
||||
name = mod_id
|
||||
for suffix in ("_cpp", "_py", "_ts", "_bash", "_go"):
|
||||
if name.endswith(suffix):
|
||||
name = name[: -len(suffix)]
|
||||
break
|
||||
md = modules_root / name / "module.md"
|
||||
fm = _read_frontmatter(md)
|
||||
if not fm:
|
||||
return None
|
||||
return {
|
||||
"name": fm.get("name", name),
|
||||
"version": fm.get("version", "0.0.0"),
|
||||
"description": fm.get("description", ""),
|
||||
}
|
||||
|
||||
|
||||
def generate(app_md: Path, modules_root: Path, app_name: str, out_path: Path) -> int:
|
||||
fm = _read_frontmatter(app_md)
|
||||
uses_modules = fm.get("uses_modules") or []
|
||||
if not isinstance(uses_modules, list):
|
||||
uses_modules = []
|
||||
|
||||
entries: list[dict] = []
|
||||
missing: list[str] = []
|
||||
for mid in uses_modules:
|
||||
info = _resolve_module(modules_root, str(mid))
|
||||
if info is None:
|
||||
missing.append(str(mid))
|
||||
continue
|
||||
entries.append(info)
|
||||
|
||||
lines: list[str] = []
|
||||
lines.append(f"// Auto-generated by codegen_app_modules.py — do not edit.")
|
||||
lines.append(f"// App: {app_name}")
|
||||
lines.append(f"// Source of truth: {app_md.as_posix()} (uses_modules)")
|
||||
lines.append("")
|
||||
lines.append('#include "app_modules.h"')
|
||||
lines.append("")
|
||||
lines.append("namespace fn {")
|
||||
if entries:
|
||||
lines.append("const ModuleInfo app_modules_array[] = {")
|
||||
for e in entries:
|
||||
lines.append(
|
||||
' { "%s", "%s", "%s" },'
|
||||
% (
|
||||
_escape_c_string(e["name"]),
|
||||
_escape_c_string(e["version"]),
|
||||
_escape_c_string(e["description"]),
|
||||
)
|
||||
)
|
||||
lines.append("};")
|
||||
lines.append(f"const unsigned long app_modules_count = {len(entries)};")
|
||||
else:
|
||||
lines.append("const ModuleInfo app_modules_array[1] = { { nullptr, nullptr, nullptr } };")
|
||||
lines.append("const unsigned long app_modules_count = 0;")
|
||||
lines.append("} // namespace fn")
|
||||
lines.append("")
|
||||
|
||||
new_content = "\n".join(lines)
|
||||
|
||||
# Idempotent: skip rewrite when content matches.
|
||||
if out_path.exists() and out_path.read_text(encoding="utf-8") == new_content:
|
||||
return 0 if not missing else 2
|
||||
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(new_content, encoding="utf-8")
|
||||
|
||||
if missing:
|
||||
sys.stderr.write(
|
||||
f"codegen_app_modules: WARNING — module(s) not found: {', '.join(missing)} "
|
||||
f"(app {app_name})\n"
|
||||
)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Generate <app>_modules_generated.cpp from app.md")
|
||||
ap.add_argument("--app-md", required=True, help="Path to app.md")
|
||||
ap.add_argument("--modules-root", required=True, help="Path to modules/ root")
|
||||
ap.add_argument("--app-name", required=True, help="App name (for comment header)")
|
||||
ap.add_argument("--out", required=True, help="Output path for generated .cpp")
|
||||
args = ap.parse_args()
|
||||
|
||||
rc = generate(
|
||||
app_md=Path(args.app_md),
|
||||
modules_root=Path(args.modules_root),
|
||||
app_name=args.app_name,
|
||||
out_path=Path(args.out),
|
||||
)
|
||||
return 0 if rc in (0, 2) else rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user