From 81b05643a87b44dffba92102eeb1b328b19b7100 Mon Sep 17 00:00:00 2001 From: Xingyu Chen Date: Wed, 10 Jun 2026 18:28:44 -0700 Subject: [PATCH] Make audit standard a configurable, editable page (standard.json + in-map editor); add copy-fix-prompt button --- README.md | 27 ++++-- SKILL.md | 10 +++ assets/template.html | 188 ++++++++++++++++++++++++++++------------ reference/DATA_MODEL.md | 8 ++ reference/standard.json | 37 ++++++++ scripts/render.py | 24 ++++- 6 files changed, 230 insertions(+), 64 deletions(-) create mode 100644 reference/standard.json diff --git a/README.md b/README.md index 1d0e5d9..12d145d 100644 --- a/README.md +++ b/README.md @@ -152,12 +152,29 @@ codemap/ template.html # the interactive map shell (data injected at render time) ``` -## Customizing the standard +## Customizing the standard (capture your own "what's a problem") -The rubric, smell taxonomy (tags), severity levels, and the exact subagent prompts live -in `reference/STANDARDS.md` — edit there and every future audit uses the new standard. -Add a tag? Also add it to the `BAD_TAGS` set (and `TAGS_ZH` for a label) in -`assets/template.html` so the map colors and counts it. +The standard is **data, not code** — it lives in `reference/standard.json` (rubric, +severities, coupling, and the issue tags with descriptions). Two ways to customize: + +- **In the map**: open the **Standard** page (header button), click **Edit**, change any + description, add your own tags (`+ Tag`), then **Export** → save the downloaded + `standard.json` to `/.claude/codemap/standard.json`. Edits are kept in the + browser until you export. Custom tags flow through the whole map (cards, filters, + report) and are used by future audits. +- **By file**: copy `reference/standard.json` to `/.claude/codemap/standard.json` + and edit it. `render.py` prefers the project file over the skill default. + +The prose version + the exact subagent audit prompt live in `reference/STANDARDS.md` +(keep the two in sync when changing the shipped defaults). + +## "Fix this" buttons & automation + +Each module's detail panel has a **Copy fix prompt** button — it copies `/codemap fix +` to the clipboard for you to paste into Claude Code (or adapt for Codex). The +page intentionally does **not** auto-launch an agent: a generated static HTML has no +privileged channel to the Claude CLI/agent, and auto-launching the CLI is fragile and +increasingly restricted. Copy-to-clipboard is the robust, future-proof bridge. ## Notes diff --git a/SKILL.md b/SKILL.md index 211d50f..3081d3f 100644 --- a/SKILL.md +++ b/SKILL.md @@ -33,6 +33,16 @@ The scoring rubric, smell taxonomy, severity levels, and the required subagent p are fixed in **`reference/STANDARDS.md`** — read it and follow it verbatim. The state schema is in **`reference/DATA_MODEL.md`**. Do not improvise scoring or invent tags. +**The standard is configurable per project.** A machine-readable copy lives in +`reference/standard.json` (rubric, severities, coupling, and the tag list with +descriptions). A project may override it by placing its own `standard.json` next to the +state file (`/.claude/codemap/standard.json`) — `render.py` picks the project +file first, else the skill default, and injects it into the map's editable **Standard** +page. **Honor the project's tag set:** when `.claude/codemap/standard.json` exists, audit +modules using *its* tags (including any custom tags the user added) — that is how users +capture their own definition of a problem. Keep `STANDARDS.md` (the prose + subagent +prompt) and `standard.json` (the machine copy) in sync if you change the defaults. + ## Conventions - `SKILL_DIR` = this skill's directory. Scripts are at `SKILL_DIR/scripts/*.py`, diff --git a/assets/template.html b/assets/template.html index 3019b82..6c5e40e 100644 --- a/assets/template.html +++ b/assets/template.html @@ -138,6 +138,14 @@ .std-range{flex:0 0 56px;font-family:var(--mono);font-size:11px;color:var(--muted)} .std-key{flex:0 0 116px} .std-row .sev{font-family:var(--mono);font-size:9px;font-weight:700;padding:2px 6px;border-radius:4px;letter-spacing:.4px} + .std-row .tg[contenteditable]{outline:none;min-width:40px} + .std-desc[contenteditable]{outline:1px dashed var(--border2);outline-offset:2px;border-radius:3px} + .xrm{flex:0 0 auto;background:none;border:none;color:var(--faint);cursor:pointer;font-size:12px;padding:0 2px;line-height:1} + .xrm:hover{color:#e0524b} + .toast{position:fixed;left:50%;bottom:26px;transform:translateX(-50%) translateY(8px);z-index:80; + background:var(--panel2);border:1px solid var(--border2);color:var(--ink);font-size:12.5px; + padding:9px 14px;border-radius:8px;opacity:0;pointer-events:none;transition:.18s;box-shadow:0 6px 20px #0007} + .toast.show{opacity:1;transform:translateX(-50%) translateY(0)} .std-desc{flex:1;font-size:12.5px;color:#cfd3d8;line-height:1.5} ::-webkit-scrollbar{width:11px;height:11px} ::-webkit-scrollbar-thumb{background:#2c3137;border-radius:6px;border:3px solid var(--bg2)} @@ -193,7 +201,7 @@ const BANDS = DATA.bands || []; const SPINE = DATA.spine || []; const REPORT_THEMES = DATA.reportThemes || []; const META = DATA.meta || {}; -const 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"]); +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) ---------- */ const LANG = META.lang || "en"; @@ -210,8 +218,12 @@ const I18N = { worst:"Worst offenders — click to inspect", commonTags:"Most-common smell tags", themes:"Cross-cutting themes", backToMap:"← Back to map", avg:"avg", filter:"Filter", modulesWord:"modules", allWord:"all", noMatches:"no matches", clearFilters:"Clear filters", btnReport:"Audit report", btnSpine:"Data-flow spine", colorCoupling:"Color: coupling", colorHealth:"Color: health", - btnStd:"Standard", stdTitle:"Audit standard", stdIntro:"How modules are scored — the same rubric for every module, language, and run.", + btnStd:"Standard", stdTitle:"Audit standard", stdIntro:"How modules are scored — the same rubric for every module, language, and run. Click Edit to add your own issue tags.", stdRubric:"Score → grade", stdSeverity:"Finding severity", stdTags:"Issue tags", stdCoupling:"Coupling (structural, not quality)", close:"Close", + stdEditBtn:"Edit", stdDone:"Done", stdExport:"Export", stdReset:"Reset", stdAddTag:"+ Tag", + stdEditHint:"Editing — changes save in this browser. Export → save as .claude/codemap/standard.json to make them permanent and used by audits.", + stdNewTagId:"New tag id (e.g. perf-risk, security):", stdExported:"Saved standard.json — put it in .claude/codemap/", + copyFix:"Copy fix prompt", copied:"Copied — paste into Claude Code / Codex", trackedLoc:(l,f)=>`${l} tracked LoC · ${f} files` }, zh:{ mapSuffix:"· 功能架构图", sub:"功能模块 · 调用层级 · 耦合 · 质量评分 · 代码行数", report:"报告", searchPh:"查找模块…", allGrades:"全部等级", anyIssue:"全部问题", scrollNote:"点击模块 · 滚动平移", @@ -225,59 +237,72 @@ const I18N = { worst:"最差模块 —— 点击查看", commonTags:"最常见问题标签", themes:"跨模块共性问题", backToMap:"← 返回地图", avg:"平均", filter:"筛选", modulesWord:"个模块", allWord:"全部", noMatches:"无匹配", clearFilters:"清除筛选", btnReport:"审计报告", btnSpine:"数据流主线", colorCoupling:"配色:耦合", colorHealth:"配色:健康度", - btnStd:"评判标准", stdTitle:"评判标准", stdIntro:"模块如何打分 —— 所有模块、所有语言、每一次运行都用同一套标准。", + btnStd:"评判标准", stdTitle:"评判标准", stdIntro:"模块如何打分 —— 所有模块、所有语言、每一次运行都用同一套标准。点「编辑」可加入你自己的问题标签。", stdRubric:"分数 → 等级", stdSeverity:"问题严重度", stdTags:"问题标签", stdCoupling:"耦合(结构维度,非质量)", close:"关闭", + stdEditBtn:"编辑", stdDone:"完成", stdExport:"导出", stdReset:"重置", stdAddTag:"+ 标签", + stdEditHint:"编辑中 —— 改动保存在本浏览器。导出 → 存为 .claude/codemap/standard.json 即永久生效并被审计采用。", + stdNewTagId:"新标签 id(如 perf-risk、security):", stdExported:"已生成 standard.json —— 放到 .claude/codemap/ 下", + copyFix:"复制修复指令", copied:"已复制 —— 粘贴到 Claude Code / Codex", trackedLoc:(l,f)=>`${l} 行(已跟踪)· ${f} 个文件` }, }; function tl(k){ const d=I18N[LANG]||I18N.en; return d[k]!=null?d[k]:(I18N.en[k]!=null?I18N.en[k]:k); } document.documentElement.lang = LANG; -/* smell-tag display labels (the tag id stays English for logic/filtering) */ -const TAGS_ZH={ monkeypatch:"猴补丁", fallback:"回退兜底", legacy:"遗留", "dual-format":"双格式", - stub:"占位桩", "fake-output":"伪造输出", bloat:"臃肿", duplication:"重复", glue:"胶水", - "silent-except":"静默吞错", "silent-catch":"静默吞错", "any-escape":"类型逃逸", - "over-fit":"过度特化", "god-component":"上帝组件", placeholder:"占位", clean:"干净" }; -function tagLabel(t){ return LANG==="zh" ? (TAGS_ZH[t]||t) : t; } +function stdText(o){ return o ? (LANG==="zh" ? (o.zh||o.en||"") : (o.en||o.zh||"")) : ""; } -/* ---------- the audit standard, shown on its own page (overlay) ---------- */ -const STD = { +/* ---------- the audit standard — editable, configurable, its own page ---------- + Effective standard = a saved browser draft (user edits), else the injected file + (DATA.standard, from .claude/codemap/standard.json or the skill default), else + this built-in fallback. Edit it on the Standard page and Export to standard.json + to make it permanent and used by audits. Custom tags flow through the whole map. */ +const BUILTIN_STD = { rubric:[ - {g:"A", s:95, r:"90–100", en:"clean, well-scoped, idiomatic", zh:"干净、职责单一、地道"}, - {g:"B", s:82, r:"75–89", en:"minor issues: a documented shim, mild bloat", zh:"小问题:有文档的兼容、轻微臃肿"}, - {g:"C", s:67, r:"60–74", en:"notable hacks / fallbacks / bloat / duplication", zh:"明显的 hack / 回退 / 臃肿 / 重复"}, - {g:"D", s:50, r:"40–59", en:"significant legacy / stubs / duplication, or a protocol violation", zh:"严重的遗留 / 占位 / 重复,或协议违规"}, - {g:"F", s:25, r:"0–39", en:"broken, fake output, or unfinished-but-wired", zh:"损坏、伪造输出,或未完成却已接线"}, + {grade:"A",score:95,range:"90–100",en:"clean, well-scoped, idiomatic",zh:"干净、职责单一、地道"}, + {grade:"B",score:82,range:"75–89",en:"minor issues: a documented shim, mild bloat",zh:"小问题:有文档的兼容、轻微臃肿"}, + {grade:"C",score:67,range:"60–74",en:"notable hacks / fallbacks / bloat / duplication",zh:"明显的 hack / 回退 / 臃肿 / 重复"}, + {grade:"D",score:50,range:"40–59",en:"significant legacy / stubs / duplication, or a protocol violation",zh:"严重的遗留 / 占位 / 重复,或协议违规"}, + {grade:"F",score:25,range:"0–39",en:"broken, fake output, or unfinished-but-wired",zh:"损坏、伪造输出,或未完成却已接线"}, ], - sev:[ - {k:"HIGH", en:"wrong / dangerous / fake, a protocol or security issue, or a genuine maintenance hazard", zh:"错误 / 危险 / 伪造、协议或安全问题,或真正的维护地雷"}, - {k:"MED", en:"a real smell a maintainer should fix", zh:"维护者该修的真坏味"}, - {k:"LOW", en:"a documented shim, a cosmetic cast, benign bloat — worth noting, not urgent", zh:"有文档的兼容、装饰性 cast、良性臃肿 —— 值得记、不紧急"}, + severities:[ + {key:"HIGH",en:"wrong / dangerous / fake, a protocol or security issue, or a genuine maintenance hazard",zh:"错误 / 危险 / 伪造、协议或安全问题,或真正的维护地雷"}, + {key:"MED",en:"a real smell a maintainer should fix",zh:"维护者该修的真坏味"}, + {key:"LOW",en:"a documented shim, a cosmetic cast, benign bloat — worth noting, not urgent",zh:"有文档的兼容、装饰性 cast、良性臃肿 —— 值得记、不紧急"}, ], coupling:[ - {k:"core", en:"system spine — central to almost everything", zh:"系统主线 —— 几乎牵连一切"}, - {k:"high", en:"many connections", zh:"连接很多"}, - {k:"med", en:"moderate", zh:"中等"}, - {k:"low", en:"leaf / self-contained", zh:"叶子 / 自洽"}, + {key:"core",en:"system spine — central to almost everything",zh:"系统主线 —— 几乎牵连一切"}, + {key:"high",en:"many connections",zh:"连接很多"}, + {key:"med",en:"moderate",zh:"中等"}, + {key:"low",en:"leaf / self-contained",zh:"叶子 / 自洽"}, + ], + tags:[ + {id:"monkeypatch",label:"monkeypatch",labelZh:"猴补丁",bad:true,en:"runtime mutation of another module / stdlib / vendor; reflection or prototype patching",zh:"运行时改写别的模块/标准库/依赖;反射或原型补丁"}, + {id:"fallback",label:"fallback",labelZh:"回退兜底",bad:true,en:"'try the real thing, then fake/degrade'; a||b||c chains that hide which value is real",zh:"“先试真的再退化/造假”;a||b||c 掩盖哪个是真值"}, + {id:"silent-except",label:"silent-except",labelZh:"静默吞错",bad:true,en:"swallowed errors: empty catch / except:pass / ignored return codes",zh:"吞掉错误:空 catch / except:pass / 忽略返回码"}, + {id:"legacy",label:"legacy",labelZh:"遗留",bad:true,en:"deprecated/back-compat shims, dead-but-shipped code, parallel old+new paths",zh:"遗留/兼容垫片、已发布的死代码、新旧并存"}, + {id:"dual-format",label:"dual-format",labelZh:"双格式",bad:true,en:"accepting two shapes for one field (snake||camel), patched through the code",zh:"同一字段接受两种形态(snake||camel)并散落各处"}, + {id:"stub",label:"stub",labelZh:"占位桩",bad:true,en:"NotImplemented / TODO / dead buttons / demo scripts presented as real",zh:"未实现/TODO/死按钮/演示脚本当成品"}, + {id:"fake-output",label:"fake-output",labelZh:"伪造输出",bad:true,en:"returns random/canned/hardcoded results where real work is implied",zh:"本应真算的地方返回随机/写死结果"}, + {id:"duplication",label:"duplication",labelZh:"重复",bad:true,en:"copy-pasted logic, or an existing shared abstraction not reused",zh:"复制粘贴,或已有共享抽象却不复用"}, + {id:"bloat",label:"bloat",labelZh:"臃肿",bad:true,en:"oversized file / function; too many responsibilities in one unit",zh:"超大文件/函数;单元职责过多"}, + {id:"glue",label:"glue",labelZh:"胶水",bad:true,en:"valueless pass-through: rows of thin forwarders / no-op adapters",zh:"无价值透传:成片薄包装 / 零转换适配器"}, + {id:"any-escape",label:"any-escape",labelZh:"类型逃逸",bad:true,en:"bypassing the type system: as any / @ts-ignore / dynamic / void* / reinterpret_cast / unsafe",zh:"绕过类型系统:as any / @ts-ignore / dynamic / void* / reinterpret_cast / unsafe"}, + {id:"over-fit",label:"over-fit",labelZh:"过度特化",bad:true,en:"hardcoded to one case where a small generalization was expected",zh:"硬编码单一情况,本应小幅泛化"}, + {id:"god-component",label:"god-component",labelZh:"上帝组件",bad:true,en:"one component/class/file doing far too much",zh:"一个组件/类/文件做太多事"}, + {id:"placeholder",label:"placeholder",labelZh:"占位",bad:true,en:"unfinished UI / data presented as if complete",zh:"未完成的 UI/数据当作已完成"}, + {id:"clean",label:"clean",labelZh:"干净",bad:false,en:"no material issues — well-scoped",zh:"无实质问题 —— 职责单一"}, ], - tags:["monkeypatch","fallback","silent-except","legacy","dual-format","stub","fake-output", - "duplication","bloat","glue","any-escape","over-fit","god-component","placeholder"], }; -const TAG_DESC = { - monkeypatch:{en:"runtime mutation of another module / stdlib / vendor; reflection or prototype patching", zh:"运行时改写别的模块/标准库/依赖;反射或原型补丁"}, - fallback:{en:"'try the real thing, then fake/degrade'; a||b||c chains that hide which value is real", zh:"“先试真的再退化/造假”;a||b||c 掩盖哪个是真值"}, - "silent-except":{en:"swallowed errors: empty catch / except:pass / ignored return codes", zh:"吞掉错误:空 catch / except:pass / 忽略返回码"}, - legacy:{en:"deprecated/back-compat shims, dead-but-shipped code, parallel old+new paths", zh:"遗留/兼容垫片、已发布的死代码、新旧并存"}, - "dual-format":{en:"accepting two shapes for one field (snake||camel), patched through the code", zh:"同一字段接受两种形态(snake||camel)并散落各处"}, - stub:{en:"NotImplemented / TODO / dead buttons / demo scripts presented as real", zh:"未实现/TODO/死按钮/演示脚本当成品"}, - "fake-output":{en:"returns random/canned/hardcoded results where real work is implied", zh:"本应真算的地方返回随机/写死结果"}, - duplication:{en:"copy-pasted logic, or an existing shared abstraction not reused", zh:"复制粘贴,或已有共享抽象却不复用"}, - bloat:{en:"oversized file / function; too many responsibilities in one unit", zh:"超大文件/函数;单元职责过多"}, - glue:{en:"valueless pass-through: rows of thin forwarders / no-op adapters", zh:"无价值透传:成片薄包装 / 零转换适配器"}, - "any-escape":{en:"bypassing the type system: as any / @ts-ignore / dynamic / void* / reinterpret_cast / unsafe", zh:"绕过类型系统:as any / @ts-ignore / dynamic / void* / reinterpret_cast / unsafe"}, - "over-fit":{en:"hardcoded to one case where a small generalization was expected", zh:"硬编码单一情况,本应小幅泛化"}, - "god-component":{en:"one component/class/file doing far too much", zh:"一个组件/类/文件做太多事"}, - placeholder:{en:"unfinished UI / data presented as if complete", zh:"未完成的 UI/数据当作已完成"}, -}; -function stdText(o){ return LANG==="zh" ? (o.zh||o.en) : o.en; } +const STD_KEY = "codemap.standard."+(META.project||"default"); +function loadStdDraft(){ try{ const s=localStorage.getItem(STD_KEY); return s?JSON.parse(s):null; }catch(e){ return null; } } +let STDDATA = loadStdDraft() || JSON.parse(JSON.stringify(DATA.standard || BUILTIN_STD)); +const TAGMETA = {}; +function rebuildTagMeta(){ + for(const k in TAGMETA) delete TAGMETA[k]; + (STDDATA.tags||[]).forEach(t=>TAGMETA[t.id]=t); + BAD_TAGS = new Set((STDDATA.tags||[]).filter(t=>t.bad!==false).map(t=>t.id)); +} +function tagMeta(id){ return TAGMETA[id] || {id,label:id,bad:true}; } +function tagLabel(t){ const m=tagMeta(t); return LANG==="zh" ? (m.labelZh||m.label||t) : (m.label||t); } +rebuildTagMeta(); /* problems POP (saturated red→amber), good RECEDES (pale low-sat green). The cue is saturation/lightness, not hue — colorblind-friendlier. */ @@ -414,8 +439,11 @@ function renderDetail(m,outs,ins){
${li(outs)}
${tl('usedBy')}
${li(ins)}
- `; + ${m.score!=null?``:""} + `; bindGo(); document.getElementById("clearBtn").addEventListener("click",clearSel); + const cf=document.getElementById("copyFixBtn"); + if(cf) cf.onclick=()=>{ copyText("/codemap fix "+m.id); toast(tl('copied')); }; } function bindGo(){detail.querySelectorAll("[data-go]").forEach(b=>b.addEventListener("click",()=>select(b.dataset.go)));} function bandTitle(b){return (BANDS.find(x=>x.id===b)||{}).t||b;} @@ -467,23 +495,69 @@ function renderReport(){ `; bindGo(); document.getElementById("toMapBtn").addEventListener("click",clearSel); } -function openStandard(){ - const rubric=STD.rubric.map(x=>`
${x.g}${x.r}${stdText(x)}
`).join(""); - const sev=STD.sev.map(x=>`
${x.k}${stdText(x)}
`).join(""); - const tags=STD.tags.map(t=>`
${tagLabel(t)}${stdText(TAG_DESC[t]||{en:t})}
`).join(""); - const coup=STD.coupling.map(x=>`
${x.k}${stdText(x)}
`).join(""); - const mo=document.getElementById("stdModal"); - mo.innerHTML=`
- -

${tl('stdTitle')}

${tl('stdIntro')}
+function toast(msg){ + let t=document.getElementById("toast"); + if(!t){ t=document.createElement("div"); t.id="toast"; t.className="toast"; document.body.appendChild(t); } + t.textContent=msg; t.classList.add("show"); + clearTimeout(t._h); t._h=setTimeout(()=>t.classList.remove("show"),2400); +} +function copyText(s){ + if(navigator.clipboard) { navigator.clipboard.writeText(s).catch(()=>{}); return; } + const ta=document.createElement("textarea"); ta.value=s; document.body.appendChild(ta); ta.select(); + try{ document.execCommand("copy"); }catch(e){} ta.remove(); +} +let STD_EDIT=false; +function openStandard(){ STD_EDIT=false; renderStd(); document.getElementById("stdModal").classList.add("open"); } +function closeStandard(){ document.getElementById("stdModal").classList.remove("open"); } +function saveStdDraft(){ try{ localStorage.setItem(STD_KEY, JSON.stringify(STDDATA)); }catch(e){} rebuildTagMeta(); } +function langKey(){ return LANG==="zh" ? "zh" : "en"; } +function renderStd(){ + const ed=STD_EDIT, ce=ed?' contenteditable="true" spellcheck="false"':''; + const rubric=STDDATA.rubric.map((x,i)=>`
${x.grade}${x.range}${stdText(x)}
`).join(""); + const sev=STDDATA.severities.map((x,i)=>`
${x.key}${stdText(x)}
`).join(""); + const tags=STDDATA.tags.map((x,i)=>`
${LANG==="zh"?(x.labelZh||x.label):x.label}${stdText(x)}${ed?``:''}
`).join(""); + const coup=STDDATA.coupling.map((x,i)=>`
${x.key}${stdText(x)}
`).join(""); + document.getElementById("stdModal").innerHTML=`
+
+ + ${ed?``:''} + +
+

${tl('stdTitle')}

${ed?tl('stdEditHint'):tl('stdIntro')}
${tl('stdRubric')}
${rubric}
${tl('stdSeverity')}
${sev} -
${tl('stdTags')}
${tags} +
${tl('stdTags')}${ed?` `:''}
${tags}
${tl('stdCoupling')}
${coup}
`; - mo.classList.add("open"); - document.getElementById("stdClose").addEventListener("click",closeStandard); + document.getElementById("stdClose").onclick=closeStandard; + document.getElementById("stdEdit").onclick=()=>{STD_EDIT=!STD_EDIT; renderStd();}; + if(ed){ + document.getElementById("stdExport").onclick=()=>{ + const blob=new Blob([JSON.stringify(STDDATA,null,1)],{type:"application/json"}); + const a=document.createElement("a"); a.href=URL.createObjectURL(blob); a.download="standard.json"; a.click(); + toast(tl('stdExported')); + }; + document.getElementById("stdReset").onclick=()=>{ + try{localStorage.removeItem(STD_KEY);}catch(e){} + STDDATA=JSON.parse(JSON.stringify(DATA.standard||BUILTIN_STD)); rebuildTagMeta(); renderStd(); + }; + document.getElementById("stdAddTag").onclick=()=>{ + const id=(prompt(tl('stdNewTagId'))||"").trim().toLowerCase().replace(/\s+/g,"-"); + if(!id || STDDATA.tags.some(t=>t.id===id)) return; + STDDATA.tags.push({id,label:id,labelZh:id,bad:true,en:"",zh:""}); saveStdDraft(); renderStd(); + }; + document.querySelectorAll("#stdModal [data-rm]").forEach(b=>b.onclick=()=>{STDDATA.tags.splice(+b.dataset.rm,1); saveStdDraft(); renderStd();}); + document.querySelectorAll("#stdModal [data-k]").forEach(el=>el.addEventListener("input",()=>{ + const i=+el.dataset.i, v=el.textContent, lk=langKey(); + const k=el.dataset.k; + if(k==="rubric") STDDATA.rubric[i][lk]=v; + else if(k==="sev") STDDATA.severities[i][lk]=v; + else if(k==="coup") STDDATA.coupling[i][lk]=v; + else if(k==="tag") STDDATA.tags[i][lk]=v; + else if(k==="taglabel") STDDATA.tags[i][LANG==="zh"?"labelZh":"label"]=v; + saveStdDraft(); + })); + } } -function closeStandard(){ document.getElementById("stdModal").classList.remove("open"); } function toggleHealth(){ const on=!board.classList.contains("show-health"); board.classList.toggle("show-health",on); diff --git a/reference/DATA_MODEL.md b/reference/DATA_MODEL.md index ae4c180..4dbc121 100644 --- a/reference/DATA_MODEL.md +++ b/reference/DATA_MODEL.md @@ -101,3 +101,11 @@ only stale + unaudited modules need a fresh subagent. `coupling` (low/med/high/core) is a **structural** property — how central the module is (degree + role). `score` is a **quality** property — how clean the code is. They are independent: a `core` module can be clean (A) and a `low` leaf can be broken (F). + +## Custom audit standard (`standard.json`) + +The scoring standard is data, not code: `reference/standard.json` holds the rubric, +severities, coupling levels, and the issue `tags` (each `{id,label,labelZh,bad,en,zh}`). +A project overrides it at `/.claude/codemap/modules.json`'s sibling +`standard.json`. The map renders + lets you edit it on the **Standard** page (Export to +save). Audits must use the project standard's tags when present — including custom ones. diff --git a/reference/standard.json b/reference/standard.json new file mode 100644 index 0000000..58e4567 --- /dev/null +++ b/reference/standard.json @@ -0,0 +1,37 @@ +{ + "rubric": [ + {"grade": "A", "score": 95, "range": "90–100", "en": "clean, well-scoped, idiomatic", "zh": "干净、职责单一、地道"}, + {"grade": "B", "score": 82, "range": "75–89", "en": "minor issues: a documented shim, mild bloat", "zh": "小问题:有文档的兼容、轻微臃肿"}, + {"grade": "C", "score": 67, "range": "60–74", "en": "notable hacks / fallbacks / bloat / duplication", "zh": "明显的 hack / 回退 / 臃肿 / 重复"}, + {"grade": "D", "score": 50, "range": "40–59", "en": "significant legacy / stubs / duplication, or a protocol violation", "zh": "严重的遗留 / 占位 / 重复,或协议违规"}, + {"grade": "F", "score": 25, "range": "0–39", "en": "broken, fake output, or unfinished-but-wired", "zh": "损坏、伪造输出,或未完成却已接线"} + ], + "severities": [ + {"key": "HIGH", "en": "wrong / dangerous / fake, a protocol or security issue, or a genuine maintenance hazard", "zh": "错误 / 危险 / 伪造、协议或安全问题,或真正的维护地雷"}, + {"key": "MED", "en": "a real smell a maintainer should fix", "zh": "维护者该修的真坏味"}, + {"key": "LOW", "en": "a documented shim, a cosmetic cast, benign bloat — worth noting, not urgent", "zh": "有文档的兼容、装饰性 cast、良性臃肿 —— 值得记、不紧急"} + ], + "coupling": [ + {"key": "core", "en": "system spine — central to almost everything", "zh": "系统主线 —— 几乎牵连一切"}, + {"key": "high", "en": "many connections", "zh": "连接很多"}, + {"key": "med", "en": "moderate", "zh": "中等"}, + {"key": "low", "en": "leaf / self-contained", "zh": "叶子 / 自洽"} + ], + "tags": [ + {"id": "monkeypatch", "label": "monkeypatch", "labelZh": "猴补丁", "bad": true, "en": "runtime mutation of another module / stdlib / vendor; reflection or prototype patching", "zh": "运行时改写别的模块/标准库/依赖;反射或原型补丁"}, + {"id": "fallback", "label": "fallback", "labelZh": "回退兜底", "bad": true, "en": "'try the real thing, then fake/degrade'; a||b||c chains that hide which value is real", "zh": "“先试真的再退化/造假”;a||b||c 掩盖哪个是真值"}, + {"id": "silent-except", "label": "silent-except", "labelZh": "静默吞错", "bad": true, "en": "swallowed errors: empty catch / except:pass / ignored return codes", "zh": "吞掉错误:空 catch / except:pass / 忽略返回码"}, + {"id": "legacy", "label": "legacy", "labelZh": "遗留", "bad": true, "en": "deprecated/back-compat shims, dead-but-shipped code, parallel old+new paths", "zh": "遗留/兼容垫片、已发布的死代码、新旧并存"}, + {"id": "dual-format", "label": "dual-format", "labelZh": "双格式", "bad": true, "en": "accepting two shapes for one field (snake||camel), patched through the code", "zh": "同一字段接受两种形态(snake||camel)并散落各处"}, + {"id": "stub", "label": "stub", "labelZh": "占位桩", "bad": true, "en": "NotImplemented / TODO / dead buttons / demo scripts presented as real", "zh": "未实现/TODO/死按钮/演示脚本当成品"}, + {"id": "fake-output", "label": "fake-output", "labelZh": "伪造输出", "bad": true, "en": "returns random/canned/hardcoded results where real work is implied", "zh": "本应真算的地方返回随机/写死结果"}, + {"id": "duplication", "label": "duplication", "labelZh": "重复", "bad": true, "en": "copy-pasted logic, or an existing shared abstraction not reused", "zh": "复制粘贴,或已有共享抽象却不复用"}, + {"id": "bloat", "label": "bloat", "labelZh": "臃肿", "bad": true, "en": "oversized file / function; too many responsibilities in one unit", "zh": "超大文件/函数;单元职责过多"}, + {"id": "glue", "label": "glue", "labelZh": "胶水", "bad": true, "en": "valueless pass-through: rows of thin forwarders / no-op adapters", "zh": "无价值透传:成片薄包装 / 零转换适配器"}, + {"id": "any-escape", "label": "any-escape", "labelZh": "类型逃逸", "bad": true, "en": "bypassing the type system: as any / @ts-ignore / dynamic / void* / reinterpret_cast / unsafe", "zh": "绕过类型系统:as any / @ts-ignore / dynamic / void* / reinterpret_cast / unsafe"}, + {"id": "over-fit", "label": "over-fit", "labelZh": "过度特化", "bad": true, "en": "hardcoded to one case where a small generalization was expected", "zh": "硬编码单一情况,本应小幅泛化"}, + {"id": "god-component", "label": "god-component", "labelZh": "上帝组件", "bad": true, "en": "one component/class/file doing far too much", "zh": "一个组件/类/文件做太多事"}, + {"id": "placeholder", "label": "placeholder", "labelZh": "占位", "bad": true, "en": "unfinished UI / data presented as if complete", "zh": "未完成的 UI/数据当作已完成"}, + {"id": "clean", "label": "clean", "labelZh": "干净", "bad": false, "en": "no material issues — well-scoped", "zh": "无实质问题 —— 职责单一"} + ] +} diff --git a/scripts/render.py b/scripts/render.py index 74bcaa2..cc7bff7 100644 --- a/scripts/render.py +++ b/scripts/render.py @@ -28,12 +28,30 @@ def band_order(state): return [b["id"] for b in state.get("bands", []) if not b.get("wire")] -def render_html(state, template): +def load_standard(state_path, explicit=None): + """Effective audit standard: explicit path → project override next to the state + file (`/standard.json`) → the skill's default `reference/standard.json`.""" + candidates = [] + if explicit: + candidates.append(explicit) + candidates.append(os.path.join(os.path.dirname(os.path.abspath(state_path)), "standard.json")) + candidates.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "reference", "standard.json")) + for c in candidates: + if c and os.path.isfile(c): + try: + return json.load(open(c, encoding="utf-8")) + except (ValueError, OSError): + pass + return None + + +def render_html(state, template, standard=None): data = { "meta": state.get("meta", {}), "bands": state.get("bands", []), "spine": state.get("spine", []), "reportThemes": state.get("reportThemes", []), + "standard": standard, "modules": [ {k: m.get(k) for k in ( "id", "label", "band", "path", "desc", "coupling", "deps", @@ -136,12 +154,14 @@ def main(): ap.add_argument("--template", required=True) ap.add_argument("--out-html", required=True) ap.add_argument("--out-md", required=True) + ap.add_argument("--standard", help="path to a custom standard.json (else project override → skill default)") args = ap.parse_args() state = json.load(open(args.state, encoding="utf-8")) template = open(args.template, encoding="utf-8").read() + standard = load_standard(args.state, args.standard) - open(args.out_html, "w", encoding="utf-8").write(render_html(state, template)) + open(args.out_html, "w", encoding="utf-8").write(render_html(state, template, standard)) open(args.out_md, "w", encoding="utf-8").write(render_md(state)) n = len(state.get("modules", [])) scored = sum(1 for m in state.get("modules", []) if m.get("score") is not None)