@ai-sdlc/orchestrator 0.5.0 → 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 (227) hide show
  1. package/dist/action-enforcement.d.ts +26 -0
  2. package/dist/action-enforcement.js +70 -0
  3. package/dist/admission-composite.d.ts +67 -0
  4. package/dist/admission-composite.js +158 -0
  5. package/dist/admission-enrichment.d.ts +142 -0
  6. package/dist/admission-enrichment.js +331 -0
  7. package/dist/admission-hc.d.ts +62 -0
  8. package/dist/admission-hc.js +83 -0
  9. package/dist/admission-score.d.ts +148 -0
  10. package/dist/admission-score.js +237 -0
  11. package/dist/analysis/analyzer.js +3 -2
  12. package/dist/analysis/convention-detector.d.ts +85 -2
  13. package/dist/analysis/convention-detector.js +375 -70
  14. package/dist/analysis/diff-analyzer.d.ts +33 -0
  15. package/dist/analysis/diff-analyzer.js +122 -0
  16. package/dist/analysis/hotspot-analyzer.js +3 -1
  17. package/dist/analysis/index.d.ts +2 -1
  18. package/dist/analysis/index.js +2 -1
  19. package/dist/artifacts/index.d.ts +65 -0
  20. package/dist/artifacts/index.js +142 -0
  21. package/dist/backlog-adapter.d.ts +121 -0
  22. package/dist/backlog-adapter.js +438 -0
  23. package/dist/calibration.d.ts +43 -0
  24. package/dist/calibration.js +76 -0
  25. package/dist/cli/commands/agents.d.ts +24 -0
  26. package/dist/cli/commands/agents.js +66 -1
  27. package/dist/cli/commands/git-remote.d.ts +49 -0
  28. package/dist/cli/commands/git-remote.js +91 -0
  29. package/dist/cli/commands/health.d.ts +4 -0
  30. package/dist/cli/commands/health.js +13 -1
  31. package/dist/cli/commands/init.d.ts +26 -0
  32. package/dist/cli/commands/init.js +164 -22
  33. package/dist/cli/commands/mcp-setup.d.ts +31 -0
  34. package/dist/cli/commands/mcp-setup.js +78 -8
  35. package/dist/cli/formatters/table.js +15 -2
  36. package/dist/cli/index.d.ts +14 -1
  37. package/dist/cli/index.js +81 -20
  38. package/dist/cli/versions.d.ts +57 -0
  39. package/dist/cli/versions.js +128 -0
  40. package/dist/code-area-classifier.d.ts +21 -0
  41. package/dist/code-area-classifier.js +48 -0
  42. package/dist/config.d.ts +33 -1
  43. package/dist/config.js +78 -8
  44. package/dist/cycle-utils.d.ts +51 -0
  45. package/dist/cycle-utils.js +77 -0
  46. package/dist/database/adapters/external.d.ts +24 -0
  47. package/dist/database/adapters/external.js +80 -0
  48. package/dist/database/adapters/neon.d.ts +41 -0
  49. package/dist/database/adapters/neon.js +98 -0
  50. package/dist/database/adapters/pg-snapshot-restore.d.ts +28 -0
  51. package/dist/database/adapters/pg-snapshot-restore.js +68 -0
  52. package/dist/database/adapters/sqlite-copy.d.ts +32 -0
  53. package/dist/database/adapters/sqlite-copy.js +145 -0
  54. package/dist/database/connection-injection.d.ts +35 -0
  55. package/dist/database/connection-injection.js +93 -0
  56. package/dist/database/index.d.ts +12 -0
  57. package/dist/database/index.js +23 -0
  58. package/dist/database/registry.d.ts +13 -0
  59. package/dist/database/registry.js +27 -0
  60. package/dist/database/topology.d.ts +52 -0
  61. package/dist/database/topology.js +44 -0
  62. package/dist/database/types.d.ts +89 -0
  63. package/dist/database/types.js +26 -0
  64. package/dist/defaults.d.ts +5 -0
  65. package/dist/defaults.js +5 -0
  66. package/dist/design-authority.d.ts +40 -0
  67. package/dist/design-authority.js +71 -0
  68. package/dist/design-lookahead.d.ts +64 -0
  69. package/dist/design-lookahead.js +86 -0
  70. package/dist/design-quality-trend.d.ts +87 -0
  71. package/dist/design-quality-trend.js +190 -0
  72. package/dist/design-system-context.d.ts +46 -0
  73. package/dist/design-system-context.js +80 -0
  74. package/dist/design-system-correction-loop.d.ts +64 -0
  75. package/dist/design-system-correction-loop.js +128 -0
  76. package/dist/design-system-metrics.d.ts +61 -0
  77. package/dist/design-system-metrics.js +104 -0
  78. package/dist/design-system-stewardship.d.ts +22 -0
  79. package/dist/design-system-stewardship.js +85 -0
  80. package/dist/design-system-validation.d.ts +37 -0
  81. package/dist/design-system-validation.js +88 -0
  82. package/dist/dispatch/index.d.ts +4 -0
  83. package/dist/dispatch/index.js +4 -0
  84. package/dist/dispatch/merge-gate.d.ts +46 -0
  85. package/dist/dispatch/merge-gate.js +90 -0
  86. package/dist/dispatch/requeue.d.ts +57 -0
  87. package/dist/dispatch/requeue.js +131 -0
  88. package/dist/dispatch/worker-pool.d.ts +62 -0
  89. package/dist/dispatch/worker-pool.js +60 -0
  90. package/dist/execute.d.ts +40 -0
  91. package/dist/execute.js +295 -30
  92. package/dist/fix-ci.js +40 -3
  93. package/dist/fix-review.d.ts +66 -0
  94. package/dist/fix-review.js +448 -0
  95. package/dist/harness/adapters/claude-code.d.ts +29 -0
  96. package/dist/harness/adapters/claude-code.js +191 -0
  97. package/dist/harness/adapters/codex.d.ts +25 -0
  98. package/dist/harness/adapters/codex.js +61 -0
  99. package/dist/harness/independence.d.ts +51 -0
  100. package/dist/harness/independence.js +67 -0
  101. package/dist/harness/index.d.ts +14 -0
  102. package/dist/harness/index.js +20 -0
  103. package/dist/harness/registry.d.ts +17 -0
  104. package/dist/harness/registry.js +31 -0
  105. package/dist/harness/types.d.ts +123 -0
  106. package/dist/harness/types.js +8 -0
  107. package/dist/harness/version-probe.d.ts +14 -0
  108. package/dist/harness/version-probe.js +123 -0
  109. package/dist/index.d.ts +39 -5
  110. package/dist/index.js +48 -3
  111. package/dist/models/classifier.d.ts +76 -0
  112. package/dist/models/classifier.js +221 -0
  113. package/dist/models/index.d.ts +3 -0
  114. package/dist/models/index.js +3 -0
  115. package/dist/models/registry.d.ts +97 -0
  116. package/dist/models/registry.js +173 -0
  117. package/dist/pillar-breakdown.d.ts +63 -0
  118. package/dist/pillar-breakdown.js +153 -0
  119. package/dist/pipeline-cycle-detector.d.ts +70 -0
  120. package/dist/pipeline-cycle-detector.js +111 -0
  121. package/dist/priority.d.ts +7 -76
  122. package/dist/priority.js +18 -5
  123. package/dist/review-meta.d.ts +65 -0
  124. package/dist/review-meta.js +149 -0
  125. package/dist/review.d.ts +35 -0
  126. package/dist/review.js +84 -0
  127. package/dist/runners/claude-code-sdk.d.ts +31 -0
  128. package/dist/runners/claude-code-sdk.js +238 -0
  129. package/dist/runners/claude-code.d.ts +3 -0
  130. package/dist/runners/claude-code.js +319 -73
  131. package/dist/runners/codex.js +4 -1
  132. package/dist/runners/copilot.js +4 -1
  133. package/dist/runners/cursor.js +4 -1
  134. package/dist/runners/git-utils.d.ts +81 -0
  135. package/dist/runners/git-utils.js +201 -0
  136. package/dist/runners/index.d.ts +5 -1
  137. package/dist/runners/index.js +4 -0
  138. package/dist/runners/review-agent.d.ts +67 -0
  139. package/dist/runners/review-agent.js +350 -0
  140. package/dist/runners/runner-registry.js +10 -0
  141. package/dist/runners/sdk-review-runner.d.ts +65 -0
  142. package/dist/runners/sdk-review-runner.js +185 -0
  143. package/dist/runners/security-triage.d.ts +20 -4
  144. package/dist/runners/security-triage.js +42 -14
  145. package/dist/runners/types.d.ts +25 -0
  146. package/dist/runtime/attestations.d.ts +274 -0
  147. package/dist/runtime/attestations.js +460 -0
  148. package/dist/runtime/git-env.d.ts +53 -0
  149. package/dist/runtime/git-env.js +60 -0
  150. package/dist/runtime/index.d.ts +7 -0
  151. package/dist/runtime/index.js +7 -0
  152. package/dist/runtime/parallelism-flag.d.ts +10 -0
  153. package/dist/runtime/parallelism-flag.js +18 -0
  154. package/dist/runtime/port-allocator.d.ts +21 -0
  155. package/dist/runtime/port-allocator.js +66 -0
  156. package/dist/runtime/worktree-pool.d.ts +86 -0
  157. package/dist/runtime/worktree-pool.js +204 -0
  158. package/dist/runtime/worktree.d.ts +25 -0
  159. package/dist/runtime/worktree.js +111 -0
  160. package/dist/sa-scoring/auto-calibrate.d.ts +69 -0
  161. package/dist/sa-scoring/auto-calibrate.js +107 -0
  162. package/dist/sa-scoring/c1-sa2-computable.d.ts +42 -0
  163. package/dist/sa-scoring/c1-sa2-computable.js +59 -0
  164. package/dist/sa-scoring/composite.d.ts +107 -0
  165. package/dist/sa-scoring/composite.js +139 -0
  166. package/dist/sa-scoring/depparse-client.d.ts +79 -0
  167. package/dist/sa-scoring/depparse-client.js +187 -0
  168. package/dist/sa-scoring/did-compiler.d.ts +122 -0
  169. package/dist/sa-scoring/did-compiler.js +286 -0
  170. package/dist/sa-scoring/drift-monitor.d.ts +84 -0
  171. package/dist/sa-scoring/drift-monitor.js +186 -0
  172. package/dist/sa-scoring/exemplar-bank.d.ts +78 -0
  173. package/dist/sa-scoring/exemplar-bank.js +154 -0
  174. package/dist/sa-scoring/feedback-store.d.ts +100 -0
  175. package/dist/sa-scoring/feedback-store.js +156 -0
  176. package/dist/sa-scoring/index.d.ts +71 -0
  177. package/dist/sa-scoring/index.js +158 -0
  178. package/dist/sa-scoring/layer1-deterministic.d.ts +115 -0
  179. package/dist/sa-scoring/layer1-deterministic.js +298 -0
  180. package/dist/sa-scoring/layer2-structural.d.ts +71 -0
  181. package/dist/sa-scoring/layer2-structural.js +151 -0
  182. package/dist/sa-scoring/layer3-llm.d.ts +86 -0
  183. package/dist/sa-scoring/layer3-llm.js +282 -0
  184. package/dist/sa-scoring/rescore-orchestrator.d.ts +52 -0
  185. package/dist/sa-scoring/rescore-orchestrator.js +47 -0
  186. package/dist/scheduling/burn-down.d.ts +27 -0
  187. package/dist/scheduling/burn-down.js +43 -0
  188. package/dist/scheduling/calibration.d.ts +42 -0
  189. package/dist/scheduling/calibration.js +150 -0
  190. package/dist/scheduling/index.d.ts +8 -0
  191. package/dist/scheduling/index.js +8 -0
  192. package/dist/scheduling/ledger.d.ts +59 -0
  193. package/dist/scheduling/ledger.js +216 -0
  194. package/dist/scheduling/off-peak.d.ts +27 -0
  195. package/dist/scheduling/off-peak.js +112 -0
  196. package/dist/scheduling/schedule-decision.d.ts +39 -0
  197. package/dist/scheduling/schedule-decision.js +89 -0
  198. package/dist/scheduling/tier-analysis.d.ts +47 -0
  199. package/dist/scheduling/tier-analysis.js +81 -0
  200. package/dist/scheduling/types.d.ts +140 -0
  201. package/dist/scheduling/types.js +11 -0
  202. package/dist/shared.d.ts +13 -0
  203. package/dist/shared.js +32 -0
  204. package/dist/state/index.d.ts +1 -1
  205. package/dist/state/schema.d.ts +6 -1
  206. package/dist/state/schema.js +276 -1
  207. package/dist/state/store.d.ts +71 -1
  208. package/dist/state/store.js +464 -7
  209. package/dist/state/types.d.ts +174 -0
  210. package/dist/types.d.ts +1 -1
  211. package/dist/validate-agent-output.js +4 -1
  212. package/dist/watch.js +6 -0
  213. package/dist/workflow-patterns/artifact-writer.d.ts +16 -0
  214. package/dist/workflow-patterns/artifact-writer.js +34 -0
  215. package/dist/workflow-patterns/classifiers.d.ts +10 -0
  216. package/dist/workflow-patterns/classifiers.js +72 -0
  217. package/dist/workflow-patterns/detector.d.ts +27 -0
  218. package/dist/workflow-patterns/detector.js +186 -0
  219. package/dist/workflow-patterns/index.d.ts +8 -0
  220. package/dist/workflow-patterns/index.js +7 -0
  221. package/dist/workflow-patterns/proposal-generator.d.ts +15 -0
  222. package/dist/workflow-patterns/proposal-generator.js +183 -0
  223. package/dist/workflow-patterns/telemetry-ingest.d.ts +27 -0
  224. package/dist/workflow-patterns/telemetry-ingest.js +103 -0
  225. package/dist/workflow-patterns/types.d.ts +61 -0
  226. package/dist/workflow-patterns/types.js +11 -0
  227. package/package.json +2 -2
