@veraxhq/verax 0.3.0 → 0.4.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 (191) hide show
  1. package/README.md +28 -20
  2. package/bin/verax.js +11 -18
  3. package/package.json +28 -7
  4. package/src/cli/commands/baseline.js +1 -2
  5. package/src/cli/commands/default.js +72 -81
  6. package/src/cli/commands/doctor.js +29 -0
  7. package/src/cli/commands/ga.js +3 -0
  8. package/src/cli/commands/gates.js +1 -1
  9. package/src/cli/commands/inspect.js +6 -133
  10. package/src/cli/commands/release-check.js +2 -0
  11. package/src/cli/commands/run.js +74 -246
  12. package/src/cli/commands/security-check.js +2 -1
  13. package/src/cli/commands/truth.js +0 -1
  14. package/src/cli/entry.js +82 -309
  15. package/src/cli/util/angular-component-extractor.js +2 -2
  16. package/src/cli/util/angular-navigation-detector.js +2 -2
  17. package/src/cli/util/ast-interactive-detector.js +4 -6
  18. package/src/cli/util/ast-network-detector.js +3 -3
  19. package/src/cli/util/ast-promise-extractor.js +581 -0
  20. package/src/cli/util/ast-usestate-detector.js +3 -3
  21. package/src/cli/util/atomic-write.js +12 -1
  22. package/src/cli/util/console-reporter.js +72 -0
  23. package/src/cli/util/detection-engine.js +105 -41
  24. package/src/cli/util/determinism-runner.js +2 -1
  25. package/src/cli/util/determinism-writer.js +1 -1
  26. package/src/cli/util/digest-engine.js +359 -0
  27. package/src/cli/util/dom-diff.js +226 -0
  28. package/src/cli/util/env-url.js +0 -4
  29. package/src/cli/util/evidence-engine.js +287 -0
  30. package/src/cli/util/expectation-extractor.js +217 -367
  31. package/src/cli/util/findings-writer.js +19 -126
  32. package/src/cli/util/framework-detector.js +572 -0
  33. package/src/cli/util/idgen.js +1 -1
  34. package/src/cli/util/interaction-planner.js +529 -0
  35. package/src/cli/util/learn-writer.js +2 -2
  36. package/src/cli/util/ledger-writer.js +110 -0
  37. package/src/cli/util/monorepo-resolver.js +162 -0
  38. package/src/cli/util/observation-engine.js +127 -278
  39. package/src/cli/util/observe-writer.js +2 -2
  40. package/src/cli/util/paths.js +12 -3
  41. package/src/cli/util/project-discovery.js +284 -3
  42. package/src/cli/util/project-writer.js +2 -2
  43. package/src/cli/util/run-id.js +23 -27
  44. package/src/cli/util/run-result.js +778 -0
  45. package/src/cli/util/selector-resolver.js +235 -0
  46. package/src/cli/util/summary-writer.js +2 -1
  47. package/src/cli/util/svelte-navigation-detector.js +3 -3
  48. package/src/cli/util/svelte-sfc-extractor.js +0 -1
  49. package/src/cli/util/svelte-state-detector.js +1 -2
  50. package/src/cli/util/trust-activation-integration.js +496 -0
  51. package/src/cli/util/trust-activation-wrapper.js +85 -0
  52. package/src/cli/util/trust-integration-hooks.js +164 -0
  53. package/src/cli/util/types.js +153 -0
  54. package/src/cli/util/url-validation.js +40 -0
  55. package/src/cli/util/vue-navigation-detector.js +4 -3
  56. package/src/cli/util/vue-sfc-extractor.js +1 -2
  57. package/src/cli/util/vue-state-detector.js +1 -1
  58. package/src/types/fs-augment.d.ts +23 -0
  59. package/src/types/global.d.ts +137 -0
  60. package/src/types/internal-types.d.ts +35 -0
  61. package/src/verax/cli/finding-explainer.js +3 -56
  62. package/src/verax/cli/init.js +4 -18
  63. package/src/verax/core/action-classifier.js +4 -3
  64. package/src/verax/core/artifacts/registry.js +0 -15
  65. package/src/verax/core/artifacts/verifier.js +18 -8
  66. package/src/verax/core/baseline/baseline.snapshot.js +2 -0
  67. package/src/verax/core/capabilities/gates.js +7 -1
  68. package/src/verax/core/confidence/confidence-compute.js +14 -7
  69. package/src/verax/core/confidence/confidence.loader.js +1 -0
  70. package/src/verax/core/confidence-engine-refactor.js +8 -3
  71. package/src/verax/core/confidence-engine.js +162 -23
  72. package/src/verax/core/contracts/types.js +1 -0
  73. package/src/verax/core/contracts/validators.js +79 -4
  74. package/src/verax/core/decision-snapshot.js +3 -30
  75. package/src/verax/core/decisions/decision.trace.js +2 -0
  76. package/src/verax/core/determinism/contract-writer.js +2 -2
  77. package/src/verax/core/determinism/contract.js +1 -1
  78. package/src/verax/core/determinism/diff.js +42 -1
  79. package/src/verax/core/determinism/engine.js +7 -6
  80. package/src/verax/core/determinism/finding-identity.js +3 -2
  81. package/src/verax/core/determinism/normalize.js +32 -4
  82. package/src/verax/core/determinism/report-writer.js +1 -0
  83. package/src/verax/core/determinism/run-fingerprint.js +7 -2
  84. package/src/verax/core/dynamic-route-intelligence.js +8 -7
  85. package/src/verax/core/evidence/evidence-capture-service.js +1 -0
  86. package/src/verax/core/evidence/evidence-intent-ledger.js +2 -1
  87. package/src/verax/core/evidence-builder.js +2 -2
  88. package/src/verax/core/execution-mode-context.js +1 -1
  89. package/src/verax/core/execution-mode-detector.js +5 -3
  90. package/src/verax/core/failures/exit-codes.js +39 -37
  91. package/src/verax/core/failures/failure-summary.js +1 -1
  92. package/src/verax/core/failures/failure.factory.js +3 -3
  93. package/src/verax/core/failures/failure.ledger.js +3 -2
  94. package/src/verax/core/ga/ga.artifact.js +1 -1
  95. package/src/verax/core/ga/ga.contract.js +3 -2
  96. package/src/verax/core/ga/ga.enforcer.js +1 -0
  97. package/src/verax/core/guardrails/policy.loader.js +1 -0
  98. package/src/verax/core/guardrails/truth-reconciliation.js +1 -1
  99. package/src/verax/core/guardrails-engine.js +2 -2
  100. package/src/verax/core/incremental-store.js +1 -0
  101. package/src/verax/core/integrity/budget.js +138 -0
  102. package/src/verax/core/integrity/determinism.js +342 -0
  103. package/src/verax/core/integrity/integrity.js +208 -0
  104. package/src/verax/core/integrity/poisoning.js +108 -0
  105. package/src/verax/core/integrity/transaction.js +140 -0
  106. package/src/verax/core/observe/run-timeline.js +2 -0
  107. package/src/verax/core/perf/perf.report.js +2 -0
  108. package/src/verax/core/pipeline-tracker.js +5 -0
  109. package/src/verax/core/release/provenance.builder.js +73 -214
  110. package/src/verax/core/release/release.enforcer.js +14 -9
  111. package/src/verax/core/release/reproducibility.check.js +1 -0
  112. package/src/verax/core/release/sbom.builder.js +32 -23
  113. package/src/verax/core/replay-validator.js +2 -0
  114. package/src/verax/core/replay.js +4 -0
  115. package/src/verax/core/report/cross-index.js +6 -3
  116. package/src/verax/core/report/human-summary.js +141 -1
  117. package/src/verax/core/route-intelligence.js +4 -3
  118. package/src/verax/core/run-id.js +6 -3
  119. package/src/verax/core/run-manifest.js +4 -3
  120. package/src/verax/core/security/secrets.scan.js +10 -7
  121. package/src/verax/core/security/security.enforcer.js +4 -0
  122. package/src/verax/core/security/supplychain.policy.js +9 -1
  123. package/src/verax/core/security/vuln.scan.js +2 -2
  124. package/src/verax/core/truth/truth.certificate.js +3 -1
  125. package/src/verax/core/ui-feedback-intelligence.js +12 -46
  126. package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
  127. package/src/verax/detect/confidence-engine.js +100 -660
  128. package/src/verax/detect/confidence-helper.js +1 -0
  129. package/src/verax/detect/detection-engine.js +1 -18
  130. package/src/verax/detect/dynamic-route-findings.js +17 -14
  131. package/src/verax/detect/expectation-chain-detector.js +1 -1
  132. package/src/verax/detect/expectation-model.js +3 -5
  133. package/src/verax/detect/failure-cause-inference.js +293 -0
  134. package/src/verax/detect/findings-writer.js +126 -166
  135. package/src/verax/detect/flow-detector.js +2 -2
  136. package/src/verax/detect/form-silent-failure.js +98 -0
  137. package/src/verax/detect/index.js +51 -234
  138. package/src/verax/detect/invariants-enforcer.js +147 -0
  139. package/src/verax/detect/journey-stall-detector.js +4 -4
  140. package/src/verax/detect/navigation-silent-failure.js +82 -0
  141. package/src/verax/detect/problem-aggregator.js +361 -0
  142. package/src/verax/detect/route-findings.js +7 -6
  143. package/src/verax/detect/summary-writer.js +477 -0
  144. package/src/verax/detect/test-failure-cause-inference.js +314 -0
  145. package/src/verax/detect/ui-feedback-findings.js +18 -18
  146. package/src/verax/detect/verdict-engine.js +3 -57
  147. package/src/verax/detect/view-switch-correlator.js +2 -2
  148. package/src/verax/flow/flow-engine.js +2 -1
  149. package/src/verax/flow/flow-spec.js +0 -6
  150. package/src/verax/index.js +48 -412
  151. package/src/verax/intel/ts-program.js +1 -0
  152. package/src/verax/intel/vue-navigation-extractor.js +3 -0
  153. package/src/verax/learn/action-contract-extractor.js +67 -682
  154. package/src/verax/learn/ast-contract-extractor.js +1 -1
  155. package/src/verax/learn/flow-extractor.js +1 -0
  156. package/src/verax/learn/project-detector.js +5 -0
  157. package/src/verax/learn/react-router-extractor.js +2 -0
  158. package/src/verax/learn/route-validator.js +1 -4
  159. package/src/verax/learn/source-instrumenter.js +1 -0
  160. package/src/verax/learn/state-extractor.js +2 -1
  161. package/src/verax/learn/static-extractor.js +1 -0
  162. package/src/verax/observe/coverage-gaps.js +132 -0
  163. package/src/verax/observe/expectation-handler.js +126 -0
  164. package/src/verax/observe/incremental-skip.js +46 -0
  165. package/src/verax/observe/index.js +735 -84
  166. package/src/verax/observe/interaction-executor.js +192 -0
  167. package/src/verax/observe/interaction-runner.js +782 -530
  168. package/src/verax/observe/network-firewall.js +86 -0
  169. package/src/verax/observe/observation-builder.js +169 -0
  170. package/src/verax/observe/observe-context.js +1 -1
  171. package/src/verax/observe/observe-helpers.js +2 -1
  172. package/src/verax/observe/observe-runner.js +28 -24
  173. package/src/verax/observe/observers/budget-observer.js +3 -3
  174. package/src/verax/observe/observers/console-observer.js +4 -4
  175. package/src/verax/observe/observers/coverage-observer.js +4 -4
  176. package/src/verax/observe/observers/interaction-observer.js +3 -3
  177. package/src/verax/observe/observers/navigation-observer.js +4 -4
  178. package/src/verax/observe/observers/network-observer.js +4 -4
  179. package/src/verax/observe/observers/safety-observer.js +1 -1
  180. package/src/verax/observe/observers/ui-feedback-observer.js +4 -4
  181. package/src/verax/observe/page-traversal.js +138 -0
  182. package/src/verax/observe/snapshot-ops.js +94 -0
  183. package/src/verax/observe/ui-signal-sensor.js +2 -148
  184. package/src/verax/scan-summary-writer.js +10 -42
  185. package/src/verax/shared/artifact-manager.js +30 -13
  186. package/src/verax/shared/caching.js +1 -0
  187. package/src/verax/shared/expectation-tracker.js +1 -0
  188. package/src/verax/shared/zip-artifacts.js +6 -0
  189. package/src/verax/core/confidence-engine.js.backup +0 -471
  190. package/src/verax/shared/config-loader.js +0 -169
  191. /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
