From 8b868067b83c8707474e0487768d5d0b9c1c3639 Mon Sep 17 00:00:00 2001
From: pierre22400 <154444977+pierre22400@users.noreply.github.com>
Date: Fri, 15 Aug 2025 11:09:32 +0200
Subject: [PATCH 1/2] Update context-snapshot.yml
---
.github/workflows/context-snapshot.yml | 765 ++++++++++++-------------
1 file changed, 379 insertions(+), 386 deletions(-)
diff --git a/.github/workflows/context-snapshot.yml b/.github/workflows/context-snapshot.yml
index 9183653..366b317 100644
--- a/.github/workflows/context-snapshot.yml
+++ b/.github/workflows/context-snapshot.yml
@@ -1,4 +1,3 @@
-
name: Context Snapshot (mARCHCode)
on:
@@ -22,7 +21,9 @@ jobs:
python-version: "3.11"
- name: Install deps
- run: pip install pyyaml
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install pyyaml
- name: Create dirs
run: mkdir -p scripts .archcode
@@ -30,265 +31,266 @@ jobs:
- name: Write context_snapshot.py
shell: bash
run: |
- cat > scripts/context_snapshot.py <<'PY'
- from __future__ import annotations
- """
- Minimal context snapshot (stable)
- - Génère .archcode/context_snapshot.yaml
- - Remplit 'project' avec fallback (GITHUB_REPOSITORY puis basename)
- """
- import ast, sys, os, platform, yaml
- from dataclasses import dataclass
- from pathlib import Path
- from typing import Any, Dict, List, Optional, Union
- from datetime import datetime
-
- IGNORED_DIRS = {".git","__pycache__",".venv","venv",".env",".mypy_cache",".pytest_cache","node_modules",".idea",".vscode"}
- DEFAULT_OUT = ".archcode/context_snapshot.yaml"
-
- class _LiteralDumper(yaml.SafeDumper): pass
- def _repr_str(dumper: yaml.Dumper, data: str):
- style = "|" if ("\n" in data) else None
- return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=style)
- _LiteralDumper.add_representer(str, _repr_str)
-
- def iter_files(root: Path) -> List[Path]:
- files: List[Path] = []
- for p in sorted(root.rglob("*")):
- try:
- rel = p.relative_to(root)
- except Exception:
- continue
- if any(seg in IGNORED_DIRS for seg in rel.parts):
- continue
- if p.is_file():
- files.append(p)
- return files
-
- def ascii_tree(root: Path) -> str:
- lines=[str(root.resolve())]
- def kids(d: Path) -> List[Path]:
- out=[]
- try:
- it=sorted(d.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
- except Exception:
- return out
- for p in it:
- try:
- rel=p.relative_to(root)
- except Exception:
- continue
- if any(seg in IGNORED_DIRS for seg in rel.parts): continue
- out.append(p)
- return out
- def walk(d: Path, prefix=""):
- ch=kids(d)
- for i,k in enumerate(ch):
- joint="└── " if i==len(ch)-1 else "├── "
- lines.append(f"{prefix}{joint}{k.name}")
- if k.is_dir():
- ext=" " if i==len(ch)-1 else "│ "
- walk(k,prefix+ext)
- walk(root)
- return "\n".join(lines)
-
- @dataclass
- class RouteInfo:
- framework:str
- method:Optional[Union[str,List[str]]]=None
- path:Optional[str]=None
-
- @dataclass
- class DefInfo:
- qualname:str; name:str; lineno:int
- params:List[str]; decorators:List[str]
- docstring:Optional[str]; route:Optional[RouteInfo]
-
- def _first_toplevel_string_after_imports(module: ast.Module) -> Optional[str]:
- for node in module.body:
- if isinstance(node, ast.Expr) and isinstance(getattr(node,"value",None), ast.Constant):
- if isinstance(node.value.value,str): return node.value.value
- return None
-
- def _decorator_to_text(dec: ast.AST) -> str:
- def name_of(n):
- if isinstance(n, ast.Name): return n.id
- if isinstance(n, ast.Attribute): return f"{name_of(n.value)}.{n.attr}"
- if isinstance(n, ast.Call): return name_of(n.func)
- return n.__class__.__name__
- return name_of(dec)
-
- def _literal_str(node):
- if isinstance(node, ast.Constant) and isinstance(node.value,str): return node.value
- return None
-
- def _literal_methods_from_kwargs(call: ast.Call):
- for kw in call.keywords:
- if kw.arg in {"methods","method"}:
- v=kw.value
- if isinstance(v,ast.List):
- out=[e.value.upper() for e in v.elts if isinstance(e,ast.Constant) and isinstance(e.value,str)]
- return out or None
- if isinstance(v,ast.Constant) and isinstance(v.value,str):
- return [v.value.upper()]
- return None
-
- def _detect_route(dec: ast.AST):
- if isinstance(dec,ast.Call) and isinstance(dec.func,ast.Attribute):
- attr=dec.func.attr.lower()
- if attr in {"get","post","put","delete","patch","options","head"}:
- return RouteInfo("fastapi", attr.upper(), _literal_str(dec.args[0]) if dec.args else None)
- if attr=="route":
- return RouteInfo("flask", _literal_methods_from_kwargs(dec) or ["GET"], _literal_str(dec.args[0]) if dec.args else None)
- if attr=="command": return RouteInfo("typer","COMMAND",None)
- if isinstance(dec,ast.Attribute):
- attr=dec.attr.lower()
- if attr in {"get","post","put","delete","patch","options","head"}: return RouteInfo("fastapi", attr.upper(), None)
- if attr=="route": return RouteInfo("flask", ["GET"], None)
- if attr=="command": return RouteInfo("typer","COMMAND",None)
- return None
-
- class _DefCollector(ast.NodeVisitor):
- def __init__(self): self.stack=[]; self.defs=[]
- def visit_ClassDef(self,node): self.stack.append(node.name); self.generic_visit(node); self.stack.pop()
- def visit_FunctionDef(self,node): self._handle_def(node)
- def visit_AsyncFunctionDef(self,node): self._handle_def(node)
- def _handle_def(self,node):
- qual=".".join(self.stack+[node.name]) if self.stack else node.name
- params=[a.arg for a in list(node.args.posonlyargs)+list(node.args.args)]
- if node.args.vararg: params.append("*"+node.args.vararg.arg)
- if node.args.kwonlyargs: params+= [ka.arg for ka in node.args.kwonlyargs]
- if node.args.kwarg: params.append("**"+node.args.kwarg.arg)
- decs=[_decorator_to_text(d) for d in node.decorator_list]
- route=None
- for d in node.decorator_list:
- r=_detect_route(d)
- if r: route=r; break
- doc=ast.get_docstring(node)
- self.defs.append(DefInfo(qual,node.name,getattr(node,"lineno",-1),params,decs,doc,route))
-
- def extract_python_file(py_path: Path) -> Dict[str,Any]:
- text = py_path.read_text(encoding="utf-8", errors="ignore")
- try:
- mod = ast.parse(text)
- except SyntaxError as e:
- return {"path": str(py_path), "error": f"SyntaxError: {e}"}
-
- # 1) docstring-literal banner (module-level string after imports)
- banner = _first_toplevel_string_after_imports(mod)
-
- # 2) fallback : bloc de commentaires en tête de fichier (consécutifs),
- # utile pour les bannières visuelles utilisant '# -----'
- if not banner:
- comment_lines = []
- for ln in text.splitlines():
- s = ln.strip()
- # shebang / encoding lines are tolerated but not captured as banner:
- if s.startswith("#!"):
- continue
- if s.startswith("# -*-") or s.startswith("# coding:"):
- continue
- if s.startswith("#"):
- # retire le '#' initial et un espace éventuel
- comment_lines.append(s.lstrip("# ").rstrip())
- continue
- # stop dès la première ligne non-comment/non-empty (après éventuels commentaires)
- if s == "":
- # autorise un blanc initial, continue si pas encore collecté
- if comment_lines:
- break
- else:
- continue
- break
- if comment_lines:
- banner = "\n".join(comment_lines)
-
- module_doc = ast.get_docstring(mod)
- col = _DefCollector()
- col.visit(mod)
- defs_out = []
- for d in col.defs:
- route_block = None
- if d.route:
- route_block = {"framework": d.route.framework, "method": d.route.method, "path": d.route.path}
- defs_out.append(
- {
- "qualname": d.qualname,
- "name": d.name,
- "lineno": d.lineno,
- "params": d.params,
- "decorators": d.decorators or [],
- "docstring": d.docstring or "",
- "route": route_block,
- }
- )
- return {
- "path": str(py_path),
- "banner": banner or "",
- "module_docstring": module_doc or "",
- "defs": defs_out,
- }
-
- def main(argv):
- # parse args
- root_arg=None; out_arg=None
- it=iter(argv)
- for a in it:
- if a=="--root": root_arg=next(it,None)
- elif a=="--out": out_arg=next(it,None)
-
- # root / out
- root=Path(root_arg) if root_arg else Path.cwd()
- if not root.exists():
- print(f"[ERROR] Racine invalide: {root}", file=sys.stderr); return 2
- out_path=Path(out_arg) if out_arg else root / DEFAULT_OUT
- out_path.parent.mkdir(parents=True, exist_ok=True)
-
- # files
- all_files=iter_files(root)
- py_files=[p for p in all_files if p.suffix==".py"]
-
- # robust project name
- project_name = root.name or ""
- if not project_name:
- repo_env = os.environ.get("GITHUB_REPOSITORY")
- if repo_env and "/" in repo_env:
- project_name = repo_env.split("/", 1)[1]
- else:
- try:
- project_name = os.path.basename(str(root.resolve()))
- except Exception:
- project_name = ""
-
- # snapshot
- snapshot={
- "snapshot": {
- "project": project_name,
- "root": str(root.resolve()),
- "generated_at": datetime.utcnow().isoformat(timespec="seconds")+"Z",
- "python": sys.version.split()[0],
- "platform": platform.platform(),
- "files_count": len(all_files),
- "py_files_count": len(py_files),
- "ignored_dirs": sorted(list(IGNORED_DIRS)),
- },
- "tree": ascii_tree(root),
- "files": [],
- }
- for py in py_files:
- item=extract_python_file(py)
- try: item["relpath"]=str(py.relative_to(root)).replace(os.sep,"/")
- except Exception: item["relpath"]=str(py)
- snapshot["files"].append(item)
-
- with out_path.open("w", encoding="utf-8") as f:
- yaml.dump(snapshot, f, Dumper=_LiteralDumper, sort_keys=False, allow_unicode=True, width=100)
- print(f"[OK] Contexte écrit → {out_path}")
- return 0
-
- if __name__ == "__main__":
- raise SystemExit(main(sys.argv[1:]))
- PY
+cat > scripts/context_snapshot.py <<'PY'
+#!/usr/bin/env python3
+from __future__ import annotations
+"""
+Minimal context snapshot (stable)
+- Génère .archcode/context_snapshot.yaml
+- Remplit 'project' avec fallback (GITHUB_REPOSITORY puis basename)
+"""
+import ast, sys, os, platform, yaml
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Union
+from datetime import datetime
+
+IGNORED_DIRS = {".git","__pycache__",".venv","venv",".env",".mypy_cache",".pytest_cache","node_modules",".idea",".vscode"}
+DEFAULT_OUT = ".archcode/context_snapshot.yaml"
+
+class _LiteralDumper(yaml.SafeDumper): pass
+def _repr_str(dumper: yaml.Dumper, data: str):
+ style = "|" if ("\n" in data) else None
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=style)
+_LiteralDumper.add_representer(str, _repr_str)
+
+def iter_files(root: Path) -> List[Path]:
+ files: List[Path] = []
+ for p in sorted(root.rglob("*")):
+ try:
+ rel = p.relative_to(root)
+ except Exception:
+ continue
+ if any(seg in IGNORED_DIRS for seg in rel.parts):
+ continue
+ if p.is_file():
+ files.append(p)
+ return files
+
+def ascii_tree(root: Path) -> str:
+ lines=[str(root.resolve())]
+ def kids(d: Path) -> List[Path]:
+ out=[]
+ try:
+ it=sorted(d.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
+ except Exception:
+ return out
+ for p in it:
+ try:
+ rel=p.relative_to(root)
+ except Exception:
+ continue
+ if any(seg in IGNORED_DIRS for seg in rel.parts): continue
+ out.append(p)
+ return out
+ def walk(d: Path, prefix=""):
+ ch=kids(d)
+ for i,k in enumerate(ch):
+ joint="└── " if i==len(ch)-1 else "├── "
+ lines.append(f"{prefix}{joint}{k.name}")
+ if k.is_dir():
+ ext=" " if i==len(ch)-1 else "│ "
+ walk(k,prefix+ext)
+ walk(root)
+ return "\n".join(lines)
+
+@dataclass
+class RouteInfo:
+ framework:str
+ method:Optional[Union[str,List[str]]]=None
+ path:Optional[str]=None
+
+@dataclass
+class DefInfo:
+ qualname:str; name:str; lineno:int
+ params:List[str]; decorators:List[str]
+ docstring:Optional[str]; route:Optional[RouteInfo]
+
+def _first_toplevel_string_after_imports(module: ast.Module) -> Optional[str]:
+ for node in module.body:
+ if isinstance(node, ast.Expr) and isinstance(getattr(node,"value",None), ast.Constant):
+ if isinstance(node.value.value,str): return node.value.value
+ return None
+
+def _decorator_to_text(dec: ast.AST) -> str:
+ def name_of(n):
+ if isinstance(n, ast.Name): return n.id
+ if isinstance(n, ast.Attribute): return f"{name_of(n.value)}.{n.attr}"
+ if isinstance(n, ast.Call): return name_of(n.func)
+ return n.__class__.__name__
+ return name_of(dec)
+
+def _literal_str(node):
+ if isinstance(node, ast.Constant) and isinstance(node.value,str): return node.value
+ return None
+
+def _literal_methods_from_kwargs(call: ast.Call):
+ for kw in call.keywords:
+ if kw.arg in {"methods","method"}:
+ v=kw.value
+ if isinstance(v,ast.List):
+ out=[e.value.upper() for e in v.elts if isinstance(e,ast.Constant) and isinstance(e.value,str)]
+ return out or None
+ if isinstance(v,ast.Constant) and isinstance(v.value,str):
+ return [v.value.upper()]
+ return None
+
+def _detect_route(dec: ast.AST):
+ if isinstance(dec,ast.Call) and isinstance(dec.func,ast.Attribute):
+ attr=dec.func.attr.lower()
+ if attr in {"get","post","put","delete","patch","options","head"}:
+ return RouteInfo("fastapi", attr.upper(), _literal_str(dec.args[0]) if dec.args else None)
+ if attr=="route":
+ return RouteInfo("flask", _literal_methods_from_kwargs(dec) or ["GET"], _literal_str(dec.args[0]) if dec.args else None)
+ if attr=="command": return RouteInfo("typer","COMMAND",None)
+ if isinstance(dec,ast.Attribute):
+ attr=dec.attr.lower()
+ if attr in {"get","post","put","delete","patch","options","head"}: return RouteInfo("fastapi", attr.upper(), None)
+ if attr=="route": return RouteInfo("flask", ["GET"], None)
+ if attr=="command": return RouteInfo("typer","COMMAND",None)
+ return None
+
+class _DefCollector(ast.NodeVisitor):
+ def __init__(self): self.stack=[]; self.defs=[]
+ def visit_ClassDef(self,node): self.stack.append(node.name); self.generic_visit(node); self.stack.pop()
+ def visit_FunctionDef(self,node): self._handle_def(node)
+ def visit_AsyncFunctionDef(self,node): self._handle_def(node)
+ def _handle_def(self,node):
+ qual=".".join(self.stack+[node.name]) if self.stack else node.name
+ params=[a.arg for a in list(node.args.posonlyargs)+list(node.args.args)]
+ if node.args.vararg: params.append("*"+node.args.vararg.arg)
+ if node.args.kwonlyargs: params+= [ka.arg for ka in node.args.kwonlyargs]
+ if node.args.kwarg: params.append("**"+node.args.kwarg.arg)
+ decs=[_decorator_to_text(d) for d in node.decorator_list]
+ route=None
+ for d in node.decorator_list:
+ r=_detect_route(d)
+ if r: route=r; break
+ doc=ast.get_docstring(node)
+ self.defs.append(DefInfo(qual,node.name,getattr(node,"lineno",-1),params,decs,doc,route))
+
+def extract_python_file(py_path: Path) -> Dict[str,Any]:
+ text = py_path.read_text(encoding="utf-8", errors="ignore")
+ try:
+ mod = ast.parse(text)
+ except SyntaxError as e:
+ return {"path": str(py_path), "error": f"SyntaxError: {e}"}
+
+ # 1) docstring-literal banner (module-level string after imports)
+ banner = _first_toplevel_string_after_imports(mod)
+
+ # 2) fallback : bloc de commentaires en tête de fichier (consécutifs),
+ # utile pour les bannières visuelles utilisant '# -----'
+ if not banner:
+ comment_lines = []
+ for ln in text.splitlines():
+ s = ln.strip()
+ # shebang / encoding lines are tolerated but not captured as banner:
+ if s.startswith("#!"):
+ continue
+ if s.startswith("# -*-") or s.startswith("# coding:"):
+ continue
+ if s.startswith("#"):
+ # retire le '#' initial et un espace éventuel
+ comment_lines.append(s.lstrip("# ").rstrip())
+ continue
+ # stop dès la première ligne non-comment/non-empty (après éventuels commentaires)
+ if s == "":
+ # autorise un blanc initial, continue si pas encore collecté
+ if comment_lines:
+ break
+ else:
+ continue
+ break
+ if comment_lines:
+ banner = "\n".join(comment_lines)
+
+ module_doc = ast.get_docstring(mod)
+ col = _DefCollector()
+ col.visit(mod)
+ defs_out = []
+ for d in col.defs:
+ route_block = None
+ if d.route:
+ route_block = {"framework": d.route.framework, "method": d.route.method, "path": d.route.path}
+ defs_out.append(
+ {
+ "qualname": d.qualname,
+ "name": d.name,
+ "lineno": d.lineno,
+ "params": d.params,
+ "decorators": d.decorators or [],
+ "docstring": d.docstring or "",
+ "route": route_block,
+ }
+ )
+ return {
+ "path": str(py_path),
+ "banner": banner or "",
+ "module_docstring": module_doc or "",
+ "defs": defs_out,
+ }
+
+def main(argv):
+ # parse args
+ root_arg=None; out_arg=None
+ it=iter(argv)
+ for a in it:
+ if a=="--root": root_arg=next(it,None)
+ elif a=="--out": out_arg=next(it,None)
+
+ # root / out
+ root=Path(root_arg) if root_arg else Path.cwd()
+ if not root.exists():
+ print(f"[ERROR] Racine invalide: {root}", file=sys.stderr); return 2
+ out_path=Path(out_arg) if out_arg else root / DEFAULT_OUT
+ out_path.parent.mkdir(parents=True, exist_ok=True)
+
+ # files
+ all_files=iter_files(root)
+ py_files=[p for p in all_files if p.suffix==".py"]
+
+ # robust project name
+ project_name = root.name or ""
+ if not project_name:
+ repo_env = os.environ.get("GITHUB_REPOSITORY")
+ if repo_env and "/" in repo_env:
+ project_name = repo_env.split("/", 1)[1]
+ else:
+ try:
+ project_name = os.path.basename(str(root.resolve()))
+ except Exception:
+ project_name = ""
+
+ # snapshot
+ snapshot={
+ "snapshot": {
+ "project": project_name,
+ "root": str(root.resolve()),
+ "generated_at": datetime.utcnow().isoformat(timespec="seconds")+"Z",
+ "python": sys.version.split()[0],
+ "platform": platform.platform(),
+ "files_count": len(all_files),
+ "py_files_count": len(py_files),
+ "ignored_dirs": sorted(list(IGNORED_DIRS)),
+ },
+ "tree": ascii_tree(root),
+ "files": [],
+ }
+ for py in py_files:
+ item=extract_python_file(py)
+ try: item["relpath"]=str(py.relative_to(root)).replace(os.sep,"/")
+ except Exception: item["relpath"]=str(py)
+ snapshot["files"].append(item)
+
+ with out_path.open("w", encoding="utf-8") as f:
+ yaml.dump(snapshot, f, Dumper=_LiteralDumper, sort_keys=False, allow_unicode=True, width=100)
+ print(f"[OK] Contexte écrit → {out_path}")
+ return 0
+
+if __name__ == "__main__":
+ raise SystemExit(main(sys.argv[1:]))
+PY
- name: Run snapshot
run: |
@@ -298,131 +300,122 @@ jobs:
- name: Generate HTML page + true TiddlyWiki .tid (no truncation)
shell: bash
run: |
- python - <<'PY'
- import yaml, html, sys, json
- from pathlib import Path
- from datetime import datetime
-
- src = Path(".archcode/context_snapshot.yaml")
- out_html = Path(".archcode/context_snapshot_tiddler.html")
- out_tid = Path(".archcode/context_snapshot.tid")
-
- if not src.exists():
- print("No snapshot YAML found", file=sys.stderr); raise SystemExit(2)
-
- s = yaml.safe_load(src.read_text(encoding="utf-8")) or {}
- meta = s.get("snapshot", {}) or {}
- proj = html.escape(meta.get("project") or "")
-
- CSS = """
-
- """
-
- # ---- build the HTML *fragment* (for TiddlyWiki) ----
- frag = []
- frag.append(f"
Context Snapshot — {proj or '—'}
")
-
- # metadata
- frag.append("")
-
- # tree
- frag.append("Arbre du projet
")
- frag.append(html.escape(s.get("tree","") or ""))
- frag.append(" ")
-
- # files
- files = s.get("files") or []
- frag.append(f"Fichiers ({len(files)})
")
- for f in files:
- rel = f.get("relpath") or f.get("path") or ""
- rel_e = html.escape(rel)
- defs = f.get("defs") or []
- ndefs = len(defs)
- err = f.get("error")
- err_badge = "
parse error" if err else ""
- frag.append(f"
{rel_e} — {ndefs} defs{err_badge}
")
-
- if err:
- frag.append(f"Erreur de parsing
{html.escape(str(err))}
")
-
- if f.get("banner"):
- frag.append("Banner"+html.escape(str(f.get("banner")))+" ")
- if f.get("module_docstring"):
- frag.append("Docstring module"+html.escape(str(f.get("module_docstring")))+" ")
-
- if defs:
- frag.append(f"Defs ({ndefs})")
- for d in defs: # pas de limite: on affiche tout
- q = d.get("qualname","")
- ln = d.get("lineno","")
- params = d.get("params") or []
- sig = f"{d.get('name','')}(" + ", ".join(params) + ")"
- route = d.get("route")
- route_txt = ""
- if isinstance(route, dict):
- method = route.get("method")
- if isinstance(method, list): method = ",".join(method)
- path = route.get("path") or ""
- route_txt = f" [{html.escape(str(route.get('framework') or ''))} {html.escape(str(method or ''))} {html.escape(str(path))}]"
-
- ds_full = (d.get("docstring") or "").strip()
- ds_first = ds_full.splitlines()[0] if ds_full else ""
- li = f"{html.escape(q)} (line {html.escape(str(ln))}) — {html.escape(sig)}{route_txt}"
- if ds_first:
- li += f" — {html.escape(ds_first)}"
- if ds_full and (('\\n' in ds_full) or (len(ds_full) > len(ds_first))):
- li += f"docstring complète
{html.escape(ds_full)} "
- li += " "
- frag.append(li)
- frag.append("
")
- frag.append("")
- frag.append("
") # end files
-
- fragment_html = CSS + "\n" + "\n".join(frag)
-
- # ---- write standalone HTML page ----
- full_html = "\nContext Snapshot — %s%s" % (proj, fragment_html)
- out_html.write_text(full_html, encoding="utf-8")
-
- # ---- write true TiddlyWiki .tid ----
- # TiddlyWiki single-tiddler format: header fields, blank line, then content.
- # We store the *fragment* only (no wrapper), as 'text/html' type.
- def tw_timestamp(dt):
- # YYYYMMDDhhmmssmmm (UTC)
- return dt.strftime("%Y%m%d%H%M%S") + f"{dt.microsecond//1000:03d}"
-
- now = datetime.utcnow()
- title = f"Context Snapshot — {meta.get('project') or '-'}"
- header = [
- f"title: {title}",
- "tags: mARCHCode Snapshot",
- f"created: {tw_timestamp(now)}",
- f"modified: {tw_timestamp(now)}",
- "type: text/html",
- ]
- out_tid.write_text("\n".join(header) + "\n\n" + fragment_html, encoding="utf-8")
-
- print("Wrote:", out_html, "and", out_tid)
- PY
+cat > .archcode/_context_snapshot_html_builder.py <<'PY'
+import yaml, html, sys, json
+from pathlib import Path
+from datetime import datetime
+
+src = Path(".archcode/context_snapshot.yaml")
+out_html = Path(".archcode/context_snapshot_tiddler.html")
+out_tid = Path(".archcode/context_snapshot.tid")
+
+if not src.exists():
+ print("No snapshot YAML found", file=sys.stderr); raise SystemExit(2)
+
+s = yaml.safe_load(src.read_text(encoding="utf-8")) or {}
+meta = s.get("snapshot", {}) or {}
+proj = html.escape(meta.get("project") or "")
+
+CSS = """
+
+"""
+
+frag = []
+frag.append(f"Context Snapshot — {proj or '—'}
")
+
+frag.append("")
+
+frag.append("Arbre du projet
")
+frag.append(html.escape(s.get("tree","") or ""))
+frag.append(" ")
+
+files = s.get("files") or []
+frag.append(f"Fichiers ({len(files)})
")
+for f in files:
+ rel = f.get("relpath") or f.get("path") or ""
+ rel_e = html.escape(rel)
+ defs = f.get("defs") or []
+ ndefs = len(defs)
+ err = f.get("error")
+ err_badge = "
parse error" if err else ""
+ frag.append(f"
{rel_e} — {ndefs} defs{err_badge}
")
+
+ if err:
+ frag.append(f"Erreur de parsing
{html.escape(str(err))}
")
+
+ if f.get("banner"):
+ frag.append("Banner"+html.escape(str(f.get("banner")))+" ")
+ if f.get("module_docstring"):
+ frag.append("Docstring module"+html.escape(str(f.get("module_docstring")))+" ")
+
+ if defs:
+ frag.append(f"Defs ({ndefs})")
+ for d in defs:
+ q = d.get("qualname","")
+ ln = d.get("lineno","")
+ params = d.get("params") or []
+ sig = f"{d.get('name','')}(" + ", ".join(params) + ")"
+ route = d.get("route")
+ route_txt = ""
+ if isinstance(route, dict):
+ method = route.get("method")
+ if isinstance(method, list): method = ",".join(method)
+ path = route.get("path") or ""
+ route_txt = f" [{html.escape(str(route.get('framework') or ''))} {html.escape(str(method or ''))} {html.escape(str(path))}]"
+
+ ds_full = (d.get("docstring") or "").strip()
+ ds_first = ds_full.splitlines()[0] if ds_full else ""
+ li = f"{html.escape(q)} (line {html.escape(str(ln))}) — {html.escape(sig)}{route_txt}"
+ if ds_first:
+ li += f" — {html.escape(ds_first)}"
+ if ds_full and (('\\n' in ds_full) or (len(ds_full) > len(ds_first))):
+ li += f"docstring complète
{html.escape(ds_full)} "
+ li += " "
+ frag.append(li)
+ frag.append("
")
+ frag.append("")
+frag.append("
")
+
+fragment_html = CSS + "\n" + "\n".join(frag)
+
+full_html = "\nContext Snapshot — %s%s" % (proj, fragment_html)
+out_html.write_text(full_html, encoding="utf-8")
+
+def tw_timestamp(dt):
+ return dt.strftime("%Y%m%d%H%M%S") + f"{dt.microsecond//1000:03d}"
+
+now = datetime.utcnow()
+title = f"Context Snapshot — {meta.get('project') or '-'}"
+header = [
+ f"title: {title}",
+ "tags: mARCHCode Snapshot",
+ f"created: {tw_timestamp(now)}",
+ f"modified: {tw_timestamp(now)}",
+ "type: text/html",
+]
+out_tid.write_text("\n".join(header) + "\n\n" + fragment_html, encoding="utf-8")
+
+print("Wrote:", out_html, "and", out_tid)
+PY
test -f .archcode/context_snapshot_tiddler.html || (echo "HTML tiddler not generated!" && exit 1)
test -f .archcode/context_snapshot.tid || (echo "TiddlyWiki .tid not generated!" && exit 1)
From 2ea955225ee04aeb907a7d87c5e8330574c2fd31 Mon Sep 17 00:00:00 2001
From: pierre22400 <154444977+pierre22400@users.noreply.github.com>
Date: Fri, 15 Aug 2025 11:23:52 +0200
Subject: [PATCH 2/2] Update context-snapshot.yml
---
.github/workflows/context-snapshot.yml | 755 +++++++++++++------------
1 file changed, 378 insertions(+), 377 deletions(-)
diff --git a/.github/workflows/context-snapshot.yml b/.github/workflows/context-snapshot.yml
index 366b317..1810c2f 100644
--- a/.github/workflows/context-snapshot.yml
+++ b/.github/workflows/context-snapshot.yml
@@ -26,271 +26,272 @@ jobs:
python -m pip install pyyaml
- name: Create dirs
- run: mkdir -p scripts .archcode
+ run: |
+ mkdir -p scripts .archcode
- name: Write context_snapshot.py
shell: bash
run: |
-cat > scripts/context_snapshot.py <<'PY'
-#!/usr/bin/env python3
-from __future__ import annotations
-"""
-Minimal context snapshot (stable)
-- Génère .archcode/context_snapshot.yaml
-- Remplit 'project' avec fallback (GITHUB_REPOSITORY puis basename)
-"""
-import ast, sys, os, platform, yaml
-from dataclasses import dataclass
-from pathlib import Path
-from typing import Any, Dict, List, Optional, Union
-from datetime import datetime
-
-IGNORED_DIRS = {".git","__pycache__",".venv","venv",".env",".mypy_cache",".pytest_cache","node_modules",".idea",".vscode"}
-DEFAULT_OUT = ".archcode/context_snapshot.yaml"
-
-class _LiteralDumper(yaml.SafeDumper): pass
-def _repr_str(dumper: yaml.Dumper, data: str):
- style = "|" if ("\n" in data) else None
- return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=style)
-_LiteralDumper.add_representer(str, _repr_str)
-
-def iter_files(root: Path) -> List[Path]:
- files: List[Path] = []
- for p in sorted(root.rglob("*")):
- try:
- rel = p.relative_to(root)
- except Exception:
- continue
- if any(seg in IGNORED_DIRS for seg in rel.parts):
- continue
- if p.is_file():
- files.append(p)
- return files
-
-def ascii_tree(root: Path) -> str:
- lines=[str(root.resolve())]
- def kids(d: Path) -> List[Path]:
- out=[]
- try:
- it=sorted(d.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
- except Exception:
- return out
- for p in it:
- try:
- rel=p.relative_to(root)
- except Exception:
- continue
- if any(seg in IGNORED_DIRS for seg in rel.parts): continue
- out.append(p)
- return out
- def walk(d: Path, prefix=""):
- ch=kids(d)
- for i,k in enumerate(ch):
- joint="└── " if i==len(ch)-1 else "├── "
- lines.append(f"{prefix}{joint}{k.name}")
- if k.is_dir():
- ext=" " if i==len(ch)-1 else "│ "
- walk(k,prefix+ext)
- walk(root)
- return "\n".join(lines)
-
-@dataclass
-class RouteInfo:
- framework:str
- method:Optional[Union[str,List[str]]]=None
- path:Optional[str]=None
-
-@dataclass
-class DefInfo:
- qualname:str; name:str; lineno:int
- params:List[str]; decorators:List[str]
- docstring:Optional[str]; route:Optional[RouteInfo]
-
-def _first_toplevel_string_after_imports(module: ast.Module) -> Optional[str]:
- for node in module.body:
- if isinstance(node, ast.Expr) and isinstance(getattr(node,"value",None), ast.Constant):
- if isinstance(node.value.value,str): return node.value.value
- return None
-
-def _decorator_to_text(dec: ast.AST) -> str:
- def name_of(n):
- if isinstance(n, ast.Name): return n.id
- if isinstance(n, ast.Attribute): return f"{name_of(n.value)}.{n.attr}"
- if isinstance(n, ast.Call): return name_of(n.func)
- return n.__class__.__name__
- return name_of(dec)
-
-def _literal_str(node):
- if isinstance(node, ast.Constant) and isinstance(node.value,str): return node.value
- return None
-
-def _literal_methods_from_kwargs(call: ast.Call):
- for kw in call.keywords:
- if kw.arg in {"methods","method"}:
- v=kw.value
- if isinstance(v,ast.List):
- out=[e.value.upper() for e in v.elts if isinstance(e,ast.Constant) and isinstance(e.value,str)]
- return out or None
- if isinstance(v,ast.Constant) and isinstance(v.value,str):
- return [v.value.upper()]
- return None
-
-def _detect_route(dec: ast.AST):
- if isinstance(dec,ast.Call) and isinstance(dec.func,ast.Attribute):
- attr=dec.func.attr.lower()
- if attr in {"get","post","put","delete","patch","options","head"}:
- return RouteInfo("fastapi", attr.upper(), _literal_str(dec.args[0]) if dec.args else None)
- if attr=="route":
- return RouteInfo("flask", _literal_methods_from_kwargs(dec) or ["GET"], _literal_str(dec.args[0]) if dec.args else None)
- if attr=="command": return RouteInfo("typer","COMMAND",None)
- if isinstance(dec,ast.Attribute):
- attr=dec.attr.lower()
- if attr in {"get","post","put","delete","patch","options","head"}: return RouteInfo("fastapi", attr.upper(), None)
- if attr=="route": return RouteInfo("flask", ["GET"], None)
- if attr=="command": return RouteInfo("typer","COMMAND",None)
- return None
-
-class _DefCollector(ast.NodeVisitor):
- def __init__(self): self.stack=[]; self.defs=[]
- def visit_ClassDef(self,node): self.stack.append(node.name); self.generic_visit(node); self.stack.pop()
- def visit_FunctionDef(self,node): self._handle_def(node)
- def visit_AsyncFunctionDef(self,node): self._handle_def(node)
- def _handle_def(self,node):
- qual=".".join(self.stack+[node.name]) if self.stack else node.name
- params=[a.arg for a in list(node.args.posonlyargs)+list(node.args.args)]
- if node.args.vararg: params.append("*"+node.args.vararg.arg)
- if node.args.kwonlyargs: params+= [ka.arg for ka in node.args.kwonlyargs]
- if node.args.kwarg: params.append("**"+node.args.kwarg.arg)
- decs=[_decorator_to_text(d) for d in node.decorator_list]
- route=None
- for d in node.decorator_list:
- r=_detect_route(d)
- if r: route=r; break
- doc=ast.get_docstring(node)
- self.defs.append(DefInfo(qual,node.name,getattr(node,"lineno",-1),params,decs,doc,route))
-
-def extract_python_file(py_path: Path) -> Dict[str,Any]:
- text = py_path.read_text(encoding="utf-8", errors="ignore")
- try:
- mod = ast.parse(text)
- except SyntaxError as e:
- return {"path": str(py_path), "error": f"SyntaxError: {e}"}
-
- # 1) docstring-literal banner (module-level string after imports)
- banner = _first_toplevel_string_after_imports(mod)
-
- # 2) fallback : bloc de commentaires en tête de fichier (consécutifs),
- # utile pour les bannières visuelles utilisant '# -----'
- if not banner:
- comment_lines = []
- for ln in text.splitlines():
- s = ln.strip()
- # shebang / encoding lines are tolerated but not captured as banner:
- if s.startswith("#!"):
- continue
- if s.startswith("# -*-") or s.startswith("# coding:"):
- continue
- if s.startswith("#"):
- # retire le '#' initial et un espace éventuel
- comment_lines.append(s.lstrip("# ").rstrip())
- continue
- # stop dès la première ligne non-comment/non-empty (après éventuels commentaires)
- if s == "":
- # autorise un blanc initial, continue si pas encore collecté
- if comment_lines:
- break
- else:
- continue
- break
- if comment_lines:
- banner = "\n".join(comment_lines)
-
- module_doc = ast.get_docstring(mod)
- col = _DefCollector()
- col.visit(mod)
- defs_out = []
- for d in col.defs:
- route_block = None
- if d.route:
- route_block = {"framework": d.route.framework, "method": d.route.method, "path": d.route.path}
- defs_out.append(
- {
- "qualname": d.qualname,
- "name": d.name,
- "lineno": d.lineno,
- "params": d.params,
- "decorators": d.decorators or [],
- "docstring": d.docstring or "",
- "route": route_block,
- }
- )
- return {
- "path": str(py_path),
- "banner": banner or "",
- "module_docstring": module_doc or "",
- "defs": defs_out,
- }
-
-def main(argv):
- # parse args
- root_arg=None; out_arg=None
- it=iter(argv)
- for a in it:
- if a=="--root": root_arg=next(it,None)
- elif a=="--out": out_arg=next(it,None)
-
- # root / out
- root=Path(root_arg) if root_arg else Path.cwd()
- if not root.exists():
- print(f"[ERROR] Racine invalide: {root}", file=sys.stderr); return 2
- out_path=Path(out_arg) if out_arg else root / DEFAULT_OUT
- out_path.parent.mkdir(parents=True, exist_ok=True)
-
- # files
- all_files=iter_files(root)
- py_files=[p for p in all_files if p.suffix==".py"]
-
- # robust project name
- project_name = root.name or ""
- if not project_name:
- repo_env = os.environ.get("GITHUB_REPOSITORY")
- if repo_env and "/" in repo_env:
- project_name = repo_env.split("/", 1)[1]
- else:
- try:
- project_name = os.path.basename(str(root.resolve()))
- except Exception:
- project_name = ""
-
- # snapshot
- snapshot={
- "snapshot": {
- "project": project_name,
- "root": str(root.resolve()),
- "generated_at": datetime.utcnow().isoformat(timespec="seconds")+"Z",
- "python": sys.version.split()[0],
- "platform": platform.platform(),
- "files_count": len(all_files),
- "py_files_count": len(py_files),
- "ignored_dirs": sorted(list(IGNORED_DIRS)),
- },
- "tree": ascii_tree(root),
- "files": [],
- }
- for py in py_files:
- item=extract_python_file(py)
- try: item["relpath"]=str(py.relative_to(root)).replace(os.sep,"/")
- except Exception: item["relpath"]=str(py)
- snapshot["files"].append(item)
-
- with out_path.open("w", encoding="utf-8") as f:
- yaml.dump(snapshot, f, Dumper=_LiteralDumper, sort_keys=False, allow_unicode=True, width=100)
- print(f"[OK] Contexte écrit → {out_path}")
- return 0
-
-if __name__ == "__main__":
- raise SystemExit(main(sys.argv[1:]))
-PY
+ cat > scripts/context_snapshot.py <<'PY'
+ #!/usr/bin/env python3
+ from __future__ import annotations
+ """
+ Minimal context snapshot (stable)
+ - Génère .archcode/context_snapshot.yaml
+ - Remplit 'project' avec fallback (GITHUB_REPOSITORY puis basename)
+ """
+ import ast, sys, os, platform, yaml
+ from dataclasses import dataclass
+ from pathlib import Path
+ from typing import Any, Dict, List, Optional, Union
+ from datetime import datetime
+
+ IGNORED_DIRS = {".git","__pycache__",".venv","venv",".env",".mypy_cache",".pytest_cache","node_modules",".idea",".vscode"}
+ DEFAULT_OUT = ".archcode/context_snapshot.yaml"
+
+ class _LiteralDumper(yaml.SafeDumper): pass
+ def _repr_str(dumper: yaml.Dumper, data: str):
+ style = "|" if ("\n" in data) else None
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=style)
+ _LiteralDumper.add_representer(str, _repr_str)
+
+ def iter_files(root: Path) -> List[Path]:
+ files: List[Path] = []
+ for p in sorted(root.rglob("*")):
+ try:
+ rel = p.relative_to(root)
+ except Exception:
+ continue
+ if any(seg in IGNORED_DIRS for seg in rel.parts):
+ continue
+ if p.is_file():
+ files.append(p)
+ return files
+
+ def ascii_tree(root: Path) -> str:
+ lines=[str(root.resolve())]
+ def kids(d: Path) -> List[Path]:
+ out=[]
+ try:
+ it=sorted(d.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
+ except Exception:
+ return out
+ for p in it:
+ try:
+ rel=p.relative_to(root)
+ except Exception:
+ continue
+ if any(seg in IGNORED_DIRS for seg in rel.parts): continue
+ out.append(p)
+ return out
+ def walk(d: Path, prefix=""):
+ ch=kids(d)
+ for i,k in enumerate(ch):
+ joint="└── " if i==len(ch)-1 else "├── "
+ lines.append(f"{prefix}{joint}{k.name}")
+ if k.is_dir():
+ ext=" " if i==len(ch)-1 else "│ "
+ walk(k,prefix+ext)
+ walk(root)
+ return "\n".join(lines)
+
+ @dataclass
+ class RouteInfo:
+ framework:str
+ method:Optional[Union[str,List[str]]]=None
+ path:Optional[str]=None
+
+ @dataclass
+ class DefInfo:
+ qualname:str; name:str; lineno:int
+ params:List[str]; decorators:List[str]
+ docstring:Optional[str]; route:Optional[RouteInfo]
+
+ def _first_toplevel_string_after_imports(module: ast.Module) -> Optional[str]:
+ for node in module.body:
+ if isinstance(node, ast.Expr) and isinstance(getattr(node,"value",None), ast.Constant):
+ if isinstance(node.value.value,str): return node.value.value
+ return None
+
+ def _decorator_to_text(dec: ast.AST) -> str:
+ def name_of(n):
+ if isinstance(n, ast.Name): return n.id
+ if isinstance(n, ast.Attribute): return f"{name_of(n.value)}.{n.attr}"
+ if isinstance(n, ast.Call): return name_of(n.func)
+ return n.__class__.__name__
+ return name_of(dec)
+
+ def _literal_str(node):
+ if isinstance(node, ast.Constant) and isinstance(node.value,str): return node.value
+ return None
+
+ def _literal_methods_from_kwargs(call: ast.Call):
+ for kw in call.keywords:
+ if kw.arg in {"methods","method"}:
+ v=kw.value
+ if isinstance(v,ast.List):
+ out=[e.value.upper() for e in v.elts if isinstance(e,ast.Constant) and isinstance(e.value,str)]
+ return out or None
+ if isinstance(v,ast.Constant) and isinstance(v.value,str):
+ return [v.value.upper()]
+ return None
+
+ def _detect_route(dec: ast.AST):
+ if isinstance(dec,ast.Call) and isinstance(dec.func,ast.Attribute):
+ attr=dec.func.attr.lower()
+ if attr in {"get","post","put","delete","patch","options","head"}:
+ return RouteInfo("fastapi", attr.upper(), _literal_str(dec.args[0]) if dec.args else None)
+ if attr=="route":
+ return RouteInfo("flask", _literal_methods_from_kwargs(dec) or ["GET"], _literal_str(dec.args[0]) if dec.args else None)
+ if attr=="command": return RouteInfo("typer","COMMAND",None)
+ if isinstance(dec,ast.Attribute):
+ attr=dec.attr.lower()
+ if attr in {"get","post","put","delete","patch","options","head"}: return RouteInfo("fastapi", attr.upper(), None)
+ if attr=="route": return RouteInfo("flask", ["GET"], None)
+ if attr=="command": return RouteInfo("typer","COMMAND",None)
+ return None
+
+ class _DefCollector(ast.NodeVisitor):
+ def __init__(self): self.stack=[]; self.defs=[]
+ def visit_ClassDef(self,node): self.stack.append(node.name); self.generic_visit(node); self.stack.pop()
+ def visit_FunctionDef(self,node): self._handle_def(node)
+ def visit_AsyncFunctionDef(self,node): self._handle_def(node)
+ def _handle_def(self,node):
+ qual=".".join(self.stack+[node.name]) if self.stack else node.name
+ params=[a.arg for a in list(node.args.posonlyargs)+list(node.args.args)]
+ if node.args.vararg: params.append("*"+node.args.vararg.arg)
+ if node.args.kwonlyargs: params+= [ka.arg for ka in node.args.kwonlyargs]
+ if node.args.kwarg: params.append("**"+node.args.kwarg.arg)
+ decs=[_decorator_to_text(d) for d in node.decorator_list]
+ route=None
+ for d in node.decorator_list:
+ r=_detect_route(d)
+ if r: route=r; break
+ doc=ast.get_docstring(node)
+ self.defs.append(DefInfo(qual,node.name,getattr(node,"lineno",-1),params,decs,doc,route))
+
+ def extract_python_file(py_path: Path) -> Dict[str,Any]:
+ text = py_path.read_text(encoding="utf-8", errors="ignore")
+ try:
+ mod = ast.parse(text)
+ except SyntaxError as e:
+ return {"path": str(py_path), "error": f"SyntaxError: {e}"}
+
+ # 1) docstring-literal banner (module-level string after imports)
+ banner = _first_toplevel_string_after_imports(mod)
+
+ # 2) fallback : bloc de commentaires en tête de fichier (consécutifs),
+ # utile pour les bannières visuelles utilisant '# -----'
+ if not banner:
+ comment_lines = []
+ for ln in text.splitlines():
+ s = ln.strip()
+ # shebang / encoding lines are tolerated but not captured as banner:
+ if s.startswith("#!"):
+ continue
+ if s.startswith("# -*-") or s.startswith("# coding:"):
+ continue
+ if s.startswith("#"):
+ # retire le '#' initial et un espace éventuel
+ comment_lines.append(s.lstrip("# ").rstrip())
+ continue
+ # stop dès la première ligne non-comment/non-empty (après éventuels commentaires)
+ if s == "":
+ # autorise un blanc initial, continue si pas encore collecté
+ if comment_lines:
+ break
+ else:
+ continue
+ break
+ if comment_lines:
+ banner = "\n".join(comment_lines)
+
+ module_doc = ast.get_docstring(mod)
+ col = _DefCollector()
+ col.visit(mod)
+ defs_out = []
+ for d in col.defs:
+ route_block = None
+ if d.route:
+ route_block = {"framework": d.route.framework, "method": d.route.method, "path": d.route.path}
+ defs_out.append(
+ {
+ "qualname": d.qualname,
+ "name": d.name,
+ "lineno": d.lineno,
+ "params": d.params,
+ "decorators": d.decorators or [],
+ "docstring": d.docstring or "",
+ "route": route_block,
+ }
+ )
+ return {
+ "path": str(py_path),
+ "banner": banner or "",
+ "module_docstring": module_doc or "",
+ "defs": defs_out,
+ }
+
+ def main(argv):
+ # parse args
+ root_arg=None; out_arg=None
+ it=iter(argv)
+ for a in it:
+ if a=="--root": root_arg=next(it,None)
+ elif a=="--out": out_arg=next(it,None)
+
+ # root / out
+ root=Path(root_arg) if root_arg else Path.cwd()
+ if not root.exists():
+ print(f"[ERROR] Racine invalide: {root}", file=sys.stderr); return 2
+ out_path=Path(out_arg) if out_arg else root / DEFAULT_OUT
+ out_path.parent.mkdir(parents=True, exist_ok=True)
+
+ # files
+ all_files=iter_files(root)
+ py_files=[p for p in all_files if p.suffix==".py"]
+
+ # robust project name
+ project_name = root.name or ""
+ if not project_name:
+ repo_env = os.environ.get("GITHUB_REPOSITORY")
+ if repo_env and "/" in repo_env:
+ project_name = repo_env.split("/", 1)[1]
+ else:
+ try:
+ project_name = os.path.basename(str(root.resolve()))
+ except Exception:
+ project_name = ""
+
+ # snapshot
+ snapshot={
+ "snapshot": {
+ "project": project_name,
+ "root": str(root.resolve()),
+ "generated_at": datetime.utcnow().isoformat(timespec="seconds")+"Z",
+ "python": sys.version.split()[0],
+ "platform": platform.platform(),
+ "files_count": len(all_files),
+ "py_files_count": len(py_files),
+ "ignored_dirs": sorted(list(IGNORED_DIRS)),
+ },
+ "tree": ascii_tree(root),
+ "files": [],
+ }
+ for py in py_files:
+ item=extract_python_file(py)
+ try: item["relpath"]=str(py.relative_to(root)).replace(os.sep,"/")
+ except Exception: item["relpath"]=str(py)
+ snapshot["files"].append(item)
+
+ with out_path.open("w", encoding="utf-8") as f:
+ yaml.dump(snapshot, f, Dumper=_LiteralDumper, sort_keys=False, allow_unicode=True, width=100)
+ print(f"[OK] Contexte écrit → {out_path}")
+ return 0
+
+ if __name__ == "__main__":
+ raise SystemExit(main(sys.argv[1:]))
+ PY
- name: Run snapshot
run: |
@@ -300,122 +301,122 @@ PY
- name: Generate HTML page + true TiddlyWiki .tid (no truncation)
shell: bash
run: |
-cat > .archcode/_context_snapshot_html_builder.py <<'PY'
-import yaml, html, sys, json
-from pathlib import Path
-from datetime import datetime
-
-src = Path(".archcode/context_snapshot.yaml")
-out_html = Path(".archcode/context_snapshot_tiddler.html")
-out_tid = Path(".archcode/context_snapshot.tid")
-
-if not src.exists():
- print("No snapshot YAML found", file=sys.stderr); raise SystemExit(2)
-
-s = yaml.safe_load(src.read_text(encoding="utf-8")) or {}
-meta = s.get("snapshot", {}) or {}
-proj = html.escape(meta.get("project") or "")
-
-CSS = """
-
-"""
-
-frag = []
-frag.append(f"Context Snapshot — {proj or '—'}
")
-
-frag.append("")
-
-frag.append("Arbre du projet
")
-frag.append(html.escape(s.get("tree","") or ""))
-frag.append(" ")
-
-files = s.get("files") or []
-frag.append(f"Fichiers ({len(files)})
")
-for f in files:
- rel = f.get("relpath") or f.get("path") or ""
- rel_e = html.escape(rel)
- defs = f.get("defs") or []
- ndefs = len(defs)
- err = f.get("error")
- err_badge = "
parse error" if err else ""
- frag.append(f"
{rel_e} — {ndefs} defs{err_badge}
")
-
- if err:
- frag.append(f"Erreur de parsing
{html.escape(str(err))}
")
-
- if f.get("banner"):
- frag.append("Banner"+html.escape(str(f.get("banner")))+" ")
- if f.get("module_docstring"):
- frag.append("Docstring module"+html.escape(str(f.get("module_docstring")))+" ")
-
- if defs:
- frag.append(f"Defs ({ndefs})")
- for d in defs:
- q = d.get("qualname","")
- ln = d.get("lineno","")
- params = d.get("params") or []
- sig = f"{d.get('name','')}(" + ", ".join(params) + ")"
- route = d.get("route")
- route_txt = ""
- if isinstance(route, dict):
- method = route.get("method")
- if isinstance(method, list): method = ",".join(method)
- path = route.get("path") or ""
- route_txt = f" [{html.escape(str(route.get('framework') or ''))} {html.escape(str(method or ''))} {html.escape(str(path))}]"
-
- ds_full = (d.get("docstring") or "").strip()
- ds_first = ds_full.splitlines()[0] if ds_full else ""
- li = f"{html.escape(q)} (line {html.escape(str(ln))}) — {html.escape(sig)}{route_txt}"
- if ds_first:
- li += f" — {html.escape(ds_first)}"
- if ds_full and (('\\n' in ds_full) or (len(ds_full) > len(ds_first))):
- li += f"docstring complète
{html.escape(ds_full)} "
- li += " "
- frag.append(li)
- frag.append("
")
- frag.append("")
-frag.append("
")
-
-fragment_html = CSS + "\n" + "\n".join(frag)
-
-full_html = "\nContext Snapshot — %s%s" % (proj, fragment_html)
-out_html.write_text(full_html, encoding="utf-8")
-
-def tw_timestamp(dt):
- return dt.strftime("%Y%m%d%H%M%S") + f"{dt.microsecond//1000:03d}"
-
-now = datetime.utcnow()
-title = f"Context Snapshot — {meta.get('project') or '-'}"
-header = [
- f"title: {title}",
- "tags: mARCHCode Snapshot",
- f"created: {tw_timestamp(now)}",
- f"modified: {tw_timestamp(now)}",
- "type: text/html",
-]
-out_tid.write_text("\n".join(header) + "\n\n" + fragment_html, encoding="utf-8")
-
-print("Wrote:", out_html, "and", out_tid)
-PY
+ cat > .archcode/_context_snapshot_html_builder.py <<'PY'
+ import yaml, html, sys, json
+ from pathlib import Path
+ from datetime import datetime
+
+ src = Path(".archcode/context_snapshot.yaml")
+ out_html = Path(".archcode/context_snapshot_tiddler.html")
+ out_tid = Path(".archcode/context_snapshot.tid")
+
+ if not src.exists():
+ print("No snapshot YAML found", file=sys.stderr); raise SystemExit(2)
+
+ s = yaml.safe_load(src.read_text(encoding="utf-8")) or {}
+ meta = s.get("snapshot", {}) or {}
+ proj = html.escape(meta.get("project") or "")
+
+ CSS = """
+
+ """
+
+ frag = []
+ frag.append(f"Context Snapshot — {proj or '—'}
")
+
+ frag.append("")
+
+ frag.append("Arbre du projet
")
+ frag.append(html.escape(s.get("tree","") or ""))
+ frag.append(" ")
+
+ files = s.get("files") or []
+ frag.append(f"Fichiers ({len(files)})
")
+ for f in files:
+ rel = f.get("relpath") or f.get("path") or ""
+ rel_e = html.escape(rel)
+ defs = f.get("defs") or []
+ ndefs = len(defs)
+ err = f.get("error")
+ err_badge = "
parse error" if err else ""
+ frag.append(f"
{rel_e} — {ndefs} defs{err_badge}
")
+
+ if err:
+ frag.append(f"Erreur de parsing
{html.escape(str(err))}
")
+
+ if f.get("banner"):
+ frag.append("Banner"+html.escape(str(f.get("banner")))+" ")
+ if f.get("module_docstring"):
+ frag.append("Docstring module"+html.escape(str(f.get("module_docstring")))+" ")
+
+ if defs:
+ frag.append(f"Defs ({ndefs})")
+ for d in defs:
+ q = d.get("qualname","")
+ ln = d.get("lineno","")
+ params = d.get("params") or []
+ sig = f"{d.get('name','')}(" + ", ".join(params) + ")"
+ route = d.get("route")
+ route_txt = ""
+ if isinstance(route, dict):
+ method = route.get("method")
+ if isinstance(method, list): method = ",".join(method)
+ path = route.get("path") or ""
+ route_txt = f" [{html.escape(str(route.get('framework') or ''))} {html.escape(str(method or ''))} {html.escape(str(path))}]"
+
+ ds_full = (d.get("docstring") or "").strip()
+ ds_first = ds_full.splitlines()[0] if ds_full else ""
+ li = f"{html.escape(q)} (line {html.escape(str(ln))}) — {html.escape(sig)}{route_txt}"
+ if ds_first:
+ li += f" — {html.escape(ds_first)}"
+ if ds_full and (('\\n' in ds_full) or (len(ds_full) > len(ds_first))):
+ li += f"docstring complète
{html.escape(ds_full)} "
+ li += " "
+ frag.append(li)
+ frag.append("
")
+ frag.append("")
+ frag.append("
")
+
+ fragment_html = CSS + "\n" + "\n".join(frag)
+
+ full_html = "\nContext Snapshot — %s%s" % (proj, fragment_html)
+ out_html.write_text(full_html, encoding="utf-8")
+
+ def tw_timestamp(dt):
+ return dt.strftime("%Y%m%d%H%M%S") + f"{dt.microsecond//1000:03d}"
+
+ now = datetime.utcnow()
+ title = f"Context Snapshot — {meta.get('project') or '-'}"
+ header = [
+ f"title: {title}",
+ "tags: mARCHCode Snapshot",
+ f"created: {tw_timestamp(now)}",
+ f"modified: {tw_timestamp(now)}",
+ "type: text/html",
+ ]
+ out_tid.write_text("\n".join(header) + "\n\n" + fragment_html, encoding="utf-8")
+
+ print("Wrote:", out_html, "and", out_tid)
+ PY
test -f .archcode/context_snapshot_tiddler.html || (echo "HTML tiddler not generated!" && exit 1)
test -f .archcode/context_snapshot.tid || (echo "TiddlyWiki .tid not generated!" && exit 1)