@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,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 18 — Determinism Diff Builder
|
|
3
|
+
*
|
|
4
|
+
* Generates structured diffs between normalized artifacts from different runs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { computeFindingIdentity as _computeFindingIdentity } from './finding-identity.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* PHASE 18: Diff reason codes
|
|
11
|
+
* PHASE 25: Extended with new reason codes
|
|
12
|
+
*/
|
|
13
|
+
export const DIFF_REASON = {
|
|
14
|
+
MISSING_ARTIFACT: 'DET_DIFF_MISSING_ARTIFACT',
|
|
15
|
+
SCHEMA_MISMATCH: 'DET_DIFF_SCHEMA_MISMATCH',
|
|
16
|
+
FINDING_ADDED: 'DET_DIFF_FINDING_ADDED',
|
|
17
|
+
FINDING_REMOVED: 'DET_DIFF_FINDING_REMOVED',
|
|
18
|
+
FINDING_STATUS_CHANGED: 'DET_DIFF_FINDING_STATUS_CHANGED',
|
|
19
|
+
FINDING_SEVERITY_CHANGED: 'DET_DIFF_FINDING_SEVERITY_CHANGED',
|
|
20
|
+
CONFIDENCE_CHANGED: 'DET_DIFF_CONFIDENCE_CHANGED',
|
|
21
|
+
CONFIDENCE_REASONS_CHANGED: 'DET_DIFF_CONFIDENCE_REASONS_CHANGED',
|
|
22
|
+
GUARDRAILS_CHANGED: 'DET_DIFF_GUARDRAILS_CHANGED',
|
|
23
|
+
EVIDENCE_COMPLETENESS_CHANGED: 'DET_DIFF_EVIDENCE_COMPLETENESS_CHANGED',
|
|
24
|
+
EVIDENCE_MISSING: 'DET_DIFF_EVIDENCE_MISSING',
|
|
25
|
+
OBSERVATION_COUNT_CHANGED: 'DET_DIFF_OBSERVATION_COUNT_CHANGED',
|
|
26
|
+
FIELD_VALUE_CHANGED: 'DET_DIFF_FIELD_VALUE_CHANGED',
|
|
27
|
+
RUN_FINGERPRINT_MISMATCH: 'DET_DIFF_RUN_FINGERPRINT_MISMATCH',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* PHASE 18: Diff categories
|
|
32
|
+
*/
|
|
33
|
+
export const DIFF_CATEGORY = {
|
|
34
|
+
FINDINGS: 'FINDINGS',
|
|
35
|
+
EXPECTATIONS: 'EXPECTATIONS',
|
|
36
|
+
OBSERVATIONS: 'OBSERVATIONS',
|
|
37
|
+
EVIDENCE: 'EVIDENCE',
|
|
38
|
+
STATUS: 'STATUS',
|
|
39
|
+
ARTIFACTS: 'ARTIFACTS',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* PHASE 18: Diff severity
|
|
44
|
+
*/
|
|
45
|
+
export const DIFF_SEVERITY = {
|
|
46
|
+
BLOCKER: 'BLOCKER',
|
|
47
|
+
WARN: 'WARN',
|
|
48
|
+
INFO: 'INFO',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function diffRunMeta(artifactA, artifactB) {
|
|
52
|
+
return diffGeneric(artifactA, artifactB, 'runMeta');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function diffDeterminismContract(artifactA, artifactB) {
|
|
56
|
+
return diffGeneric(artifactA, artifactB, 'determinismContract');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function diffReportArtifact(artifactA, artifactB, artifactName) {
|
|
60
|
+
return diffGeneric(artifactA, artifactB, artifactName);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* PHASE 18: Diff artifacts
|
|
65
|
+
*
|
|
66
|
+
* @param {Object} artifactA - First artifact (normalized)
|
|
67
|
+
* @param {Object} artifactB - Second artifact (normalized)
|
|
68
|
+
* @param {string} artifactName - Name of artifact
|
|
69
|
+
* @param {Map} findingIdentityMap - Map of finding identity to finding (for matching)
|
|
70
|
+
* @returns {Array} Array of diff objects
|
|
71
|
+
*/
|
|
72
|
+
export function diffArtifacts(artifactA, artifactB, artifactName, findingIdentityMap = null) {
|
|
73
|
+
const diffs = [];
|
|
74
|
+
|
|
75
|
+
// Check if artifacts exist
|
|
76
|
+
if (!artifactA && artifactB) {
|
|
77
|
+
diffs.push({
|
|
78
|
+
category: DIFF_CATEGORY.ARTIFACTS,
|
|
79
|
+
severity: DIFF_SEVERITY.BLOCKER,
|
|
80
|
+
reasonCode: DIFF_REASON.MISSING_ARTIFACT,
|
|
81
|
+
message: `Artifact ${artifactName} missing in first run`,
|
|
82
|
+
artifact: artifactName,
|
|
83
|
+
});
|
|
84
|
+
return diffs;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (artifactA && !artifactB) {
|
|
88
|
+
diffs.push({
|
|
89
|
+
category: DIFF_CATEGORY.ARTIFACTS,
|
|
90
|
+
severity: DIFF_SEVERITY.BLOCKER,
|
|
91
|
+
reasonCode: DIFF_REASON.MISSING_ARTIFACT,
|
|
92
|
+
message: `Artifact ${artifactName} missing in second run`,
|
|
93
|
+
artifact: artifactName,
|
|
94
|
+
});
|
|
95
|
+
return diffs;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!artifactA && !artifactB) {
|
|
99
|
+
return diffs; // Both missing, no diff
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Artifact-specific diffing
|
|
103
|
+
if (artifactName === 'findings') {
|
|
104
|
+
diffs.push(...diffFindings(artifactA, artifactB, findingIdentityMap));
|
|
105
|
+
} else if (artifactName === 'runMeta') {
|
|
106
|
+
diffs.push(...diffRunMeta(artifactA, artifactB));
|
|
107
|
+
} else if (artifactName === 'determinismContract') {
|
|
108
|
+
diffs.push(...diffDeterminismContract(artifactA, artifactB));
|
|
109
|
+
} else if (artifactName === 'confidenceReport' || artifactName === 'guardrailsReport' || artifactName === 'evidenceIntent') {
|
|
110
|
+
diffs.push(...diffReportArtifact(artifactA, artifactB, artifactName));
|
|
111
|
+
} else {
|
|
112
|
+
// Generic diff for other artifacts
|
|
113
|
+
diffs.push(...diffGeneric(artifactA, artifactB, artifactName));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return diffs;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Diff findings artifacts
|
|
121
|
+
*/
|
|
122
|
+
function diffFindings(artifactA, artifactB, findingIdentityMap) {
|
|
123
|
+
const diffs = [];
|
|
124
|
+
|
|
125
|
+
const findingsA = artifactA.findings || [];
|
|
126
|
+
const findingsB = artifactB.findings || [];
|
|
127
|
+
|
|
128
|
+
// Check counts
|
|
129
|
+
if (findingsA.length !== findingsB.length) {
|
|
130
|
+
diffs.push({
|
|
131
|
+
category: DIFF_CATEGORY.FINDINGS,
|
|
132
|
+
severity: DIFF_SEVERITY.BLOCKER,
|
|
133
|
+
reasonCode: DIFF_REASON.OBSERVATION_COUNT_CHANGED,
|
|
134
|
+
message: `Finding count changed: ${findingsA.length} → ${findingsB.length}`,
|
|
135
|
+
artifact: 'findings',
|
|
136
|
+
field: 'findings.length',
|
|
137
|
+
oldValue: findingsA.length,
|
|
138
|
+
newValue: findingsB.length,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Build identity maps if not provided
|
|
143
|
+
const mapA = new Map();
|
|
144
|
+
const mapB = new Map();
|
|
145
|
+
|
|
146
|
+
for (const finding of findingsA) {
|
|
147
|
+
const identity = findingIdentityMap ? findingIdentityMap.get(finding) : computeFindingIdentitySimple(finding);
|
|
148
|
+
mapA.set(identity, finding);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const finding of findingsB) {
|
|
152
|
+
const identity = findingIdentityMap ? findingIdentityMap.get(finding) : computeFindingIdentitySimple(finding);
|
|
153
|
+
mapB.set(identity, finding);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Find added findings
|
|
157
|
+
for (const [identity, finding] of mapB) {
|
|
158
|
+
if (!mapA.has(identity)) {
|
|
159
|
+
diffs.push({
|
|
160
|
+
category: DIFF_CATEGORY.FINDINGS,
|
|
161
|
+
severity: DIFF_SEVERITY.BLOCKER,
|
|
162
|
+
reasonCode: DIFF_REASON.FINDING_ADDED,
|
|
163
|
+
message: `Finding added: ${finding.type || 'unknown'}`,
|
|
164
|
+
artifact: 'findings',
|
|
165
|
+
findingIdentity: identity,
|
|
166
|
+
finding: finding,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Find removed findings
|
|
172
|
+
for (const [identity, finding] of mapA) {
|
|
173
|
+
if (!mapB.has(identity)) {
|
|
174
|
+
diffs.push({
|
|
175
|
+
category: DIFF_CATEGORY.FINDINGS,
|
|
176
|
+
severity: DIFF_SEVERITY.BLOCKER,
|
|
177
|
+
reasonCode: DIFF_REASON.FINDING_REMOVED,
|
|
178
|
+
message: `Finding removed: ${finding.type || 'unknown'}`,
|
|
179
|
+
artifact: 'findings',
|
|
180
|
+
findingIdentity: identity,
|
|
181
|
+
finding: finding,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Find changed findings
|
|
187
|
+
for (const [identity, findingA] of mapA) {
|
|
188
|
+
const findingB = mapB.get(identity);
|
|
189
|
+
if (findingB) {
|
|
190
|
+
diffs.push(...diffFinding(findingA, findingB, identity));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return diffs;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Diff individual finding
|
|
199
|
+
*/
|
|
200
|
+
function diffFinding(findingA, findingB, identity) {
|
|
201
|
+
const diffs = [];
|
|
202
|
+
|
|
203
|
+
// Check status/severity
|
|
204
|
+
const statusA = findingA.severity || findingA.status;
|
|
205
|
+
const statusB = findingB.severity || findingB.status;
|
|
206
|
+
if (statusA !== statusB) {
|
|
207
|
+
diffs.push({
|
|
208
|
+
category: DIFF_CATEGORY.FINDINGS,
|
|
209
|
+
severity: DIFF_SEVERITY.BLOCKER,
|
|
210
|
+
reasonCode: DIFF_REASON.FINDING_SEVERITY_CHANGED,
|
|
211
|
+
message: `Finding severity changed: ${statusA} → ${statusB}`,
|
|
212
|
+
artifact: 'findings',
|
|
213
|
+
findingIdentity: identity,
|
|
214
|
+
field: 'severity',
|
|
215
|
+
oldValue: statusA,
|
|
216
|
+
newValue: statusB,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check confidence
|
|
221
|
+
const confA = findingA.confidence || 0;
|
|
222
|
+
const confB = findingB.confidence || 0;
|
|
223
|
+
if (Math.abs(confA - confB) > 0.001) {
|
|
224
|
+
diffs.push({
|
|
225
|
+
category: DIFF_CATEGORY.FINDINGS,
|
|
226
|
+
severity: DIFF_SEVERITY.WARN,
|
|
227
|
+
reasonCode: DIFF_REASON.CONFIDENCE_CHANGED,
|
|
228
|
+
message: `Finding confidence changed: ${confA.toFixed(3)} → ${confB.toFixed(3)}`,
|
|
229
|
+
artifact: 'findings',
|
|
230
|
+
findingIdentity: identity,
|
|
231
|
+
field: 'confidence',
|
|
232
|
+
oldValue: confA,
|
|
233
|
+
newValue: confB,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check confidence reasons
|
|
238
|
+
const reasonsA = findingA.confidenceReasons || [];
|
|
239
|
+
const reasonsB = findingB.confidenceReasons || [];
|
|
240
|
+
if (JSON.stringify(reasonsA.sort()) !== JSON.stringify(reasonsB.sort())) {
|
|
241
|
+
diffs.push({
|
|
242
|
+
category: DIFF_CATEGORY.FINDINGS,
|
|
243
|
+
severity: DIFF_SEVERITY.WARN,
|
|
244
|
+
reasonCode: DIFF_REASON.CONFIDENCE_REASONS_CHANGED,
|
|
245
|
+
message: `Finding confidence reasons changed`,
|
|
246
|
+
artifact: 'findings',
|
|
247
|
+
findingIdentity: identity,
|
|
248
|
+
field: 'confidenceReasons',
|
|
249
|
+
oldValue: reasonsA,
|
|
250
|
+
newValue: reasonsB,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check guardrails
|
|
255
|
+
const guardrailsA = findingA.guardrails;
|
|
256
|
+
const guardrailsB = findingB.guardrails;
|
|
257
|
+
if (guardrailsA || guardrailsB) {
|
|
258
|
+
if (!guardrailsA || !guardrailsB) {
|
|
259
|
+
diffs.push({
|
|
260
|
+
category: DIFF_CATEGORY.FINDINGS,
|
|
261
|
+
severity: DIFF_SEVERITY.WARN,
|
|
262
|
+
reasonCode: DIFF_REASON.GUARDRAILS_CHANGED,
|
|
263
|
+
message: `Finding guardrails presence changed`,
|
|
264
|
+
artifact: 'findings',
|
|
265
|
+
findingIdentity: identity,
|
|
266
|
+
field: 'guardrails',
|
|
267
|
+
});
|
|
268
|
+
} else if (guardrailsA.finalDecision !== guardrailsB.finalDecision) {
|
|
269
|
+
diffs.push({
|
|
270
|
+
category: DIFF_CATEGORY.FINDINGS,
|
|
271
|
+
severity: DIFF_SEVERITY.WARN,
|
|
272
|
+
reasonCode: DIFF_REASON.GUARDRAILS_CHANGED,
|
|
273
|
+
message: `Finding guardrails decision changed: ${guardrailsA.finalDecision} → ${guardrailsB.finalDecision}`,
|
|
274
|
+
artifact: 'findings',
|
|
275
|
+
findingIdentity: identity,
|
|
276
|
+
field: 'guardrails.finalDecision',
|
|
277
|
+
oldValue: guardrailsA.finalDecision,
|
|
278
|
+
newValue: guardrailsB.finalDecision,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check evidence completeness
|
|
284
|
+
const evidenceA = findingA.evidenceCompleteness;
|
|
285
|
+
const evidenceB = findingB.evidenceCompleteness;
|
|
286
|
+
if (evidenceA || evidenceB) {
|
|
287
|
+
if (!evidenceA || !evidenceB) {
|
|
288
|
+
diffs.push({
|
|
289
|
+
category: DIFF_CATEGORY.EVIDENCE,
|
|
290
|
+
severity: DIFF_SEVERITY.BLOCKER,
|
|
291
|
+
reasonCode: DIFF_REASON.EVIDENCE_COMPLETENESS_CHANGED,
|
|
292
|
+
message: `Finding evidence completeness presence changed`,
|
|
293
|
+
artifact: 'findings',
|
|
294
|
+
findingIdentity: identity,
|
|
295
|
+
field: 'evidenceCompleteness',
|
|
296
|
+
});
|
|
297
|
+
} else if (evidenceA.isComplete !== evidenceB.isComplete) {
|
|
298
|
+
diffs.push({
|
|
299
|
+
category: DIFF_CATEGORY.EVIDENCE,
|
|
300
|
+
severity: DIFF_SEVERITY.BLOCKER,
|
|
301
|
+
reasonCode: DIFF_REASON.EVIDENCE_COMPLETENESS_CHANGED,
|
|
302
|
+
message: `Finding evidence completeness changed: ${evidenceA.isComplete} → ${evidenceB.isComplete}`,
|
|
303
|
+
artifact: 'findings',
|
|
304
|
+
findingIdentity: identity,
|
|
305
|
+
field: 'evidenceCompleteness.isComplete',
|
|
306
|
+
oldValue: evidenceA.isComplete,
|
|
307
|
+
newValue: evidenceB.isComplete,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check evidencePackage presence
|
|
313
|
+
const evidencePackageA = findingA.evidencePackage;
|
|
314
|
+
const evidencePackageB = findingB.evidencePackage;
|
|
315
|
+
if (!evidencePackageA && evidencePackageB) {
|
|
316
|
+
diffs.push({
|
|
317
|
+
category: DIFF_CATEGORY.EVIDENCE,
|
|
318
|
+
severity: DIFF_SEVERITY.BLOCKER,
|
|
319
|
+
reasonCode: DIFF_REASON.EVIDENCE_MISSING,
|
|
320
|
+
message: `Finding evidence package missing in first run`,
|
|
321
|
+
artifact: 'findings',
|
|
322
|
+
findingIdentity: identity,
|
|
323
|
+
field: 'evidencePackage',
|
|
324
|
+
});
|
|
325
|
+
} else if (evidencePackageA && !evidencePackageB) {
|
|
326
|
+
diffs.push({
|
|
327
|
+
category: DIFF_CATEGORY.EVIDENCE,
|
|
328
|
+
severity: DIFF_SEVERITY.BLOCKER,
|
|
329
|
+
reasonCode: DIFF_REASON.EVIDENCE_MISSING,
|
|
330
|
+
message: `Finding evidence package missing in second run`,
|
|
331
|
+
artifact: 'findings',
|
|
332
|
+
findingIdentity: identity,
|
|
333
|
+
field: 'evidencePackage',
|
|
334
|
+
});
|
|
335
|
+
} else if (evidencePackageA && evidencePackageB) {
|
|
336
|
+
if (evidencePackageA.isComplete !== evidencePackageB.isComplete) {
|
|
337
|
+
diffs.push({
|
|
338
|
+
category: DIFF_CATEGORY.EVIDENCE,
|
|
339
|
+
severity: DIFF_SEVERITY.BLOCKER,
|
|
340
|
+
reasonCode: DIFF_REASON.EVIDENCE_COMPLETENESS_CHANGED,
|
|
341
|
+
message: `Evidence completeness changed: ${evidencePackageA.isComplete} → ${evidencePackageB.isComplete}`,
|
|
342
|
+
artifact: 'findings',
|
|
343
|
+
findingIdentity: identity,
|
|
344
|
+
field: 'evidencePackage.isComplete',
|
|
345
|
+
oldValue: evidencePackageA.isComplete,
|
|
346
|
+
newValue: evidencePackageB.isComplete,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
const missingA = Array.isArray(evidencePackageA.missingEvidence) ? evidencePackageA.missingEvidence.sort() : [];
|
|
350
|
+
const missingB = Array.isArray(evidencePackageB.missingEvidence) ? evidencePackageB.missingEvidence.sort() : [];
|
|
351
|
+
if (missingA.join('|') !== missingB.join('|')) {
|
|
352
|
+
diffs.push({
|
|
353
|
+
category: DIFF_CATEGORY.EVIDENCE,
|
|
354
|
+
severity: DIFF_SEVERITY.BLOCKER,
|
|
355
|
+
reasonCode: DIFF_REASON.EVIDENCE_MISSING,
|
|
356
|
+
message: `Missing evidence changed`,
|
|
357
|
+
artifact: 'findings',
|
|
358
|
+
findingIdentity: identity,
|
|
359
|
+
field: 'evidencePackage.missingEvidence',
|
|
360
|
+
oldValue: missingA,
|
|
361
|
+
newValue: missingB,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return diffs;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Diff generic artifacts
|
|
371
|
+
*/
|
|
372
|
+
function diffGeneric(artifactA, artifactB, artifactName) {
|
|
373
|
+
const diffs = [];
|
|
374
|
+
|
|
375
|
+
// Simple deep comparison
|
|
376
|
+
const jsonA = JSON.stringify(artifactA, null, 2);
|
|
377
|
+
const jsonB = JSON.stringify(artifactB, null, 2);
|
|
378
|
+
|
|
379
|
+
if (jsonA !== jsonB) {
|
|
380
|
+
diffs.push({
|
|
381
|
+
category: DIFF_CATEGORY.ARTIFACTS,
|
|
382
|
+
severity: DIFF_SEVERITY.WARN,
|
|
383
|
+
reasonCode: DIFF_REASON.FIELD_VALUE_CHANGED,
|
|
384
|
+
message: `Artifact ${artifactName} content changed`,
|
|
385
|
+
artifact: artifactName,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return diffs;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Simple finding identity computation (fallback)
|
|
394
|
+
*/
|
|
395
|
+
function computeFindingIdentitySimple(finding) {
|
|
396
|
+
const parts = [
|
|
397
|
+
finding.type || 'unknown',
|
|
398
|
+
finding.interaction?.type || '',
|
|
399
|
+
finding.interaction?.selector || '',
|
|
400
|
+
finding.expectation?.targetPath || '',
|
|
401
|
+
finding.expectation?.urlPath || '',
|
|
402
|
+
];
|
|
403
|
+
return parts.join('|');
|
|
404
|
+
}
|
|
405
|
+
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 18 — Determinism Engine
|
|
3
|
+
* PHASE 21.2 — Determinism Truth Lock: Enforces HARD verdict
|
|
4
|
+
*
|
|
5
|
+
* Runs the same scan multiple times and compares results for determinism.
|
|
6
|
+
* PHASE 21.2: Also checks DecisionRecorder for adaptive events that break determinism.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, existsSync } from 'fs';
|
|
10
|
+
import { join as _join, resolve } from 'path';
|
|
11
|
+
import { normalizeArtifact } from './normalize.js';
|
|
12
|
+
import { diffArtifacts } from './diff.js';
|
|
13
|
+
import { computeFindingIdentity } from './finding-identity.js';
|
|
14
|
+
import { computeDeterminismVerdict, DETERMINISM_VERDICT } from './contract.js';
|
|
15
|
+
import { DecisionRecorder } from '../determinism-model.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* PHASE 18: Determinism verdict (re-exported from contract for backward compatibility)
|
|
19
|
+
*/
|
|
20
|
+
export { DETERMINISM_VERDICT, DETERMINISM_REASON } from './contract.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* PHASE 18: Run determinism check
|
|
24
|
+
*
|
|
25
|
+
* @param {Function} runFn - Function that executes a scan and returns artifact paths or in-memory artifacts
|
|
26
|
+
* @param {Object} options - Options
|
|
27
|
+
* @param {number} options.runs - Number of runs (default: 2)
|
|
28
|
+
* @param {Object} options.config - Configuration for runs
|
|
29
|
+
* @param {boolean} options.normalize - Whether to normalize artifacts (default: true)
|
|
30
|
+
* @returns {Promise<Object>} { verdict, summary, diffs, runsMeta }
|
|
31
|
+
*/
|
|
32
|
+
export async function runDeterminismCheck(runFn, options = { runs: 2, config: {}, normalize: true }) {
|
|
33
|
+
const { runs = 2, config = {}, normalize = true } = options;
|
|
34
|
+
|
|
35
|
+
const runsMeta = [];
|
|
36
|
+
const runArtifacts = [];
|
|
37
|
+
|
|
38
|
+
// Execute runs
|
|
39
|
+
for (let i = 0; i < runs; i++) {
|
|
40
|
+
const runResult = await runFn(config);
|
|
41
|
+
runsMeta.push({
|
|
42
|
+
runIndex: i + 1,
|
|
43
|
+
runId: runResult.runId || null,
|
|
44
|
+
timestamp: new Date().toISOString(),
|
|
45
|
+
artifactPaths: runResult.artifactPaths || {},
|
|
46
|
+
artifacts: runResult.artifacts || {},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Load artifacts if paths provided
|
|
50
|
+
const artifacts = {};
|
|
51
|
+
if (runResult.artifactPaths) {
|
|
52
|
+
for (const [key, path] of Object.entries(runResult.artifactPaths)) {
|
|
53
|
+
try {
|
|
54
|
+
const content = readFileSync(path, 'utf-8');
|
|
55
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
56
|
+
artifacts[key] = JSON.parse(content);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Artifact not found or invalid
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else if (runResult.artifacts) {
|
|
62
|
+
Object.assign(artifacts, runResult.artifacts);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
runArtifacts.push(artifacts);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Compare runs
|
|
69
|
+
const diffs = [];
|
|
70
|
+
const allArtifacts = new Set();
|
|
71
|
+
|
|
72
|
+
// Collect all artifact names
|
|
73
|
+
for (const artifacts of runArtifacts) {
|
|
74
|
+
for (const key of Object.keys(artifacts)) {
|
|
75
|
+
allArtifacts.add(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Compare each artifact across runs
|
|
80
|
+
for (const artifactName of allArtifacts) {
|
|
81
|
+
const artifacts = runArtifacts.map(run => run[artifactName]);
|
|
82
|
+
|
|
83
|
+
// Normalize if requested
|
|
84
|
+
const normalizedArtifacts = normalize
|
|
85
|
+
? artifacts.map(art => art ? normalizeArtifact(artifactName, art) : null)
|
|
86
|
+
: artifacts;
|
|
87
|
+
|
|
88
|
+
// Compare first run with all subsequent runs
|
|
89
|
+
for (let i = 1; i < normalizedArtifacts.length; i++) {
|
|
90
|
+
const artifactA = normalizedArtifacts[0];
|
|
91
|
+
const artifactB = normalizedArtifacts[i];
|
|
92
|
+
|
|
93
|
+
// Build finding identity map for findings artifacts
|
|
94
|
+
let findingIdentityMap = null;
|
|
95
|
+
if (artifactName === 'findings' && artifactA && artifactB) {
|
|
96
|
+
findingIdentityMap = buildFindingIdentityMap(artifactA, artifactB);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const artifactDiffs = diffArtifacts(artifactA, artifactB, artifactName, findingIdentityMap);
|
|
100
|
+
|
|
101
|
+
// Add run context to diffs
|
|
102
|
+
for (const diff of artifactDiffs) {
|
|
103
|
+
diff.runA = 1;
|
|
104
|
+
diff.runB = i + 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
diffs.push(...artifactDiffs);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// PHASE 21.2: Check for adaptive events in DecisionRecorder (if available)
|
|
112
|
+
// HARD RULE: If adaptive events exist → verdict MUST be NON_DETERMINISTIC
|
|
113
|
+
let adaptiveVerdict = null;
|
|
114
|
+
let adaptiveReasons = [];
|
|
115
|
+
let adaptiveEvents = [];
|
|
116
|
+
|
|
117
|
+
// Try to load decisions.json from first run
|
|
118
|
+
if (runsMeta.length > 0 && runsMeta[0].artifactPaths?.runDir) {
|
|
119
|
+
const runDir = runsMeta[0].artifactPaths.runDir;
|
|
120
|
+
const decisionsPath = resolve(runDir, 'decisions.json');
|
|
121
|
+
|
|
122
|
+
if (existsSync(decisionsPath)) {
|
|
123
|
+
try {
|
|
124
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
125
|
+
const decisionsData = JSON.parse(readFileSync(decisionsPath, 'utf-8'));
|
|
126
|
+
const decisionRecorder = DecisionRecorder.fromExport(decisionsData);
|
|
127
|
+
const adaptiveCheck = computeDeterminismVerdict(decisionRecorder);
|
|
128
|
+
|
|
129
|
+
adaptiveVerdict = adaptiveCheck.verdict;
|
|
130
|
+
adaptiveReasons = adaptiveCheck.reasons;
|
|
131
|
+
adaptiveEvents = adaptiveCheck.adaptiveEvents;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
// Ignore errors reading decisions
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// PHASE 21.2: Determine verdict
|
|
139
|
+
// HARD RULE: If adaptive events exist → verdict MUST be NON_DETERMINISTIC (even if artifacts match)
|
|
140
|
+
const blockerDiffs = diffs.filter(d => d.severity === 'BLOCKER');
|
|
141
|
+
const artifactVerdict = blockerDiffs.length === 0 ? DETERMINISM_VERDICT.DETERMINISTIC : DETERMINISM_VERDICT.NON_DETERMINISTIC;
|
|
142
|
+
|
|
143
|
+
// PHASE 21.2: Final verdict - adaptive events override artifact comparison
|
|
144
|
+
const verdict = (adaptiveVerdict === DETERMINISM_VERDICT.NON_DETERMINISTIC)
|
|
145
|
+
? DETERMINISM_VERDICT.NON_DETERMINISTIC
|
|
146
|
+
: artifactVerdict;
|
|
147
|
+
|
|
148
|
+
// Build summary
|
|
149
|
+
const summary = buildSummary(diffs, runsMeta);
|
|
150
|
+
|
|
151
|
+
// PHASE 21.2: Include adaptive event information in summary
|
|
152
|
+
if (adaptiveVerdict === DETERMINISM_VERDICT.NON_DETERMINISTIC) {
|
|
153
|
+
summary.adaptiveEventsDetected = true;
|
|
154
|
+
summary.adaptiveEventCount = adaptiveEvents.length;
|
|
155
|
+
summary.adaptiveReasons = adaptiveReasons;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
verdict,
|
|
160
|
+
summary,
|
|
161
|
+
diffs,
|
|
162
|
+
runsMeta,
|
|
163
|
+
// PHASE 21.2: Include adaptive event information
|
|
164
|
+
adaptiveVerdict,
|
|
165
|
+
adaptiveReasons,
|
|
166
|
+
adaptiveEvents
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build finding identity map for matching findings across runs
|
|
172
|
+
*/
|
|
173
|
+
function buildFindingIdentityMap(artifactA, artifactB) {
|
|
174
|
+
const map = new Map();
|
|
175
|
+
|
|
176
|
+
const findingsA = artifactA.findings || [];
|
|
177
|
+
const findingsB = artifactB.findings || [];
|
|
178
|
+
|
|
179
|
+
// Build identity for findings in both runs
|
|
180
|
+
for (const finding of [...findingsA, ...findingsB]) {
|
|
181
|
+
const identity = computeFindingIdentity(finding);
|
|
182
|
+
map.set(finding, identity);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return map;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Build summary from diffs
|
|
190
|
+
*/
|
|
191
|
+
function buildSummary(diffs, _runsMeta) {
|
|
192
|
+
const blockerCount = diffs.filter(d => d.severity === 'BLOCKER').length;
|
|
193
|
+
const warnCount = diffs.filter(d => d.severity === 'WARN').length;
|
|
194
|
+
const infoCount = diffs.filter(d => d.severity === 'INFO').length;
|
|
195
|
+
|
|
196
|
+
// Group by reason code
|
|
197
|
+
const reasonCounts = {};
|
|
198
|
+
for (const diff of diffs) {
|
|
199
|
+
const code = diff.reasonCode || 'UNKNOWN';
|
|
200
|
+
reasonCounts[code] = (reasonCounts[code] || 0) + 1;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Top reasons
|
|
204
|
+
const topReasons = Object.entries(reasonCounts)
|
|
205
|
+
.sort((a, b) => b[1] - a[1])
|
|
206
|
+
.slice(0, 5)
|
|
207
|
+
.map(([code, count]) => ({ code, count }));
|
|
208
|
+
|
|
209
|
+
// Stability score (0..1)
|
|
210
|
+
const totalDiffs = diffs.length;
|
|
211
|
+
const stabilityScore = totalDiffs === 0 ? 1.0 : Math.max(0, 1.0 - (blockerCount * 0.5 + warnCount * 0.2 + infoCount * 0.1) / Math.max(1, totalDiffs));
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
totalDiffs,
|
|
215
|
+
blockerCount,
|
|
216
|
+
warnCount,
|
|
217
|
+
infoCount,
|
|
218
|
+
topReasons,
|
|
219
|
+
stabilityScore,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|