@veraxhq/verax 0.2.1 → 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 +10 -6
- package/bin/verax.js +11 -11
- package/package.json +29 -8
- package/src/cli/commands/baseline.js +103 -0
- package/src/cli/commands/default.js +51 -6
- package/src/cli/commands/doctor.js +29 -0
- package/src/cli/commands/ga.js +246 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +4 -2
- package/src/cli/commands/release-check.js +215 -0
- package/src/cli/commands/run.js +45 -6
- package/src/cli/commands/security-check.js +212 -0
- package/src/cli/commands/truth.js +113 -0
- package/src/cli/entry.js +30 -20
- 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 +544 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-promise-extractor.js +581 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/atomic-write.js +12 -1
- package/src/cli/util/bootstrap-guard.js +86 -0
- 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 +124 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/digest-engine.js +359 -0
- package/src/cli/util/dom-diff.js +226 -0
- package/src/cli/util/evidence-engine.js +287 -0
- package/src/cli/util/expectation-extractor.js +151 -5
- package/src/cli/util/findings-writer.js +3 -0
- 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 -0
- 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 -0
- package/src/cli/util/project-discovery.js +284 -0
- package/src/cli/util/project-writer.js +2 -0
- package/src/cli/util/run-id.js +23 -27
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/run-result.js +778 -0
- package/src/cli/util/selector-resolver.js +235 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +2 -0
- 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 +146 -0
- package/src/cli/util/svelte-state-detector.js +242 -0
- 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 +178 -0
- package/src/cli/util/vue-sfc-extractor.js +161 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- 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/init.js +4 -18
- package/src/verax/core/action-classifier.js +4 -3
- package/src/verax/core/artifacts/registry.js +139 -0
- package/src/verax/core/artifacts/verifier.js +990 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +233 -0
- package/src/verax/core/capabilities/gates.js +505 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +144 -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 +80 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +489 -0
- package/src/verax/core/confidence-engine.js +625 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +186 -0
- package/src/verax/core/contracts/validators.js +456 -0
- package/src/verax/core/decisions/decision.trace.js +278 -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 +405 -0
- package/src/verax/core/determinism/engine.js +222 -0
- package/src/verax/core/determinism/finding-identity.js +149 -0
- package/src/verax/core/determinism/normalize.js +466 -0
- package/src/verax/core/determinism/report-writer.js +93 -0
- package/src/verax/core/determinism/run-fingerprint.js +123 -0
- package/src/verax/core/dynamic-route-intelligence.js +529 -0
- package/src/verax/core/evidence/evidence-capture-service.js +308 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +166 -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 +192 -0
- package/src/verax/core/failures/exit-codes.js +88 -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 +133 -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 +435 -0
- package/src/verax/core/ga/ga.enforcer.js +87 -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 +84 -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 +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 +318 -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 +200 -0
- package/src/verax/core/pipeline-tracker.js +243 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +130 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +164 -0
- package/src/verax/core/release/reproducibility.check.js +222 -0
- package/src/verax/core/release/sbom.builder.js +292 -0
- 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 +195 -0
- package/src/verax/core/report/human-summary.js +362 -0
- package/src/verax/core/route-intelligence.js +420 -0
- 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 +329 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +128 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +334 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/truth/truth.certificate.js +252 -0
- package/src/verax/core/ui-feedback-intelligence.js +481 -0
- package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
- package/src/verax/detect/confidence-engine.js +62 -34
- package/src/verax/detect/confidence-helper.js +34 -0
- package/src/verax/detect/dynamic-route-findings.js +338 -0
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +2 -2
- package/src/verax/detect/failure-cause-inference.js +293 -0
- package/src/verax/detect/findings-writer.js +131 -35
- 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 +46 -5
- package/src/verax/detect/invariants-enforcer.js +147 -0
- package/src/verax/detect/journey-stall-detector.js +558 -0
- 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 +219 -0
- 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 +207 -0
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/flow/flow-engine.js +2 -1
- package/src/verax/flow/flow-spec.js +0 -6
- package/src/verax/index.js +4 -0
- 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 +3 -0
- 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/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 +51 -155
- package/src/verax/observe/interaction-executor.js +192 -0
- package/src/verax/observe/interaction-runner.js +782 -513
- 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 +205 -0
- package/src/verax/observe/observe-helpers.js +192 -0
- package/src/verax/observe/observe-runner.js +230 -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/page-traversal.js +138 -0
- package/src/verax/observe/snapshot-ops.js +94 -0
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/scan-summary-writer.js +2 -0
- package/src/verax/shared/artifact-manager.js +25 -5
- package/src/verax/shared/caching.js +1 -0
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/expectation-tracker.js +1 -0
- package/src/verax/shared/view-switch-rules.js +208 -0
- package/src/verax/shared/zip-artifacts.js +6 -0
- package/src/verax/shared/config-loader.js +0 -169
- /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selector Resolver
|
|
3
|
+
* Finds and validates selectors for extracted promises
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a selector to an actual element
|
|
8
|
+
* Returns {found: boolean, selector: string, reason?: string}
|
|
9
|
+
*/
|
|
10
|
+
export async function resolveSelector(page, promise) {
|
|
11
|
+
// Try promise.selector first if it exists
|
|
12
|
+
if (promise.selector) {
|
|
13
|
+
const resolved = await trySelectorVariants(page, promise.selector);
|
|
14
|
+
if (resolved.found) {
|
|
15
|
+
return resolved;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Try to derive selector from promise details
|
|
20
|
+
if (promise.category === 'button') {
|
|
21
|
+
return await resolveButtonSelector(page, promise);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (promise.category === 'form') {
|
|
25
|
+
return await resolveFormSelector(page, promise);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (promise.category === 'validation') {
|
|
29
|
+
return await resolveValidationSelector(page, promise);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (promise.type === 'navigation') {
|
|
33
|
+
return await resolveNavigationSelector(page, promise);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
found: false,
|
|
38
|
+
selector: null,
|
|
39
|
+
reason: 'unsupported-promise-type',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Try multiple selector variants
|
|
45
|
+
*/
|
|
46
|
+
async function trySelectorVariants(page, baseSelector) {
|
|
47
|
+
const variants = [
|
|
48
|
+
baseSelector,
|
|
49
|
+
baseSelector.replace(/button:contains/, 'button:has-text'),
|
|
50
|
+
baseSelector.replace(/"/g, "'"),
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
for (const selector of variants) {
|
|
54
|
+
try {
|
|
55
|
+
const count = await page.locator(selector).count();
|
|
56
|
+
if (count === 1) {
|
|
57
|
+
return { found: true, selector };
|
|
58
|
+
}
|
|
59
|
+
if (count > 1) {
|
|
60
|
+
return { found: false, selector, reason: 'ambiguous-selector' };
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// Try next variant
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { found: false, selector: baseSelector, reason: 'not-found' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve button selector
|
|
72
|
+
*/
|
|
73
|
+
async function resolveButtonSelector(page, promise) {
|
|
74
|
+
// First try the provided selector
|
|
75
|
+
if (promise.selector) {
|
|
76
|
+
const result = await trySelectorVariants(page, promise.selector);
|
|
77
|
+
if (result.found) return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Try all button variants
|
|
81
|
+
const selectors = [
|
|
82
|
+
'button[onClick]',
|
|
83
|
+
'button',
|
|
84
|
+
'[role="button"][onClick]',
|
|
85
|
+
'[role="button"]',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
for (const selector of selectors) {
|
|
89
|
+
try {
|
|
90
|
+
const count = await page.locator(selector).count();
|
|
91
|
+
if (count === 1) {
|
|
92
|
+
return { found: true, selector };
|
|
93
|
+
}
|
|
94
|
+
if (count > 0) {
|
|
95
|
+
// Multiple buttons - try to narrow down by content
|
|
96
|
+
const buttons = await page.locator(selector).all();
|
|
97
|
+
for (const btn of buttons) {
|
|
98
|
+
const text = await btn.textContent();
|
|
99
|
+
if (text && (text.includes('Save') || text.includes('Submit') || text.includes('Click'))) {
|
|
100
|
+
const specific = `${selector}:has-text("${text.trim()}")`;
|
|
101
|
+
const count = await page.locator(specific).count();
|
|
102
|
+
if (count === 1) {
|
|
103
|
+
return { found: true, selector: specific };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
// Try next
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { found: false, selector: null, reason: 'not-found' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve form selector
|
|
118
|
+
*/
|
|
119
|
+
async function resolveFormSelector(page, promise) {
|
|
120
|
+
if (promise.selector) {
|
|
121
|
+
const result = await trySelectorVariants(page, promise.selector);
|
|
122
|
+
if (result.found) return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const selectors = [
|
|
126
|
+
'form[onSubmit]',
|
|
127
|
+
'form',
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
for (const selector of selectors) {
|
|
131
|
+
try {
|
|
132
|
+
const count = await page.locator(selector).count();
|
|
133
|
+
if (count === 1) {
|
|
134
|
+
return { found: true, selector };
|
|
135
|
+
}
|
|
136
|
+
} catch (e) {
|
|
137
|
+
// Try next
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { found: false, selector: null, reason: 'not-found' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolve validation selector (for required inputs)
|
|
146
|
+
*/
|
|
147
|
+
async function resolveValidationSelector(page, promise) {
|
|
148
|
+
if (promise.selector) {
|
|
149
|
+
const result = await trySelectorVariants(page, promise.selector);
|
|
150
|
+
if (result.found) return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Look for required inputs
|
|
154
|
+
const selectors = [
|
|
155
|
+
'input[required]',
|
|
156
|
+
'textarea[required]',
|
|
157
|
+
'select[required]',
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
for (const selector of selectors) {
|
|
161
|
+
try {
|
|
162
|
+
const count = await page.locator(selector).count();
|
|
163
|
+
if (count >= 1) {
|
|
164
|
+
return { found: true, selector };
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {
|
|
167
|
+
// Try next
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { found: false, selector: null, reason: 'not-found' };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Resolve navigation selector (for anchor tags)
|
|
176
|
+
*/
|
|
177
|
+
async function resolveNavigationSelector(page, promise) {
|
|
178
|
+
const targetPath = promise.promise.value;
|
|
179
|
+
|
|
180
|
+
const selectors = [
|
|
181
|
+
`a[href="${targetPath}"]`,
|
|
182
|
+
`a[href*="${targetPath}"]`,
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
for (const selector of selectors) {
|
|
186
|
+
try {
|
|
187
|
+
const count = await page.locator(selector).count();
|
|
188
|
+
if (count === 1) {
|
|
189
|
+
return { found: true, selector };
|
|
190
|
+
}
|
|
191
|
+
} catch (e) {
|
|
192
|
+
// Try next
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { found: false, selector: null, reason: 'not-found' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Find submit button in form
|
|
201
|
+
*/
|
|
202
|
+
export async function findSubmitButton(page, formSelector) {
|
|
203
|
+
try {
|
|
204
|
+
// Look for explicit submit button in form
|
|
205
|
+
const submitBtn = await page.locator(`${formSelector} button[type="submit"]`).first();
|
|
206
|
+
const count = await submitBtn.count();
|
|
207
|
+
if (count > 0) {
|
|
208
|
+
return submitBtn;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Look for any button in form
|
|
212
|
+
const btn = await page.locator(`${formSelector} button`).first();
|
|
213
|
+
if (await btn.count() > 0) {
|
|
214
|
+
return btn;
|
|
215
|
+
}
|
|
216
|
+
} catch (e) {
|
|
217
|
+
// Fall through
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if element is clickable/visible
|
|
225
|
+
*/
|
|
226
|
+
export async function isElementInteractable(page, selector) {
|
|
227
|
+
try {
|
|
228
|
+
const element = page.locator(selector).first();
|
|
229
|
+
const isVisible = await element.isVisible();
|
|
230
|
+
const isEnabled = await element.isEnabled();
|
|
231
|
+
return isVisible && isEnabled;
|
|
232
|
+
} catch (e) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { extname, join, resolve } from 'path';
|
|
3
|
+
import { DataError } from './errors.js';
|
|
4
|
+
import { getSourceCodeRequirementBanner } from '../../verax/core/product-definition.js';
|
|
5
|
+
|
|
6
|
+
const CODE_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.html']);
|
|
7
|
+
|
|
8
|
+
function safeReaddir(dirPath) {
|
|
9
|
+
try {
|
|
10
|
+
return readdirSync(dirPath);
|
|
11
|
+
} catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function directoryHasCode(dirPath) {
|
|
17
|
+
const entries = safeReaddir(dirPath);
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const fullPath = join(dirPath, entry);
|
|
20
|
+
let stats;
|
|
21
|
+
try {
|
|
22
|
+
stats = statSync(fullPath);
|
|
23
|
+
} catch {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (stats.isFile() && CODE_EXTS.has(extname(entry).toLowerCase())) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (stats.isDirectory() && (entry === 'src' || entry === 'app')) {
|
|
32
|
+
const nested = safeReaddir(fullPath).slice(0, 50);
|
|
33
|
+
if (nested.some((name) => CODE_EXTS.has(extname(name).toLowerCase()))) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function assertHasLocalSource(srcPath) {
|
|
42
|
+
const resolved = resolve(srcPath);
|
|
43
|
+
const hasPackageJson = existsSync(join(resolved, 'package.json'));
|
|
44
|
+
const hasIndexHtml = existsSync(join(resolved, 'index.html'));
|
|
45
|
+
const hasCodeFiles = directoryHasCode(resolved);
|
|
46
|
+
|
|
47
|
+
if (!hasPackageJson && !hasIndexHtml && !hasCodeFiles) {
|
|
48
|
+
const banner = getSourceCodeRequirementBanner();
|
|
49
|
+
throw new DataError(
|
|
50
|
+
`${banner} Provide --src pointing to your repository so VERAX can analyze expectations. See docs/README.md for the canonical product contract.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { hasPackageJson, hasIndexHtml, hasCodeFiles };
|
|
55
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { atomicWriteJson } from './atomic-write.js';
|
|
2
|
+
import { ARTIFACT_REGISTRY } from '../../verax/core/artifacts/registry.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Write summary.json with deterministic digest
|
|
@@ -7,6 +8,7 @@ import { atomicWriteJson } from './atomic-write.js';
|
|
|
7
8
|
*/
|
|
8
9
|
export function writeSummaryJson(summaryPath, summaryData, stats = {}) {
|
|
9
10
|
const payload = {
|
|
11
|
+
contractVersion: ARTIFACT_REGISTRY.summary.contractVersion,
|
|
10
12
|
runId: summaryData.runId,
|
|
11
13
|
status: summaryData.status,
|
|
12
14
|
startedAt: summaryData.startedAt,
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 20 — Svelte Navigation Detector
|
|
3
|
+
*
|
|
4
|
+
* Detects navigation promises in Svelte applications:
|
|
5
|
+
* - <a href="/path"> links
|
|
6
|
+
* - goto() calls from SvelteKit
|
|
7
|
+
* - programmatic navigation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { extractSvelteSFC, extractTemplateBindings as _extractTemplateBindings, mapTemplateHandlersToScript as _mapTemplateHandlersToScript } from './svelte-sfc-extractor.js';
|
|
11
|
+
import { parse } from '@babel/parser';
|
|
12
|
+
import traverse from '@babel/traverse';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Detect navigation promises in Svelte SFC
|
|
16
|
+
*
|
|
17
|
+
* @param {string} filePath - Path to .svelte file
|
|
18
|
+
* @param {string} content - Full file content
|
|
19
|
+
* @param {string} _projectRoot - Project root directory (unused)
|
|
20
|
+
* @returns {Array} Array of navigation expectations
|
|
21
|
+
*/
|
|
22
|
+
export function detectSvelteNavigation(filePath, content, _projectRoot) {
|
|
23
|
+
const expectations = [];
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const sfc = extractSvelteSFC(content);
|
|
27
|
+
const { scriptBlocks, markup } = sfc;
|
|
28
|
+
|
|
29
|
+
// Extract navigation from markup (links)
|
|
30
|
+
if (markup && markup.content) {
|
|
31
|
+
const linkRegex = /<a\s+[^>]*href=["']([^"']+)["'][^>]*>/gi;
|
|
32
|
+
let linkMatch;
|
|
33
|
+
while ((linkMatch = linkRegex.exec(markup.content)) !== null) {
|
|
34
|
+
const href = linkMatch[1];
|
|
35
|
+
// Skip external links and hash-only links
|
|
36
|
+
if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('#')) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const beforeMatch = markup.content.substring(0, linkMatch.index);
|
|
41
|
+
const line = markup.startLine + (beforeMatch.match(/\n/g) || []).length;
|
|
42
|
+
|
|
43
|
+
expectations.push({
|
|
44
|
+
type: 'navigation',
|
|
45
|
+
target: href,
|
|
46
|
+
context: 'markup',
|
|
47
|
+
sourceRef: {
|
|
48
|
+
file: filePath,
|
|
49
|
+
line,
|
|
50
|
+
snippet: linkMatch[0],
|
|
51
|
+
},
|
|
52
|
+
proof: 'PROVEN_EXPECTATION',
|
|
53
|
+
metadata: {
|
|
54
|
+
navigationType: 'link',
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Extract navigation from script blocks (goto, navigate, etc.)
|
|
61
|
+
for (const scriptBlock of scriptBlocks) {
|
|
62
|
+
if (!scriptBlock.content) continue;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const ast = parse(scriptBlock.content, {
|
|
66
|
+
sourceType: 'module',
|
|
67
|
+
plugins: ['typescript', 'jsx'],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
traverse.default(ast, {
|
|
71
|
+
CallExpression(path) {
|
|
72
|
+
const { node } = path;
|
|
73
|
+
|
|
74
|
+
// Detect goto() calls (SvelteKit)
|
|
75
|
+
if (
|
|
76
|
+
node.callee.type === 'Identifier' &&
|
|
77
|
+
node.callee.name === 'goto' &&
|
|
78
|
+
node.arguments.length > 0
|
|
79
|
+
) {
|
|
80
|
+
const arg = node.arguments[0];
|
|
81
|
+
let target = null;
|
|
82
|
+
|
|
83
|
+
if (arg.type === 'StringLiteral') {
|
|
84
|
+
target = arg.value;
|
|
85
|
+
} else if (arg.type === 'TemplateLiteral' && arg.quasis.length === 1) {
|
|
86
|
+
target = arg.quasis[0].value.raw;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (target && !target.startsWith('http://') && !target.startsWith('https://') && !target.startsWith('#')) {
|
|
90
|
+
const location = node.loc;
|
|
91
|
+
const line = scriptBlock.startLine + (location ? location.start.line - 1 : 0);
|
|
92
|
+
|
|
93
|
+
expectations.push({
|
|
94
|
+
type: 'navigation',
|
|
95
|
+
target,
|
|
96
|
+
context: 'goto',
|
|
97
|
+
sourceRef: {
|
|
98
|
+
file: filePath,
|
|
99
|
+
line,
|
|
100
|
+
snippet: scriptBlock.content.substring(
|
|
101
|
+
node.start - (ast.program.body[0]?.start || 0),
|
|
102
|
+
node.end - (ast.program.body[0]?.start || 0)
|
|
103
|
+
),
|
|
104
|
+
},
|
|
105
|
+
proof: arg.type === 'StringLiteral' ? 'PROVEN_EXPECTATION' : 'LIKELY_EXPECTATION',
|
|
106
|
+
metadata: {
|
|
107
|
+
navigationType: 'goto',
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Detect navigate() calls (if imported)
|
|
114
|
+
if (
|
|
115
|
+
node.callee.type === 'MemberExpression' &&
|
|
116
|
+
node.callee.property.name === 'navigate' &&
|
|
117
|
+
node.arguments.length > 0
|
|
118
|
+
) {
|
|
119
|
+
const arg = node.arguments[0];
|
|
120
|
+
let target = null;
|
|
121
|
+
|
|
122
|
+
if (arg.type === 'StringLiteral') {
|
|
123
|
+
target = arg.value;
|
|
124
|
+
} else if (arg.type === 'TemplateLiteral' && arg.quasis.length === 1) {
|
|
125
|
+
target = arg.quasis[0].value.raw;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (target && !target.startsWith('http://') && !target.startsWith('https://') && !target.startsWith('#')) {
|
|
129
|
+
const location = node.loc;
|
|
130
|
+
const line = scriptBlock.startLine + (location ? location.start.line - 1 : 0);
|
|
131
|
+
|
|
132
|
+
expectations.push({
|
|
133
|
+
type: 'navigation',
|
|
134
|
+
target,
|
|
135
|
+
context: 'navigate',
|
|
136
|
+
sourceRef: {
|
|
137
|
+
file: filePath,
|
|
138
|
+
line,
|
|
139
|
+
snippet: scriptBlock.content.substring(
|
|
140
|
+
node.start - (ast.program.body[0]?.start || 0),
|
|
141
|
+
node.end - (ast.program.body[0]?.start || 0)
|
|
142
|
+
),
|
|
143
|
+
},
|
|
144
|
+
proof: arg.type === 'StringLiteral' ? 'PROVEN_EXPECTATION' : 'LIKELY_EXPECTATION',
|
|
145
|
+
metadata: {
|
|
146
|
+
navigationType: 'navigate',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
} catch (parseError) {
|
|
154
|
+
// Skip if parsing fails
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
// Skip if extraction fails
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return expectations;
|
|
162
|
+
}
|
|
163
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 20 — Svelte Network Detector
|
|
3
|
+
*
|
|
4
|
+
* Detects network calls (fetch, axios) in Svelte component handlers and lifecycle functions.
|
|
5
|
+
* Reuses AST network detector but ensures it works with Svelte SFC script blocks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { extractSvelteSFC, extractTemplateBindings, mapTemplateHandlersToScript } from './svelte-sfc-extractor.js';
|
|
9
|
+
import { detectNetworkCallsAST } from './ast-network-detector.js';
|
|
10
|
+
import { relative } from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Detect network promises in Svelte SFC
|
|
14
|
+
*
|
|
15
|
+
* @param {string} filePath - Path to .svelte file
|
|
16
|
+
* @param {string} content - Full file content
|
|
17
|
+
* @param {string} projectRoot - Project root directory
|
|
18
|
+
* @returns {Array} Array of network expectations
|
|
19
|
+
*/
|
|
20
|
+
export function detectSvelteNetwork(filePath, content, projectRoot) {
|
|
21
|
+
const expectations = [];
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const sfc = extractSvelteSFC(content);
|
|
25
|
+
const { scriptBlocks, markup } = sfc;
|
|
26
|
+
|
|
27
|
+
// Extract event handlers from markup to identify UI-bound handlers
|
|
28
|
+
const templateBindings = markup ? extractTemplateBindings(markup.content) : { eventHandlers: [] };
|
|
29
|
+
const mappedHandlers = scriptBlocks.length > 0 && templateBindings.eventHandlers.length > 0
|
|
30
|
+
? mapTemplateHandlersToScript(templateBindings.eventHandlers, scriptBlocks[0].content)
|
|
31
|
+
: [];
|
|
32
|
+
|
|
33
|
+
const uiBoundHandlers = new Set(mappedHandlers.map(h => h.handler));
|
|
34
|
+
|
|
35
|
+
// Process each script block
|
|
36
|
+
for (const scriptBlock of scriptBlocks) {
|
|
37
|
+
if (!scriptBlock.content) continue;
|
|
38
|
+
|
|
39
|
+
// Use AST network detector on script content
|
|
40
|
+
const networkCalls = detectNetworkCallsAST(scriptBlock.content, filePath, relative(projectRoot, filePath));
|
|
41
|
+
|
|
42
|
+
// Filter and enhance network calls
|
|
43
|
+
for (const networkCall of networkCalls) {
|
|
44
|
+
// Check if this is in a UI-bound handler
|
|
45
|
+
const isUIBound = networkCall.context && uiBoundHandlers.has(networkCall.context);
|
|
46
|
+
|
|
47
|
+
// Skip analytics-only calls (filtered by guardrails later)
|
|
48
|
+
if (networkCall.target && (
|
|
49
|
+
networkCall.target.includes('/api/analytics') ||
|
|
50
|
+
networkCall.target.includes('/api/track') ||
|
|
51
|
+
networkCall.target.includes('/api/beacon')
|
|
52
|
+
)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
expectations.push({
|
|
57
|
+
type: 'network',
|
|
58
|
+
target: networkCall.target,
|
|
59
|
+
method: networkCall.method || 'GET',
|
|
60
|
+
context: networkCall.context || 'component',
|
|
61
|
+
sourceRef: {
|
|
62
|
+
file: filePath,
|
|
63
|
+
line: networkCall.line || scriptBlock.startLine,
|
|
64
|
+
snippet: networkCall.snippet || '',
|
|
65
|
+
},
|
|
66
|
+
proof: networkCall.proof || 'LIKELY_EXPECTATION',
|
|
67
|
+
metadata: {
|
|
68
|
+
isUIBound,
|
|
69
|
+
handlerContext: networkCall.context,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// Skip if extraction fails
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return expectations;
|
|
79
|
+
}
|
|
80
|
+
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 20 — Svelte SFC (Single File Component) Extractor
|
|
3
|
+
*
|
|
4
|
+
* Extracts <script>, <script context="module">, and markup content from .svelte files.
|
|
5
|
+
* Deterministic and robust (no external runtime execution).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* PHASE 20: Extract Svelte SFC blocks
|
|
10
|
+
*
|
|
11
|
+
* @param {string} content - Full .svelte file content
|
|
12
|
+
* @returns {Object} { scriptBlocks: [{content, lang, startLine, isModule}], markup: {content, startLine} }
|
|
13
|
+
*/
|
|
14
|
+
export function extractSvelteSFC(content) {
|
|
15
|
+
const scriptBlocks = [];
|
|
16
|
+
let markup = null;
|
|
17
|
+
|
|
18
|
+
// Extract <script> blocks (including <script context="module">)
|
|
19
|
+
const scriptRegex = /<script(?:\s+context=["']module["'])?(?:\s+lang=["']([^"']+)["'])?[^>]*>([\s\S]*?)<\/script>/gi;
|
|
20
|
+
let scriptMatch;
|
|
21
|
+
|
|
22
|
+
while ((scriptMatch = scriptRegex.exec(content)) !== null) {
|
|
23
|
+
const isModule = scriptMatch[0].includes('context="module"') || scriptMatch[0].includes("context='module'");
|
|
24
|
+
const lang = scriptMatch[1] || 'js';
|
|
25
|
+
const scriptContent = scriptMatch[2];
|
|
26
|
+
|
|
27
|
+
// Calculate start line
|
|
28
|
+
const beforeMatch = content.substring(0, scriptMatch.index);
|
|
29
|
+
const startLine = (beforeMatch.match(/\n/g) || []).length + 1;
|
|
30
|
+
|
|
31
|
+
scriptBlocks.push({
|
|
32
|
+
content: scriptContent.trim(),
|
|
33
|
+
lang: lang.toLowerCase(),
|
|
34
|
+
startLine,
|
|
35
|
+
isModule,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Extract markup (everything outside script/style tags)
|
|
40
|
+
// Svelte markup is the template content
|
|
41
|
+
const styleRegex = /<style[^>]*>[\s\S]*?<\/style>/gi;
|
|
42
|
+
const allScriptRegex = /<script[^>]*>[\s\S]*?<\/script>/gi;
|
|
43
|
+
|
|
44
|
+
let markupContent = content;
|
|
45
|
+
// Remove style blocks
|
|
46
|
+
markupContent = markupContent.replace(styleRegex, '');
|
|
47
|
+
// Remove script blocks
|
|
48
|
+
markupContent = markupContent.replace(allScriptRegex, '');
|
|
49
|
+
|
|
50
|
+
// Find first non-whitespace line for markup
|
|
51
|
+
const lines = content.split('\n');
|
|
52
|
+
let markupStartLine = 1;
|
|
53
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54
|
+
const line = lines[i];
|
|
55
|
+
if (line.trim() && !line.trim().startsWith('<script') && !line.trim().startsWith('<style')) {
|
|
56
|
+
markupStartLine = i + 1;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (markupContent.trim()) {
|
|
62
|
+
markup = {
|
|
63
|
+
content: markupContent.trim(),
|
|
64
|
+
startLine: markupStartLine,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
scriptBlocks,
|
|
70
|
+
markup,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Extract template bindings from Svelte markup
|
|
76
|
+
* Detects reactive statements, event handlers, and bindings
|
|
77
|
+
*
|
|
78
|
+
* @param {string} markupContent - Svelte markup content
|
|
79
|
+
* @returns {Object} { reactiveStatements: [], eventHandlers: [], bindings: [] }
|
|
80
|
+
*/
|
|
81
|
+
export function extractTemplateBindings(markupContent) {
|
|
82
|
+
const reactiveStatements = [];
|
|
83
|
+
const eventHandlers = [];
|
|
84
|
+
const bindings = [];
|
|
85
|
+
|
|
86
|
+
// Extract reactive statements: $: statements
|
|
87
|
+
const reactiveRegex = /\$:\s*([^;]+);/g;
|
|
88
|
+
let reactiveMatch;
|
|
89
|
+
while ((reactiveMatch = reactiveRegex.exec(markupContent)) !== null) {
|
|
90
|
+
reactiveStatements.push({
|
|
91
|
+
statement: reactiveMatch[1].trim(),
|
|
92
|
+
line: (markupContent.substring(0, reactiveMatch.index).match(/\n/g) || []).length + 1,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Extract event handlers: on:click, on:submit, etc.
|
|
97
|
+
const eventHandlerRegex = /on:(\w+)=["']([^"']+)["']/g;
|
|
98
|
+
let handlerMatch;
|
|
99
|
+
while ((handlerMatch = eventHandlerRegex.exec(markupContent)) !== null) {
|
|
100
|
+
eventHandlers.push({
|
|
101
|
+
event: handlerMatch[1],
|
|
102
|
+
handler: handlerMatch[2],
|
|
103
|
+
line: (markupContent.substring(0, handlerMatch.index).match(/\n/g) || []).length + 1,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Extract bindings: bind:value, bind:checked, etc.
|
|
108
|
+
const bindingRegex = /bind:(\w+)=["']([^"']+)["']/g;
|
|
109
|
+
let bindingMatch;
|
|
110
|
+
while ((bindingMatch = bindingRegex.exec(markupContent)) !== null) {
|
|
111
|
+
bindings.push({
|
|
112
|
+
property: bindingMatch[1],
|
|
113
|
+
variable: bindingMatch[2],
|
|
114
|
+
line: (markupContent.substring(0, bindingMatch.index).match(/\n/g) || []).length + 1,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
reactiveStatements,
|
|
120
|
+
eventHandlers,
|
|
121
|
+
bindings,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Map template handlers to script functions
|
|
127
|
+
* Helps identify which handlers are UI-bound
|
|
128
|
+
*
|
|
129
|
+
* @param {Array} eventHandlers - Event handlers from template
|
|
130
|
+
* @param {string} scriptContent - Script block content
|
|
131
|
+
* @returns {Array} Mapped handlers with function references
|
|
132
|
+
*/
|
|
133
|
+
export function mapTemplateHandlersToScript(eventHandlers, scriptContent) {
|
|
134
|
+
return eventHandlers.map(handler => {
|
|
135
|
+
// Try to find function definition in script
|
|
136
|
+
const functionRegex = new RegExp(`(?:function\\s+${handler.handler}|const\\s+${handler.handler}\\s*=\\s*[^(]*\\(|${handler.handler}\\s*=\\s*[^(]*\\(|export\\s+function\\s+${handler.handler})`, 'g');
|
|
137
|
+
const functionMatch = functionRegex.exec(scriptContent);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
...handler,
|
|
141
|
+
functionFound: !!functionMatch,
|
|
142
|
+
functionLine: functionMatch ? (scriptContent.substring(0, functionMatch.index).match(/\n/g) || []).length + 1 : null,
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|