@@ -0,0 +1,572 @@
1
+ /**
2
+ * PHASE H6 - Framework Detection Module
3
+ *
4
+ * Robustly identifies framework type with confidence scoring.
5
+ * Returns deterministic output including evidence trail for auditing.
6
+ *
7
+ * Supported frameworks (in priority order):
8
+ * 1. Next.js
9
+ * 2. Vue + Vite
10
+ * 3. Vue CLI (legacy)
11
+ * 4. Svelte + SvelteKit
12
+ * 5. Angular
13
+ * 6. Remix
14
+ * 7. React + Vite
15
+ * 8. Create React App
16
+ * 9. Vite (generic)
17
+ * 10. Generic/Static
18
+ */
19
+
20
+ import { existsSync, readFileSync, readdirSync } from 'fs';
21
+ import { resolve } from 'path';
22
+
23
+ /**
24
+ * Detection result structure
25
+ */
26
+ export function createDetectionResult() {
27
+ return {
28
+ framework: 'unknown',
29
+ confidence: 0, // 0-100
30
+ evidence: [],
31
+ devCommand: null,
32
+ defaultPortCandidates: [3000, 5173, 4200, 8080],
33
+ appRootCandidates: [],
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Load and parse package.json safely
39
+ */
40
+ function loadPackageJson(projectRoot) {
41
+ try {
42
+ const pkgPath = resolve(projectRoot, 'package.json');
43
+ if (!existsSync(pkgPath)) return null;
44
+ const content = readFileSync(pkgPath, 'utf8');
45
+ // @ts-expect-error - readFileSync with encoding returns string
46
+ return JSON.parse(content);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Check for file existence (deterministic ordering)
54
+ */
55
+ function filesExist(projectRoot, patterns) {
56
+ const found = [];
57
+ for (const pattern of patterns) {
58
+ const path = resolve(projectRoot, pattern);
59
+ if (existsSync(path)) {
60
+ found.push(pattern);
61
+ }
62
+ }
63
+ return found;
64
+ }
65
+
66
+ /**
67
+ * List directories at path (sorted deterministically)
68
+ */
69
+ function listDirs(projectRoot) {
70
+ try {
71
+ const dirs = readdirSync(projectRoot);
72
+ return dirs
73
+ .filter(name => {
74
+ try {
75
+ const stat = require('fs').statSync(resolve(projectRoot, name));
76
+ return stat.isDirectory();
77
+ } catch {
78
+ return false;
79
+ }
80
+ })
81
+ .sort();
82
+ } catch {
83
+ return [];
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Detect Next.js
89
+ */
90
+ function detectNextJs(projectRoot, pkg) {
91
+ const result = createDetectionResult();
92
+ let score = 0;
93
+
94
+ // Check dependencies
95
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
96
+ if (deps.next) {
97
+ score += 40;
98
+ result.evidence.push('next dependency found in package.json');
99
+ }
100
+ if (deps['react'] && deps.next) {
101
+ score += 10;
102
+ result.evidence.push('react + next detected');
103
+ }
104
+
105
+ // Check next.config.js/mjs
106
+ const configFiles = filesExist(projectRoot, ['next.config.js', 'next.config.mjs']);
107
+ if (configFiles.length > 0) {
108
+ score += 30;
109
+ result.evidence.push(`next config file found: ${configFiles.join(', ')}`);
110
+ }
111
+
112
+ // Check .next directory (build artifact)
113
+ if (existsSync(resolve(projectRoot, '.next'))) {
114
+ score += 5;
115
+ result.evidence.push('.next build directory found');
116
+ }
117
+
118
+ // Check src/pages or app directory (Next.js structure)
119
+ if (existsSync(resolve(projectRoot, 'src', 'pages')) ||
120
+ existsSync(resolve(projectRoot, 'pages')) ||
121
+ existsSync(resolve(projectRoot, 'src', 'app')) ||
122
+ existsSync(resolve(projectRoot, 'app'))) {
123
+ score += 10;
124
+ result.evidence.push('Next.js pages or app directory found');
125
+ }
126
+
127
+ // Check dev script
128
+ if (pkg?.scripts?.dev?.includes('next')) {
129
+ score += 5;
130
+ result.evidence.push('next command in dev script');
131
+ }
132
+
133
+ if (score > 0) {
134
+ result.framework = 'next';
135
+ result.confidence = Math.min(score, 100);
136
+ result.devCommand = pkg?.scripts?.dev || 'next dev';
137
+ result.defaultPortCandidates = [3000];
138
+ }
139
+
140
+ return result;
141
+ }
142
+
143
+ /**
144
+ * Detect Vue (Vite or CLI)
145
+ */
146
+ function detectVue(projectRoot, pkg) {
147
+ const result = createDetectionResult();
148
+ let score = 0;
149
+ let isVite = false;
150
+
151
+ // Check dependencies
152
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
153
+ if (deps.vue) {
154
+ score += 40;
155
+ result.evidence.push('vue dependency found');
156
+ }
157
+
158
+ // Check for Vite
159
+ if (deps.vite) {
160
+ score += 20;
161
+ result.evidence.push('vite found (Vue + Vite setup)');
162
+ isVite = true;
163
+ }
164
+
165
+ // Check vite.config.js/ts
166
+ const viteConfigs = filesExist(projectRoot, ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']);
167
+ if (viteConfigs.length > 0) {
168
+ score += 15;
169
+ result.evidence.push(`vite config found: ${viteConfigs.join(', ')}`);
170
+ isVite = true;
171
+ }
172
+
173
+ // Check vue.config.js (Vue CLI)
174
+ if (existsSync(resolve(projectRoot, 'vue.config.js'))) {
175
+ score += 15;
176
+ result.evidence.push('vue.config.js found (Vue CLI)');
177
+ }
178
+
179
+ // Check src/App.vue or src/components directory
180
+ if (existsSync(resolve(projectRoot, 'src', 'App.vue')) ||
181
+ existsSync(resolve(projectRoot, 'src', 'components'))) {
182
+ score += 10;
183
+ result.evidence.push('Vue component structure found');
184
+ }
185
+
186
+ // Check dev script
187
+ if (pkg?.scripts?.dev?.includes('vue') || pkg?.scripts?.dev?.includes('vite')) {
188
+ score += 5;
189
+ result.evidence.push('vue or vite in dev script');
190
+ }
191
+
192
+ if (score > 0) {
193
+ result.framework = 'vue';
194
+ result.confidence = Math.min(score, 100);
195
+ result.devCommand = pkg?.scripts?.dev || (isVite ? 'vite' : 'vue-cli-service serve');
196
+ result.defaultPortCandidates = isVite ? [5173] : [8080];
197
+ }
198
+
199
+ return result;
200
+ }
201
+
202
+ /**
203
+ * Detect Svelte (SvelteKit)
204
+ */
205
+ function detectSvelte(projectRoot, pkg) {
206
+ const result = createDetectionResult();
207
+ let score = 0;
208
+
209
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
210
+
211
+ // Check for SvelteKit
212
+ if (deps['@sveltejs/kit']) {
213
+ score += 50;
214
+ result.evidence.push('@sveltejs/kit found (SvelteKit)');
215
+ } else if (deps.svelte) {
216
+ score += 30;
217
+ result.evidence.push('svelte dependency found');
218
+ }
219
+
220
+ // Check svelte.config.js
221
+ if (existsSync(resolve(projectRoot, 'svelte.config.js'))) {
222
+ score += 20;
223
+ result.evidence.push('svelte.config.js found');
224
+ }
225
+
226
+ // Check src directory structure
227
+ if (existsSync(resolve(projectRoot, 'src', 'routes'))) {
228
+ score += 15;
229
+ result.evidence.push('SvelteKit routes structure found');
230
+ }
231
+
232
+ // Check dev script
233
+ if (pkg?.scripts?.dev?.includes('svelte') || pkg?.scripts?.dev?.includes('vite')) {
234
+ score += 5;
235
+ result.evidence.push('svelte or vite in dev script');
236
+ }
237
+
238
+ if (score > 0) {
239
+ result.framework = 'svelte';
240
+ result.confidence = Math.min(score, 100);
241
+ result.devCommand = pkg?.scripts?.dev || 'vite dev';
242
+ result.defaultPortCandidates = [5173];
243
+ }
244
+
245
+ return result;
246
+ }
247
+
248
+ /**
249
+ * Detect Angular
250
+ */
251
+ function detectAngular(projectRoot, pkg) {
252
+ const result = createDetectionResult();
253
+ let score = 0;
254
+
255
+ // Check angular.json (strongest signal)
256
+ if (existsSync(resolve(projectRoot, 'angular.json'))) {
257
+ score += 50;
258
+ result.evidence.push('angular.json found');
259
+ }
260
+
261
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
262
+ if (deps['@angular/core']) {
263
+ score += 40;
264
+ result.evidence.push('@angular/core dependency found');
265
+ }
266
+
267
+ // Check CLI
268
+ if (deps['@angular/cli']) {
269
+ score += 10;
270
+ result.evidence.push('@angular/cli found');
271
+ }
272
+
273
+ // Check src/main.ts (Angular entry point)
274
+ if (existsSync(resolve(projectRoot, 'src', 'main.ts'))) {
275
+ score += 10;
276
+ result.evidence.push('Angular src/main.ts found');
277
+ }
278
+
279
+ if (pkg?.scripts?.serve?.includes('ng serve')) {
280
+ score += 5;
281
+ result.evidence.push('ng serve in scripts');
282
+ }
283
+
284
+ if (score > 0) {
285
+ result.framework = 'angular';
286
+ result.confidence = Math.min(score, 100);
287
+ result.devCommand = pkg?.scripts?.serve || 'ng serve';
288
+ result.defaultPortCandidates = [4200];
289
+ }
290
+
291
+ return result;
292
+ }
293
+
294
+ /**
295
+ * Detect Remix (or remix-like Node.js framework)
296
+ */
297
+ function detectRemix(projectRoot, pkg) {
298
+ const result = createDetectionResult();
299
+ let score = 0;
300
+
301
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
302
+ if (deps['@remix-run/react'] || deps['remix']) {
303
+ score += 50;
304
+ result.evidence.push('@remix-run/react or remix dependency found');
305
+ }
306
+
307
+ // Check remix config
308
+ if (existsSync(resolve(projectRoot, 'remix.config.js'))) {
309
+ score += 30;
310
+ result.evidence.push('remix.config.js found');
311
+ }
312
+
313
+ // Check app directory (Remix routing)
314
+ if (existsSync(resolve(projectRoot, 'app'))) {
315
+ score += 10;
316
+ result.evidence.push('app directory found (Remix structure)');
317
+ }
318
+
319
+ // Check for Express or Node.js server.js (remix-like framework)
320
+ if (existsSync(resolve(projectRoot, 'server.js')) && (deps['express'])) {
321
+ score += 45;
322
+ result.evidence.push('Express server.js found (remix-like Node.js framework)');
323
+ }
324
+
325
+ if (score > 0) {
326
+ result.framework = 'remix';
327
+ result.confidence = Math.min(score, 100);
328
+ result.devCommand = pkg?.scripts?.dev || 'remix dev';
329
+ result.defaultPortCandidates = [3000, 5173];
330
+ }
331
+
332
+ return result;
333
+ }
334
+
335
+ /**
336
+ * Detect React (Vite or CRA)
337
+ */
338
+ function detectReact(projectRoot, pkg) {
339
+ const result = createDetectionResult();
340
+ let score = 0;
341
+
342
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
343
+ if (deps['react']) {
344
+ score += 40;
345
+ result.evidence.push('react dependency found');
346
+ }
347
+
348
+ if (deps['react-dom']) {
349
+ score += 10;
350
+ result.evidence.push('react-dom found');
351
+ }
352
+
353
+ // Check for CRA (react-scripts)
354
+ if (deps['react-scripts']) {
355
+ score += 30;
356
+ result.evidence.push('react-scripts found (Create React App)');
357
+ result.devCommand = pkg?.scripts?.start || 'react-scripts start';
358
+ result.defaultPortCandidates = [3000];
359
+ }
360
+
361
+ // Check for Vite + React
362
+ if (deps['vite'] && deps['@vitejs/plugin-react']) {
363
+ score += 25;
364
+ result.evidence.push('vite + @vitejs/plugin-react found');
365
+ result.devCommand = pkg?.scripts?.dev || 'vite';
366
+ result.defaultPortCandidates = [5173];
367
+ }
368
+
369
+ // Check vite.config.js with react plugin
370
+ const viteConfigs = filesExist(projectRoot, ['vite.config.js', 'vite.config.ts']);
371
+ if (viteConfigs.length > 0) {
372
+ score += 10;
373
+ result.evidence.push(`vite config found: ${viteConfigs.join(', ')}`);
374
+ }
375
+
376
+ // Check src/App.jsx or src/App.tsx
377
+ if (existsSync(resolve(projectRoot, 'src', 'App.jsx')) ||
378
+ existsSync(resolve(projectRoot, 'src', 'App.tsx'))) {
379
+ score += 10;
380
+ result.evidence.push('React App component found');
381
+ }
382
+
383
+ if (score > 0) {
384
+ result.framework = 'react';
385
+ result.confidence = Math.min(score, 100);
386
+ result.devCommand = result.devCommand || pkg?.scripts?.dev || 'vite';
387
+ }
388
+
389
+ return result;
390
+ }
391
+
392
+ /**
393
+ * Detect generic Vite
394
+ */
395
+ function detectVite(projectRoot, pkg) {
396
+ const result = createDetectionResult();
397
+ let score = 0;
398
+
399
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
400
+ if (deps['vite']) {
401
+ score += 40;
402
+ result.evidence.push('vite dependency found (generic Vite project)');
403
+ }
404
+
405
+ const viteConfigs = filesExist(projectRoot, ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']);
406
+ if (viteConfigs.length > 0) {
407
+ score += 30;
408
+ result.evidence.push(`vite config: ${viteConfigs.join(', ')}`);
409
+ }
410
+
411
+ if (pkg?.scripts?.dev?.includes('vite')) {
412
+ score += 10;
413
+ result.evidence.push('vite in dev script');
414
+ }
415
+
416
+ if (score > 0) {
417
+ result.framework = 'vite';
418
+ result.confidence = Math.min(score, 100);
419
+ result.devCommand = pkg?.scripts?.dev || 'vite';
420
+ result.defaultPortCandidates = [5173];
421
+ }
422
+
423
+ return result;
424
+ }
425
+
426
+ /**
427
+ * Detect generic static/HTML project
428
+ */
429
+ function detectStatic(projectRoot, pkg) {
430
+ const result = createDetectionResult();
431
+ let score = 0;
432
+
433
+ // Check for index.html
434
+ if (existsSync(resolve(projectRoot, 'index.html'))) {
435
+ score += 30;
436
+ result.evidence.push('index.html found (static site)');
437
+ }
438
+
439
+ // Check for dist (built static)
440
+ if (existsSync(resolve(projectRoot, 'dist'))) {
441
+ score += 10;
442
+ result.evidence.push('dist directory found');
443
+ }
444
+
445
+ // No framework detected but has HTML
446
+ if (score > 0 && (!pkg || Object.keys(pkg?.dependencies || {}).length === 0)) {
447
+ result.framework = 'static';
448
+ result.confidence = Math.min(score, 100);
449
+ result.devCommand = pkg?.scripts?.serve || 'http-server .';
450
+ result.defaultPortCandidates = [8000, 8080, 5000];
451
+ }
452
+
453
+ return result;
454
+ }
455
+
456
+ /**
457
+ * Main framework detection function
458
+ *
459
+ * Returns most confident match, ordered by detection priority
460
+ */
461
+ export function detectFramework(projectRoot) {
462
+ const pkg = loadPackageJson(projectRoot);
463
+
464
+ // Priority-ordered detection (deterministic order)
465
+ const detections = [
466
+ detectNextJs(projectRoot, pkg),
467
+ detectVue(projectRoot, pkg),
468
+ detectSvelte(projectRoot, pkg),
469
+ detectAngular(projectRoot, pkg),
470
+ detectRemix(projectRoot, pkg),
471
+ detectReact(projectRoot, pkg),
472
+ detectVite(projectRoot, pkg),
473
+ detectStatic(projectRoot, pkg),
474
+ ];
475
+
476
+ // Filter out results with 0 confidence
477
+ const scored = detections.filter(d => d.confidence > 0);
478
+
479
+ // Sort by confidence descending (deterministic tie-break by framework name)
480
+ scored.sort((a, b) => {
481
+ if (b.confidence !== a.confidence) {
482
+ return b.confidence - a.confidence;
483
+ }
484
+ return a.framework.localeCompare(b.framework);
485
+ });
486
+
487
+ // Return highest confidence, or unknown
488
+ if (scored.length > 0) {
489
+ return scored[0];
490
+ }
491
+
492
+ const result = createDetectionResult();
493
+ if (pkg) {
494
+ result.evidence.push('package.json found but no framework detected');
495
+ result.defaultPortCandidates = [3000, 5173, 8080];
496
+ } else {
497
+ result.evidence.push('No package.json found; assuming static site');
498
+ }
499
+ return result;
500
+ }
501
+
502
+ /**
503
+ * Find app root candidates in a monorepo
504
+ * Scans workspace for package.json + framework markers
505
+ * Returns deterministically ordered list of candidates with evidence
506
+ */
507
+ export function findAppRootCandidates(workspaceRoot, maxDepth = 3) {
508
+ const candidates = [];
509
+
510
+ function scanDir(currentPath, depth) {
511
+ if (depth > maxDepth) return;
512
+
513
+ const dirs = listDirs(currentPath);
514
+ for (const dir of dirs) {
515
+ // Skip common non-app directories
516
+ if (['node_modules', '.git', '.next', 'dist', 'build', '.svelte-kit'].includes(dir)) {
517
+ continue;
518
+ }
519
+
520
+ const appPath = resolve(currentPath, dir);
521
+ const pkg = loadPackageJson(appPath);
522
+
523
+ // If has package.json with scripts.dev or framework markers, it's a candidate
524
+ if (pkg?.scripts?.dev || detectFramework(appPath).confidence > 20) {
525
+ const detection = detectFramework(appPath);
526
+ candidates.push({
527
+ path: appPath,
528
+ framework: detection.framework,
529
+ confidence: detection.confidence,
530
+ hasDevScript: Boolean(pkg?.scripts?.dev),
531
+ depth,
532
+ });
533
+ }
534
+
535
+ // Recurse
536
+ scanDir(appPath, depth + 1);
537
+ }
538
+ }
539
+
540
+ scanDir(workspaceRoot, 0);
541
+
542
+ // Sort candidates deterministically:
543
+ // 1. Highest framework confidence
544
+ // 2. Has dev script (true > false)
545
+ // 3. Shallowest depth
546
+ // 4. Alphabetical path
547
+ candidates.sort((a, b) => {
548
+ if (b.confidence !== a.confidence) return b.confidence - a.confidence;
549
+ if (b.hasDevScript !== a.hasDevScript) return b.hasDevScript ? 1 : -1;
550
+ if (a.depth !== b.depth) return a.depth - b.depth;
551
+ return a.path.localeCompare(b.path);
552
+ });
553
+
554
+ return candidates;
555
+ }
556
+
557
+ /**
558
+ * Export for testing
559
+ */
560
+ export const _internal = {
561
+ loadPackageJson,
562
+ filesExist,
563
+ listDirs,
564
+ detectNextJs,
565
+ detectVue,
566
+ detectSvelte,
567
+ detectAngular,
568
+ detectRemix,
569
+ detectReact,
570
+ detectVite,
571
+ detectStatic,
572
+ };
@@ -35,7 +35,7 @@ export function expIdFromHash(file, line, column, kind, value) {
35
35
  const hash = crypto.createHash('sha256').update(hashInput).digest('hex');
36
36
 
37
37
  // Use first 6 characters for brevity while maintaining low collision risk
38
- const hashSuffix = hash.substring(0, 6);
38
+ const hashSuffix = String(hash).substring(0, 6);
39
39
 
40
40
  return `exp_${hashSuffix}`;
41
41
  }