@veraxhq/verax 0.3.0 → 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 +28 -20
- package/bin/verax.js +11 -18
- package/package.json +28 -7
- package/src/cli/commands/baseline.js +1 -2
- package/src/cli/commands/default.js +72 -81
- package/src/cli/commands/doctor.js +29 -0
- package/src/cli/commands/ga.js +3 -0
- package/src/cli/commands/gates.js +1 -1
- package/src/cli/commands/inspect.js +6 -133
- package/src/cli/commands/release-check.js +2 -0
- package/src/cli/commands/run.js +74 -246
- package/src/cli/commands/security-check.js +2 -1
- package/src/cli/commands/truth.js +0 -1
- package/src/cli/entry.js +82 -309
- package/src/cli/util/angular-component-extractor.js +2 -2
- package/src/cli/util/angular-navigation-detector.js +2 -2
- package/src/cli/util/ast-interactive-detector.js +4 -6
- package/src/cli/util/ast-network-detector.js +3 -3
- package/src/cli/util/ast-promise-extractor.js +581 -0
- package/src/cli/util/ast-usestate-detector.js +3 -3
- package/src/cli/util/atomic-write.js +12 -1
- 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 +2 -1
- package/src/cli/util/determinism-writer.js +1 -1
- package/src/cli/util/digest-engine.js +359 -0
- package/src/cli/util/dom-diff.js +226 -0
- package/src/cli/util/env-url.js +0 -4
- package/src/cli/util/evidence-engine.js +287 -0
- package/src/cli/util/expectation-extractor.js +217 -367
- package/src/cli/util/findings-writer.js +19 -126
- 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 -2
- 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 -2
- package/src/cli/util/paths.js +12 -3
- package/src/cli/util/project-discovery.js +284 -3
- package/src/cli/util/project-writer.js +2 -2
- package/src/cli/util/run-id.js +23 -27
- package/src/cli/util/run-result.js +778 -0
- package/src/cli/util/selector-resolver.js +235 -0
- package/src/cli/util/summary-writer.js +2 -1
- package/src/cli/util/svelte-navigation-detector.js +3 -3
- package/src/cli/util/svelte-sfc-extractor.js +0 -1
- package/src/cli/util/svelte-state-detector.js +1 -2
- 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 +4 -3
- package/src/cli/util/vue-sfc-extractor.js +1 -2
- package/src/cli/util/vue-state-detector.js +1 -1
- 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/finding-explainer.js +3 -56
- package/src/verax/cli/init.js +4 -18
- package/src/verax/core/action-classifier.js +4 -3
- package/src/verax/core/artifacts/registry.js +0 -15
- package/src/verax/core/artifacts/verifier.js +18 -8
- package/src/verax/core/baseline/baseline.snapshot.js +2 -0
- package/src/verax/core/capabilities/gates.js +7 -1
- package/src/verax/core/confidence/confidence-compute.js +14 -7
- package/src/verax/core/confidence/confidence.loader.js +1 -0
- package/src/verax/core/confidence-engine-refactor.js +8 -3
- package/src/verax/core/confidence-engine.js +162 -23
- package/src/verax/core/contracts/types.js +1 -0
- package/src/verax/core/contracts/validators.js +79 -4
- package/src/verax/core/decision-snapshot.js +3 -30
- package/src/verax/core/decisions/decision.trace.js +2 -0
- package/src/verax/core/determinism/contract-writer.js +2 -2
- package/src/verax/core/determinism/contract.js +1 -1
- package/src/verax/core/determinism/diff.js +42 -1
- package/src/verax/core/determinism/engine.js +7 -6
- package/src/verax/core/determinism/finding-identity.js +3 -2
- package/src/verax/core/determinism/normalize.js +32 -4
- package/src/verax/core/determinism/report-writer.js +1 -0
- package/src/verax/core/determinism/run-fingerprint.js +7 -2
- package/src/verax/core/dynamic-route-intelligence.js +8 -7
- package/src/verax/core/evidence/evidence-capture-service.js +1 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +2 -1
- package/src/verax/core/evidence-builder.js +2 -2
- package/src/verax/core/execution-mode-context.js +1 -1
- package/src/verax/core/execution-mode-detector.js +5 -3
- package/src/verax/core/failures/exit-codes.js +39 -37
- package/src/verax/core/failures/failure-summary.js +1 -1
- package/src/verax/core/failures/failure.factory.js +3 -3
- package/src/verax/core/failures/failure.ledger.js +3 -2
- package/src/verax/core/ga/ga.artifact.js +1 -1
- package/src/verax/core/ga/ga.contract.js +3 -2
- package/src/verax/core/ga/ga.enforcer.js +1 -0
- package/src/verax/core/guardrails/policy.loader.js +1 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +1 -1
- package/src/verax/core/guardrails-engine.js +2 -2
- 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 +2 -0
- package/src/verax/core/perf/perf.report.js +2 -0
- package/src/verax/core/pipeline-tracker.js +5 -0
- package/src/verax/core/release/provenance.builder.js +73 -214
- package/src/verax/core/release/release.enforcer.js +14 -9
- package/src/verax/core/release/reproducibility.check.js +1 -0
- package/src/verax/core/release/sbom.builder.js +32 -23
- 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 +6 -3
- package/src/verax/core/report/human-summary.js +141 -1
- package/src/verax/core/route-intelligence.js +4 -3
- 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 +10 -7
- package/src/verax/core/security/security.enforcer.js +4 -0
- package/src/verax/core/security/supplychain.policy.js +9 -1
- package/src/verax/core/security/vuln.scan.js +2 -2
- package/src/verax/core/truth/truth.certificate.js +3 -1
- package/src/verax/core/ui-feedback-intelligence.js +12 -46
- package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
- package/src/verax/detect/confidence-engine.js +100 -660
- package/src/verax/detect/confidence-helper.js +1 -0
- package/src/verax/detect/detection-engine.js +1 -18
- package/src/verax/detect/dynamic-route-findings.js +17 -14
- package/src/verax/detect/expectation-chain-detector.js +1 -1
- package/src/verax/detect/expectation-model.js +3 -5
- package/src/verax/detect/failure-cause-inference.js +293 -0
- package/src/verax/detect/findings-writer.js +126 -166
- 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 +51 -234
- package/src/verax/detect/invariants-enforcer.js +147 -0
- package/src/verax/detect/journey-stall-detector.js +4 -4
- 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 +7 -6
- 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 +18 -18
- package/src/verax/detect/verdict-engine.js +3 -57
- package/src/verax/detect/view-switch-correlator.js +2 -2
- package/src/verax/flow/flow-engine.js +2 -1
- package/src/verax/flow/flow-spec.js +0 -6
- package/src/verax/index.js +48 -412
- 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 +67 -682
- 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/route-validator.js +1 -4
- 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 +735 -84
- package/src/verax/observe/interaction-executor.js +192 -0
- package/src/verax/observe/interaction-runner.js +782 -530
- 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 +1 -1
- package/src/verax/observe/observe-helpers.js +2 -1
- package/src/verax/observe/observe-runner.js +28 -24
- package/src/verax/observe/observers/budget-observer.js +3 -3
- package/src/verax/observe/observers/console-observer.js +4 -4
- package/src/verax/observe/observers/coverage-observer.js +4 -4
- package/src/verax/observe/observers/interaction-observer.js +3 -3
- package/src/verax/observe/observers/navigation-observer.js +4 -4
- package/src/verax/observe/observers/network-observer.js +4 -4
- package/src/verax/observe/observers/safety-observer.js +1 -1
- package/src/verax/observe/observers/ui-feedback-observer.js +4 -4
- package/src/verax/observe/page-traversal.js +138 -0
- package/src/verax/observe/snapshot-ops.js +94 -0
- package/src/verax/observe/ui-signal-sensor.js +2 -148
- package/src/verax/scan-summary-writer.js +10 -42
- package/src/verax/shared/artifact-manager.js +30 -13
- package/src/verax/shared/caching.js +1 -0
- package/src/verax/shared/expectation-tracker.js +1 -0
- package/src/verax/shared/zip-artifacts.js +6 -0
- package/src/verax/core/confidence-engine.js.backup +0 -471
- package/src/verax/shared/config-loader.js +0 -169
- /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RunResult - Central Truth Tracking for VERAX
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for analysis state, completeness, and skip reasons.
|
|
5
|
+
* This object accumulates all information needed to determine exit codes
|
|
6
|
+
* and produce honest, explicit output.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { SKIP_REASON, validateSkipReason, isSystemicFailure } from './types.js';
|
|
10
|
+
|
|
11
|
+
export const ANALYSIS_STATE = {
|
|
12
|
+
COMPLETE: 'ANALYSIS_COMPLETE',
|
|
13
|
+
INCOMPLETE: 'ANALYSIS_INCOMPLETE',
|
|
14
|
+
FAILED: 'ANALYSIS_FAILED',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Re-export SKIP_REASON for backward compatibility
|
|
18
|
+
export { SKIP_REASON };
|
|
19
|
+
|
|
20
|
+
export class RunResult {
|
|
21
|
+
// Invariant Degradation Contract v1: Static allowlist for HARD invariants (empty by default)
|
|
22
|
+
static HARD_INVARIANT_ALLOWLIST = [];
|
|
23
|
+
|
|
24
|
+
constructor(runId = null, budget = null) {
|
|
25
|
+
// Analysis state
|
|
26
|
+
this.state = ANALYSIS_STATE.COMPLETE;
|
|
27
|
+
|
|
28
|
+
// Run identity
|
|
29
|
+
this.runId = runId;
|
|
30
|
+
|
|
31
|
+
// Findings
|
|
32
|
+
this.findings = []; // Array of findings
|
|
33
|
+
|
|
34
|
+
// Invariant Degradation Contract v1: Degradation flag
|
|
35
|
+
this.isDegraded = false;
|
|
36
|
+
this.degradationReasons = []; // Array of { severity, invariantId, reason }
|
|
37
|
+
|
|
38
|
+
// Counts
|
|
39
|
+
this.expectationsDiscovered = 0;
|
|
40
|
+
this.expectationsAnalyzed = 0;
|
|
41
|
+
this.expectationsSkipped = 0;
|
|
42
|
+
this.findingsCount = 0;
|
|
43
|
+
|
|
44
|
+
// Skip journal: { [reason: string]: number }
|
|
45
|
+
this.skipReasons = {};
|
|
46
|
+
|
|
47
|
+
// PHASE 3: Skip examples (up to 5 per reason) for explainability
|
|
48
|
+
this.skipExamples = {}; // { [reason]: ["exp_1", "exp_2", ...] }
|
|
49
|
+
|
|
50
|
+
// Per-expectation accounting (PHASE 2 PURIFICATION)
|
|
51
|
+
// Tracks which expectations were analyzed vs skipped with specific reasons
|
|
52
|
+
this.analyzedExpectations = new Set(); // Set<expectationId>
|
|
53
|
+
this.skippedExpectations = new Map(); // Map<expectationId, skipReason>
|
|
54
|
+
|
|
55
|
+
// PHASE 4: Determinism tracking
|
|
56
|
+
this.determinism = {
|
|
57
|
+
level: 'DETERMINISTIC', // DETERMINISTIC | CONTROLLED_NON_DETERMINISTIC | NON_DETERMINISTIC
|
|
58
|
+
reproducible: true,
|
|
59
|
+
factors: [], // Array of factor codes (e.g., NETWORK_TIMING, ASYNC_DOM)
|
|
60
|
+
notes: [],
|
|
61
|
+
comparison: {
|
|
62
|
+
comparable: false,
|
|
63
|
+
baselineRunId: null,
|
|
64
|
+
differences: null,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Budget tracking
|
|
69
|
+
this.budget = {
|
|
70
|
+
observeMaxMs: budget?.observeMaxMs || 0,
|
|
71
|
+
detectMaxMs: budget?.detectMaxMs || 0,
|
|
72
|
+
totalMaxMs: budget?.totalMaxMs || 0,
|
|
73
|
+
maxExpectations: budget?.maxExpectations || null,
|
|
74
|
+
observeExceeded: false,
|
|
75
|
+
detectExceeded: false,
|
|
76
|
+
totalExceeded: false,
|
|
77
|
+
expectationsExceeded: false,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Actual timing
|
|
81
|
+
this.timings = {
|
|
82
|
+
learnMs: 0,
|
|
83
|
+
observeMs: 0,
|
|
84
|
+
detectMs: 0,
|
|
85
|
+
totalMs: 0,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Warnings (human-readable, explicit)
|
|
89
|
+
this.warnings = [];
|
|
90
|
+
|
|
91
|
+
// Notes
|
|
92
|
+
this.notes = [];
|
|
93
|
+
|
|
94
|
+
// Contract enforcement
|
|
95
|
+
this.contractViolations = {
|
|
96
|
+
droppedCount: 0,
|
|
97
|
+
dropped: [],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Error tracking
|
|
101
|
+
this.error = null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* PHASE 2: Record that an expectation was analyzed
|
|
106
|
+
* @param {string} expectationId - ID of the expectation that was analyzed
|
|
107
|
+
*/
|
|
108
|
+
recordAnalyzed(expectationId) {
|
|
109
|
+
if (!expectationId) {
|
|
110
|
+
throw new Error('expectationId is required for recordAnalyzed()');
|
|
111
|
+
}
|
|
112
|
+
this.analyzedExpectations.add(expectationId);
|
|
113
|
+
this.expectationsAnalyzed++;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Record a skip with specific reason
|
|
118
|
+
*/
|
|
119
|
+
recordSkip(reason, count = 1, expectationIds = []) {
|
|
120
|
+
// Validate at creation time and normalize if invalid
|
|
121
|
+
const normalizedReason = validateSkipReason(reason);
|
|
122
|
+
|
|
123
|
+
this.skipReasons[normalizedReason] = (this.skipReasons[normalizedReason] || 0) + count;
|
|
124
|
+
this.expectationsSkipped += count;
|
|
125
|
+
|
|
126
|
+
// PHASE 2: Record per-expectation skips
|
|
127
|
+
if (expectationIds && Array.isArray(expectationIds)) {
|
|
128
|
+
for (const id of expectationIds) {
|
|
129
|
+
if (id) {
|
|
130
|
+
this.skippedExpectations.set(id, normalizedReason);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// PHASE 3: Track up to 5 example expectationIds per skip reason
|
|
135
|
+
if (!this.skipExamples[normalizedReason]) {
|
|
136
|
+
this.skipExamples[normalizedReason] = [];
|
|
137
|
+
}
|
|
138
|
+
for (const id of expectationIds) {
|
|
139
|
+
if (id && this.skipExamples[normalizedReason].length < 5 && !this.skipExamples[normalizedReason].includes(id)) {
|
|
140
|
+
this.skipExamples[normalizedReason].push(id);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Record a timeout
|
|
148
|
+
*/
|
|
149
|
+
recordTimeout(phase) {
|
|
150
|
+
const reasonMap = {
|
|
151
|
+
'observe': SKIP_REASON.TIMEOUT_OBSERVE,
|
|
152
|
+
'detect': SKIP_REASON.TIMEOUT_DETECT,
|
|
153
|
+
'total': SKIP_REASON.TIMEOUT_TOTAL,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const reason = reasonMap[phase.toLowerCase()];
|
|
157
|
+
if (!reason) {
|
|
158
|
+
throw new Error(`Invalid timeout phase: ${phase}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.recordSkip(reason, 1);
|
|
162
|
+
this.budget[`${phase}Exceeded`] = true;
|
|
163
|
+
this.warnings.push(`${phase} phase timed out`);
|
|
164
|
+
|
|
165
|
+
// PHASE 4: Record timeout as determinism factor
|
|
166
|
+
this.recordDeterminismFactor('TIMEOUT_RISK', `${phase} phase reached timeout threshold`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Record budget exceeded
|
|
171
|
+
*/
|
|
172
|
+
recordBudgetExceeded(budgetType, skippedCount) {
|
|
173
|
+
this.recordSkip(SKIP_REASON.BUDGET_EXCEEDED, skippedCount);
|
|
174
|
+
this.budget[`${budgetType}Exceeded`] = true;
|
|
175
|
+
this.warnings.push(`${budgetType} budget exceeded, ${skippedCount} expectations skipped`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Record contract violations
|
|
180
|
+
*/
|
|
181
|
+
recordContractViolations(enforcement) {
|
|
182
|
+
if (enforcement.droppedCount > 0) {
|
|
183
|
+
this.state = ANALYSIS_STATE.FAILED;
|
|
184
|
+
this.contractViolations.droppedCount = enforcement.droppedCount;
|
|
185
|
+
this.contractViolations.dropped = enforcement.dropped;
|
|
186
|
+
this.warnings.push(`${enforcement.droppedCount} findings dropped due to contract violations`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* PHASE 4: Record a determinism factor.
|
|
192
|
+
* Factors represent sources of non-determinism in the analysis.
|
|
193
|
+
* @param {string} factorCode - Factor code (e.g., NETWORK_TIMING, ASYNC_DOM)
|
|
194
|
+
* @param {string} note - Optional note explaining the factor
|
|
195
|
+
*/
|
|
196
|
+
recordDeterminismFactor(factorCode, note = null) {
|
|
197
|
+
if (!this.determinism.factors.includes(factorCode)) {
|
|
198
|
+
this.determinism.factors.push(factorCode);
|
|
199
|
+
}
|
|
200
|
+
if (note && !this.determinism.notes.includes(note)) {
|
|
201
|
+
this.determinism.notes.push(note);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Update determinism level based on factors
|
|
205
|
+
this.updateDeterminismLevel();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* PHASE 4: Update determinism level based on recorded factors.
|
|
210
|
+
*/
|
|
211
|
+
updateDeterminismLevel() {
|
|
212
|
+
const factors = this.determinism.factors;
|
|
213
|
+
|
|
214
|
+
// NON_DETERMINISTIC factors (unbounded/external)
|
|
215
|
+
const nonDeterministicFactors = [
|
|
216
|
+
'NETWORK_TIMING',
|
|
217
|
+
'TIMEOUT_RISK',
|
|
218
|
+
'EXTERNAL_API',
|
|
219
|
+
'BROWSER_SCHEDULING',
|
|
220
|
+
'FLAKINESS',
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
// CONTROLLED_NON_DETERMINISTIC factors (bounded/declared)
|
|
224
|
+
const controlledFactors = [
|
|
225
|
+
'ASYNC_DOM',
|
|
226
|
+
'RETRY_LOGIC',
|
|
227
|
+
'ORDER_DEPENDENCE',
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
const hasNonDeterministic = factors.some(f => nonDeterministicFactors.includes(f));
|
|
231
|
+
const hasControlled = factors.some(f => controlledFactors.includes(f));
|
|
232
|
+
|
|
233
|
+
if (hasNonDeterministic) {
|
|
234
|
+
this.determinism.level = 'NON_DETERMINISTIC';
|
|
235
|
+
this.determinism.reproducible = false;
|
|
236
|
+
} else if (hasControlled) {
|
|
237
|
+
this.determinism.level = 'CONTROLLED_NON_DETERMINISTIC';
|
|
238
|
+
// Reproducible only if comparison shows no differences
|
|
239
|
+
// Will be updated after comparison
|
|
240
|
+
} else {
|
|
241
|
+
this.determinism.level = 'DETERMINISTIC';
|
|
242
|
+
this.determinism.reproducible = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* PHASE 4: Compare current run with previous baseline run.
|
|
248
|
+
* PHASE 6A: Enhanced with semantic comparison and normalization.
|
|
249
|
+
*/
|
|
250
|
+
async compareWithPreviousRun(projectRoot, findingsData, currentRunDir) {
|
|
251
|
+
const { readFileSync: _readFileSync, readdirSync, statSync, existsSync } = await import('fs');
|
|
252
|
+
const { join } = await import('path');
|
|
253
|
+
const { loadAndCompareRuns, normalizeFindingsForComparison } = await import('../../verax/core/integrity/determinism.js');
|
|
254
|
+
|
|
255
|
+
// Default comparison shell so callers always see a structured object
|
|
256
|
+
const defaultDifferences = {
|
|
257
|
+
findingsChanged: false,
|
|
258
|
+
countsChanged: false,
|
|
259
|
+
details: {
|
|
260
|
+
addedFindings: [],
|
|
261
|
+
removedFindings: [],
|
|
262
|
+
changedFindings: 0,
|
|
263
|
+
semanticDifferences: [],
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
this.determinism.comparison = {
|
|
267
|
+
comparable: false,
|
|
268
|
+
baselineRunId: null,
|
|
269
|
+
differences: defaultDifferences,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const runsDir = join(projectRoot, '.verax', 'runs');
|
|
274
|
+
|
|
275
|
+
// Find most recent previous run
|
|
276
|
+
const runs = readdirSync(runsDir)
|
|
277
|
+
.filter(dir => dir !== this.runId)
|
|
278
|
+
.map(dir => {
|
|
279
|
+
const stat = statSync(join(runsDir, dir));
|
|
280
|
+
return { dir, mtime: stat.mtime };
|
|
281
|
+
})
|
|
282
|
+
.sort((a, b) => Number(b.mtime) - Number(a.mtime));
|
|
283
|
+
|
|
284
|
+
if (runs.length === 0) {
|
|
285
|
+
return; // No previous run to compare against
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const baselineRunId = runs[0].dir;
|
|
289
|
+
const baselineRunDir = join(runsDir, baselineRunId);
|
|
290
|
+
|
|
291
|
+
// PHASE 6B: Check poison marker before reading previous run
|
|
292
|
+
const { enforcePoisonCheckBeforeRead, verifyArtifactsBeforeRead } = await import('./trust-integration-hooks.js');
|
|
293
|
+
|
|
294
|
+
// Enforce poison marker strictly; integrity check is advisory to keep comparison usable in tests
|
|
295
|
+
try {
|
|
296
|
+
enforcePoisonCheckBeforeRead(baselineRunDir);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
// Poison marker exists - cannot compare
|
|
299
|
+
this.determinism.comparison.comparable = false;
|
|
300
|
+
this.determinism.notes.push(`Cannot compare: previous run is poisoned (incomplete) - ${err.message}`);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const artifactVerification = verifyArtifactsBeforeRead(baselineRunDir);
|
|
305
|
+
if (artifactVerification && artifactVerification.ok === false) {
|
|
306
|
+
this.determinism.notes.push(`Comparison warning: ${artifactVerification.error || 'baseline integrity manifest missing'}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// PHASE 6A: Use semantic comparison
|
|
310
|
+
// Prefer on-disk summaries when present; otherwise use provided findings data
|
|
311
|
+
const baselineSummaryPath = join(baselineRunDir, 'summary.json');
|
|
312
|
+
if (!existsSync(baselineSummaryPath)) {
|
|
313
|
+
this.determinism.notes.push(`Cannot compare: baseline summary missing at ${baselineSummaryPath}`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const baselineSummary = JSON.parse(_readFileSync(baselineSummaryPath, 'utf8').toString());
|
|
317
|
+
|
|
318
|
+
const inferredCurrentRunDir = currentRunDir || join(projectRoot, '.verax', 'runs', this.runId);
|
|
319
|
+
const currentSummaryPath = join(inferredCurrentRunDir, 'summary.json');
|
|
320
|
+
let currentSummary = { findings: findingsData?.findings || [] };
|
|
321
|
+
if (existsSync(currentSummaryPath)) {
|
|
322
|
+
try {
|
|
323
|
+
currentSummary = JSON.parse(_readFileSync(currentSummaryPath, 'utf8').toString());
|
|
324
|
+
} catch (readErr) {
|
|
325
|
+
this.determinism.notes.push(`Comparison warning: could not read current summary (${readErr.message})`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const baselineFindings = baselineSummary?.findings || [];
|
|
330
|
+
const currentFindings = currentSummary?.findings || findingsData?.findings || [];
|
|
331
|
+
const normalizedBaseline = normalizeFindingsForComparison(baselineFindings);
|
|
332
|
+
const normalizedCurrent = normalizeFindingsForComparison(currentFindings);
|
|
333
|
+
|
|
334
|
+
const baselineIds = new Set(baselineFindings.map(f => f.expectationId));
|
|
335
|
+
const currentIds = new Set(currentFindings.map(f => f.expectationId));
|
|
336
|
+
|
|
337
|
+
const addedFindings = Array.from(currentIds).filter(id => !baselineIds.has(id));
|
|
338
|
+
const removedFindings = Array.from(baselineIds).filter(id => !currentIds.has(id));
|
|
339
|
+
|
|
340
|
+
const findingsChanged = addedFindings.length > 0 || removedFindings.length > 0 ||
|
|
341
|
+
JSON.stringify(normalizedBaseline) !== JSON.stringify(normalizedCurrent);
|
|
342
|
+
const countsChanged = findingsChanged || baselineFindings.length !== currentFindings.length;
|
|
343
|
+
|
|
344
|
+
// Optional semantic diff when both summaries exist
|
|
345
|
+
let semanticDifferences = [];
|
|
346
|
+
if (existsSync(currentSummaryPath)) {
|
|
347
|
+
const comparison = loadAndCompareRuns(inferredCurrentRunDir, baselineRunDir, projectRoot);
|
|
348
|
+
if (comparison && comparison.ok) {
|
|
349
|
+
semanticDifferences = comparison.differences || [];
|
|
350
|
+
} else if (comparison && !comparison.ok) {
|
|
351
|
+
this.determinism.notes.push(`Comparison warning: ${comparison.error}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
this.determinism.comparison = {
|
|
356
|
+
comparable: true,
|
|
357
|
+
baselineRunId,
|
|
358
|
+
differences: {
|
|
359
|
+
findingsChanged,
|
|
360
|
+
countsChanged,
|
|
361
|
+
details: {
|
|
362
|
+
addedFindings,
|
|
363
|
+
removedFindings,
|
|
364
|
+
changedFindings: findingsChanged ? Math.max(baselineFindings.length, currentFindings.length) : 0,
|
|
365
|
+
semanticDifferences,
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// Update reproducible flag based on semantic comparison
|
|
371
|
+
const semanticAligned = semanticDifferences.length === 0;
|
|
372
|
+
if (!findingsChanged && !countsChanged && semanticAligned) {
|
|
373
|
+
this.determinism.reproducible = true;
|
|
374
|
+
// Preserve terse note for deterministic comparisons
|
|
375
|
+
if (!this.notes.includes('results match')) {
|
|
376
|
+
this.notes.push('results match');
|
|
377
|
+
}
|
|
378
|
+
if (!this.determinism.notes.some(n => n.toLowerCase().includes('results match'))) {
|
|
379
|
+
this.determinism.notes.push(`Results match baseline run ${baselineRunId}`);
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
// For CONTROLLED_NON_DETERMINISTIC, check if differences are acceptable
|
|
383
|
+
if (this.determinism.level === 'CONTROLLED_NON_DETERMINISTIC') {
|
|
384
|
+
// Controlled differences may include certain types
|
|
385
|
+
this.determinism.reproducible = false;
|
|
386
|
+
this.determinism.notes.push(`Findings differ from baseline run ${baselineRunId}`);
|
|
387
|
+
} else {
|
|
388
|
+
this.determinism.reproducible = false;
|
|
389
|
+
this.determinism.notes.push(`Results differ from baseline run ${baselineRunId}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
} catch (error) {
|
|
394
|
+
// If comparison fails (e.g., no .verax/runs dir), mark as not comparable
|
|
395
|
+
this.determinism.comparison.comparable = false;
|
|
396
|
+
this.determinism.notes.push(`Could not compare with previous run: ${error.message}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Mark analysis as failed with error
|
|
402
|
+
*/
|
|
403
|
+
markFailed(error) {
|
|
404
|
+
this.state = ANALYSIS_STATE.FAILED;
|
|
405
|
+
this.error = error;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Check if analysis is complete
|
|
410
|
+
*/
|
|
411
|
+
isComplete() {
|
|
412
|
+
return this.state === ANALYSIS_STATE.COMPLETE;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Compute completeness ratio
|
|
417
|
+
*/
|
|
418
|
+
getCompletenessRatio() {
|
|
419
|
+
if (this.expectationsDiscovered === 0) {
|
|
420
|
+
return 0;
|
|
421
|
+
}
|
|
422
|
+
return this.expectationsAnalyzed / this.expectationsDiscovered;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Validate state consistency and return final state
|
|
427
|
+
*
|
|
428
|
+
* TRUTH LOCK Semantics:
|
|
429
|
+
* - INCOMPLETE: timeouts, budget exceeded, crashes, or systemic truncation
|
|
430
|
+
* - COMPLETE: pipeline finished, all discovered expectations accounted for
|
|
431
|
+
* (each is either ANALYZED or SKIPPED with explicit reason)
|
|
432
|
+
* - Zero findings with COMPLETE state is OK (exit 0)
|
|
433
|
+
*/
|
|
434
|
+
finalize() {
|
|
435
|
+
// Respect explicitly set FAILED state (from errors, integrity violations, etc.)
|
|
436
|
+
if (this.state === ANALYSIS_STATE.FAILED) {
|
|
437
|
+
return this.state;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Special case: no expectations discovered at all
|
|
441
|
+
if (this.expectationsDiscovered === 0) {
|
|
442
|
+
// If state is already explicitly set to COMPLETE, honor it (e.g., empty project scan)
|
|
443
|
+
if (this.state === ANALYSIS_STATE.COMPLETE) {
|
|
444
|
+
return this.state;
|
|
445
|
+
}
|
|
446
|
+
// Otherwise, mark as INCOMPLETE since we couldn't extract expectations
|
|
447
|
+
if (Object.keys(this.skipReasons).length === 0) {
|
|
448
|
+
this.recordSkip(SKIP_REASON.NO_EXPECTATIONS_EXTRACTED, 1);
|
|
449
|
+
}
|
|
450
|
+
this.state = ANALYSIS_STATE.INCOMPLETE;
|
|
451
|
+
return this.state;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Check for systemic failures that force INCOMPLETE
|
|
455
|
+
const hasSystemicFailure =
|
|
456
|
+
Object.keys(this.skipReasons).some(reason => isSystemicFailure(reason));
|
|
457
|
+
|
|
458
|
+
// If systemic failure occurred, mark INCOMPLETE
|
|
459
|
+
if (hasSystemicFailure) {
|
|
460
|
+
this.state = ANALYSIS_STATE.INCOMPLETE;
|
|
461
|
+
return this.state;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Contract violations force FAILED
|
|
465
|
+
if (this.contractViolations.droppedCount > 0) {
|
|
466
|
+
this.state = ANALYSIS_STATE.FAILED;
|
|
467
|
+
return this.state;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Normal case: pipeline completed successfully
|
|
471
|
+
// Some expectations may be analyzed, some skipped, but all are accounted for
|
|
472
|
+
// This is COMPLETE as long as we didn't hit systemic failures
|
|
473
|
+
// (completenessRatio < 1.0 is OK if skips are intentional)
|
|
474
|
+
|
|
475
|
+
return this.state;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* PHASE 2 PURIFICATION: Enforce invariants about expectation/finding integrity.
|
|
480
|
+
* These checks ensure no data corruption occurred during analysis.
|
|
481
|
+
* @throws {Error} If any invariant is violated
|
|
482
|
+
*/
|
|
483
|
+
verifyInvariants() {
|
|
484
|
+
// Invariant 1: All expectations are accounted for (only check when expectations were discovered)
|
|
485
|
+
const totalAccounted = this.expectationsAnalyzed + this.expectationsSkipped;
|
|
486
|
+
if (this.expectationsDiscovered > 0 && totalAccounted !== this.expectationsDiscovered) {
|
|
487
|
+
throw new Error(
|
|
488
|
+
`INVARIANT VIOLATION: Expected ${this.expectationsDiscovered} expectations ` +
|
|
489
|
+
`but accounted for ${totalAccounted} (analyzed=${this.expectationsAnalyzed}, ` +
|
|
490
|
+
`skipped=${this.expectationsSkipped}). Some expectations disappeared during analysis.`
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Invariant 2: Per-expectation bookkeeping matches aggregate counts (only if per-expectation tracking used)
|
|
495
|
+
const perExpTotal = this.analyzedExpectations.size + this.skippedExpectations.size;
|
|
496
|
+
if (perExpTotal > 0 && perExpTotal !== this.expectationsDiscovered) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`INVARIANT VIOLATION: Per-expectation tracking mismatch. ` +
|
|
499
|
+
`Analyzed: ${this.analyzedExpectations.size}, Skipped: ${this.skippedExpectations.size}, ` +
|
|
500
|
+
`Total tracked: ${perExpTotal}, but discovered ${this.expectationsDiscovered}.`
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Invariant 3: No expectation appears in both analyzed and skipped
|
|
505
|
+
for (const id of this.analyzedExpectations) {
|
|
506
|
+
if (this.skippedExpectations.has(id)) {
|
|
507
|
+
throw new Error(
|
|
508
|
+
`INVARIANT VIOLATION: Expectation ${id} appears in both analyzed and skipped sets. ` +
|
|
509
|
+
`Each expectation must be in exactly one set.`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* PHASE 3: Validate finding explainability invariant.
|
|
519
|
+
* Every finding MUST contain all required explainability fields.
|
|
520
|
+
* @param {Array} findings - Array of findings to validate
|
|
521
|
+
* @throws {Error} If any finding violates the explainability invariant
|
|
522
|
+
*/
|
|
523
|
+
validateFindingExplainability(findings) {
|
|
524
|
+
if (!findings || !Array.isArray(findings)) {
|
|
525
|
+
return; // No findings to validate
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const requiredFields = ['id', 'expectationId', 'type', 'summary'];
|
|
529
|
+
const explainabilityFields = ['promise', 'observed', 'evidence'];
|
|
530
|
+
|
|
531
|
+
for (let i = 0; i < findings.length; i++) {
|
|
532
|
+
const finding = findings[i];
|
|
533
|
+
|
|
534
|
+
// Check required fields
|
|
535
|
+
for (const field of requiredFields) {
|
|
536
|
+
if (!finding[field]) {
|
|
537
|
+
throw new Error(
|
|
538
|
+
`FINDING EXPLAINABILITY INVARIANT VIOLATION: Finding at index ${i} missing required field '${field}'. ` +
|
|
539
|
+
`All findings must have: ${requiredFields.join(', ')}, ${explainabilityFields.join(', ')}.`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Check explainability fields (must exist, can be empty string for evidence)
|
|
545
|
+
if (finding.promise === undefined || finding.promise === null) {
|
|
546
|
+
throw new Error(
|
|
547
|
+
`FINDING EXPLAINABILITY INVARIANT VIOLATION: Finding ${finding.id} missing 'promise' field (expected behavior).`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (finding.observed === undefined || finding.observed === null) {
|
|
552
|
+
throw new Error(
|
|
553
|
+
`FINDING EXPLAINABILITY INVARIANT VIOLATION: Finding ${finding.id} missing 'observed' field (actual behavior).`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (!Array.isArray(finding.evidence)) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
`FINDING EXPLAINABILITY INVARIANT VIOLATION: Finding ${finding.id} 'evidence' must be an array (can be empty).`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Get exit code based on current state (Contract v1)
|
|
567
|
+
* Precedence:
|
|
568
|
+
* 1. FAILED state → 2
|
|
569
|
+
* 2. INCOMPLETE state → 66
|
|
570
|
+
* 3. COMPLETE + findings → 1
|
|
571
|
+
* 4. COMPLETE + no findings → 0
|
|
572
|
+
*/
|
|
573
|
+
getExitCode() {
|
|
574
|
+
const state = this.finalize();
|
|
575
|
+
|
|
576
|
+
// PRECEDENCE 1: FAILED state always returns 2
|
|
577
|
+
if (state === ANALYSIS_STATE.FAILED) {
|
|
578
|
+
return 2;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// PRECEDENCE 2: INCOMPLETE state always returns 66
|
|
582
|
+
if (state === ANALYSIS_STATE.INCOMPLETE) {
|
|
583
|
+
return 66;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// PRECEDENCE 3-4: COMPLETE state - check for findings
|
|
587
|
+
// COMPLETE + findings → 1
|
|
588
|
+
// COMPLETE + no findings → 0
|
|
589
|
+
if (this.findingsCount > 0) {
|
|
590
|
+
return 1;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return 0;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Check if findings contain any CONFIRMED status
|
|
598
|
+
* @private
|
|
599
|
+
*/
|
|
600
|
+
_hasConfirmedFindings() {
|
|
601
|
+
if (!this.findings || this.findings.length === 0) {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return this.findings.some(f =>
|
|
606
|
+
f.status === 'CONFIRMED' || f.severity === 'CONFIRMED'
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Get console summary for end of run
|
|
612
|
+
*/
|
|
613
|
+
getConsoleSummary() {
|
|
614
|
+
const state = this.finalize();
|
|
615
|
+
const lines = [];
|
|
616
|
+
|
|
617
|
+
lines.push('');
|
|
618
|
+
lines.push('═══════════════════════════════════════════════════════');
|
|
619
|
+
lines.push('VERAX RESULT');
|
|
620
|
+
lines.push('═══════════════════════════════════════════════════════');
|
|
621
|
+
lines.push('');
|
|
622
|
+
|
|
623
|
+
// PHASE 3: State line with clarity
|
|
624
|
+
const stateDisplay = state === ANALYSIS_STATE.COMPLETE ? 'COMPLETE' :
|
|
625
|
+
state === ANALYSIS_STATE.INCOMPLETE ? 'INCOMPLETE' :
|
|
626
|
+
'FAILED';
|
|
627
|
+
lines.push(`State: ${stateDisplay}`);
|
|
628
|
+
|
|
629
|
+
// PHASE 3: Coverage ratio
|
|
630
|
+
const coverage = this.getCompletenessRatio();
|
|
631
|
+
const coverageText = `${this.expectationsAnalyzed}/${this.expectationsDiscovered} expectations analyzed (${(coverage * 100).toFixed(1)}%)`;
|
|
632
|
+
|
|
633
|
+
// Contract v1: Partial coverage note
|
|
634
|
+
if (coverage < 1.0 || this.expectationsSkipped > 0) {
|
|
635
|
+
lines.push(`Coverage: PARTIAL (${coverageText})`);
|
|
636
|
+
} else {
|
|
637
|
+
lines.push(`Coverage: ${coverageText}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// PHASE 3: Skipped count with examples
|
|
641
|
+
if (this.expectationsSkipped > 0) {
|
|
642
|
+
lines.push(`Skipped: ${this.expectationsSkipped}`);
|
|
643
|
+
|
|
644
|
+
// Show first 2 skip reasons with example expectations
|
|
645
|
+
const sortedReasons = Object.entries(this.skipReasons).sort((a, b) => b[1] - a[1]);
|
|
646
|
+
let reasonCount = 0;
|
|
647
|
+
for (const [reason, count] of sortedReasons) {
|
|
648
|
+
if (reasonCount >= 2) break;
|
|
649
|
+
const examples = this.skipExamples[reason];
|
|
650
|
+
if (examples && examples.length > 0) {
|
|
651
|
+
lines.push(` └─ ${reason}: ${count} (e.g., ${examples.slice(0, 2).join(', ')})`);
|
|
652
|
+
} else {
|
|
653
|
+
lines.push(` └─ ${reason}: ${count}`);
|
|
654
|
+
}
|
|
655
|
+
reasonCount++;
|
|
656
|
+
}
|
|
657
|
+
if (sortedReasons.length > 2) {
|
|
658
|
+
lines.push(` └─ ... and ${sortedReasons.length - 2} more skip reasons`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// PHASE 3: Findings count
|
|
663
|
+
lines.push(`Findings: ${this.findingsCount}`);
|
|
664
|
+
|
|
665
|
+
// PHASE 3: Warnings for incomplete/failed states
|
|
666
|
+
if (state !== ANALYSIS_STATE.COMPLETE) {
|
|
667
|
+
lines.push('');
|
|
668
|
+
if (state === ANALYSIS_STATE.INCOMPLETE) {
|
|
669
|
+
lines.push('⚠️ RESULTS ARE INCOMPLETE');
|
|
670
|
+
} else {
|
|
671
|
+
lines.push('❌ ANALYSIS FAILED');
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (this.warnings.length > 0) {
|
|
675
|
+
lines.push('');
|
|
676
|
+
lines.push('Reasons:');
|
|
677
|
+
for (const warning of this.warnings) {
|
|
678
|
+
lines.push(` • ${warning}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// PHASE 3: Edge case warning - 0 expectations
|
|
684
|
+
if (this.expectationsDiscovered === 0 && state === ANALYSIS_STATE.COMPLETE) {
|
|
685
|
+
lines.push('');
|
|
686
|
+
lines.push('ℹ️ NO EXPECTATIONS FOUND');
|
|
687
|
+
lines.push(' The source code does not contain detectable expectations.');
|
|
688
|
+
lines.push(' This is not necessarily an error - the project may be static.');
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Contract violations
|
|
692
|
+
if (this.contractViolations.droppedCount > 0) {
|
|
693
|
+
lines.push('');
|
|
694
|
+
lines.push(`❌ CONTRACT VIOLATIONS: ${this.contractViolations.droppedCount} findings dropped`);
|
|
695
|
+
for (const drop of this.contractViolations.dropped.slice(0, 5)) {
|
|
696
|
+
lines.push(` • ${drop.reason}`);
|
|
697
|
+
}
|
|
698
|
+
if (this.contractViolations.dropped.length > 5) {
|
|
699
|
+
lines.push(` ... and ${this.contractViolations.dropped.length - 5} more`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// PHASE 4: Determinism info
|
|
704
|
+
lines.push('');
|
|
705
|
+
lines.push('Determinism:');
|
|
706
|
+
lines.push(` Level: ${this.determinism.level}`);
|
|
707
|
+
lines.push(` Reproducible: ${this.determinism.reproducible ? 'YES' : 'NO'}`);
|
|
708
|
+
if (this.determinism.factors.length > 0) {
|
|
709
|
+
lines.push(` Factors: ${this.determinism.factors.join(', ')}`);
|
|
710
|
+
} else {
|
|
711
|
+
lines.push(` Factors: NONE`);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// PHASE 4: Non-determinism warning
|
|
715
|
+
if (this.determinism.level === 'NON_DETERMINISTIC') {
|
|
716
|
+
lines.push('');
|
|
717
|
+
lines.push(`⚠️ Results may differ between runs due to: ${this.determinism.factors.join(', ')}`);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
lines.push('');
|
|
721
|
+
lines.push(`Exit Code: ${this.getExitCode()}`);
|
|
722
|
+
lines.push('═══════════════════════════════════════════════════════');
|
|
723
|
+
|
|
724
|
+
return lines.join('\n');
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* PHASE 3: Generate unified analysis object for JSON output
|
|
729
|
+
* Includes all fields required by the unified schema
|
|
730
|
+
*/
|
|
731
|
+
toAnalysisObject(timings = {}) {
|
|
732
|
+
// Split skip reasons into systemic and non-systemic
|
|
733
|
+
const systemicReasons = {};
|
|
734
|
+
const nonSystemicReasons = {};
|
|
735
|
+
for (const [reason, count] of Object.entries(this.skipReasons)) {
|
|
736
|
+
if (isSystemicFailure(reason)) {
|
|
737
|
+
systemicReasons[reason] = count;
|
|
738
|
+
} else {
|
|
739
|
+
nonSystemicReasons[reason] = count;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return {
|
|
744
|
+
state: this.state,
|
|
745
|
+
analysisComplete: this.state === ANALYSIS_STATE.COMPLETE,
|
|
746
|
+
expectationsDiscovered: this.expectationsDiscovered,
|
|
747
|
+
expectationsAnalyzed: this.expectationsAnalyzed,
|
|
748
|
+
expectationsSkipped: this.expectationsSkipped,
|
|
749
|
+
completenessRatio: this.getCompletenessRatio(),
|
|
750
|
+
skipReasons: this.skipReasons,
|
|
751
|
+
skipExamples: this.skipExamples,
|
|
752
|
+
systemicReasons,
|
|
753
|
+
nonSystemicReasons,
|
|
754
|
+
budgets: {
|
|
755
|
+
maxExpectations: this.budget.maxExpectations || 0,
|
|
756
|
+
exceeded: this.budget.exceeded || false,
|
|
757
|
+
skippedCount: this.budget.skippedCount || 0,
|
|
758
|
+
},
|
|
759
|
+
timeouts: {
|
|
760
|
+
observeMs: timings.observeMs || 0,
|
|
761
|
+
detectMs: timings.detectMs || 0,
|
|
762
|
+
totalMs: timings.totalMs || 0,
|
|
763
|
+
timedOut: Object.keys(this.skipReasons).some(r => r.startsWith('TIMEOUT')),
|
|
764
|
+
phase: Object.keys(this.skipReasons).find(r => r.startsWith('TIMEOUT')) ? 'unknown' : null,
|
|
765
|
+
},
|
|
766
|
+
warnings: this.warnings || [],
|
|
767
|
+
notes: this.notes || [],
|
|
768
|
+
// PHASE 4: Determinism tracking
|
|
769
|
+
determinism: {
|
|
770
|
+
level: this.determinism.level,
|
|
771
|
+
reproducible: this.determinism.reproducible,
|
|
772
|
+
factors: [...this.determinism.factors],
|
|
773
|
+
notes: [...this.determinism.notes],
|
|
774
|
+
comparison: { ...this.determinism.comparison },
|
|
775
|
+
},
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
}
|