@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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +206 -0
  3. package/package.json +35 -0
  4. package/rules/anti-patterns.md +38 -0
  5. package/rules/chinese.md +18 -0
  6. package/rules/durable-context.md +27 -0
  7. package/rules/english.md +14 -0
  8. package/scripts/build_metadata.py +360 -0
  9. package/scripts/check_routing_drift.py +82 -0
  10. package/scripts/dispatcher-template.md +43 -0
  11. package/scripts/dispatcher.md +53 -0
  12. package/scripts/package-skill.sh +71 -0
  13. package/scripts/packaging_filter.py +55 -0
  14. package/scripts/setup-rule.sh +109 -0
  15. package/scripts/setup-statusline.sh +127 -0
  16. package/scripts/skill_checks.py +483 -0
  17. package/scripts/skill_frontmatter.py +110 -0
  18. package/scripts/statusline.sh +321 -0
  19. package/scripts/validate_package.py +66 -0
  20. package/scripts/verify_skills.py +100 -0
  21. package/skills/RESOLVER.md +91 -0
  22. package/skills/check/SKILL.md +338 -0
  23. package/skills/check/agents/reviewer-architecture.md +39 -0
  24. package/skills/check/agents/reviewer-security.md +39 -0
  25. package/skills/check/references/persona-catalog.md +56 -0
  26. package/skills/check/references/project-context.md +107 -0
  27. package/skills/check/references/public-reply.md +14 -0
  28. package/skills/check/scripts/audit_signals.py +485 -0
  29. package/skills/check/scripts/run-tests.sh +19 -0
  30. package/skills/design/SKILL.md +134 -0
  31. package/skills/design/references/design-aesthetic-quality.md +67 -0
  32. package/skills/design/references/design-data-viz.md +34 -0
  33. package/skills/design/references/design-reference.md +278 -0
  34. package/skills/design/references/design-tokens.md +53 -0
  35. package/skills/design/references/design-traps.md +43 -0
  36. package/skills/health/SKILL.md +231 -0
  37. package/skills/health/agents/inspector-context.md +119 -0
  38. package/skills/health/agents/inspector-control.md +84 -0
  39. package/skills/health/agents/inspector-maintainability.md +55 -0
  40. package/skills/health/scripts/check-agent-context.sh +5 -0
  41. package/skills/health/scripts/check-doc-refs.sh +8 -0
  42. package/skills/health/scripts/check-maintainability.sh +8 -0
  43. package/skills/health/scripts/check-verifier-output.sh +5 -0
  44. package/skills/health/scripts/check_agent_context.py +407 -0
  45. package/skills/health/scripts/check_doc_refs.py +110 -0
  46. package/skills/health/scripts/check_maintainability.py +629 -0
  47. package/skills/health/scripts/check_verifier_output.py +116 -0
  48. package/skills/health/scripts/collect-data.sh +760 -0
  49. package/skills/hunt/SKILL.md +197 -0
  50. package/skills/hunt/references/failure-patterns.md +75 -0
  51. package/skills/hunt/references/ime-unicode.md +58 -0
  52. package/skills/hunt/references/logging-techniques.md +72 -0
  53. package/skills/hunt/references/rendering-debug.md +34 -0
  54. package/skills/learn/SKILL.md +128 -0
  55. package/skills/read/SKILL.md +108 -0
  56. package/skills/read/references/read-methods.md +110 -0
  57. package/skills/read/references/save-paths.md +33 -0
  58. package/skills/read/scripts/fetch.sh +105 -0
  59. package/skills/read/scripts/fetch_feishu.py +246 -0
  60. package/skills/read/scripts/fetch_local.py +218 -0
  61. package/skills/read/scripts/fetch_weixin.py +107 -0
  62. package/skills/think/SKILL.md +155 -0
  63. package/skills/write/SKILL.md +129 -0
  64. package/skills/write/references/write-en.md +197 -0
  65. package/skills/write/references/write-zh-bilingual.md +60 -0
  66. package/skills/write/references/write-zh-prose.md +48 -0
  67. package/skills/write/references/write-zh-release-notes.md +38 -0
  68. package/skills/write/references/write-zh.md +645 -0