@@ -1,7 +1,30 @@
1
1
  /**
2
2
  * Detect coding conventions: naming style, test organization, import style.
3
+ *
4
+ * The detector accepts an optional `repoPath` so it can read project-level
5
+ * configuration (`package.json`, `vite.config.{js,ts}`, `tsconfig.json`,
6
+ * `jsconfig.json`, `webpack.config.*`) to interpret the file list more
7
+ * accurately. When `repoPath` is omitted the detector falls back to a pure
8
+ * file-name analysis (the original v1 behaviour).
9
+ *
10
+ * Three classes of false-positives motivated the project-config plumbing
11
+ * (AISDLC-80):
12
+ *
13
+ * 1. React projects mix PascalCase (components) with camelCase (hooks/stores
14
+ * and plain modules) — the v1 detector flagged this as `mixed`. With
15
+ * `package.json` `dependencies.react` visible we treat the dual-pattern
16
+ * case as the expected React convention.
17
+ * 2. Many repos host tests in multiple directories (`tests/`, `tests/e2e/`,
18
+ * `src/tests/`, `cypress/`, ...) plus collocated `*.test.*`. The v1
19
+ * detector picked one bucket and called it `mixed` when the proportions
20
+ * were close. We now enumerate the FULL set of locations.
21
+ * 3. Vite / TS / webpack path aliases (`@components`, `@engine`, ...) were
22
+ * classified as external imports because the v1 detector never read the
23
+ * alias maps. We now parse the alias map from each known config file and
24
+ * report alias usage as its own bucket.
3
25
  */
