sentinelayer-cli 0.6.2 → 0.8.1

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 (280) hide show
  1. package/README.md +1009 -996
  2. package/bin/create-sentinelayer.js +5 -5
  3. package/bin/sentinelayer-cli.js +4 -4
  4. package/bin/sl.js +5 -5
  5. package/package.json +64 -63
  6. package/src/agents/ai-governance/index.js +12 -0
  7. package/src/agents/ai-governance/tools/base.js +171 -0
  8. package/src/agents/ai-governance/tools/eval-regression.js +47 -0
  9. package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
  10. package/src/agents/ai-governance/tools/index.js +52 -0
  11. package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
  12. package/src/agents/ai-governance/tools/provenance-check.js +69 -0
  13. package/src/agents/backend/index.js +12 -0
  14. package/src/agents/backend/tools/base.js +189 -0
  15. package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
  16. package/src/agents/backend/tools/idempotency-audit.js +105 -0
  17. package/src/agents/backend/tools/index.js +87 -0
  18. package/src/agents/backend/tools/retry-audit.js +132 -0
  19. package/src/agents/backend/tools/timeout-audit.js +144 -0
  20. package/src/agents/code-quality/index.js +12 -0
  21. package/src/agents/code-quality/tools/base.js +159 -0
  22. package/src/agents/code-quality/tools/complexity-measure.js +197 -0
  23. package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
  24. package/src/agents/code-quality/tools/cycle-detect.js +49 -0
  25. package/src/agents/code-quality/tools/dep-graph.js +196 -0
  26. package/src/agents/code-quality/tools/index.js +89 -0
  27. package/src/agents/data-layer/index.js +12 -0
  28. package/src/agents/data-layer/tools/base.js +181 -0
  29. package/src/agents/data-layer/tools/index-audit.js +165 -0
  30. package/src/agents/data-layer/tools/index.js +83 -0
  31. package/src/agents/data-layer/tools/migration-scan.js +135 -0
  32. package/src/agents/data-layer/tools/query-explain.js +120 -0
  33. package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
  34. package/src/agents/documentation/index.js +12 -0
  35. package/src/agents/documentation/tools/api-diff.js +91 -0
  36. package/src/agents/documentation/tools/base.js +151 -0
  37. package/src/agents/documentation/tools/dead-link-check.js +58 -0
  38. package/src/agents/documentation/tools/docstring-coverage.js +78 -0
  39. package/src/agents/documentation/tools/index.js +52 -0
  40. package/src/agents/documentation/tools/readme-freshness.js +61 -0
  41. package/src/agents/envelope/fix-cycle.js +45 -0
  42. package/src/agents/envelope/index.js +31 -0
  43. package/src/agents/envelope/loop.js +150 -0
  44. package/src/agents/envelope/pulse.js +18 -0
  45. package/src/agents/envelope/stream.js +40 -0
  46. package/src/agents/infrastructure/index.js +12 -0
  47. package/src/agents/infrastructure/tools/base.js +171 -0
  48. package/src/agents/infrastructure/tools/checkov-run.js +32 -0
  49. package/src/agents/infrastructure/tools/drift-detect.js +59 -0
  50. package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
  51. package/src/agents/infrastructure/tools/index.js +52 -0
  52. package/src/agents/infrastructure/tools/tflint-run.js +31 -0
  53. package/src/agents/jules/config/definition.js +160 -160
  54. package/src/agents/jules/config/system-prompt.js +182 -182
  55. package/src/agents/jules/error-intake.js +51 -51
  56. package/src/agents/jules/fix-cycle.js +17 -17
  57. package/src/agents/jules/loop.js +460 -450
  58. package/src/agents/jules/pulse.js +10 -10
  59. package/src/agents/jules/stream.js +187 -186
  60. package/src/agents/jules/swarm/file-scanner.js +74 -74
  61. package/src/agents/jules/swarm/index.js +11 -11
  62. package/src/agents/jules/swarm/orchestrator.js +362 -362
  63. package/src/agents/jules/swarm/pattern-hunter.js +123 -123
  64. package/src/agents/jules/swarm/sub-agent.js +315 -309
  65. package/src/agents/jules/tools/aidenid-email.js +189 -189
  66. package/src/agents/jules/tools/auth-audit.js +1708 -1691
  67. package/src/agents/jules/tools/dispatch.js +340 -335
  68. package/src/agents/jules/tools/file-edit.js +2 -2
  69. package/src/agents/jules/tools/file-read.js +2 -2
  70. package/src/agents/jules/tools/frontend-analyze.js +570 -570
  71. package/src/agents/jules/tools/glob.js +2 -2
  72. package/src/agents/jules/tools/grep.js +2 -2
  73. package/src/agents/jules/tools/index.js +29 -29
  74. package/src/agents/jules/tools/path-guards.js +2 -2
  75. package/src/agents/jules/tools/runtime-audit.js +507 -507
  76. package/src/agents/jules/tools/shell.js +2 -2
  77. package/src/agents/jules/tools/url-policy.js +100 -100
  78. package/src/agents/mode.js +113 -0
  79. package/src/agents/observability/index.js +12 -0
  80. package/src/agents/observability/tools/alert-audit.js +39 -0
  81. package/src/agents/observability/tools/base.js +181 -0
  82. package/src/agents/observability/tools/dashboard-gap.js +42 -0
  83. package/src/agents/observability/tools/index.js +54 -0
  84. package/src/agents/observability/tools/log-schema-check.js +74 -0
  85. package/src/agents/observability/tools/span-coverage.js +74 -0
  86. package/src/agents/persona-visuals.js +102 -61
  87. package/src/agents/release/index.js +12 -0
  88. package/src/agents/release/tools/base.js +181 -0
  89. package/src/agents/release/tools/changelog-diff.js +86 -0
  90. package/src/agents/release/tools/feature-flag-audit.js +126 -0
  91. package/src/agents/release/tools/index.js +61 -0
  92. package/src/agents/release/tools/rollback-verify.js +129 -0
  93. package/src/agents/release/tools/semver-check.js +109 -0
  94. package/src/agents/reliability/index.js +12 -0
  95. package/src/agents/reliability/tools/backpressure-check.js +129 -0
  96. package/src/agents/reliability/tools/base.js +181 -0
  97. package/src/agents/reliability/tools/chaos-probe.js +109 -0
  98. package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
  99. package/src/agents/reliability/tools/health-check-audit.js +111 -0
  100. package/src/agents/reliability/tools/index.js +87 -0
  101. package/src/agents/run-persona.js +109 -0
  102. package/src/agents/security/index.js +12 -0
  103. package/src/agents/security/tools/authz-audit.js +134 -0
  104. package/src/agents/security/tools/base.js +190 -0
  105. package/src/agents/security/tools/crypto-review.js +175 -0
  106. package/src/agents/security/tools/index.js +97 -0
  107. package/src/agents/security/tools/sast-scan.js +175 -0
  108. package/src/agents/security/tools/secrets-scan.js +216 -0
  109. package/src/agents/shared-tools/dispatch-core.js +320 -315
  110. package/src/agents/shared-tools/file-edit.js +180 -180
  111. package/src/agents/shared-tools/file-read.js +100 -100
  112. package/src/agents/shared-tools/glob.js +168 -168
  113. package/src/agents/shared-tools/grep.js +228 -228
  114. package/src/agents/shared-tools/index.js +46 -46
  115. package/src/agents/shared-tools/path-guards.js +161 -161
  116. package/src/agents/shared-tools/shell.js +383 -383
  117. package/src/agents/supply-chain/index.js +12 -0
  118. package/src/agents/supply-chain/tools/attestation-check.js +42 -0
  119. package/src/agents/supply-chain/tools/base.js +151 -0
  120. package/src/agents/supply-chain/tools/index.js +52 -0
  121. package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
  122. package/src/agents/supply-chain/tools/package-verify.js +56 -0
  123. package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
  124. package/src/agents/testing/index.js +12 -0
  125. package/src/agents/testing/tools/base.js +202 -0
  126. package/src/agents/testing/tools/coverage-gap.js +144 -0
  127. package/src/agents/testing/tools/flake-detect.js +125 -0
  128. package/src/agents/testing/tools/index.js +85 -0
  129. package/src/agents/testing/tools/mutation-test.js +143 -0
  130. package/src/agents/testing/tools/snapshot-diff.js +103 -0
  131. package/src/ai/aidenid.js +1021 -1009
  132. package/src/ai/client.js +553 -553
  133. package/src/ai/domain-target-store.js +268 -268
  134. package/src/ai/identity-store.js +270 -270
  135. package/src/ai/proxy.js +137 -137
  136. package/src/ai/site-store.js +145 -145
  137. package/src/audit/agents/architecture.js +180 -180
  138. package/src/audit/agents/compliance.js +179 -179
  139. package/src/audit/agents/documentation.js +165 -165
  140. package/src/audit/agents/performance.js +145 -145
  141. package/src/audit/agents/security.js +215 -215
  142. package/src/audit/agents/testing.js +172 -172
  143. package/src/audit/orchestrator.js +557 -557
  144. package/src/audit/package.js +204 -204
  145. package/src/audit/registry.js +284 -284
  146. package/src/audit/replay.js +103 -103
  147. package/src/auth/gate.js +428 -371
  148. package/src/auth/http.js +681 -611
  149. package/src/auth/service.js +1106 -1106
  150. package/src/auth/session-store.js +813 -813
  151. package/src/cli.js +257 -252
  152. package/src/commands/ai/identity-lifecycle.js +1338 -1338
  153. package/src/commands/ai/provision-governance.js +1272 -1272
  154. package/src/commands/ai/shared.js +147 -147
  155. package/src/commands/ai.js +11 -11
  156. package/src/commands/apply.js +12 -12
  157. package/src/commands/audit.js +1171 -1166
  158. package/src/commands/auth.js +419 -419
  159. package/src/commands/chat.js +184 -191
  160. package/src/commands/config.js +184 -184
  161. package/src/commands/cost.js +311 -311
  162. package/src/commands/daemon/core.js +850 -850
  163. package/src/commands/daemon/extended.js +1048 -1048
  164. package/src/commands/daemon/shared.js +213 -213
  165. package/src/commands/daemon.js +11 -11
  166. package/src/commands/guide.js +174 -174
  167. package/src/commands/ingest.js +58 -58
  168. package/src/commands/init.js +55 -55
  169. package/src/commands/legacy-args.js +20 -10
  170. package/src/commands/mcp.js +461 -461
  171. package/src/commands/omargate.js +63 -29
  172. package/src/commands/persona.js +65 -20
  173. package/src/commands/plugin.js +260 -260
  174. package/src/commands/policy.js +132 -132
  175. package/src/commands/prompt.js +238 -238
  176. package/src/commands/review.js +704 -704
  177. package/src/commands/scan.js +865 -872
  178. package/src/commands/session.js +1238 -0
  179. package/src/commands/spec.js +771 -716
  180. package/src/commands/swarm.js +651 -651
  181. package/src/commands/telemetry.js +202 -202
  182. package/src/commands/watch.js +511 -511
  183. package/src/config/agent-dictionary.js +182 -182
  184. package/src/config/io.js +56 -56
  185. package/src/config/paths.js +18 -18
  186. package/src/config/schema.js +55 -55
  187. package/src/config/service.js +184 -184
  188. package/src/coord/events-log.js +141 -0
  189. package/src/coord/handshake.js +719 -0
  190. package/src/coord/index.js +35 -0
  191. package/src/coord/paths.js +84 -0
  192. package/src/coord/priority.js +62 -0
  193. package/src/coord/tarjan.js +157 -0
  194. package/src/cost/budget.js +235 -235
  195. package/src/cost/history.js +188 -188
  196. package/src/cost/tokenizer.js +160 -0
  197. package/src/cost/tracker.js +232 -171
  198. package/src/daemon/artifact-lineage.js +896 -534
  199. package/src/daemon/assignment-ledger.js +1083 -770
  200. package/src/daemon/ast-drift.js +496 -0
  201. package/src/daemon/ast-parser-layer.js +258 -258
  202. package/src/daemon/budget-governor.js +633 -633
  203. package/src/daemon/callgraph-overlay.js +646 -646
  204. package/src/daemon/error-worker.js +1209 -626
  205. package/src/daemon/fix-cycle.js +384 -377
  206. package/src/daemon/hybrid-mapper.js +929 -929
  207. package/src/daemon/ingest-refresh.js +79 -11
  208. package/src/daemon/jira-lifecycle.js +767 -632
  209. package/src/daemon/operator-control.js +657 -657
  210. package/src/daemon/pulse.js +327 -327
  211. package/src/daemon/reliability-lane.js +471 -471
  212. package/src/daemon/scope-engine.js +1068 -0
  213. package/src/daemon/watchdog.js +971 -971
  214. package/src/events/schema.js +190 -0
  215. package/src/guide/generator.js +316 -316
  216. package/src/ingest/engine.js +933 -918
  217. package/src/ingest/ownership.js +380 -0
  218. package/src/interactive/index.js +97 -97
  219. package/src/legacy-cli.js +3228 -2994
  220. package/src/mcp/registry.js +695 -695
  221. package/src/memory/blackboard.js +301 -301
  222. package/src/memory/retrieval.js +581 -581
  223. package/src/orchestrator/kai-chen.js +126 -0
  224. package/src/plugin/manifest.js +553 -553
  225. package/src/policy/packs.js +144 -144
  226. package/src/prompt/generator.js +136 -118
  227. package/src/review/ai-review.js +672 -679
  228. package/src/review/compliance-pack.js +389 -0
  229. package/src/review/investor-dd-config.js +54 -0
  230. package/src/review/investor-dd-file-loop.js +303 -0
  231. package/src/review/investor-dd-file-router.js +406 -0
  232. package/src/review/investor-dd-html-report.js +233 -0
  233. package/src/review/investor-dd-notification.js +120 -0
  234. package/src/review/investor-dd-orchestrator.js +405 -0
  235. package/src/review/investor-dd-persona-runner.js +275 -0
  236. package/src/review/live-validator.js +253 -0
  237. package/src/review/local-review.js +1351 -1305
  238. package/src/review/omargate-interactive.js +68 -68
  239. package/src/review/omargate-orchestrator.js +492 -300
  240. package/src/review/persona-prompts.js +484 -296
  241. package/src/review/reconciliation-rules.js +329 -0
  242. package/src/review/replay.js +235 -235
  243. package/src/review/report.js +664 -664
  244. package/src/review/reproducibility-chain.js +136 -0
  245. package/src/review/scan-modes.js +147 -42
  246. package/src/review/spec-binding.js +487 -487
  247. package/src/scaffold/generator.js +67 -67
  248. package/src/scaffold/templates.js +150 -150
  249. package/src/scan/generator.js +418 -418
  250. package/src/scan/gh-secrets.js +107 -107
  251. package/src/session/agent-registry.js +359 -0
  252. package/src/session/analytics.js +479 -0
  253. package/src/session/daemon.js +1396 -0
  254. package/src/session/file-locks.js +666 -0
  255. package/src/session/paths.js +37 -0
  256. package/src/session/recap.js +567 -0
  257. package/src/session/redact.js +82 -0
  258. package/src/session/runtime-bridge.js +762 -0
  259. package/src/session/scoring.js +406 -0
  260. package/src/session/setup-guides.js +304 -0
  261. package/src/session/store.js +704 -0
  262. package/src/session/stream.js +333 -0
  263. package/src/session/sync.js +753 -0
  264. package/src/session/tasks.js +1054 -0
  265. package/src/session/templates.js +188 -0
  266. package/src/spec/generator.js +619 -519
  267. package/src/spec/regenerate.js +237 -237
  268. package/src/spec/templates.js +91 -91
  269. package/src/swarm/dashboard.js +247 -247
  270. package/src/swarm/factory.js +363 -363
  271. package/src/swarm/pentest.js +934 -934
  272. package/src/swarm/registry.js +419 -419
  273. package/src/swarm/report.js +158 -158
  274. package/src/swarm/runtime.js +569 -576
  275. package/src/swarm/scenario-dsl.js +272 -272
  276. package/src/telemetry/ledger.js +302 -302
  277. package/src/telemetry/session-tracker.js +234 -234
  278. package/src/telemetry/sync.js +203 -203
  279. package/src/ui/command-hints.js +13 -13
  280. package/src/ui/markdown.js +220 -220
