@veraxhq/verax 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +24 -36
- package/src/cli/commands/default.js +681 -0
- package/src/cli/commands/doctor.js +197 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +586 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +297 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +110 -0
- package/src/cli/util/expectation-extractor.js +388 -0
- package/src/cli/util/findings-writer.js +32 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +412 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +30 -0
- package/src/cli/util/project-discovery.js +297 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +43 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +111 -0
- package/src/verax/cli/wizard.js +109 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +432 -0
- package/src/verax/core/incremental-store.js +245 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +523 -0
- package/src/verax/detect/comparison.js +7 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +127 -0
- package/src/verax/detect/expectation-model.js +241 -168
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +41 -12
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +200 -288
- package/src/verax/detect/interactive-findings.js +612 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +561 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +103 -15
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +642 -0
- package/src/verax/intel/vue-router-extractor.js +325 -0
- package/src/verax/learn/action-contract-extractor.js +338 -104
- package/src/verax/learn/ast-contract-extractor.js +148 -6
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +122 -58
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +28 -97
- package/src/verax/learn/route-validator.js +8 -7
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +119 -10
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +30 -6
- package/src/verax/observe/console-sensor.js +2 -18
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +513 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +660 -273
- package/src/verax/observe/index.js +910 -26
- package/src/verax/observe/interaction-discovery.js +378 -15
- package/src/verax/observe/interaction-runner.js +562 -197
- package/src/verax/observe/loading-sensor.js +145 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +38 -17
- package/src/verax/observe/state-sensor.js +393 -0
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +73 -21
- package/src/verax/observe/ui-signal-sensor.js +143 -17
- package/src/verax/scan-summary-writer.js +80 -15
- package/src/verax/shared/artifact-manager.js +111 -9
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +169 -0
- package/src/verax/shared/dynamic-route-utils.js +224 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +9 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +66 -0
- package/src/verax/validate/context-validator.js +244 -0
|
@@ -4,6 +4,7 @@ import { readFileSync } from 'fs';
|
|
|
4
4
|
import { glob } from 'glob';
|
|
5
5
|
import { resolve } from 'path';
|
|
6
6
|
import { ExpectationProof } from '../shared/expectation-proof.js';
|
|
7
|
+
import { normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';
|
|
7
8
|
|
|
8
9
|
const MAX_FILES_TO_SCAN = 200;
|
|
9
10
|
|
|
@@ -20,6 +21,11 @@ function extractStaticStringValue(node) {
|
|
|
20
21
|
if (node.type === 'StringLiteral') {
|
|
21
22
|
return node.value;
|
|
22
23
|
}
|
|
24
|
+
|
|
25
|
+
// Template literal without interpolation
|
|
26
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0 && node.quasis.length === 1) {
|
|
27
|
+
return node.quasis[0].value.cooked;
|
|
28
|
+
}
|
|
23
29
|
|
|
24
30
|
// JSX expression: href={'/about'} or href={`/about`}
|
|
25
31
|
if (node.type === 'JSXExpressionContainer') {
|
|
@@ -45,18 +51,89 @@ function extractStaticStringValue(node) {
|
|
|
45
51
|
return null;
|
|
46
52
|
}
|
|
47
53
|
|
|
54
|
+
function extractStaticPropValue(propsNode, propNames) {
|
|
55
|
+
if (!propsNode || propsNode.type !== 'ObjectExpression') return { attributeName: null, targetPath: null };
|
|
56
|
+
const names = new Set(propNames);
|
|
57
|
+
for (const prop of propsNode.properties || []) {
|
|
58
|
+
if (prop.type !== 'ObjectProperty') continue;
|
|
59
|
+
const key = prop.key;
|
|
60
|
+
const name = key.type === 'Identifier' ? key.name : (key.type === 'StringLiteral' ? key.value : null);
|
|
61
|
+
if (!name || !names.has(name)) continue;
|
|
62
|
+
const targetPath = extractStaticStringValue(prop.value);
|
|
63
|
+
if (targetPath) {
|
|
64
|
+
return { attributeName: name, targetPath };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { attributeName: null, targetPath: null };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extracts template literal pattern from Babel TemplateLiteral node.
|
|
72
|
+
* Returns null if template has complex expressions that cannot be normalized.
|
|
73
|
+
*
|
|
74
|
+
* Examples:
|
|
75
|
+
* - `${id}` → { pattern: '${id}', examplePath: '/1' }
|
|
76
|
+
* - `/users/${id}` → { pattern: '/users/${id}', examplePath: '/users/1' }
|
|
77
|
+
* - `/posts/${slug}` → { pattern: '/posts/${slug}', examplePath: '/posts/example' }
|
|
78
|
+
*/
|
|
79
|
+
function extractTemplatePattern(templateNode) {
|
|
80
|
+
if (!templateNode || templateNode.type !== 'TemplateLiteral') {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build the template string with ${} placeholders
|
|
85
|
+
let templateStr = templateNode.quasis[0]?.value?.cooked || '';
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < templateNode.expressions.length; i++) {
|
|
88
|
+
const expr = templateNode.expressions[i];
|
|
89
|
+
|
|
90
|
+
// Only support simple identifiers: ${id}, ${slug}
|
|
91
|
+
if (expr.type === 'Identifier') {
|
|
92
|
+
templateStr += '${' + expr.name + '}';
|
|
93
|
+
} else {
|
|
94
|
+
// Complex expressions like function calls - cannot normalize
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Add the next quasi
|
|
99
|
+
if (templateNode.quasis[i + 1]) {
|
|
100
|
+
templateStr += templateNode.quasis[i + 1].value.cooked || '';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Must be a valid route pattern (start with /)
|
|
105
|
+
if (!templateStr.startsWith('/')) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Normalize to example path
|
|
110
|
+
const normalized = normalizeTemplateLiteral(templateStr);
|
|
111
|
+
if (normalized) {
|
|
112
|
+
return {
|
|
113
|
+
pattern: templateStr,
|
|
114
|
+
examplePath: normalized.examplePath,
|
|
115
|
+
isDynamic: true,
|
|
116
|
+
originalPattern: normalized.originalPattern
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
48
123
|
/**
|
|
49
124
|
* Extracts PROVEN navigation contracts from JSX elements and imperative calls.
|
|
50
125
|
*
|
|
51
|
-
* Supported patterns (all require static string literals):
|
|
52
|
-
* - Next.js: <Link href="/about">
|
|
53
|
-
* - React Router: <Link to="/about"> or <
|
|
126
|
+
* Supported patterns (all require static string literals or template patterns):
|
|
127
|
+
* - Next.js: <Link href="/about"> or <Link href={`/about`}>
|
|
128
|
+
* - React Router: <Link to="/about"> or <RouterLink :to="`/users/${id}`">
|
|
54
129
|
* - Plain JSX: <a href="/about">
|
|
55
|
-
* - Imperative: navigate("/about"), router.push("/about")
|
|
130
|
+
* - Imperative: navigate("/about"), router.push("/about"), router.push(`/users/${id}`)
|
|
56
131
|
*
|
|
57
132
|
* Returns array of contracts with:
|
|
58
133
|
* - kind: 'NAVIGATION'
|
|
59
|
-
* - targetPath: string
|
|
134
|
+
* - targetPath: string (example path for dynamic routes)
|
|
135
|
+
* - originalPattern: string (original pattern for dynamic routes)
|
|
136
|
+
* - isDynamic: boolean (true if dynamic route)
|
|
60
137
|
* - sourceFile: string
|
|
61
138
|
* - element: 'Link' | 'NavLink' | 'a' | 'navigate' | 'router'
|
|
62
139
|
* - attribute: 'href' | 'to' (for runtime matching)
|
|
@@ -120,6 +197,37 @@ function extractContractsFromFile(filePath, fileContent) {
|
|
|
120
197
|
// since we cannot reliably tie them to clicked elements
|
|
121
198
|
CallExpression(path) {
|
|
122
199
|
const callee = path.node.callee;
|
|
200
|
+
|
|
201
|
+
// React.createElement(Link, { to: '/about' }) or createElement('a', { href: '/about' })
|
|
202
|
+
const isCreateElement = (
|
|
203
|
+
(callee.type === 'MemberExpression' && callee.object.type === 'Identifier' && callee.object.name === 'React' && callee.property.type === 'Identifier' && callee.property.name === 'createElement') ||
|
|
204
|
+
(callee.type === 'Identifier' && callee.name === 'createElement')
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (isCreateElement) {
|
|
208
|
+
const [componentArg, propsArg] = path.node.arguments;
|
|
209
|
+
let elementName = null;
|
|
210
|
+
if (componentArg?.type === 'Identifier') {
|
|
211
|
+
elementName = componentArg.name;
|
|
212
|
+
} else if (componentArg?.type === 'StringLiteral') {
|
|
213
|
+
elementName = componentArg.value;
|
|
214
|
+
}
|
|
215
|
+
if (elementName) {
|
|
216
|
+
const { attributeName, targetPath } = extractStaticPropValue(propsArg, ['to', 'href']);
|
|
217
|
+
if (targetPath && !targetPath.startsWith('http://') && !targetPath.startsWith('https://') && !targetPath.startsWith('mailto:') && !targetPath.startsWith('tel:')) {
|
|
218
|
+
const normalized = targetPath.startsWith('/') ? targetPath : '/' + targetPath;
|
|
219
|
+
contracts.push({
|
|
220
|
+
kind: 'NAVIGATION',
|
|
221
|
+
targetPath: normalized,
|
|
222
|
+
sourceFile: filePath,
|
|
223
|
+
element: elementName,
|
|
224
|
+
attribute: attributeName,
|
|
225
|
+
proof: ExpectationProof.PROVEN_EXPECTATION,
|
|
226
|
+
line: path.node.loc?.start.line || null
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
123
231
|
|
|
124
232
|
// navigate("/about") - useNavigate hook
|
|
125
233
|
if (callee.type === 'Identifier' && callee.name === 'navigate') {
|
|
@@ -138,6 +246,23 @@ function extractContractsFromFile(filePath, fileContent) {
|
|
|
138
246
|
imperativeOnly: true // Cannot match to DOM element
|
|
139
247
|
});
|
|
140
248
|
}
|
|
249
|
+
} else if (firstArg && firstArg.type === 'TemplateLiteral') {
|
|
250
|
+
// Template literal: navigate(`/users/${id}`)
|
|
251
|
+
const templatePattern = extractTemplatePattern(firstArg);
|
|
252
|
+
if (templatePattern) {
|
|
253
|
+
contracts.push({
|
|
254
|
+
kind: 'NAVIGATION',
|
|
255
|
+
targetPath: templatePattern.examplePath,
|
|
256
|
+
originalPattern: templatePattern.pattern,
|
|
257
|
+
isDynamic: true,
|
|
258
|
+
sourceFile: filePath,
|
|
259
|
+
element: 'navigate',
|
|
260
|
+
attribute: null,
|
|
261
|
+
proof: ExpectationProof.PROVEN_EXPECTATION,
|
|
262
|
+
line: path.node.loc?.start.line || null,
|
|
263
|
+
imperativeOnly: true // Cannot match to DOM element
|
|
264
|
+
});
|
|
265
|
+
}
|
|
141
266
|
}
|
|
142
267
|
}
|
|
143
268
|
|
|
@@ -162,6 +287,23 @@ function extractContractsFromFile(filePath, fileContent) {
|
|
|
162
287
|
imperativeOnly: true // Cannot match to DOM element
|
|
163
288
|
});
|
|
164
289
|
}
|
|
290
|
+
} else if (firstArg && firstArg.type === 'TemplateLiteral') {
|
|
291
|
+
// Template literal: router.push(`/users/${id}`)
|
|
292
|
+
const templatePattern = extractTemplatePattern(firstArg);
|
|
293
|
+
if (templatePattern) {
|
|
294
|
+
contracts.push({
|
|
295
|
+
kind: 'NAVIGATION',
|
|
296
|
+
targetPath: templatePattern.examplePath,
|
|
297
|
+
originalPattern: templatePattern.pattern,
|
|
298
|
+
isDynamic: true,
|
|
299
|
+
sourceFile: filePath,
|
|
300
|
+
element: 'router',
|
|
301
|
+
attribute: null,
|
|
302
|
+
proof: ExpectationProof.PROVEN_EXPECTATION,
|
|
303
|
+
line: path.node.loc?.start.line || null,
|
|
304
|
+
imperativeOnly: true // Cannot match to DOM element
|
|
305
|
+
});
|
|
306
|
+
}
|
|
165
307
|
}
|
|
166
308
|
}
|
|
167
309
|
}
|
|
@@ -218,7 +360,7 @@ export async function extractASTContracts(projectDir) {
|
|
|
218
360
|
* Converts AST contracts to manifest expectations format.
|
|
219
361
|
* Only includes contracts that can be matched at runtime (excludes imperativeOnly).
|
|
220
362
|
*/
|
|
221
|
-
export function contractsToExpectations(contracts,
|
|
363
|
+
export function contractsToExpectations(contracts, _projectType) {
|
|
222
364
|
const expectations = [];
|
|
223
365
|
const seenPaths = new Set();
|
|
224
366
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FLOW INTELLIGENCE v1 — Flow Extractor
|
|
3
|
+
*
|
|
4
|
+
* Extracts multi-step flows from PROVEN expectations.
|
|
5
|
+
* NO HEURISTICS: Only sequences with explicit PROVEN expectations qualify as flows.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
import { isProvenExpectation } from '../shared/expectation-prover.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate stable flow ID from ordered steps
|
|
13
|
+
*/
|
|
14
|
+
function generateFlowId(steps) {
|
|
15
|
+
const hashInput = steps.map(s => `${s.expectationType}:${s.source || s.handlerRef}`).join('|');
|
|
16
|
+
const hash = createHash('sha256').update(hashInput).digest('hex');
|
|
17
|
+
return `flow-${hash.substring(0, 8)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Determine if an expectation can be part of a flow.
|
|
22
|
+
* Only PROVEN expectations with clear sequential intent qualify.
|
|
23
|
+
*/
|
|
24
|
+
function isFlowableExpectation(exp) {
|
|
25
|
+
// Navigation expectations: always flowable (user journey)
|
|
26
|
+
if (exp.expectationType === 'navigation') return true;
|
|
27
|
+
|
|
28
|
+
// Network actions: flowable (form submissions, API calls)
|
|
29
|
+
if (exp.kind === 'NETWORK_ACTION') return true;
|
|
30
|
+
|
|
31
|
+
// State actions: flowable (state updates in sequence)
|
|
32
|
+
if (exp.kind === 'STATE_ACTION') return true;
|
|
33
|
+
|
|
34
|
+
// Validation blocks: NOT flowable alone (they block, don't progress)
|
|
35
|
+
if (exp.kind === 'VALIDATION_BLOCK') return false;
|
|
36
|
+
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extract flows from manifest expectations.
|
|
42
|
+
*
|
|
43
|
+
* Flow definition:
|
|
44
|
+
* - Sequence of 2-5 PROVEN expectations
|
|
45
|
+
* - Temporally ordered within same route/page
|
|
46
|
+
* - Each step must be deterministically executable
|
|
47
|
+
*
|
|
48
|
+
* @param {Object} manifest - Learned manifest
|
|
49
|
+
* @returns {Array} Array of flow definitions
|
|
50
|
+
*/
|
|
51
|
+
export function extractFlows(manifest) {
|
|
52
|
+
const flows = [];
|
|
53
|
+
|
|
54
|
+
// Collect all flowable expectations
|
|
55
|
+
const flowableExpectations = [];
|
|
56
|
+
|
|
57
|
+
// SPA expectations (navigation)
|
|
58
|
+
if (manifest.spaExpectations && manifest.spaExpectations.length > 0) {
|
|
59
|
+
for (const exp of manifest.spaExpectations) {
|
|
60
|
+
if (isFlowableExpectation(exp)) {
|
|
61
|
+
flowableExpectations.push({
|
|
62
|
+
source: exp.source || exp.sourceFile,
|
|
63
|
+
expectationType: 'navigation',
|
|
64
|
+
targetPath: exp.targetPath,
|
|
65
|
+
proof: 'PROVEN_EXPECTATION',
|
|
66
|
+
raw: exp
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Action contracts (network, state)
|
|
73
|
+
if (manifest.actionContracts && manifest.actionContracts.length > 0) {
|
|
74
|
+
for (const contract of manifest.actionContracts) {
|
|
75
|
+
if (isFlowableExpectation(contract)) {
|
|
76
|
+
flowableExpectations.push({
|
|
77
|
+
source: contract.source,
|
|
78
|
+
handlerRef: contract.handlerRef,
|
|
79
|
+
expectationType: contract.kind === 'NETWORK_ACTION' ? 'network_action' : 'state_action',
|
|
80
|
+
method: contract.method,
|
|
81
|
+
urlPath: contract.urlPath,
|
|
82
|
+
proof: 'PROVEN_EXPECTATION',
|
|
83
|
+
raw: contract
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Static expectations (navigation, network_action, form submissions)
|
|
90
|
+
if (manifest.staticExpectations && manifest.staticExpectations.length > 0) {
|
|
91
|
+
for (const exp of manifest.staticExpectations) {
|
|
92
|
+
// FLOW INTELLIGENCE v1: Only include PROVEN expectations
|
|
93
|
+
if (!isProvenExpectation(exp)) continue;
|
|
94
|
+
|
|
95
|
+
let expType = null;
|
|
96
|
+
if (exp.type === 'navigation' || exp.type === 'spa_navigation') {
|
|
97
|
+
expType = 'navigation';
|
|
98
|
+
} else if (exp.type === 'network_action') {
|
|
99
|
+
expType = 'network_action';
|
|
100
|
+
} else if (exp.type === 'form_submission') {
|
|
101
|
+
expType = 'network_action'; // Form submissions are network actions
|
|
102
|
+
} else {
|
|
103
|
+
continue; // Skip non-flowable types
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
flowableExpectations.push({
|
|
107
|
+
source: exp.evidence?.source || exp.fromPath || exp.metadata?.elementFile,
|
|
108
|
+
handlerRef: exp.handlerRef || exp.metadata?.handlerFile,
|
|
109
|
+
expectationType: expType,
|
|
110
|
+
targetPath: exp.targetPath || exp.expectedTarget,
|
|
111
|
+
urlPath: exp.urlPath || exp.expectedTarget,
|
|
112
|
+
method: exp.method,
|
|
113
|
+
proof: 'PROVEN_EXPECTATION',
|
|
114
|
+
raw: exp
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// DETERMINISTIC FLOW DETECTION: Group by source page
|
|
120
|
+
// Flows are sequences on the same page (same source file/route)
|
|
121
|
+
const bySource = new Map();
|
|
122
|
+
|
|
123
|
+
for (const exp of flowableExpectations) {
|
|
124
|
+
// Extract file name from source (e.g., "index.html" from "index.html:56:68")
|
|
125
|
+
let sourceKey = 'unknown';
|
|
126
|
+
if (exp.source) {
|
|
127
|
+
sourceKey = exp.source.split(':')[0]; // Get filename part
|
|
128
|
+
} else if (exp.handlerRef) {
|
|
129
|
+
sourceKey = exp.handlerRef.split(':')[0];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!bySource.has(sourceKey)) {
|
|
133
|
+
bySource.set(sourceKey, []);
|
|
134
|
+
}
|
|
135
|
+
bySource.get(sourceKey).push(exp);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// For each source, create flows of 2-5 sequential expectations
|
|
139
|
+
for (const [source, expectations] of bySource.entries()) {
|
|
140
|
+
if (expectations.length < 2) continue; // Need at least 2 for a flow
|
|
141
|
+
|
|
142
|
+
// Take sequences of 2-5 expectations as potential flows
|
|
143
|
+
const maxFlowLength = Math.min(5, expectations.length);
|
|
144
|
+
|
|
145
|
+
for (let length = 2; length <= maxFlowLength; length++) {
|
|
146
|
+
// Only create one flow per source (the full sequence up to 5 steps)
|
|
147
|
+
if (length === Math.min(expectations.length, 5)) {
|
|
148
|
+
const flowSteps = expectations.slice(0, length).map((exp, idx) => ({
|
|
149
|
+
stepIndex: idx,
|
|
150
|
+
expectationType: exp.expectationType,
|
|
151
|
+
source: exp.source,
|
|
152
|
+
handlerRef: exp.handlerRef,
|
|
153
|
+
targetPath: exp.targetPath,
|
|
154
|
+
method: exp.method,
|
|
155
|
+
urlPath: exp.urlPath
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
const flowId = generateFlowId(flowSteps);
|
|
159
|
+
|
|
160
|
+
flows.push({
|
|
161
|
+
flowId,
|
|
162
|
+
source,
|
|
163
|
+
stepCount: length,
|
|
164
|
+
steps: flowSteps,
|
|
165
|
+
proof: 'PROVEN_EXPECTATION'
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return flows;
|
|
172
|
+
}
|
package/src/verax/learn/index.js
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
import { resolve } from 'path';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
3
|
import { detectProjectType } from './project-detector.js';
|
|
4
4
|
import { extractRoutes } from './route-extractor.js';
|
|
5
5
|
import { writeManifest } from './manifest-writer.js';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} LearnResult
|
|
9
|
+
* @property {number} version
|
|
10
|
+
* @property {string} learnedAt
|
|
11
|
+
* @property {string} projectDir
|
|
12
|
+
* @property {string} projectType
|
|
13
|
+
* @property {Array} routes
|
|
14
|
+
* @property {Array<string>} publicRoutes
|
|
15
|
+
* @property {Array<string>} internalRoutes
|
|
16
|
+
* @property {Array} [staticExpectations]
|
|
17
|
+
* @property {Array} [flows]
|
|
18
|
+
* @property {string} [expectationsStatus]
|
|
19
|
+
* @property {Array} [coverageGaps]
|
|
20
|
+
* @property {Array} notes
|
|
21
|
+
* @property {Object} [learnTruth]
|
|
22
|
+
* @property {string} [manifestPath] - Optional manifest path (added when loaded from file)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} projectDir
|
|
27
|
+
* @returns {Promise<LearnResult>}
|
|
28
|
+
*/
|
|
7
29
|
export async function learn(projectDir) {
|
|
8
30
|
const absoluteProjectDir = resolve(projectDir);
|
|
9
31
|
|
|
@@ -14,5 +36,17 @@ export async function learn(projectDir) {
|
|
|
14
36
|
const projectType = await detectProjectType(absoluteProjectDir);
|
|
15
37
|
const routes = await extractRoutes(absoluteProjectDir, projectType);
|
|
16
38
|
|
|
17
|
-
|
|
39
|
+
const manifest = await writeManifest(absoluteProjectDir, projectType, routes);
|
|
40
|
+
|
|
41
|
+
// Write manifest to disk and return path
|
|
42
|
+
const veraxDir = resolve(absoluteProjectDir, '.verax');
|
|
43
|
+
mkdirSync(veraxDir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
const manifestPath = resolve(veraxDir, 'project.json');
|
|
46
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
...manifest,
|
|
50
|
+
manifestPath
|
|
51
|
+
};
|
|
18
52
|
}
|
|
@@ -1,12 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
import { writeFileSync, mkdirSync } from 'fs';
|
|
1
|
+
// resolve, writeFileSync, mkdirSync imports removed - currently unused
|
|
3
2
|
import { extractStaticExpectations } from './static-extractor.js';
|
|
4
3
|
import { assessLearnTruth } from './truth-assessor.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
4
|
+
import { runCodeIntelligence } from '../intel/index.js';
|
|
5
|
+
import { isProvenExpectation } from '../shared/expectation-prover.js';
|
|
6
|
+
import { extractFlows } from './flow-extractor.js';
|
|
7
|
+
import { createTSProgram } from '../intel/ts-program.js';
|
|
8
|
+
import { extractVueNavigationPromises } from '../intel/vue-navigation-extractor.js';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} Manifest
|
|
12
|
+
* @property {number} version
|
|
13
|
+
* @property {string} learnedAt
|
|
14
|
+
* @property {string} projectDir
|
|
15
|
+
* @property {string} projectType
|
|
16
|
+
* @property {Array} routes
|
|
17
|
+
* @property {Array<string>} publicRoutes
|
|
18
|
+
* @property {Array<string>} internalRoutes
|
|
19
|
+
* @property {Array} [staticExpectations]
|
|
20
|
+
* @property {Array} [flows]
|
|
21
|
+
* @property {string} [expectationsStatus]
|
|
22
|
+
* @property {Array} [coverageGaps]
|
|
23
|
+
* @property {Array} notes
|
|
24
|
+
* @property {Object} [learnTruth]
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} projectDir
|
|
29
|
+
* @param {string} projectType
|
|
30
|
+
* @param {Array} routes
|
|
31
|
+
* @returns {Promise<Manifest>}
|
|
32
|
+
*/
|
|
10
33
|
export async function writeManifest(projectDir, projectType, routes) {
|
|
11
34
|
const publicRoutes = routes.filter(r => r.public).map(r => r.path);
|
|
12
35
|
const internalRoutes = routes.filter(r => !r.public).map(r => r.path);
|
|
@@ -19,7 +42,8 @@ export async function writeManifest(projectDir, projectType, routes) {
|
|
|
19
42
|
routes: routes.map(r => ({
|
|
20
43
|
path: r.path,
|
|
21
44
|
source: r.source,
|
|
22
|
-
public: r.public
|
|
45
|
+
public: r.public,
|
|
46
|
+
sourceRef: r.sourceRef || null
|
|
23
47
|
})),
|
|
24
48
|
publicRoutes: publicRoutes,
|
|
25
49
|
internalRoutes: internalRoutes,
|
|
@@ -27,71 +51,111 @@ export async function writeManifest(projectDir, projectType, routes) {
|
|
|
27
51
|
};
|
|
28
52
|
|
|
29
53
|
let staticExpectations = null;
|
|
30
|
-
let
|
|
31
|
-
let
|
|
54
|
+
let intelExpectations = [];
|
|
55
|
+
let allExpectations = [];
|
|
32
56
|
|
|
57
|
+
// Static sites: extract from HTML
|
|
33
58
|
if (projectType === 'static' && routes.length > 0) {
|
|
34
59
|
staticExpectations = await extractStaticExpectations(projectDir, routes);
|
|
35
60
|
manifest.staticExpectations = staticExpectations;
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
61
|
+
allExpectations = staticExpectations || [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Vue projects: extract navigation promises from code
|
|
65
|
+
if (projectType === 'vue_router' || projectType === 'vue_spa') {
|
|
66
|
+
const program = createTSProgram(projectDir, { includeJs: true });
|
|
41
67
|
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
68
|
+
if (!program.error) {
|
|
69
|
+
const vueNavPromises = await extractVueNavigationPromises(program, projectDir);
|
|
70
|
+
|
|
71
|
+
if (vueNavPromises && vueNavPromises.length > 0) {
|
|
72
|
+
intelExpectations = vueNavPromises.filter(exp => isProvenExpectation(exp));
|
|
73
|
+
|
|
74
|
+
if (!manifest.staticExpectations) {
|
|
75
|
+
manifest.staticExpectations = [];
|
|
76
|
+
}
|
|
77
|
+
manifest.staticExpectations.push(...intelExpectations);
|
|
78
|
+
allExpectations = intelExpectations;
|
|
79
|
+
}
|
|
47
80
|
}
|
|
48
|
-
} else {
|
|
49
|
-
manifest.expectationProof = ExpectationProof.UNKNOWN_EXPECTATION;
|
|
50
81
|
}
|
|
51
82
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
83
|
+
// React SPAs, Next.js, and Vue: use code intelligence (AST-based) - NO FALLBACKS
|
|
84
|
+
if (projectType === 'react_spa' ||
|
|
85
|
+
projectType === 'nextjs_app_router' ||
|
|
86
|
+
projectType === 'nextjs_pages_router' ||
|
|
87
|
+
projectType === 'vue_router' ||
|
|
88
|
+
projectType === 'vue_spa') {
|
|
89
|
+
const intelResult = await runCodeIntelligence(projectDir);
|
|
90
|
+
|
|
91
|
+
if (!intelResult.error && intelResult.expectations) {
|
|
92
|
+
// ZERO-HEURISTIC: Only include expectations that pass isProvenExpectation()
|
|
93
|
+
intelExpectations = intelResult.expectations.filter(exp => isProvenExpectation(exp));
|
|
94
|
+
|
|
95
|
+
// STATE INTELLIGENCE: Also extract state expectations from AST
|
|
96
|
+
const { extractStateExpectationsFromAST } = await import('./state-extractor.js');
|
|
97
|
+
const stateResult = await extractStateExpectationsFromAST(projectDir);
|
|
98
|
+
|
|
99
|
+
if (stateResult.expectations && stateResult.expectations.length > 0) {
|
|
100
|
+
// Convert state expectations to manifest format with sourceRef
|
|
101
|
+
const stateExpectations = stateResult.expectations.map(exp => ({
|
|
102
|
+
type: 'state_action',
|
|
103
|
+
expectedTarget: exp.expectedTarget,
|
|
104
|
+
storeType: exp.storeType,
|
|
105
|
+
proof: 'PROVEN_EXPECTATION',
|
|
106
|
+
sourceRef: exp.line ? `${exp.sourceFile}:${exp.line}` : exp.sourceFile,
|
|
107
|
+
selectorHint: null, // State actions don't have selector hints from AST
|
|
108
|
+
metadata: {
|
|
109
|
+
sourceFile: exp.sourceFile,
|
|
110
|
+
line: exp.line
|
|
111
|
+
}
|
|
112
|
+
})).filter(exp => isProvenExpectation(exp));
|
|
113
|
+
|
|
114
|
+
intelExpectations.push(...stateExpectations);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Add intel expectations to manifest
|
|
118
|
+
if (!manifest.staticExpectations) {
|
|
119
|
+
manifest.staticExpectations = [];
|
|
120
|
+
}
|
|
121
|
+
manifest.staticExpectations.push(...intelExpectations);
|
|
122
|
+
allExpectations = intelExpectations;
|
|
63
123
|
}
|
|
64
124
|
}
|
|
65
|
-
|
|
66
|
-
|
|
125
|
+
|
|
126
|
+
// ZERO-HEURISTIC: Set expectationsStatus and coverageGaps
|
|
127
|
+
const provenCount = allExpectations.filter(exp => isProvenExpectation(exp)).length;
|
|
128
|
+
|
|
129
|
+
if (provenCount === 0 && (projectType === 'react_spa' ||
|
|
130
|
+
projectType === 'nextjs_app_router' ||
|
|
131
|
+
projectType === 'nextjs_pages_router' ||
|
|
132
|
+
projectType === 'vue_router' ||
|
|
133
|
+
projectType === 'vue_spa')) {
|
|
134
|
+
manifest.expectationsStatus = 'NO_PROVEN_EXPECTATIONS';
|
|
135
|
+
manifest.coverageGaps = [{
|
|
136
|
+
reason: 'NO_PROVEN_EXPECTATIONS_AVAILABLE',
|
|
137
|
+
message: 'Code intelligence found no extractable literal effects (navigation, network, validation) with static string literals',
|
|
138
|
+
projectType: projectType
|
|
139
|
+
}];
|
|
140
|
+
} else {
|
|
141
|
+
manifest.expectationsStatus = 'PROVEN_EXPECTATIONS_AVAILABLE';
|
|
142
|
+
manifest.coverageGaps = [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// FLOW INTELLIGENCE v1: Extract flows from PROVEN expectations
|
|
146
|
+
const flows = extractFlows(manifest);
|
|
147
|
+
if (flows.length > 0) {
|
|
148
|
+
manifest.flows = flows;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const learnTruth = await assessLearnTruth(projectDir, projectType, routes, allExpectations);
|
|
67
152
|
manifest.notes.push({
|
|
68
153
|
type: 'truth',
|
|
69
154
|
learn: learnTruth
|
|
70
155
|
});
|
|
71
156
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const manifestPath = resolve(manifestDir, 'site-manifest.json');
|
|
76
|
-
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
...manifest,
|
|
80
|
-
manifestPath: manifestPath,
|
|
81
|
-
learnTruth: learnTruth,
|
|
82
|
-
actionContracts
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function dedupeActionContracts(contracts) {
|
|
87
|
-
const seen = new Set();
|
|
88
|
-
const out = [];
|
|
89
|
-
for (const c of contracts) {
|
|
90
|
-
const key = `${c.kind}|${c.method}|${c.urlPath}|${c.source || ''}|${c.handlerRef || ''}`;
|
|
91
|
-
if (seen.has(key)) continue;
|
|
92
|
-
seen.add(key);
|
|
93
|
-
out.push(c);
|
|
94
|
-
}
|
|
95
|
-
return out;
|
|
157
|
+
// Note: This function is still used by learn() function
|
|
158
|
+
// Direct usage is deprecated in favor of CLI learn.json writer
|
|
159
|
+
return manifest;
|
|
96
160
|
}
|
|
97
161
|
|