vibe-coding-master 0.8.13 → 0.9.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 (42) hide show
  1. package/README.md +7 -3
  2. package/dist/backend/cli/install-vcm-harness.js +57 -18
  3. package/dist/backend/gateway/gateway-service.js +5 -3
  4. package/dist/backend/runtime/terminal-submit.js +1 -0
  5. package/dist/backend/server.js +14 -4
  6. package/dist/backend/services/app-settings-service.js +3 -2
  7. package/dist/backend/services/auto-memory-service.js +4 -3
  8. package/dist/backend/services/claude-hook-service.js +6 -0
  9. package/dist/backend/services/claude-transcript-service.js +2 -13
  10. package/dist/backend/services/claude-version-check.js +46 -0
  11. package/dist/backend/services/gate-review-service.js +47 -8
  12. package/dist/backend/services/harness-service.js +41 -20
  13. package/dist/backend/services/message-service.js +30 -6
  14. package/dist/backend/services/project-service.js +8 -2
  15. package/dist/backend/services/runtime-recovery-service.js +36 -9
  16. package/dist/backend/services/session-service.js +10 -19
  17. package/dist/backend/services/task-close-service.js +1 -0
  18. package/dist/backend/services/terminal-process-exit-service.js +4 -1
  19. package/dist/backend/services/workflow-control-service.js +55 -11
  20. package/dist/backend/templates/harness/architect-worker-frontmatter.js +33 -0
  21. package/dist/backend/templates/harness/check-scaffold-ledger.js +480 -511
  22. package/dist/backend/templates/harness/gate-review.js +676 -643
  23. package/dist/backend/templates/harness/node-tool-runtime.js +102 -0
  24. package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +49 -55
  25. package/dist/backend/templates/harness/restart-architect-skill.js +49 -58
  26. package/dist/backend/templates/harness/vcm-ask-user-skill.js +57 -59
  27. package/dist/backend/templates/harness/vcm-task-state-skill.js +66 -72
  28. package/dist/shared/types/app-settings.js +31 -0
  29. package/dist-frontend/assets/index-CShYYQQa.css +32 -0
  30. package/dist-frontend/assets/{index-DqANomt1.js → index-DOvvS76Q.js} +41 -41
  31. package/dist-frontend/index.html +2 -2
  32. package/package.json +1 -1
  33. package/scripts/harness-tools/check-durable-docs +332 -298
  34. package/scripts/harness-tools/generate-module-index +1047 -512
  35. package/scripts/harness-tools/generate-public-surface +1231 -1069
  36. package/scripts/harness-tools/package.json +3 -0
  37. package/scripts/harness-tools/run-long-check +608 -531
  38. package/scripts/harness-tools/vcm-artifact +181 -113
  39. package/scripts/harness-tools/vcm-bash-guard +839 -605
  40. package/scripts/harness-tools/vcm-subagent-guard +67 -63
  41. package/scripts/harness-tools/watch-job +313 -238
  42. package/dist-frontend/assets/index-xgiQeUD5.css +0 -32
