diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..6f6b566
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,51 @@
+name: tests
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+jobs:
+ test:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ python: ["3.9", "3.12"]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python }}
+
+ - name: Compile scripts (syntax)
+ run: python -m py_compile scripts/scan.py scripts/query.py scripts/apply_audit.py scripts/render.py
+
+ - name: Unit + golden tests
+ run: python -m unittest discover -s tests -v
+
+ - name: Render the sample project (smoke)
+ run: >
+ python scripts/render.py
+ --state examples/sample-project/modules.json
+ --template assets/template.html
+ --out-html /tmp/codemap.html --out-md /tmp/codemap.md
+ shell: bash
+
+ template-js:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ - name: Syntax-check the template's inline JS
+ run: |
+ python - <<'PY'
+ import re
+ html = open("assets/template.html", encoding="utf-8").read()
+ m = re.search(r"", html, re.S)
+ open("/tmp/_t.js", "w", encoding="utf-8").write(m.group(1).replace("__ARCH_DATA__", "{}"))
+ PY
+ node --check /tmp/_t.js
diff --git a/README.md b/README.md
index 6f6dd9a..645b89f 100644
--- a/README.md
+++ b/README.md
@@ -9,11 +9,13 @@ helps you **pay down the cruft** — incrementally, one commit at a time.