4
- import { basename, dirname, extname } from 'node:path';
26
+ import { readFile } from 'node:fs/promises';
27
+ import { basename, dirname, extname, join } from 'node:path';
5
28
  // ── Naming style helpers ──────────────────────────────────────────
6
29
  function isCamelCase(name) {
7
30
  return /^[a-z][a-zA-Z0-9]*$/.test(name);
@@ -15,12 +38,80 @@ function isKebabCase(name) {
15
38
  function isSnakeCase(name) {
16
39
  return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
17
40
  }
18
- function detectFileNamingStyle(files) {
41
+ const REACT_COMPONENT_EXTENSIONS = new Set(['.jsx', '.tsx']);
42
+ const REACT_MODULE_EXTENSIONS = new Set(['.js', '.ts']);
43
+ /**
44
+ * Recognised React project structure: PascalCase for `.jsx`/`.tsx` (components)
45
+ * AND camelCase for `.js`/`.ts` (hooks, stores, utilities). At least 60% of
46
+ * each extension family must follow the expected style for the heuristic to
47
+ * fire so an accidentally-mixed repo still gets flagged.
48
+ */
49
+ function detectReactDualNamingPattern(files) {
50
+ let componentTotal = 0;
51
+ let componentPascal = 0;
52
+ let moduleTotal = 0;
53
+ let moduleCamel = 0;
54
+ const componentExamples = [];
55
+ const moduleExamples = [];
56
+ for (const file of files) {
57
+ const name = stripTestSuffix(basename(file.relativePath, extname(file.relativePath)));
58
+ if (!name || name === 'index')
59
+ continue;
60
+ if (REACT_COMPONENT_EXTENSIONS.has(file.extension)) {
61
+ componentTotal++;
62
+ if (isPascalCase(name)) {
63
+ componentPascal++;
64
+ if (componentExamples.length < 3)
65
+ componentExamples.push(file.relativePath);
66
+ }
67
+ }
68
+ else if (REACT_MODULE_EXTENSIONS.has(file.extension)) {
69
+ moduleTotal++;
70
+ // Hooks (`useFoo.js`) and store/util modules (`gameplayStore.js`) are
71
+ // both camelCase under React community convention.
72
+ if (isCamelCase(name)) {
73
+ moduleCamel++;
74
+ if (moduleExamples.length < 3)
75
+ moduleExamples.push(file.relativePath);
76
+ }
77
+ }
78
+ }
79
+ // Need meaningful evidence of BOTH families and at least 60% adherence in each.
80
+ if (componentTotal < 2 || moduleTotal < 2) {
81
+ return { matched: false, componentExamples, moduleExamples };
82
+ }
83
+ const componentRatio = componentPascal / componentTotal;
84
+ const moduleRatio = moduleCamel / moduleTotal;
85
+ return {
86
+ matched: componentRatio >= 0.6 && moduleRatio >= 0.6,
87
+ componentExamples,
88
+ moduleExamples,
89
+ };
90
+ }
91
+ function stripTestSuffix(name) {
92
+ return name.replace(/\.(test|spec|stories|d)$/, '');
93
+ }
94
+ function detectFileNamingStyle(files, reactMode) {
95
+ // React projects get a dual-pattern check first — if it matches we report
96
+ // the friendly "PascalCase + camelCase (React)" label and skip the legacy
97
+ // single-style heuristic that would otherwise (correctly but unhelpfully)
98
+ // call this `mixed`.
99
+ if (reactMode) {
100
+ const dual = detectReactDualNamingPattern(files);
101
+ if (dual.matched) {
102
+ return {
103
+ style: 'PascalCase + camelCase (React)',
104
+ confidence: 0.9,
105
+ examples: [...dual.componentExamples, ...dual.moduleExamples].slice(0, 3),
106
+ };
107
+ }
108
+ }
19
109
  const counts = {
20
110
  camelCase: 0,
21
111
  PascalCase: 0,
22
112
  'kebab-case': 0,
23
113
  snake_case: 0,
114
+ 'PascalCase + camelCase (React)': 0,
24
115
  mixed: 0,
25
116
  };
26
117
  const examples = {
@@ -28,10 +119,11 @@ function detectFileNamingStyle(files) {
28
119
  PascalCase: [],
29
120
  'kebab-case': [],
30
121
  snake_case: [],
122
+ 'PascalCase + camelCase (React)': [],
31
123
  mixed: [],
32
124
  };
33
125
  for (const file of files) {
34
- const name = basename(file.relativePath, extname(file.relativePath)).replace(/\.(test|spec|stories|d)$/, ''); // Strip test/spec suffixes
126
+ const name = stripTestSuffix(basename(file.relativePath, extname(file.relativePath)));
35
127
  if (!name || name === 'index')
36
128
  continue;
37
129
  if (isKebabCase(name)) {
@@ -67,105 +159,318 @@ function detectFileNamingStyle(files) {
67
159
  examples: examples[topStyle],
68
160
  };
69
161
  }
70
- function detectTestingStyle(files) {
71
- let coLocated = 0;
72
- let testsDir = 0;
73
- let testDir = 0;
74
- const examples = {
75
- 'co-located': [],
76
- __tests__: [],
77
- 'test-directory': [],
78
- mixed: [],
79
- };
162
+ const KNOWN_TEST_DIRECTORY_PREFIXES = [
163
+ // Order matters: more specific matchers first so `tests/e2e/foo.test.ts`
164
+ // ends up in `tests/e2e/` rather than `tests/`.
165
+ { label: 'tests/e2e/', matches: (d) => /(^|\/)tests\/e2e(\/|$)/.test(d) },
166
+ { label: 'tests/integration/', matches: (d) => /(^|\/)tests\/integration(\/|$)/.test(d) },
167
+ { label: 'tests/unit/', matches: (d) => /(^|\/)tests\/unit(\/|$)/.test(d) },
168
+ { label: 'src/tests/', matches: (d) => /(^|\/)src\/tests(\/|$)/.test(d) },
169
+ { label: '__tests__/', matches: (d) => /(^|\/)__tests__(\/|$)/.test(d) },
170
+ { label: 'cypress/', matches: (d) => /(^|\/)cypress(\/|$)/.test(d) },
171
+ { label: 'e2e/', matches: (d) => /(^|\/)e2e(\/|$)/.test(d) && !/tests\/e2e/.test(d) },
172
+ { label: 'tests/', matches: (d) => /(^|\/)tests(\/|$)/.test(d) },
173
+ { label: 'test/', matches: (d) => /(^|\/)test(\/|$)/.test(d) && !/__tests__/.test(d) },
174
+ ];
175
+ /**
176
+ * Classify every test file found into ALL the locations it actually lives,
177
+ * preserving the multi-directory reality of real projects. Returns the
178
+ * complete set sorted by file count (descending) with examples for each.
179
+ */
180
+ export function enumerateTestLocations(files) {
181
+ const buckets = new Map();
182
+ function bump(label, path) {
183
+ const existing = buckets.get(label) ?? { count: 0, examples: [] };
184
+ existing.count += 1;
185
+ if (existing.examples.length < 3)
186
+ existing.examples.push(path);
187
+ buckets.set(label, existing);
188
+ }
80
189
  const testFiles = files.filter((f) => {
81
190
  const name = basename(f.relativePath);
82
- return (name.includes('.test.') || name.includes('.spec.') || f.relativePath.includes('__tests__'));
191
+ return (name.includes('.test.') ||
192
+ name.includes('.spec.') ||
193
+ f.relativePath.includes('__tests__') ||
194
+ // Cypress puts integration specs under `cypress/e2e/foo.cy.js` — pick
195
+ // those up too even if they don't carry a `.test.` / `.spec.` suffix.
196
+ /(^|\/)cypress\/.*\.(js|ts|jsx|tsx)$/.test(f.relativePath) ||
197
+ /(^|\/)tests?\/.*\.(js|ts|jsx|tsx|mjs|cjs)$/.test(f.relativePath));
83
198
  });
84
199
  for (const file of testFiles) {
85
200
  const dir = dirname(file.relativePath);
86
- if (dir.includes('__tests__')) {
87
- testsDir++;
88
- if (examples.__tests__.length < 3)
89
- examples.__tests__.push(file.relativePath);
90
- }
91
- else if (dir === 'test' ||
92
- dir === 'tests' ||
93
- dir.startsWith('test/') ||
94
- dir.startsWith('tests/')) {
95
- testDir++;
96
- if (examples['test-directory'].length < 3)
97
- examples['test-directory'].push(file.relativePath);
201
+ let matched = false;
202
+ for (const matcher of KNOWN_TEST_DIRECTORY_PREFIXES) {
203
+ if (matcher.matches(dir)) {
204
+ bump(matcher.label, file.relativePath);
205
+ matched = true;
206
+ break;
207
+ }
98
208
  }
99
- else {
100
- coLocated++;
101
- if (examples['co-located'].length < 3)
102
- examples['co-located'].push(file.relativePath);
209
+ if (!matched) {
210
+ // Anything else — collocated with source.
211
+ bump('co-located', file.relativePath);
103
212
  }
104
213
  }
105
- const total = coLocated + testsDir + testDir;
106
- if (total === 0)
107
- return { style: 'mixed', confidence: 0, examples: [] };
108
- const counts = { 'co-located': coLocated, __tests__: testsDir, 'test-directory': testDir };
109
- const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
110
- const [topStyle, topCount] = sorted[0];
111
- const confidence = topCount / total;
214
+ return [...buckets.entries()]
215
+ .map(([label, info]) => ({ label, count: info.count, examples: info.examples }))
216
+ .sort((a, b) => b.count - a.count);
217
+ }
218
+ function summariseTestLocations(locations) {
219
+ if (locations.length === 0) {
220
+ return { pattern: 'No test files detected', examples: [], confidence: 0 };
221
+ }
222
+ const total = locations.reduce((sum, loc) => sum + loc.count, 0);
223
+ const labelList = locations.map((loc) => loc.label).join(', ');
224
+ // Confidence is the share of test files that fall into the largest bucket
225
+ // — but we still REPORT the full set in `pattern` so users see every place
226
+ // tests actually live.
227
+ const confidence = Math.round((locations[0].count / total) * 100) / 100;
112
228
  return {
113
- style: confidence >= 0.5 ? topStyle : 'mixed',
114
- confidence: Math.round(confidence * 100) / 100,
115
- examples: examples[topStyle],
229
+ pattern: `Test files in: ${labelList}`,
230
+ examples: locations.flatMap((loc) => loc.examples).slice(0, 3),
231
+ confidence,
116
232
  };
117
233
  }
118
- function detectImportStyle(files) {
119
- // Check for barrel index files
234
+ async function readFileSafe(path) {
235
+ try {
236
+ return await readFile(path, 'utf-8');
237
+ }
238
+ catch {
239
+ return null;
240
+ }
241
+ }
242
+ function stripJsonComments(text) {
243
+ // Tolerant pre-pass for `tsconfig.json` / `jsconfig.json` which permit
244
+ // line + block comments and trailing commas. The implementation is
245
+ // intentionally narrow: strings are scanned char-by-char so we don't
246
+ // accidentally strip "//" inside a string literal.
247
+ let out = '';
248
+ let i = 0;
249
+ let inString = null;
250
+ while (i < text.length) {
251
+ const ch = text[i];
252
+ const next = text[i + 1];
253
+ if (inString) {
254
+ out += ch;
255
+ if (ch === '\\' && i + 1 < text.length) {
256
+ out += next;
257
+ i += 2;
258
+ continue;
259
+ }
260
+ if (ch === inString)
261
+ inString = null;
262
+ i++;
263
+ continue;
264
+ }
265
+ if (ch === '"' || ch === "'") {
266
+ inString = ch;
267
+ out += ch;
268
+ i++;
269
+ continue;
270
+ }
271
+ if (ch === '/' && next === '/') {
272
+ while (i < text.length && text[i] !== '\n')
273
+ i++;
274
+ continue;
275
+ }
276
+ if (ch === '/' && next === '*') {
277
+ i += 2;
278
+ while (i < text.length && !(text[i] === '*' && text[i + 1] === '/'))
279
+ i++;
280
+ i += 2;
281
+ continue;
282
+ }
283
+ out += ch;
284
+ i++;
285
+ }
286
+ // Trailing comma cleanup.
287
+ return out.replace(/,(\s*[\]}])/g, '$1');
288
+ }
289
+ /**
290
+ * Parse `compilerOptions.paths` from a `tsconfig.json` / `jsconfig.json`.
291
+ * The TS config format maps `"@components/*": ["src/components/*"]` — we
292
+ * normalise to `@components` → `src/components/*`.
293
+ */
294
+ export function parseTsConfigAliases(content) {
295
+ const aliases = {};
296
+ let parsed;
297
+ try {
298
+ parsed = JSON.parse(stripJsonComments(content));
299
+ }
300
+ catch {
301
+ return aliases;
302
+ }
303
+ const obj = parsed;
304
+ const paths = obj?.compilerOptions?.paths;
305
+ if (!paths || typeof paths !== 'object')
306
+ return aliases;
307
+ for (const [pattern, target] of Object.entries(paths)) {
308
+ if (!Array.isArray(target) || target.length === 0)
309
+ continue;
310
+ const firstTarget = target[0];
311
+ if (typeof firstTarget !== 'string')
312
+ continue;
313
+ // Drop wildcard suffix for the alias key — `"@/*"` → `@`,
314
+ // `"@components/*"` → `@components`.
315
+ const aliasKey = pattern.replace(/\/\*$/, '');
316
+ aliases[aliasKey] = firstTarget;
317
+ }
318
+ return aliases;
319
+ }
320
+ /**
321
+ * Extract `resolve.alias` (object literal form) from a Vite or webpack config
322
+ * file. We use a regex rather than a full JS parser because the contents are
323
+ * config files written in a small, well-known shape — the v1 detector's
324
+ * "regex over AST" tradeoff carries forward.
325
+ *
326
+ * Recognises both:
327
+ * resolve: { alias: { '@components': path.resolve(__dirname, 'src/components') } }
328
+ * alias: { '@engine': '/abs/path' }
329
+ */
330
+ export function parseViteOrWebpackAliases(content) {
331
+ const aliases = {};
332
+ // Find an `alias:` or `alias =` followed by an object literal. Only the
333
+ // first occurrence is parsed — vite configs typically declare exactly one.
334
+ const aliasMatch = /alias\s*[:=]\s*\{([\s\S]*?)\}/m.exec(content);
335
+ if (!aliasMatch)
336
+ return aliases;
337
+ const body = aliasMatch[1];
338
+ // Each entry is `'key': <expr>` or `"key": <expr>`. We grab the key and
339
+ // ignore the expression because we only need the alias prefix to match
340
+ // user code.
341
+ const entryRe = /['"]([^'"]+)['"]\s*:\s*([^,\n}]+)/g;
342
+ let m;
343
+ while ((m = entryRe.exec(body)) !== null) {
344
+ const key = m[1];
345
+ const valueExpr = m[2].trim();
346
+ aliases[key] = valueExpr;
347
+ }
348
+ return aliases;
349
+ }
350
+ /**
351
+ * Read every alias source we know about and merge into a single map. Sources
352
+ * are read in parallel; later sources take precedence if there is overlap.
353
+ */
354
+ export async function loadProjectAliases(repoPath) {
355
+ const candidates = [
356
+ 'tsconfig.json',
357
+ 'jsconfig.json',
358
+ 'vite.config.js',
359
+ 'vite.config.ts',
360
+ 'vite.config.mjs',
361
+ 'vite.config.cjs',
362
+ 'webpack.config.js',
363
+ 'webpack.config.ts',
364
+ 'webpack.config.mjs',
365
+ 'webpack.config.cjs',
366
+ ];
367
+ const reads = await Promise.all(candidates.map(async (name) => ({ name, content: await readFileSafe(join(repoPath, name)) })));
368
+ const merged = {};
369
+ for (const { name, content } of reads) {
370
+ if (!content)
371
+ continue;
372
+ const isTsConfig = name === 'tsconfig.json' || name === 'jsconfig.json';
373
+ const parsed = isTsConfig ? parseTsConfigAliases(content) : parseViteOrWebpackAliases(content);
374
+ Object.assign(merged, parsed);
375
+ }
376
+ return merged;
377
+ }
378
+ function detectImportStyle(files, aliases) {
379
+ // Barrel detection is unchanged from v1 — it's a structural signal taken
380
+ // straight off the file list.
120
381
  const indexFiles = files.filter((f) => basename(f.relativePath).startsWith('index.'));
121
382
  const usesBarrels = indexFiles.length > 3;
122
- // For a proper import analysis we'd need to read file contents,
123
- // but for the convention detector we just note barrel presence
383
+ const aliasKeys = Object.keys(aliases);
384
+ const hasAliases = aliasKeys.length > 0;
385
+ let style = 'relative';
386
+ if (hasAliases && usesBarrels) {
387
+ style = 'mixed-relative-and-aliases';
388
+ }
389
+ else if (hasAliases) {
390
+ style = 'path-alias';
391
+ }
392
+ const aliasExamples = aliasKeys.slice(0, 3);
393
+ const barrelExamples = indexFiles.slice(0, 3).map((f) => f.relativePath);
394
+ const examples = hasAliases ? aliasExamples : barrelExamples;
124
395
  return {
125
- style: 'relative',
396
+ style,
126
397
  confidence: 0.8,
127
398
  usesBarrels,
128
- examples: indexFiles.slice(0, 3).map((f) => f.relativePath),
399
+ examples,
400
+ aliases,
129
401
  };
130
402
  }
131
- // ── Public API ──────────────────────────────────────────────────
403
+ // ── React project detection ─────────────────────────────────────
132
404
  /**
133
- * Detect coding conventions from the file list.
405
+ * Read `package.json` and decide whether this is a React project. Looks at
406
+ * `dependencies` AND `devDependencies` because Vite-driven SPAs sometimes
407
+ * declare React as a dev dep via templates.
134
408
  */
135
- export function detectConventions(files) {
409
+ export async function detectReactProject(repoPath) {
410
+ const content = await readFileSafe(join(repoPath, 'package.json'));
411
+ if (!content)
412
+ return false;
413
+ let pkg;
414
+ try {
415
+ pkg = JSON.parse(content);
416
+ }
417
+ catch {
418
+ return false;
419
+ }
420
+ const obj = pkg;
421
+ return Boolean(obj.dependencies?.react ?? obj.devDependencies?.react);
422
+ }
423
+ /**
424
+ * Detect coding conventions from the file list. When `options.repoPath` is
425
+ * supplied the detector additionally reads project-level configuration to
426
+ * suppress the three classes of false-positives described at the top of this
427
+ * file (AISDLC-80).
428
+ */
429
+ export async function detectConventions(files, options = {}) {
136
430
  const conventions = [];
137
- // File naming convention
138
- const naming = detectFileNamingStyle(files);
431
+ const repoPath = options.repoPath;
432
+ const reactMode = repoPath ? await detectReactProject(repoPath) : false;
433
+ const aliases = repoPath ? await loadProjectAliases(repoPath) : {};
434
+ // ── File naming ──
435
+ const naming = detectFileNamingStyle(files, reactMode);
139
436
  if (naming.confidence > 0) {
437
+ const labelSuffix = naming.style.startsWith('PascalCase + camelCase')
438
+ ? ' for React components/hooks'
439
+ : ' for file names';
140
440
  conventions.push({
141
441
  category: 'naming',
142
- pattern: `${naming.style} for file names`,
442
+ pattern: `${naming.style}${labelSuffix}`,
143
443
  confidence: naming.confidence,
144
444
  examples: naming.examples,
145
445
  });
146
446
  }
147
- // Testing convention
148
- const testing = detectTestingStyle(files);
149
- if (testing.confidence > 0) {
150
- const desc = testing.style === 'co-located'
151
- ? 'Co-located test files (*.test.ts)'
152
- : testing.style === '__tests__'
153
- ? 'Test files in __tests__/ directories'
154
- : testing.style === 'test-directory'
155
- ? 'Test files in test/ directory'
156
- : 'Mixed test file placement';
447
+ // ── Testing ──
448
+ const locations = enumerateTestLocations(files);
449
+ const testingSummary = summariseTestLocations(locations);
450
+ if (testingSummary.confidence > 0) {
157
451
  conventions.push({
158
452
  category: 'testing',
159
- pattern: desc,
160
- confidence: testing.confidence,
161
- examples: testing.examples,
453
+ pattern: testingSummary.pattern,
454
+ confidence: testingSummary.confidence,
455
+ examples: testingSummary.examples,
162
456
  });
163
457
  }
164
- // Import style convention
165
- const imports = detectImportStyle(files);
166
- const importDesc = imports.usesBarrels
167
- ? 'Relative imports, barrel re-exports via index.ts'
168
- : 'Relative imports';
458
+ // ── Imports ──
459
+ const imports = detectImportStyle(files, aliases);
460
+ const aliasKeys = Object.keys(imports.aliases);
461
+ let importDesc;
462
+ if (imports.style === 'mixed-relative-and-aliases') {
463
+ importDesc = `Path aliases (${aliasKeys.join(', ')}) + relative imports + barrel re-exports via index.ts`;
464
+ }
465
+ else if (imports.style === 'path-alias') {
466
+ importDesc = `Path aliases (${aliasKeys.join(', ')}) + relative imports`;
467
+ }
468
+ else if (imports.usesBarrels) {
469
+ importDesc = 'Relative imports, barrel re-exports via index.ts';
470
+ }
471
+ else {
472
+ importDesc = 'Relative imports';
473
+ }
169
474
  conventions.push({
170
475
  category: 'imports',
171
476
  pattern: importDesc,
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Diff analyzer — runs deterministic structural checks on changed files
3
+ * BEFORE LLM review agents see the diff.
4
+ *
5
+ * Reuses existing analysis infrastructure:
6
+ * - parseImports() for import counting
7
+ * - computeFileComplexity() for per-file complexity scoring
8
+ */
9
+ export interface DiffFinding {
10
+ type: 'complexity' | 'imports' | 'file-length';
11
+ file: string;
12
+ severity: 'info' | 'warning';
13
+ message: string;
14
+ }
15
+ export interface DiffAnalysisResult {
16
+ /** Files touched by the diff. */
17
+ changedFiles: string[];
18
+ /** Deterministic structural findings. */
19
+ findings: DiffFinding[];
20
+ /** Formatted summary for injection into review agent context. */
21
+ summary: string;
22
+ }
23
+ /**
24
+ * Extract changed file paths from a unified diff.
25
+ * Parses `--- a/path` and `+++ b/path` lines.
26
+ */
27
+ export declare function extractChangedFiles(diff: string): string[];
28
+ /**
29
+ * Analyze a PR diff for structural issues.
30
+ * Returns deterministic findings that review agents can skip.
31
+ */
32
+ export declare function analyzeDiff(diff: string, repoPath: string): Promise<DiffAnalysisResult>;
33
+ //# sourceMappingURL=diff-analyzer.d.ts.map
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Diff analyzer — runs deterministic structural checks on changed files
3
+ * BEFORE LLM review agents see the diff.
4
+ *
5
+ * Reuses existing analysis infrastructure:
6
+ * - parseImports() for import counting
7
+ * - computeFileComplexity() for per-file complexity scoring
8
+ */
9
+ import { existsSync } from 'node:fs';
10
+ import { readFile } from 'node:fs/promises';
11
+ import { join } from 'node:path';
12
+ import { parseImports } from './dependency-parser.js';
13
+ import { computeFileComplexity } from './hotspot-analyzer.js';
14
+ // ── Thresholds ───────────────────────────────────────────────────────
15
+ const COMPLEXITY_THRESHOLD = 7;
16
+ const FILE_LENGTH_THRESHOLD = 300;
17
+ const IMPORT_COUNT_THRESHOLD = 15;
18
+ const CODE_EXTENSIONS = new Set([
19
+ '.ts',
20
+ '.tsx',
21
+ '.js',
22
+ '.jsx',
23
+ '.mjs',
24
+ '.cjs',
25
+ '.py',
26
+ '.go',
27
+ '.rs',
28
+ ]);
29
+ // ── Diff parsing ─────────────────────────────────────────────────────
30
+ /**
31
+ * Extract changed file paths from a unified diff.
32
+ * Parses `--- a/path` and `+++ b/path` lines.
33
+ */
34
+ export function extractChangedFiles(diff) {
35
+ const files = new Set();
36
+ for (const line of diff.split('\n')) {
37
+ const match = line.match(/^[+-]{3}\s[ab]\/(.+)$/);
38
+ if (match && match[1] !== '/dev/null') {
39
+ files.add(match[1]);
40
+ }
41
+ }
42
+ return [...files];
43
+ }
44
+ // ── Analysis ─────────────────────────────────────────────────────────
45
+ /**
46
+ * Analyze a PR diff for structural issues.
47
+ * Returns deterministic findings that review agents can skip.
48
+ */
49
+ export async function analyzeDiff(diff, repoPath) {
50
+ const changedFiles = extractChangedFiles(diff);
51
+ const findings = [];
52
+ // Only analyze code files that exist on disk (skip deleted files)
53
+ const codeFiles = changedFiles.filter((f) => {
54
+ const ext = '.' + f.split('.').pop();
55
+ return CODE_EXTENSIONS.has(ext) && existsSync(join(repoPath, f));
56
+ });
57
+ for (const file of codeFiles) {
58
+ const fullPath = join(repoPath, file);
59
+ // Read file for line count
60
+ let content;
61
+ try {
62
+ content = await readFile(fullPath, 'utf-8');
63
+ }
64
+ catch {
65
+ continue;
66
+ }
67
+ const lineCount = content.split('\n').length;
68
+ // Parse imports
69
+ const imports = await parseImports(fullPath);
70
+ const importCount = imports.length;
71
+ // Check 1: File complexity
72
+ const complexity = computeFileComplexity(lineCount, importCount);
73
+ if (complexity >= COMPLEXITY_THRESHOLD) {
74
+ findings.push({
75
+ type: 'complexity',
76
+ file,
77
+ severity: 'warning',
78
+ message: `High complexity (${complexity}/10) — ${lineCount} lines, ${importCount} imports. Review with extra care.`,
79
+ });
80
+ }
81
+ // Check 2: Large file
82
+ if (lineCount > FILE_LENGTH_THRESHOLD) {
83
+ findings.push({
84
+ type: 'file-length',
85
+ file,
86
+ severity: lineCount > 500 ? 'warning' : 'info',
87
+ message: `Large file (${lineCount} lines). Consider splitting if it has multiple responsibilities.`,
88
+ });
89
+ }
90
+ // Check 3: High import count (coupling)
91
+ if (importCount >= IMPORT_COUNT_THRESHOLD) {
92
+ findings.push({
93
+ type: 'imports',
94
+ file,
95
+ severity: 'warning',
96
+ message: `High coupling (${importCount} imports). May indicate the file has too many responsibilities.`,
97
+ });
98
+ }
99
+ }
100
+ const summary = formatSummary(changedFiles, codeFiles, findings);
101
+ return { changedFiles, findings, summary };
102
+ }
103
+ // ── Formatting ───────────────────────────────────────────────────────
104
+ function formatSummary(allFiles, codeFiles, findings) {
105
+ if (findings.length === 0) {
106
+ return `## Pre-Verified Structural Analysis\n\nAnalyzed ${codeFiles.length} code files (${allFiles.length} total changed). No structural issues found.`;
107
+ }
108
+ const lines = [
109
+ `## Pre-Verified Structural Analysis`,
110
+ '',
111
+ `Analyzed ${codeFiles.length} code files (${allFiles.length} total changed). ${findings.length} structural finding(s):`,
112
+ '',
113
+ ];
114
+ for (const f of findings) {
115
+ const icon = f.severity === 'warning' ? '⚠️' : 'ℹ️';
116
+ lines.push(`- ${icon} **${f.file}**: ${f.message}`);
117
+ }
118
+ lines.push('');
119
+ lines.push('*These findings are deterministic. Do NOT re-analyze these files for the issues above.*');
120
+ return lines.join('\n');
121
+ }
122
+ //# sourceMappingURL=diff-analyzer.js.map