@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
|
@@ -1,31 +1,142 @@
|
|
|
1
1
|
import { atomicWriteJson } from './atomic-write.js';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
import { findingIdFromExpectationId } from './idgen.js';
|
|
4
|
+
import {
|
|
5
|
+
enforceContractsOnFindings,
|
|
6
|
+
FINDING_STATUS,
|
|
7
|
+
CONFIDENCE_LEVEL,
|
|
8
|
+
IMPACT,
|
|
9
|
+
USER_RISK,
|
|
10
|
+
OWNERSHIP,
|
|
11
|
+
} from '../../verax/core/contracts/index.js';
|
|
12
|
+
import { ARTIFACT_REGISTRY, getArtifactVersions } from '../../verax/core/artifacts/registry.js';
|
|
4
13
|
|
|
5
14
|
/**
|
|
6
|
-
* Write findings.json artifact with deterministic IDs
|
|
15
|
+
* Write findings.json artifact with deterministic IDs and contract enforcement
|
|
7
16
|
*/
|
|
8
17
|
export function writeFindingsJson(runDir, findingsData) {
|
|
9
|
-
const findingsPath = resolve(runDir,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
const findingsPath = resolve(runDir, ARTIFACT_REGISTRY.findings.filename);
|
|
19
|
+
const normalizedFindings = normalizeFindings(findingsData?.findings || []);
|
|
20
|
+
const enforcement = enforceContractsOnFindings(normalizedFindings);
|
|
21
|
+
|
|
22
|
+
const findingsWithIds = enforcement.valid.map((finding) => ({
|
|
13
23
|
...finding,
|
|
14
|
-
findingId: findingIdFromExpectationId(finding.id),
|
|
24
|
+
findingId: findingIdFromExpectationId(finding.id || finding.expectationId || ''),
|
|
15
25
|
}));
|
|
16
|
-
|
|
26
|
+
|
|
27
|
+
const stats = findingsData?.stats || {};
|
|
17
28
|
const payload = {
|
|
29
|
+
contractVersion: 1,
|
|
30
|
+
artifactVersions: getArtifactVersions(),
|
|
18
31
|
findings: findingsWithIds,
|
|
32
|
+
total: findingsWithIds.length,
|
|
19
33
|
stats: {
|
|
20
|
-
total:
|
|
21
|
-
silentFailures:
|
|
22
|
-
observed:
|
|
23
|
-
coverageGaps:
|
|
24
|
-
unproven:
|
|
25
|
-
informational:
|
|
34
|
+
total: findingsWithIds.length,
|
|
35
|
+
silentFailures: stats.silentFailures || 0,
|
|
36
|
+
observed: stats.observed || 0,
|
|
37
|
+
coverageGaps: stats.coverageGaps || 0,
|
|
38
|
+
unproven: stats.unproven || 0,
|
|
39
|
+
informational: stats.informational || 0,
|
|
40
|
+
},
|
|
41
|
+
detectedAt: findingsData?.detectedAt || new Date().toISOString(),
|
|
42
|
+
enforcement: {
|
|
43
|
+
droppedCount: enforcement.dropped.length,
|
|
44
|
+
downgradedCount: enforcement.downgrades.length,
|
|
45
|
+
downgrades: enforcement.downgrades.map((entry) => ({
|
|
46
|
+
reason: entry.reason,
|
|
47
|
+
originalStatus: entry.original?.status,
|
|
48
|
+
downgradeToStatus: entry.downgraded?.status,
|
|
49
|
+
})),
|
|
50
|
+
dropped: enforcement.dropped.map((entry) => ({
|
|
51
|
+
reason: entry.reason,
|
|
52
|
+
})),
|
|
26
53
|
},
|
|
27
|
-
detectedAt: findingsData.detectedAt || new Date().toISOString(),
|
|
28
54
|
};
|
|
29
|
-
|
|
55
|
+
|
|
30
56
|
atomicWriteJson(findingsPath, payload);
|
|
57
|
+
return { path: findingsPath, payload };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeFindings(findings) {
|
|
61
|
+
return findings.map((finding) => normalizeFinding(finding));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeFinding(finding) {
|
|
65
|
+
const status = finding.status || mapClassificationToStatus(finding.classification);
|
|
66
|
+
const evidence = normalizeEvidence(finding.evidence);
|
|
67
|
+
const confidence = normalizeConfidence(finding.confidence);
|
|
68
|
+
const signals = normalizeSignals(finding);
|
|
69
|
+
const interaction = finding.interaction || {
|
|
70
|
+
type: finding.type || 'unknown',
|
|
71
|
+
selector: finding.promise?.selector || finding.promise?.value || finding.source?.file || 'unknown',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
...finding,
|
|
76
|
+
status,
|
|
77
|
+
evidence,
|
|
78
|
+
confidence,
|
|
79
|
+
signals,
|
|
80
|
+
interaction,
|
|
81
|
+
what_happened: finding.what_happened || finding.reason || 'Expectation was exercised during scan.',
|
|
82
|
+
what_was_expected: finding.what_was_expected || finding.promise?.value || 'Expectation derived from source code.',
|
|
83
|
+
what_was_observed: finding.what_was_observed || finding.reason || 'Observation recorded for expectation.',
|
|
84
|
+
why_it_matters: finding.why_it_matters || 'Potential silent failure identified by expectation vs observation.',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function mapClassificationToStatus(classification) {
|
|
89
|
+
if (classification === 'observed') return FINDING_STATUS.CONFIRMED;
|
|
90
|
+
if (classification === 'silent-failure') return FINDING_STATUS.SUSPECTED;
|
|
91
|
+
if (classification === 'coverage-gap' || classification === 'unproven') return FINDING_STATUS.SUSPECTED;
|
|
92
|
+
return FINDING_STATUS.INFORMATIONAL;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeConfidence(confidence) {
|
|
96
|
+
if (confidence && typeof confidence === 'object' && confidence.level && confidence.score !== undefined) {
|
|
97
|
+
return confidence;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const numeric = typeof confidence === 'number' ? Math.max(0, Math.min(1, confidence)) : 0;
|
|
101
|
+
const score = Math.round(numeric * 100);
|
|
102
|
+
let level = CONFIDENCE_LEVEL.UNPROVEN;
|
|
103
|
+
if (score >= 80) level = CONFIDENCE_LEVEL.HIGH;
|
|
104
|
+
else if (score >= 60) level = CONFIDENCE_LEVEL.MEDIUM;
|
|
105
|
+
else if (score > 0) level = CONFIDENCE_LEVEL.LOW;
|
|
106
|
+
|
|
107
|
+
return { level, score };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeEvidence(evidenceArray) {
|
|
111
|
+
const items = Array.isArray(evidenceArray) ? evidenceArray : [];
|
|
112
|
+
const hasNetworkActivity = items.some((item) => item?.type === 'network-log');
|
|
113
|
+
const hasDomChange = items.some((item) => item?.type === 'screenshot');
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
type: hasNetworkActivity ? 'network_activity' : undefined,
|
|
117
|
+
hasDomChange,
|
|
118
|
+
hasUrlChange: false,
|
|
119
|
+
hasNetworkActivity,
|
|
120
|
+
hasStateChange: false,
|
|
121
|
+
networkRequests: items.filter((item) => item?.type === 'network-log'),
|
|
122
|
+
before: items.find((item) => item?.type === 'screenshot')?.path,
|
|
123
|
+
after: undefined,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeSignals(finding) {
|
|
128
|
+
if (finding.signals && typeof finding.signals === 'object') {
|
|
129
|
+
return finding.signals;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const impact = (finding.impact && IMPACT[finding.impact]) ? finding.impact : IMPACT.MEDIUM;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
impact,
|
|
136
|
+
userRisk: USER_RISK.CONFUSES,
|
|
137
|
+
ownership: OWNERSHIP.FRONTEND,
|
|
138
|
+
grouping: {
|
|
139
|
+
expectationType: finding.type || 'unknown',
|
|
140
|
+
},
|
|
141
|
+
};
|
|
31
142
|
}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { resolve } from 'path';
|
|
2
2
|
import { atomicWriteJson } from './atomic-write.js';
|
|
3
3
|
import { compareExpectations } from './idgen.js';
|
|
4
|
+
import { ARTIFACT_REGISTRY } from '../../verax/core/artifacts/registry.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Write learn.json artifact
|
|
7
8
|
* Maintains deterministic ordering for stable output
|
|
8
9
|
*/
|
|
9
10
|
export function writeLearnJson(runPaths, expectations, skipped) {
|
|
10
|
-
const learnJsonPath = resolve(runPaths.baseDir,
|
|
11
|
+
const learnJsonPath = resolve(runPaths.baseDir, ARTIFACT_REGISTRY.learn.filename);
|
|
11
12
|
|
|
12
13
|
// Sort expectations deterministically for stable output
|
|
13
14
|
const sortedExpectations = [...expectations].sort(compareExpectations);
|
|
14
15
|
|
|
15
16
|
const learnJson = {
|
|
17
|
+
contractVersion: 1,
|
|
16
18
|
expectations: sortedExpectations,
|
|
17
19
|
stats: {
|
|
18
20
|
totalExpectations: sortedExpectations.length,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chromium } from 'playwright';
|
|
2
2
|
import { writeFileSync, mkdirSync } from 'fs';
|
|
3
|
-
import { resolve
|
|
3
|
+
import { resolve } from 'path';
|
|
4
4
|
import { redactHeaders, redactUrl, redactBody, redactConsole, getRedactionCounters } from './redact.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -60,9 +60,16 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
// Navigate to base URL first
|
|
63
|
+
// Navigate to base URL first with explicit timeout
|
|
64
64
|
try {
|
|
65
|
-
await page.goto(url, {
|
|
65
|
+
await page.goto(url, {
|
|
66
|
+
waitUntil: 'domcontentloaded', // Use domcontentloaded instead of networkidle for faster timeout
|
|
67
|
+
timeout: 30000
|
|
68
|
+
});
|
|
69
|
+
// Wait for network idle with separate timeout
|
|
70
|
+
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
|
|
71
|
+
// Network idle timeout is acceptable, continue
|
|
72
|
+
});
|
|
66
73
|
} catch (error) {
|
|
67
74
|
// Continue even if initial load fails
|
|
68
75
|
if (onProgress) {
|
|
@@ -96,6 +103,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
96
103
|
type: exp.type,
|
|
97
104
|
promise: exp.promise,
|
|
98
105
|
source: exp.source,
|
|
106
|
+
attempted: false,
|
|
99
107
|
observed: false,
|
|
100
108
|
observedAt: null,
|
|
101
109
|
evidenceFiles: [],
|
|
@@ -107,6 +115,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
107
115
|
let evidence = null;
|
|
108
116
|
|
|
109
117
|
if (exp.type === 'navigation') {
|
|
118
|
+
observation.attempted = true; // Mark as attempted
|
|
110
119
|
result = await observeNavigation(
|
|
111
120
|
page,
|
|
112
121
|
exp,
|
|
@@ -117,6 +126,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
117
126
|
);
|
|
118
127
|
evidence = result ? `nav_${expNum}_after.png` : null;
|
|
119
128
|
} else if (exp.type === 'network') {
|
|
129
|
+
observation.attempted = true; // Mark as attempted
|
|
120
130
|
result = await observeNetwork(page, exp, networkLogs, 5000);
|
|
121
131
|
if (result) {
|
|
122
132
|
const evidenceFile = `network_${expNum}.json`;
|
|
@@ -135,6 +145,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
135
145
|
evidence = null;
|
|
136
146
|
}
|
|
137
147
|
} else if (exp.type === 'state') {
|
|
148
|
+
observation.attempted = true; // Mark as attempted
|
|
138
149
|
result = await observeState(page, exp, evidencePath, expNum);
|
|
139
150
|
evidence = result ? `state_${expNum}_after.png` : null;
|
|
140
151
|
}
|
|
@@ -187,19 +198,47 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
|
|
|
187
198
|
observedAt: new Date().toISOString(),
|
|
188
199
|
};
|
|
189
200
|
} finally {
|
|
190
|
-
//
|
|
201
|
+
// Robust cleanup: ensure browser/context/page are closed
|
|
202
|
+
// Remove all event listeners to prevent leaks
|
|
191
203
|
if (page) {
|
|
192
204
|
try {
|
|
193
|
-
|
|
205
|
+
// Remove all listeners
|
|
206
|
+
page.removeAllListeners();
|
|
207
|
+
// @ts-expect-error - Playwright page.close() doesn't accept timeout option, but we use it for safety
|
|
208
|
+
await page.close({ timeout: 5000 }).catch(() => {});
|
|
194
209
|
} catch (e) {
|
|
195
|
-
// Ignore close errors
|
|
210
|
+
// Ignore close errors but emit warning if onProgress available
|
|
211
|
+
if (onProgress) {
|
|
212
|
+
onProgress({
|
|
213
|
+
event: 'observe:warning',
|
|
214
|
+
message: `Page cleanup warning: ${e.message}`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
196
217
|
}
|
|
197
218
|
}
|
|
219
|
+
|
|
220
|
+
// Close browser context if it exists
|
|
198
221
|
if (browser) {
|
|
199
222
|
try {
|
|
200
|
-
|
|
223
|
+
const contexts = browser.contexts();
|
|
224
|
+
for (const context of contexts) {
|
|
225
|
+
try {
|
|
226
|
+
// @ts-expect-error - Playwright context.close() doesn't accept timeout option, but we use it for safety
|
|
227
|
+
await context.close({ timeout: 5000 }).catch(() => {});
|
|
228
|
+
} catch (e) {
|
|
229
|
+
// Ignore context close errors
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// @ts-expect-error - Playwright browser.close() doesn't accept timeout option, but we use it for safety
|
|
233
|
+
await browser.close({ timeout: 5000 }).catch(() => {});
|
|
201
234
|
} catch (e) {
|
|
202
|
-
// Ignore close errors
|
|
235
|
+
// Ignore browser close errors but emit warning if onProgress available
|
|
236
|
+
if (onProgress) {
|
|
237
|
+
onProgress({
|
|
238
|
+
event: 'observe:warning',
|
|
239
|
+
message: `Browser cleanup warning: ${e.message}`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
203
242
|
}
|
|
204
243
|
}
|
|
205
244
|
}
|
|
@@ -242,6 +281,7 @@ async function observeNavigation(page, expectation, baseUrl, visitedUrls, eviden
|
|
|
242
281
|
await page.click(`a[href="${element.href}"]`);
|
|
243
282
|
} catch (e2) {
|
|
244
283
|
// Try clicking by text content
|
|
284
|
+
// eslint-disable-next-line no-undef
|
|
245
285
|
const text = await page.evaluate((href) => {
|
|
246
286
|
const anchors = Array.from(document.querySelectorAll('a'));
|
|
247
287
|
const found = anchors.find(a => a.getAttribute('href') === href);
|
|
@@ -254,14 +294,23 @@ async function observeNavigation(page, expectation, baseUrl, visitedUrls, eviden
|
|
|
254
294
|
}
|
|
255
295
|
}
|
|
256
296
|
|
|
257
|
-
// Wait for navigation or SPA update
|
|
297
|
+
// Wait for navigation or SPA update with explicit timeout
|
|
258
298
|
try {
|
|
259
|
-
await page.waitForNavigation({
|
|
299
|
+
await page.waitForNavigation({
|
|
300
|
+
waitUntil: 'domcontentloaded',
|
|
301
|
+
timeout: 5000
|
|
302
|
+
}).catch(() => {
|
|
303
|
+
// Navigation timeout is acceptable for SPAs
|
|
304
|
+
});
|
|
305
|
+
// Wait for network idle with separate timeout
|
|
306
|
+
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
|
307
|
+
// Network idle timeout is acceptable
|
|
308
|
+
});
|
|
260
309
|
} catch (e) {
|
|
261
|
-
// Navigation might not happen
|
|
310
|
+
// Navigation might not happen, continue
|
|
262
311
|
}
|
|
263
312
|
|
|
264
|
-
// Wait for potential SPA updates
|
|
313
|
+
// Wait for potential SPA updates (bounded)
|
|
265
314
|
await page.waitForTimeout(300);
|
|
266
315
|
|
|
267
316
|
// Screenshot after interaction
|
|
@@ -304,13 +353,21 @@ async function observeNetwork(page, expectation, networkLogs, timeoutMs) {
|
|
|
304
353
|
if (found) {
|
|
305
354
|
clearInterval(checkTimer);
|
|
306
355
|
resolve(true);
|
|
356
|
+
return;
|
|
307
357
|
}
|
|
308
358
|
|
|
309
359
|
if (Date.now() - startTime > timeoutMs) {
|
|
310
360
|
clearInterval(checkTimer);
|
|
311
361
|
resolve(false);
|
|
362
|
+
return;
|
|
312
363
|
}
|
|
313
364
|
}, 100);
|
|
365
|
+
|
|
366
|
+
// CRITICAL: Unref the interval so it doesn't keep the process alive
|
|
367
|
+
// This allows tests to exit cleanly even if interval is not cleared
|
|
368
|
+
if (checkTimer && checkTimer.unref) {
|
|
369
|
+
checkTimer.unref();
|
|
370
|
+
}
|
|
314
371
|
});
|
|
315
372
|
}
|
|
316
373
|
|
|
@@ -353,14 +410,3 @@ async function observeState(page, expectation, evidencePath, expNum) {
|
|
|
353
410
|
}
|
|
354
411
|
}
|
|
355
412
|
|
|
356
|
-
/**
|
|
357
|
-
* Check if page content changed (for SPA detection)
|
|
358
|
-
*/
|
|
359
|
-
async function checkPageContentChanged(page) {
|
|
360
|
-
try {
|
|
361
|
-
const bodyText = await page.locator('body').textContent();
|
|
362
|
-
return bodyText && bodyText.length > 0;
|
|
363
|
-
} catch (error) {
|
|
364
|
-
return false;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { atomicWriteJson } from './atomic-write.js';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
|
+
import { ARTIFACT_REGISTRY } from '../../verax/core/artifacts/registry.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Write observe.json artifact
|
|
6
7
|
*/
|
|
7
8
|
export function writeObserveJson(runDir, observeData) {
|
|
8
|
-
const observePath = resolve(runDir,
|
|
9
|
+
const observePath = resolve(runDir, ARTIFACT_REGISTRY.observe.filename);
|
|
9
10
|
|
|
10
11
|
const payload = {
|
|
12
|
+
contractVersion: 1,
|
|
11
13
|
observations: observeData.observations || [],
|
|
12
14
|
stats: {
|
|
13
15
|
attempted: observeData.stats?.attempted || 0,
|
package/src/cli/util/paths.js
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
|
-
import { join } from 'path';
|
|
1
|
+
import { join, isAbsolute } from 'path';
|
|
2
2
|
import { mkdirSync } from 'fs';
|
|
3
|
+
import { buildRunArtifactPaths } from '../../verax/core/artifacts/registry.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Build run artifact paths
|
|
6
7
|
*/
|
|
7
8
|
export function getRunPaths(projectRoot, outDir, runId) {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
runStatusJson: join(baseDir, 'run.status.json'),
|
|
13
|
-
runMetaJson: join(baseDir, 'run.meta.json'),
|
|
14
|
-
summaryJson: join(baseDir, 'summary.json'),
|
|
15
|
-
findingsJson: join(baseDir, 'findings.json'),
|
|
16
|
-
tracesJsonl: join(baseDir, 'traces.jsonl'),
|
|
17
|
-
evidenceDir: join(baseDir, 'evidence'),
|
|
18
|
-
learnJson: join(baseDir, 'learn.json'),
|
|
19
|
-
observeJson: join(baseDir, 'observe.json'),
|
|
20
|
-
};
|
|
9
|
+
const outBase = isAbsolute(outDir) ? outDir : join(projectRoot, outDir);
|
|
10
|
+
const baseDir = join(outBase, 'runs', runId);
|
|
11
|
+
|
|
12
|
+
return buildRunArtifactPaths(baseDir);
|
|
21
13
|
}
|
|
22
14
|
|
|
23
15
|
/**
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
2
|
import { resolve, dirname } from 'path';
|
|
3
|
+
import { assertExecutionBootstrapAllowed } from './bootstrap-guard.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Project Discovery Module
|
|
6
7
|
* Detects framework, router, source root, and dev server configuration
|
|
8
|
+
*
|
|
9
|
+
* @typedef {Object} ProjectProfile
|
|
10
|
+
* @property {string} framework
|
|
11
|
+
* @property {string|null} router
|
|
12
|
+
* @property {string} sourceRoot
|
|
13
|
+
* @property {string} packageManager
|
|
14
|
+
* @property {{dev: string|null, build: string|null, start: string|null}} scripts
|
|
15
|
+
* @property {string} detectedAt
|
|
16
|
+
* @property {string|null} packageJsonPath
|
|
17
|
+
* @property {number} [fileCount] - Optional file count for budget calculation
|
|
7
18
|
*/
|
|
8
19
|
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} srcPath
|
|
22
|
+
* @returns {Promise<ProjectProfile>}
|
|
23
|
+
*/
|
|
9
24
|
export async function discoverProject(srcPath) {
|
|
25
|
+
// PHASE 21.6.1: Runtime guard - crash if called during inspection
|
|
26
|
+
assertExecutionBootstrapAllowed('discoverProject');
|
|
10
27
|
const projectRoot = resolve(srcPath);
|
|
11
28
|
|
|
12
29
|
// Find the nearest package.json
|
|
@@ -62,6 +79,12 @@ function findPackageJson(startPath) {
|
|
|
62
79
|
return immediatePackage;
|
|
63
80
|
}
|
|
64
81
|
|
|
82
|
+
// For static HTML projects, don't walk up - use the startPath as project root
|
|
83
|
+
// This prevents finding parent package.json files that aren't relevant
|
|
84
|
+
if (hasStaticHtml(currentPath)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
65
88
|
// Then walk up (limit to 5 levels for monorepos, not 10)
|
|
66
89
|
for (let i = 0; i < 5; i++) {
|
|
67
90
|
const parentPath = dirname(currentPath);
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { atomicWriteJson } from './atomic-write.js';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
|
+
import { ARTIFACT_REGISTRY } from '../../verax/core/artifacts/registry.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Write project profile artifact
|
|
6
7
|
*/
|
|
7
8
|
export function writeProjectJson(runPaths, projectProfile) {
|
|
8
|
-
const projectJsonPath = resolve(runPaths.baseDir,
|
|
9
|
+
const projectJsonPath = resolve(runPaths.baseDir, ARTIFACT_REGISTRY.project.filename);
|
|
9
10
|
|
|
10
11
|
const projectJson = {
|
|
12
|
+
contractVersion: 1,
|
|
11
13
|
framework: projectProfile.framework,
|
|
12
14
|
router: projectProfile.router,
|
|
13
15
|
sourceRoot: projectProfile.sourceRoot,
|
package/src/cli/util/redact.js
CHANGED
|
@@ -105,14 +105,14 @@ export function redactTokensInText(text, counters = { headersRedacted: 0, tokens
|
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
// Bearer tokens
|
|
108
|
-
output = output.replace(/Bearer\s+([A-Za-z0-9._-]+)/gi, (
|
|
108
|
+
output = output.replace(/Bearer\s+([A-Za-z0-9._-]+)/gi, (_match, _token) => {
|
|
109
109
|
c.tokensRedacted += 1;
|
|
110
110
|
return `Bearer ${REDACTED}`;
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
// JWT-like strings (three base64url-ish segments)
|
|
114
114
|
// More specific: require uppercase or numbers, not just domain patterns like "api.example.com"
|
|
115
|
-
output = output.replace(/[A-Z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, (
|
|
115
|
+
output = output.replace(/[A-Z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, (_match) => {
|
|
116
116
|
c.tokensRedacted += 1;
|
|
117
117
|
return REDACTED;
|
|
118
118
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 21.6.1 — Run Resolver
|
|
3
|
+
*
|
|
4
|
+
* Pure filesystem logic to resolve run IDs.
|
|
5
|
+
* No side effects, no execution dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readdirSync, statSync, existsSync } from 'fs';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Find the latest run ID from .verax/runs/
|
|
13
|
+
*
|
|
14
|
+
* @param {string} projectDir - Project directory
|
|
15
|
+
* @returns {string|null} Latest run ID or null if no runs found
|
|
16
|
+
*/
|
|
17
|
+
export function findLatestRunId(projectDir) {
|
|
18
|
+
const runsDir = resolve(projectDir, '.verax', 'runs');
|
|
19
|
+
|
|
20
|
+
if (!existsSync(runsDir)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const runs = readdirSync(runsDir, { withFileTypes: true })
|
|
26
|
+
.filter(dirent => dirent.isDirectory())
|
|
27
|
+
.map(dirent => {
|
|
28
|
+
const runPath = resolve(runsDir, dirent.name);
|
|
29
|
+
try {
|
|
30
|
+
const stats = statSync(runPath);
|
|
31
|
+
return {
|
|
32
|
+
name: dirent.name,
|
|
33
|
+
mtimeMs: stats.mtimeMs
|
|
34
|
+
};
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
.filter(run => run !== null);
|
|
40
|
+
|
|
41
|
+
if (runs.length === 0) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Sort by modification time (descending) and return latest
|
|
46
|
+
runs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
47
|
+
return runs[0].name;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate that a run ID exists
|
|
55
|
+
*
|
|
56
|
+
* @param {string} projectDir - Project directory
|
|
57
|
+
* @param {string} runId - Run ID to validate
|
|
58
|
+
* @returns {boolean} Whether run exists
|
|
59
|
+
*/
|
|
60
|
+
export function validateRunId(projectDir, runId) {
|
|
61
|
+
const runDir = resolve(projectDir, '.verax', 'runs', runId);
|
|
62
|
+
return existsSync(runDir);
|
|
63
|
+
}
|
|
64
|
+
|