@veraxhq/verax 0.1.0 → 0.2.1
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 +123 -88
- package/bin/verax.js +11 -452
- package/package.json +24 -36
- package/src/cli/commands/default.js +681 -0
- package/src/cli/commands/doctor.js +197 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +586 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +297 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +110 -0
- package/src/cli/util/expectation-extractor.js +388 -0
- package/src/cli/util/findings-writer.js +32 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +412 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +30 -0
- package/src/cli/util/project-discovery.js +297 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +43 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +111 -0
- package/src/verax/cli/wizard.js +109 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +432 -0
- package/src/verax/core/incremental-store.js +245 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +523 -0
- package/src/verax/detect/comparison.js +7 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +127 -0
- package/src/verax/detect/expectation-model.js +241 -168
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +41 -12
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +200 -288
- package/src/verax/detect/interactive-findings.js +612 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +561 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +103 -15
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +642 -0
- package/src/verax/intel/vue-router-extractor.js +325 -0
- package/src/verax/learn/action-contract-extractor.js +338 -104
- package/src/verax/learn/ast-contract-extractor.js +148 -6
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +122 -58
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +28 -97
- package/src/verax/learn/route-validator.js +8 -7
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +119 -10
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +30 -6
- package/src/verax/observe/console-sensor.js +2 -18
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +513 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +660 -273
- package/src/verax/observe/index.js +910 -26
- package/src/verax/observe/interaction-discovery.js +378 -15
- package/src/verax/observe/interaction-runner.js +562 -197
- package/src/verax/observe/loading-sensor.js +145 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +38 -17
- package/src/verax/observe/state-sensor.js +393 -0
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +73 -21
- package/src/verax/observe/ui-signal-sensor.js +143 -17
- package/src/verax/scan-summary-writer.js +80 -15
- package/src/verax/shared/artifact-manager.js +111 -9
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +169 -0
- package/src/verax/shared/dynamic-route-utils.js +224 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +9 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +66 -0
- package/src/verax/validate/context-validator.js +244 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Invariants Enforcement
|
|
3
|
+
*
|
|
4
|
+
* Production lock: ensures VERAX philosophy cannot be violated.
|
|
5
|
+
* Any finding that violates invariants is dropped silently.
|
|
6
|
+
*
|
|
7
|
+
* Invariants:
|
|
8
|
+
* 1. Every finding MUST have evidence, confidence, and signals
|
|
9
|
+
* 2. Every finding MUST be derived from proven expectation OR observable sensor
|
|
10
|
+
* 3. No finding may exist with missing evidence or contradictory signals
|
|
11
|
+
* 4. Ambiguous findings (low confidence + conflicting signals) are dropped
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { isProvenExpectation } from '../shared/expectation-prover.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Production lock flag - when enabled, enforces all invariants strictly
|
|
18
|
+
*/
|
|
19
|
+
export const VERAX_PRODUCTION_LOCK = true;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Minimum confidence threshold for ambiguous findings
|
|
23
|
+
*/
|
|
24
|
+
const AMBIGUITY_CONFIDENCE_THRESHOLD = 60; // Score, not level
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Enforce all invariants on a finding
|
|
28
|
+
* @param {Object} finding - Finding object to validate
|
|
29
|
+
* @param {Object} trace - Associated trace (for sensor evidence)
|
|
30
|
+
* @param {Object} matchedExpectation - Matched expectation if any
|
|
31
|
+
* @returns {Object} { shouldDrop: boolean, reason: string }
|
|
32
|
+
*/
|
|
33
|
+
export function enforceInvariants(finding, trace = {}, matchedExpectation = null) {
|
|
34
|
+
if (!VERAX_PRODUCTION_LOCK) {
|
|
35
|
+
// Production lock disabled - skip checks (development mode only)
|
|
36
|
+
return { shouldDrop: false, reason: null };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// INVARIANT 1: Every finding MUST have evidence
|
|
40
|
+
if (!finding.evidence || typeof finding.evidence !== 'object') {
|
|
41
|
+
return {
|
|
42
|
+
shouldDrop: true,
|
|
43
|
+
reason: 'missing_evidence',
|
|
44
|
+
message: 'Finding lacks evidence object'
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// INVARIANT 2: Every finding MUST have confidence
|
|
49
|
+
if (!finding.confidence || typeof finding.confidence !== 'object') {
|
|
50
|
+
return {
|
|
51
|
+
shouldDrop: true,
|
|
52
|
+
reason: 'missing_confidence',
|
|
53
|
+
message: 'Finding lacks confidence object'
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// INVARIANT 3: Every finding MUST have signals (Phase 9)
|
|
58
|
+
if (!finding.signals || typeof finding.signals !== 'object') {
|
|
59
|
+
return {
|
|
60
|
+
shouldDrop: true,
|
|
61
|
+
reason: 'missing_signals',
|
|
62
|
+
message: 'Finding lacks signals object (impact, userRisk, ownership)'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// INVARIANT 4: Required signal fields must exist
|
|
67
|
+
const requiredSignals = ['impact', 'userRisk', 'ownership', 'grouping'];
|
|
68
|
+
for (const field of requiredSignals) {
|
|
69
|
+
if (!finding.signals[field]) {
|
|
70
|
+
return {
|
|
71
|
+
shouldDrop: true,
|
|
72
|
+
reason: `missing_signal_${field}`,
|
|
73
|
+
message: `Finding signals missing required field: ${field}`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// INVARIANT 5: Impact, userRisk, ownership must be valid enums
|
|
79
|
+
const validImpacts = ['LOW', 'MEDIUM', 'HIGH'];
|
|
80
|
+
if (!validImpacts.includes(finding.signals.impact)) {
|
|
81
|
+
return {
|
|
82
|
+
shouldDrop: true,
|
|
83
|
+
reason: 'invalid_impact',
|
|
84
|
+
message: `Finding has invalid impact: ${finding.signals.impact}`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const validUserRisks = ['BLOCKS', 'CONFUSES', 'DEGRADES'];
|
|
89
|
+
if (!validUserRisks.includes(finding.signals.userRisk)) {
|
|
90
|
+
return {
|
|
91
|
+
shouldDrop: true,
|
|
92
|
+
reason: 'invalid_userRisk',
|
|
93
|
+
message: `Finding has invalid userRisk: ${finding.signals.userRisk}`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const validOwnership = ['FRONTEND', 'BACKEND', 'INTEGRATION', 'ACCESSIBILITY', 'PERFORMANCE'];
|
|
98
|
+
if (!validOwnership.includes(finding.signals.ownership)) {
|
|
99
|
+
return {
|
|
100
|
+
shouldDrop: true,
|
|
101
|
+
reason: 'invalid_ownership',
|
|
102
|
+
message: `Finding has invalid ownership: ${finding.signals.ownership}`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// INVARIANT 6: Finding MUST be derived from proven expectation OR observable sensor
|
|
107
|
+
// Check for proven expectation using the canonical isProvenExpectation function
|
|
108
|
+
const hasProvenExpectation = matchedExpectation && isProvenExpectation(matchedExpectation);
|
|
109
|
+
|
|
110
|
+
// Check for observed expectation (runtime-derived, also valid as it comes from observable behavior)
|
|
111
|
+
const hasObservedExpectation = trace?.observedExpectation || finding.expectationId;
|
|
112
|
+
|
|
113
|
+
// Check for observable sensor evidence (at least one sensor must have activity)
|
|
114
|
+
const sensors = trace?.sensors || {};
|
|
115
|
+
const findingEvidence = finding.evidence || {};
|
|
116
|
+
const hasObservableSensor = (
|
|
117
|
+
(sensors.network?.totalRequests || 0) > 0 ||
|
|
118
|
+
(sensors.console?.errors?.length || 0) > 0 ||
|
|
119
|
+
sensors.uiSignals?.diff?.changed === true ||
|
|
120
|
+
sensors.focus ||
|
|
121
|
+
sensors.aria ||
|
|
122
|
+
sensors.timing ||
|
|
123
|
+
sensors.loading ||
|
|
124
|
+
sensors.state ||
|
|
125
|
+
findingEvidence.beforeUrl ||
|
|
126
|
+
findingEvidence.afterUrl ||
|
|
127
|
+
findingEvidence.beforeScreenshot ||
|
|
128
|
+
findingEvidence.afterScreenshot ||
|
|
129
|
+
findingEvidence.before ||
|
|
130
|
+
findingEvidence.after
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Finding must have at least one grounding: proven expectation, observed expectation, or sensor evidence
|
|
134
|
+
if (!hasProvenExpectation && !hasObservedExpectation && !hasObservableSensor) {
|
|
135
|
+
return {
|
|
136
|
+
shouldDrop: true,
|
|
137
|
+
reason: 'ungrounded_finding',
|
|
138
|
+
message: 'Finding not derived from proven expectation, observed expectation, or observable sensor'
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// INVARIANT 7: Ambiguity guard - low confidence + conflicting signals = drop
|
|
143
|
+
const confidenceScore = finding.confidence?.score || 0;
|
|
144
|
+
const confidenceLevel = finding.confidence?.level || 'UNKNOWN';
|
|
145
|
+
|
|
146
|
+
// Only apply ambiguity guard if confidence is below threshold AND level is LOW
|
|
147
|
+
// High/MEDIUM confidence findings are trusted even if score is slightly below threshold
|
|
148
|
+
if (confidenceScore < AMBIGUITY_CONFIDENCE_THRESHOLD && confidenceLevel === 'LOW') {
|
|
149
|
+
// Check for conflicting sensor signals
|
|
150
|
+
const hasConflictingSignals = detectConflictingSignals(finding, trace);
|
|
151
|
+
|
|
152
|
+
if (hasConflictingSignals) {
|
|
153
|
+
return {
|
|
154
|
+
shouldDrop: true,
|
|
155
|
+
reason: 'ambiguous_finding',
|
|
156
|
+
message: `Low confidence finding (score: ${confidenceScore}, level: ${confidenceLevel}) with conflicting sensor signals - dropped for safety`
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// INVARIANT 8: Contradictory signals check
|
|
162
|
+
const hasContradiction = detectContradictorySignals(finding);
|
|
163
|
+
if (hasContradiction) {
|
|
164
|
+
return {
|
|
165
|
+
shouldDrop: true,
|
|
166
|
+
reason: 'contradictory_signals',
|
|
167
|
+
message: 'Finding has contradictory signals (e.g., HIGH impact but LOW confidence with no evidence)'
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// All invariants passed
|
|
172
|
+
return {
|
|
173
|
+
shouldDrop: false,
|
|
174
|
+
reason: null
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Detect conflicting sensor signals (ambiguity indicator)
|
|
180
|
+
* Returns true if signals conflict (ambiguous finding)
|
|
181
|
+
*/
|
|
182
|
+
function detectConflictingSignals(finding, trace) {
|
|
183
|
+
const sensors = trace?.sensors || {};
|
|
184
|
+
const evidence = finding.evidence || {};
|
|
185
|
+
const confidenceScore = finding.confidence?.score || 0;
|
|
186
|
+
|
|
187
|
+
// Only check conflicts for low-confidence findings (high confidence findings are trusted)
|
|
188
|
+
if (confidenceScore >= AMBIGUITY_CONFIDENCE_THRESHOLD) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Conflict: Network success but no UI change (partial success)
|
|
193
|
+
// This is actually a valid finding type - not a conflict
|
|
194
|
+
if (finding.type === 'partial_success_silent_failure') {
|
|
195
|
+
return false; // Explicitly allowed finding type
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Conflict: High impact but insufficient evidence for low confidence
|
|
199
|
+
if (finding.signals?.impact === 'HIGH') {
|
|
200
|
+
const hasSensorActivity = (
|
|
201
|
+
(sensors.network?.totalRequests || 0) > 0 ||
|
|
202
|
+
(sensors.console?.errors?.length || 0) > 0 ||
|
|
203
|
+
sensors.uiSignals?.diff?.changed === true ||
|
|
204
|
+
sensors.focus ||
|
|
205
|
+
sensors.aria ||
|
|
206
|
+
sensors.timing ||
|
|
207
|
+
sensors.loading
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// For HIGH impact + LOW confidence, need strong evidence (not just beforeUrl)
|
|
211
|
+
const hasStrongEvidence = (
|
|
212
|
+
evidence.afterUrl ||
|
|
213
|
+
evidence.beforeScreenshot ||
|
|
214
|
+
evidence.afterScreenshot ||
|
|
215
|
+
(evidence.networkRequests || 0) > 0 ||
|
|
216
|
+
evidence.urlChanged === false || // Explicitly checked
|
|
217
|
+
evidence.domChanged === false ||
|
|
218
|
+
evidence.uiChanged === false
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// If only beforeUrl exists without sensor activity or strong evidence, it's ambiguous for HIGH impact
|
|
222
|
+
if (!hasSensorActivity && !hasStrongEvidence && evidence.beforeUrl) {
|
|
223
|
+
// HIGH impact with only beforeUrl (weak evidence) and no sensor activity is ambiguous
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// No evidence at all
|
|
228
|
+
if (!hasSensorActivity && !hasStrongEvidence && !evidence.beforeUrl) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Conflict: BLOCKS userRisk but no blocking evidence
|
|
234
|
+
if (finding.signals?.userRisk === 'BLOCKS') {
|
|
235
|
+
const hasBlockingEvidence = (
|
|
236
|
+
finding.type?.includes('navigation') ||
|
|
237
|
+
finding.type?.includes('auth') ||
|
|
238
|
+
finding.type?.includes('loading_stuck') ||
|
|
239
|
+
finding.type?.includes('freeze_like') ||
|
|
240
|
+
finding.type === 'observed_break' ||
|
|
241
|
+
(sensors.network?.totalRequests || 0) > 0 ||
|
|
242
|
+
evidence.urlChanged === false || // Explicitly checked and failed
|
|
243
|
+
(evidence.networkRequests || 0) > 0 ||
|
|
244
|
+
evidence.afterUrl // Navigation happened
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// BLOCKS requires blocking-type evidence, not just beforeUrl
|
|
248
|
+
if (!hasBlockingEvidence) {
|
|
249
|
+
// BLOCKS claim without blocking evidence - ambiguous
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Conflict: BACKEND ownership but no network evidence
|
|
255
|
+
if (finding.signals?.ownership === 'BACKEND' && confidenceScore < AMBIGUITY_CONFIDENCE_THRESHOLD) {
|
|
256
|
+
const hasNetworkEvidence = (
|
|
257
|
+
sensors.network?.totalRequests > 0 ||
|
|
258
|
+
evidence.networkRequests > 0 ||
|
|
259
|
+
finding.type?.includes('network') ||
|
|
260
|
+
finding.type?.includes('auth')
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (!hasNetworkEvidence) {
|
|
264
|
+
// BACKEND ownership without network evidence - ambiguous
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Detect contradictory signals within finding itself
|
|
274
|
+
*/
|
|
275
|
+
function detectContradictorySignals(finding) {
|
|
276
|
+
const evidence = finding.evidence || {};
|
|
277
|
+
|
|
278
|
+
// Contradiction: HIGH impact but LOW confidence with weak evidence
|
|
279
|
+
if (finding.signals?.impact === 'HIGH' && finding.confidence?.level === 'LOW') {
|
|
280
|
+
// For HIGH impact + LOW confidence, need strong evidence (multiple pieces or actionable evidence)
|
|
281
|
+
const hasStrongEvidence = (
|
|
282
|
+
(evidence.afterUrl && evidence.beforeUrl) || // Navigation occurred
|
|
283
|
+
(evidence.beforeScreenshot && evidence.afterScreenshot) || // Both screenshots
|
|
284
|
+
(evidence.networkRequests || 0) > 0 ||
|
|
285
|
+
evidence.urlChanged === false || // Explicitly checked
|
|
286
|
+
evidence.domChanged === false ||
|
|
287
|
+
evidence.uiChanged === false
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Only beforeUrl alone is weak evidence for HIGH impact
|
|
291
|
+
if (!hasStrongEvidence) {
|
|
292
|
+
// HIGH impact + LOW confidence + weak evidence = contradiction
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Contradiction: BACKEND ownership but no network evidence
|
|
298
|
+
if (finding.signals?.ownership === 'BACKEND') {
|
|
299
|
+
const hasNetworkEvidence = (
|
|
300
|
+
(evidence.networkRequests || 0) > 0 ||
|
|
301
|
+
finding.type?.includes('network') ||
|
|
302
|
+
finding.type?.includes('auth')
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
if (!hasNetworkEvidence) {
|
|
306
|
+
// BACKEND ownership without network evidence
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Contradiction: PERFORMANCE ownership but no timing/loading evidence
|
|
312
|
+
if (finding.signals?.ownership === 'PERFORMANCE') {
|
|
313
|
+
const hasPerformanceEvidence = (
|
|
314
|
+
finding.type?.includes('loading') ||
|
|
315
|
+
finding.type?.includes('freeze') ||
|
|
316
|
+
finding.type?.includes('feedback_gap') ||
|
|
317
|
+
finding.evidence?.timingBreakdown
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
if (!hasPerformanceEvidence) {
|
|
321
|
+
// PERFORMANCE ownership without performance evidence
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Validate finding structure (non-destructive check)
|
|
331
|
+
* Returns validation result without modifying finding
|
|
332
|
+
*/
|
|
333
|
+
export function validateFindingStructure(finding) {
|
|
334
|
+
const errors = [];
|
|
335
|
+
|
|
336
|
+
if (!finding.type) {
|
|
337
|
+
errors.push('Missing type field');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!finding.evidence) {
|
|
341
|
+
errors.push('Missing evidence object');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!finding.confidence) {
|
|
345
|
+
errors.push('Missing confidence object');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!finding.signals) {
|
|
349
|
+
errors.push('Missing signals object');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
valid: errors.length === 0,
|
|
354
|
+
errors
|
|
355
|
+
};
|
|
356
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROMISE MODEL - Canonical representation of what interactions promise
|
|
3
|
+
*
|
|
4
|
+
* A Promise is a technical contract between code and user:
|
|
5
|
+
* "When user executes this interaction, this observable signal will change"
|
|
6
|
+
*
|
|
7
|
+
* CRITICAL: Promises are derived ONLY from observable technical signals.
|
|
8
|
+
* No business logic, no semantics, no guessing what "should" happen.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Canonical Promise Types - What observable outcome is promised
|
|
13
|
+
*/
|
|
14
|
+
export const PROMISE_TYPES = {
|
|
15
|
+
// URL or route path changes (including client-side routing)
|
|
16
|
+
NAVIGATION_PROMISE: 'NAVIGATION_PROMISE',
|
|
17
|
+
|
|
18
|
+
// HTTP request sent + success/error response handling
|
|
19
|
+
SUBMISSION_PROMISE: 'SUBMISSION_PROMISE',
|
|
20
|
+
|
|
21
|
+
// DOM or application state changes (not just network)
|
|
22
|
+
STATE_CHANGE_PROMISE: 'STATE_CHANGE_PROMISE',
|
|
23
|
+
|
|
24
|
+
// Visual or textual feedback to user (spinner, toast, modal, etc.)
|
|
25
|
+
FEEDBACK_PROMISE: 'FEEDBACK_PROMISE'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Promise Type Definitions
|
|
30
|
+
*/
|
|
31
|
+
export const PROMISE_DEFINITIONS = {
|
|
32
|
+
NAVIGATION_PROMISE: {
|
|
33
|
+
title: 'Navigation',
|
|
34
|
+
description: 'Interaction implies a change in page URL or client-side route',
|
|
35
|
+
example: 'Clicking a link; navigating to next step in wizard',
|
|
36
|
+
expected_signal: 'window.location changes OR route state changes'
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
SUBMISSION_PROMISE: {
|
|
40
|
+
title: 'Submission',
|
|
41
|
+
description: 'Interaction implies form/data submission with response handling',
|
|
42
|
+
example: 'Clicking "Send", "Submit", "Save"; HTTP request with success/error handling',
|
|
43
|
+
expected_signal: 'HTTP request sent AND response triggers success feedback OR error message'
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
STATE_CHANGE_PROMISE: {
|
|
47
|
+
title: 'State Change',
|
|
48
|
+
description: 'Interaction implies application/DOM state change (cart, filters, view)',
|
|
49
|
+
example: 'Adding to cart; toggling visibility; filtering; sorting',
|
|
50
|
+
expected_signal: 'DOM change or app state variable changes'
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
FEEDBACK_PROMISE: {
|
|
54
|
+
title: 'Feedback',
|
|
55
|
+
description: 'Interaction implies user-visible feedback (confirmation, error, info)',
|
|
56
|
+
example: 'Click should trigger spinner, toast, modal, or message',
|
|
57
|
+
expected_signal: 'Visual/textual feedback appears: spinner, toast, error, success message'
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* PromiseDescriptor - What a specific interaction promises
|
|
63
|
+
*
|
|
64
|
+
* @typedef {Object} PromiseDescriptor
|
|
65
|
+
* @property {string} type - One of PROMISE_TYPES
|
|
66
|
+
* @property {string} source - What interaction type implied this promise (button_click, form_submit, link_click, etc.)
|
|
67
|
+
* @property {string} expected_signal - What observable signal would satisfy this promise
|
|
68
|
+
* @property {Object} context - Additional context about the promise
|
|
69
|
+
* - {string} [target] - Target URL/route for NAVIGATION
|
|
70
|
+
* - {string} [endpoint] - Endpoint for SUBMISSION
|
|
71
|
+
* - {string} [stateKey] - State variable for STATE_CHANGE
|
|
72
|
+
* - {Array} [feedbackTypes] - Types of feedback for FEEDBACK (spinner, toast, modal, message)
|
|
73
|
+
* @property {string} [reason] - If UNPROVEN, why promise is ambiguous
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Infer Promise from interaction type and context
|
|
78
|
+
*
|
|
79
|
+
* Returns a PromiseDescriptor or null if no promise can be inferred
|
|
80
|
+
*/
|
|
81
|
+
export function inferPromiseFromInteraction(interaction) {
|
|
82
|
+
if (!interaction) return null;
|
|
83
|
+
|
|
84
|
+
const type = interaction.type?.toLowerCase() || '';
|
|
85
|
+
const label = interaction.label?.toLowerCase() || '';
|
|
86
|
+
const ariaLabel = interaction.ariaLabel?.toLowerCase() || '';
|
|
87
|
+
const allText = `${type} ${label} ${ariaLabel}`.toLowerCase();
|
|
88
|
+
|
|
89
|
+
// NAVIGATION_PROMISE: links, navigation buttons
|
|
90
|
+
if (type === 'link' || type === 'navigation_link') {
|
|
91
|
+
return {
|
|
92
|
+
type: PROMISE_TYPES.NAVIGATION_PROMISE,
|
|
93
|
+
source: 'link_click',
|
|
94
|
+
expected_signal: 'URL or client-side route changes',
|
|
95
|
+
context: { target: interaction.href }
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// NAVIGATION_PROMISE: "next", "go to", "navigate" buttons
|
|
100
|
+
if (type === 'button' && (
|
|
101
|
+
allText.includes('next') ||
|
|
102
|
+
allText.includes('goto') ||
|
|
103
|
+
allText.includes('go to') ||
|
|
104
|
+
allText.includes('navigate') ||
|
|
105
|
+
allText.includes('back') ||
|
|
106
|
+
allText.includes('previous')
|
|
107
|
+
)) {
|
|
108
|
+
return {
|
|
109
|
+
type: PROMISE_TYPES.NAVIGATION_PROMISE,
|
|
110
|
+
source: 'navigation_button',
|
|
111
|
+
expected_signal: 'URL or client-side route changes',
|
|
112
|
+
context: {}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// SUBMISSION_PROMISE: form submit, "send", "submit", "save" buttons
|
|
117
|
+
if (type === 'form_submit' || (type === 'button' && (
|
|
118
|
+
allText.includes('submit') ||
|
|
119
|
+
allText.includes('send') ||
|
|
120
|
+
allText.includes('save') ||
|
|
121
|
+
allText.includes('apply') ||
|
|
122
|
+
allText.includes('confirm') ||
|
|
123
|
+
allText.includes('post')
|
|
124
|
+
))) {
|
|
125
|
+
return {
|
|
126
|
+
type: PROMISE_TYPES.SUBMISSION_PROMISE,
|
|
127
|
+
source: 'form_submit',
|
|
128
|
+
expected_signal: 'HTTP request sent AND success or error feedback displayed',
|
|
129
|
+
context: {}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// FEEDBACK_PROMISE: buttons with generic labels (click, press, tap)
|
|
134
|
+
// These usually expect some feedback but promise type is ambiguous
|
|
135
|
+
if (type === 'button' && (
|
|
136
|
+
allText.includes('click') ||
|
|
137
|
+
allText.includes('tap') ||
|
|
138
|
+
allText.includes('press') ||
|
|
139
|
+
allText.length === 0
|
|
140
|
+
)) {
|
|
141
|
+
return {
|
|
142
|
+
type: PROMISE_TYPES.FEEDBACK_PROMISE,
|
|
143
|
+
source: 'generic_button',
|
|
144
|
+
expected_signal: 'Visual or textual feedback appears',
|
|
145
|
+
context: {},
|
|
146
|
+
reason: 'Generic button label; promise type cannot be determined'
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// STATE_CHANGE_PROMISE: toggle, add, remove, filter, sort
|
|
151
|
+
if (allText.includes('toggle') ||
|
|
152
|
+
allText.includes('add') ||
|
|
153
|
+
allText.includes('remove') ||
|
|
154
|
+
allText.includes('delete') ||
|
|
155
|
+
allText.includes('filter') ||
|
|
156
|
+
allText.includes('sort') ||
|
|
157
|
+
allText.includes('show') ||
|
|
158
|
+
allText.includes('hide') ||
|
|
159
|
+
allText.includes('cart')) {
|
|
160
|
+
return {
|
|
161
|
+
type: PROMISE_TYPES.STATE_CHANGE_PROMISE,
|
|
162
|
+
source: 'state_action_button',
|
|
163
|
+
expected_signal: 'DOM or application state changes',
|
|
164
|
+
context: {}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Default to FEEDBACK_PROMISE if interaction is a button but type unclear
|
|
169
|
+
if (type === 'button') {
|
|
170
|
+
return {
|
|
171
|
+
type: PROMISE_TYPES.FEEDBACK_PROMISE,
|
|
172
|
+
source: 'button',
|
|
173
|
+
expected_signal: 'User-visible feedback or state change',
|
|
174
|
+
context: {},
|
|
175
|
+
reason: 'Button intent unclear; assuming feedback expected'
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// No promise can be inferred
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Validate that a promise descriptor is well-formed
|
|
185
|
+
*/
|
|
186
|
+
export function isValidPromiseDescriptor(promise) {
|
|
187
|
+
return promise &&
|
|
188
|
+
Object.values(PROMISE_TYPES).includes(promise.type) &&
|
|
189
|
+
typeof promise.source === 'string' &&
|
|
190
|
+
typeof promise.expected_signal === 'string';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Format Promise for human display
|
|
195
|
+
*/
|
|
196
|
+
export function formatPromiseForDisplay(promise) {
|
|
197
|
+
if (!promise) return 'No promise';
|
|
198
|
+
|
|
199
|
+
const def = PROMISE_DEFINITIONS[promise.type] || {};
|
|
200
|
+
return {
|
|
201
|
+
type: promise.type,
|
|
202
|
+
title: def.title || promise.type,
|
|
203
|
+
expected: promise.expected_signal,
|
|
204
|
+
source: promise.source
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create a promise descriptor explicitly
|
|
210
|
+
*/
|
|
211
|
+
export function createPromiseDescriptor(type, source, expected_signal, context = {}, reason = null) {
|
|
212
|
+
if (!Object.values(PROMISE_TYPES).includes(type)) {
|
|
213
|
+
throw new Error(`Invalid promise type: ${type}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const descriptor = {
|
|
217
|
+
type,
|
|
218
|
+
source,
|
|
219
|
+
expected_signal,
|
|
220
|
+
context
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
if (reason) {
|
|
224
|
+
descriptor.reason = reason;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return descriptor;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export default PROMISE_TYPES;
|