mirror of
https://github.com/Asixa/codemap-skill.git
synced 2026-08-28 22:01:26 +00:00
Add Standard page, multi-language excludes, Codex/AGENTS.md support
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# codemap — agent instructions
|
||||
|
||||
This repository **is the codemap tool**: it builds and incrementally maintains an
|
||||
interactive architecture map + per-module code-quality audit for any codebase. It is a
|
||||
Claude Code skill, but the engine is tool-agnostic — **Codex, Cursor, Aider, or any
|
||||
agent that can read instructions and spawn sub-tasks can drive it.**
|
||||
|
||||
## Read these first
|
||||
- **`SKILL.md`** — the full workflow and the `generate / check / update / test / fix`
|
||||
commands. Follow it exactly.
|
||||
- **`reference/STANDARDS.md`** — the fixed scoring rubric, smell taxonomy (with
|
||||
per-language equivalents), severity levels, and the **exact per-module audit prompt**.
|
||||
- **`reference/DATA_MODEL.md`** — the `modules.json` schema (the source of truth).
|
||||
|
||||
## Non-negotiable rules
|
||||
1. **`modules.json` is the source of truth.** The HTML and MD are generated by
|
||||
`scripts/render.py` — never hand-edit them.
|
||||
2. **Every module's score comes from a separate, independent sub-task** (one module per
|
||||
sub-task), using the prompt in `reference/STANDARDS.md`. Never score many modules in
|
||||
one pass; never reuse one module's score for another.
|
||||
3. For `fix`: keep the **four roles separate** — test-author, fixer, acceptance/verifier,
|
||||
auditor. A fix is accepted only when an independent acceptance step shows the pre-fix
|
||||
green tests are still green and the build is clean.
|
||||
|
||||
## The scripts (deterministic, Python 3 stdlib, no install)
|
||||
```
|
||||
python scripts/scan.py --root <proj> --state <state> [--write] # LoC + hash + staleness
|
||||
python scripts/query.py --state <state> --max-grade C --format ids # pick targets cheaply
|
||||
python scripts/apply_audit.py --state <state> --id <id> --json '<result>'
|
||||
python scripts/render.py --state <state> --template assets/template.html \
|
||||
--out-html <html> --out-md <md>
|
||||
```
|
||||
Use `query.py` (`--format ids|paths|findings`) to target work without reading the whole
|
||||
`modules.json`. Only scoring / fixing / test-writing need the model; everything else is
|
||||
a plain script call.
|
||||
@@ -31,12 +31,26 @@ The HTML and MD are **generated** from `modules.json` and must never be hand-edi
|
||||
recede to a muted green — colorblind-friendly, the cue is saturation not just hue).
|
||||
- **Filters**: by grade level (≤ B/C/D/F) and by issue tag; live match count.
|
||||
- **Audit report** view: averages, grade spread, worst offenders, cross-cutting themes.
|
||||
- **Standard page**: a built-in "Standard" view explaining the score→grade rubric, the
|
||||
finding severities, and every smell tag — so the scores are self-documenting.
|
||||
- **i18n**: set `meta.lang` to `"en"` or `"zh"` (module names are never translated).
|
||||
|
||||
## Languages
|
||||
|
||||
Language-agnostic. The scripts count LoC and hash bytes for **any** text source, and
|
||||
`paths` are plain globs, so it works for Python, **TypeScript/JS, Rust, C#/.NET, C/C++**,
|
||||
Go, Java, Swift, and more. Build/test/generated trees are excluded out of the box
|
||||
(`target/`, `bin/`, `obj/`, `node_modules/`, `cmake-build*`, `__pycache__/`, `dist/`,
|
||||
`*.d.ts`, `*.Designer.cs`, …). The audit rubric names *behaviors*, not syntax —
|
||||
`reference/STANDARDS.md` maps each smell to its per-language form (e.g. `any-escape` =
|
||||
`as any` / `dynamic` / `void*` / `reinterpret_cast` / `unsafe`).
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Python 3** (standard library only — no `pip install`, no external packages).
|
||||
- **Claude Code** (the skill orchestrates subagents for the audit/fix/test steps).
|
||||
- **An AI coding agent** to drive the audit/fix/test steps — **Claude Code** (native
|
||||
skill) or **any other agent that can read instructions and spawn sub-tasks**, e.g.
|
||||
OpenAI **Codex** (see [Using with Codex / other agents](#using-with-codex--other-agents)).
|
||||
- A browser to open the generated HTML. That's it.
|
||||
|
||||
## Install
|
||||
@@ -81,6 +95,28 @@ python3 $S/scripts/render.py --state .claude/codemap/modules.json \
|
||||
|
||||
> On Windows use `python` instead of `python3`.
|
||||
|
||||
## Using with Codex / other agents
|
||||
|
||||
The skill mechanism is Claude-specific, but the **engine is tool-agnostic**: the four
|
||||
scripts are deterministic stdlib Python, and the workflow + rubric are plain Markdown
|
||||
(`SKILL.md`, `reference/STANDARDS.md`). Any capable agent can drive it.
|
||||
|
||||
**OpenAI Codex** auto-reads an `AGENTS.md` in the working directory — this repo ships one
|
||||
that points Codex at the workflow and rubric. To use codemap from Codex (or Cursor,
|
||||
Aider, etc.):
|
||||
|
||||
1. Make the tool available — clone this repo somewhere the agent can read it, e.g.
|
||||
`git clone <url> ~/.codemap` (or vendor it into your project).
|
||||
2. Tell the agent: *"Use the codemap tool at `<path>` to build/update the architecture
|
||||
map for this project. Follow its `SKILL.md`; score each module with a separate
|
||||
sub-task using `reference/STANDARDS.md`."*
|
||||
3. The agent runs the same commands shown above (`scan.py` → per-module audit →
|
||||
`apply_audit.py` → `render.py`), using `query.py` to pick targets cheaply.
|
||||
|
||||
The deterministic parts (scan / query / render / apply_audit) you can also run **by
|
||||
hand** with no agent at all — only the *scoring*, *fixing*, and *test-writing* need a
|
||||
model, and those just follow `reference/STANDARDS.md`.
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
|
||||
@@ -126,6 +126,19 @@
|
||||
.grade-A{color:#76b39a;border-color:#3a5249} .grade-B{color:#b9c0c7;border-color:var(--border2)}
|
||||
.grade-C{color:#d9a441;border-color:#5a4720} .grade-D{color:#e08a4a;border-color:#5a3a20} .grade-F{color:#e0524b;border-color:#5a2420}
|
||||
.scrollnote{position:absolute;right:14px;bottom:12px;z-index:5;font-size:10.5px;color:var(--faint);font-family:var(--mono);pointer-events:none;background:#0e1012aa;padding:3px 8px;border-radius:6px;border:1px solid var(--border)}
|
||||
.modal{position:fixed;inset:0;z-index:60;display:none;background:#0a0b0ccc;overflow:auto;padding:46px 20px}
|
||||
.modal.open{display:block}
|
||||
.modal .sheet{position:relative;max-width:740px;margin:0 auto;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:24px 26px 30px}
|
||||
.modal .sheet h2{font-size:17px;font-weight:650;margin:0 0 3px;letter-spacing:.1px}
|
||||
.modal .sheet .intro{font-size:12.5px;color:var(--faint);margin:0 0 6px;line-height:1.5}
|
||||
.modal .xbtn{position:absolute;top:16px;right:16px}
|
||||
.std-h4{font-size:10.5px;text-transform:uppercase;letter-spacing:1px;color:var(--faint);margin:22px 0 4px}
|
||||
.std-row{display:flex;gap:13px;align-items:baseline;padding:8px 0;border-top:1px solid var(--border)}
|
||||
.std-badge{flex:0 0 56px;font-family:var(--mono);font-weight:700;font-size:12px;text-align:center;border-radius:6px;padding:3px 0;color:#0c0d0e}
|
||||
.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-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)}
|
||||
::-webkit-scrollbar-track{background:transparent}
|
||||
@@ -153,6 +166,7 @@
|
||||
</select>
|
||||
<select class="filt" id="tagFilter" title="Show only modules with this issue"><option value="">Any issue</option></select>
|
||||
<span class="fcount" id="fcount"></span>
|
||||
<button class="btn" id="stdBtn">Standard</button>
|
||||
<button class="btn" id="reportBtn">Audit report</button>
|
||||
<button class="btn" id="healthBtn">Color: coupling</button>
|
||||
<button class="btn" id="spineBtn">Data-flow spine</button>
|
||||
@@ -170,6 +184,7 @@
|
||||
</div>
|
||||
<aside id="aside"><div id="detail"></div></aside>
|
||||
</div>
|
||||
<div class="modal" id="stdModal"></div>
|
||||
|
||||
<script>
|
||||
const DATA = __ARCH_DATA__;
|
||||
@@ -195,6 +210,8 @@ 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.",
|
||||
stdRubric:"Score → grade", stdSeverity:"Finding severity", stdTags:"Issue tags", stdCoupling:"Coupling (structural, not quality)", close:"Close",
|
||||
trackedLoc:(l,f)=>`${l} tracked LoC · ${f} files` },
|
||||
zh:{ mapSuffix:"· 功能架构图", sub:"功能模块 · 调用层级 · 耦合 · 质量评分 · 代码行数",
|
||||
report:"报告", searchPh:"查找模块…", allGrades:"全部等级", anyIssue:"全部问题", scrollNote:"点击模块 · 滚动平移",
|
||||
@@ -208,6 +225,8 @@ const I18N = {
|
||||
worst:"最差模块 —— 点击查看", commonTags:"最常见问题标签", themes:"跨模块共性问题", backToMap:"← 返回地图", avg:"平均",
|
||||
filter:"筛选", modulesWord:"个模块", allWord:"全部", noMatches:"无匹配", clearFilters:"清除筛选",
|
||||
btnReport:"审计报告", btnSpine:"数据流主线", colorCoupling:"配色:耦合", colorHealth:"配色:健康度",
|
||||
btnStd:"评判标准", stdTitle:"评判标准", stdIntro:"模块如何打分 —— 所有模块、所有语言、每一次运行都用同一套标准。",
|
||||
stdRubric:"分数 → 等级", stdSeverity:"问题严重度", stdTags:"问题标签", stdCoupling:"耦合(结构维度,非质量)", close:"关闭",
|
||||
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); }
|
||||
@@ -219,6 +238,47 @@ const TAGS_ZH={ monkeypatch:"猴补丁", fallback:"回退兜底", legacy:"遗留
|
||||
"over-fit":"过度特化", "god-component":"上帝组件", placeholder:"占位", clean:"干净" };
|
||||
function tagLabel(t){ return LANG==="zh" ? (TAGS_ZH[t]||t) : t; }
|
||||
|
||||
/* ---------- the audit standard, shown on its own page (overlay) ---------- */
|
||||
const 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:"损坏、伪造输出,或未完成却已接线"},
|
||||
],
|
||||
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、良性臃肿 —— 值得记、不紧急"},
|
||||
],
|
||||
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:"叶子 / 自洽"},
|
||||
],
|
||||
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; }
|
||||
|
||||
/* problems POP (saturated red→amber), good RECEDES (pale low-sat green).
|
||||
The cue is saturation/lightness, not hue — colorblind-friendlier. */
|
||||
function healthColor(s){
|
||||
@@ -248,6 +308,7 @@ document.querySelector(".scrollnote").textContent = tl("scrollNote");
|
||||
document.getElementById("search").placeholder = tl("searchPh");
|
||||
document.getElementById("reportBtn").textContent = tl("btnReport");
|
||||
document.getElementById("spineBtn").textContent = tl("btnSpine");
|
||||
document.getElementById("stdBtn").textContent = tl("btnStd");
|
||||
document.getElementById("gradeFilter").options[0].textContent = tl("allGrades");
|
||||
document.getElementById("tagFilter").options[0].textContent = tl("anyIssue");
|
||||
|
||||
@@ -406,6 +467,23 @@ function renderReport(){
|
||||
<button class="btn clearbtn" id="toMapBtn">${tl('backToMap')}</button></div>`;
|
||||
bindGo(); document.getElementById("toMapBtn").addEventListener("click",clearSel);
|
||||
}
|
||||
function openStandard(){
|
||||
const rubric=STD.rubric.map(x=>`<div class="std-row"><span class="std-badge" style="background:${healthColor(x.s)}">${x.g}</span><span class="std-range">${x.r}</span><span class="std-desc">${stdText(x)}</span></div>`).join("");
|
||||
const sev=STD.sev.map(x=>`<div class="std-row"><span class="std-key"><span class="sev sev-${x.k}">${x.k}</span></span><span class="std-desc">${stdText(x)}</span></div>`).join("");
|
||||
const tags=STD.tags.map(t=>`<div class="std-row"><span class="std-key"><span class="tg bad">${tagLabel(t)}</span></span><span class="std-desc">${stdText(TAG_DESC[t]||{en:t})}</span></div>`).join("");
|
||||
const coup=STD.coupling.map(x=>`<div class="std-row"><span class="std-key" style="font-family:var(--mono);font-size:11.5px;color:var(--ink)">${x.k}</span><span class="std-desc">${stdText(x)}</span></div>`).join("");
|
||||
const mo=document.getElementById("stdModal");
|
||||
mo.innerHTML=`<div class="sheet">
|
||||
<button class="btn xbtn" id="stdClose">${tl('close')}</button>
|
||||
<h2>${tl('stdTitle')}</h2><div class="intro">${tl('stdIntro')}</div>
|
||||
<div class="std-h4">${tl('stdRubric')}</div>${rubric}
|
||||
<div class="std-h4">${tl('stdSeverity')}</div>${sev}
|
||||
<div class="std-h4">${tl('stdTags')}</div>${tags}
|
||||
<div class="std-h4">${tl('stdCoupling')}</div>${coup}</div>`;
|
||||
mo.classList.add("open");
|
||||
document.getElementById("stdClose").addEventListener("click",closeStandard);
|
||||
}
|
||||
function closeStandard(){ document.getElementById("stdModal").classList.remove("open"); }
|
||||
function toggleHealth(){
|
||||
const on=!board.classList.contains("show-health");
|
||||
board.classList.toggle("show-health",on);
|
||||
@@ -462,6 +540,9 @@ document.getElementById("tagFilter").addEventListener("change",e=>{filterTag=e.t
|
||||
document.getElementById("healthBtn").addEventListener("click",toggleHealth);
|
||||
document.getElementById("reportBtn").addEventListener("click",renderReport);
|
||||
document.getElementById("spineBtn").addEventListener("click",()=>{spineOn?clearSel():showSpine();});
|
||||
document.getElementById("stdBtn").addEventListener("click",openStandard);
|
||||
document.getElementById("stdModal").addEventListener("click",e=>{ if(e.target.id==="stdModal") closeStandard(); });
|
||||
window.addEventListener("keydown",e=>{ if(e.key==="Escape") closeStandard(); });
|
||||
board.addEventListener("click",()=>{filtersActive()?runFilter():clearSel();});
|
||||
let rt; window.addEventListener("resize",()=>{clearTimeout(rt);rt=setTimeout(()=>{const c=current;if(c){current=null;select(c);}else if(spineOn)showSpine();},120);});
|
||||
board.classList.add("show-health");
|
||||
|
||||
@@ -47,6 +47,23 @@ Use these exact tag strings. `clean` is the only positive tag; the rest are nega
|
||||
- `over-fit` — hardcoded to one case where a small generalization was expected.
|
||||
- `clean` — no material issues.
|
||||
|
||||
### The taxonomy is language-agnostic — recognize the per-language form
|
||||
|
||||
The tags name *behaviors*, not syntax. Map each to whatever the target language does:
|
||||
|
||||
| tag | Python | TS / JS | C# / .NET | Rust | C / C++ |
|
||||
|---|---|---|---|---|---|
|
||||
| `any-escape` | `# type: ignore`, `Any` | `as any`, `@ts-ignore`, `!` | `dynamic`, `object` casts, `#nullable disable` | `unsafe`, `transmute`, blanket `.unwrap()` | `void*`, `reinterpret_cast`, C-style casts |
|
||||
| `silent-except` | `except: pass` | empty `catch {}` | `catch (Exception) {}` | `let _ = x;`, `.ok()`, `unwrap_or_default` to hide | empty `catch`, ignored return codes / `errno` |
|
||||
| `monkeypatch` | `setattr`, `sys.modules` | prototype patching, global override | reflection / Harmony patching | macro / `static mut` hacks | `#define` overrides, weak-symbol swap |
|
||||
| `dual-format` | `a or b` (snake/camel) | `a ?? b`, `a \|\| b` | nullable + alias props | `Option` chains for two shapes | overloads accepting two layouts |
|
||||
| `fallback` | try real then stub | `try/catch` → canned data | `try/catch` fallback | `unwrap_or(fakeDefault)` | `#ifdef` to fake impl |
|
||||
|
||||
`legacy`, `stub`, `fake-output`, `bloat`, `god-component`, `duplication`, `glue`,
|
||||
`over-fit` are the same idea in every language. Build / test / generated files are out of
|
||||
audit scope — the module `paths` globs plus `scan.py` excludes handle that across stacks
|
||||
(`target/`, `bin/`, `obj/`, `node_modules/`, `__pycache__/`, `dist/`, …).
|
||||
|
||||
Judgement rules:
|
||||
- A *documented, bounded* compat shim that deliberately refuses to silently coerce is
|
||||
`legacy` at most LOW — do not over-penalize disciplined shims.
|
||||
|
||||
+12
-5
@@ -21,12 +21,19 @@ Stdlib only.
|
||||
import argparse, glob, hashlib, json, os, sys
|
||||
|
||||
DEFAULT_EXCLUDES = [
|
||||
"__pycache__", "/node_modules/", "/dist/", "/build/", "/.git/",
|
||||
"/vendor/", ".min.js", ".min.css", "/.venv/", "/venv/",
|
||||
".pytest", "/coverage/", ".map",
|
||||
# vcs / editor
|
||||
"/.git/", "/.svn/", "/.hg/", "/.idea/", "/.vs/",
|
||||
# build / output dirs (py, js/ts, rust, c#/.net, c/c++/cmake, jvm, swift, next/nuxt)
|
||||
"__pycache__", "/node_modules/", "/dist/", "/build/", "/out/", "/target/",
|
||||
"/bin/", "/obj/", "/cmake-build", "/.gradle/", "/pods/", "/.next/", "/.nuxt/",
|
||||
# deps / vendored / generated
|
||||
"/vendor/", "/third_party/", "/external/", "/.venv/", "/venv/", "/coverage/",
|
||||
".min.js", ".min.css", ".map", ".pytest", ".d.ts",
|
||||
".designer.cs", ".g.cs", ".generated.", ".pb.go", "_pb2.py",
|
||||
# tests are the regression net, not part of a module's audit scope:
|
||||
"/tests/", "/test/", "/__tests__/", ".test.", ".spec.", "_test.py",
|
||||
"conftest.py", ".stories.",
|
||||
"/tests/", "/test/", "/__tests__/", "/spec/", ".test.", ".spec.",
|
||||
"_test.py", "_test.go", "_test.rs", "conftest.py", ".stories.",
|
||||
".tests/", "tests.cs",
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user