+[](https://github.com/Asixa/codemap-skill/actions/workflows/test.yml)
> Every codebase accumulates cruft over time — monkeypatches, silent fallbacks, dead
> "legacy" paths, half-finished stubs, copy-pasted duplication, god-files, and valueless
> glue. **codemap surfaces that rot, ranks it, and hands an AI agent a clear punch-list to
-> fix it** — with a regression-gated fix loop so the cleanup never breaks your build.
+> fix it** — with a regression-gated fix loop: a change is accepted only when an
+> independent check shows your tests still pass.

@@ -63,12 +65,24 @@ Most "architecture diagram" tools draw *files and imports*. codemap is different
- **Incremental + git-aware.** A per-module content hash + the last-run commit mean re-runs
only re-audit what changed, and `update` shows you the **commits since last time** and
which modules they touched.
-- **Cleanup that can't regress.** `fix` runs a four-role loop — lock a test baseline →
- fix → an **independent acceptance check** proves the pre-fix tests still pass → re-score.
+- **Regression-gated cleanup.** `fix` runs a four-role loop — lock a test baseline →
+ fix → an **independent acceptance check** must show the pre-fix tests still pass → re-score.
It's the maintenance pass you never have time to do, turned into something an agent can
run on a schedule.
+> **What it is (and isn't).** codemap is an agent-orchestration framework that makes the
+> map + audit *consistent and reviewable* — deterministic scripts handle LoC, hashing,
+> staleness, filtering and rendering, and a fixed rubric forces `file:line` evidence and
+> an independent audit per module. But the **module decomposition and the scores are model
+> judgments**, not the output of a deterministic static analyzer. Treat the map as a
+> high-quality, reviewable starting point — and commit `modules.json` so every score is
+> diffable in PRs.
+
+Want to see it before installing? Open
+**[`examples/sample-project/codemap.html`](examples/sample-project/codemap.html)** — a
+fully rendered demo (the sample used for the screenshots).
+
## Screenshots
**Click any module** to highlight what it calls (downstream) and what depends on it
@@ -155,11 +169,15 @@ codemap/
scripts/ # deterministic, stdlib-only Python
scan.py # LoC + content hash + git diff + staleness
query.py # filter modules (grade/tag/severity/…) → ids/paths/findings
- apply_audit.py # merge one subagent's audit into the state
+ apply_audit.py # validate + merge one subagent's audit into the state
render.py # modules.json → HTML + report
assets/
template.html # the interactive map shell (data injected at render time)
- examples/ # the screenshots above
+ tests/ # stdlib unittest golden tests for the scripts
+ examples/
+ 01-map.png … # the screenshots above
+ sample-project/ # a fully rendered demo (modules.json + codemap.html/md)
+ .github/workflows/test.yml # CI: py_compile + unittest + render + JS syntax check
```
## License
diff --git a/SKILL.md b/SKILL.md
index 8f5e8ee..5b25da8 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -91,10 +91,10 @@ per-module subagent loop — never load the full `modules.json` just to pick tar
## Hard rules
-1. **Every module score comes from an independent subagent.** One subagent audits one
+1. **Every module score comes from an independent sub-task.** One sub-task audits one
module against its `paths`, using the prompt in `reference/STANDARDS.md`. Never score
- inline in the main thread; never copy one module's score to another. Spawn them in
- parallel (one message, multiple Agent calls — Explore or general-purpose).
+ inline in the main thread; never copy one module's score to another. Run them in
+ parallel where the platform supports it (see *Capabilities & platform mapping*).
2. **Scripts are deterministic; only decomposition, auditing, and theme-synthesis are
model work.** `scan.py` / `render.py` / `apply_audit.py` never make quality judgments.
3. **`modules.json` is the only thing you edit by hand** (structure/decomposition).
@@ -111,14 +111,29 @@ per-module subagent loop — never load the full `modules.json` just to pick tar
build/typecheck is clean. A fixer may not write/edit its own tests or grade its own
work — that defeats the gate.
+## Capabilities & platform mapping
+
+This workflow needs three capabilities. Each has a graceful fallback, so it runs on any
+agent — only the convenience changes, never the rules above.
+
+| Capability | Native (Claude Code) | Codex / Cursor | Fallback if unavailable |
+|---|---|---|---|
+| **Independent sub-tasks** (one auditor/fixer per module) | `Agent` tool, many in parallel | their subagent/task tool | Audit modules **one at a time in the main thread** — still one module per pass against the rubric, never batch-scoring. Slower, fully valid. |
+| **Structured result** (the audit JSON) | `schema` on the Agent call | tool-specific schema, or just ask for JSON | Ask the sub-task to return **only** the JSON object; `apply_audit.py` validates it and rejects malformed/inconsistent results — no schema feature required. |
+| **Ask the user** (preferences on `init`) | `AskUserQuestion` | tool's prompt UI | Ask in plain text, or apply defaults (`lang=en`, output `.codemap/`, title = repo folder name) and tell the user how to change them in `.codemap/config.json`. |
+
+The non-negotiables (independent per-module audit, deterministic scripts, the four-role
+fix gate) hold on every platform; the table only changes *how* you spawn the work.
+
---
## Command: `init` (first build)
Use when no `modules.json` exists yet. (Also accepts `generate` as an alias.)
-0. **Ask the user for preferences first** (use the AskUserQuestion tool), then save them to
- `/.codemap/config.json`:
+0. **Ask the user for preferences first** (use `AskUserQuestion` if available, else just ask
+ in plain text; or apply the defaults from *Capabilities & platform mapping*), then save
+ them to `/.codemap/config.json`:
- **UI language** — `en` or `zh` (localizes the map chrome + report; module names are
never translated). → `meta.lang`.
- **Output location** — where the HTML/MD go. Default `.codemap/` (kept with the tool
diff --git a/assets/template.html b/assets/template.html
index e751cc7..d29a4ed 100644
--- a/assets/template.html
+++ b/assets/template.html
@@ -201,6 +201,11 @@ const BANDS = DATA.bands || [];
const SPINE = DATA.spine || [];
const REPORT_THEMES = DATA.reportThemes || [];
const META = DATA.meta || {};
+/* HTML-escape every string that comes from modules.json (labels, descriptions,
+ findings, tags, paths, meta, themes, standard) before it enters innerHTML. The only
+ trusted HTML is the template's own i18n strings (tl) and the structural markup. */
+function esc(s){ return String(s==null?"":s).replace(/[&<>"']/g, c=>(
+ {"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); }
let BAD_TAGS = new Set(["monkeypatch","fallback","legacy","dual-format","stub","fake-output","bloat","duplication","glue","silent-except","silent-catch","any-escape","over-fit","god-component","placeholder"]);
/* ---------- i18n: display language via meta.lang (module names never translated) ---------- */
@@ -325,9 +330,9 @@ const svg=document.getElementById("edges");
const detail=document.getElementById("detail");
const cardEl={};
-document.getElementById("projTitle").innerHTML = (META.project||"Project")+` ${tl('mapSuffix')}`;
-document.getElementById("subLine").innerHTML = (META.subtitle||tl('sub'))+
- (META.mdPath?` · ${tl('report')} ↗`:"");
+document.getElementById("projTitle").innerHTML = esc(META.project||"Project")+` ${tl('mapSuffix')}`;
+document.getElementById("subLine").innerHTML = esc(META.subtitle||tl('sub'))+
+ (META.mdPath?` · ${tl('report')} ↗`:"");
/* localize static header chrome (module names/labels are never translated) */
document.querySelector(".scrollnote").textContent = tl("scrollNote");
document.getElementById("search").placeholder = tl("searchPh");
@@ -340,14 +345,14 @@ document.getElementById("tagFilter").options[0].textContent = tl("anyIssue");
BANDS.forEach(b=>{
if(b.wire){
const w=document.createElement("div"); w.className="wire";
- w.innerHTML=`