@@ -0,0 +1,629 @@
1
+ #!/usr/bin/env python3
2
+ """AI maintainability audit: project shape, context surface, verification surface,
3
+ decision artifacts, drift markers, hotspot ownership, markdown links.
4
+
5
+ Run as: python3 check_maintainability.py [ROOT] [summary|deep]
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import os
13
+ import re
14
+ import subprocess
15
+ import sys
16
+ import urllib.parse
17
+ from collections import Counter
18
+ from pathlib import Path
19
+
20
+
21
+ EXCLUDED_DIRS = {
22
+ ".git",
23
+ ".hg",
24
+ ".svn",
25
+ "node_modules",
26
+ "dist",
27
+ "build",
28
+ ".next",
29
+ "__pycache__",
30
+ ".turbo",
31
+ "target",
32
+ ".venv",
33
+ "venv",
34
+ "vendor",
35
+ "coverage",
36
+ ".cache",
37
+ ".parcel-cache",
38
+ ".pytest_cache",
39
+ ".mypy_cache",
40
+ ".ruff_cache",
41
+ }
42
+
43
+ SOURCE_EXTS = {
44
+ ".c", ".cc", ".cpp", ".cs", ".css", ".go", ".h", ".hpp", ".html",
45
+ ".java", ".js", ".jsx", ".kt", ".lua", ".m", ".mm", ".md", ".mjs",
46
+ ".php", ".py", ".rb", ".rs", ".scss", ".sh", ".swift", ".ts", ".tsx",
47
+ ".vue", ".yaml", ".yml",
48
+ }
49
+
50
+ MARKER_RE = re.compile(r"\b(TODO|FIXME|HACK|XXX)\b", re.IGNORECASE)
51
+ MAKE_RE = re.compile(r"^([A-Za-z0-9_.-]+)\s*:(?![=])")
52
+ MAKE_CMD_RE = re.compile(r"\bmake\s+([A-Za-z0-9_.-]+)\b")
53
+ NPM_CMD_RE = re.compile(r"\b(?:npm|pnpm|yarn|bun)\s+run\s+([A-Za-z0-9:_-]+)\b")
54
+ COMMAND_LINE_RE = re.compile(r"^(?:make|npm|pnpm|yarn|bun)\s+")
55
+ MARKDOWN_LINK_RE = re.compile(r"!?\[[^\]]*\]\(([^)]+)\)")
56
+ URL_RE = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
57
+ HOTSPOT_WORD_RE = re.compile(
58
+ r"(hotspot|large file|ownership|owner|boundary|module|owned|owns|"
59
+ r"负责|边界|模块|热点|大文件)",
60
+ re.IGNORECASE,
61
+ )
62
+ VERIFICATION_WORD_RE = re.compile(
63
+ r"(verification|verify|test|check|make\s+\w+|pytest|go test|cargo test|"
64
+ r"npm test|npm run|pnpm|yarn|swift test|xcodebuild|验证|测试)",
65
+ re.IGNORECASE,
66
+ )
67
+
68
+
69
+ def rel(path: Path, root: Path) -> str:
70
+ try:
71
+ return path.resolve().relative_to(root).as_posix()
72
+ except ValueError:
73
+ return path.as_posix()
74
+
75
+
76
+ def is_excluded(path: Path, root: Path) -> bool:
77
+ parts = path.relative_to(root).parts if path.is_absolute() else path.parts
78
+ return any(part in EXCLUDED_DIRS for part in parts)
79
+
80
+
81
+ def read_text(path: Path, limit: int | None = None) -> str:
82
+ try:
83
+ data = path.read_text(encoding="utf-8", errors="replace")
84
+ except OSError:
85
+ return ""
86
+ return data[:limit] if limit else data
87
+
88
+
89
+ def iter_files(root: Path) -> list[Path]:
90
+ try:
91
+ proc = subprocess.run(
92
+ ["git", "-C", str(root), "ls-files", "--cached", "--others", "--exclude-standard"],
93
+ text=True,
94
+ stdout=subprocess.PIPE,
95
+ stderr=subprocess.DEVNULL,
96
+ check=False,
97
+ )
98
+ if proc.returncode == 0 and proc.stdout.strip():
99
+ files = []
100
+ for line in proc.stdout.splitlines():
101
+ path = root / line
102
+ if path.is_file() and not is_excluded(path, root):
103
+ files.append(path)
104
+ return files
105
+ except OSError:
106
+ pass
107
+
108
+ files = []
109
+ for dirpath, dirnames, filenames in os.walk(root):
110
+ current = Path(dirpath)
111
+ dirnames[:] = [name for name in dirnames if name not in EXCLUDED_DIRS]
112
+ if is_excluded(current, root):
113
+ continue
114
+ for filename in filenames:
115
+ path = current / filename
116
+ if path.is_file() and not is_excluded(path, root):
117
+ files.append(path)
118
+ return files
119
+
120
+
121
+ def line_count(path: Path) -> int:
122
+ try:
123
+ with path.open("rb") as handle:
124
+ return sum(1 for _ in handle)
125
+ except OSError:
126
+ return 0
127
+
128
+
129
+ def print_list(items: list[str], empty: str = "(none)", limit: int | None = None) -> None:
130
+ shown = items if limit is None else items[:limit]
131
+ if not shown:
132
+ print(f" {empty}")
133
+ return
134
+ for item in shown:
135
+ print(f" {item}")
136
+ if limit is not None and len(items) > limit:
137
+ print(f" ... {len(items) - limit} more")
138
+
139
+
140
+ def instruction_paths(root: Path) -> list[Path]:
141
+ candidates = [
142
+ root / "AGENTS.md",
143
+ root / "CLAUDE.md",
144
+ root / ".github" / "copilot-instructions.md",
145
+ root / "GEMINI.md",
146
+ ]
147
+ instructions_dir = root / ".github" / "instructions"
148
+ if instructions_dir.is_dir():
149
+ candidates.extend(sorted(instructions_dir.glob("*.md")))
150
+ return [path for path in candidates if path.is_file() and not is_excluded(path, root)]
151
+
152
+
153
+ def find_text_signal(paths: list[Path], patterns: list[str]) -> bool:
154
+ regexes = [re.compile(pattern, re.IGNORECASE) for pattern in patterns]
155
+ for path in paths:
156
+ text = read_text(path, 200_000)
157
+ if any(regex.search(text) for regex in regexes):
158
+ return True
159
+ return False
160
+
161
+
162
+ def parse_makefile(root: Path) -> tuple[set[str], list[str]]:
163
+ makefile = root / "Makefile"
164
+ targets: set[str] = set()
165
+ commands: list[str] = []
166
+ if not makefile.is_file():
167
+ return targets, commands
168
+ for line in read_text(makefile).splitlines():
169
+ match = MAKE_RE.match(line)
170
+ if not match:
171
+ continue
172
+ target = match.group(1)
173
+ if target.startswith("."):
174
+ continue
175
+ targets.add(target)
176
+ if re.search(r"(test|check|lint|type|build|package|verify|smoke)", target, re.IGNORECASE):
177
+ commands.append(f"make {target}")
178
+ return targets, commands
179
+
180
+
181
+ def parse_package_json(root: Path) -> tuple[set[str], list[str]]:
182
+ package = root / "package.json"
183
+ script_names: set[str] = set()
184
+ commands: list[str] = []
185
+ if not package.is_file():
186
+ return script_names, commands
187
+ try:
188
+ data = json.loads(read_text(package))
189
+ except json.JSONDecodeError:
190
+ return script_names, commands
191
+ scripts = data.get("scripts", {})
192
+ if not isinstance(scripts, dict):
193
+ return script_names, commands
194
+ for name in sorted(scripts):
195
+ script_names.add(name)
196
+ if re.search(r"(test|check|lint|type|build|verify)", name, re.IGNORECASE):
197
+ commands.append(f"npm run {name}")
198
+ return script_names, commands
199
+
200
+
201
+ def parse_ci_commands(root: Path) -> list[str]:
202
+ workflows_dir = root / ".github" / "workflows"
203
+ workflows = sorted(workflows_dir.glob("*.yml")) if workflows_dir.is_dir() else []
204
+ workflows += sorted(workflows_dir.glob("*.yaml")) if workflows_dir.is_dir() else []
205
+ commands: list[str] = []
206
+ for workflow in workflows:
207
+ for raw in read_text(workflow).splitlines():
208
+ line = raw.strip()
209
+ if line.startswith("- run:"):
210
+ command = line.split("- run:", 1)[1].strip().strip("'\"")
211
+ elif line.startswith("run:"):
212
+ command = line.split("run:", 1)[1].strip().strip("'\"")
213
+ else:
214
+ continue
215
+ if command and command != "|":
216
+ commands.append(f"{rel(workflow, root)}: {command}")
217
+ return commands
218
+
219
+
220
+ def scan_markdown_links(files: list[Path], root: Path) -> list[str]:
221
+ missing: list[str] = []
222
+ markdown_files = [path for path in files if path.suffix.lower() == ".md"]
223
+ for path in markdown_files:
224
+ for lineno, line in enumerate(read_text(path).splitlines(), 1):
225
+ for raw in MARKDOWN_LINK_RE.findall(line):
226
+ target = raw.strip().split()[0].strip("<>")
227
+ if not target or target.startswith("#") or URL_RE.match(target):
228
+ continue
229
+ target = urllib.parse.unquote(target.split("#", 1)[0])
230
+ if not target:
231
+ continue
232
+ full = (path.parent / target).resolve()
233
+ if not full.exists():
234
+ missing.append(f"{rel(path, root)}:{lineno} -> {target}")
235
+ return missing
236
+
237
+
238
+ def verification_surface(
239
+ root: Path, instruction_files: list[Path]
240
+ ) -> tuple[list[str], list[str], set[str], set[str]]:
241
+ make_targets, make_commands = parse_makefile(root)
242
+ package_scripts, package_commands = parse_package_json(root)
243
+ commands = make_commands + package_commands + parse_ci_commands(root)
244
+
245
+ if (root / "Cargo.toml").is_file():
246
+ commands.extend(["cargo test", "cargo check"])
247
+ if (root / "go.mod").is_file():
248
+ commands.append("go test ./...")
249
+ if (root / "pyproject.toml").is_file() or (root / "pytest.ini").is_file():
250
+ commands.append("pytest")
251
+ if (root / "pom.xml").is_file():
252
+ commands.append("mvn test")
253
+ if (root / "deno.json").is_file() or (root / "deno.jsonc").is_file():
254
+ commands.append("deno test")
255
+
256
+ missing: list[str] = []
257
+ for path in instruction_files:
258
+ text = read_text(path, 200_000)
259
+ snippets: list[str] = []
260
+ for raw_line in text.splitlines():
261
+ snippets.extend(re.findall(r"`([^`]+)`", raw_line))
262
+ stripped = raw_line.strip().strip("`")
263
+ if COMMAND_LINE_RE.match(stripped):
264
+ snippets.append(stripped)
265
+ for snippet in snippets:
266
+ for target in MAKE_CMD_RE.findall(snippet):
267
+ if target not in make_targets:
268
+ missing.append(f"{rel(path, root)} references missing make target: {target}")
269
+ for script in NPM_CMD_RE.findall(snippet):
270
+ if script not in package_scripts:
271
+ missing.append(f"{rel(path, root)} references missing package script: {script}")
272
+
273
+ unique_commands = list(dict.fromkeys(commands))
274
+ unique_missing = list(dict.fromkeys(missing))
275
+ return unique_commands, unique_missing, make_targets, package_scripts
276
+
277
+
278
+ def hotspot_ownership_surface(
279
+ records: list[tuple[int, int, Path]],
280
+ instruction_files: list[Path],
281
+ mode: str,
282
+ root: Path,
283
+ ) -> tuple[str, list[str], list[str], list[str]]:
284
+ if mode != "deep":
285
+ return "SKIPPED", [], [], []
286
+ if not records:
287
+ return "PASS", [], [], []
288
+
289
+ snippets: list[str] = []
290
+ for path in instruction_files:
291
+ snippets.append(f"\n# {rel(path, root)}\n{read_text(path, 200_000)}")
292
+ instruction_text = "\n".join(snippets)
293
+ lower_text = instruction_text.lower()
294
+ instruction_lines = instruction_text.splitlines()
295
+
296
+ documented: list[str] = []
297
+ missing: list[str] = []
298
+ for lines, _size, path in records:
299
+ relative = rel(path, root)
300
+ relative_lower = relative.lower()
301
+ indices: list[int] = []
302
+ start = 0
303
+ while True:
304
+ index = lower_text.find(relative_lower, start)
305
+ if index < 0:
306
+ break
307
+ indices.append(index)
308
+ start = index + len(relative_lower)
309
+
310
+ if not indices:
311
+ missing.append(f"{relative} lines={lines} reason=not mentioned in agent instructions")
312
+ continue
313
+
314
+ saw_hotspot_context = False
315
+ saw_verification_context = False
316
+ has_documented_entry = False
317
+ for index in indices:
318
+ window = lower_text[max(0, index - 700): index + len(relative_lower) + 700]
319
+ line_no = lower_text[:index].count("\n")
320
+ local_lines = instruction_lines[max(0, line_no - 1): line_no + 4]
321
+ local_context = "\n".join(local_lines)
322
+ has_hotspot_context = bool(HOTSPOT_WORD_RE.search(window))
323
+ has_verification_context = bool(VERIFICATION_WORD_RE.search(local_context))
324
+ saw_hotspot_context = saw_hotspot_context or has_hotspot_context
325
+ saw_verification_context = saw_verification_context or has_verification_context
326
+ if has_hotspot_context and has_verification_context:
327
+ has_documented_entry = True
328
+ break
329
+
330
+ if has_documented_entry:
331
+ documented.append(f"{relative} lines={lines}")
332
+ else:
333
+ reasons = []
334
+ if not saw_hotspot_context:
335
+ reasons.append("missing ownership/boundary context")
336
+ if not saw_verification_context:
337
+ reasons.append("missing verification context")
338
+ if saw_hotspot_context and saw_verification_context:
339
+ reasons.append("ownership and verification are not in the same hotspot entry")
340
+ missing.append(f"{relative} lines={lines} reason={'; '.join(reasons)}")
341
+
342
+ findings: list[str] = []
343
+ if missing:
344
+ findings.append("large source files lack hotspot ownership or verification map")
345
+ return ("WARN" if missing else "PASS"), documented, missing, findings
346
+
347
+
348
+ def main() -> int:
349
+ parser = argparse.ArgumentParser(description=__doc__)
350
+ parser.add_argument("root", nargs="?", default=".", help="Repo root (default: cwd)")
351
+ parser.add_argument(
352
+ "mode", nargs="?", default="summary", choices=("summary", "deep"),
353
+ help="Output detail level",
354
+ )
355
+ args = parser.parse_args()
356
+ root = Path(args.root).resolve()
357
+ mode = args.mode
358
+
359
+ if not root.is_dir():
360
+ print(f"Repo root not found: {root}", file=sys.stderr)
361
+ return 2
362
+
363
+ files = iter_files(root)
364
+ tracked_count = len(files)
365
+ extensions = Counter(path.suffix.lower() or "(none)" for path in files)
366
+ detected_manifests = [
367
+ name
368
+ for name in [
369
+ "Makefile", "package.json", "Cargo.toml", "go.mod", "pyproject.toml",
370
+ "pytest.ini", "pom.xml", "deno.json", "deno.jsonc",
371
+ ]
372
+ if (root / name).is_file()
373
+ ]
374
+ workflows_dir = root / ".github" / "workflows"
375
+ workflow_count = 0
376
+ if workflows_dir.is_dir():
377
+ workflow_count = len(list(workflows_dir.glob("*.yml"))) + len(list(workflows_dir.glob("*.yaml")))
378
+ if workflow_count:
379
+ detected_manifests.append(f".github/workflows ({workflow_count})")
380
+
381
+ source_files = [path for path in files if path.suffix.lower() in SOURCE_EXTS]
382
+ source_stats: list[tuple[int, int, Path]] = []
383
+ for path in source_files:
384
+ try:
385
+ size = path.stat().st_size
386
+ except OSError:
387
+ size = 0
388
+ source_stats.append((line_count(path), size, path))
389
+ source_stats.sort(key=lambda item: (item[0], item[1]), reverse=True)
390
+
391
+ dir_counts: Counter[str] = Counter()
392
+ for path in files:
393
+ relative_parts = Path(rel(path, root)).parts
394
+ top = relative_parts[0] if len(relative_parts) > 1 else "."
395
+ dir_counts[top] += 1
396
+
397
+ instruction_files = instruction_paths(root)
398
+ project_map = find_text_signal(
399
+ instruction_files,
400
+ [r"repository map", r"project map", r"repo map", r"\bproject\b", r"目录", r"仓库", r"结构"],
401
+ )
402
+ instruction_verification = find_text_signal(
403
+ instruction_files,
404
+ [r"verification", r"test plan", r"make test", r"npm test", r"pytest", r"cargo test", r"验证", r"测试"],
405
+ )
406
+ boundaries = find_text_signal(
407
+ instruction_files,
408
+ [r"not for", r"do not", r"non-?goals?", r"scope", r"boundar", r"never", r"avoid", r"边界", r"非目标", r"不要"],
409
+ )
410
+ commands, missing_references, make_targets, _package_scripts = verification_surface(root, instruction_files)
411
+ stable_make_targets = sorted(make_targets & {"check", "test", "verify"})
412
+ wrapper_warnings: list[str] = []
413
+ if len(commands) >= 2 and (root / "Makefile").is_file() and not stable_make_targets:
414
+ wrapper_warnings.append(
415
+ "multiple verification commands discovered but Makefile lacks check/test/verify wrapper"
416
+ )
417
+
418
+ decision_artifacts = {
419
+ "docs_dir": (root / "docs").is_dir(),
420
+ "specs_dir": (root / "specs").is_dir(),
421
+ "specify_dir": (root / ".specify").is_dir(),
422
+ "handoff_md": any(path.name.upper() == "HANDOFF.MD" for path in root.glob("*.md")),
423
+ "changelog": any(path.name.upper().startswith("CHANGELOG") for path in root.glob("*")),
424
+ "issue_templates": (root / ".github" / "ISSUE_TEMPLATE").exists(),
425
+ "pr_template": any(
426
+ path.is_file()
427
+ for path in [
428
+ root / ".github" / "pull_request_template.md",
429
+ root / ".github" / "PULL_REQUEST_TEMPLATE.md",
430
+ ]
431
+ ),
432
+ }
433
+
434
+ todo_counts: Counter[str] = Counter()
435
+ todo_total = 0
436
+ for path in source_files:
437
+ text = read_text(path, 200_000)
438
+ # Count marker-bearing lines, not marker words. Documentation often names
439
+ # the full marker family in one rule line; treating that as four issues
440
+ # makes the checker flag itself instead of real TODO piles.
441
+ count = sum(1 for line in text.splitlines() if MARKER_RE.search(line))
442
+ if count:
443
+ todo_counts[rel(path, root)] += count
444
+ todo_total += count
445
+
446
+ large_line_limit = 1200 if mode == "summary" else 800
447
+ large_file_records = [
448
+ (lines, size, path) for lines, size, path in source_stats if lines >= large_line_limit
449
+ ]
450
+ large_files = [f"{rel(path, root)} lines={lines} bytes={size}" for lines, size, path in large_file_records]
451
+ todo_hotspots = [
452
+ f"{path} markers={count}" for path, count in todo_counts.most_common(8 if mode == "deep" else 5)
453
+ ]
454
+
455
+ doc_ref_status = "unavailable"
456
+ doc_ref_detail = ""
457
+ checker = os.environ.get("DOC_REF_CHECKER")
458
+ if checker and Path(checker).is_file():
459
+ proc = subprocess.run(
460
+ ["bash", checker, str(root)],
461
+ text=True,
462
+ stdout=subprocess.PIPE,
463
+ stderr=subprocess.STDOUT,
464
+ check=False,
465
+ )
466
+ doc_ref_status = "pass" if proc.returncode == 0 else "fail"
467
+ if proc.stdout.strip():
468
+ first_lines = proc.stdout.strip().splitlines()[:8]
469
+ doc_ref_detail = " | ".join(first_lines)
470
+
471
+ has_instruction_surface = bool(instruction_files)
472
+ has_command_surface = bool(commands)
473
+ context_warnings: list[str] = []
474
+ verification_warnings: list[str] = []
475
+ drift_warnings: list[str] = []
476
+
477
+ if not has_instruction_surface:
478
+ context_warnings.append("no agent instruction surface")
479
+ if has_instruction_surface and not project_map:
480
+ context_warnings.append("instructions lack project map")
481
+ if has_instruction_surface and not instruction_verification:
482
+ context_warnings.append("instructions lack verification guidance")
483
+ if has_instruction_surface and not boundaries:
484
+ context_warnings.append("instructions lack scope/boundary language")
485
+ if not has_command_surface:
486
+ verification_warnings.append("no executable verification command discovered")
487
+ if missing_references:
488
+ verification_warnings.append("instruction references missing commands")
489
+ if todo_total >= (50 if mode == "summary" else 25):
490
+ drift_warnings.append("TODO/FIXME/HACK/XXX markers are concentrated")
491
+ hotspot_status, documented_hotspots, missing_hotspot_ownership, hotspot_findings = (
492
+ hotspot_ownership_surface(large_file_records, instruction_files, mode, root)
493
+ )
494
+ if large_files and mode != "deep":
495
+ drift_warnings.append("large source files need ownership or module boundaries")
496
+ if hotspot_status == "WARN":
497
+ drift_warnings.extend(hotspot_findings)
498
+ if doc_ref_status == "fail":
499
+ drift_warnings.append("broken documentation references")
500
+
501
+ markdown_missing: list[str] = []
502
+ markdown_link_status = "SKIPPED"
503
+ if mode == "deep":
504
+ markdown_missing = scan_markdown_links(files, root)
505
+ markdown_link_status = "WARN" if markdown_missing else "PASS"
506
+ if markdown_missing:
507
+ drift_warnings.append("broken Markdown links")
508
+
509
+ context_status = "FAIL" if not has_instruction_surface else ("WARN" if context_warnings else "PASS")
510
+ verification_status = "FAIL" if not has_command_surface else ("WARN" if verification_warnings else "PASS")
511
+ decision_status = "PASS"
512
+ wrapper_status = "WARN" if wrapper_warnings else "PASS"
513
+ drift_status = "WARN" if drift_warnings else "PASS"
514
+
515
+ if context_status == "FAIL" or verification_status == "FAIL" or doc_ref_status == "fail":
516
+ overall = "FAIL"
517
+ elif "WARN" in {
518
+ context_status, verification_status, decision_status, wrapper_status,
519
+ drift_status, markdown_link_status, hotspot_status,
520
+ }:
521
+ overall = "WARN"
522
+ else:
523
+ overall = "PASS"
524
+
525
+ top_ext = [f"{ext} files={count}" for ext, count in extensions.most_common(10)]
526
+ largest_sources = [
527
+ f"{rel(path, root)} lines={lines} bytes={size}"
528
+ for lines, size, path in source_stats[: (10 if mode == "deep" else 5)]
529
+ ]
530
+ largest_dirs = [f"{directory} files={count}" for directory, count in dir_counts.most_common(8)]
531
+
532
+ print("=== PROJECT SHAPE ===")
533
+ print(f"maintainability_status: {overall}")
534
+ print(f"mode: {mode}")
535
+ print(f"tracked_files: {tracked_count}")
536
+ print("top_extensions:")
537
+ print_list(top_ext)
538
+ print("largest_source_files:")
539
+ print_list(largest_sources)
540
+ print("largest_directories:")
541
+ print_list(largest_dirs)
542
+
543
+ print("=== AI CONTEXT SURFACE ===")
544
+ print(f"context_status: {context_status}")
545
+ print(f"AGENTS.md: {'yes' if (root / 'AGENTS.md').is_file() else 'no'}")
546
+ print(f"CLAUDE.md: {'yes' if (root / 'CLAUDE.md').is_file() else 'no'}")
547
+ print(f".github/copilot-instructions.md: {'yes' if (root / '.github' / 'copilot-instructions.md').is_file() else 'no'}")
548
+ github_instruction_count = (
549
+ len(list((root / ".github" / "instructions").glob("*.md")))
550
+ if (root / ".github" / "instructions").is_dir() else 0
551
+ )
552
+ print(f".github/instructions/*.md: {github_instruction_count}")
553
+ print(f"GEMINI.md: {'yes' if (root / 'GEMINI.md').is_file() else 'no'}")
554
+ print(f"project_map: {'yes' if project_map else 'no'}")
555
+ print(f"verification_guidance: {'yes' if instruction_verification else 'no'}")
556
+ print(f"boundary_guidance: {'yes' if boundaries else 'no'}")
557
+ print("context_findings:")
558
+ print_list(context_warnings)
559
+ print("instruction_files:")
560
+ print_list([rel(path, root) for path in instruction_files])
561
+
562
+ print("=== VERIFICATION SURFACE ===")
563
+ print(f"verification_status: {verification_status}")
564
+ print("detected_manifests:")
565
+ print_list(detected_manifests)
566
+ print("commands:")
567
+ print_list(commands, limit=12 if mode == "summary" else None)
568
+ print("missing_referenced_commands:")
569
+ print_list(missing_references, limit=10 if mode == "summary" else None)
570
+ print("verification_findings:")
571
+ print_list(verification_warnings)
572
+
573
+ print("=== VERIFICATION WRAPPER SURFACE ===")
574
+ print(f"wrapper_status: {wrapper_status}")
575
+ print(f"makefile_present: {'yes' if (root / 'Makefile').is_file() else 'no'}")
576
+ print("stable_make_targets:")
577
+ print_list([f"make {target}" for target in stable_make_targets])
578
+ print("wrapper_findings:")
579
+ print_list(wrapper_warnings)
580
+
581
+ print("=== DECISION ARTIFACTS ===")
582
+ print(f"decision_artifacts_status: {decision_status}")
583
+ for key, value in decision_artifacts.items():
584
+ print(f"{key}: {'yes' if value else 'no'}")
585
+
586
+ print("=== DRIFT MARKERS ===")
587
+ print(f"drift_status: {drift_status}")
588
+ print(f"todo_markers: {todo_total}")
589
+ print("todo_hotspots:")
590
+ print_list(todo_hotspots)
591
+ print("large_source_files:")
592
+ print_list(large_files[: (10 if mode == "deep" else 5)])
593
+ print(f"broken_doc_references: {doc_ref_status}")
594
+ if doc_ref_detail and (mode == "deep" or doc_ref_status == "fail"):
595
+ print(f"broken_doc_reference_detail: {doc_ref_detail}")
596
+ print("drift_findings:")
597
+ print_list(drift_warnings)
598
+
599
+ print("=== HOTSPOT OWNERSHIP SURFACE ===")
600
+ print(f"hotspot_ownership_status: {hotspot_status}")
601
+ print(f"large_hotspot_threshold_lines: {large_line_limit if mode == 'deep' else '(deep mode only)'}")
602
+ print("documented_hotspots:")
603
+ if mode == "deep":
604
+ print_list(documented_hotspots)
605
+ else:
606
+ print(" (skipped: deep mode only)")
607
+ print("missing_hotspot_ownership:")
608
+ if mode == "deep":
609
+ print_list(missing_hotspot_ownership)
610
+ else:
611
+ print(" (skipped: deep mode only)")
612
+ print("hotspot_ownership_findings:")
613
+ if mode == "deep":
614
+ print_list(hotspot_findings)
615
+ else:
616
+ print(" (skipped: deep mode only)")
617
+
618
+ print("=== MARKDOWN LINK SURFACE ===")
619
+ print(f"markdown_link_status: {markdown_link_status}")
620
+ print("missing_markdown_links:")
621
+ if mode == "deep":
622
+ print_list(markdown_missing, limit=20)
623
+ else:
624
+ print(" (skipped: deep mode only)")
625
+ return 0
626
+
627
+
628
+ if __name__ == "__main__":
629
+ sys.exit(main())