@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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 18 — Determinism Engine
|
|
3
|
+
* PHASE 21.2 — Determinism Truth Lock: Enforces HARD verdict
|
|
4
|
+
*
|
|
5
|
+
* Runs the same scan multiple times and compares results for determinism.
|
|
6
|
+
* PHASE 21.2: Also checks DecisionRecorder for adaptive events that break determinism.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, existsSync } from 'fs';
|
|
10
|
+
import { join, resolve } from 'path';
|
|
11
|
+
import { normalizeArtifact } from './normalize.js';
|
|
12
|
+
import { diffArtifacts } from './diff.js';
|
|
13
|
+
import { computeFindingIdentity } from './finding-identity.js';
|
|
14
|
+
import { ARTIFACT_REGISTRY } from '../artifacts/registry.js';
|
|
15
|
+
import { computeDeterminismVerdict, DETERMINISM_VERDICT, DETERMINISM_REASON } from './contract.js';
|
|
16
|
+
import { DecisionRecorder } from '../determinism-model.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* PHASE 18: Determinism verdict (re-exported from contract for backward compatibility)
|
|
20
|
+
*/
|
|
21
|
+
export { DETERMINISM_VERDICT, DETERMINISM_REASON } from './contract.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* PHASE 18: Run determinism check
|
|
25
|
+
*
|
|
26
|
+
* @param {Function} runFn - Function that executes a scan and returns artifact paths or in-memory artifacts
|
|
27
|
+
* @param {Object} options - Options
|
|
28
|
+
* @param {number} options.runs - Number of runs (default: 2)
|
|
29
|
+
* @param {Object} options.config - Configuration for runs
|
|
30
|
+
* @param {boolean} options.normalize - Whether to normalize artifacts (default: true)
|
|
31
|
+
* @returns {Object} { verdict, summary, diffs, runsMeta }
|
|
32
|
+
*/
|
|
33
|
+
export async function runDeterminismCheck(runFn, options = {}) {
|
|
34
|
+
const { runs = 2, config = {}, normalize = true } = options;
|
|
35
|
+
|
|
36
|
+
const runsMeta = [];
|
|
37
|
+
const runArtifacts = [];
|
|
38
|
+
|
|
39
|
+
// Execute runs
|
|
40
|
+
for (let i = 0; i < runs; i++) {
|
|
41
|
+
const runResult = await runFn(config);
|
|
42
|
+
runsMeta.push({
|
|
43
|
+
runIndex: i + 1,
|
|
44
|
+
runId: runResult.runId || null,
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
artifactPaths: runResult.artifactPaths || {},
|
|
47
|
+
artifacts: runResult.artifacts || {},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Load artifacts if paths provided
|
|
51
|
+
const artifacts = {};
|
|
52
|
+
if (runResult.artifactPaths) {
|
|
53
|
+
for (const [key, path] of Object.entries(runResult.artifactPaths)) {
|
|
54
|
+
try {
|
|
55
|
+
const content = readFileSync(path, 'utf-8');
|
|
56
|
+
artifacts[key] = JSON.parse(content);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Artifact not found or invalid
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else if (runResult.artifacts) {
|
|
62
|
+
Object.assign(artifacts, runResult.artifacts);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
runArtifacts.push(artifacts);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Compare runs
|
|
69
|
+
const diffs = [];
|
|
70
|
+
const allArtifacts = new Set();
|
|
71
|
+
|
|
72
|
+
// Collect all artifact names
|
|
73
|
+
for (const artifacts of runArtifacts) {
|
|
74
|
+
for (const key of Object.keys(artifacts)) {
|
|
75
|
+
allArtifacts.add(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Compare each artifact across runs
|
|
80
|
+
for (const artifactName of allArtifacts) {
|
|
81
|
+
const artifacts = runArtifacts.map(run => run[artifactName]);
|
|
82
|
+
|
|
83
|
+
// Normalize if requested
|
|
84
|
+
const normalizedArtifacts = normalize
|
|
85
|
+
? artifacts.map(art => art ? normalizeArtifact(artifactName, art) : null)
|
|
86
|
+
: artifacts;
|
|
87
|
+
|
|
88
|
+
// Compare first run with all subsequent runs
|
|
89
|
+
for (let i = 1; i < normalizedArtifacts.length; i++) {
|
|
90
|
+
const artifactA = normalizedArtifacts[0];
|
|
91
|
+
const artifactB = normalizedArtifacts[i];
|
|
92
|
+
|
|
93
|
+
// Build finding identity map for findings artifacts
|
|
94
|
+
let findingIdentityMap = null;
|
|
95
|
+
if (artifactName === 'findings' && artifactA && artifactB) {
|
|
96
|
+
findingIdentityMap = buildFindingIdentityMap(artifactA, artifactB);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const artifactDiffs = diffArtifacts(artifactA, artifactB, artifactName, findingIdentityMap);
|
|
100
|
+
|
|
101
|
+
// Add run context to diffs
|
|
102
|
+
for (const diff of artifactDiffs) {
|
|
103
|
+
diff.runA = 1;
|
|
104
|
+
diff.runB = i + 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
diffs.push(...artifactDiffs);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// PHASE 21.2: Check for adaptive events in DecisionRecorder (if available)
|
|
112
|
+
// HARD RULE: If adaptive events exist → verdict MUST be NON_DETERMINISTIC
|
|
113
|
+
let adaptiveVerdict = null;
|
|
114
|
+
let adaptiveReasons = [];
|
|
115
|
+
let adaptiveEvents = [];
|
|
116
|
+
|
|
117
|
+
// Try to load decisions.json from first run
|
|
118
|
+
if (runsMeta.length > 0 && runsMeta[0].artifactPaths?.runDir) {
|
|
119
|
+
const runDir = runsMeta[0].artifactPaths.runDir;
|
|
120
|
+
const decisionsPath = resolve(runDir, 'decisions.json');
|
|
121
|
+
|
|
122
|
+
if (existsSync(decisionsPath)) {
|
|
123
|
+
try {
|
|
124
|
+
const decisionsData = JSON.parse(readFileSync(decisionsPath, 'utf-8'));
|
|
125
|
+
const decisionRecorder = DecisionRecorder.fromExport(decisionsData);
|
|
126
|
+
const adaptiveCheck = computeDeterminismVerdict(decisionRecorder);
|
|
127
|
+
|
|
128
|
+
adaptiveVerdict = adaptiveCheck.verdict;
|
|
129
|
+
adaptiveReasons = adaptiveCheck.reasons;
|
|
130
|
+
adaptiveEvents = adaptiveCheck.adaptiveEvents;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// Ignore errors reading decisions
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// PHASE 21.2: Determine verdict
|
|
138
|
+
// HARD RULE: If adaptive events exist → verdict MUST be NON_DETERMINISTIC (even if artifacts match)
|
|
139
|
+
const blockerDiffs = diffs.filter(d => d.severity === 'BLOCKER');
|
|
140
|
+
const artifactVerdict = blockerDiffs.length === 0 ? DETERMINISM_VERDICT.DETERMINISTIC : DETERMINISM_VERDICT.NON_DETERMINISTIC;
|
|
141
|
+
|
|
142
|
+
// PHASE 21.2: Final verdict - adaptive events override artifact comparison
|
|
143
|
+
const verdict = (adaptiveVerdict === DETERMINISM_VERDICT.NON_DETERMINISTIC)
|
|
144
|
+
? DETERMINISM_VERDICT.NON_DETERMINISTIC
|
|
145
|
+
: artifactVerdict;
|
|
146
|
+
|
|
147
|
+
// Build summary
|
|
148
|
+
const summary = buildSummary(diffs, runsMeta);
|
|
149
|
+
|
|
150
|
+
// PHASE 21.2: Include adaptive event information in summary
|
|
151
|
+
if (adaptiveVerdict === DETERMINISM_VERDICT.NON_DETERMINISTIC) {
|
|
152
|
+
summary.adaptiveEventsDetected = true;
|
|
153
|
+
summary.adaptiveEventCount = adaptiveEvents.length;
|
|
154
|
+
summary.adaptiveReasons = adaptiveReasons;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
verdict,
|
|
159
|
+
summary,
|
|
160
|
+
diffs,
|
|
161
|
+
runsMeta,
|
|
162
|
+
// PHASE 21.2: Include adaptive event information
|
|
163
|
+
adaptiveVerdict,
|
|
164
|
+
adaptiveReasons,
|
|
165
|
+
adaptiveEvents
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Build finding identity map for matching findings across runs
|
|
171
|
+
*/
|
|
172
|
+
function buildFindingIdentityMap(artifactA, artifactB) {
|
|
173
|
+
const map = new Map();
|
|
174
|
+
|
|
175
|
+
const findingsA = artifactA.findings || [];
|
|
176
|
+
const findingsB = artifactB.findings || [];
|
|
177
|
+
|
|
178
|
+
// Build identity for findings in both runs
|
|
179
|
+
for (const finding of [...findingsA, ...findingsB]) {
|
|
180
|
+
const identity = computeFindingIdentity(finding);
|
|
181
|
+
map.set(finding, identity);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return map;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Build summary from diffs
|
|
189
|
+
*/
|
|
190
|
+
function buildSummary(diffs, runsMeta) {
|
|
191
|
+
const blockerCount = diffs.filter(d => d.severity === 'BLOCKER').length;
|
|
192
|
+
const warnCount = diffs.filter(d => d.severity === 'WARN').length;
|
|
193
|
+
const infoCount = diffs.filter(d => d.severity === 'INFO').length;
|
|
194
|
+
|
|
195
|
+
// Group by reason code
|
|
196
|
+
const reasonCounts = {};
|
|
197
|
+
for (const diff of diffs) {
|
|
198
|
+
const code = diff.reasonCode || 'UNKNOWN';
|
|
199
|
+
reasonCounts[code] = (reasonCounts[code] || 0) + 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Top reasons
|
|
203
|
+
const topReasons = Object.entries(reasonCounts)
|
|
204
|
+
.sort((a, b) => b[1] - a[1])
|
|
205
|
+
.slice(0, 5)
|
|
206
|
+
.map(([code, count]) => ({ code, count }));
|
|
207
|
+
|
|
208
|
+
// Stability score (0..1)
|
|
209
|
+
const totalDiffs = diffs.length;
|
|
210
|
+
const stabilityScore = totalDiffs === 0 ? 1.0 : Math.max(0, 1.0 - (blockerCount * 0.5 + warnCount * 0.2 + infoCount * 0.1) / Math.max(1, totalDiffs));
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
totalDiffs,
|
|
214
|
+
blockerCount,
|
|
215
|
+
warnCount,
|
|
216
|
+
infoCount,
|
|
217
|
+
topReasons,
|
|
218
|
+
stabilityScore,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 18 — Stable Finding Identity
|
|
3
|
+
*
|
|
4
|
+
* Computes stable identity keys for findings that are consistent across runs.
|
|
5
|
+
* Used for matching findings between runs for comparison.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* PHASE 18: Compute stable finding identity
|
|
12
|
+
*
|
|
13
|
+
* Uses type + promise signature + route/network target + interaction selector/context chain
|
|
14
|
+
* Includes evidence trigger source signature where available (AST snippet hash or location)
|
|
15
|
+
* Must NOT include runId/timestamps
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} finding - Finding object
|
|
18
|
+
* @returns {string} Stable identity key
|
|
19
|
+
*/
|
|
20
|
+
export function computeFindingIdentity(finding) {
|
|
21
|
+
const parts = [];
|
|
22
|
+
|
|
23
|
+
// Part 1: Finding type
|
|
24
|
+
parts.push(`type:${finding.type || 'unknown'}`);
|
|
25
|
+
|
|
26
|
+
// Part 2: Interaction signature
|
|
27
|
+
const interaction = finding.interaction || {};
|
|
28
|
+
if (interaction.type && interaction.selector) {
|
|
29
|
+
parts.push(`interaction:${interaction.type}:${normalizeSelector(interaction.selector)}`);
|
|
30
|
+
}
|
|
31
|
+
if (interaction.label) {
|
|
32
|
+
parts.push(`label:${interaction.label}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Part 3: Promise signature
|
|
36
|
+
const expectation = finding.expectation || {};
|
|
37
|
+
const promise = finding.promise || {};
|
|
38
|
+
|
|
39
|
+
if (expectation.type) {
|
|
40
|
+
parts.push(`promiseType:${expectation.type}`);
|
|
41
|
+
}
|
|
42
|
+
if (expectation.targetPath) {
|
|
43
|
+
parts.push(`targetPath:${normalizePath(expectation.targetPath)}`);
|
|
44
|
+
}
|
|
45
|
+
if (expectation.urlPath) {
|
|
46
|
+
parts.push(`urlPath:${normalizePath(expectation.urlPath)}`);
|
|
47
|
+
}
|
|
48
|
+
if (promise.type) {
|
|
49
|
+
parts.push(`promise:${promise.type}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Part 4: Route/network target
|
|
53
|
+
if (finding.route) {
|
|
54
|
+
const route = finding.route;
|
|
55
|
+
if (route.path) {
|
|
56
|
+
parts.push(`route:${normalizePath(route.path)}`);
|
|
57
|
+
}
|
|
58
|
+
if (route.originalPattern) {
|
|
59
|
+
parts.push(`routePattern:${normalizePath(route.originalPattern)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (finding.evidence?.networkRequest?.url) {
|
|
64
|
+
parts.push(`networkUrl:${normalizeUrl(finding.evidence.networkRequest.url)}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Part 5: Evidence trigger source signature
|
|
68
|
+
const source = finding.source || expectation.source || {};
|
|
69
|
+
if (source.file) {
|
|
70
|
+
parts.push(`sourceFile:${normalizePath(source.file)}`);
|
|
71
|
+
}
|
|
72
|
+
if (source.line) {
|
|
73
|
+
parts.push(`sourceLine:${source.line}`);
|
|
74
|
+
}
|
|
75
|
+
if (source.astSource) {
|
|
76
|
+
// Hash AST source for stability
|
|
77
|
+
const astHash = hashString(source.astSource);
|
|
78
|
+
parts.push(`astHash:${astHash}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Part 6: Evidence trigger from evidencePackage
|
|
82
|
+
if (finding.evidencePackage?.trigger?.astSource) {
|
|
83
|
+
const triggerHash = hashString(finding.evidencePackage.trigger.astSource);
|
|
84
|
+
parts.push(`triggerHash:${triggerHash}`);
|
|
85
|
+
}
|
|
86
|
+
if (finding.evidencePackage?.trigger?.source?.file) {
|
|
87
|
+
parts.push(`triggerFile:${normalizePath(finding.evidencePackage.trigger.source.file)}`);
|
|
88
|
+
}
|
|
89
|
+
if (finding.evidencePackage?.trigger?.source?.line) {
|
|
90
|
+
parts.push(`triggerLine:${finding.evidencePackage.trigger.source.line}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Part 7: Context chain (if available)
|
|
94
|
+
if (finding.contextChain && Array.isArray(finding.contextChain)) {
|
|
95
|
+
const contextSig = finding.contextChain.map(c => `${c.type}:${c.name || ''}`).join('>');
|
|
96
|
+
parts.push(`context:${contextSig}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Combine all parts into stable identity
|
|
100
|
+
const identity = parts.join('|');
|
|
101
|
+
|
|
102
|
+
// Return hash for compactness and stability
|
|
103
|
+
return hashString(identity);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Normalize selector (remove volatile parts)
|
|
108
|
+
*/
|
|
109
|
+
function normalizeSelector(selector) {
|
|
110
|
+
if (!selector || typeof selector !== 'string') return '';
|
|
111
|
+
// Remove any dynamic IDs or classes that might change
|
|
112
|
+
return selector.replace(/\[data-[^\]]+\]/g, '').replace(/\.\w+-\d+/g, '');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Normalize path (remove absolute paths, normalize separators)
|
|
117
|
+
*/
|
|
118
|
+
function normalizePath(path) {
|
|
119
|
+
if (!path || typeof path !== 'string') return '';
|
|
120
|
+
// Normalize separators
|
|
121
|
+
let normalized = path.replace(/\\/g, '/');
|
|
122
|
+
// Remove absolute path prefixes (keep relative structure)
|
|
123
|
+
normalized = normalized.replace(/^[A-Z]:\/[^\/]+/, '');
|
|
124
|
+
normalized = normalized.replace(/^\/[^\/]+/, '');
|
|
125
|
+
return normalized;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Normalize URL (remove query params, hash, normalize domain)
|
|
130
|
+
*/
|
|
131
|
+
function normalizeUrl(url) {
|
|
132
|
+
if (!url || typeof url !== 'string') return '';
|
|
133
|
+
try {
|
|
134
|
+
const urlObj = new URL(url);
|
|
135
|
+
// Keep only pathname for comparison
|
|
136
|
+
return urlObj.pathname;
|
|
137
|
+
} catch {
|
|
138
|
+
return url;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Hash string for stable identity
|
|
144
|
+
*/
|
|
145
|
+
function hashString(str) {
|
|
146
|
+
return createHash('sha256').update(str).digest('hex').substring(0, 16);
|
|
147
|
+
}
|
|
148
|
+
|