@veraxhq/verax 0.2.1 → 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 +3 -3
- package/src/cli/commands/baseline.js +104 -0
- package/src/cli/commands/default.js +79 -25
- 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 +246 -35
- package/src/cli/commands/security-check.js +211 -0
- package/src/cli/commands/truth.js +114 -0
- package/src/cli/entry.js +304 -67
- 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/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/expectation-extractor.js +369 -73
- package/src/cli/util/findings-writer.js +126 -16
- package/src/cli/util/learn-writer.js +3 -1
- package/src/cli/util/observe-writer.js +3 -1
- package/src/cli/util/paths.js +3 -12
- package/src/cli/util/project-discovery.js +3 -0
- package/src/cli/util/project-writer.js +3 -1
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +1 -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 +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/verax/cli/finding-explainer.js +56 -3
- 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/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 +30 -3
- 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/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/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/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/truth/truth.certificate.js +250 -0
- package/src/verax/core/ui-feedback-intelligence.js +515 -0
- package/src/verax/detect/confidence-engine.js +628 -40
- package/src/verax/detect/confidence-helper.js +33 -0
- package/src/verax/detect/detection-engine.js +18 -1
- package/src/verax/detect/dynamic-route-findings.js +335 -0
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +3 -1
- package/src/verax/detect/findings-writer.js +141 -5
- package/src/verax/detect/index.js +229 -5
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/route-findings.js +218 -0
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/verdict-engine.js +57 -3
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/index.js +413 -45
- package/src/verax/learn/action-contract-extractor.js +682 -64
- package/src/verax/learn/route-validator.js +4 -1
- package/src/verax/observe/index.js +88 -843
- package/src/verax/observe/interaction-runner.js +25 -8
- 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/ui-feedback-detector.js +742 -0
- package/src/verax/observe/ui-signal-sensor.js +148 -2
- package/src/verax/scan-summary-writer.js +42 -8
- package/src/verax/shared/artifact-manager.js +8 -5
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/view-switch-rules.js +208 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXPECTATION CHAIN DETECTION
|
|
3
|
+
*
|
|
4
|
+
* Detects silent failures where a sequence of promised user expectations breaks mid-journey.
|
|
5
|
+
*
|
|
6
|
+
* Requirements:
|
|
7
|
+
* 1) Build expectation chains from source-derived expectations:
|
|
8
|
+
* - navigation → content → next action
|
|
9
|
+
* 2) Track which chain steps were fulfilled vs broken
|
|
10
|
+
* 3) Emit finding type: "expectation-chain-break"
|
|
11
|
+
* 4) Include metadata:
|
|
12
|
+
* - chainLength
|
|
13
|
+
* - fulfilledSteps
|
|
14
|
+
* - brokenStepIndex
|
|
15
|
+
* 5) Confidence rules:
|
|
16
|
+
* - Longer chains + late break = higher confidence
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Expectation Chain Detector
|
|
21
|
+
*
|
|
22
|
+
* Analyzes sequences of proven expectations to detect when they break
|
|
23
|
+
* in a predictable chain pattern.
|
|
24
|
+
*/
|
|
25
|
+
export class ExpectationChainDetector {
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.minChainLength = options.minChainLength || 2; // Minimum 2 for a "chain"
|
|
28
|
+
this.maxChainLength = options.maxChainLength || 10; // Limit to 10 for analysis
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detect expectation chains that break
|
|
33
|
+
* @param {Array} expectations - Proven expectations from source analysis
|
|
34
|
+
* @param {Array} traces - Interaction traces from observe phase
|
|
35
|
+
* @returns {Array} Expectation chain break findings
|
|
36
|
+
*/
|
|
37
|
+
detectChainBreaks(expectations = [], traces = []) {
|
|
38
|
+
if (!Array.isArray(expectations) || !Array.isArray(traces)) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const findings = [];
|
|
43
|
+
|
|
44
|
+
// Build expectation chains from proven expectations
|
|
45
|
+
const chains = this._buildChains(expectations);
|
|
46
|
+
|
|
47
|
+
// Validate each chain against traces
|
|
48
|
+
for (const chain of chains) {
|
|
49
|
+
const chainBreak = this._validateChain(chain, traces);
|
|
50
|
+
if (chainBreak) {
|
|
51
|
+
findings.push(chainBreak);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return findings;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build expectation chains from a list of expectations
|
|
60
|
+
* Chain pattern: navigation → content → interaction
|
|
61
|
+
* @private
|
|
62
|
+
*/
|
|
63
|
+
_buildChains(expectations) {
|
|
64
|
+
const chains = [];
|
|
65
|
+
|
|
66
|
+
// Filter to PROVEN expectations only
|
|
67
|
+
const provenExpectations = expectations.filter(exp => this._isProven(exp));
|
|
68
|
+
|
|
69
|
+
if (provenExpectations.length < this.minChainLength) {
|
|
70
|
+
return chains;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Group expectations by sequence/order
|
|
74
|
+
const ordered = this._orderExpectationsBySequence(provenExpectations);
|
|
75
|
+
|
|
76
|
+
// Build chains from consecutive expectations
|
|
77
|
+
let currentChain = [];
|
|
78
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
79
|
+
const exp = ordered[i];
|
|
80
|
+
|
|
81
|
+
// Add to current chain
|
|
82
|
+
currentChain.push({
|
|
83
|
+
index: i,
|
|
84
|
+
expectation: exp,
|
|
85
|
+
type: this._normalizeExpectationType(exp),
|
|
86
|
+
sourceRef: exp.sourceRef || exp.filename,
|
|
87
|
+
lineNumber: exp.lineNumber
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Check if chain should end
|
|
91
|
+
const isLastExp = i === ordered.length - 1;
|
|
92
|
+
const nextExp = !isLastExp ? ordered[i + 1] : null;
|
|
93
|
+
|
|
94
|
+
// Chain ends if:
|
|
95
|
+
// 1. Gap in expectation sequence (next type is unrelated)
|
|
96
|
+
// 2. Chain is at max length
|
|
97
|
+
// 3. We're at the end
|
|
98
|
+
const shouldBreakChain =
|
|
99
|
+
currentChain.length >= this.maxChainLength ||
|
|
100
|
+
isLastExp ||
|
|
101
|
+
(nextExp && !this._isConsecutiveExpectation(exp, nextExp));
|
|
102
|
+
|
|
103
|
+
if (shouldBreakChain && currentChain.length >= this.minChainLength) {
|
|
104
|
+
chains.push(currentChain);
|
|
105
|
+
currentChain = [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return chains;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Validate a chain against actual traces
|
|
114
|
+
* Returns finding if chain breaks (some steps work, later steps fail)
|
|
115
|
+
* @private
|
|
116
|
+
*/
|
|
117
|
+
_validateChain(chain, traces) {
|
|
118
|
+
if (chain.length < this.minChainLength) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let fulfilledSteps = 0;
|
|
123
|
+
let brokenStepIndex = -1;
|
|
124
|
+
|
|
125
|
+
// Check each step in the chain
|
|
126
|
+
for (let i = 0; i < chain.length; i++) {
|
|
127
|
+
const step = chain[i];
|
|
128
|
+
const stepType = step.type;
|
|
129
|
+
|
|
130
|
+
// Find matching trace for this step
|
|
131
|
+
const matchingTrace = traces.find(trace =>
|
|
132
|
+
this._traceMatchesExpectation(trace, step.expectation)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (!matchingTrace) {
|
|
136
|
+
// Step not found in traces - chain breaks here
|
|
137
|
+
brokenStepIndex = i;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check if step expectation was fulfilled
|
|
142
|
+
const isFulfilled = this._isExpectationFulfilled(
|
|
143
|
+
step.expectation,
|
|
144
|
+
matchingTrace
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (isFulfilled) {
|
|
148
|
+
fulfilledSteps++;
|
|
149
|
+
} else {
|
|
150
|
+
// Expectation exists but not fulfilled
|
|
151
|
+
brokenStepIndex = i;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Only emit finding if:
|
|
157
|
+
// 1. Chain had at least 1 fulfilled step before breaking
|
|
158
|
+
// 2. Breaking happened (not all steps fulfilled)
|
|
159
|
+
if (brokenStepIndex > 0 && fulfilledSteps > 0) {
|
|
160
|
+
return {
|
|
161
|
+
id: `expectation-chain-break-${Date.now()}-${Math.random()
|
|
162
|
+
.toString(36)
|
|
163
|
+
.slice(2, 9)}`,
|
|
164
|
+
type: 'expectation-chain-break',
|
|
165
|
+
severity: this._computeSeverity(chain.length, fulfilledSteps, brokenStepIndex),
|
|
166
|
+
evidence: {
|
|
167
|
+
chainLength: chain.length,
|
|
168
|
+
fulfilledSteps,
|
|
169
|
+
brokenStepIndex,
|
|
170
|
+
chain: chain.map(step => ({
|
|
171
|
+
type: step.type,
|
|
172
|
+
sourceRef: step.sourceRef,
|
|
173
|
+
lineNumber: step.lineNumber
|
|
174
|
+
})),
|
|
175
|
+
brokenStep: chain[brokenStepIndex]
|
|
176
|
+
? {
|
|
177
|
+
type: chain[brokenStepIndex].type,
|
|
178
|
+
sourceRef: chain[brokenStepIndex].sourceRef,
|
|
179
|
+
lineNumber: chain[brokenStepIndex].lineNumber
|
|
180
|
+
}
|
|
181
|
+
: null,
|
|
182
|
+
description: this._describeChainBreak(
|
|
183
|
+
chain,
|
|
184
|
+
fulfilledSteps,
|
|
185
|
+
brokenStepIndex
|
|
186
|
+
)
|
|
187
|
+
},
|
|
188
|
+
expectation: {
|
|
189
|
+
proof: 'PROVEN_EXPECTATION',
|
|
190
|
+
explicit: true,
|
|
191
|
+
evidence: {
|
|
192
|
+
source: 'expectation-chain-analysis',
|
|
193
|
+
chainAnalysis: true
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
actualOutcome: 'expectation_chain_breaks_at_step',
|
|
197
|
+
impact: 'HIGH'
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if trace matches expectation
|
|
206
|
+
* @private
|
|
207
|
+
*/
|
|
208
|
+
_traceMatchesExpectation(trace, expectation) {
|
|
209
|
+
if (!trace || !expectation) return false;
|
|
210
|
+
|
|
211
|
+
// Match by route if available
|
|
212
|
+
if (expectation.route && trace.after?.url) {
|
|
213
|
+
const routePath = expectation.route.replace(/\/$/, '') || '/';
|
|
214
|
+
const urlPath = this._getUrlPath(trace.after.url);
|
|
215
|
+
return routePath === urlPath;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Match by type
|
|
219
|
+
const expType = this._normalizeExpectationType(expectation);
|
|
220
|
+
if (
|
|
221
|
+
expType === 'navigation' &&
|
|
222
|
+
trace.interaction?.type === 'link'
|
|
223
|
+
) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (
|
|
228
|
+
expType === 'form_submission' &&
|
|
229
|
+
trace.interaction?.type === 'form'
|
|
230
|
+
) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (expType === 'interaction' && trace.interaction) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check if expectation was fulfilled in the trace
|
|
243
|
+
* @private
|
|
244
|
+
*/
|
|
245
|
+
_isExpectationFulfilled(expectation, trace) {
|
|
246
|
+
if (!trace) return false;
|
|
247
|
+
|
|
248
|
+
const expType = this._normalizeExpectationType(expectation);
|
|
249
|
+
|
|
250
|
+
// Navigation expectation: URL must change
|
|
251
|
+
if (expType === 'navigation') {
|
|
252
|
+
const urlChanged = trace.sensors?.navigation?.urlChanged === true;
|
|
253
|
+
const afterUrl = trace.after?.url;
|
|
254
|
+
return urlChanged && afterUrl !== trace.before?.url;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Content expectation: DOM must change
|
|
258
|
+
if (expType === 'content') {
|
|
259
|
+
const domChanged = trace.sensors?.uiSignals?.diff?.domChanged === true;
|
|
260
|
+
const newContent =
|
|
261
|
+
trace.sensors?.uiFeedback?.signals?.domChange?.happened === true;
|
|
262
|
+
return domChanged || newContent;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Interaction expectation: must have been performed
|
|
266
|
+
if (expType === 'interaction') {
|
|
267
|
+
const performed = trace.interaction?.performed === true;
|
|
268
|
+
return performed;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Form submission: must have network request
|
|
272
|
+
if (expType === 'form_submission') {
|
|
273
|
+
const hasNetworkRequest = (trace.sensors?.network?.totalRequests || 0) > 0;
|
|
274
|
+
return hasNetworkRequest;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Default: consider fulfilled if trace exists
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Check if expectation is PROVEN
|
|
283
|
+
* @private
|
|
284
|
+
*/
|
|
285
|
+
_isProven(expectation) {
|
|
286
|
+
if (!expectation) return false;
|
|
287
|
+
|
|
288
|
+
// Explicit from source code analysis
|
|
289
|
+
if (expectation.explicit === true) return true;
|
|
290
|
+
|
|
291
|
+
// Has proof marker
|
|
292
|
+
if (expectation.proof === 'PROVEN_EXPECTATION') return true;
|
|
293
|
+
|
|
294
|
+
// Has sourceRef (AST-derived)
|
|
295
|
+
if (expectation.sourceRef) return true;
|
|
296
|
+
|
|
297
|
+
// Has source evidence
|
|
298
|
+
if (expectation.evidence?.source) return true;
|
|
299
|
+
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Normalize expectation type
|
|
305
|
+
* @private
|
|
306
|
+
*/
|
|
307
|
+
_normalizeExpectationType(expectation) {
|
|
308
|
+
if (!expectation) return 'unknown';
|
|
309
|
+
|
|
310
|
+
const type = expectation.type || expectation.expectationType || '';
|
|
311
|
+
|
|
312
|
+
if (type.includes('navigation') || type === 'spa_navigation') {
|
|
313
|
+
return 'navigation';
|
|
314
|
+
}
|
|
315
|
+
if (type.includes('content') || type.includes('dom')) {
|
|
316
|
+
return 'content';
|
|
317
|
+
}
|
|
318
|
+
if (type.includes('form') || type === 'form_submission') {
|
|
319
|
+
return 'form_submission';
|
|
320
|
+
}
|
|
321
|
+
if (type.includes('interaction') || type.includes('action')) {
|
|
322
|
+
return 'interaction';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return type || 'unknown';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Order expectations by sequence (chain order)
|
|
330
|
+
* @private
|
|
331
|
+
*/
|
|
332
|
+
_orderExpectationsBySequence(expectations) {
|
|
333
|
+
// If expectations have explicit order/lineNumber, use that
|
|
334
|
+
const withOrder = expectations.filter(exp => exp.lineNumber !== undefined);
|
|
335
|
+
if (withOrder.length > 0) {
|
|
336
|
+
return withOrder.sort((a, b) => a.lineNumber - b.lineNumber);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Otherwise return in given order
|
|
340
|
+
return expectations;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Check if two expectations are consecutive in chain
|
|
345
|
+
* @private
|
|
346
|
+
*/
|
|
347
|
+
_isConsecutiveExpectation(currentExp, nextExp) {
|
|
348
|
+
if (!currentExp || !nextExp) return false;
|
|
349
|
+
|
|
350
|
+
const currentType = this._normalizeExpectationType(currentExp);
|
|
351
|
+
const nextType = this._normalizeExpectationType(nextExp);
|
|
352
|
+
|
|
353
|
+
// Valid chain progressions:
|
|
354
|
+
// navigation → content, interaction, form_submission
|
|
355
|
+
// content → interaction, form_submission
|
|
356
|
+
// form_submission → content, navigation
|
|
357
|
+
// interaction → content, form_submission, navigation
|
|
358
|
+
|
|
359
|
+
const validProgressions = {
|
|
360
|
+
navigation: ['content', 'interaction', 'form_submission'],
|
|
361
|
+
content: ['interaction', 'form_submission', 'navigation'],
|
|
362
|
+
form_submission: ['content', 'navigation', 'interaction'],
|
|
363
|
+
interaction: ['content', 'form_submission', 'navigation']
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const validNext = validProgressions[currentType] || [];
|
|
367
|
+
return validNext.includes(nextType);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Compute severity based on chain depth and break position
|
|
372
|
+
* @private
|
|
373
|
+
*/
|
|
374
|
+
_computeSeverity(chainLength, fulfilledSteps, brokenStepIndex) {
|
|
375
|
+
// Late breaks (deeper in chain) are higher severity
|
|
376
|
+
const breakDepth = brokenStepIndex / chainLength;
|
|
377
|
+
|
|
378
|
+
if (breakDepth >= 0.7) {
|
|
379
|
+
return 'CRITICAL'; // Broke in final 30%
|
|
380
|
+
}
|
|
381
|
+
if (breakDepth >= 0.5) {
|
|
382
|
+
return 'HIGH'; // Broke in second half
|
|
383
|
+
}
|
|
384
|
+
if (breakDepth >= 0.3) {
|
|
385
|
+
return 'MEDIUM'; // Broke in second third
|
|
386
|
+
}
|
|
387
|
+
return 'LOW'; // Early break
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Generate description of chain break
|
|
392
|
+
* @private
|
|
393
|
+
*/
|
|
394
|
+
_describeChainBreak(chain, fulfilledSteps, brokenStepIndex) {
|
|
395
|
+
const chainTypes = chain.map(s => s.type).join(' → ');
|
|
396
|
+
const brokenType = chain[brokenStepIndex]?.type || 'unknown';
|
|
397
|
+
|
|
398
|
+
return `Expectation chain broke at step ${brokenStepIndex + 1}/${
|
|
399
|
+
chain.length
|
|
400
|
+
}: ${chainTypes}. Successfully completed ${fulfilledSteps} step(s) before ${brokenType} failed.`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Extract URL path from full URL
|
|
405
|
+
* @private
|
|
406
|
+
*/
|
|
407
|
+
_getUrlPath(url) {
|
|
408
|
+
if (!url) return '';
|
|
409
|
+
try {
|
|
410
|
+
return new URL(url).pathname;
|
|
411
|
+
} catch {
|
|
412
|
+
return url;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export default ExpectationChainDetector;
|
|
@@ -256,9 +256,11 @@ export function getExpectation(manifest, interaction, beforeUrl, attemptMeta = {
|
|
|
256
256
|
for (const contract of manifest.actionContracts) {
|
|
257
257
|
if (contract.source === sourceRef) {
|
|
258
258
|
const expectationType = contract.kind === 'NETWORK_ACTION' ? 'network_action' : 'action';
|
|
259
|
+
// Dynamic URLs never produce PROVEN_EXPECTATION (truth boundary)
|
|
260
|
+
const proof = contract.isDynamic ? 'UNPROVEN_EXPECTATION' : 'PROVEN_EXPECTATION';
|
|
259
261
|
return {
|
|
260
262
|
hasExpectation: true,
|
|
261
|
-
proof
|
|
263
|
+
proof,
|
|
262
264
|
expectationType,
|
|
263
265
|
method: contract.method,
|
|
264
266
|
urlPath: contract.urlPath,
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { resolve } from 'path';
|
|
2
2
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
3
3
|
import { CANONICAL_OUTCOMES } from '../core/canonical-outcomes.js';
|
|
4
|
+
import { enforceContractsOnFindings, FINDING_STATUS } from '../core/contracts/index.js';
|
|
5
|
+
import { ARTIFACT_REGISTRY, getArtifactVersions } from '../core/artifacts/registry.js';
|
|
6
|
+
import { buildAndEnforceEvidencePackage, EvidenceBuildError, validateEvidencePackageStrict } from '../core/evidence-builder.js';
|
|
7
|
+
import { writeEvidenceIntentLedger } from '../core/evidence/evidence-intent-ledger.js';
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Write findings to canonical artifact root.
|
|
7
11
|
* Writes to .verax/runs/<runId>/findings.json.
|
|
8
12
|
*
|
|
13
|
+
* PHASE 0 ENFORCEMENT: Applies contracts enforcement
|
|
14
|
+
* - Downgrades findings without evidence from CONFIRMED to SUSPECTED
|
|
15
|
+
* - Drops findings that violate critical contracts
|
|
16
|
+
*
|
|
9
17
|
* PHASE 2: Includes outcome classification summary.
|
|
10
18
|
* PHASE 3: Includes promise type summary.
|
|
11
19
|
*
|
|
@@ -14,15 +22,118 @@ import { CANONICAL_OUTCOMES } from '../core/canonical-outcomes.js';
|
|
|
14
22
|
* @param {Array} findings
|
|
15
23
|
* @param {Array} coverageGaps
|
|
16
24
|
* @param {string} runDirOpt - Required absolute run directory path
|
|
25
|
+
* @returns {Object} findings report with enforcement metadata
|
|
17
26
|
*/
|
|
18
27
|
export function writeFindings(projectDir, url, findings, coverageGaps = [], runDirOpt) {
|
|
19
28
|
if (!runDirOpt) {
|
|
20
29
|
throw new Error('runDirOpt is required');
|
|
21
30
|
}
|
|
22
31
|
mkdirSync(runDirOpt, { recursive: true });
|
|
23
|
-
const findingsPath = resolve(runDirOpt,
|
|
32
|
+
const findingsPath = resolve(runDirOpt, ARTIFACT_REGISTRY.findings.filename);
|
|
24
33
|
|
|
25
|
-
//
|
|
34
|
+
// ARCHITECTURAL HARDENING: Evidence capture failures downgrade findings, never drop them
|
|
35
|
+
// A finding must NEVER disappear. If evidence capture fails, downgrade explicitly.
|
|
36
|
+
const findingsWithEvidence = [];
|
|
37
|
+
const evidenceBuildFailures = [];
|
|
38
|
+
|
|
39
|
+
for (const finding of (findings || [])) {
|
|
40
|
+
// If evidencePackage already exists, validate it strictly for CONFIRMED findings
|
|
41
|
+
if (finding.evidencePackage) {
|
|
42
|
+
const severity = finding.severity || finding.status || 'SUSPECTED';
|
|
43
|
+
if (severity === 'CONFIRMED') {
|
|
44
|
+
try {
|
|
45
|
+
validateEvidencePackageStrict(finding.evidencePackage, severity);
|
|
46
|
+
findingsWithEvidence.push(finding);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
// CONFIRMED finding with incomplete evidencePackage → DOWNGRADE to SUSPECTED
|
|
49
|
+
const downgradedFinding = {
|
|
50
|
+
...finding,
|
|
51
|
+
severity: 'SUSPECTED',
|
|
52
|
+
status: 'SUSPECTED',
|
|
53
|
+
evidenceCompleteness: {
|
|
54
|
+
downgraded: true,
|
|
55
|
+
reason: `Evidence package incomplete: ${error.message}`,
|
|
56
|
+
originalSeverity: 'CONFIRMED'
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
findingsWithEvidence.push(downgradedFinding);
|
|
60
|
+
evidenceBuildFailures.push({
|
|
61
|
+
finding: { type: finding.type || 'unknown', id: finding.findingId || finding.id },
|
|
62
|
+
reason: `EVIDENCE_VALIDATION_FAILED: ${error.message}`,
|
|
63
|
+
errorCode: error.code || 'EVIDENCE_VALIDATION_FAILED',
|
|
64
|
+
action: 'DOWNGRADED'
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
findingsWithEvidence.push(finding);
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Build evidence package from finding data
|
|
74
|
+
// If building fails → DOWNGRADE finding (never drop)
|
|
75
|
+
try {
|
|
76
|
+
const findingWithEvidence = buildAndEnforceEvidencePackage(finding, {
|
|
77
|
+
expectation: finding.expectation || null,
|
|
78
|
+
trace: {
|
|
79
|
+
interaction: finding.interaction || {},
|
|
80
|
+
before: finding.evidence?.before ? { screenshot: finding.evidence.before, url: finding.evidence.beforeUrl } : null,
|
|
81
|
+
after: finding.evidence?.after ? { screenshot: finding.evidence.after, url: finding.evidence.afterUrl } : null,
|
|
82
|
+
sensors: finding.evidence?.sensors || {},
|
|
83
|
+
dom: finding.evidence?.dom || {},
|
|
84
|
+
},
|
|
85
|
+
evidence: finding.evidence || {},
|
|
86
|
+
confidence: finding.confidenceLevel ? {
|
|
87
|
+
level: finding.confidenceLevel,
|
|
88
|
+
score: finding.confidence,
|
|
89
|
+
reasons: finding.confidenceReasons || [],
|
|
90
|
+
} : null,
|
|
91
|
+
});
|
|
92
|
+
findingsWithEvidence.push(findingWithEvidence);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// Evidence building failure → DOWNGRADE finding (never drop)
|
|
95
|
+
const originalSeverity = finding.severity || finding.status || 'SUSPECTED';
|
|
96
|
+
const downgradedFinding = {
|
|
97
|
+
...finding,
|
|
98
|
+
severity: 'SUSPECTED',
|
|
99
|
+
status: 'SUSPECTED',
|
|
100
|
+
evidenceCompleteness: {
|
|
101
|
+
downgraded: true,
|
|
102
|
+
reason: `Evidence capture failed: ${error.message}`,
|
|
103
|
+
originalSeverity: originalSeverity,
|
|
104
|
+
errorCode: error.code || 'EVIDENCE_BUILD_FAILED',
|
|
105
|
+
missingFields: error.missingFields || []
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
findingsWithEvidence.push(downgradedFinding);
|
|
109
|
+
|
|
110
|
+
if (error instanceof EvidenceBuildError) {
|
|
111
|
+
evidenceBuildFailures.push({
|
|
112
|
+
finding: { type: finding.type || 'unknown', id: finding.findingId || finding.id },
|
|
113
|
+
reason: `EVIDENCE_BUILD_FAILED: ${error.message}`,
|
|
114
|
+
errorCode: error.code || 'EVIDENCE_BUILD_FAILED',
|
|
115
|
+
missingFields: error.missingFields || [],
|
|
116
|
+
action: 'DOWNGRADED'
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
evidenceBuildFailures.push({
|
|
120
|
+
finding: { type: finding.type || 'unknown', id: finding.findingId || finding.id },
|
|
121
|
+
reason: `EVIDENCE_BUILD_FAILED: Unexpected error: ${error.message}`,
|
|
122
|
+
errorCode: 'EVIDENCE_BUILD_FAILED',
|
|
123
|
+
action: 'DOWNGRADED'
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// PHASE 0: Enforce contracts on all findings (Evidence Law)
|
|
130
|
+
const { valid: enforcedFindings, dropped, downgrades } = enforceContractsOnFindings(findingsWithEvidence);
|
|
131
|
+
|
|
132
|
+
// ARCHITECTURAL HARDENING: Track evidence build failures separately (they result in downgrades, not drops)
|
|
133
|
+
// Only contract violations result in drops
|
|
134
|
+
const allDropped = dropped;
|
|
135
|
+
|
|
136
|
+
// PHASE 2: Compute outcome summary (using enforced findings)
|
|
26
137
|
const outcomeSummary = {};
|
|
27
138
|
Object.values(CANONICAL_OUTCOMES).forEach(outcome => {
|
|
28
139
|
outcomeSummary[outcome] = 0;
|
|
@@ -31,7 +142,7 @@ export function writeFindings(projectDir, url, findings, coverageGaps = [], runD
|
|
|
31
142
|
// PHASE 3: Compute promise summary
|
|
32
143
|
const promiseSummary = {};
|
|
33
144
|
|
|
34
|
-
for (const finding of
|
|
145
|
+
for (const finding of enforcedFindings) {
|
|
35
146
|
const outcome = finding.outcome || CANONICAL_OUTCOMES.SILENT_FAILURE;
|
|
36
147
|
outcomeSummary[outcome] = (outcomeSummary[outcome] || 0) + 1;
|
|
37
148
|
|
|
@@ -41,20 +152,45 @@ export function writeFindings(projectDir, url, findings, coverageGaps = [], runD
|
|
|
41
152
|
|
|
42
153
|
const findingsReport = {
|
|
43
154
|
version: 1,
|
|
155
|
+
contractVersion: 1, // Track schema changes from contracts enforcement
|
|
156
|
+
artifactVersions: getArtifactVersions(),
|
|
44
157
|
detectedAt: new Date().toISOString(),
|
|
45
158
|
url: url,
|
|
46
159
|
outcomeSummary: outcomeSummary, // PHASE 2
|
|
47
160
|
promiseSummary: promiseSummary, // PHASE 3
|
|
48
|
-
findings:
|
|
161
|
+
findings: enforcedFindings,
|
|
49
162
|
coverageGaps: coverageGaps,
|
|
163
|
+
// PHASE 0: Enforcement metadata
|
|
164
|
+
// PHASE 21.1: Include evidence build failures in dropped count
|
|
165
|
+
enforcement: {
|
|
166
|
+
droppedCount: allDropped.length,
|
|
167
|
+
downgradedCount: downgrades.length,
|
|
168
|
+
downgrades: downgrades.map(d => ({
|
|
169
|
+
reason: d.reason,
|
|
170
|
+
originalStatus: d.original.status,
|
|
171
|
+
downgradeToStatus: d.downgraded.status
|
|
172
|
+
})),
|
|
173
|
+
evidenceBuildFailures: evidenceBuildFailures.map(f => ({
|
|
174
|
+
reason: f.reason,
|
|
175
|
+
errorCode: f.errorCode,
|
|
176
|
+
findingType: f.finding.type || 'unknown',
|
|
177
|
+
findingId: f.finding.id || null,
|
|
178
|
+
missingFields: f.missingFields || [],
|
|
179
|
+
action: f.action || 'DOWNGRADED'
|
|
180
|
+
}))
|
|
181
|
+
},
|
|
50
182
|
notes: []
|
|
51
183
|
};
|
|
52
184
|
|
|
53
185
|
writeFileSync(findingsPath, JSON.stringify(findingsReport, null, 2) + '\n');
|
|
54
186
|
|
|
187
|
+
// PHASE 22: Write evidence intent ledger
|
|
188
|
+
const evidenceIntentPath = writeEvidenceIntentLedger(runDirOpt, enforcedFindings, captureFailuresMap);
|
|
189
|
+
|
|
55
190
|
return {
|
|
56
191
|
...findingsReport,
|
|
57
|
-
findingsPath: findingsPath
|
|
192
|
+
findingsPath: findingsPath,
|
|
193
|
+
evidenceIntentPath: evidenceIntentPath
|
|
58
194
|
};
|
|
59
195
|
}
|
|
60
196
|
|