@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.
- package/README.md +28 -20
- package/bin/verax.js +11 -18
- package/package.json +28 -7
- package/src/cli/commands/baseline.js +1 -2
- package/src/cli/commands/default.js +72 -81
- package/src/cli/commands/doctor.js +29 -0
- package/src/cli/commands/ga.js +3 -0
- package/src/cli/commands/gates.js +1 -1
- package/src/cli/commands/inspect.js +6 -133
- package/src/cli/commands/release-check.js +2 -0
- package/src/cli/commands/run.js +74 -246
- package/src/cli/commands/security-check.js +2 -1
- package/src/cli/commands/truth.js +0 -1
- package/src/cli/entry.js +82 -309
- package/src/cli/util/angular-component-extractor.js +2 -2
- package/src/cli/util/angular-navigation-detector.js +2 -2
- package/src/cli/util/ast-interactive-detector.js +4 -6
- package/src/cli/util/ast-network-detector.js +3 -3
- package/src/cli/util/ast-promise-extractor.js +581 -0
- package/src/cli/util/ast-usestate-detector.js +3 -3
- package/src/cli/util/atomic-write.js +12 -1
- package/src/cli/util/console-reporter.js +72 -0
- package/src/cli/util/detection-engine.js +105 -41
- package/src/cli/util/determinism-runner.js +2 -1
- package/src/cli/util/determinism-writer.js +1 -1
- package/src/cli/util/digest-engine.js +359 -0
- package/src/cli/util/dom-diff.js +226 -0
- package/src/cli/util/env-url.js +0 -4
- package/src/cli/util/evidence-engine.js +287 -0
- package/src/cli/util/expectation-extractor.js +217 -367
- package/src/cli/util/findings-writer.js +19 -126
- package/src/cli/util/framework-detector.js +572 -0
- package/src/cli/util/idgen.js +1 -1
- package/src/cli/util/interaction-planner.js +529 -0
- package/src/cli/util/learn-writer.js +2 -2
- package/src/cli/util/ledger-writer.js +110 -0
- package/src/cli/util/monorepo-resolver.js +162 -0
- package/src/cli/util/observation-engine.js +127 -278
- package/src/cli/util/observe-writer.js +2 -2
- package/src/cli/util/paths.js +12 -3
- package/src/cli/util/project-discovery.js +284 -3
- package/src/cli/util/project-writer.js +2 -2
- package/src/cli/util/run-id.js +23 -27
- package/src/cli/util/run-result.js +778 -0
- package/src/cli/util/selector-resolver.js +235 -0
- package/src/cli/util/summary-writer.js +2 -1
- package/src/cli/util/svelte-navigation-detector.js +3 -3
- package/src/cli/util/svelte-sfc-extractor.js +0 -1
- package/src/cli/util/svelte-state-detector.js +1 -2
- package/src/cli/util/trust-activation-integration.js +496 -0
- package/src/cli/util/trust-activation-wrapper.js +85 -0
- package/src/cli/util/trust-integration-hooks.js +164 -0
- package/src/cli/util/types.js +153 -0
- package/src/cli/util/url-validation.js +40 -0
- package/src/cli/util/vue-navigation-detector.js +4 -3
- package/src/cli/util/vue-sfc-extractor.js +1 -2
- package/src/cli/util/vue-state-detector.js +1 -1
- package/src/types/fs-augment.d.ts +23 -0
- package/src/types/global.d.ts +137 -0
- package/src/types/internal-types.d.ts +35 -0
- package/src/verax/cli/finding-explainer.js +3 -56
- package/src/verax/cli/init.js +4 -18
- package/src/verax/core/action-classifier.js +4 -3
- package/src/verax/core/artifacts/registry.js +0 -15
- package/src/verax/core/artifacts/verifier.js +18 -8
- package/src/verax/core/baseline/baseline.snapshot.js +2 -0
- package/src/verax/core/capabilities/gates.js +7 -1
- package/src/verax/core/confidence/confidence-compute.js +14 -7
- package/src/verax/core/confidence/confidence.loader.js +1 -0
- package/src/verax/core/confidence-engine-refactor.js +8 -3
- package/src/verax/core/confidence-engine.js +162 -23
- package/src/verax/core/contracts/types.js +1 -0
- package/src/verax/core/contracts/validators.js +79 -4
- package/src/verax/core/decision-snapshot.js +3 -30
- package/src/verax/core/decisions/decision.trace.js +2 -0
- package/src/verax/core/determinism/contract-writer.js +2 -2
- package/src/verax/core/determinism/contract.js +1 -1
- package/src/verax/core/determinism/diff.js +42 -1
- package/src/verax/core/determinism/engine.js +7 -6
- package/src/verax/core/determinism/finding-identity.js +3 -2
- package/src/verax/core/determinism/normalize.js +32 -4
- package/src/verax/core/determinism/report-writer.js +1 -0
- package/src/verax/core/determinism/run-fingerprint.js +7 -2
- package/src/verax/core/dynamic-route-intelligence.js +8 -7
- package/src/verax/core/evidence/evidence-capture-service.js +1 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +2 -1
- package/src/verax/core/evidence-builder.js +2 -2
- package/src/verax/core/execution-mode-context.js +1 -1
- package/src/verax/core/execution-mode-detector.js +5 -3
- package/src/verax/core/failures/exit-codes.js +39 -37
- package/src/verax/core/failures/failure-summary.js +1 -1
- package/src/verax/core/failures/failure.factory.js +3 -3
- package/src/verax/core/failures/failure.ledger.js +3 -2
- package/src/verax/core/ga/ga.artifact.js +1 -1
- package/src/verax/core/ga/ga.contract.js +3 -2
- package/src/verax/core/ga/ga.enforcer.js +1 -0
- package/src/verax/core/guardrails/policy.loader.js +1 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +1 -1
- package/src/verax/core/guardrails-engine.js +2 -2
- package/src/verax/core/incremental-store.js +1 -0
- package/src/verax/core/integrity/budget.js +138 -0
- package/src/verax/core/integrity/determinism.js +342 -0
- package/src/verax/core/integrity/integrity.js +208 -0
- package/src/verax/core/integrity/poisoning.js +108 -0
- package/src/verax/core/integrity/transaction.js +140 -0
- package/src/verax/core/observe/run-timeline.js +2 -0
- package/src/verax/core/perf/perf.report.js +2 -0
- package/src/verax/core/pipeline-tracker.js +5 -0
- package/src/verax/core/release/provenance.builder.js +73 -214
- package/src/verax/core/release/release.enforcer.js +14 -9
- package/src/verax/core/release/reproducibility.check.js +1 -0
- package/src/verax/core/release/sbom.builder.js +32 -23
- package/src/verax/core/replay-validator.js +2 -0
- package/src/verax/core/replay.js +4 -0
- package/src/verax/core/report/cross-index.js +6 -3
- package/src/verax/core/report/human-summary.js +141 -1
- package/src/verax/core/route-intelligence.js +4 -3
- package/src/verax/core/run-id.js +6 -3
- package/src/verax/core/run-manifest.js +4 -3
- package/src/verax/core/security/secrets.scan.js +10 -7
- package/src/verax/core/security/security.enforcer.js +4 -0
- package/src/verax/core/security/supplychain.policy.js +9 -1
- package/src/verax/core/security/vuln.scan.js +2 -2
- package/src/verax/core/truth/truth.certificate.js +3 -1
- package/src/verax/core/ui-feedback-intelligence.js +12 -46
- package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
- package/src/verax/detect/confidence-engine.js +100 -660
- package/src/verax/detect/confidence-helper.js +1 -0
- package/src/verax/detect/detection-engine.js +1 -18
- package/src/verax/detect/dynamic-route-findings.js +17 -14
- package/src/verax/detect/expectation-chain-detector.js +1 -1
- package/src/verax/detect/expectation-model.js +3 -5
- package/src/verax/detect/failure-cause-inference.js +293 -0
- package/src/verax/detect/findings-writer.js +126 -166
- package/src/verax/detect/flow-detector.js +2 -2
- package/src/verax/detect/form-silent-failure.js +98 -0
- package/src/verax/detect/index.js +51 -234
- package/src/verax/detect/invariants-enforcer.js +147 -0
- package/src/verax/detect/journey-stall-detector.js +4 -4
- package/src/verax/detect/navigation-silent-failure.js +82 -0
- package/src/verax/detect/problem-aggregator.js +361 -0
- package/src/verax/detect/route-findings.js +7 -6
- package/src/verax/detect/summary-writer.js +477 -0
- package/src/verax/detect/test-failure-cause-inference.js +314 -0
- package/src/verax/detect/ui-feedback-findings.js +18 -18
- package/src/verax/detect/verdict-engine.js +3 -57
- package/src/verax/detect/view-switch-correlator.js +2 -2
- package/src/verax/flow/flow-engine.js +2 -1
- package/src/verax/flow/flow-spec.js +0 -6
- package/src/verax/index.js +48 -412
- package/src/verax/intel/ts-program.js +1 -0
- package/src/verax/intel/vue-navigation-extractor.js +3 -0
- package/src/verax/learn/action-contract-extractor.js +67 -682
- package/src/verax/learn/ast-contract-extractor.js +1 -1
- package/src/verax/learn/flow-extractor.js +1 -0
- package/src/verax/learn/project-detector.js +5 -0
- package/src/verax/learn/react-router-extractor.js +2 -0
- package/src/verax/learn/route-validator.js +1 -4
- package/src/verax/learn/source-instrumenter.js +1 -0
- package/src/verax/learn/state-extractor.js +2 -1
- package/src/verax/learn/static-extractor.js +1 -0
- package/src/verax/observe/coverage-gaps.js +132 -0
- package/src/verax/observe/expectation-handler.js +126 -0
- package/src/verax/observe/incremental-skip.js +46 -0
- package/src/verax/observe/index.js +735 -84
- package/src/verax/observe/interaction-executor.js +192 -0
- package/src/verax/observe/interaction-runner.js +782 -530
- package/src/verax/observe/network-firewall.js +86 -0
- package/src/verax/observe/observation-builder.js +169 -0
- package/src/verax/observe/observe-context.js +1 -1
- package/src/verax/observe/observe-helpers.js +2 -1
- package/src/verax/observe/observe-runner.js +28 -24
- package/src/verax/observe/observers/budget-observer.js +3 -3
- package/src/verax/observe/observers/console-observer.js +4 -4
- package/src/verax/observe/observers/coverage-observer.js +4 -4
- package/src/verax/observe/observers/interaction-observer.js +3 -3
- package/src/verax/observe/observers/navigation-observer.js +4 -4
- package/src/verax/observe/observers/network-observer.js +4 -4
- package/src/verax/observe/observers/safety-observer.js +1 -1
- package/src/verax/observe/observers/ui-feedback-observer.js +4 -4
- package/src/verax/observe/page-traversal.js +138 -0
- package/src/verax/observe/snapshot-ops.js +94 -0
- package/src/verax/observe/ui-signal-sensor.js +2 -148
- package/src/verax/scan-summary-writer.js +10 -42
- package/src/verax/shared/artifact-manager.js +30 -13
- package/src/verax/shared/caching.js +1 -0
- package/src/verax/shared/expectation-tracker.js +1 -0
- package/src/verax/shared/zip-artifacts.js +6 -0
- package/src/verax/core/confidence-engine.js.backup +0 -471
- package/src/verax/shared/config-loader.js +0 -169
- /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, statSync } from 'fs';
|
|
2
2
|
import { join, relative, resolve } from 'path';
|
|
3
3
|
import { expIdFromHash, compareExpectations } from './idgen.js';
|
|
4
|
-
import {
|
|
5
|
-
import { detectUseStatePromises } from './ast-usestate-detector.js';
|
|
6
|
-
import { detectInteractiveElementsAST } from './ast-interactive-detector.js';
|
|
7
|
-
import { extractVueSFC, extractTemplateBindings, mapTemplateHandlersToScript } from './vue-sfc-extractor.js';
|
|
8
|
-
import { detectVueNavigationPromises } from './vue-navigation-detector.js';
|
|
9
|
-
import { detectVueStatePromises } from './vue-state-detector.js';
|
|
10
|
-
import { extractSvelteSFC, extractTemplateBindings as extractSvelteTemplateBindings, mapTemplateHandlersToScript as mapSvelteTemplateHandlersToScript } from './svelte-sfc-extractor.js';
|
|
11
|
-
import { detectSvelteNavigation } from './svelte-navigation-detector.js';
|
|
12
|
-
import { detectSvelteNetwork } from './svelte-network-detector.js';
|
|
13
|
-
import { detectSvelteState } from './svelte-state-detector.js';
|
|
14
|
-
import { extractAngularComponent, extractTemplateBindings as extractAngularTemplateBindings, mapTemplateHandlersToClass } from './angular-component-extractor.js';
|
|
15
|
-
import { detectAngularNavigation } from './angular-navigation-detector.js';
|
|
16
|
-
import { detectAngularNetwork } from './angular-network-detector.js';
|
|
17
|
-
import { detectAngularState } from './angular-state-detector.js';
|
|
4
|
+
import { extractPromisesFromAST } from './ast-promise-extractor.js';
|
|
18
5
|
|
|
19
6
|
/**
|
|
20
7
|
* Static Expectation Extractor
|
|
21
|
-
*
|
|
8
|
+
* PHASE H2/M2: AST-based promise extraction
|
|
9
|
+
* Extracts explicit, static expectations from source files using AST parsing
|
|
22
10
|
*/
|
|
23
11
|
|
|
24
12
|
export async function extractExpectations(projectProfile, _srcPath) {
|
|
@@ -75,7 +63,16 @@ function getScanPaths(projectProfile, sourceRoot) {
|
|
|
75
63
|
}
|
|
76
64
|
|
|
77
65
|
if (framework === 'react-vite' || framework === 'react-cra') {
|
|
78
|
-
|
|
66
|
+
const srcPath = resolve(sourceRoot, 'src');
|
|
67
|
+
try {
|
|
68
|
+
if (statSync(srcPath).isDirectory()) {
|
|
69
|
+
return [srcPath];
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// src doesn't exist - scan root for React files
|
|
73
|
+
}
|
|
74
|
+
// Fallback: scan root directory for React files
|
|
75
|
+
return [sourceRoot];
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
if (framework === 'static-html') {
|
|
@@ -92,7 +89,7 @@ function getScanPaths(projectProfile, sourceRoot) {
|
|
|
92
89
|
}
|
|
93
90
|
}
|
|
94
91
|
|
|
95
|
-
// Unknown framework - scan src if it exists
|
|
92
|
+
// Unknown framework - scan src if it exists, otherwise scan root
|
|
96
93
|
const srcPath = resolve(sourceRoot, 'src');
|
|
97
94
|
try {
|
|
98
95
|
if (statSync(srcPath).isDirectory()) {
|
|
@@ -102,7 +99,8 @@ function getScanPaths(projectProfile, sourceRoot) {
|
|
|
102
99
|
// src doesn't exist
|
|
103
100
|
}
|
|
104
101
|
|
|
105
|
-
|
|
102
|
+
// Fallback: scan root directory
|
|
103
|
+
return [sourceRoot];
|
|
106
104
|
}
|
|
107
105
|
|
|
108
106
|
/**
|
|
@@ -162,8 +160,8 @@ function shouldSkipDirectory(name) {
|
|
|
162
160
|
* Check if file should be scanned
|
|
163
161
|
*/
|
|
164
162
|
function shouldScanFile(name) {
|
|
165
|
-
const extensions = ['.js', '.jsx', '.ts', '.tsx', '.html', '.mjs'
|
|
166
|
-
return extensions.some(ext => name.endsWith(ext))
|
|
163
|
+
const extensions = ['.js', '.jsx', '.ts', '.tsx', '.html', '.mjs'];
|
|
164
|
+
return extensions.some(ext => name.endsWith(ext));
|
|
167
165
|
}
|
|
168
166
|
|
|
169
167
|
/**
|
|
@@ -177,18 +175,15 @@ function scanFile(filePath, sourceRoot, skipped) {
|
|
|
177
175
|
const relPath = relative(sourceRoot, filePath);
|
|
178
176
|
|
|
179
177
|
if (filePath.endsWith('.html')) {
|
|
178
|
+
// HTML files: Use regex-based extraction (static HTML fallback)
|
|
180
179
|
const htmlExpectations = extractHtmlExpectations(content, filePath, relPath);
|
|
181
180
|
expectations.push(...htmlExpectations);
|
|
182
|
-
} else if (filePath.endsWith('.vue')) {
|
|
183
|
-
const vueExpectations = extractVueExpectations(content, filePath, relPath, skipped);
|
|
184
|
-
expectations.push(...vueExpectations);
|
|
185
|
-
} else if (filePath.endsWith('.svelte')) {
|
|
186
|
-
const svelteExpectations = extractSvelteExpectations(content, filePath, relPath, skipped);
|
|
187
|
-
expectations.push(...svelteExpectations);
|
|
188
|
-
} else if (filePath.endsWith('.component.ts') || (filePath.endsWith('.ts') && content.includes('@Component'))) {
|
|
189
|
-
const angularExpectations = extractAngularExpectations(content, filePath, relPath, skipped);
|
|
190
|
-
expectations.push(...angularExpectations);
|
|
191
181
|
} else {
|
|
182
|
+
// JS/JSX/TS/TSX files: Use AST-based extraction (PHASE H2)
|
|
183
|
+
const astPromises = extractPromisesFromAST(content, filePath, relPath);
|
|
184
|
+
expectations.push(...astPromises);
|
|
185
|
+
|
|
186
|
+
// Legacy regex-based extraction (preserved for navigation/network/state)
|
|
192
187
|
const jsExpectations = extractJsExpectations(content, filePath, relPath, skipped);
|
|
193
188
|
expectations.push(...jsExpectations);
|
|
194
189
|
}
|
|
@@ -201,13 +196,12 @@ function scanFile(filePath, sourceRoot, skipped) {
|
|
|
201
196
|
|
|
202
197
|
/**
|
|
203
198
|
* Extract expectations from HTML files
|
|
204
|
-
* PHASE
|
|
199
|
+
* PHASE H2: Enhanced with button, form, and validation detection
|
|
205
200
|
*/
|
|
206
201
|
function extractHtmlExpectations(content, filePath, relPath) {
|
|
207
202
|
const expectations = [];
|
|
208
|
-
const skipped = { dynamic: 0, computed: 0, external: 0, parseError: 0, other: 0 };
|
|
209
203
|
|
|
210
|
-
// Extract <a href="/path"> links
|
|
204
|
+
// Extract <a href="/path"> links (navigation)
|
|
211
205
|
const hrefRegex = /<a\s+[^>]*href=["']([^"']+)["']/gi;
|
|
212
206
|
let match;
|
|
213
207
|
|
|
@@ -233,233 +227,143 @@ function extractHtmlExpectations(content, filePath, relPath) {
|
|
|
233
227
|
}
|
|
234
228
|
}
|
|
235
229
|
|
|
236
|
-
//
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
while ((scriptMatch = scriptRegex.exec(content)) !== null) {
|
|
242
|
-
const scriptContent = scriptMatch[1].trim();
|
|
243
|
-
const scriptStartIndex = scriptMatch.index;
|
|
244
|
-
const scriptStartLine = content.substring(0, scriptStartIndex).split('\n').length;
|
|
230
|
+
// Extract <button> elements (interaction promise)
|
|
231
|
+
const buttonRegex = /<button\s+[^>]*>/gi;
|
|
232
|
+
while ((match = buttonRegex.exec(content)) !== null) {
|
|
233
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
234
|
+
const buttonText = extractTextFromTag(content, match.index, 'button');
|
|
245
235
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
236
|
+
expectations.push({
|
|
237
|
+
category: 'button',
|
|
238
|
+
type: 'interaction',
|
|
239
|
+
promise: {
|
|
240
|
+
kind: 'click',
|
|
241
|
+
value: buttonText || 'button click',
|
|
242
|
+
},
|
|
243
|
+
source: {
|
|
244
|
+
file: relPath,
|
|
245
|
+
line: lineNum,
|
|
246
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
247
|
+
},
|
|
248
|
+
selector: buttonText ? `button:contains("${buttonText}")` : 'button',
|
|
249
|
+
action: 'click',
|
|
250
|
+
expectedOutcome: 'ui-change',
|
|
251
|
+
confidenceHint: 'low',
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Extract <form> elements (submission promise)
|
|
256
|
+
const formRegex = /<form\s+[^>]*>/gi;
|
|
257
|
+
while ((match = formRegex.exec(content)) !== null) {
|
|
258
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
259
|
+
const formTag = match[0];
|
|
250
260
|
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
for (const call of networkCalls) {
|
|
256
|
-
const url = call.url;
|
|
257
|
-
|
|
258
|
-
// Skip dynamic URLs but count them
|
|
259
|
-
if (url === '<dynamic>') {
|
|
260
|
-
skipped.dynamic++;
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Only extract absolute URLs (http/https) or relative API paths
|
|
265
|
-
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/api/')) {
|
|
266
|
-
// Calculate correct line number in HTML file
|
|
267
|
-
const callLineInScript = call.location.line || 1;
|
|
268
|
-
const htmlLineNumber = scriptStartLine + callLineInScript - 1;
|
|
269
|
-
|
|
270
|
-
expectations.push({
|
|
271
|
-
type: 'network',
|
|
272
|
-
promise: {
|
|
273
|
-
kind: 'request',
|
|
274
|
-
value: url,
|
|
275
|
-
method: call.method,
|
|
276
|
-
},
|
|
277
|
-
source: {
|
|
278
|
-
file: relPath,
|
|
279
|
-
line: htmlLineNumber,
|
|
280
|
-
column: call.location.column || 0,
|
|
281
|
-
context: call.context,
|
|
282
|
-
astSource: call.astSource || null,
|
|
283
|
-
},
|
|
284
|
-
confidence: call.isUIBound ? 1.0 : 0.9,
|
|
285
|
-
metadata: {
|
|
286
|
-
networkKind: call.kind,
|
|
287
|
-
isUIBound: call.isUIBound || false,
|
|
288
|
-
astSource: call.astSource || null,
|
|
289
|
-
},
|
|
290
|
-
});
|
|
291
|
-
} else {
|
|
292
|
-
skipped.external++;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
} catch (error) {
|
|
296
|
-
skipped.parseError++;
|
|
297
|
-
}
|
|
261
|
+
// Check for action attribute
|
|
262
|
+
const actionMatch = /action=["']([^"']+)["']/.exec(formTag);
|
|
263
|
+
const hasAction = actionMatch && actionMatch[1];
|
|
298
264
|
|
|
299
|
-
|
|
265
|
+
expectations.push({
|
|
266
|
+
category: 'form',
|
|
267
|
+
type: 'interaction',
|
|
268
|
+
promise: {
|
|
269
|
+
kind: 'submit',
|
|
270
|
+
value: hasAction ? `form submit to ${actionMatch[1]}` : 'form submission',
|
|
271
|
+
},
|
|
272
|
+
source: {
|
|
273
|
+
file: relPath,
|
|
274
|
+
line: lineNum,
|
|
275
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
276
|
+
},
|
|
277
|
+
selector: hasAction ? `form[action="${actionMatch[1]}"]` : 'form',
|
|
278
|
+
action: 'submit',
|
|
279
|
+
expectedOutcome: hasAction ? 'navigation' : 'ui-change',
|
|
280
|
+
confidenceHint: hasAction ? 'medium' : 'low',
|
|
281
|
+
});
|
|
300
282
|
}
|
|
301
283
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const networkCalls = detectNetworkCallsAST(content, filePath, relPath);
|
|
314
|
-
for (const call of networkCalls) {
|
|
315
|
-
const url = call.url;
|
|
316
|
-
|
|
317
|
-
// Skip dynamic URLs but count them
|
|
318
|
-
if (url === '<dynamic>') {
|
|
319
|
-
skipped.dynamic++;
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Only extract absolute URLs (http/https)
|
|
324
|
-
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
325
|
-
expectations.push({
|
|
326
|
-
type: 'network',
|
|
327
|
-
promise: {
|
|
328
|
-
kind: 'request',
|
|
329
|
-
value: url,
|
|
330
|
-
method: call.method,
|
|
331
|
-
},
|
|
332
|
-
source: {
|
|
333
|
-
file: relPath,
|
|
334
|
-
line: call.location.line,
|
|
335
|
-
column: call.location.column,
|
|
336
|
-
context: call.context,
|
|
337
|
-
// PHASE 9: Include AST source code for evidence
|
|
338
|
-
astSource: call.astSource || null,
|
|
339
|
-
},
|
|
340
|
-
confidence: call.isUIBound ? 1.0 : 0.9, // Higher confidence for UI-bound handlers
|
|
341
|
-
metadata: {
|
|
342
|
-
networkKind: call.kind,
|
|
343
|
-
isUIBound: call.isUIBound || false,
|
|
344
|
-
// PHASE 9: Include AST source in metadata for evidence generation
|
|
345
|
-
astSource: call.astSource || null,
|
|
346
|
-
},
|
|
347
|
-
});
|
|
348
|
-
} else {
|
|
349
|
-
// Relative or non-http URLs
|
|
350
|
-
skipped.external++;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// PHASE 10: AST-based useState detection (state-driven UI promises)
|
|
355
|
-
const statePromises = detectUseStatePromises(content, filePath, relPath);
|
|
356
|
-
for (const statePromise of statePromises) {
|
|
357
|
-
// PHASE 10: Extract context and AST source from first setter call
|
|
358
|
-
const firstSetterCall = statePromise.metadata.setterCalls?.[0];
|
|
359
|
-
const context = firstSetterCall?.context || `component:${statePromise.componentName}`;
|
|
360
|
-
const astSource = firstSetterCall?.astSource || null;
|
|
361
|
-
const isUIBound = firstSetterCall?.isUIBound || false;
|
|
284
|
+
// Extract required input fields (validation promise)
|
|
285
|
+
const requiredRegex = /<input\s+[^>]*required[^>]*>/gi;
|
|
286
|
+
while ((match = requiredRegex.exec(content)) !== null) {
|
|
287
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
288
|
+
const inputTag = match[0];
|
|
289
|
+
|
|
290
|
+
// Extract name or id for selector
|
|
291
|
+
const nameMatch = /name=["']([^"']+)["']/.exec(inputTag);
|
|
292
|
+
const idMatch = /id=["']([^"']+)["']/.exec(inputTag);
|
|
293
|
+
const selector = nameMatch ? `input[name="${nameMatch[1]}"]` :
|
|
294
|
+
idMatch ? `input#${idMatch[1]}` : 'input[required]';
|
|
362
295
|
|
|
363
296
|
expectations.push({
|
|
364
|
-
|
|
297
|
+
category: 'validation',
|
|
298
|
+
type: 'feedback',
|
|
365
299
|
promise: {
|
|
366
|
-
kind: '
|
|
367
|
-
value:
|
|
368
|
-
stateName: statePromise.stateName,
|
|
369
|
-
setterName: statePromise.setterName,
|
|
300
|
+
kind: 'validation',
|
|
301
|
+
value: 'required field validation',
|
|
370
302
|
},
|
|
371
303
|
source: {
|
|
372
304
|
file: relPath,
|
|
373
|
-
line:
|
|
374
|
-
column:
|
|
375
|
-
context: context, // PHASE 10: Enhanced context (handler/hook/function)
|
|
376
|
-
astSource: astSource, // PHASE 10: AST source for evidence
|
|
377
|
-
},
|
|
378
|
-
confidence: isUIBound ? 1.0 : 0.9, // PHASE 10: Higher confidence for UI-bound handlers
|
|
379
|
-
metadata: {
|
|
380
|
-
componentName: statePromise.componentName,
|
|
381
|
-
setterCallCount: statePromise.setterCallCount,
|
|
382
|
-
jsxUsageCount: statePromise.jsxUsageCount,
|
|
383
|
-
usageTypes: statePromise.usageTypes,
|
|
384
|
-
hasUpdaterFunction: statePromise.metadata.hasUpdaterFunction,
|
|
385
|
-
// PHASE 10: Include all setter calls with context and AST source
|
|
386
|
-
setterCalls: statePromise.metadata.setterCalls || [],
|
|
387
|
-
isUIBound: isUIBound,
|
|
388
|
-
astSource: astSource, // PHASE 10: AST source in metadata for evidence generation
|
|
305
|
+
line: lineNum,
|
|
306
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
389
307
|
},
|
|
308
|
+
selector,
|
|
309
|
+
action: 'observe',
|
|
310
|
+
expectedOutcome: 'feedback',
|
|
311
|
+
confidenceHint: 'medium',
|
|
390
312
|
});
|
|
391
313
|
}
|
|
392
314
|
|
|
393
|
-
//
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
line: element.location.line,
|
|
416
|
-
column: element.location.column,
|
|
417
|
-
context: element.context,
|
|
418
|
-
astSource: element.astSource || promise.astSource || null,
|
|
419
|
-
},
|
|
420
|
-
confidence: element.isUIBound ? 1.0 : 0.9,
|
|
421
|
-
metadata: {
|
|
422
|
-
tagName: element.tagName,
|
|
423
|
-
role: element.role,
|
|
424
|
-
hasOnClick: element.hasOnClick,
|
|
425
|
-
hasOnSubmit: element.hasOnSubmit,
|
|
426
|
-
isRouterLink: element.isRouterLink,
|
|
427
|
-
navigationType: promise.type,
|
|
428
|
-
selectorHint: element.selectorHint,
|
|
429
|
-
isUIBound: element.isUIBound,
|
|
430
|
-
astSource: element.astSource || promise.astSource || null,
|
|
431
|
-
},
|
|
432
|
-
});
|
|
433
|
-
} else if (element.isRouterLink && (element.linkTo || element.linkHref)) {
|
|
434
|
-
// Router Link component with static href/to
|
|
435
|
-
const target = element.linkTo || element.linkHref;
|
|
436
|
-
|
|
437
|
-
if (target && target !== '<dynamic>') {
|
|
438
|
-
expectations.push({
|
|
439
|
-
type: 'navigation',
|
|
440
|
-
promise: {
|
|
441
|
-
kind: 'navigate',
|
|
442
|
-
value: target,
|
|
443
|
-
},
|
|
444
|
-
source: {
|
|
445
|
-
file: relPath,
|
|
446
|
-
line: element.location.line,
|
|
447
|
-
column: element.location.column,
|
|
448
|
-
context: element.context,
|
|
449
|
-
astSource: element.astSource || null,
|
|
450
|
-
},
|
|
451
|
-
confidence: 1.0,
|
|
452
|
-
metadata: {
|
|
453
|
-
tagName: element.tagName,
|
|
454
|
-
isRouterLink: true,
|
|
455
|
-
selectorHint: element.selectorHint,
|
|
456
|
-
astSource: element.astSource || null,
|
|
457
|
-
},
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
}
|
|
315
|
+
// Extract aria-live regions (feedback promise)
|
|
316
|
+
const ariaLiveRegex = /<[^>]+aria-live=["']([^"']+)["'][^>]*>/gi;
|
|
317
|
+
while ((match = ariaLiveRegex.exec(content)) !== null) {
|
|
318
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
319
|
+
|
|
320
|
+
expectations.push({
|
|
321
|
+
category: 'feedback',
|
|
322
|
+
type: 'feedback',
|
|
323
|
+
promise: {
|
|
324
|
+
kind: 'ui-feedback',
|
|
325
|
+
value: 'live region update',
|
|
326
|
+
},
|
|
327
|
+
source: {
|
|
328
|
+
file: relPath,
|
|
329
|
+
line: lineNum,
|
|
330
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
331
|
+
},
|
|
332
|
+
selector: '[aria-live]',
|
|
333
|
+
action: 'observe',
|
|
334
|
+
expectedOutcome: 'feedback',
|
|
335
|
+
confidenceHint: 'medium',
|
|
336
|
+
});
|
|
461
337
|
}
|
|
462
338
|
|
|
339
|
+
return expectations;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Extract text content from HTML tag
|
|
344
|
+
*/
|
|
345
|
+
function extractTextFromTag(content, startIndex, tagName) {
|
|
346
|
+
const closeTagRegex = new RegExp(`</${tagName}>`, 'i');
|
|
347
|
+
const closeMatch = closeTagRegex.exec(content.substring(startIndex));
|
|
348
|
+
|
|
349
|
+
if (!closeMatch) return '';
|
|
350
|
+
|
|
351
|
+
const endOfOpenTag = content.indexOf('>', startIndex);
|
|
352
|
+
if (endOfOpenTag === -1) return '';
|
|
353
|
+
|
|
354
|
+
const textContent = content.substring(endOfOpenTag + 1, startIndex + closeMatch.index);
|
|
355
|
+
|
|
356
|
+
// Strip HTML tags and trim
|
|
357
|
+
return textContent.replace(/<[^>]+>/g, '').trim();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Extract expectations from JavaScript/TypeScript files
|
|
362
|
+
*/
|
|
363
|
+
function extractJsExpectations(content, filePath, relPath, skipped) {
|
|
364
|
+
const expectations = [];
|
|
365
|
+
const lines = content.split('\n');
|
|
366
|
+
|
|
463
367
|
lines.forEach((line, lineIdx) => {
|
|
464
368
|
const lineNum = lineIdx + 1;
|
|
465
369
|
|
|
@@ -518,6 +422,77 @@ function extractJsExpectations(content, filePath, relPath, skipped) {
|
|
|
518
422
|
}
|
|
519
423
|
}
|
|
520
424
|
|
|
425
|
+
// Extract fetch("https://...")
|
|
426
|
+
const fetchRegex = /fetch\(["']([^"']+)["']\)/g;
|
|
427
|
+
while ((match = fetchRegex.exec(line)) !== null) {
|
|
428
|
+
const url = match[1];
|
|
429
|
+
|
|
430
|
+
// Only extract absolute URLs (https://)
|
|
431
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
432
|
+
expectations.push({
|
|
433
|
+
type: 'network',
|
|
434
|
+
promise: {
|
|
435
|
+
kind: 'request',
|
|
436
|
+
value: url,
|
|
437
|
+
},
|
|
438
|
+
source: {
|
|
439
|
+
file: relPath,
|
|
440
|
+
line: lineNum,
|
|
441
|
+
column: match.index,
|
|
442
|
+
},
|
|
443
|
+
confidence: 1.0,
|
|
444
|
+
});
|
|
445
|
+
} else if (!url.includes('${') && !url.includes('+') && !url.includes('`')) {
|
|
446
|
+
skipped.external++;
|
|
447
|
+
} else {
|
|
448
|
+
skipped.dynamic++;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Extract axios.get/post("https://...")
|
|
453
|
+
const axiosRegex = /axios\.(get|post|put|delete|patch)\(["']([^"']+)["']\)/g;
|
|
454
|
+
while ((match = axiosRegex.exec(line)) !== null) {
|
|
455
|
+
const url = match[2];
|
|
456
|
+
|
|
457
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
458
|
+
expectations.push({
|
|
459
|
+
type: 'network',
|
|
460
|
+
promise: {
|
|
461
|
+
kind: 'request',
|
|
462
|
+
value: url,
|
|
463
|
+
},
|
|
464
|
+
source: {
|
|
465
|
+
file: relPath,
|
|
466
|
+
line: lineNum,
|
|
467
|
+
column: match.index,
|
|
468
|
+
},
|
|
469
|
+
confidence: 1.0,
|
|
470
|
+
});
|
|
471
|
+
} else if (!url.includes('${') && !url.includes('+') && !url.includes('`')) {
|
|
472
|
+
skipped.external++;
|
|
473
|
+
} else {
|
|
474
|
+
skipped.dynamic++;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Extract useState setters
|
|
479
|
+
const useStateRegex = /useState\([^)]*\)/g;
|
|
480
|
+
if ((match = useStateRegex.exec(line)) !== null) {
|
|
481
|
+
expectations.push({
|
|
482
|
+
type: 'state',
|
|
483
|
+
promise: {
|
|
484
|
+
kind: 'state_mutation',
|
|
485
|
+
value: 'state management',
|
|
486
|
+
},
|
|
487
|
+
source: {
|
|
488
|
+
file: relPath,
|
|
489
|
+
line: lineNum,
|
|
490
|
+
column: match.index,
|
|
491
|
+
},
|
|
492
|
+
confidence: 0.8,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
521
496
|
// Extract Redux dispatch calls
|
|
522
497
|
const dispatchRegex = /dispatch\(\{/g;
|
|
523
498
|
if ((match = dispatchRegex.exec(line)) !== null) {
|
|
@@ -557,128 +532,3 @@ function extractJsExpectations(content, filePath, relPath, skipped) {
|
|
|
557
532
|
|
|
558
533
|
return expectations;
|
|
559
534
|
}
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* PHASE 20: Extract expectations from Vue SFC files
|
|
563
|
-
*/
|
|
564
|
-
function extractVueExpectations(content, filePath, relPath, skipped) {
|
|
565
|
-
const expectations = [];
|
|
566
|
-
|
|
567
|
-
try {
|
|
568
|
-
// Extract SFC blocks
|
|
569
|
-
const sfc = extractVueSFC(content);
|
|
570
|
-
|
|
571
|
-
// Extract template bindings
|
|
572
|
-
let templateBindings = null;
|
|
573
|
-
if (sfc.template) {
|
|
574
|
-
templateBindings = extractTemplateBindings(sfc.template.content);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Process each script block
|
|
578
|
-
for (const scriptBlock of sfc.scriptBlocks) {
|
|
579
|
-
const scriptContent = scriptBlock.content;
|
|
580
|
-
const startLine = scriptBlock.startLine;
|
|
581
|
-
|
|
582
|
-
// Map template handlers to script functions
|
|
583
|
-
let handlerMap = new Map();
|
|
584
|
-
if (templateBindings) {
|
|
585
|
-
handlerMap = mapTemplateHandlersToScript(templateBindings, [scriptBlock]);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// PHASE 20: Detect Vue navigation promises (router-link, router.push/replace)
|
|
589
|
-
const navigationPromises = detectVueNavigationPromises(
|
|
590
|
-
scriptContent,
|
|
591
|
-
filePath,
|
|
592
|
-
relPath,
|
|
593
|
-
scriptBlock,
|
|
594
|
-
templateBindings
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
for (const navPromise of navigationPromises) {
|
|
598
|
-
// Adjust line numbers for SFC
|
|
599
|
-
navPromise.source.line = startLine + (navPromise.source.line || 1) - 1;
|
|
600
|
-
expectations.push(navPromise);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// PHASE 20: Detect network calls in Vue script blocks
|
|
604
|
-
const networkCalls = detectNetworkCallsAST(scriptContent, filePath, relPath);
|
|
605
|
-
for (const call of networkCalls) {
|
|
606
|
-
const url = call.url;
|
|
607
|
-
|
|
608
|
-
if (url === '<dynamic>') {
|
|
609
|
-
skipped.dynamic++;
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Check if handler is UI-bound via template
|
|
614
|
-
const isUIBound = handlerMap.has(call.handlerName || '');
|
|
615
|
-
|
|
616
|
-
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/api/')) {
|
|
617
|
-
expectations.push({
|
|
618
|
-
type: 'network',
|
|
619
|
-
promise: {
|
|
620
|
-
kind: 'request',
|
|
621
|
-
value: url,
|
|
622
|
-
method: call.method,
|
|
623
|
-
},
|
|
624
|
-
source: {
|
|
625
|
-
file: relPath,
|
|
626
|
-
line: startLine + (call.location.line || 1) - 1,
|
|
627
|
-
column: call.location.column || 0,
|
|
628
|
-
context: call.context,
|
|
629
|
-
astSource: call.astSource || null,
|
|
630
|
-
},
|
|
631
|
-
confidence: isUIBound ? 1.0 : 0.9,
|
|
632
|
-
metadata: {
|
|
633
|
-
networkKind: call.kind,
|
|
634
|
-
isUIBound: isUIBound || false,
|
|
635
|
-
astSource: call.astSource || null,
|
|
636
|
-
},
|
|
637
|
-
});
|
|
638
|
-
} else {
|
|
639
|
-
skipped.external++;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// PHASE 20: Detect Vue state promises (ref/reactive)
|
|
644
|
-
if (templateBindings) {
|
|
645
|
-
const statePromises = detectVueStatePromises(
|
|
646
|
-
scriptContent,
|
|
647
|
-
filePath,
|
|
648
|
-
relPath,
|
|
649
|
-
scriptBlock,
|
|
650
|
-
templateBindings
|
|
651
|
-
);
|
|
652
|
-
|
|
653
|
-
for (const statePromise of statePromises) {
|
|
654
|
-
statePromise.source.line = startLine + (statePromise.source.line || 1) - 1;
|
|
655
|
-
expectations.push(statePromise);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// PHASE 20: Detect interactive elements from template
|
|
660
|
-
if (templateBindings) {
|
|
661
|
-
for (const routerLink of templateBindings.routerLinks) {
|
|
662
|
-
expectations.push({
|
|
663
|
-
type: 'navigation',
|
|
664
|
-
promise: {
|
|
665
|
-
kind: 'navigate',
|
|
666
|
-
value: routerLink.to,
|
|
667
|
-
},
|
|
668
|
-
source: {
|
|
669
|
-
file: relPath,
|
|
670
|
-
line: sfc.template.startLine,
|
|
671
|
-
column: 0,
|
|
672
|
-
context: 'template',
|
|
673
|
-
},
|
|
674
|
-
confidence: 1.0,
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
} catch (error) {
|
|
680
|
-
skipped.parseError++;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
return expectations;
|
|
684
|
-
}
|