@@ -0,0 +1,144 @@
1
+ // coverage-gap — find source files without a matching test file (#A15).
2
+ //
3
+ // Zero-dep static pass: we don't try to read c8 / istanbul coverage JSON
4
+ // (that lives in a later PR). Instead we use filename-convention matching —
5
+ // for every `src/foo/bar.ts`, check whether any of the standard test file
6
+ // names exists. Misses catches the most valuable 80% of coverage gaps while
7
+ // staying fast and self-contained.
8
+
9
+ import path from "node:path";
10
+
11
+ import { createFinding, isTestFile, toPosix, walkRepoFiles } from "./base.js";
12
+
13
+ const SOURCE_EXTENSIONS = new Set([
14
+ ".js",
15
+ ".jsx",
16
+ ".ts",
17
+ ".tsx",
18
+ ".mjs",
19
+ ".cjs",
20
+ ".py",
21
+ ]);
22
+
23
+ // Generate plausible test-file locations for a source file. For
24
+ // src/foo/bar.ts, try tests/foo/bar.test.ts, src/foo/bar.test.ts,
25
+ // __tests__/foo/bar.test.ts, tests/foo/test_bar.py (Python), …
26
+ function candidateTestPaths(sourceRelativePath) {
27
+ const posix = toPosix(sourceRelativePath);
28
+ const ext = path.extname(posix).toLowerCase();
29
+ const base = posix.slice(0, posix.length - ext.length);
30
+ const fileName = path.posix.basename(base);
31
+ const dir = path.posix.dirname(base);
32
+ const candidates = new Set();
33
+
34
+ if (ext === ".py") {
35
+ candidates.add(`${dir}/${fileName}_test.py`);
36
+ candidates.add(`${dir}/test_${fileName}.py`);
37
+ candidates.add(`tests/${dir}/${fileName}_test.py`);
38
+ candidates.add(`tests/${dir}/test_${fileName}.py`);
39
+ } else {
40
+ const testExts = [ext, `.test${ext}`];
41
+ for (const testExt of testExts) {
42
+ candidates.add(`${base}.test${ext}`);
43
+ candidates.add(`${base}.spec${ext}`);
44
+ candidates.add(`${dir}/__tests__/${fileName}.test${ext}`);
45
+ candidates.add(`${dir}/__tests__/${fileName}${ext}`);
46
+ candidates.add(`tests/${base}.test${ext}`);
47
+ candidates.add(`tests/${dir}/${fileName}.test${ext}`);
48
+ candidates.add(`test/${dir}/${fileName}.test${ext}`);
49
+ candidates.add(`test/${dir}/${fileName}.spec${ext}`);
50
+ // mjs test convention: tests/unit.{name}.test.mjs
51
+ candidates.add(`tests/unit.${fileName}.test.mjs`);
52
+ candidates.add(`tests/unit.${fileName}.test.js`);
53
+ }
54
+ }
55
+ return candidates;
56
+ }
57
+
58
+ function isLikelyEntryFile(relativePath) {
59
+ const p = toPosix(relativePath);
60
+ return (
61
+ /(^|\/)(index|main)\.[jt]sx?$/.test(p) ||
62
+ /(^|\/)(bin|scripts)\//.test(p) ||
63
+ /(^|\/)cli\.[jt]s$/.test(p)
64
+ );
65
+ }
66
+
67
+ function isLikelyConfig(relativePath) {
68
+ const p = toPosix(relativePath);
69
+ return (
70
+ /(^|\/)(config|constants|types?|schema|\.d\.ts)(\.[jt]sx?)?$/.test(p) ||
71
+ /\.d\.ts$/.test(p)
72
+ );
73
+ }
74
+
75
+ export async function runCoverageGap({ rootPath, files = null } = {}) {
76
+ const resolvedRoot = path.resolve(String(rootPath || "."));
77
+
78
+ // Pass 1: walk the repo once, collect source + test file lists.
79
+ const sourceFiles = [];
80
+ const testFiles = new Set();
81
+ const iterator =
82
+ Array.isArray(files) && files.length > 0
83
+ ? iterateExplicitFiles(resolvedRoot, files)
84
+ : walkRepoFiles({ rootPath: resolvedRoot, extensions: SOURCE_EXTENSIONS });
85
+
86
+ for await (const { relativePath } of iterator) {
87
+ if (isTestFile(relativePath)) {
88
+ testFiles.add(toPosix(relativePath));
89
+ continue;
90
+ }
91
+ sourceFiles.push(toPosix(relativePath));
92
+ }
93
+
94
+ const findings = [];
95
+ for (const source of sourceFiles) {
96
+ if (isLikelyEntryFile(source) || isLikelyConfig(source)) {
97
+ continue;
98
+ }
99
+ const candidates = candidateTestPaths(source);
100
+ const covered = Array.from(testFiles).some((test) => {
101
+ for (const candidate of candidates) {
102
+ if (test === candidate || test.endsWith(`/${path.posix.basename(candidate)}`)) {
103
+ return true;
104
+ }
105
+ }
106
+ return false;
107
+ });
108
+ if (covered) {
109
+ continue;
110
+ }
111
+ findings.push(
112
+ createFinding({
113
+ tool: "coverage-gap",
114
+ kind: "testing.coverage-gap",
115
+ severity: "P2",
116
+ file: source,
117
+ line: 1,
118
+ evidence: `No test file found for source: ${source}`,
119
+ rootCause:
120
+ "Source file has no corresponding test under standard naming conventions (`*.test.*`, `*.spec.*`, `test_*.py`, `__tests__/…`).",
121
+ recommendedFix:
122
+ "Add a unit test covering the file's exports, or add an explicit `.notest` marker / coverage-ignore annotation if this file is intentionally untested.",
123
+ confidence: 0.6,
124
+ })
125
+ );
126
+ }
127
+ return findings;
128
+ }
129
+
130
+ async function* iterateExplicitFiles(resolvedRoot, files) {
131
+ for (const file of files) {
132
+ const trimmed = String(file || "").trim();
133
+ if (!trimmed) {
134
+ continue;
135
+ }
136
+ const fullPath = path.isAbsolute(trimmed)
137
+ ? trimmed
138
+ : path.join(resolvedRoot, trimmed);
139
+ const relativePath = path
140
+ .relative(resolvedRoot, fullPath)
141
+ .replace(/\\/g, "/");
142
+ yield { fullPath, relativePath };
143
+ }
144
+ }
@@ -0,0 +1,125 @@
1
+ // flake-detect — flag flakiness smells in test files (#A15).
2
+ //
3
+ // The usual suspects: tests that sleep, rely on wall-clock arithmetic, hit
4
+ // the real network, or seed randomness without a fixed seed. We scan test
5
+ // files specifically (the coverage-gap heuristic for "is this a test") so
6
+ // the tool doesn't flag production code that legitimately uses setTimeout.
7
+
8
+ import fsp from "node:fs/promises";
9
+ import path from "node:path";
10
+
11
+ import { createFinding, findLineMatches, getLineContent, isTestFile, toPosix, walkRepoFiles } from "./base.js";
12
+
13
+ const TEST_EXTENSIONS = new Set([
14
+ ".js",
15
+ ".jsx",
16
+ ".ts",
17
+ ".tsx",
18
+ ".mjs",
19
+ ".cjs",
20
+ ".py",
21
+ ]);
22
+
23
+ const RULES = [
24
+ {
25
+ id: "flake.sleep-in-test",
26
+ // setTimeout(fn, 500) or sleep(2) in a test file — schedule-based flake
27
+ pattern: /\b(?:setTimeout|setInterval|sleep|time\.sleep|asyncio\.sleep)\s*\(\s*(?:\w+\s*,\s*)?\d{3,}\s*[,)]/,
28
+ severity: "P2",
29
+ rootCause:
30
+ "Test sleeps for a fixed wall-clock duration — slow on CI, flaky on loaded machines.",
31
+ recommendedFix:
32
+ "Use fake timers (jest.useFakeTimers, vi.useFakeTimers, freezegun) or event-based waits (await page.waitForSelector / waitForResponse).",
33
+ confidence: 0.7,
34
+ },
35
+ {
36
+ id: "flake.wall-clock-assertion",
37
+ pattern: /expect\s*\(\s*(?:Date\.now\(\)|new\s+Date\(\)\.getTime\(\))\s*\)/,
38
+ severity: "P1",
39
+ rootCause:
40
+ "Assertion compares against the live wall clock — value drifts between runs.",
41
+ recommendedFix:
42
+ "Freeze time (jest.setSystemTime, vi.setSystemTime, freezegun) or pass a Date supplier the SUT reads from.",
43
+ confidence: 0.8,
44
+ },
45
+ {
46
+ id: "flake.unstubbed-network",
47
+ // fetch / axios / requests in a test file — likely reaching out to real
48
+ // network. Real-network hits are the #1 flake source.
49
+ pattern: /\b(?:fetch|axios(?:\.[a-z]+)?|got(?:\.[a-z]+)?|requests\.(?:get|post|put|patch|delete|request))\s*\(/,
50
+ severity: "P1",
51
+ rootCause:
52
+ "Test makes a live network call. Real-network tests flake on DNS / TLS / rate limits and make CI unreliable.",
53
+ recommendedFix:
54
+ "Mock the client with msw / nock / vcr-py, or inject an HTTP transport and pass a fake in tests.",
55
+ confidence: 0.65,
56
+ },
57
+ {
58
+ id: "flake.unseeded-random",
59
+ pattern: /\b(?:Math\.random|random\.(?:random|uniform|shuffle|choice))\s*\(/,
60
+ severity: "P2",
61
+ rootCause:
62
+ "Test uses unseeded randomness — two runs can take different branches and produce different results.",
63
+ recommendedFix:
64
+ "Seed the generator or pass a stub random() into the SUT via DI. For Jest / Vitest you can mock Math.random.",
65
+ confidence: 0.55,
66
+ },
67
+ ];
68
+
69
+ export async function runFlakeDetect({ rootPath, files = null } = {}) {
70
+ const resolvedRoot = path.resolve(String(rootPath || "."));
71
+ const iterator =
72
+ Array.isArray(files) && files.length > 0
73
+ ? iterateExplicitFiles(resolvedRoot, files)
74
+ : walkRepoFiles({ rootPath: resolvedRoot, extensions: TEST_EXTENSIONS });
75
+
76
+ const findings = [];
77
+ for await (const { fullPath, relativePath } of iterator) {
78
+ const relPos = toPosix(relativePath);
79
+ if (!isTestFile(relPos)) {
80
+ continue;
81
+ }
82
+ let content;
83
+ try {
84
+ content = await fsp.readFile(fullPath, "utf-8");
85
+ } catch {
86
+ continue;
87
+ }
88
+ for (const rule of RULES) {
89
+ for (const match of findLineMatches(content, rule.pattern)) {
90
+ findings.push(
91
+ createFinding({
92
+ tool: "flake-detect",
93
+ kind: rule.id,
94
+ severity: rule.severity,
95
+ file: relPos,
96
+ line: match.line,
97
+ evidence: getLineContent(content, match.line),
98
+ rootCause: rule.rootCause,
99
+ recommendedFix: rule.recommendedFix,
100
+ confidence: rule.confidence,
101
+ })
102
+ );
103
+ }
104
+ }
105
+ }
106
+ return findings;
107
+ }
108
+
109
+ async function* iterateExplicitFiles(resolvedRoot, files) {
110
+ for (const file of files) {
111
+ const trimmed = String(file || "").trim();
112
+ if (!trimmed) {
113
+ continue;
114
+ }
115
+ const fullPath = path.isAbsolute(trimmed)
116
+ ? trimmed
117
+ : path.join(resolvedRoot, trimmed);
118
+ const relativePath = path
119
+ .relative(resolvedRoot, fullPath)
120
+ .replace(/\\/g, "/");
121
+ yield { fullPath, relativePath };
122
+ }
123
+ }
124
+
125
+ export { RULES as FLAKE_RULES };
@@ -0,0 +1,85 @@
1
+ // Priya (testing persona) domain-tool registry (#A15).
2
+
3
+ import { runCoverageGap } from "./coverage-gap.js";
4
+ import { runFlakeDetect } from "./flake-detect.js";
5
+ import { runMutationTest } from "./mutation-test.js";
6
+ import { runSnapshotDiff } from "./snapshot-diff.js";
7
+
8
+ export const TESTING_TOOLS = Object.freeze({
9
+ "coverage-gap": {
10
+ id: "coverage-gap",
11
+ description:
12
+ "Walk the repo and flag source files that have no matching test file under standard naming conventions (*.test.*, *.spec.*, test_*.py, __tests__/…).",
13
+ schema: {
14
+ type: "object",
15
+ properties: {
16
+ rootPath: { type: "string" },
17
+ files: { type: "array", items: { type: "string" } },
18
+ },
19
+ },
20
+ handler: runCoverageGap,
21
+ },
22
+ "flake-detect": {
23
+ id: "flake-detect",
24
+ description:
25
+ "Scan test files for flakiness smells: fixed-duration sleeps, wall-clock assertions, live network calls (fetch / axios / requests), unseeded randomness.",
26
+ schema: {
27
+ type: "object",
28
+ properties: {
29
+ rootPath: { type: "string" },
30
+ files: { type: "array", items: { type: "string" } },
31
+ },
32
+ },
33
+ handler: runFlakeDetect,
34
+ },
35
+ "snapshot-diff": {
36
+ id: "snapshot-diff",
37
+ description:
38
+ "Walk *.snap / *.ambr files and flag stale (> 90 days untouched) or oversized (> 64 KiB) snapshots.",
39
+ schema: {
40
+ type: "object",
41
+ properties: {
42
+ rootPath: { type: "string" },
43
+ staleDays: { type: "number" },
44
+ files: { type: "array", items: { type: "string" } },
45
+ },
46
+ },
47
+ handler: runSnapshotDiff,
48
+ },
49
+ "mutation-test": {
50
+ id: "mutation-test",
51
+ description:
52
+ "Configuration-check pass: verify Stryker / mutmut is wired up and the latest mutation report is fresh (< 30 days).",
53
+ schema: {
54
+ type: "object",
55
+ properties: { rootPath: { type: "string" } },
56
+ },
57
+ handler: runMutationTest,
58
+ },
59
+ });
60
+
61
+ export const TESTING_TOOL_IDS = Object.freeze(Object.keys(TESTING_TOOLS));
62
+
63
+ export async function dispatchTestingTool(toolId, args = {}) {
64
+ const tool = TESTING_TOOLS[toolId];
65
+ if (!tool) {
66
+ throw new Error(`Unknown testing tool: ${toolId}`);
67
+ }
68
+ return tool.handler(args);
69
+ }
70
+
71
+ export async function runAllTestingTools({ rootPath, files = null } = {}) {
72
+ const findings = [];
73
+ for (const toolId of TESTING_TOOL_IDS) {
74
+ const out = await dispatchTestingTool(toolId, { rootPath, files });
75
+ findings.push(...out);
76
+ }
77
+ return findings;
78
+ }
79
+
80
+ export {
81
+ runCoverageGap,
82
+ runFlakeDetect,
83
+ runMutationTest,
84
+ runSnapshotDiff,
85
+ };
@@ -0,0 +1,143 @@
1
+ // mutation-test — check for mutation-testing configuration (#A15).
2
+ //
3
+ // Priya wants mutation testing (Stryker / pitest / mutmut) as the ceiling
4
+ // signal: do the tests actually assert anything, or is coverage a green
5
+ // but empty number? True mutation runs are expensive — this tool ships as
6
+ // a configuration check first (is Stryker wired up? is there an up-to-date
7
+ // report?). The LLM / operator can dispatch a real run from the resulting
8
+ // advisory.
9
+
10
+ import fsp from "node:fs/promises";
11
+ import path from "node:path";
12
+
13
+ import { createFinding, toPosix } from "./base.js";
14
+
15
+ const CONFIG_CANDIDATES = [
16
+ "stryker.conf.js",
17
+ "stryker.conf.cjs",
18
+ "stryker.conf.mjs",
19
+ "stryker.config.json",
20
+ ".stryker-tmp",
21
+ "setup.cfg", // Python mutmut section
22
+ "mutmut_config.py",
23
+ "pyproject.toml", // check for [tool.mutmut]
24
+ ];
25
+
26
+ const REPORT_CANDIDATES = [
27
+ "reports/mutation/mutation.html",
28
+ "reports/mutation/mutation.json",
29
+ "mutmut_results.json",
30
+ ];
31
+
32
+ const REPORT_FRESH_DAYS = 30;
33
+
34
+ async function fileExists(fullPath) {
35
+ try {
36
+ const stat = await fsp.stat(fullPath);
37
+ return { exists: true, mtimeMs: Number(stat.mtimeMs || 0) };
38
+ } catch {
39
+ return { exists: false };
40
+ }
41
+ }
42
+
43
+ async function readTextIfExists(fullPath) {
44
+ try {
45
+ return await fsp.readFile(fullPath, "utf-8");
46
+ } catch {
47
+ return "";
48
+ }
49
+ }
50
+
51
+ export async function runMutationTest({ rootPath } = {}) {
52
+ const resolvedRoot = path.resolve(String(rootPath || "."));
53
+ const findings = [];
54
+
55
+ // Config presence check
56
+ let configFound = false;
57
+ for (const candidate of CONFIG_CANDIDATES) {
58
+ const fullPath = path.join(resolvedRoot, candidate);
59
+ const result = await fileExists(fullPath);
60
+ if (result.exists) {
61
+ if (candidate === "pyproject.toml" || candidate === "setup.cfg") {
62
+ const text = await readTextIfExists(fullPath);
63
+ if (!/\[tool\.mutmut\]|\[mutmut\]/.test(text)) {
64
+ continue;
65
+ }
66
+ }
67
+ configFound = true;
68
+ break;
69
+ }
70
+ }
71
+
72
+ if (!configFound) {
73
+ findings.push(
74
+ createFinding({
75
+ tool: "mutation-test",
76
+ kind: "testing.no-mutation-config",
77
+ severity: "P3",
78
+ file: toPosix("pyproject.toml"),
79
+ line: 0,
80
+ evidence: "No Stryker / mutmut / pitest configuration file found.",
81
+ rootCause:
82
+ "Without mutation testing, the test suite's assertions could be vacuous — 90% line coverage means nothing if the tests don't fail when the code changes.",
83
+ recommendedFix:
84
+ "Wire up @stryker-mutator/core (JS/TS) or mutmut (Python). Start with a single critical module and let the score guide new tests.",
85
+ confidence: 0.5,
86
+ })
87
+ );
88
+ return findings;
89
+ }
90
+
91
+ // Report freshness check
92
+ let reportFound = false;
93
+ let latestReport = 0;
94
+ for (const candidate of REPORT_CANDIDATES) {
95
+ const fullPath = path.join(resolvedRoot, candidate);
96
+ const result = await fileExists(fullPath);
97
+ if (result.exists) {
98
+ reportFound = true;
99
+ latestReport = Math.max(latestReport, result.mtimeMs);
100
+ }
101
+ }
102
+ if (!reportFound) {
103
+ findings.push(
104
+ createFinding({
105
+ tool: "mutation-test",
106
+ kind: "testing.no-mutation-report",
107
+ severity: "P3",
108
+ file: toPosix("reports/mutation/"),
109
+ line: 0,
110
+ evidence: "Stryker / mutmut config present but no mutation report on disk.",
111
+ rootCause:
112
+ "Config without a report suggests mutation testing is configured but not actually run.",
113
+ recommendedFix:
114
+ "Wire a mutation run into CI on a cadence (weekly is reasonable) so drift in assertion quality is visible.",
115
+ confidence: 0.55,
116
+ })
117
+ );
118
+ return findings;
119
+ }
120
+
121
+ const ageDays = Math.floor((Date.now() - latestReport) / (24 * 60 * 60 * 1000));
122
+ if (ageDays > REPORT_FRESH_DAYS) {
123
+ findings.push(
124
+ createFinding({
125
+ tool: "mutation-test",
126
+ kind: "testing.mutation-report-stale",
127
+ severity: "P3",
128
+ file: toPosix("reports/mutation/"),
129
+ line: 0,
130
+ evidence: `Latest mutation report is ${ageDays} days old (threshold ${REPORT_FRESH_DAYS})`,
131
+ rootCause:
132
+ "Stale mutation reports mean we're not actually watching assertion quality — drift goes undetected until it matters.",
133
+ recommendedFix:
134
+ "Schedule a recurring mutation job (weekly) and file an issue auto-generated from the diff vs. the prior run.",
135
+ confidence: 0.55,
136
+ })
137
+ );
138
+ }
139
+
140
+ return findings;
141
+ }
142
+
143
+ export { CONFIG_CANDIDATES, REPORT_CANDIDATES, REPORT_FRESH_DAYS };
@@ -0,0 +1,103 @@
1
+ // snapshot-diff — flag stale / oversized / obsolete snapshots (#A15).
2
+ //
3
+ // We walk *.snap files (Jest) and *.raw.snap / *.ambr (Ariadne) and flag:
4
+ // 1. Snapshots that haven't been touched in > STALE_DAYS days — stale
5
+ // values are a legitimate concern.
6
+ // 2. Snapshots larger than LARGE_SIZE_BYTES — huge blobs are an anti-
7
+ // pattern (unreviewable diffs, hide regressions).
8
+ // 3. Python doctest / pytest-snapshot *.ambr files that reference a
9
+ // stored block. Same staleness / size rules.
10
+ //
11
+ // We don't try to diff against the producing code — that's the job of the
12
+ // test runner. We only flag maintenance smells.
13
+
14
+ import path from "node:path";
15
+
16
+ import { createFinding, toPosix, walkRepoFiles } from "./base.js";
17
+
18
+ const SNAPSHOT_EXTENSIONS = new Set([
19
+ ".snap",
20
+ ".ambr",
21
+ ]);
22
+ const STALE_DAYS = 90;
23
+ const LARGE_SIZE_BYTES = 64 * 1024; // 64 KiB
24
+
25
+ export async function runSnapshotDiff({ rootPath, files = null, staleDays = STALE_DAYS } = {}) {
26
+ const resolvedRoot = path.resolve(String(rootPath || "."));
27
+ const now = Date.now();
28
+ const staleThreshold = now - staleDays * 24 * 60 * 60 * 1000;
29
+ const iterator =
30
+ Array.isArray(files) && files.length > 0
31
+ ? iterateExplicitFiles(resolvedRoot, files)
32
+ : walkRepoFiles({ rootPath: resolvedRoot, extensions: SNAPSHOT_EXTENSIONS });
33
+
34
+ const findings = [];
35
+ for await (const { relativePath, stat } of iterator) {
36
+ const mtime = stat ? Number(stat.mtimeMs || 0) : 0;
37
+ const size = stat ? Number(stat.size || 0) : 0;
38
+ const rel = toPosix(relativePath);
39
+
40
+ if (mtime && mtime < staleThreshold) {
41
+ const days = Math.floor((now - mtime) / (24 * 60 * 60 * 1000));
42
+ findings.push(
43
+ createFinding({
44
+ tool: "snapshot-diff",
45
+ kind: "testing.snapshot-stale",
46
+ severity: "P3",
47
+ file: rel,
48
+ line: 0,
49
+ evidence: `Last modified ${days} days ago (threshold ${staleDays})`,
50
+ rootCause:
51
+ "Snapshot has been unchanged for longer than the staleness threshold — a stale snapshot can hide regressions silently.",
52
+ recommendedFix:
53
+ "Re-run the test suite with `--updateSnapshot` (or equivalent) after verifying the current output is actually correct. Delete if the underlying code has been removed.",
54
+ confidence: 0.5,
55
+ })
56
+ );
57
+ }
58
+
59
+ if (size > LARGE_SIZE_BYTES) {
60
+ findings.push(
61
+ createFinding({
62
+ tool: "snapshot-diff",
63
+ kind: "testing.snapshot-oversized",
64
+ severity: "P2",
65
+ file: rel,
66
+ line: 0,
67
+ evidence: `Snapshot is ${Math.round(size / 1024)} KiB (threshold ${Math.round(LARGE_SIZE_BYTES / 1024)} KiB)`,
68
+ rootCause:
69
+ "Oversized snapshots are unreviewable in PRs and hide meaningful regressions inside unrelated noise.",
70
+ recommendedFix:
71
+ "Split the snapshot into smaller focused tests, switch to a structural assertion, or mask non-essential fields (timestamps, IDs) before snapshotting.",
72
+ confidence: 0.7,
73
+ })
74
+ );
75
+ }
76
+ }
77
+ return findings;
78
+ }
79
+
80
+ async function* iterateExplicitFiles(resolvedRoot, files) {
81
+ const fsp = await import("node:fs/promises");
82
+ for (const file of files) {
83
+ const trimmed = String(file || "").trim();
84
+ if (!trimmed) {
85
+ continue;
86
+ }
87
+ const fullPath = path.isAbsolute(trimmed)
88
+ ? trimmed
89
+ : path.join(resolvedRoot, trimmed);
90
+ const relativePath = path
91
+ .relative(resolvedRoot, fullPath)
92
+ .replace(/\\/g, "/");
93
+ let stat = null;
94
+ try {
95
+ stat = await fsp.stat(fullPath);
96
+ } catch {
97
+ stat = null;
98
+ }
99
+ yield { fullPath, relativePath, stat };
100
+ }
101
+ }
102
+
103
+ export { LARGE_SIZE_BYTES, STALE_DAYS };