@veraxhq/verax 0.2.0 → 0.3.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 +14 -18
- package/bin/verax.js +7 -0
- package/package.json +15 -5
- package/src/cli/commands/baseline.js +104 -0
- package/src/cli/commands/default.js +323 -111
- package/src/cli/commands/doctor.js +36 -4
- package/src/cli/commands/ga.js +243 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +131 -2
- package/src/cli/commands/release-check.js +213 -0
- package/src/cli/commands/run.js +498 -103
- package/src/cli/commands/security-check.js +211 -0
- package/src/cli/commands/truth.js +114 -0
- package/src/cli/entry.js +305 -68
- package/src/cli/util/angular-component-extractor.js +179 -0
- package/src/cli/util/angular-navigation-detector.js +141 -0
- package/src/cli/util/angular-network-detector.js +161 -0
- package/src/cli/util/angular-state-detector.js +162 -0
- package/src/cli/util/ast-interactive-detector.js +546 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/bootstrap-guard.js +86 -0
- package/src/cli/util/detection-engine.js +4 -3
- package/src/cli/util/determinism-runner.js +123 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/env-url.js +4 -0
- package/src/cli/util/events.js +76 -0
- package/src/cli/util/expectation-extractor.js +380 -74
- package/src/cli/util/findings-writer.js +126 -15
- package/src/cli/util/learn-writer.js +3 -1
- package/src/cli/util/observation-engine.js +69 -23
- package/src/cli/util/observe-writer.js +3 -1
- package/src/cli/util/paths.js +6 -14
- package/src/cli/util/project-discovery.js +23 -0
- package/src/cli/util/project-writer.js +3 -1
- package/src/cli/util/redact.js +2 -2
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +13 -1
- package/src/cli/util/svelte-navigation-detector.js +163 -0
- package/src/cli/util/svelte-network-detector.js +80 -0
- package/src/cli/util/svelte-sfc-extractor.js +147 -0
- package/src/cli/util/svelte-state-detector.js +243 -0
- package/src/cli/util/vue-navigation-detector.js +177 -0
- package/src/cli/util/vue-sfc-extractor.js +162 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/doctor.js +2 -2
- package/src/verax/cli/finding-explainer.js +56 -3
- package/src/verax/cli/init.js +1 -1
- package/src/verax/cli/url-safety.js +12 -2
- package/src/verax/cli/wizard.js +13 -2
- package/src/verax/core/artifacts/registry.js +154 -0
- package/src/verax/core/artifacts/verifier.js +980 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +231 -0
- package/src/verax/core/budget-engine.js +1 -1
- package/src/verax/core/capabilities/gates.js +499 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +137 -0
- package/src/verax/core/confidence/confidence-invariants.js +234 -0
- package/src/verax/core/confidence/confidence-report-writer.js +112 -0
- package/src/verax/core/confidence/confidence-weights.js +44 -0
- package/src/verax/core/confidence/confidence.defaults.js +65 -0
- package/src/verax/core/confidence/confidence.loader.js +79 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +484 -0
- package/src/verax/core/confidence-engine.js +486 -0
- package/src/verax/core/confidence-engine.js.backup +471 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +185 -0
- package/src/verax/core/contracts/validators.js +381 -0
- package/src/verax/core/decision-snapshot.js +31 -4
- package/src/verax/core/decisions/decision.trace.js +276 -0
- package/src/verax/core/determinism/contract-writer.js +89 -0
- package/src/verax/core/determinism/contract.js +139 -0
- package/src/verax/core/determinism/diff.js +364 -0
- package/src/verax/core/determinism/engine.js +221 -0
- package/src/verax/core/determinism/finding-identity.js +148 -0
- package/src/verax/core/determinism/normalize.js +438 -0
- package/src/verax/core/determinism/report-writer.js +92 -0
- package/src/verax/core/determinism/run-fingerprint.js +118 -0
- package/src/verax/core/determinism-model.js +35 -6
- package/src/verax/core/dynamic-route-intelligence.js +528 -0
- package/src/verax/core/evidence/evidence-capture-service.js +307 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
- package/src/verax/core/evidence-builder.js +487 -0
- package/src/verax/core/execution-mode-context.js +77 -0
- package/src/verax/core/execution-mode-detector.js +190 -0
- package/src/verax/core/failures/exit-codes.js +86 -0
- package/src/verax/core/failures/failure-summary.js +76 -0
- package/src/verax/core/failures/failure.factory.js +225 -0
- package/src/verax/core/failures/failure.ledger.js +132 -0
- package/src/verax/core/failures/failure.types.js +196 -0
- package/src/verax/core/failures/index.js +10 -0
- package/src/verax/core/ga/ga-report-writer.js +43 -0
- package/src/verax/core/ga/ga.artifact.js +49 -0
- package/src/verax/core/ga/ga.contract.js +434 -0
- package/src/verax/core/ga/ga.enforcer.js +86 -0
- package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
- package/src/verax/core/guardrails/policy.defaults.js +210 -0
- package/src/verax/core/guardrails/policy.loader.js +83 -0
- package/src/verax/core/guardrails/policy.schema.js +110 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
- package/src/verax/core/guardrails-engine.js +505 -0
- package/src/verax/core/incremental-store.js +15 -7
- package/src/verax/core/observe/run-timeline.js +316 -0
- package/src/verax/core/perf/perf.contract.js +186 -0
- package/src/verax/core/perf/perf.display.js +65 -0
- package/src/verax/core/perf/perf.enforcer.js +91 -0
- package/src/verax/core/perf/perf.monitor.js +209 -0
- package/src/verax/core/perf/perf.report.js +198 -0
- package/src/verax/core/pipeline-tracker.js +238 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +271 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +159 -0
- package/src/verax/core/release/reproducibility.check.js +221 -0
- package/src/verax/core/release/sbom.builder.js +283 -0
- package/src/verax/core/replay-validator.js +4 -4
- package/src/verax/core/replay.js +1 -1
- package/src/verax/core/report/cross-index.js +192 -0
- package/src/verax/core/report/human-summary.js +222 -0
- package/src/verax/core/route-intelligence.js +419 -0
- package/src/verax/core/security/secrets.scan.js +326 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +124 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +326 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/silence-impact.js +1 -1
- package/src/verax/core/silence-model.js +9 -7
- package/src/verax/core/truth/truth.certificate.js +250 -0
- package/src/verax/core/ui-feedback-intelligence.js +515 -0
- package/src/verax/detect/comparison.js +8 -3
- package/src/verax/detect/confidence-engine.js +645 -57
- package/src/verax/detect/confidence-helper.js +33 -0
- package/src/verax/detect/detection-engine.js +19 -2
- package/src/verax/detect/dynamic-route-findings.js +335 -0
- package/src/verax/detect/evidence-index.js +15 -65
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +56 -3
- package/src/verax/detect/explanation-helpers.js +1 -1
- package/src/verax/detect/finding-detector.js +2 -2
- package/src/verax/detect/findings-writer.js +149 -20
- package/src/verax/detect/flow-detector.js +4 -4
- package/src/verax/detect/index.js +265 -15
- package/src/verax/detect/interactive-findings.js +3 -4
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/route-findings.js +218 -0
- package/src/verax/detect/signal-mapper.js +2 -2
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/verdict-engine.js +61 -9
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +413 -33
- package/src/verax/intel/effect-detector.js +1 -1
- package/src/verax/intel/index.js +2 -2
- package/src/verax/intel/route-extractor.js +3 -3
- package/src/verax/intel/vue-navigation-extractor.js +81 -18
- package/src/verax/intel/vue-router-extractor.js +4 -2
- package/src/verax/learn/action-contract-extractor.js +684 -66
- package/src/verax/learn/ast-contract-extractor.js +53 -1
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +28 -14
- package/src/verax/learn/route-extractor.js +1 -1
- package/src/verax/learn/route-validator.js +12 -8
- package/src/verax/learn/state-extractor.js +1 -1
- package/src/verax/learn/static-extractor-navigation.js +1 -1
- package/src/verax/learn/static-extractor-validation.js +2 -2
- package/src/verax/learn/static-extractor.js +8 -7
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/browser.js +22 -3
- package/src/verax/observe/console-sensor.js +2 -2
- package/src/verax/observe/expectation-executor.js +2 -1
- package/src/verax/observe/focus-sensor.js +1 -1
- package/src/verax/observe/human-driver.js +29 -10
- package/src/verax/observe/index.js +92 -844
- package/src/verax/observe/interaction-discovery.js +27 -15
- package/src/verax/observe/interaction-runner.js +31 -14
- package/src/verax/observe/loading-sensor.js +6 -0
- package/src/verax/observe/navigation-sensor.js +1 -1
- package/src/verax/observe/observe-context.js +205 -0
- package/src/verax/observe/observe-helpers.js +191 -0
- package/src/verax/observe/observe-runner.js +226 -0
- package/src/verax/observe/observers/budget-observer.js +185 -0
- package/src/verax/observe/observers/console-observer.js +102 -0
- package/src/verax/observe/observers/coverage-observer.js +107 -0
- package/src/verax/observe/observers/interaction-observer.js +471 -0
- package/src/verax/observe/observers/navigation-observer.js +132 -0
- package/src/verax/observe/observers/network-observer.js +87 -0
- package/src/verax/observe/observers/safety-observer.js +82 -0
- package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
- package/src/verax/observe/settle.js +1 -0
- package/src/verax/observe/state-sensor.js +8 -4
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/traces-writer.js +27 -16
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/observe/ui-signal-sensor.js +155 -2
- package/src/verax/scan-summary-writer.js +46 -9
- package/src/verax/shared/artifact-manager.js +9 -6
- package/src/verax/shared/budget-profiles.js +2 -2
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/config-loader.js +1 -2
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/dynamic-route-utils.js +12 -6
- package/src/verax/shared/retry-policy.js +1 -6
- package/src/verax/shared/root-artifacts.js +1 -1
- package/src/verax/shared/view-switch-rules.js +208 -0
- package/src/verax/shared/zip-artifacts.js +1 -0
- package/src/verax/validate/context-validator.js +1 -1
- package/src/verax/observe/index.js.backup +0 -1
- package/src/verax/validate/context-validator.js.bak +0 -0
|
@@ -21,6 +21,11 @@ function extractStaticStringValue(node) {
|
|
|
21
21
|
if (node.type === 'StringLiteral') {
|
|
22
22
|
return node.value;
|
|
23
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
|
+
}
|
|
24
29
|
|
|
25
30
|
// JSX expression: href={'/about'} or href={`/about`}
|
|
26
31
|
if (node.type === 'JSXExpressionContainer') {
|
|
@@ -46,6 +51,22 @@ function extractStaticStringValue(node) {
|
|
|
46
51
|
return null;
|
|
47
52
|
}
|
|
48
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
|
+
|
|
49
70
|
/**
|
|
50
71
|
* Extracts template literal pattern from Babel TemplateLiteral node.
|
|
51
72
|
* Returns null if template has complex expressions that cannot be normalized.
|
|
@@ -176,6 +197,37 @@ function extractContractsFromFile(filePath, fileContent) {
|
|
|
176
197
|
// since we cannot reliably tie them to clicked elements
|
|
177
198
|
CallExpression(path) {
|
|
178
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
|
+
}
|
|
179
231
|
|
|
180
232
|
// navigate("/about") - useNavigate hook
|
|
181
233
|
if (callee.type === 'Identifier' && callee.name === 'navigate') {
|
|
@@ -308,7 +360,7 @@ export async function extractASTContracts(projectDir) {
|
|
|
308
360
|
* Converts AST contracts to manifest expectations format.
|
|
309
361
|
* Only includes contracts that can be matched at runtime (excludes imperativeOnly).
|
|
310
362
|
*/
|
|
311
|
-
export function contractsToExpectations(contracts,
|
|
363
|
+
export function contractsToExpectations(contracts, _projectType) {
|
|
312
364
|
const expectations = [];
|
|
313
365
|
const seenPaths = new Set();
|
|
314
366
|
|
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,5 +1,4 @@
|
|
|
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
4
|
import { runCodeIntelligence } from '../intel/index.js';
|
|
@@ -8,6 +7,29 @@ import { extractFlows } from './flow-extractor.js';
|
|
|
8
7
|
import { createTSProgram } from '../intel/ts-program.js';
|
|
9
8
|
import { extractVueNavigationPromises } from '../intel/vue-navigation-extractor.js';
|
|
10
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
|
+
*/
|
|
11
33
|
export async function writeManifest(projectDir, projectType, routes) {
|
|
12
34
|
const publicRoutes = routes.filter(r => r.public).map(r => r.path);
|
|
13
35
|
const internalRoutes = routes.filter(r => !r.public).map(r => r.path);
|
|
@@ -44,7 +66,7 @@ export async function writeManifest(projectDir, projectType, routes) {
|
|
|
44
66
|
const program = createTSProgram(projectDir, { includeJs: true });
|
|
45
67
|
|
|
46
68
|
if (!program.error) {
|
|
47
|
-
const vueNavPromises = extractVueNavigationPromises(program, projectDir);
|
|
69
|
+
const vueNavPromises = await extractVueNavigationPromises(program, projectDir);
|
|
48
70
|
|
|
49
71
|
if (vueNavPromises && vueNavPromises.length > 0) {
|
|
50
72
|
intelExpectations = vueNavPromises.filter(exp => isProvenExpectation(exp));
|
|
@@ -132,16 +154,8 @@ export async function writeManifest(projectDir, projectType, routes) {
|
|
|
132
154
|
learn: learnTruth
|
|
133
155
|
});
|
|
134
156
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const manifestPath = resolve(manifestDir, 'site-manifest.json');
|
|
139
|
-
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
...manifest,
|
|
143
|
-
manifestPath: manifestPath,
|
|
144
|
-
learnTruth: learnTruth
|
|
145
|
-
};
|
|
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;
|
|
146
160
|
}
|
|
147
161
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
// resolve import removed - currently unused
|
|
2
2
|
import { extractStaticRoutes } from './static-extractor.js';
|
|
3
3
|
import { createTSProgram } from '../intel/ts-program.js';
|
|
4
4
|
import { extractRoutes as extractRoutesAST } from '../intel/route-extractor.js';
|
|
@@ -15,7 +15,10 @@ export async function validateRoutes(manifest, baseUrl) {
|
|
|
15
15
|
routesReachable: 0,
|
|
16
16
|
routesUnreachable: 0,
|
|
17
17
|
details: [],
|
|
18
|
-
warnings: [
|
|
18
|
+
warnings: [{
|
|
19
|
+
code: 'INVALID_BASE_URL',
|
|
20
|
+
message: `Cannot parse base URL for validation: ${error.message}`
|
|
21
|
+
}]
|
|
19
22
|
};
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -99,14 +102,15 @@ export async function validateRoutes(manifest, baseUrl) {
|
|
|
99
102
|
|
|
100
103
|
const request = response.request();
|
|
101
104
|
|
|
102
|
-
// Use redirectChain
|
|
105
|
+
// Use redirectChain property if available (Playwright API)
|
|
103
106
|
let redirectChain = [];
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
// @ts-expect-error - redirectChain exists in Playwright runtime but not in TypeScript types
|
|
108
|
+
if (request.redirectChain && Array.isArray(request.redirectChain)) {
|
|
109
|
+
// @ts-expect-error - redirectChain exists in Playwright runtime but not in TypeScript types
|
|
110
|
+
redirectChain = request.redirectChain;
|
|
111
|
+
} else if (request.redirectedFrom) {
|
|
112
|
+
// Fall back to redirectedFrom if redirectChain not available
|
|
113
|
+
redirectChain = [request.redirectedFrom];
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
// If redirectChain is empty or not available, build chain from redirectedFrom
|
|
@@ -11,7 +11,7 @@ const MAX_FILES_TO_SCAN = 200;
|
|
|
11
11
|
* Extracts static string value from call expression arguments.
|
|
12
12
|
* Returns null if value is dynamic.
|
|
13
13
|
*/
|
|
14
|
-
function
|
|
14
|
+
function _extractStaticActionName(node) {
|
|
15
15
|
if (!node) return null;
|
|
16
16
|
|
|
17
17
|
// String literal: dispatch('increment')
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Detects router.push/replace/navigate calls in inline scripts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
function extractNavigationExpectations(root, fromPath, file,
|
|
6
|
+
function extractNavigationExpectations(root, fromPath, file, _projectDir) {
|
|
7
7
|
const expectations = [];
|
|
8
8
|
|
|
9
9
|
// Extract from inline scripts
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Detects preventDefault() calls in onSubmit handlers.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
function extractValidationExpectations(root, fromPath, file,
|
|
6
|
+
function extractValidationExpectations(root, fromPath, file, _projectDir) {
|
|
7
7
|
const expectations = [];
|
|
8
8
|
|
|
9
9
|
// Extract from inline scripts
|
|
@@ -19,7 +19,7 @@ function extractValidationExpectations(root, fromPath, file, projectDir) {
|
|
|
19
19
|
// Check if this is in an onSubmit context
|
|
20
20
|
// Look for function definitions that might be onSubmit handlers
|
|
21
21
|
const beforeMatch = scriptContent.substring(0, match.index);
|
|
22
|
-
const
|
|
22
|
+
const _afterMatch = scriptContent.substring(match.index);
|
|
23
23
|
|
|
24
24
|
// Check if there's a function definition before this preventDefault
|
|
25
25
|
const functionMatch = beforeMatch.match(/function\s+(\w+)\s*\([^)]*event[^)]*\)/);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { glob } from 'glob';
|
|
2
|
-
import { resolve, dirname,
|
|
2
|
+
import { resolve, dirname, relative } from 'path';
|
|
3
3
|
import { readFileSync, existsSync } from 'fs';
|
|
4
4
|
import { parse } from 'node-html-parser';
|
|
5
5
|
|
|
6
6
|
const MAX_HTML_FILES = 200;
|
|
7
7
|
|
|
8
8
|
function htmlFileToRoute(file) {
|
|
9
|
-
let path = file.replace(/[
|
|
10
|
-
path = path.replace(/\/index
|
|
9
|
+
let path = file.replace(/[\\/]/g, '/');
|
|
10
|
+
path = path.replace(/\/index.html$/, '');
|
|
11
11
|
path = path.replace(/\.html$/, '');
|
|
12
12
|
path = path.replace(/^index$/, '');
|
|
13
13
|
|
|
@@ -159,7 +159,7 @@ function extractFormSubmissionExpectations(root, fromPath, file, routeMap, proje
|
|
|
159
159
|
return expectations;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
function extractNetworkExpectations(root, fromPath, file,
|
|
162
|
+
function extractNetworkExpectations(root, fromPath, file, _projectDir) {
|
|
163
163
|
const expectations = [];
|
|
164
164
|
|
|
165
165
|
// Extract from inline scripts
|
|
@@ -286,9 +286,10 @@ export async function extractStaticExpectations(projectDir, routes) {
|
|
|
286
286
|
const targetPath = resolveLinkPath(href, file, projectDir);
|
|
287
287
|
if (!targetPath) continue;
|
|
288
288
|
|
|
289
|
-
if
|
|
290
|
-
|
|
291
|
-
|
|
289
|
+
// Extract expectation even if target route doesn't exist yet
|
|
290
|
+
// This allows detection of broken links or prevented navigation
|
|
291
|
+
// (The route may not exist, but the link promises navigation)
|
|
292
|
+
const _linkText = link.textContent?.trim() || '';
|
|
292
293
|
const selectorHint = link.id ? `#${link.id}` : `a[href="${href}"]`;
|
|
293
294
|
|
|
294
295
|
expectations.push({
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import ts from 'typescript';
|
|
11
11
|
import { resolve, relative, dirname, sep, join } from 'path';
|
|
12
|
-
import {
|
|
12
|
+
import { existsSync, statSync } from 'fs';
|
|
13
13
|
import { glob } from 'glob';
|
|
14
14
|
|
|
15
15
|
const MAX_DEPTH = 3;
|
|
@@ -45,14 +45,8 @@ export async function resolveActionContracts(rootDir, workspaceRoot) {
|
|
|
45
45
|
const checker = program.getTypeChecker();
|
|
46
46
|
const contracts = [];
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!sourcePath.toLowerCase().startsWith(normalizedRoot.toLowerCase())) continue;
|
|
51
|
-
if (sourceFile.isDeclarationFile) continue;
|
|
52
|
-
|
|
53
|
-
const importMap = buildImportMap(sourceFile, rootDir, workspaceRoot);
|
|
54
|
-
|
|
55
|
-
function visit(node) {
|
|
48
|
+
function createVisitFunction(importMap, sourceFile) {
|
|
49
|
+
return function visit(node) {
|
|
56
50
|
if (ts.isJsxAttribute(node)) {
|
|
57
51
|
const name = node.name.getText();
|
|
58
52
|
if (name !== 'onClick' && name !== 'onSubmit') return;
|
|
@@ -64,7 +58,7 @@ export async function resolveActionContracts(rootDir, workspaceRoot) {
|
|
|
64
58
|
// We only handle identifier handlers (cross-file capable)
|
|
65
59
|
if (!ts.isIdentifier(expr)) return;
|
|
66
60
|
|
|
67
|
-
const
|
|
61
|
+
const _handlerName = expr.text;
|
|
68
62
|
const handlerRef = deriveHandlerRef(expr, importMap, sourceFile, workspaceRoot);
|
|
69
63
|
if (!handlerRef) return;
|
|
70
64
|
|
|
@@ -112,8 +106,16 @@ export async function resolveActionContracts(rootDir, workspaceRoot) {
|
|
|
112
106
|
}
|
|
113
107
|
}
|
|
114
108
|
ts.forEachChild(node, visit);
|
|
115
|
-
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
116
111
|
|
|
112
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
113
|
+
const sourcePath = resolve(sourceFile.fileName);
|
|
114
|
+
if (!sourcePath.toLowerCase().startsWith(normalizedRoot.toLowerCase())) continue;
|
|
115
|
+
if (sourceFile.isDeclarationFile) continue;
|
|
116
|
+
|
|
117
|
+
const importMap = buildImportMap(sourceFile, rootDir, workspaceRoot);
|
|
118
|
+
const visit = createVisitFunction(importMap, sourceFile);
|
|
117
119
|
visit(sourceFile);
|
|
118
120
|
}
|
|
119
121
|
|
|
@@ -313,7 +315,7 @@ function analyzeStateCall(node) {
|
|
|
313
315
|
|
|
314
316
|
// Zustand: store.set(...) or setState(...)
|
|
315
317
|
if (ts.isPropertyAccessExpression(callee)) {
|
|
316
|
-
const
|
|
318
|
+
const _obj = callee.expression;
|
|
317
319
|
const prop = callee.name;
|
|
318
320
|
// Common pattern: storeObj.set(...)
|
|
319
321
|
if (prop.text === 'set') {
|
|
@@ -16,12 +16,31 @@ export async function navigateToUrl(page, url, scanBudget = DEFAULT_SCAN_BUDGET)
|
|
|
16
16
|
if (url.startsWith('file:') || url.includes('localhost:') || url.includes('127.0.0.1')) {
|
|
17
17
|
stableWait = 200; // Short wait for local fixtures
|
|
18
18
|
}
|
|
19
|
-
} catch {
|
|
20
|
-
|
|
19
|
+
} catch {
|
|
20
|
+
// Ignore config errors
|
|
21
|
+
}
|
|
22
|
+
// Use domcontentloaded first for faster timeout, then wait for networkidle separately
|
|
23
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: scanBudget.initialNavigationTimeoutMs });
|
|
24
|
+
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
|
|
25
|
+
// Network idle timeout is acceptable, continue
|
|
26
|
+
});
|
|
21
27
|
await page.waitForTimeout(stableWait);
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
export async function closeBrowser(browser) {
|
|
25
|
-
|
|
31
|
+
try {
|
|
32
|
+
// Close all contexts first
|
|
33
|
+
const contexts = browser.contexts();
|
|
34
|
+
for (const context of contexts) {
|
|
35
|
+
try {
|
|
36
|
+
await context.close({ timeout: 5000 }).catch(() => {});
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// Ignore context close errors
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
await browser.close({ timeout: 5000 }).catch(() => {});
|
|
42
|
+
} catch (e) {
|
|
43
|
+
// Ignore browser close errors - best effort cleanup
|
|
44
|
+
}
|
|
26
45
|
}
|
|
27
46
|
|
|
@@ -57,7 +57,7 @@ export class ConsoleSensor {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
// Capture unhandled promise rejections
|
|
60
|
-
const
|
|
60
|
+
const _onUnhandledRejection = (promise, reason) => {
|
|
61
61
|
const message = (reason?.toString?.() || String(reason)).slice(0, 200);
|
|
62
62
|
state.unhandledRejections.push({
|
|
63
63
|
message: message,
|
|
@@ -104,7 +104,7 @@ export class ConsoleSensor {
|
|
|
104
104
|
/**
|
|
105
105
|
* Stop monitoring and return a summary for the window.
|
|
106
106
|
*/
|
|
107
|
-
stopWindow(windowId,
|
|
107
|
+
stopWindow(windowId, _page) {
|
|
108
108
|
const state = this.windows.get(windowId);
|
|
109
109
|
if (!state) {
|
|
110
110
|
return this.getEmptySummary();
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Executes every PROVEN expectation from the manifest.
|
|
5
5
|
* Each expectation must result in: VERIFIED, SILENT_FAILURE, or COVERAGE_GAP.
|
|
6
|
+
* @typedef {import('playwright').Page} Page
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { isProvenExpectation } from '../shared/expectation-prover.js';
|
|
@@ -11,7 +12,7 @@ import { hasMeaningfulUrlChange, hasVisibleChange, hasDomChange } from '../detec
|
|
|
11
12
|
import { runInteraction } from './interaction-runner.js';
|
|
12
13
|
import { captureScreenshot } from './evidence-capture.js';
|
|
13
14
|
import { getBaseOrigin } from './domain-boundary.js';
|
|
14
|
-
import { resolve
|
|
15
|
+
import { resolve } from 'path';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Execute a single PROVEN expectation.
|
|
@@ -155,7 +155,7 @@ export class FocusSensor {
|
|
|
155
155
|
/**
|
|
156
156
|
* Detect if focus didn't move into modal (expected but didn't happen)
|
|
157
157
|
*/
|
|
158
|
-
detectModalFocusFailure(
|
|
158
|
+
detectModalFocusFailure(_page) {
|
|
159
159
|
// This is checked via modal detection - focus should move to modal
|
|
160
160
|
// Presence of modal without focus change = failure
|
|
161
161
|
return false; // Caller will check if modal opened
|
|
@@ -158,7 +158,9 @@ export class HumanBehaviorDriver {
|
|
|
158
158
|
} else if (url.includes('localhost:') || url.includes('127.0.0.1')) {
|
|
159
159
|
timeoutMs = 200; // Short wait for local http fixtures
|
|
160
160
|
}
|
|
161
|
-
} catch {
|
|
161
|
+
} catch {
|
|
162
|
+
// Ignore config errors
|
|
163
|
+
}
|
|
162
164
|
|
|
163
165
|
const waitForUiIdle = async () => {
|
|
164
166
|
const start = Date.now();
|
|
@@ -336,7 +338,10 @@ export class HumanBehaviorDriver {
|
|
|
336
338
|
|
|
337
339
|
const focusableSelectors = await page.evaluate(() => {
|
|
338
340
|
const focusables = Array.from(document.querySelectorAll('a[href], button, input, select, textarea, [tabindex], [role="button"], [role="menuitem"], [contenteditable="true"]'))
|
|
339
|
-
.filter(el =>
|
|
341
|
+
.filter(el => {
|
|
342
|
+
const htmlEl = /** @type {HTMLElement} */ (el);
|
|
343
|
+
return !el.hasAttribute('disabled') && htmlEl.tabIndex >= 0 && htmlEl.offsetParent !== null;
|
|
344
|
+
});
|
|
340
345
|
const describe = (el) => {
|
|
341
346
|
if (!el) return 'body';
|
|
342
347
|
if (el.id) return `#${el.id}`;
|
|
@@ -609,7 +614,9 @@ export class HumanBehaviorDriver {
|
|
|
609
614
|
page.waitForTimeout(CLICK_TIMEOUT_MS)
|
|
610
615
|
]);
|
|
611
616
|
await this.waitAfterAction(page, 600);
|
|
612
|
-
} catch {
|
|
617
|
+
} catch {
|
|
618
|
+
// Ignore form submission errors
|
|
619
|
+
}
|
|
613
620
|
|
|
614
621
|
const afterUrl = page.url();
|
|
615
622
|
const afterStorageKeys = await page.evaluate(() => Object.keys(localStorage));
|
|
@@ -660,7 +667,9 @@ export class HumanBehaviorDriver {
|
|
|
660
667
|
clicked = true;
|
|
661
668
|
await this.waitAfterAction(page, 400);
|
|
662
669
|
break;
|
|
663
|
-
} catch {
|
|
670
|
+
} catch {
|
|
671
|
+
// Ignore interaction errors
|
|
672
|
+
}
|
|
664
673
|
}
|
|
665
674
|
}
|
|
666
675
|
|
|
@@ -686,17 +695,23 @@ export class HumanBehaviorDriver {
|
|
|
686
695
|
const beforeUrl = page.url();
|
|
687
696
|
try {
|
|
688
697
|
await page.goto(url, { waitUntil: 'load', timeout: CLICK_TIMEOUT_MS }).catch(() => null);
|
|
689
|
-
} catch {
|
|
698
|
+
} catch {
|
|
699
|
+
// Ignore navigation errors
|
|
700
|
+
}
|
|
690
701
|
|
|
691
702
|
const afterUrl = page.url();
|
|
692
|
-
const
|
|
703
|
+
const redirectedToLogin = beforeUrl !== afterUrl && (afterUrl.includes('/login') || afterUrl.includes('/signin'));
|
|
693
704
|
const content = await page.content();
|
|
694
705
|
const hasAccessDenied = content.includes('401') || content.includes('403') || content.includes('unauthorized') || content.includes('forbidden');
|
|
706
|
+
const isProtected = redirectedToLogin || hasAccessDenied;
|
|
695
707
|
|
|
696
708
|
return {
|
|
697
709
|
url,
|
|
698
|
-
|
|
699
|
-
|
|
710
|
+
beforeUrl,
|
|
711
|
+
afterUrl,
|
|
712
|
+
isProtected,
|
|
713
|
+
redirectedToLogin,
|
|
714
|
+
hasAccessDenied,
|
|
700
715
|
httpStatus: hasAccessDenied ? (content.includes('403') ? 403 : 401) : 200
|
|
701
716
|
};
|
|
702
717
|
}
|
|
@@ -710,7 +725,9 @@ export class HumanBehaviorDriver {
|
|
|
710
725
|
const key = window.localStorage.key(i);
|
|
711
726
|
if (key) result[key] = window.localStorage.getItem(key);
|
|
712
727
|
}
|
|
713
|
-
} catch
|
|
728
|
+
} catch {
|
|
729
|
+
// Ignore localStorage access errors
|
|
730
|
+
}
|
|
714
731
|
return result;
|
|
715
732
|
});
|
|
716
733
|
|
|
@@ -721,7 +738,9 @@ export class HumanBehaviorDriver {
|
|
|
721
738
|
const key = window.sessionStorage.key(i);
|
|
722
739
|
if (key) result[key] = window.sessionStorage.getItem(key);
|
|
723
740
|
}
|
|
724
|
-
} catch
|
|
741
|
+
} catch {
|
|
742
|
+
// Ignore sessionStorage access errors
|
|
743
|
+
}
|
|
725
744
|
return result;
|
|
726
745
|
});
|
|
727
746
|
|