@tw93/waza 3.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/package.json +35 -0
- package/rules/anti-patterns.md +38 -0
- package/rules/chinese.md +18 -0
- package/rules/durable-context.md +27 -0
- package/rules/english.md +14 -0
- package/scripts/build_metadata.py +360 -0
- package/scripts/check_routing_drift.py +82 -0
- package/scripts/dispatcher-template.md +43 -0
- package/scripts/dispatcher.md +53 -0
- package/scripts/package-skill.sh +71 -0
- package/scripts/packaging_filter.py +55 -0
- package/scripts/setup-rule.sh +109 -0
- package/scripts/setup-statusline.sh +127 -0
- package/scripts/skill_checks.py +483 -0
- package/scripts/skill_frontmatter.py +110 -0
- package/scripts/statusline.sh +321 -0
- package/scripts/validate_package.py +66 -0
- package/scripts/verify_skills.py +100 -0
- package/skills/RESOLVER.md +91 -0
- package/skills/check/SKILL.md +338 -0
- package/skills/check/agents/reviewer-architecture.md +39 -0
- package/skills/check/agents/reviewer-security.md +39 -0
- package/skills/check/references/persona-catalog.md +56 -0
- package/skills/check/references/project-context.md +107 -0
- package/skills/check/references/public-reply.md +14 -0
- package/skills/check/scripts/audit_signals.py +485 -0
- package/skills/check/scripts/run-tests.sh +19 -0
- package/skills/design/SKILL.md +134 -0
- package/skills/design/references/design-aesthetic-quality.md +67 -0
- package/skills/design/references/design-data-viz.md +34 -0
- package/skills/design/references/design-reference.md +278 -0
- package/skills/design/references/design-tokens.md +53 -0
- package/skills/design/references/design-traps.md +43 -0
- package/skills/health/SKILL.md +231 -0
- package/skills/health/agents/inspector-context.md +119 -0
- package/skills/health/agents/inspector-control.md +84 -0
- package/skills/health/agents/inspector-maintainability.md +55 -0
- package/skills/health/scripts/check-agent-context.sh +5 -0
- package/skills/health/scripts/check-doc-refs.sh +8 -0
- package/skills/health/scripts/check-maintainability.sh +8 -0
- package/skills/health/scripts/check-verifier-output.sh +5 -0
- package/skills/health/scripts/check_agent_context.py +407 -0
- package/skills/health/scripts/check_doc_refs.py +110 -0
- package/skills/health/scripts/check_maintainability.py +629 -0
- package/skills/health/scripts/check_verifier_output.py +116 -0
- package/skills/health/scripts/collect-data.sh +760 -0
- package/skills/hunt/SKILL.md +197 -0
- package/skills/hunt/references/failure-patterns.md +75 -0
- package/skills/hunt/references/ime-unicode.md +58 -0
- package/skills/hunt/references/logging-techniques.md +72 -0
- package/skills/hunt/references/rendering-debug.md +34 -0
- package/skills/learn/SKILL.md +128 -0
- package/skills/read/SKILL.md +108 -0
- package/skills/read/references/read-methods.md +110 -0
- package/skills/read/references/save-paths.md +33 -0
- package/skills/read/scripts/fetch.sh +105 -0
- package/skills/read/scripts/fetch_feishu.py +246 -0
- package/skills/read/scripts/fetch_local.py +218 -0
- package/skills/read/scripts/fetch_weixin.py +107 -0
- package/skills/think/SKILL.md +155 -0
- package/skills/write/SKILL.md +129 -0
- package/skills/write/references/write-en.md +197 -0
- package/skills/write/references/write-zh-bilingual.md +60 -0
- package/skills/write/references/write-zh-prose.md +48 -0
- package/skills/write/references/write-zh-release-notes.md +38 -0
- package/skills/write/references/write-zh.md +645 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Project audit signals (Phase 1) for /check audit mode.
|
|
3
|
+
|
|
4
|
+
Walks a project root and emits 10 structured signal blocks to stdout.
|
|
5
|
+
Each block ends with `status: PASS|WARN|FAIL` so the LLM driving the
|
|
6
|
+
4-axis Linus-style scorecard can skim quickly.
|
|
7
|
+
|
|
8
|
+
Pure stdlib. Read-only. Exits 0 even on WARN/FAIL so the harness does
|
|
9
|
+
not confuse "finding surfaced" with "script broken".
|
|
10
|
+
|
|
11
|
+
Run as: python3 skills/check/scripts/audit_signals.py --root <path>
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
EXCLUDED_DIRS = {
|
|
25
|
+
".git", ".hg", ".svn", "node_modules", "dist", "build", ".next",
|
|
26
|
+
"__pycache__", ".turbo", "target", ".venv", "venv", "vendor",
|
|
27
|
+
"coverage", ".cache", ".parcel-cache", ".pytest_cache", ".mypy_cache",
|
|
28
|
+
".ruff_cache", "Pods", "Carthage", ".swiftpm", ".gradle",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
SOURCE_EXTS = {
|
|
32
|
+
".py", ".swift", ".rs", ".go", ".ts", ".tsx", ".js", ".jsx", ".sh",
|
|
33
|
+
".bash", ".zsh", ".rb", ".java", ".kt", ".m", ".mm", ".vue", ".c",
|
|
34
|
+
".cc", ".cpp", ".h", ".hpp", ".cs",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
HOTSPOT_LINES = 500
|
|
38
|
+
HOTSPOT_FAIL = 1500
|
|
39
|
+
HEREDOC_LINES = 100
|
|
40
|
+
DRIFT_WARN = 50
|
|
41
|
+
DRIFT_FAIL = 150
|
|
42
|
+
DUP_JACCARD = 0.70
|
|
43
|
+
|
|
44
|
+
MARKER_RE = re.compile(r"\b(TODO|FIXME|HACK|XXX)\b")
|
|
45
|
+
HEREDOC_OPEN_RE = re.compile(
|
|
46
|
+
r"(python3?|node|ruby|perl|php)\b[^|\n]*?<<-?\s*['\"]?(\w+)['\"]?"
|
|
47
|
+
)
|
|
48
|
+
INSTALL_URL_RE = re.compile(
|
|
49
|
+
r"raw\.githubusercontent\.com/[^/\s]+/[^/\s]+/([^/\s]+)/"
|
|
50
|
+
)
|
|
51
|
+
# --exclude requires = or trailing value to avoid matching git's --exclude-standard
|
|
52
|
+
DENYLIST_HINT_RE = re.compile(
|
|
53
|
+
r"(^\s*(skip|exclude)\s*=|\s--exclude=|!\*\.\w+|grep\s+-v\b|--ignore=)",
|
|
54
|
+
re.IGNORECASE,
|
|
55
|
+
)
|
|
56
|
+
MINIFIED_RE = re.compile(r"\.min\.[a-z]+$", re.IGNORECASE)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_excluded(path: Path, root: Path) -> bool:
|
|
60
|
+
try:
|
|
61
|
+
parts = path.relative_to(root).parts
|
|
62
|
+
except ValueError:
|
|
63
|
+
parts = path.parts
|
|
64
|
+
if any(p in EXCLUDED_DIRS for p in parts):
|
|
65
|
+
return True
|
|
66
|
+
return bool(MINIFIED_RE.search(path.name))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def iter_files(root: Path) -> list[Path]:
|
|
70
|
+
try:
|
|
71
|
+
proc = subprocess.run(
|
|
72
|
+
["git", "-C", str(root), "ls-files",
|
|
73
|
+
"--cached", "--others", "--exclude-standard"],
|
|
74
|
+
text=True, stdout=subprocess.PIPE,
|
|
75
|
+
stderr=subprocess.DEVNULL, check=False,
|
|
76
|
+
)
|
|
77
|
+
if proc.returncode == 0 and proc.stdout.strip():
|
|
78
|
+
out = []
|
|
79
|
+
for line in proc.stdout.splitlines():
|
|
80
|
+
p = root / line
|
|
81
|
+
if p.is_file() and not is_excluded(p, root):
|
|
82
|
+
out.append(p)
|
|
83
|
+
return out
|
|
84
|
+
except OSError:
|
|
85
|
+
pass
|
|
86
|
+
out = []
|
|
87
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
88
|
+
current = Path(dirpath)
|
|
89
|
+
dirnames[:] = [d for d in dirnames if d not in EXCLUDED_DIRS]
|
|
90
|
+
if is_excluded(current, root):
|
|
91
|
+
continue
|
|
92
|
+
for fname in filenames:
|
|
93
|
+
p = current / fname
|
|
94
|
+
if p.is_file() and not is_excluded(p, root):
|
|
95
|
+
out.append(p)
|
|
96
|
+
return out
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def line_count(path: Path) -> int:
|
|
100
|
+
try:
|
|
101
|
+
with path.open("rb") as fh:
|
|
102
|
+
return sum(1 for _ in fh)
|
|
103
|
+
except OSError:
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def read_text(path: Path, limit: int = 0) -> str:
|
|
108
|
+
try:
|
|
109
|
+
data = path.read_text(encoding="utf-8", errors="replace")
|
|
110
|
+
except OSError:
|
|
111
|
+
return ""
|
|
112
|
+
return data[:limit] if limit else data
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def rel(path: Path, root: Path) -> str:
|
|
116
|
+
try:
|
|
117
|
+
return path.relative_to(root).as_posix()
|
|
118
|
+
except ValueError:
|
|
119
|
+
return path.as_posix()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def header(name: str) -> None:
|
|
123
|
+
print(f"=== {name} ===")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def status(label: str) -> None:
|
|
127
|
+
print(f"status: {label}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def block_hotspots(files: list[Path], root: Path) -> None:
|
|
131
|
+
header("FILE SIZE HOTSPOTS")
|
|
132
|
+
big = sorted(
|
|
133
|
+
((p, line_count(p)) for p in files
|
|
134
|
+
if p.suffix in SOURCE_EXTS and line_count(p) >= HOTSPOT_LINES),
|
|
135
|
+
key=lambda x: -x[1],
|
|
136
|
+
)[:10]
|
|
137
|
+
if not big:
|
|
138
|
+
print("(no source files >= 800 lines)")
|
|
139
|
+
status("PASS")
|
|
140
|
+
return
|
|
141
|
+
for path, n in big:
|
|
142
|
+
print(f" {n:>5} {rel(path, root)}")
|
|
143
|
+
if any(n >= HOTSPOT_FAIL for _, n in big):
|
|
144
|
+
status("FAIL")
|
|
145
|
+
elif len(big) > 3:
|
|
146
|
+
status("WARN")
|
|
147
|
+
else:
|
|
148
|
+
status("WARN")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def block_heredoc(files: list[Path], root: Path) -> None:
|
|
152
|
+
header("HEREDOC BLOAT")
|
|
153
|
+
hits: list[tuple[str, int, str, int]] = []
|
|
154
|
+
for path in files:
|
|
155
|
+
if path.suffix not in {".sh", ".bash", ".zsh"}:
|
|
156
|
+
continue
|
|
157
|
+
text = read_text(path)
|
|
158
|
+
if not text:
|
|
159
|
+
continue
|
|
160
|
+
lines = text.splitlines()
|
|
161
|
+
i = 0
|
|
162
|
+
while i < len(lines):
|
|
163
|
+
m = HEREDOC_OPEN_RE.search(lines[i])
|
|
164
|
+
if not m:
|
|
165
|
+
i += 1
|
|
166
|
+
continue
|
|
167
|
+
lang, marker = m.group(1), m.group(2)
|
|
168
|
+
j = i + 1
|
|
169
|
+
close = re.compile(r"^\s*" + re.escape(marker) + r"\s*$")
|
|
170
|
+
while j < len(lines) and not close.match(lines[j]):
|
|
171
|
+
j += 1
|
|
172
|
+
size = j - i
|
|
173
|
+
if size >= HEREDOC_LINES:
|
|
174
|
+
hits.append((rel(path, root), i + 1, lang, size))
|
|
175
|
+
i = j + 1
|
|
176
|
+
if not hits:
|
|
177
|
+
print("(no python/node/ruby/perl/php heredocs >= 100 lines)")
|
|
178
|
+
status("PASS")
|
|
179
|
+
return
|
|
180
|
+
for f, ln, lang, sz in hits:
|
|
181
|
+
print(f" {f}:{ln} lang={lang} block_lines={sz}")
|
|
182
|
+
status("WARN")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def block_test_ci(files: list[Path], root: Path) -> None:
|
|
186
|
+
header("TEST AND CI SURFACE")
|
|
187
|
+
test_files = [
|
|
188
|
+
p for p in files
|
|
189
|
+
if p.suffix in SOURCE_EXTS
|
|
190
|
+
and (("test" in p.name.lower()) or ("spec" in p.name.lower()))
|
|
191
|
+
]
|
|
192
|
+
src_files = [p for p in files if p.suffix in SOURCE_EXTS]
|
|
193
|
+
wf_dir = root / ".github" / "workflows"
|
|
194
|
+
workflows = []
|
|
195
|
+
if wf_dir.is_dir():
|
|
196
|
+
workflows = sorted(list(wf_dir.glob("*.yml")) + list(wf_dir.glob("*.yaml")))
|
|
197
|
+
job_names: list[str] = []
|
|
198
|
+
for wf in workflows:
|
|
199
|
+
text = read_text(wf, 50_000)
|
|
200
|
+
for m in re.finditer(r"^name:\s*(.+?)\s*$", text, re.MULTILINE):
|
|
201
|
+
job_names.append(f"{wf.name}: {m.group(1)[:60]}")
|
|
202
|
+
break
|
|
203
|
+
ratio = len(test_files) / max(len(src_files), 1)
|
|
204
|
+
print(f"tests_count={len(test_files)} source_count={len(src_files)} "
|
|
205
|
+
f"ratio={ratio:.1%}")
|
|
206
|
+
print(f"ci_workflow_files={len(workflows)}")
|
|
207
|
+
for j in job_names[:10]:
|
|
208
|
+
print(f" workflow: {j}")
|
|
209
|
+
if not test_files and not workflows:
|
|
210
|
+
status("FAIL")
|
|
211
|
+
elif not test_files or not workflows:
|
|
212
|
+
status("WARN")
|
|
213
|
+
else:
|
|
214
|
+
status("PASS")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _grep_version(path: Path, pattern: str) -> str | None:
|
|
218
|
+
text = read_text(path, 20_000)
|
|
219
|
+
if not text:
|
|
220
|
+
return None
|
|
221
|
+
m = re.search(pattern, text, re.MULTILINE)
|
|
222
|
+
return m.group(1).strip() if m else None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def block_version_sources(root: Path) -> None:
|
|
226
|
+
header("VERSION SOURCE COUNT")
|
|
227
|
+
found: list[tuple[str, str]] = []
|
|
228
|
+
v = root / "VERSION"
|
|
229
|
+
if v.is_file():
|
|
230
|
+
first = read_text(v).strip().splitlines()
|
|
231
|
+
if first:
|
|
232
|
+
found.append(("VERSION", first[0]))
|
|
233
|
+
probes = [
|
|
234
|
+
("package.json", r'"version"\s*:\s*"([^"]+)"'),
|
|
235
|
+
("Cargo.toml", r'^\s*version\s*=\s*"([^"]+)"'),
|
|
236
|
+
("pyproject.toml", r'^\s*version\s*=\s*"([^"]+)"'),
|
|
237
|
+
("setup.py", r"version\s*=\s*['\"]([^'\"]+)['\"]"),
|
|
238
|
+
]
|
|
239
|
+
for fname, pat in probes:
|
|
240
|
+
p = root / fname
|
|
241
|
+
if p.is_file():
|
|
242
|
+
v_str = _grep_version(p, pat)
|
|
243
|
+
if v_str:
|
|
244
|
+
found.append((fname, v_str))
|
|
245
|
+
for pat in ("*.podspec", "*.csproj"):
|
|
246
|
+
for path in root.glob(pat):
|
|
247
|
+
v_str = _grep_version(
|
|
248
|
+
path, r'(?i)version\s*[:=]\s*["\']?(\d+\.\d+\.\d+[\w.-]*)'
|
|
249
|
+
)
|
|
250
|
+
if v_str:
|
|
251
|
+
found.append((path.name, v_str))
|
|
252
|
+
for path in list(root.glob("build.gradle*")):
|
|
253
|
+
v_str = _grep_version(
|
|
254
|
+
path, r'(?i)version\s*[:=]\s*["\']?(\d+\.\d+\.\d+[\w.-]*)'
|
|
255
|
+
)
|
|
256
|
+
if v_str:
|
|
257
|
+
found.append((path.name, v_str))
|
|
258
|
+
if not found:
|
|
259
|
+
print("(no declared version source found)")
|
|
260
|
+
status("PASS")
|
|
261
|
+
return
|
|
262
|
+
for f, val in found:
|
|
263
|
+
print(f" {f}: {val}")
|
|
264
|
+
distinct = {val for _, val in found if val}
|
|
265
|
+
print(f"sources={len(found)} distinct_values={len(distinct)}")
|
|
266
|
+
if len(found) > 1 and len(distinct) > 1:
|
|
267
|
+
status("WARN")
|
|
268
|
+
else:
|
|
269
|
+
status("PASS")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def block_packaging_posture(root: Path) -> None:
|
|
273
|
+
header("PACKAGING FILTER POSTURE")
|
|
274
|
+
allowlist_files = list(root.glob("*.allowlist")) + list(root.glob("MANIFEST.in"))
|
|
275
|
+
pkg_scripts = (list(root.glob("scripts/package*.sh"))
|
|
276
|
+
+ list(root.glob("scripts/release*.sh")))
|
|
277
|
+
denylist_hits = 0
|
|
278
|
+
for sp in pkg_scripts:
|
|
279
|
+
for line in read_text(sp).splitlines():
|
|
280
|
+
if DENYLIST_HINT_RE.search(line):
|
|
281
|
+
denylist_hits += 1
|
|
282
|
+
if allowlist_files:
|
|
283
|
+
for f in allowlist_files:
|
|
284
|
+
print(f" allowlist: {rel(f, root)}")
|
|
285
|
+
print(f"posture=allowlist denylist_hits_in_scripts={denylist_hits}")
|
|
286
|
+
status("PASS")
|
|
287
|
+
return
|
|
288
|
+
if denylist_hits:
|
|
289
|
+
for sp in pkg_scripts:
|
|
290
|
+
print(f" script: {rel(sp, root)}")
|
|
291
|
+
print(f"posture=denylist denylist_hits_in_scripts={denylist_hits}")
|
|
292
|
+
status("WARN")
|
|
293
|
+
return
|
|
294
|
+
print("posture=none (no packaging scripts)")
|
|
295
|
+
status("N/A")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def block_install_url(root: Path) -> None:
|
|
299
|
+
header("INSTALL URL PINNING")
|
|
300
|
+
targets: list[Path] = [root / "README.md"]
|
|
301
|
+
targets += list(root.glob("scripts/setup*.sh"))
|
|
302
|
+
targets += list(root.glob("scripts/install*.sh"))
|
|
303
|
+
findings: list[tuple[str, int, str]] = []
|
|
304
|
+
for path in targets:
|
|
305
|
+
if not path.is_file():
|
|
306
|
+
continue
|
|
307
|
+
text = read_text(path, 200_000)
|
|
308
|
+
for i, line in enumerate(text.splitlines(), start=1):
|
|
309
|
+
for m in INSTALL_URL_RE.finditer(line):
|
|
310
|
+
findings.append((rel(path, root), i, m.group(1)))
|
|
311
|
+
if not findings:
|
|
312
|
+
print("(no raw.githubusercontent.com refs found)")
|
|
313
|
+
status("PASS")
|
|
314
|
+
return
|
|
315
|
+
moving = [f for f in findings if f[2] in ("main", "master", "HEAD")]
|
|
316
|
+
for f, ln, ref in findings[:20]:
|
|
317
|
+
marker = " [MOVING]" if ref in ("main", "master", "HEAD") else ""
|
|
318
|
+
print(f" {f}:{ln} ref={ref}{marker}")
|
|
319
|
+
print(f"total={len(findings)} moving={len(moving)}")
|
|
320
|
+
if moving:
|
|
321
|
+
status("WARN")
|
|
322
|
+
else:
|
|
323
|
+
status("PASS")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def block_agent_doc_dedup(root: Path) -> None:
|
|
327
|
+
header("AGENT DOC DEDUP")
|
|
328
|
+
claude = root / "CLAUDE.md"
|
|
329
|
+
agents = root / "AGENTS.md"
|
|
330
|
+
have_c = claude.exists() or claude.is_symlink()
|
|
331
|
+
have_a = agents.exists() or agents.is_symlink()
|
|
332
|
+
if not have_c and not have_a:
|
|
333
|
+
print("posture=none")
|
|
334
|
+
status("PASS")
|
|
335
|
+
return
|
|
336
|
+
if not (have_c and have_a):
|
|
337
|
+
print(f"posture=single-file ({'CLAUDE.md' if have_c else 'AGENTS.md'} only)")
|
|
338
|
+
status("PASS")
|
|
339
|
+
return
|
|
340
|
+
if claude.is_symlink() and claude.resolve(strict=False).name == "AGENTS.md":
|
|
341
|
+
print("posture=symlink (CLAUDE.md -> AGENTS.md)")
|
|
342
|
+
status("PASS")
|
|
343
|
+
return
|
|
344
|
+
if agents.is_symlink() and agents.resolve(strict=False).name == "CLAUDE.md":
|
|
345
|
+
print("posture=symlink (AGENTS.md -> CLAUDE.md)")
|
|
346
|
+
status("PASS")
|
|
347
|
+
return
|
|
348
|
+
a = read_text(claude)
|
|
349
|
+
b = read_text(agents)
|
|
350
|
+
if a and a == b:
|
|
351
|
+
print("posture=identical (consider symlink to dedup)")
|
|
352
|
+
status("WARN")
|
|
353
|
+
return
|
|
354
|
+
cross = ("AGENTS.md" in a) or ("CLAUDE.md" in b)
|
|
355
|
+
a_set = {ln.strip() for ln in a.splitlines()
|
|
356
|
+
if ln.strip() and not ln.strip().startswith("#")}
|
|
357
|
+
b_set = {ln.strip() for ln in b.splitlines()
|
|
358
|
+
if ln.strip() and not ln.strip().startswith("#")}
|
|
359
|
+
union = a_set | b_set
|
|
360
|
+
jaccard = len(a_set & b_set) / len(union) if union else 0.0
|
|
361
|
+
print(f"jaccard={jaccard:.2f} cross_refs={cross}")
|
|
362
|
+
if jaccard >= 0.20:
|
|
363
|
+
print("posture=divergent-overlap (drift risk; consider symlink)")
|
|
364
|
+
status("WARN")
|
|
365
|
+
return
|
|
366
|
+
if cross:
|
|
367
|
+
print("posture=cross-ref (one references the other)")
|
|
368
|
+
status("WARN")
|
|
369
|
+
return
|
|
370
|
+
print("posture=independent")
|
|
371
|
+
status("PASS")
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def block_drift_markers(files: list[Path], root: Path) -> None:
|
|
375
|
+
header("DRIFT MARKERS")
|
|
376
|
+
counts: list[tuple[str, int]] = []
|
|
377
|
+
total = 0
|
|
378
|
+
for path in files:
|
|
379
|
+
if path.suffix not in SOURCE_EXTS:
|
|
380
|
+
continue
|
|
381
|
+
text = read_text(path, 200_000)
|
|
382
|
+
if not text:
|
|
383
|
+
continue
|
|
384
|
+
n = len(MARKER_RE.findall(text))
|
|
385
|
+
if n:
|
|
386
|
+
counts.append((rel(path, root), n))
|
|
387
|
+
total += n
|
|
388
|
+
counts.sort(key=lambda x: -x[1])
|
|
389
|
+
for f, n in counts[:5]:
|
|
390
|
+
print(f" {n:>4} {f}")
|
|
391
|
+
print(f"total={total}")
|
|
392
|
+
if total >= DRIFT_FAIL:
|
|
393
|
+
status("FAIL")
|
|
394
|
+
elif total >= DRIFT_WARN:
|
|
395
|
+
status("WARN")
|
|
396
|
+
else:
|
|
397
|
+
status("PASS")
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def block_duplicate_setup(root: Path) -> None:
|
|
401
|
+
header("DUPLICATE SETUP SCRIPTS")
|
|
402
|
+
scripts = (list(root.glob("scripts/setup-*.sh"))
|
|
403
|
+
+ list(root.glob("scripts/install-*.sh")))
|
|
404
|
+
if len(scripts) < 2:
|
|
405
|
+
print("(fewer than 2 setup-* scripts to compare)")
|
|
406
|
+
status("N/A")
|
|
407
|
+
return
|
|
408
|
+
sets: dict[Path, set[str]] = {}
|
|
409
|
+
for sp in scripts:
|
|
410
|
+
sets[sp] = {ln.strip() for ln in read_text(sp).splitlines()
|
|
411
|
+
if ln.strip() and not ln.strip().startswith("#")}
|
|
412
|
+
pairs: list[tuple[str, str, float]] = []
|
|
413
|
+
names = list(sets.keys())
|
|
414
|
+
for i, a in enumerate(names):
|
|
415
|
+
for b in names[i + 1:]:
|
|
416
|
+
union = sets[a] | sets[b]
|
|
417
|
+
if not union:
|
|
418
|
+
continue
|
|
419
|
+
j = len(sets[a] & sets[b]) / len(union)
|
|
420
|
+
if j >= DUP_JACCARD:
|
|
421
|
+
pairs.append((rel(a, root), rel(b, root), j))
|
|
422
|
+
if not pairs:
|
|
423
|
+
print("(no setup pairs with jaccard >= 0.70)")
|
|
424
|
+
status("PASS")
|
|
425
|
+
return
|
|
426
|
+
for a, b, j in pairs:
|
|
427
|
+
print(f" {a} vs {b} jaccard={j:.2f}")
|
|
428
|
+
status("WARN")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def block_denylist_in_build(root: Path) -> None:
|
|
432
|
+
header("DENYLIST IN BUILD")
|
|
433
|
+
targets = (list(root.glob("scripts/package*.sh"))
|
|
434
|
+
+ list(root.glob("scripts/release*.sh"))
|
|
435
|
+
+ [root / "Makefile", root / "Justfile"])
|
|
436
|
+
real_targets = [p for p in targets if p.is_file()]
|
|
437
|
+
if not real_targets:
|
|
438
|
+
print("(no build scripts present)")
|
|
439
|
+
status("N/A")
|
|
440
|
+
return
|
|
441
|
+
hits: list[tuple[str, int, str]] = []
|
|
442
|
+
for path in real_targets:
|
|
443
|
+
text = read_text(path, 100_000)
|
|
444
|
+
for i, line in enumerate(text.splitlines(), start=1):
|
|
445
|
+
if DENYLIST_HINT_RE.search(line):
|
|
446
|
+
hits.append((rel(path, root), i, line.strip()[:80]))
|
|
447
|
+
if not hits:
|
|
448
|
+
print("(no denylist patterns found in build scripts)")
|
|
449
|
+
status("PASS")
|
|
450
|
+
return
|
|
451
|
+
for f, ln, s in hits[:20]:
|
|
452
|
+
print(f" {f}:{ln} {s}")
|
|
453
|
+
status("WARN")
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def main() -> int:
|
|
457
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
458
|
+
parser.add_argument(
|
|
459
|
+
"--root", type=Path, default=Path.cwd(),
|
|
460
|
+
help="Project root to audit (default: current working directory)",
|
|
461
|
+
)
|
|
462
|
+
args = parser.parse_args()
|
|
463
|
+
root = args.root.resolve()
|
|
464
|
+
if not root.is_dir():
|
|
465
|
+
print(f"audit_signals: not a directory: {root}", file=sys.stderr)
|
|
466
|
+
return 2
|
|
467
|
+
files = iter_files(root)
|
|
468
|
+
print(f"project_root: {root}")
|
|
469
|
+
print(f"files_scanned: {len(files)}")
|
|
470
|
+
print()
|
|
471
|
+
block_hotspots(files, root); print()
|
|
472
|
+
block_heredoc(files, root); print()
|
|
473
|
+
block_test_ci(files, root); print()
|
|
474
|
+
block_version_sources(root); print()
|
|
475
|
+
block_packaging_posture(root); print()
|
|
476
|
+
block_install_url(root); print()
|
|
477
|
+
block_agent_doc_dedup(root); print()
|
|
478
|
+
block_drift_markers(files, root); print()
|
|
479
|
+
block_duplicate_setup(root); print()
|
|
480
|
+
block_denylist_in_build(root)
|
|
481
|
+
return 0
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
if __name__ == "__main__":
|
|
485
|
+
sys.exit(main())
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Auto-detect and run project verification (lint + typecheck + tests).
|
|
3
|
+
# Run from the project root. Exits non-zero on failure.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
if [ -f Cargo.toml ]; then
|
|
7
|
+
cargo check && cargo test
|
|
8
|
+
elif [ -f tsconfig.json ]; then
|
|
9
|
+
npx tsc --noEmit && npm test
|
|
10
|
+
elif [ -f package.json ] && grep -q '"test"' package.json; then
|
|
11
|
+
npm test
|
|
12
|
+
elif [ -f Makefile ] && grep -q '^test:' Makefile; then
|
|
13
|
+
make test
|
|
14
|
+
elif [ -f pytest.ini ] || [ -f pyproject.toml ] || find . -maxdepth 2 -name "test_*.py" | grep -q .; then
|
|
15
|
+
pytest
|
|
16
|
+
else
|
|
17
|
+
echo "(no test command detected - ask the user for the verification command)"
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design
|
|
3
|
+
description: "Produces distinctive, production-grade UI for pages, components, visual interfaces, typography, and screenshot-driven polish. Use when users ask 设计/做页面/做组件/UI/前端/截图 or say a screen is ugly, unclear, inconsistent, or visually wrong. Not for backend logic or data pipelines."
|
|
4
|
+
when_to_use: "设计, 做页面, 做组件, 不好看, 不和谐, 不清晰, 很丑, 很怪, 很傻, 突兀, 不协调, 字体, 字形, 排印, 排版, 样式, 前端, UI, 截图, build page, create component, make it look good, style, design, screenshot with visual complaint, typography, font looks wrong"
|
|
5
|
+
dispatch_intent: "UI, component, page, visual interface, frontend, artifact-grounded screenshot aesthetic complaint"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Design: Build It With a Point of View
|
|
9
|
+
|
|
10
|
+
Prefix your first line with 🥷 inline, not as its own paragraph.
|
|
11
|
+
|
|
12
|
+
If it could have been generated by a default prompt, it is not good enough.
|
|
13
|
+
|
|
14
|
+
**Output language rule:** Never use em-dash (—) in any output from this skill. Use commas, colons, or periods instead.
|
|
15
|
+
|
|
16
|
+
**Chinese gut-feel complaints**: when the user says "很傻", "很怪", "突兀", "不协调", "不和谐" about a visual, treat it as an aesthetic rejection, not a debugging symptom. Route to Screenshot Iteration Mode, not to `/hunt`.
|
|
17
|
+
|
|
18
|
+
## Durable Context Preflight
|
|
19
|
+
|
|
20
|
+
See [rules/durable-context.md](../../rules/durable-context.md) for when to read durable context, the read-order budget, and the memory-type mapping.
|
|
21
|
+
|
|
22
|
+
For `/design`, visual constraints are `decision`, `preference`, and `principle` entries; reusable product and UI patterns are `pattern` and `learning`. Current screenshots, rendered output, code, design tokens, and user feedback override memory. Reuse durable visual preferences and mature interaction patterns, but still name the current visual problem from the screenshot or source before changing code.
|
|
23
|
+
|
|
24
|
+
## Screenshot Iteration Mode
|
|
25
|
+
|
|
26
|
+
Activate when the user sends a screenshot or image alongside a complaint ("这里很丑", "这个不对", "fix this", "looks wrong"). The existing product is the direction. Skip the five-question direction lock.
|
|
27
|
+
|
|
28
|
+
**Flow:**
|
|
29
|
+
|
|
30
|
+
1. Read the screenshot. State the problem in one sentence: what specifically looks wrong (spacing, contrast, alignment, typeface, color, density, hierarchy). Preserve the user's negative label when it is diagnostic; do not translate "丑", "乱", "不清晰", or "怪" into vague "make it modern" language.
|
|
31
|
+
2. Wait for the user to confirm the diagnosis before touching code.
|
|
32
|
+
3. If the user provides a reference screenshot, older version, or "this one is good" example, compare current vs. reference and name the visual deltas before choosing a fix.
|
|
33
|
+
4. If the diagnosis is a known UX problem (split-view sync, infinite scroll, virtualised list, sticky header), spend one round surveying how 2-3 mature products in the same category solve it before writing code. Cite what each does. Skip only if the fix is purely cosmetic (color, spacing, copy).
|
|
34
|
+
5. Find the responsible code: grep for the component name or class, read the actual file. Do not rely on memory or assumptions about file location.
|
|
35
|
+
6. Apply the minimal fix. For existing products, try material/opacity, geometry, spacing, typography, or text-fit adjustments before redesigning the surface.
|
|
36
|
+
7. Verify the result in a browser, native app, screenshot tool, or rendered artifact at desktop width and 375px mobile width when applicable. Check long words, localized strings, button labels, and compact states for overflow. If the host cannot render, say that explicitly and hand off the exact view the user should check.
|
|
37
|
+
8. Ask the user to verify in the browser. Do not hand off without this step.
|
|
38
|
+
|
|
39
|
+
**Calibration rules:**
|
|
40
|
+
- The user's screenshot is the strongest design brief in the turn. Keep it visible in the reasoning until the fix is done.
|
|
41
|
+
- The real running product is the oracle. Product pages, app screenshots, release pages, and current UI state override generic style instincts.
|
|
42
|
+
- Do not flatten specific taste feedback into generic UI adjectives. "More premium" is not a diagnosis; "caption baseline drifts above the Chinese line" is.
|
|
43
|
+
- If the screenshot exposes a regression, broken render, timing issue, or generated asset defect rather than taste, route to `/hunt` and preserve the visual evidence.
|
|
44
|
+
|
|
45
|
+
**Boundary**: if the fix requires changing 3 or more components, or if it reveals a direction problem rather than a specific bug, pause and run the full direction lock before continuing.
|
|
46
|
+
|
|
47
|
+
**Redesign priority order** (when reworking an existing UI rather than building from scratch): font replacement → color cleanup → hover/active states → layout and whitespace → replace generic components → add loading/empty/error states → typographic polish. This order maximizes visual lift while minimizing the blast radius of each pass. Full rules in `references/design-reference.md`. Common traps and absolute CSS bans: `references/design-traps.md`.
|
|
48
|
+
|
|
49
|
+
## Lock the Direction First
|
|
50
|
+
|
|
51
|
+
**Before starting any component, page, or visual work**: list 2-3 mature products in the same category (e.g. Notion, Linear, Typora, iA Writer, Raycast), and write one sentence each on how they solve the specific problem at hand. Then write code. Skip only if the task is purely cosmetic (color, spacing, copy).
|
|
52
|
+
|
|
53
|
+
Before writing any code, ask the user directly, using the environment's native question or approval mechanism if it has one:
|
|
54
|
+
|
|
55
|
+
1. **Who uses this, and in what context?** Analyst dashboard differs from landing page or onboarding flow. See "App shell exception" below if the answer is a sidebar + main workspace layout.
|
|
56
|
+
2. **What is the aesthetic direction?** Name it precisely: dense editorial, raw terminal, ink-on-paper, brutalist grid, warm analog. "Clean and modern" is not a direction. If the user names a reference site or product ("feels like Linear / Claude.ai / Vercel"), do not accept it as a direction -- extract 3 concrete properties from it: button radius philosophy, surface depth treatment (shadow vs background step vs border), and accent color family. Name those instead.
|
|
57
|
+
|
|
58
|
+
**Shortcut for well-known brands**: see "Brand preset flow" in `references/design-reference.md`. Ask first, run the preset, then decompose against the generated file.
|
|
59
|
+
3. **What is the one thing this leaves in memory?** A typeface, color system, unexpected motion, asymmetric layout. Pick one and make it obvious.
|
|
60
|
+
4. **What are the hard constraints?** Framework, bundle size, contrast minimums, keyboard accessibility.
|
|
61
|
+
5. **What is the signature micro-interaction?** Scale on press, staggered reveal, or contextual icon animation. Pick one and know exactly how it's implemented.
|
|
62
|
+
|
|
63
|
+
Do not proceed until all five are answered.
|
|
64
|
+
|
|
65
|
+
### Source repo as reference
|
|
66
|
+
|
|
67
|
+
When the user provides a repository URL or pastes source code of an existing product to recreate or extend: the file tree is a menu, not the meal. Do not reconstruct the UI from memory or training data. Instead, read the actual source:
|
|
68
|
+
- Theme and token files: `theme.ts`, `colors.ts`, `tokens.css`, `_variables.scss`, or equivalent
|
|
69
|
+
- Global stylesheets and layout scaffolds
|
|
70
|
+
- The specific components the user mentioned
|
|
71
|
+
|
|
72
|
+
Lift exact values: hex codes, spacing scale entries, font stacks, border radii. A rough approximation is not pixel fidelity.
|
|
73
|
+
|
|
74
|
+
Only attach the target component folder or package. Exclude `.git`, `node_modules`, `dist`, and lock files. Dragging in an entire monorepo pollutes the context with irrelevant code and degrades output quality.
|
|
75
|
+
|
|
76
|
+
### App shell exception (sidebar + main workspace)
|
|
77
|
+
|
|
78
|
+
If question 1 is an app shell (Slack, Linear, Notion class), load the "App shell rules" section in `references/design-reference.md` and apply those constraints before proceeding.
|
|
79
|
+
|
|
80
|
+
### Data dashboard exception
|
|
81
|
+
|
|
82
|
+
If the surface is a dashboard, analytics view, or chart-heavy interface, also load `references/design-data-viz.md` for chart selection, number alignment, and product-benchmark rules. Skip when building marketing pages, landing pages, or generic components.
|
|
83
|
+
|
|
84
|
+
State the chosen direction in one sentence, then load `references/design-reference.md` and check the tech stack conflicts table. Name the single CSS strategy before writing the first component. For token decisions (color, font, motion): load `references/design-tokens.md`. For aesthetic quality review and production structure: load `references/design-aesthetic-quality.md`.
|
|
85
|
+
|
|
86
|
+
Summarize the direction as three lines before writing any code:
|
|
87
|
+
- **Visual thesis**: mood, material, and energy in one sentence (e.g. "warm brutalist editorial with high-contrast ink type and rough paper texture")
|
|
88
|
+
- **Content plan**: hero -> support -> detail -> final CTA, one line each. For **app/dashboard surfaces**: skip the marketing structure, default to utility mode (orient, show status, enable action), no hero unless explicitly requested.
|
|
89
|
+
- **Interaction thesis**: 2-3 specific motion ideas that change how the page feels (e.g. "hero text slides in on load, section headers pin while content scrolls beneath, CTA pulses on hover")
|
|
90
|
+
|
|
91
|
+
For production or multi-page UIs, expand the thesis into the 9-section DESIGN.md scaffold in `references/design-reference.md` (theme, palette, typography, components, layout, depth, do/don't, responsive, prompt guide). For a single component, the three lines are sufficient.
|
|
92
|
+
|
|
93
|
+
## Non-Negotiable Constraints
|
|
94
|
+
|
|
95
|
+
`references/design-reference.md` is already loaded during direction lock. It owns the full rules: typography, OKLCH color, motion timings, layout defaults, CSS-pattern bans, accessibility baseline, and complexity matching. Apply them. Do not restate them here.
|
|
96
|
+
|
|
97
|
+
## When Asked For Options
|
|
98
|
+
|
|
99
|
+
Give at least 3 variations across genuinely different dimensions (density, typography, color, layout, motion). See "Options guide" in `references/design-reference.md` for the full variation framework. Three options differing only by accent color are not three variations.
|
|
100
|
+
|
|
101
|
+
## Gotchas
|
|
102
|
+
|
|
103
|
+
| What happened | Rule |
|
|
104
|
+
|---------------|------|
|
|
105
|
+
| Used Inter as the display font | It communicates nothing. Pick something with a personality. |
|
|
106
|
+
| Three cards, identical shadows, identical padding -- a template | If swapping content doesn't require layout changes, redo it. |
|
|
107
|
+
| Claimed it looked right without opening a browser | Code correct in your head can look broken in the browser. Open it. |
|
|
108
|
+
| Chose glassmorphism, ignored the mobile constraint | `backdrop-filter` is expensive on low-power devices. Name the tradeoff. |
|
|
109
|
+
| Light-mode app: white panel on white background, visually indistinguishable | Adjacent nested surfaces must differ visually. Either background step (sidebar vs main ≥4% lightness difference) or shadow minimum `0 1px 3px rgba(0,0,0,0.10)`. |
|
|
110
|
+
| Fixed visual polish by redesigning the whole surface | Locate the concrete visual delta first, then make the smallest material, opacity, geometry, or typography change that addresses it. |
|
|
111
|
+
| English looked fine, localized text overflowed | Test long words and localized strings before handoff, especially inside buttons, tabs, nav, and compact cards. |
|
|
112
|
+
|
|
113
|
+
## Aesthetic Review
|
|
114
|
+
|
|
115
|
+
After significant build phases and at handoff, re-read the visual thesis from direction lock. If what is on screen drifted toward a generic default, identify the specific element that broke first (typeface, color, card treatment, spacing) and fix it before continuing.
|
|
116
|
+
|
|
117
|
+
Run these checks before the handoff summary:
|
|
118
|
+
- Is the brand or product unmistakable in the first screen?
|
|
119
|
+
- Is there one strong visual anchor (real imagery, not a decorative gradient)?
|
|
120
|
+
- Can the page be understood by scanning headlines only?
|
|
121
|
+
- Does each section have one job?
|
|
122
|
+
- Are cards actually necessary, or just default styling?
|
|
123
|
+
- Does motion improve hierarchy or atmosphere, or is it ornamental?
|
|
124
|
+
- Would the design still feel premium if all decorative shadows were removed?
|
|
125
|
+
- AI Slop Test: scan the first screen for default patterns (reflex font, purple-to-blue gradient, centered hero with two CTAs side by side, three identical cards, generic top nav). If any appear unintentionally, fix typography, color, or layout until none remain.
|
|
126
|
+
|
|
127
|
+
If any check fails, fix first. Ask the user to verify at full width and at 375px; if the layout breaks at mobile width, fix before handing off.
|
|
128
|
+
|
|
129
|
+
End with:
|
|
130
|
+
- Aesthetic direction, named and justified in 2-3 sentences
|
|
131
|
+
- Non-obvious choices explained: typeface, color decisions, layout logic
|
|
132
|
+
- Instructions for replacing placeholder content with real content
|
|
133
|
+
|
|
134
|
+
After handoff, stop.
|