@veraxhq/verax 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -6
- package/bin/verax.js +11 -11
- package/package.json +29 -8
- package/src/cli/commands/baseline.js +103 -0
- package/src/cli/commands/default.js +51 -6
- package/src/cli/commands/doctor.js +29 -0
- package/src/cli/commands/ga.js +246 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +4 -2
- package/src/cli/commands/release-check.js +215 -0
- package/src/cli/commands/run.js +45 -6
- package/src/cli/commands/security-check.js +212 -0
- package/src/cli/commands/truth.js +113 -0
- package/src/cli/entry.js +30 -20
- package/src/cli/util/angular-component-extractor.js +179 -0
- package/src/cli/util/angular-navigation-detector.js +141 -0
- package/src/cli/util/angular-network-detector.js +161 -0
- package/src/cli/util/angular-state-detector.js +162 -0
- package/src/cli/util/ast-interactive-detector.js +544 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-promise-extractor.js +581 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/atomic-write.js +12 -1
- package/src/cli/util/bootstrap-guard.js +86 -0
- package/src/cli/util/console-reporter.js +72 -0
- package/src/cli/util/detection-engine.js +105 -41
- package/src/cli/util/determinism-runner.js +124 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/digest-engine.js +359 -0
- package/src/cli/util/dom-diff.js +226 -0
- package/src/cli/util/evidence-engine.js +287 -0
- package/src/cli/util/expectation-extractor.js +151 -5
- package/src/cli/util/findings-writer.js +3 -0
- package/src/cli/util/framework-detector.js +572 -0
- package/src/cli/util/idgen.js +1 -1
- package/src/cli/util/interaction-planner.js +529 -0
- package/src/cli/util/learn-writer.js +2 -0
- package/src/cli/util/ledger-writer.js +110 -0
- package/src/cli/util/monorepo-resolver.js +162 -0
- package/src/cli/util/observation-engine.js +127 -278
- package/src/cli/util/observe-writer.js +2 -0
- package/src/cli/util/project-discovery.js +284 -0
- package/src/cli/util/project-writer.js +2 -0
- package/src/cli/util/run-id.js +23 -27
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/run-result.js +778 -0
- package/src/cli/util/selector-resolver.js +235 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +2 -0
- package/src/cli/util/svelte-navigation-detector.js +163 -0
- package/src/cli/util/svelte-network-detector.js +80 -0
- package/src/cli/util/svelte-sfc-extractor.js +146 -0
- package/src/cli/util/svelte-state-detector.js +242 -0
- package/src/cli/util/trust-activation-integration.js +496 -0
- package/src/cli/util/trust-activation-wrapper.js +85 -0
- package/src/cli/util/trust-integration-hooks.js +164 -0
- package/src/cli/util/types.js +153 -0
- package/src/cli/util/url-validation.js +40 -0
- package/src/cli/util/vue-navigation-detector.js +178 -0
- package/src/cli/util/vue-sfc-extractor.js +161 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- package/src/types/fs-augment.d.ts +23 -0
- package/src/types/global.d.ts +137 -0
- package/src/types/internal-types.d.ts +35 -0
- package/src/verax/cli/init.js +4 -18
- package/src/verax/core/action-classifier.js +4 -3
- package/src/verax/core/artifacts/registry.js +139 -0
- package/src/verax/core/artifacts/verifier.js +990 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +233 -0
- package/src/verax/core/capabilities/gates.js +505 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +144 -0
- package/src/verax/core/confidence/confidence-invariants.js +234 -0
- package/src/verax/core/confidence/confidence-report-writer.js +112 -0
- package/src/verax/core/confidence/confidence-weights.js +44 -0
- package/src/verax/core/confidence/confidence.defaults.js +65 -0
- package/src/verax/core/confidence/confidence.loader.js +80 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +489 -0
- package/src/verax/core/confidence-engine.js +625 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +186 -0
- package/src/verax/core/contracts/validators.js +456 -0
- package/src/verax/core/decisions/decision.trace.js +278 -0
- package/src/verax/core/determinism/contract-writer.js +89 -0
- package/src/verax/core/determinism/contract.js +139 -0
- package/src/verax/core/determinism/diff.js +405 -0
- package/src/verax/core/determinism/engine.js +222 -0
- package/src/verax/core/determinism/finding-identity.js +149 -0
- package/src/verax/core/determinism/normalize.js +466 -0
- package/src/verax/core/determinism/report-writer.js +93 -0
- package/src/verax/core/determinism/run-fingerprint.js +123 -0
- package/src/verax/core/dynamic-route-intelligence.js +529 -0
- package/src/verax/core/evidence/evidence-capture-service.js +308 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +166 -0
- package/src/verax/core/evidence-builder.js +487 -0
- package/src/verax/core/execution-mode-context.js +77 -0
- package/src/verax/core/execution-mode-detector.js +192 -0
- package/src/verax/core/failures/exit-codes.js +88 -0
- package/src/verax/core/failures/failure-summary.js +76 -0
- package/src/verax/core/failures/failure.factory.js +225 -0
- package/src/verax/core/failures/failure.ledger.js +133 -0
- package/src/verax/core/failures/failure.types.js +196 -0
- package/src/verax/core/failures/index.js +10 -0
- package/src/verax/core/ga/ga-report-writer.js +43 -0
- package/src/verax/core/ga/ga.artifact.js +49 -0
- package/src/verax/core/ga/ga.contract.js +435 -0
- package/src/verax/core/ga/ga.enforcer.js +87 -0
- package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
- package/src/verax/core/guardrails/policy.defaults.js +210 -0
- package/src/verax/core/guardrails/policy.loader.js +84 -0
- package/src/verax/core/guardrails/policy.schema.js +110 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
- package/src/verax/core/guardrails-engine.js +505 -0
- package/src/verax/core/incremental-store.js +1 -0
- package/src/verax/core/integrity/budget.js +138 -0
- package/src/verax/core/integrity/determinism.js +342 -0
- package/src/verax/core/integrity/integrity.js +208 -0
- package/src/verax/core/integrity/poisoning.js +108 -0
- package/src/verax/core/integrity/transaction.js +140 -0
- package/src/verax/core/observe/run-timeline.js +318 -0
- package/src/verax/core/perf/perf.contract.js +186 -0
- package/src/verax/core/perf/perf.display.js +65 -0
- package/src/verax/core/perf/perf.enforcer.js +91 -0
- package/src/verax/core/perf/perf.monitor.js +209 -0
- package/src/verax/core/perf/perf.report.js +200 -0
- package/src/verax/core/pipeline-tracker.js +243 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +130 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +164 -0
- package/src/verax/core/release/reproducibility.check.js +222 -0
- package/src/verax/core/release/sbom.builder.js +292 -0
- package/src/verax/core/replay-validator.js +2 -0
- package/src/verax/core/replay.js +4 -0
- package/src/verax/core/report/cross-index.js +195 -0
- package/src/verax/core/report/human-summary.js +362 -0
- package/src/verax/core/route-intelligence.js +420 -0
- package/src/verax/core/run-id.js +6 -3
- package/src/verax/core/run-manifest.js +4 -3
- package/src/verax/core/security/secrets.scan.js +329 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +128 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +334 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/truth/truth.certificate.js +252 -0
- package/src/verax/core/ui-feedback-intelligence.js +481 -0
- package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
- package/src/verax/detect/confidence-engine.js +62 -34
- package/src/verax/detect/confidence-helper.js +34 -0
- package/src/verax/detect/dynamic-route-findings.js +338 -0
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +2 -2
- package/src/verax/detect/failure-cause-inference.js +293 -0
- package/src/verax/detect/findings-writer.js +131 -35
- package/src/verax/detect/flow-detector.js +2 -2
- package/src/verax/detect/form-silent-failure.js +98 -0
- package/src/verax/detect/index.js +46 -5
- package/src/verax/detect/invariants-enforcer.js +147 -0
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/navigation-silent-failure.js +82 -0
- package/src/verax/detect/problem-aggregator.js +361 -0
- package/src/verax/detect/route-findings.js +219 -0
- package/src/verax/detect/summary-writer.js +477 -0
- package/src/verax/detect/test-failure-cause-inference.js +314 -0
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/flow/flow-engine.js +2 -1
- package/src/verax/flow/flow-spec.js +0 -6
- package/src/verax/index.js +4 -0
- package/src/verax/intel/ts-program.js +1 -0
- package/src/verax/intel/vue-navigation-extractor.js +3 -0
- package/src/verax/learn/action-contract-extractor.js +3 -0
- package/src/verax/learn/ast-contract-extractor.js +1 -1
- package/src/verax/learn/flow-extractor.js +1 -0
- package/src/verax/learn/project-detector.js +5 -0
- package/src/verax/learn/react-router-extractor.js +2 -0
- package/src/verax/learn/source-instrumenter.js +1 -0
- package/src/verax/learn/state-extractor.js +2 -1
- package/src/verax/learn/static-extractor.js +1 -0
- package/src/verax/observe/coverage-gaps.js +132 -0
- package/src/verax/observe/expectation-handler.js +126 -0
- package/src/verax/observe/incremental-skip.js +46 -0
- package/src/verax/observe/index.js +51 -155
- package/src/verax/observe/interaction-executor.js +192 -0
- package/src/verax/observe/interaction-runner.js +782 -513
- package/src/verax/observe/network-firewall.js +86 -0
- package/src/verax/observe/observation-builder.js +169 -0
- package/src/verax/observe/observe-context.js +205 -0
- package/src/verax/observe/observe-helpers.js +192 -0
- package/src/verax/observe/observe-runner.js +230 -0
- package/src/verax/observe/observers/budget-observer.js +185 -0
- package/src/verax/observe/observers/console-observer.js +102 -0
- package/src/verax/observe/observers/coverage-observer.js +107 -0
- package/src/verax/observe/observers/interaction-observer.js +471 -0
- package/src/verax/observe/observers/navigation-observer.js +132 -0
- package/src/verax/observe/observers/network-observer.js +87 -0
- package/src/verax/observe/observers/safety-observer.js +82 -0
- package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
- package/src/verax/observe/page-traversal.js +138 -0
- package/src/verax/observe/snapshot-ops.js +94 -0
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/scan-summary-writer.js +2 -0
- package/src/verax/shared/artifact-manager.js +25 -5
- package/src/verax/shared/caching.js +1 -0
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/expectation-tracker.js +1 -0
- package/src/verax/shared/view-switch-rules.js +208 -0
- package/src/verax/shared/zip-artifacts.js +6 -0
- package/src/verax/shared/config-loader.js +0 -169
- /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 21.4 — Guardrails Engine (Policy-Driven)
|
|
3
|
+
*
|
|
4
|
+
* Central guardrails engine that prevents false CONFIRMED findings
|
|
5
|
+
* by enforcing policy-driven rules.
|
|
6
|
+
*
|
|
7
|
+
* All rules are mandatory and cannot be disabled.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { loadGuardrailsPolicy, getPolicyReport } from './guardrails/policy.loader.js';
|
|
11
|
+
import { GUARDRAILS_RULE } from './guardrails/policy.defaults.js';
|
|
12
|
+
|
|
13
|
+
// Re-export for backward compatibility
|
|
14
|
+
export { GUARDRAILS_RULE };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* PHASE 17: Guardrails Severity Levels
|
|
18
|
+
*/
|
|
19
|
+
export const GUARDRAILS_SEVERITY = {
|
|
20
|
+
BLOCK_CONFIRMED: 'BLOCK_CONFIRMED', // Prevents CONFIRMED status
|
|
21
|
+
DOWNGRADE: 'DOWNGRADE', // Recommends downgrade
|
|
22
|
+
INFORMATIONAL: 'INFORMATIONAL', // Makes finding informational
|
|
23
|
+
DROP: 'DROP', // Recommends dropping finding
|
|
24
|
+
WARNING: 'WARNING', // Warning only, no status change
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Global policy cache (loaded once per process)
|
|
28
|
+
let cachedPolicy = null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get guardrails policy (cached)
|
|
32
|
+
*
|
|
33
|
+
* @param {string|null} policyPath - Custom policy path (optional)
|
|
34
|
+
* @param {string} projectDir - Project directory
|
|
35
|
+
* @returns {Object} Guardrails policy
|
|
36
|
+
*/
|
|
37
|
+
function getGuardrailsPolicy(policyPath = null, projectDir = null) {
|
|
38
|
+
if (!cachedPolicy) {
|
|
39
|
+
cachedPolicy = loadGuardrailsPolicy(policyPath, projectDir);
|
|
40
|
+
}
|
|
41
|
+
return cachedPolicy;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* PHASE 21.4: Apply guardrails to a finding using policy
|
|
46
|
+
*
|
|
47
|
+
* @param {Object} finding - Finding object
|
|
48
|
+
* @param {Object} context - Context including evidencePackage, signals, confidenceReasons, promise type
|
|
49
|
+
* @param {Object} options - Options { policyPath, projectDir }
|
|
50
|
+
* @returns {Object} { finding: updatedFinding, guardrails: report }
|
|
51
|
+
*/
|
|
52
|
+
export function applyGuardrails(finding, context = {}, options = {}) {
|
|
53
|
+
const evidencePackage = context.evidencePackage || finding.evidencePackage || {};
|
|
54
|
+
const signals = context.signals || evidencePackage.signals || {};
|
|
55
|
+
const _confidenceReasons = context.confidenceReasons || finding.confidenceReasons || [];
|
|
56
|
+
const _promiseType = context.promiseType || finding.expectation?.type || finding.promise?.type || null;
|
|
57
|
+
|
|
58
|
+
// Load policy
|
|
59
|
+
const policy = getGuardrailsPolicy(options.policyPath, options.projectDir);
|
|
60
|
+
const policyReport = getPolicyReport(policy);
|
|
61
|
+
|
|
62
|
+
const appliedRules = [];
|
|
63
|
+
const contradictions = [];
|
|
64
|
+
let recommendedStatus = finding.severity || finding.status || 'SUSPECTED';
|
|
65
|
+
const confidenceAdjustments = [];
|
|
66
|
+
let confidenceDelta = 0;
|
|
67
|
+
|
|
68
|
+
// Apply rules in deterministic order (by rule id)
|
|
69
|
+
const sortedRules = [...policy.rules].sort((a, b) => a.id.localeCompare(b.id));
|
|
70
|
+
|
|
71
|
+
for (const rule of sortedRules) {
|
|
72
|
+
// Check if rule applies to this finding type
|
|
73
|
+
const appliesToFinding = rule.appliesTo.includes('*') ||
|
|
74
|
+
rule.appliesTo.some(cap => finding.type?.includes(cap));
|
|
75
|
+
|
|
76
|
+
if (!appliesToFinding) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Evaluate rule
|
|
81
|
+
const evaluation = evaluateRule(rule, finding, signals, evidencePackage);
|
|
82
|
+
|
|
83
|
+
if (evaluation.applies) {
|
|
84
|
+
appliedRules.push({
|
|
85
|
+
code: rule.id,
|
|
86
|
+
severity: mapActionToSeverity(rule.action),
|
|
87
|
+
message: evaluation.message,
|
|
88
|
+
ruleId: rule.id,
|
|
89
|
+
category: rule.category
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (evaluation.contradiction) {
|
|
93
|
+
contradictions.push({
|
|
94
|
+
code: rule.id,
|
|
95
|
+
message: evaluation.message,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (evaluation.recommendedStatus) {
|
|
100
|
+
recommendedStatus = evaluation.recommendedStatus;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const delta = rule.confidenceDelta || 0;
|
|
104
|
+
confidenceDelta += delta;
|
|
105
|
+
|
|
106
|
+
if (delta !== 0) {
|
|
107
|
+
confidenceAdjustments.push({
|
|
108
|
+
reason: rule.id,
|
|
109
|
+
delta: delta,
|
|
110
|
+
message: evaluation.message,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Apply confidence adjustments
|
|
117
|
+
let finalConfidence = finding.confidence || 0;
|
|
118
|
+
if (confidenceDelta !== 0) {
|
|
119
|
+
finalConfidence = Math.max(0, Math.min(1, finalConfidence + confidenceDelta));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Build guardrails report with policy metadata
|
|
123
|
+
const guardrailsReport = {
|
|
124
|
+
appliedRules,
|
|
125
|
+
contradictions,
|
|
126
|
+
recommendedStatus,
|
|
127
|
+
confidenceAdjustments,
|
|
128
|
+
confidenceDelta,
|
|
129
|
+
finalDecision: recommendedStatus,
|
|
130
|
+
policyReport: {
|
|
131
|
+
version: policyReport.version,
|
|
132
|
+
source: policyReport.source,
|
|
133
|
+
appliedRuleIds: appliedRules.map(r => r.code)
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Update finding
|
|
138
|
+
const updatedFinding = {
|
|
139
|
+
...finding,
|
|
140
|
+
severity: recommendedStatus,
|
|
141
|
+
status: recommendedStatus, // Also update status for backward compatibility
|
|
142
|
+
confidence: finalConfidence,
|
|
143
|
+
guardrails: guardrailsReport,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
finding: updatedFinding,
|
|
148
|
+
guardrails: guardrailsReport,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Map policy action to severity
|
|
154
|
+
*/
|
|
155
|
+
function mapActionToSeverity(action) {
|
|
156
|
+
const mapping = {
|
|
157
|
+
'BLOCK': GUARDRAILS_SEVERITY.BLOCK_CONFIRMED,
|
|
158
|
+
'DOWNGRADE': GUARDRAILS_SEVERITY.DOWNGRADE,
|
|
159
|
+
'INFO': GUARDRAILS_SEVERITY.INFORMATIONAL
|
|
160
|
+
};
|
|
161
|
+
return mapping[action] || GUARDRAILS_SEVERITY.WARNING;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Evaluate a guardrails rule
|
|
166
|
+
*
|
|
167
|
+
* @param {Object} rule - Policy rule
|
|
168
|
+
* @param {Object} finding - Finding object
|
|
169
|
+
* @param {Object} signals - Sensor signals
|
|
170
|
+
* @param {Object} evidencePackage - Evidence package
|
|
171
|
+
* @returns {Object} { applies, message, contradiction, recommendedStatus }
|
|
172
|
+
*/
|
|
173
|
+
function evaluateRule(rule, finding, signals, evidencePackage) {
|
|
174
|
+
const evalType = rule.evaluation.type;
|
|
175
|
+
const isConfirmed = finding.severity === 'CONFIRMED' || finding.status === 'CONFIRMED';
|
|
176
|
+
|
|
177
|
+
switch (evalType) {
|
|
178
|
+
case 'network_success_no_ui':
|
|
179
|
+
return evaluateNetSuccessNoUi(finding, signals, evidencePackage, isConfirmed);
|
|
180
|
+
|
|
181
|
+
case 'analytics_only':
|
|
182
|
+
return evaluateAnalyticsOnly(finding, signals, evidencePackage, isConfirmed);
|
|
183
|
+
|
|
184
|
+
case 'shallow_routing':
|
|
185
|
+
return evaluateShallowRouting(finding, signals, evidencePackage, isConfirmed);
|
|
186
|
+
|
|
187
|
+
case 'ui_feedback_present':
|
|
188
|
+
return evaluateUiFeedbackPresent(finding, signals, evidencePackage, isConfirmed);
|
|
189
|
+
|
|
190
|
+
case 'interaction_blocked':
|
|
191
|
+
return evaluateInteractionBlocked(finding, signals, evidencePackage, isConfirmed);
|
|
192
|
+
|
|
193
|
+
case 'validation_present':
|
|
194
|
+
return evaluateValidationPresent(finding, signals, evidencePackage, isConfirmed);
|
|
195
|
+
|
|
196
|
+
case 'contradict_evidence':
|
|
197
|
+
return evaluateContradictEvidence(finding, signals, evidencePackage, isConfirmed);
|
|
198
|
+
|
|
199
|
+
case 'view_switch_minor_change':
|
|
200
|
+
return evaluateViewSwitchMinorChange(finding, signals, evidencePackage, isConfirmed);
|
|
201
|
+
|
|
202
|
+
case 'view_switch_analytics_only':
|
|
203
|
+
return evaluateViewSwitchAnalyticsOnly(finding, signals, evidencePackage, isConfirmed);
|
|
204
|
+
|
|
205
|
+
case 'view_switch_ambiguous':
|
|
206
|
+
return evaluateViewSwitchAmbiguous(finding, signals, evidencePackage, isConfirmed);
|
|
207
|
+
|
|
208
|
+
default:
|
|
209
|
+
return { applies: false };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Rule 1: GUARD_NET_SUCCESS_NO_UI
|
|
215
|
+
*/
|
|
216
|
+
function evaluateNetSuccessNoUi(finding, signals, evidencePackage, isConfirmed) {
|
|
217
|
+
const networkSignals = signals.network || {};
|
|
218
|
+
const uiSignals = signals.uiSignals || {};
|
|
219
|
+
const uiFeedback = signals.uiFeedback || {};
|
|
220
|
+
|
|
221
|
+
const hasNetworkSuccess = networkSignals.successfulRequests > 0 &&
|
|
222
|
+
networkSignals.failedRequests === 0;
|
|
223
|
+
const hasNoUiChange = !uiSignals.changed &&
|
|
224
|
+
(!uiFeedback.overallUiFeedbackScore || uiFeedback.overallUiFeedbackScore < 0.3);
|
|
225
|
+
const hasNoErrors = !networkSignals.failedRequests &&
|
|
226
|
+
(!signals.console || signals.console.errorCount === 0);
|
|
227
|
+
|
|
228
|
+
const isSilentFailure = finding.type?.includes('silent_failure') ||
|
|
229
|
+
finding.type?.includes('network');
|
|
230
|
+
|
|
231
|
+
if (hasNetworkSuccess && hasNoUiChange && hasNoErrors && isSilentFailure && isConfirmed) {
|
|
232
|
+
return {
|
|
233
|
+
applies: true,
|
|
234
|
+
message: 'Network request succeeded but no UI change observed. This is not a silent failure.',
|
|
235
|
+
contradiction: true,
|
|
236
|
+
recommendedStatus: 'SUSPECTED',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { applies: false };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Rule 2: GUARD_ANALYTICS_ONLY
|
|
245
|
+
*/
|
|
246
|
+
function evaluateAnalyticsOnly(finding, signals, evidencePackage, isConfirmed) {
|
|
247
|
+
const networkSignals = signals.network || {};
|
|
248
|
+
const networkRequests = networkSignals.topFailedUrls ||
|
|
249
|
+
networkSignals.observedRequestUrls ||
|
|
250
|
+
[];
|
|
251
|
+
|
|
252
|
+
const isAnalyticsOnly = networkRequests.some(url => {
|
|
253
|
+
if (!url || typeof url !== 'string') return false;
|
|
254
|
+
const lowerUrl = url.toLowerCase();
|
|
255
|
+
return lowerUrl.includes('/analytics') ||
|
|
256
|
+
lowerUrl.includes('/beacon') ||
|
|
257
|
+
lowerUrl.includes('/tracking') ||
|
|
258
|
+
lowerUrl.includes('/pixel') ||
|
|
259
|
+
lowerUrl.includes('google-analytics') ||
|
|
260
|
+
lowerUrl.includes('segment.io') ||
|
|
261
|
+
lowerUrl.includes('mixpanel');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const isNetworkFinding = finding.type?.includes('network') ||
|
|
265
|
+
finding.type?.includes('silent_failure');
|
|
266
|
+
|
|
267
|
+
if (isAnalyticsOnly && isNetworkFinding && isConfirmed && networkRequests.length === 1) {
|
|
268
|
+
return {
|
|
269
|
+
applies: true,
|
|
270
|
+
message: 'Only analytics/beacon requests detected. These are not user promises.',
|
|
271
|
+
contradiction: true,
|
|
272
|
+
recommendedStatus: 'INFORMATIONAL',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { applies: false };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Rule 3: GUARD_SHALLOW_ROUTING
|
|
281
|
+
*/
|
|
282
|
+
function evaluateShallowRouting(finding, signals, evidencePackage, isConfirmed) {
|
|
283
|
+
const navigationSignals = signals.navigation || {};
|
|
284
|
+
const beforeUrl = evidencePackage.before?.url || '';
|
|
285
|
+
const afterUrl = evidencePackage.after?.url || '';
|
|
286
|
+
|
|
287
|
+
const isHashOnly = beforeUrl && afterUrl &&
|
|
288
|
+
beforeUrl.split('#')[0] === afterUrl.split('#')[0] &&
|
|
289
|
+
(beforeUrl.includes('#') || afterUrl.includes('#'));
|
|
290
|
+
const isShallowRouting = navigationSignals.shallowRouting === true &&
|
|
291
|
+
!navigationSignals.urlChanged;
|
|
292
|
+
|
|
293
|
+
const isNavigationFinding = finding.type?.includes('navigation') ||
|
|
294
|
+
finding.type?.includes('route');
|
|
295
|
+
|
|
296
|
+
if ((isHashOnly || isShallowRouting) && isNavigationFinding && isConfirmed) {
|
|
297
|
+
return {
|
|
298
|
+
applies: true,
|
|
299
|
+
message: 'Hash-only or shallow routing detected. Cannot confirm navigation without route intelligence verification.',
|
|
300
|
+
contradiction: true,
|
|
301
|
+
recommendedStatus: 'SUSPECTED',
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { applies: false };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Rule 4: GUARD_UI_FEEDBACK_PRESENT
|
|
310
|
+
*/
|
|
311
|
+
function evaluateUiFeedbackPresent(finding, signals, evidencePackage, isConfirmed) {
|
|
312
|
+
const uiFeedback = signals.uiFeedback || {};
|
|
313
|
+
const uiSignals = signals.uiSignals || {};
|
|
314
|
+
|
|
315
|
+
const hasFeedback = (uiFeedback.overallUiFeedbackScore || 0) > 0.5 ||
|
|
316
|
+
uiSignals.hasLoadingIndicator ||
|
|
317
|
+
uiSignals.hasDialog ||
|
|
318
|
+
uiSignals.hasErrorSignal ||
|
|
319
|
+
uiSignals.changed;
|
|
320
|
+
|
|
321
|
+
const isSilentFailure = finding.type?.includes('silent_failure') ||
|
|
322
|
+
finding.type?.includes('feedback_missing');
|
|
323
|
+
|
|
324
|
+
if (hasFeedback && isSilentFailure && isConfirmed) {
|
|
325
|
+
return {
|
|
326
|
+
applies: true,
|
|
327
|
+
message: 'UI feedback is present. This contradicts a silent failure claim.',
|
|
328
|
+
contradiction: true,
|
|
329
|
+
recommendedStatus: 'SUSPECTED',
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { applies: false };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Rule 5: GUARD_INTERACTION_BLOCKED
|
|
338
|
+
*/
|
|
339
|
+
function evaluateInteractionBlocked(finding, signals, evidencePackage, isConfirmed) {
|
|
340
|
+
const interaction = finding.interaction || {};
|
|
341
|
+
const action = evidencePackage.action || {};
|
|
342
|
+
|
|
343
|
+
const isDisabled = interaction.disabled === true ||
|
|
344
|
+
action.interaction?.disabled === true ||
|
|
345
|
+
finding.evidence?.interactionBlocked === true;
|
|
346
|
+
|
|
347
|
+
const isSilentFailure = finding.type?.includes('silent_failure');
|
|
348
|
+
|
|
349
|
+
if (isDisabled && isSilentFailure && isConfirmed) {
|
|
350
|
+
return {
|
|
351
|
+
applies: true,
|
|
352
|
+
message: 'Interaction was disabled/blocked. This is expected behavior, not a silent failure.',
|
|
353
|
+
recommendedStatus: 'INFORMATIONAL',
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return { applies: false };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Rule 6: GUARD_VALIDATION_PRESENT
|
|
362
|
+
*/
|
|
363
|
+
function evaluateValidationPresent(finding, signals, evidencePackage, isConfirmed) {
|
|
364
|
+
const uiSignals = signals.uiSignals || {};
|
|
365
|
+
const uiFeedback = signals.uiFeedback || {};
|
|
366
|
+
|
|
367
|
+
const hasValidationFeedback = uiSignals.hasErrorSignal ||
|
|
368
|
+
uiSignals.hasValidationMessage ||
|
|
369
|
+
(uiFeedback.signals?.validation?.happened === true);
|
|
370
|
+
|
|
371
|
+
const isValidationFailure = finding.type?.includes('validation') ||
|
|
372
|
+
finding.type?.includes('form');
|
|
373
|
+
|
|
374
|
+
if (hasValidationFeedback && isValidationFailure && isConfirmed) {
|
|
375
|
+
return {
|
|
376
|
+
applies: true,
|
|
377
|
+
message: 'Validation feedback is present. This contradicts a validation silent failure claim.',
|
|
378
|
+
contradiction: true,
|
|
379
|
+
recommendedStatus: 'SUSPECTED',
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return { applies: false };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Rule 7: GUARD_CONTRADICT_EVIDENCE
|
|
388
|
+
*/
|
|
389
|
+
function evaluateContradictEvidence(finding, signals, evidencePackage, isConfirmed) {
|
|
390
|
+
if (!evidencePackage || !evidencePackage.isComplete) {
|
|
391
|
+
const missingFields = evidencePackage.missingEvidence || [];
|
|
392
|
+
if (isConfirmed && missingFields.length > 0) {
|
|
393
|
+
return {
|
|
394
|
+
applies: true,
|
|
395
|
+
message: `Evidence package is incomplete. Missing: ${missingFields.join(', ')}`,
|
|
396
|
+
contradiction: true,
|
|
397
|
+
recommendedStatus: 'SUSPECTED',
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return { applies: false };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Rule: GUARD_VIEW_SWITCH_MINOR_CHANGE
|
|
407
|
+
* If URL unchanged and change is minor (e.g. button text change only) -> cannot be CONFIRMED
|
|
408
|
+
*/
|
|
409
|
+
function evaluateViewSwitchMinorChange(finding, signals, evidencePackage, isConfirmed) {
|
|
410
|
+
const isViewSwitch = finding.type?.includes('view_switch') ||
|
|
411
|
+
finding.expectation?.kind === 'VIEW_SWITCH_PROMISE';
|
|
412
|
+
const beforeUrl = evidencePackage.before?.url || '';
|
|
413
|
+
const afterUrl = evidencePackage.after?.url || '';
|
|
414
|
+
const urlUnchanged = beforeUrl === afterUrl;
|
|
415
|
+
|
|
416
|
+
const uiSignals = signals.uiSignals || {};
|
|
417
|
+
const uiFeedback = signals.uiFeedback || {};
|
|
418
|
+
|
|
419
|
+
// Check if change is minor (only button text, no structural change)
|
|
420
|
+
const isMinorChange = (
|
|
421
|
+
uiSignals.textChanged === true &&
|
|
422
|
+
!uiSignals.domChanged &&
|
|
423
|
+
!uiSignals.visibleChanged &&
|
|
424
|
+
!uiSignals.ariaChanged &&
|
|
425
|
+
(!uiFeedback.overallUiFeedbackScore || uiFeedback.overallUiFeedbackScore < 0.2)
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
if (isViewSwitch && urlUnchanged && isMinorChange && isConfirmed) {
|
|
429
|
+
return {
|
|
430
|
+
applies: true,
|
|
431
|
+
message: 'URL unchanged and change is minor (e.g. button text only). Cannot confirm view switch.',
|
|
432
|
+
contradiction: true,
|
|
433
|
+
recommendedStatus: 'SUSPECTED',
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return { applies: false };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Rule: GUARD_VIEW_SWITCH_ANALYTICS_ONLY
|
|
442
|
+
* If only analytics fired -> ignore
|
|
443
|
+
*/
|
|
444
|
+
function evaluateViewSwitchAnalyticsOnly(finding, signals, evidencePackage, isConfirmed) {
|
|
445
|
+
const isViewSwitch = finding.type?.includes('view_switch') ||
|
|
446
|
+
finding.expectation?.kind === 'VIEW_SWITCH_PROMISE';
|
|
447
|
+
|
|
448
|
+
const networkSignals = signals.network || {};
|
|
449
|
+
const networkRequests = networkSignals.topFailedUrls ||
|
|
450
|
+
networkSignals.observedRequestUrls ||
|
|
451
|
+
[];
|
|
452
|
+
|
|
453
|
+
const analyticsOnly = networkRequests.length > 0 && networkRequests.every(url => {
|
|
454
|
+
if (!url || typeof url !== 'string') return false;
|
|
455
|
+
const lowerUrl = url.toLowerCase();
|
|
456
|
+
return lowerUrl.includes('/analytics') ||
|
|
457
|
+
lowerUrl.includes('/beacon') ||
|
|
458
|
+
lowerUrl.includes('/tracking') ||
|
|
459
|
+
lowerUrl.includes('/pixel') ||
|
|
460
|
+
lowerUrl.includes('google-analytics') ||
|
|
461
|
+
lowerUrl.includes('segment.io') ||
|
|
462
|
+
lowerUrl.includes('mixpanel');
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const uiSignals = signals.uiSignals || {};
|
|
466
|
+
const hasNoUiChange = !uiSignals.changed && !uiSignals.domChanged && !uiSignals.visibleChanged;
|
|
467
|
+
|
|
468
|
+
if (isViewSwitch && analyticsOnly && hasNoUiChange && isConfirmed) {
|
|
469
|
+
return {
|
|
470
|
+
applies: true,
|
|
471
|
+
message: 'Only analytics fired, no UI change. Cannot confirm view switch.',
|
|
472
|
+
contradiction: true,
|
|
473
|
+
recommendedStatus: 'INFORMATIONAL',
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return { applies: false };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Rule: GUARD_VIEW_SWITCH_AMBIGUOUS
|
|
482
|
+
* If state change promise exists but UI outcome ambiguous (one signal only) -> SUSPECTED
|
|
483
|
+
*/
|
|
484
|
+
function evaluateViewSwitchAmbiguous(finding, signals, evidencePackage, isConfirmed) {
|
|
485
|
+
const isViewSwitch = finding.type?.includes('view_switch') ||
|
|
486
|
+
finding.expectation?.kind === 'VIEW_SWITCH_PROMISE';
|
|
487
|
+
const hasPromise = finding.expectation?.kind === 'VIEW_SWITCH_PROMISE' ||
|
|
488
|
+
finding.promise?.type === 'view_switch';
|
|
489
|
+
|
|
490
|
+
// Check correlation result - if only one signal, it's ambiguous
|
|
491
|
+
const correlation = finding.correlation || {};
|
|
492
|
+
const signalCount = correlation.signals?.length || 0;
|
|
493
|
+
const ambiguousOutcome = signalCount === 1;
|
|
494
|
+
|
|
495
|
+
if (isViewSwitch && hasPromise && ambiguousOutcome && isConfirmed) {
|
|
496
|
+
return {
|
|
497
|
+
applies: true,
|
|
498
|
+
message: 'State change promise exists but UI outcome ambiguous (one signal only). Downgrading to SUSPECTED.',
|
|
499
|
+
contradiction: false,
|
|
500
|
+
recommendedStatus: 'SUSPECTED',
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return { applies: false };
|
|
505
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 6A: Hard Budget Enforcement
|
|
3
|
+
*
|
|
4
|
+
* Enforces performance budgets as HARD limits with immediate termination.
|
|
5
|
+
* No warnings, no soft limits - budget exceeded = ANALYSIS_INCOMPLETE.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { SKIP_REASON } from '../../../cli/util/types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if budget is exceeded and enforce hard limit
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} budget - Budget configuration
|
|
14
|
+
* @param {Object} metrics - Current metrics
|
|
15
|
+
* @param {Function} recordSkip - Function to record skip
|
|
16
|
+
* @param {Function} markIncomplete - Function to mark analysis incomplete
|
|
17
|
+
* @returns {{ exceeded: boolean, phase?: string, limit?: number, actual?: number }} Result
|
|
18
|
+
*/
|
|
19
|
+
export function enforceBudget(budget, metrics, recordSkip, markIncomplete) {
|
|
20
|
+
// Check observe budget
|
|
21
|
+
if (budget.observeMaxMs && metrics.observeMs >= budget.observeMaxMs) {
|
|
22
|
+
recordSkip(SKIP_REASON.TIMEOUT_OBSERVE, 1);
|
|
23
|
+
markIncomplete('observe', budget.observeMaxMs, metrics.observeMs);
|
|
24
|
+
return {
|
|
25
|
+
exceeded: true,
|
|
26
|
+
phase: 'observe',
|
|
27
|
+
limit: budget.observeMaxMs,
|
|
28
|
+
actual: metrics.observeMs,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check detect budget
|
|
33
|
+
if (budget.detectMaxMs && metrics.detectMs >= budget.detectMaxMs) {
|
|
34
|
+
recordSkip(SKIP_REASON.TIMEOUT_DETECT, 1);
|
|
35
|
+
markIncomplete('detect', budget.detectMaxMs, metrics.detectMs);
|
|
36
|
+
return {
|
|
37
|
+
exceeded: true,
|
|
38
|
+
phase: 'detect',
|
|
39
|
+
limit: budget.detectMaxMs,
|
|
40
|
+
actual: metrics.detectMs,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check total budget
|
|
45
|
+
if (budget.totalMaxMs && metrics.totalMs >= budget.totalMaxMs) {
|
|
46
|
+
recordSkip(SKIP_REASON.TIMEOUT_TOTAL, 1);
|
|
47
|
+
markIncomplete('total', budget.totalMaxMs, metrics.totalMs);
|
|
48
|
+
return {
|
|
49
|
+
exceeded: true,
|
|
50
|
+
phase: 'total',
|
|
51
|
+
limit: budget.totalMaxMs,
|
|
52
|
+
actual: metrics.totalMs,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check expectations budget
|
|
57
|
+
if (budget.maxExpectations && metrics.expectationsAnalyzed >= budget.maxExpectations) {
|
|
58
|
+
const skippedCount = metrics.expectationsDiscovered - budget.maxExpectations;
|
|
59
|
+
if (skippedCount > 0) {
|
|
60
|
+
recordSkip(SKIP_REASON.BUDGET_EXCEEDED, skippedCount);
|
|
61
|
+
markIncomplete('expectations', budget.maxExpectations, metrics.expectationsAnalyzed);
|
|
62
|
+
return {
|
|
63
|
+
exceeded: true,
|
|
64
|
+
phase: 'expectations',
|
|
65
|
+
limit: budget.maxExpectations,
|
|
66
|
+
actual: metrics.expectationsAnalyzed,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { exceeded: false };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create budget guard that throws on budget exceeded
|
|
76
|
+
*
|
|
77
|
+
* @param {Object} budget - Budget configuration
|
|
78
|
+
* @param {Object} metrics - Metrics object (will be updated)
|
|
79
|
+
* @returns {Function} Guard function to check budget
|
|
80
|
+
*/
|
|
81
|
+
export function createBudgetGuard(budget, metrics) {
|
|
82
|
+
return (phase) => {
|
|
83
|
+
const check = enforceBudget(
|
|
84
|
+
budget,
|
|
85
|
+
metrics,
|
|
86
|
+
() => {}, // Skip recording handled elsewhere
|
|
87
|
+
() => {} // Marking handled elsewhere
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (check.exceeded) {
|
|
91
|
+
const error = new Error(
|
|
92
|
+
`Budget exceeded: ${phase} phase (limit: ${check.limit}ms, actual: ${check.actual}ms)`
|
|
93
|
+
);
|
|
94
|
+
error.name = 'BudgetExceededError';
|
|
95
|
+
// @ts-expect-error - Dynamic error properties
|
|
96
|
+
error.code = 'BUDGET_EXCEEDED';
|
|
97
|
+
// @ts-expect-error - Dynamic error properties
|
|
98
|
+
error.phase = phase;
|
|
99
|
+
// @ts-expect-error - Dynamic error properties
|
|
100
|
+
error.limit = check.limit;
|
|
101
|
+
// @ts-expect-error - Dynamic error properties
|
|
102
|
+
error.actual = check.actual;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Wrap async operation with budget enforcement
|
|
110
|
+
*
|
|
111
|
+
* @param {Promise} operation - Async operation
|
|
112
|
+
* @param {Function} budgetGuard - Budget guard function
|
|
113
|
+
* @param {string} phase - Phase name
|
|
114
|
+
* @param {Function} onCheck - Called periodically to check budget
|
|
115
|
+
* @returns {Promise} Operation result or throws on budget exceeded
|
|
116
|
+
*/
|
|
117
|
+
export async function withBudgetEnforcement(operation, budgetGuard, phase, onCheck) {
|
|
118
|
+
const checkInterval = setInterval(() => {
|
|
119
|
+
try {
|
|
120
|
+
budgetGuard(phase);
|
|
121
|
+
if (onCheck) {
|
|
122
|
+
onCheck();
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
clearInterval(checkInterval);
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}, 1000); // Check every second
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const result = await operation;
|
|
132
|
+
clearInterval(checkInterval);
|
|
133
|
+
return result;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
clearInterval(checkInterval);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|