@@ -1,298 +1,332 @@
1
- #!/usr/bin/env python3
2
- import argparse
3
- import json
4
- import re
5
- import subprocess
6
- import sys
7
- from dataclasses import asdict, dataclass
8
- from pathlib import Path
9
-
10
-
11
- EXCLUDED_PARTS = {".git", ".claude", ".ai", "target", "node_modules", "dist", "build"}
12
- KNOWN_ISSUE_FIELDS = (
13
- "status",
14
- "category",
15
- "affected modules/surfaces",
16
- "current gap",
17
- "impact",
18
- "mitigation or workaround",
19
- "resolution condition",
20
- "related issues",
21
- )
22
- TERMINAL_PLAN_WORDS = ("complete", "completed", "shipped", "delivered", "closed", "done", "superseded", "obsolete")
23
-
24
-
25
- @dataclass(frozen=True)
26
- class Finding:
27
- code: str
28
- path: str
29
- line: int
30
- message: str
31
-
32
-
33
- def parse_args() -> argparse.Namespace:
34
- parser = argparse.ArgumentParser(description="Check VCM durable project documentation contracts.")
35
- parser.add_argument("--json", action="store_true", dest="json_output", help="Write machine-readable output.")
36
- return parser.parse_args()
37
-
38
-
39
- def repository_root() -> Path:
40
- try:
41
- result = subprocess.run(
42
- ["git", "rev-parse", "--show-toplevel"],
43
- check=False,
44
- text=True,
45
- stdout=subprocess.PIPE,
46
- stderr=subprocess.DEVNULL,
47
- )
48
- except FileNotFoundError:
49
- return Path.cwd().resolve()
50
- if result.returncode == 0 and result.stdout.strip():
51
- return Path(result.stdout.strip()).resolve()
52
- return Path.cwd().resolve()
53
-
54
-
55
- def relative(path: Path, root: Path) -> str:
56
- return path.resolve().relative_to(root).as_posix()
57
-
58
-
59
- def read_text(path: Path) -> str:
60
- return path.read_text(encoding="utf-8")
61
-
62
-
63
- def markdown_headings(lines: list[str], pattern: re.Pattern[str]) -> list[tuple[int, re.Match[str]]]:
64
- headings: list[tuple[int, re.Match[str]]] = []
65
- fenced = False
66
- for index, line in enumerate(lines):
67
- if re.match(r"^\s*(```|~~~)", line):
68
- fenced = not fenced
69
- continue
70
- if fenced:
71
- continue
72
- match = pattern.match(line)
73
- if match:
74
- headings.append((index, match))
75
- return headings
76
-
77
-
78
- def first_match_line(lines: list[str], pattern: re.Pattern[str], start: int = 0, end: int | None = None) -> int | None:
79
- stop = len(lines) if end is None else min(end, len(lines))
80
- for index in range(start, stop):
81
- if pattern.search(lines[index]):
82
- return index + 1
83
- return None
84
-
85
-
86
- def check_known_issues(root: Path, findings: list[Finding]) -> None:
87
- path = root / "docs/known-issues.md"
88
- if not path.is_file():
89
- return
90
- lines = read_text(path).splitlines()
91
- heading_pattern = re.compile(r"^##\s+(?:\[)?KI-(\d+)(?:\])?\b", re.IGNORECASE)
92
- headings = markdown_headings(lines, heading_pattern)
93
- for heading_index, (start, match) in enumerate(headings):
94
- end = headings[heading_index + 1][0] if heading_index + 1 < len(headings) else len(lines)
95
- body = "\n".join(lines[start + 1:end])
96
- issue_id = f"KI-{match.group(1)}"
97
- fields: dict[str, str] = {}
98
- for line in lines[start + 1:end]:
99
- field_match = re.match(r"^\s*-\s*([^:]+):\s*(.*)$", line)
100
- if field_match:
101
- fields[field_match.group(1).strip().lower()] = field_match.group(2).strip()
102
- missing = [field for field in KNOWN_ISSUE_FIELDS if field not in fields]
103
- if missing:
104
- findings.append(Finding(
105
- "DD_KI_FIELDS",
106
- relative(path, root),
107
- start + 1,
108
- f"{issue_id} is missing required current-state fields: {', '.join(missing)}.",
109
- ))
110
- status = fields.get("status", "").lower()
111
- if status and status not in {"open", "planned", "accepted"}:
112
- findings.append(Finding(
113
- "DD_KI_STATUS",
114
- relative(path, root),
115
- start + 1,
116
- f"{issue_id} has non-current status '{fields['status']}'. Remove terminal issues instead of retaining history.",
117
- ))
118
- resolved_line = first_match_line(
119
- lines,
120
- re.compile(r"(?:\bRESOLVED\b|\bFIXED\b|\bCLOSED\b|\bOBSOLETE\b|nothing remains open)", re.IGNORECASE),
121
- start,
122
- end,
123
- )
124
- if resolved_line is not None:
125
- findings.append(Finding(
126
- "DD_KI_RESOLVED_HISTORY",
127
- relative(path, root),
128
- resolved_line,
129
- f"{issue_id} retains resolved or terminal history. Rewrite the entry around only the current unresolved gap.",
130
- ))
131
- provenance_line = first_match_line(
132
- lines,
133
- re.compile(r"\bROUND-?\d+\b|\b(?:reviewer|tester)[ -]?(?:confirmed|approved|proven)|\bcommit\s+`?[0-9a-f]{7,40}\b", re.IGNORECASE),
134
- start,
135
- end,
136
- )
137
- if provenance_line is not None:
138
- findings.append(Finding(
139
- "DD_KI_TASK_HISTORY",
140
- relative(path, root),
141
- provenance_line,
142
- f"{issue_id} contains task or validation provenance that belongs in Git, PR, or task evidence.",
143
- ))
144
-
145
-
146
- def check_active_plans(root: Path, findings: list[Finding]) -> None:
147
- plans_root = root / "docs/plans"
148
- if not plans_root.is_dir():
149
- return
150
- terminal_pattern = re.compile(r"\b(?:" + "|".join(TERMINAL_PLAN_WORDS) + r")\b", re.IGNORECASE)
151
- status_pattern = re.compile(r"^\s*(?:>\s*)?(?:\*\*)?(?:status|stability)(?:\*\*)?\s*[:\-\u2014]", re.IGNORECASE)
152
- closed_doc_pattern = re.compile(r"\b(?:plan|task|document|doc|phase|work)\b.{0,40}\b(?:is\s+)?(?:closed|complete|completed|shipped|delivered|done)\b", re.IGNORECASE)
153
- for path in sorted(plans_root.rglob("*.md")):
154
- lines = read_text(path).splitlines()
155
- for index, line in enumerate(lines[:60]):
156
- if (status_pattern.search(line) and terminal_pattern.search(line)) or (index < 15 and closed_doc_pattern.search(line)):
157
- findings.append(Finding(
158
- "DD_ACTIVE_PLAN_TERMINAL",
159
- relative(path, root),
160
- index + 1,
161
- "Completed or terminal plans must leave the active plans collection; Git and PR history preserve execution history.",
162
- ))
163
- break
164
-
165
-
166
- def architecture_docs(root: Path) -> list[Path]:
167
- paths: list[Path] = []
168
- project_doc = root / "docs/ARCHITECTURE.md"
169
- if project_doc.is_file():
170
- paths.append(project_doc)
171
- for path in root.rglob("ARCHITECTURE.md"):
172
- if path == project_doc or any(part in EXCLUDED_PARTS for part in path.relative_to(root).parts):
173
- continue
174
- paths.append(path)
175
- return sorted(set(paths))
176
-
177
-
178
- def check_architecture_history(root: Path, findings: list[Finding]) -> None:
179
- patterns = (
180
- ("DD_ARCH_TASK_LABEL", re.compile(r"\bSCF-[A-Za-z0-9.-]+\b|\bROUND-?\d+\b", re.IGNORECASE), "task labels"),
181
- ("DD_ARCH_COMMIT_HISTORY", re.compile(r"\bcommit\s+`?[0-9a-f]{7,40}\b", re.IGNORECASE), "commit history"),
182
- ("DD_ARCH_VALIDATION_HISTORY", re.compile(r"\b(?:reviewer|tester).{0,40}(?:PASS|confirmed|approved|proven)\b", re.IGNORECASE), "role validation history"),
183
- )
184
- for path in architecture_docs(root):
185
- lines = read_text(path).splitlines()
186
- for code, pattern, label in patterns:
187
- line = first_match_line(lines, pattern)
188
- if line is not None:
189
- findings.append(Finding(
190
- code,
191
- relative(path, root),
192
- line,
193
- f"Architecture documentation contains {label}; keep only current architecture and use Git, PR, or task evidence for history.",
194
- ))
195
-
196
-
197
- def check_testing_history(root: Path, findings: list[Finding]) -> None:
198
- path = root / "docs/TESTING.md"
199
- if not path.is_file():
200
- return
201
- lines = read_text(path).splitlines()
202
- patterns = (
203
- ("DD_TEST_TASK_HISTORY", re.compile(r"\bROUND-?\d+\b|\bcommit\s+`?[0-9a-f]{7,40}\b", re.IGNORECASE), "task or commit history"),
204
- ("DD_TEST_VERDICT_HISTORY", re.compile(r"\b(?:reviewer|tester)[ -]?(?:confirmed|approved|proven)|\bL[0-4]\s+(?:PASS|FAIL)\b", re.IGNORECASE), "past validation verdicts"),
205
- ("DD_TEST_LEGACY_OWNER", re.compile(r"\breviewer-owned\b|\brun by the reviewer\b", re.IGNORECASE), "legacy Reviewer ownership"),
206
- )
207
- for code, pattern, label in patterns:
208
- line = first_match_line(lines, pattern)
209
- if line is not None:
210
- findings.append(Finding(
211
- code,
212
- relative(path, root),
213
- line,
214
- f"Testing documentation contains {label}; keep current strategy, runnable commands, case definitions, and current gaps only.",
215
- ))
216
-
217
-
218
- def load_module_index(root: Path) -> dict | None:
219
- path = root / ".ai/generated/module-index.json"
220
- if not path.is_file():
221
- return None
222
- try:
223
- value = json.loads(read_text(path))
224
- except (json.JSONDecodeError, OSError):
225
- return None
226
- return value if isinstance(value, dict) else None
227
-
228
-
229
- def check_generated_architecture_facts(root: Path, findings: list[Finding]) -> None:
230
- index = load_module_index(root)
231
- if index is None:
232
- return
233
- layers = index.get("layers")
234
- if not isinstance(layers, list):
235
- return
236
- layer_counts: dict[str, int] = {}
237
- for layer in layers:
238
- if not isinstance(layer, dict) or not isinstance(layer.get("name"), str) or not isinstance(layer.get("modules"), list):
239
- continue
240
- name = layer["name"]
241
- modules = layer["modules"]
242
- layer_counts[name] = len(modules)
243
- for module in modules:
244
- if not isinstance(module, dict):
245
- continue
246
- architecture_doc = module.get("architectureDoc")
247
- if isinstance(architecture_doc, str) and architecture_doc and not (root / architecture_doc).is_file():
248
- findings.append(Finding(
249
- "DD_MODULE_ARCH_MISSING",
250
- architecture_doc,
251
- 1,
252
- f"Module index names a missing architecture document for module '{module.get('name', 'unknown')}'.",
253
- ))
254
- project_doc = root / "docs/ARCHITECTURE.md"
255
- if not project_doc.is_file():
256
- return
257
- row_pattern = re.compile(r"^\|\s*`?([^`|]+)`?\s*\|\s*(\d+)\s*\|")
258
- for index_number, line in enumerate(read_text(project_doc).splitlines()):
259
- match = row_pattern.match(line)
260
- if not match:
261
- continue
262
- name = match.group(1).strip()
263
- if name not in layer_counts:
264
- continue
265
- documented = int(match.group(2))
266
- generated = layer_counts[name]
267
- if documented != generated:
268
- findings.append(Finding(
269
- "DD_ARCH_GENERATED_COUNT",
270
- relative(project_doc, root),
271
- index_number + 1,
272
- f"Layer '{name}' documents {documented} modules, but .ai/generated/module-index.json contains {generated}.",
273
- ))
274
-
275
-
276
- def main() -> int:
277
- args = parse_args()
278
- root = repository_root()
279
- findings: list[Finding] = []
280
- check_known_issues(root, findings)
281
- check_active_plans(root, findings)
282
- check_architecture_history(root, findings)
283
- check_testing_history(root, findings)
284
- check_generated_architecture_facts(root, findings)
285
- findings.sort(key=lambda item: (item.path, item.line, item.code))
286
- if args.json_output:
287
- print(json.dumps({"ok": not findings, "findings": [asdict(item) for item in findings]}, indent=2))
288
- elif findings:
289
- for item in findings:
290
- print(f"{item.path}:{item.line}: [{item.code}] {item.message}")
291
- print(f"Durable docs audit failed with {len(findings)} finding(s).")
292
- else:
293
- print("Durable docs audit passed.")
294
- return 1 if findings else 0
295
-
296
-
297
- if __name__ == "__main__":
298
- sys.exit(main())
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // Check VCM durable project documentation contracts.
4
+
5
+ const childProcess = require("node:child_process");
6
+ const fs = require("node:fs");
7
+ const path = require("node:path");
8
+
9
+ const EXCLUDED_PARTS = new Set([".git", ".claude", ".ai", "target", "node_modules", "dist", "build"]);
10
+ const KNOWN_ISSUE_FIELDS = [
11
+ "status",
12
+ "category",
13
+ "affected modules/surfaces",
14
+ "current gap",
15
+ "impact",
16
+ "mitigation or workaround",
17
+ "resolution condition",
18
+ "related issues"
19
+ ];
20
+ const TERMINAL_PLAN_WORDS = ["complete", "completed", "shipped", "delivered", "closed", "done", "superseded", "obsolete"];
21
+
22
+ function parseArgs(argv) {
23
+ const args = { jsonOutput: false };
24
+ for (const argument of argv) {
25
+ if (argument === "--json") {
26
+ args.jsonOutput = true;
27
+ } else if (argument === "-h" || argument === "--help") {
28
+ process.stdout.write("usage: check-durable-docs [-h] [--json]\n\nCheck VCM durable project documentation contracts.\n");
29
+ process.exit(0);
30
+ } else {
31
+ process.stderr.write("usage: check-durable-docs [-h] [--json]\ncheck-durable-docs: error: unrecognized arguments: " + argument + "\n");
32
+ process.exit(2);
33
+ }
34
+ }
35
+ return args;
36
+ }
37
+
38
+ function repositoryRoot() {
39
+ const result = childProcess.spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" });
40
+ if (!result.error && result.status === 0 && result.stdout.trim()) {
41
+ return fs.realpathSync(result.stdout.trim());
42
+ }
43
+ return fs.realpathSync(process.cwd());
44
+ }
45
+
46
+ function relative(target, root) {
47
+ return path.relative(root, fs.realpathSync(target)).split(path.sep).join("/");
48
+ }
49
+
50
+ function readLines(target) {
51
+ return fs.readFileSync(target, "utf8").split(/\r\n|\n|\r/).filter((line, index, all) => index < all.length - 1 || line !== "");
52
+ }
53
+
54
+ function isFile(target) {
55
+ try {
56
+ return fs.statSync(target).isFile();
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ function markdownHeadings(lines, pattern) {
63
+ const headings = [];
64
+ let fenced = false;
65
+ lines.forEach((line, index) => {
66
+ if (/^\s*(```|~~~)/.test(line)) {
67
+ fenced = !fenced;
68
+ return;
69
+ }
70
+ if (fenced) {
71
+ return;
72
+ }
73
+ const match = pattern.exec(line);
74
+ if (match) {
75
+ headings.push({ index, match });
76
+ }
77
+ });
78
+ return headings;
79
+ }
80
+
81
+ function firstMatchLine(lines, pattern, start = 0, end = undefined) {
82
+ const stop = end === undefined ? lines.length : Math.min(end, lines.length);
83
+ for (let index = start; index < stop; index += 1) {
84
+ if (pattern.test(lines[index])) {
85
+ return index + 1;
86
+ }
87
+ }
88
+ return null;
89
+ }
90
+
91
+ function finding(code, file, line, message) {
92
+ return { code, path: file, line, message };
93
+ }
94
+
95
+ function checkKnownIssues(root, findings) {
96
+ const file = path.join(root, "docs/known-issues.md");
97
+ if (!isFile(file)) {
98
+ return;
99
+ }
100
+ const lines = readLines(file);
101
+ const headings = markdownHeadings(lines, /^##\s+(?:\[)?KI-(\d+)(?:\])?\b/i);
102
+ headings.forEach(({ index: start, match }, headingIndex) => {
103
+ const end = headingIndex + 1 < headings.length ? headings[headingIndex + 1].index : lines.length;
104
+ const issueId = "KI-" + match[1];
105
+ const fields = {};
106
+ for (const line of lines.slice(start + 1, end)) {
107
+ const fieldMatch = /^\s*-\s*([^:]+):\s*(.*)$/.exec(line);
108
+ if (fieldMatch) {
109
+ fields[fieldMatch[1].trim().toLowerCase()] = fieldMatch[2].trim();
110
+ }
111
+ }
112
+ const missing = KNOWN_ISSUE_FIELDS.filter((field) => !(field in fields));
113
+ if (missing.length > 0) {
114
+ findings.push(finding("DD_KI_FIELDS", relative(file, root), start + 1,
115
+ issueId + " is missing required current-state fields: " + missing.join(", ") + "."));
116
+ }
117
+ const status = (fields.status || "").toLowerCase();
118
+ if (status && !["open", "planned", "accepted"].includes(status)) {
119
+ findings.push(finding("DD_KI_STATUS", relative(file, root), start + 1,
120
+ issueId + " has non-current status '" + fields.status + "'. Remove terminal issues instead of retaining history."));
121
+ }
122
+ const resolvedLine = firstMatchLine(lines, /(?:\bRESOLVED\b|\bFIXED\b|\bCLOSED\b|\bOBSOLETE\b|nothing remains open)/i, start, end);
123
+ if (resolvedLine !== null) {
124
+ findings.push(finding("DD_KI_RESOLVED_HISTORY", relative(file, root), resolvedLine,
125
+ issueId + " retains resolved or terminal history. Rewrite the entry around only the current unresolved gap."));
126
+ }
127
+ const provenanceLine = firstMatchLine(
128
+ lines,
129
+ /\bROUND-?\d+\b|\b(?:reviewer|tester)[ -]?(?:confirmed|approved|proven)|\bcommit\s+`?[0-9a-f]{7,40}\b/i,
130
+ start,
131
+ end
132
+ );
133
+ if (provenanceLine !== null) {
134
+ findings.push(finding("DD_KI_TASK_HISTORY", relative(file, root), provenanceLine,
135
+ issueId + " contains task or validation provenance that belongs in Git, PR, or task evidence."));
136
+ }
137
+ });
138
+ }
139
+
140
+ function walkFiles(directory, accept, results = []) {
141
+ let entries;
142
+ try {
143
+ entries = fs.readdirSync(directory, { withFileTypes: true });
144
+ } catch {
145
+ return results;
146
+ }
147
+ for (const entry of entries) {
148
+ const target = path.join(directory, entry.name);
149
+ if (entry.isDirectory()) {
150
+ walkFiles(target, accept, results);
151
+ } else if (entry.isFile() && accept(entry.name)) {
152
+ results.push(target);
153
+ }
154
+ }
155
+ return results;
156
+ }
157
+
158
+ function checkActivePlans(root, findings) {
159
+ const plansRoot = path.join(root, "docs/plans");
160
+ try {
161
+ if (!fs.statSync(plansRoot).isDirectory()) {
162
+ return;
163
+ }
164
+ } catch {
165
+ return;
166
+ }
167
+ const terminalPattern = new RegExp("\\b(?:" + TERMINAL_PLAN_WORDS.join("|") + ")\\b", "i");
168
+ const statusPattern = /^\s*(?:>\s*)?(?:\*\*)?(?:status|stability)(?:\*\*)?\s*[:\-—]/i;
169
+ const closedDocPattern = /\b(?:plan|task|document|doc|phase|work)\b.{0,40}\b(?:is\s+)?(?:closed|complete|completed|shipped|delivered|done)\b/i;
170
+ for (const file of walkFiles(plansRoot, (name) => name.endsWith(".md")).sort()) {
171
+ const lines = readLines(file);
172
+ for (const [index, line] of lines.slice(0, 60).entries()) {
173
+ if ((statusPattern.test(line) && terminalPattern.test(line)) || (index < 15 && closedDocPattern.test(line))) {
174
+ findings.push(finding("DD_ACTIVE_PLAN_TERMINAL", relative(file, root), index + 1,
175
+ "Completed or terminal plans must leave the active plans collection; Git and PR history preserve execution history."));
176
+ break;
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ function architectureDocs(root) {
183
+ const paths = new Set();
184
+ const projectDoc = path.join(root, "docs/ARCHITECTURE.md");
185
+ if (isFile(projectDoc)) {
186
+ paths.add(projectDoc);
187
+ }
188
+ const visit = (directory) => {
189
+ let entries;
190
+ try {
191
+ entries = fs.readdirSync(directory, { withFileTypes: true });
192
+ } catch {
193
+ return;
194
+ }
195
+ for (const entry of entries) {
196
+ if (EXCLUDED_PARTS.has(entry.name)) {
197
+ continue;
198
+ }
199
+ const target = path.join(directory, entry.name);
200
+ if (entry.isDirectory()) {
201
+ visit(target);
202
+ } else if (entry.isFile() && entry.name === "ARCHITECTURE.md") {
203
+ paths.add(target);
204
+ }
205
+ }
206
+ };
207
+ visit(root);
208
+ return [...paths].sort();
209
+ }
210
+
211
+ function checkArchitectureHistory(root, findings) {
212
+ const patterns = [
213
+ ["DD_ARCH_TASK_LABEL", /\bSCF-[A-Za-z0-9.-]+\b|\bROUND-?\d+\b/i, "task labels"],
214
+ ["DD_ARCH_COMMIT_HISTORY", /\bcommit\s+`?[0-9a-f]{7,40}\b/i, "commit history"],
215
+ ["DD_ARCH_VALIDATION_HISTORY", /\b(?:reviewer|tester).{0,40}(?:PASS|confirmed|approved|proven)\b/i, "role validation history"]
216
+ ];
217
+ for (const file of architectureDocs(root)) {
218
+ const lines = readLines(file);
219
+ for (const [code, pattern, label] of patterns) {
220
+ const line = firstMatchLine(lines, pattern);
221
+ if (line !== null) {
222
+ findings.push(finding(code, relative(file, root), line,
223
+ "Architecture documentation contains " + label + "; keep only current architecture and use Git, PR, or task evidence for history."));
224
+ }
225
+ }
226
+ }
227
+ }
228
+
229
+ function checkTestingHistory(root, findings) {
230
+ const file = path.join(root, "docs/TESTING.md");
231
+ if (!isFile(file)) {
232
+ return;
233
+ }
234
+ const lines = readLines(file);
235
+ const patterns = [
236
+ ["DD_TEST_TASK_HISTORY", /\bROUND-?\d+\b|\bcommit\s+`?[0-9a-f]{7,40}\b/i, "task or commit history"],
237
+ ["DD_TEST_VERDICT_HISTORY", /\b(?:reviewer|tester)[ -]?(?:confirmed|approved|proven)|\bL[0-4]\s+(?:PASS|FAIL)\b/i, "past validation verdicts"],
238
+ ["DD_TEST_LEGACY_OWNER", /\breviewer-owned\b|\brun by the reviewer\b/i, "legacy Reviewer ownership"]
239
+ ];
240
+ for (const [code, pattern, label] of patterns) {
241
+ const line = firstMatchLine(lines, pattern);
242
+ if (line !== null) {
243
+ findings.push(finding(code, relative(file, root), line,
244
+ "Testing documentation contains " + label + "; keep current strategy, runnable commands, case definitions, and current gaps only."));
245
+ }
246
+ }
247
+ }
248
+
249
+ function loadModuleIndex(root) {
250
+ const file = path.join(root, ".ai/generated/module-index.json");
251
+ if (!isFile(file)) {
252
+ return null;
253
+ }
254
+ try {
255
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
256
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
257
+ } catch {
258
+ return null;
259
+ }
260
+ }
261
+
262
+ function checkGeneratedArchitectureFacts(root, findings) {
263
+ const index = loadModuleIndex(root);
264
+ if (index === null || !Array.isArray(index.layers)) {
265
+ return;
266
+ }
267
+ const layerCounts = new Map();
268
+ for (const layer of index.layers) {
269
+ if (!layer || typeof layer !== "object" || typeof layer.name !== "string" || !Array.isArray(layer.modules)) {
270
+ continue;
271
+ }
272
+ layerCounts.set(layer.name, layer.modules.length);
273
+ for (const module of layer.modules) {
274
+ if (!module || typeof module !== "object") {
275
+ continue;
276
+ }
277
+ const architectureDoc = module.architectureDoc;
278
+ if (typeof architectureDoc === "string" && architectureDoc && !isFile(path.join(root, architectureDoc))) {
279
+ findings.push(finding("DD_MODULE_ARCH_MISSING", architectureDoc, 1,
280
+ "Module index names a missing architecture document for module '" + (module.name ?? "unknown") + "'."));
281
+ }
282
+ }
283
+ }
284
+ const projectDoc = path.join(root, "docs/ARCHITECTURE.md");
285
+ if (!isFile(projectDoc)) {
286
+ return;
287
+ }
288
+ readLines(projectDoc).forEach((line, lineIndex) => {
289
+ const match = /^\|\s*`?([^`|]+)`?\s*\|\s*(\d+)\s*\|/.exec(line);
290
+ if (!match) {
291
+ return;
292
+ }
293
+ const name = match[1].trim();
294
+ if (!layerCounts.has(name)) {
295
+ return;
296
+ }
297
+ const documented = Number(match[2]);
298
+ const generated = layerCounts.get(name);
299
+ if (documented !== generated) {
300
+ findings.push(finding("DD_ARCH_GENERATED_COUNT", relative(projectDoc, root), lineIndex + 1,
301
+ "Layer '" + name + "' documents " + documented + " modules, but .ai/generated/module-index.json contains " + generated + "."));
302
+ }
303
+ });
304
+ }
305
+
306
+ function main(argv) {
307
+ const args = parseArgs(argv);
308
+ const root = repositoryRoot();
309
+ const findings = [];
310
+ checkKnownIssues(root, findings);
311
+ checkActivePlans(root, findings);
312
+ checkArchitectureHistory(root, findings);
313
+ checkTestingHistory(root, findings);
314
+ checkGeneratedArchitectureFacts(root, findings);
315
+ findings.sort((left, right) =>
316
+ left.path < right.path ? -1 : left.path > right.path ? 1
317
+ : left.line !== right.line ? left.line - right.line
318
+ : left.code < right.code ? -1 : left.code > right.code ? 1 : 0);
319
+ if (args.jsonOutput) {
320
+ process.stdout.write(JSON.stringify({ ok: findings.length === 0, findings }, null, 2) + "\n");
321
+ } else if (findings.length > 0) {
322
+ for (const item of findings) {
323
+ process.stdout.write(item.path + ":" + item.line + ": [" + item.code + "] " + item.message + "\n");
324
+ }
325
+ process.stdout.write("Durable docs audit failed with " + findings.length + " finding(s).\n");
326
+ } else {
327
+ process.stdout.write("Durable docs audit passed.\n");
328
+ }
329
+ return findings.length > 0 ? 1 : 0;
330
+ }
331
+
332
+ process.exitCode = main(process.argv.slice(2));