@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,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 6B: ACTIVATION
|
|
3
|
+
*
|
|
4
|
+
* Activates Phase 6A by enforcing it as MANDATORY for all scan paths.
|
|
5
|
+
*
|
|
6
|
+
* This module wraps Phase 6A and integrates it with run.js to ensure:
|
|
7
|
+
* 1. All scans use Phase 6A (no bypass possible)
|
|
8
|
+
* 2. Artifacts can only be written to staging
|
|
9
|
+
* 3. Integrity is verified before commit
|
|
10
|
+
* 4. Poison markers prevent reading corrupted runs
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { initPhase6A as phase6aInit, completePhase6A as phase6aComplete, rollbackPhase6A as phase6aRollback, checkPoisonMarker } from './trust-activation-integration.js';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
import { existsSync } from 'fs';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize Phase 6A (poison marker + staging directory)
|
|
19
|
+
* MANDATORY at run start - called before any artifact writing
|
|
20
|
+
*
|
|
21
|
+
* @param {string} runDir - Run directory (e.g., .verax/runs/<runId>)
|
|
22
|
+
* @returns {Promise<{ success: boolean, error?: Error }>}
|
|
23
|
+
*/
|
|
24
|
+
export async function initializePhase6A(runDir) {
|
|
25
|
+
try {
|
|
26
|
+
const result = await phase6aInit(runDir);
|
|
27
|
+
return result;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return { success: false, error };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get paths with staging directory redirection
|
|
35
|
+
* MANDATORY - replaces all artifact paths to point to staging directory
|
|
36
|
+
*
|
|
37
|
+
* This ensures ALL artifact writes go to staging instead of final location.
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} paths - Original paths from getRunPaths()
|
|
40
|
+
* @returns {Object} Modified paths with staging redirection
|
|
41
|
+
*/
|
|
42
|
+
export function getStagingRedirectedPaths(paths) {
|
|
43
|
+
const stagingDir = join(paths.baseDir, '.staging');
|
|
44
|
+
|
|
45
|
+
const redirected = { ...paths };
|
|
46
|
+
|
|
47
|
+
// List of artifact path keys that should be redirected to staging
|
|
48
|
+
const artifactKeys = [
|
|
49
|
+
'summary', 'findings', 'ledger', 'learn', 'observe',
|
|
50
|
+
'summaryJson', 'findingsJson', 'learnJson', 'observeJson', 'ledgerJson'
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
for (const key of artifactKeys) {
|
|
54
|
+
if (redirected[key] && typeof redirected[key] === 'string') {
|
|
55
|
+
// Extract just the filename from the path (works on Windows and Unix)
|
|
56
|
+
// Split by both / and \ to handle any path separator
|
|
57
|
+
const parts = redirected[key].replace(/\\/g, '/').split('/');
|
|
58
|
+
const filename = parts[parts.length - 1];
|
|
59
|
+
|
|
60
|
+
if (filename) {
|
|
61
|
+
// Place in staging directory
|
|
62
|
+
redirected[key] = join(stagingDir, filename);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return redirected;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Complete Phase 6A - verify integrity and commit artifacts
|
|
72
|
+
* MANDATORY on success - generates manifest, verifies, commits staging to final
|
|
73
|
+
*
|
|
74
|
+
* @param {string} runDir - Run directory (base, not staging)
|
|
75
|
+
* @returns {Promise<{ success: boolean, verification?: any, error?: Error }>}
|
|
76
|
+
*/
|
|
77
|
+
export async function completePhase6A(runDir) {
|
|
78
|
+
try {
|
|
79
|
+
const result = await phase6aComplete(runDir);
|
|
80
|
+
return result;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return { success: false, error };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Rollback Phase 6A on error
|
|
88
|
+
* MANDATORY in catch block - cleans staging but KEEPS poison marker
|
|
89
|
+
*
|
|
90
|
+
* @param {string} runDir - Run directory (base, not staging)
|
|
91
|
+
* @returns {Promise<{ success: boolean, error?: Error }>}
|
|
92
|
+
*/
|
|
93
|
+
export async function rollbackPhase6A(runDir) {
|
|
94
|
+
try {
|
|
95
|
+
// Pass generic error - the phase6a module will record it
|
|
96
|
+
const error = new Error('Scan execution failed or was interrupted');
|
|
97
|
+
const result = await phase6aRollback(runDir, error);
|
|
98
|
+
return result;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return { success: false, error };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Enforce poison marker check before reading a run
|
|
106
|
+
* MANDATORY before inspect, before loading artifacts, etc
|
|
107
|
+
*
|
|
108
|
+
* Prevents reading from incomplete/corrupted previous runs.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} runDir - Run directory
|
|
111
|
+
* @throws {Error} If poison marker exists
|
|
112
|
+
*/
|
|
113
|
+
export function enforcePoisonCheckBeforeRead(runDir) {
|
|
114
|
+
const poisonCheck = checkPoisonMarker(runDir);
|
|
115
|
+
|
|
116
|
+
if (poisonCheck.hasPoisonMarker) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Cannot read from this run: poison marker present (incomplete/corrupted run). ` +
|
|
119
|
+
`The previous scan did not complete successfully. ` +
|
|
120
|
+
`Artifacts may be incomplete or corrupted. ` +
|
|
121
|
+
`Run 'verax doctor' to diagnose.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return { safe: true };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Verify artifacts before reading (check integrity manifest)
|
|
129
|
+
* RECOMMENDED before loading summary.json, findings.json, etc
|
|
130
|
+
*
|
|
131
|
+
* @param {string} runDir - Run directory
|
|
132
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
133
|
+
*/
|
|
134
|
+
export function verifyArtifactsBeforeRead(runDir) {
|
|
135
|
+
try {
|
|
136
|
+
// Check that integrity manifest exists (in staging directory)
|
|
137
|
+
const stagingDir = join(runDir, '.staging');
|
|
138
|
+
const manifestPath = join(stagingDir, 'integrity.manifest.json');
|
|
139
|
+
|
|
140
|
+
if (!existsSync(manifestPath)) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
error: 'Integrity manifest missing - run may be incomplete or from older version'
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// In future: verify checksums match manifest
|
|
148
|
+
|
|
149
|
+
return { ok: true };
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return { ok: false, error: error.message };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check budget compliance during execution
|
|
157
|
+
* Placeholder for Phase 6C
|
|
158
|
+
*
|
|
159
|
+
* @returns {{ ok: boolean, violations?: string[] }}
|
|
160
|
+
*/
|
|
161
|
+
export function checkBudgetCompliance() {
|
|
162
|
+
// Placeholder for Phase 6C
|
|
163
|
+
return { ok: true };
|
|
164
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 2: Canonical Type System for VERAX
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for all valid types in the analysis pipeline:
|
|
5
|
+
* - EXPECTATION_TYPE: Types of expectations extracted from source code
|
|
6
|
+
* - FINDING_TYPE: Types of findings detected during analysis
|
|
7
|
+
* - SKIP_REASON: Structured reasons why expectations were skipped
|
|
8
|
+
*
|
|
9
|
+
* Validation happens at CREATION TIME, not at enforcement time.
|
|
10
|
+
* No object may be created with an invalid type.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Valid expectation types extracted from source code.
|
|
15
|
+
* These represent what the code is expected to do.
|
|
16
|
+
*/
|
|
17
|
+
export const EXPECTATION_TYPE = Object.freeze({
|
|
18
|
+
// Navigation: Expects a URL change or page navigation
|
|
19
|
+
NAVIGATION: 'navigation',
|
|
20
|
+
|
|
21
|
+
// Network: Expects a network call to be made
|
|
22
|
+
NETWORK: 'network',
|
|
23
|
+
|
|
24
|
+
// State: Expects a state change or promise fulfillment
|
|
25
|
+
STATE: 'state',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Valid finding types detected during analysis.
|
|
30
|
+
* These represent what the code is NOT doing that it should be.
|
|
31
|
+
*/
|
|
32
|
+
export const FINDING_TYPE = Object.freeze({
|
|
33
|
+
// Silent Failures: Expectation not met with no observable effect
|
|
34
|
+
SILENT_FAILURE: 'silent_failure',
|
|
35
|
+
NETWORK_SILENT_FAILURE: 'network_silent_failure',
|
|
36
|
+
VALIDATION_SILENT_FAILURE: 'validation_silent_failure',
|
|
37
|
+
FLOW_SILENT_FAILURE: 'flow_silent_failure',
|
|
38
|
+
DYNAMIC_ROUTE_SILENT_FAILURE: 'dynamic_route_silent_failure',
|
|
39
|
+
|
|
40
|
+
// Observable Breaks: Expectation not met with visible effects
|
|
41
|
+
OBSERVED_BREAK: 'observed_break',
|
|
42
|
+
|
|
43
|
+
// UI Feedback: Missing user feedback after action
|
|
44
|
+
MISSING_FEEDBACK_FAILURE: 'missing_feedback_failure',
|
|
45
|
+
CSS_LOADING_FEEDBACK_FAILURE: 'css_loading_feedback_failure',
|
|
46
|
+
|
|
47
|
+
// Route Issues: Problems with dynamic route detection
|
|
48
|
+
DYNAMIC_ROUTE_MISMATCH: 'dynamic_route_mismatch',
|
|
49
|
+
|
|
50
|
+
// Interactive: Issues with interactive elements
|
|
51
|
+
NAVIGATION_SILENT_FAILURE: 'navigation_silent_failure',
|
|
52
|
+
JOURNEY_STALL_SILENT_FAILURE: 'journey_stall_silent_failure',
|
|
53
|
+
|
|
54
|
+
// Network: Issues with network calls
|
|
55
|
+
MISSING_NETWORK_ACTION: 'missing_network_action',
|
|
56
|
+
NETWORK_SUCCESS_NO_FEEDBACK: 'network_success_no_feedback',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Structured reasons why expectations were skipped (not analyzed).
|
|
61
|
+
* Distinguish between systemic failures and intentional filtering.
|
|
62
|
+
*/
|
|
63
|
+
export const SKIP_REASON = Object.freeze({
|
|
64
|
+
// Systemic Failures: Pipeline cannot continue (force INCOMPLETE state)
|
|
65
|
+
NO_EXPECTATIONS_EXTRACTED: 'NO_EXPECTATIONS_EXTRACTED',
|
|
66
|
+
TIMEOUT_OBSERVE: 'TIMEOUT_OBSERVE',
|
|
67
|
+
TIMEOUT_DETECT: 'TIMEOUT_DETECT',
|
|
68
|
+
TIMEOUT_TOTAL: 'TIMEOUT_TOTAL',
|
|
69
|
+
BUDGET_EXCEEDED: 'BUDGET_EXCEEDED',
|
|
70
|
+
MISSING_SOURCE_DIR: 'MISSING_SOURCE_DIR',
|
|
71
|
+
UNREACHABLE_URL: 'UNREACHABLE_URL',
|
|
72
|
+
|
|
73
|
+
// Intentional Filtering: Skips that don't indicate truncation (COMPLETE is allowed)
|
|
74
|
+
DYNAMIC_ROUTE_UNSUPPORTED: 'DYNAMIC_ROUTE_UNSUPPORTED',
|
|
75
|
+
EXTERNAL_URL_SKIPPED: 'EXTERNAL_URL_SKIPPED',
|
|
76
|
+
PARSE_ERROR: 'PARSE_ERROR',
|
|
77
|
+
UNSUPPORTED_FILE: 'UNSUPPORTED_FILE',
|
|
78
|
+
OBSERVATION_FAILED: 'OBSERVATION_FAILED',
|
|
79
|
+
CONTRACT_VIOLATION: 'CONTRACT_VIOLATION',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate that a type string is a valid expectation type.
|
|
84
|
+
* @param {string} type - The type to validate
|
|
85
|
+
* @returns {boolean} True if valid
|
|
86
|
+
* @throws {Error} If invalid
|
|
87
|
+
*/
|
|
88
|
+
export function validateExpectationType(type) {
|
|
89
|
+
// @ts-expect-error - Runtime string comparison against enum values
|
|
90
|
+
if (!Object.values(EXPECTATION_TYPE).includes(type)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Invalid expectation type: "${type}". Must be one of: ${Object.values(EXPECTATION_TYPE).join(', ')}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validate that a type string is a valid finding type.
|
|
100
|
+
* @param {string} type - The type to validate
|
|
101
|
+
* @returns {boolean} True if valid
|
|
102
|
+
* @throws {Error} If invalid
|
|
103
|
+
*/
|
|
104
|
+
export function validateFindingType(type) {
|
|
105
|
+
// @ts-expect-error - Runtime string comparison against enum values
|
|
106
|
+
if (!Object.values(FINDING_TYPE).includes(type)) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Invalid finding type: "${type}". Must be one of: ${Object.values(FINDING_TYPE).join(', ')}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Validate that a reason is a valid skip reason.
|
|
116
|
+
* @param {string} reason - The reason to validate
|
|
117
|
+
* @returns {string} Normalized valid skip reason
|
|
118
|
+
* @throws {Error} If invalid and cannot be normalized
|
|
119
|
+
*/
|
|
120
|
+
export function validateSkipReason(reason) {
|
|
121
|
+
// CRASH GUARD: Normalize undefined/invalid reasons instead of throwing
|
|
122
|
+
if (reason === undefined || reason === null || reason === 'undefined') {
|
|
123
|
+
console.warn(`[VERAX] Invalid skip reason "${reason}" normalized to OBSERVATION_FAILED`);
|
|
124
|
+
return SKIP_REASON.OBSERVATION_FAILED;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// @ts-expect-error - Runtime string comparison against enum values
|
|
128
|
+
if (!Object.values(SKIP_REASON).includes(reason)) {
|
|
129
|
+
console.warn(`[VERAX] Unknown skip reason "${reason}" normalized to OBSERVATION_FAILED`);
|
|
130
|
+
return SKIP_REASON.OBSERVATION_FAILED;
|
|
131
|
+
}
|
|
132
|
+
return reason;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if a skip reason is a systemic failure.
|
|
137
|
+
* Systemic failures force analysis state to INCOMPLETE.
|
|
138
|
+
* @param {string} reason - The skip reason
|
|
139
|
+
* @returns {boolean} True if this is a systemic failure
|
|
140
|
+
*/
|
|
141
|
+
export function isSystemicFailure(reason) {
|
|
142
|
+
const systemicFailures = [
|
|
143
|
+
SKIP_REASON.NO_EXPECTATIONS_EXTRACTED,
|
|
144
|
+
SKIP_REASON.TIMEOUT_OBSERVE,
|
|
145
|
+
SKIP_REASON.TIMEOUT_DETECT,
|
|
146
|
+
SKIP_REASON.TIMEOUT_TOTAL,
|
|
147
|
+
SKIP_REASON.BUDGET_EXCEEDED,
|
|
148
|
+
SKIP_REASON.MISSING_SOURCE_DIR,
|
|
149
|
+
SKIP_REASON.UNREACHABLE_URL,
|
|
150
|
+
];
|
|
151
|
+
// @ts-expect-error - Runtime string comparison against enum values
|
|
152
|
+
return systemicFailures.includes(reason);
|
|
153
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL Validation Utilities
|
|
3
|
+
* Ensures strict URL format validation to prevent false greens
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function validateUrl(urlString) {
|
|
7
|
+
if (!urlString || typeof urlString !== 'string') {
|
|
8
|
+
throw new Error('URL must be a non-empty string');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const url = new URL(urlString);
|
|
13
|
+
|
|
14
|
+
// Require http or https
|
|
15
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
16
|
+
throw new Error(`Invalid protocol: ${url.protocol} (must be http or https)`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Require hostname
|
|
20
|
+
if (!url.hostname) {
|
|
21
|
+
throw new Error('URL must include a hostname');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check for localhost without port (common misconfiguration)
|
|
25
|
+
if (url.hostname === 'localhost' && !url.port) {
|
|
26
|
+
// This is OK - localhost defaults to port 80/443
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { valid: true, url };
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new Error(`Invalid URL: ${error.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateFlags(flags, allowed) {
|
|
36
|
+
const invalidFlags = flags.filter(flag => !allowed.includes(flag));
|
|
37
|
+
if (invalidFlags.length > 0) {
|
|
38
|
+
throw new Error(`Unknown flag(s): ${invalidFlags.join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -17,11 +17,12 @@ const traverse = _traverse.default || _traverse;
|
|
|
17
17
|
* @param {string} scriptContent - Script block content
|
|
18
18
|
* @param {string} filePath - File path
|
|
19
19
|
* @param {string} relPath - Relative path
|
|
20
|
-
* @param {Object}
|
|
21
|
-
* @param {Object}
|
|
20
|
+
* @param {Object} _scriptBlock - Script block metadata
|
|
21
|
+
* @param {Object} _templateBindings - Template bindings (optional)
|
|
22
|
+
* @ts-expect-error - JSDoc params documented but unused
|
|
22
23
|
* @returns {Array} Navigation promises
|
|
23
24
|
*/
|
|
24
|
-
export function detectVueNavigationPromises(scriptContent, filePath, relPath,
|
|
25
|
+
export function detectVueNavigationPromises(scriptContent, filePath, relPath, _scriptBlock, _templateBindings) {
|
|
25
26
|
const promises = [];
|
|
26
27
|
|
|
27
28
|
try {
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* PHASE 20: Extract Vue SFC blocks
|
|
10
10
|
*
|
|
11
11
|
* @param {string} content - Full .vue file content
|
|
12
|
-
* @param {string} filePath - Path to the .vue file (for context)
|
|
13
12
|
* @returns {Object} { scriptBlocks: [{content, lang, startLine}], template: {content, startLine} }
|
|
14
13
|
*/
|
|
15
14
|
export function extractVueSFC(content) {
|
|
@@ -19,7 +18,7 @@ export function extractVueSFC(content) {
|
|
|
19
18
|
// Extract <script> blocks (including <script setup>)
|
|
20
19
|
const scriptRegex = /<script(?:\s+setup)?(?:\s+lang=["']([^"']+)["'])?[^>]*>([\s\S]*?)<\/script>/gi;
|
|
21
20
|
let scriptMatch;
|
|
22
|
-
let
|
|
21
|
+
let _lineOffset = 1;
|
|
23
22
|
|
|
24
23
|
while ((scriptMatch = scriptRegex.exec(content)) !== null) {
|
|
25
24
|
const isSetup = scriptMatch[0].includes('setup');
|
|
@@ -108,7 +108,7 @@ export function detectVueStatePromises(scriptContent, filePath, relPath, scriptB
|
|
|
108
108
|
const varName = left.object.name;
|
|
109
109
|
|
|
110
110
|
if (refDeclarations.has(varName) && templateVars.has(varName)) {
|
|
111
|
-
const
|
|
111
|
+
const _decl = refDeclarations.get(varName);
|
|
112
112
|
const loc = node.loc;
|
|
113
113
|
const line = loc ? loc.start.line : 1;
|
|
114
114
|
const column = loc ? loc.start.column : 0;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type augmentation for Node.js fs module
|
|
3
|
+
* Provides more precise return types for readFileSync with encoding
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare module 'fs' {
|
|
7
|
+
import { PathLike } from 'fs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Augment readFileSync to return string when encoding is specified
|
|
11
|
+
*/
|
|
12
|
+
export function readFileSync(
|
|
13
|
+
path: PathLike | number,
|
|
14
|
+
options?: { encoding?: null; flag?: string } | null
|
|
15
|
+
): Buffer;
|
|
16
|
+
|
|
17
|
+
export function readFileSync(
|
|
18
|
+
path: PathLike | number,
|
|
19
|
+
options: { encoding: BufferEncoding; flag?: string } | BufferEncoding
|
|
20
|
+
): string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export {};
|
package/src/types/global.d.ts
CHANGED
|
@@ -24,5 +24,142 @@ import type { Page as PlaywrightPage } from 'playwright';
|
|
|
24
24
|
// Re-export for use in JS files
|
|
25
25
|
export type Page = PlaywrightPage;
|
|
26
26
|
|
|
27
|
+
// Node.js built-in module declarations
|
|
28
|
+
declare module 'fs' {
|
|
29
|
+
export interface Stats {
|
|
30
|
+
isDirectory(): boolean;
|
|
31
|
+
isFile(): boolean;
|
|
32
|
+
size: number;
|
|
33
|
+
mtime: Date;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface WriteFileOptions {
|
|
37
|
+
encoding?: BufferEncoding;
|
|
38
|
+
mode?: number;
|
|
39
|
+
flag?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ReadFileOptions {
|
|
43
|
+
encoding?: BufferEncoding | string;
|
|
44
|
+
flag?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RmOptions {
|
|
48
|
+
force?: boolean;
|
|
49
|
+
recursive?: boolean;
|
|
50
|
+
maxRetries?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface MkdirOptions {
|
|
54
|
+
recursive?: boolean;
|
|
55
|
+
mode?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ReaddirOptions {
|
|
59
|
+
encoding?: BufferEncoding | string;
|
|
60
|
+
withFileTypes?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function readFileSync(path: string | Buffer, encoding?: string | ReadFileOptions): string | Buffer;
|
|
64
|
+
export function writeFileSync(path: string, data: string | Buffer, options?: WriteFileOptions | string): void;
|
|
65
|
+
export function existsSync(path: string | Buffer): boolean;
|
|
66
|
+
export function mkdirSync(path: string, options?: MkdirOptions): string | undefined;
|
|
67
|
+
export function rmSync(path: string, options?: RmOptions): void;
|
|
68
|
+
export function readdirSync(path: string, options?: ReaddirOptions | string): string[] | any[];
|
|
69
|
+
export function statSync(path: string): Stats;
|
|
70
|
+
export function renameSync(oldPath: string, newPath: string): void;
|
|
71
|
+
export function unlinkSync(path: string): void;
|
|
72
|
+
export function mkdtempSync(prefix: string): string;
|
|
73
|
+
export function appendFileSync(path: string, data: string | Buffer, options?: WriteFileOptions | string): void;
|
|
74
|
+
export function createWriteStream(path: string, options?: any): any;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
declare module 'path' {
|
|
78
|
+
export function resolve(...paths: string[]): string;
|
|
79
|
+
export function join(...paths: string[]): string;
|
|
80
|
+
export function dirname(p: string): string;
|
|
81
|
+
export function basename(p: string, ext?: string): string;
|
|
82
|
+
export function extname(p: string): string;
|
|
83
|
+
export function relative(from: string, to: string): string;
|
|
84
|
+
export function normalize(p: string): string;
|
|
85
|
+
export const sep: string;
|
|
86
|
+
export const delimiter: string;
|
|
87
|
+
export const posix: {
|
|
88
|
+
resolve: typeof resolve;
|
|
89
|
+
join: typeof join;
|
|
90
|
+
dirname: typeof dirname;
|
|
91
|
+
basename: typeof basename;
|
|
92
|
+
};
|
|
93
|
+
export const win32: {
|
|
94
|
+
resolve: typeof resolve;
|
|
95
|
+
join: typeof join;
|
|
96
|
+
dirname: typeof dirname;
|
|
97
|
+
basename: typeof basename;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
declare module 'crypto' {
|
|
102
|
+
export interface Hash {
|
|
103
|
+
update(data: string | Buffer, encoding?: string): Hash;
|
|
104
|
+
digest(encoding?: string): string | Buffer;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function createHash(algorithm: string): Hash;
|
|
108
|
+
export function randomBytes(size: number): Buffer;
|
|
109
|
+
export function createHmac(algorithm: string, key: string | Buffer): {
|
|
110
|
+
update(data: string | Buffer, encoding?: string): any;
|
|
111
|
+
digest(encoding?: string): string;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
declare module 'os' {
|
|
116
|
+
export function tmpdir(): string;
|
|
117
|
+
export function cwd(): string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
declare module 'url' {
|
|
121
|
+
export function fileURLToPath(url: string | URL): string;
|
|
122
|
+
export class URL {
|
|
123
|
+
constructor(input: string, base?: string | URL);
|
|
124
|
+
href: string;
|
|
125
|
+
protocol: string;
|
|
126
|
+
host: string;
|
|
127
|
+
pathname: string;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
declare module 'http' {
|
|
132
|
+
export interface Server {
|
|
133
|
+
close(callback?: () => void): void;
|
|
134
|
+
listen(port?: number, host?: string, callback?: () => void): void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createServer(requestListener?: (req: any, res: any) => void): Server;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
declare global {
|
|
141
|
+
class Buffer {
|
|
142
|
+
static from(data: string, encoding?: string): Buffer;
|
|
143
|
+
static isBuffer(obj: any): obj is Buffer;
|
|
144
|
+
toString(encoding?: string): string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
namespace NodeJS {
|
|
148
|
+
interface ProcessEnv {
|
|
149
|
+
[key: string]: string | undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface Process {
|
|
153
|
+
env: ProcessEnv;
|
|
154
|
+
argv: string[];
|
|
155
|
+
cwd(): string;
|
|
156
|
+
exit(code?: number): never;
|
|
157
|
+
on(event: string, listener: (...args: any[]) => void): void;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const process: NodeJS.Process;
|
|
162
|
+
}
|
|
163
|
+
|
|
27
164
|
export {};
|
|
28
165
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal type definitions for VERAX
|
|
3
|
+
* Minimal types to satisfy TypeScript checking
|
|
4
|
+
*
|
|
5
|
+
* @typedef {Object} ObserveContext
|
|
6
|
+
* @property {string} url
|
|
7
|
+
* @property {string} projectDir
|
|
8
|
+
* @property {string} runId
|
|
9
|
+
* @property {SilenceTracker} silenceTracker
|
|
10
|
+
* @property {*} decisionRecorder
|
|
11
|
+
* @property {*} scanBudget
|
|
12
|
+
*
|
|
13
|
+
* @typedef {Object} RunState
|
|
14
|
+
* @property {number} totalInteractions
|
|
15
|
+
* @property {number} totalPages
|
|
16
|
+
* @property {number} startTime
|
|
17
|
+
*
|
|
18
|
+
* @typedef {Object} SilenceTracker
|
|
19
|
+
* @property {function(*): void} record
|
|
20
|
+
* @property {function(): *} getSilenceReport
|
|
21
|
+
*
|
|
22
|
+
* @typedef {Object} PageFrontier
|
|
23
|
+
* @property {function(string, *=): void} addPage
|
|
24
|
+
* @property {function(): *} getNextPage
|
|
25
|
+
* @property {function(): boolean} hasPages
|
|
26
|
+
*
|
|
27
|
+
* @typedef {Object} Observation
|
|
28
|
+
* @property {string} type
|
|
29
|
+
* @property {*} data
|
|
30
|
+
* @property {number=} timestamp
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// Export as module
|
|
34
|
+
export {};
|
|
35
|
+
|
|
@@ -14,22 +14,11 @@
|
|
|
14
14
|
export function formatFinding(finding, expectation = null) {
|
|
15
15
|
const lines = [];
|
|
16
16
|
|
|
17
|
-
//
|
|
18
|
-
const confidenceLevel = finding.
|
|
19
|
-
const confidenceScore = finding.confidence
|
|
20
|
-
? (finding.confidence <= 1 ? Math.round(finding.confidence * 100) : finding.confidence)
|
|
21
|
-
: (finding.confidence?.score || 0);
|
|
22
|
-
const confidenceReasons = finding.confidenceReasons || [];
|
|
23
|
-
|
|
17
|
+
// Finding type and confidence
|
|
18
|
+
const confidenceLevel = finding.confidence?.level || 'UNKNOWN';
|
|
19
|
+
const confidenceScore = finding.confidence?.score || 0;
|
|
24
20
|
lines.push(` [${confidenceLevel} (${confidenceScore}%)] ${finding.type || 'unknown'}`);
|
|
25
21
|
|
|
26
|
-
// PHASE 15: Show top confidence reasons
|
|
27
|
-
if (confidenceReasons.length > 0) {
|
|
28
|
-
const topReasons = confidenceReasons.slice(0, 3);
|
|
29
|
-
const reasonText = topReasons.join(', ');
|
|
30
|
-
lines.push(` └─ Confidence: ${reasonText}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
22
|
// Expectation (what was promised)
|
|
34
23
|
if (expectation) {
|
|
35
24
|
let expectationDesc = '';
|
|
@@ -95,48 +84,6 @@ export function formatFinding(finding, expectation = null) {
|
|
|
95
84
|
lines.push(` └─ Source: ${expectation.source.file}${sourceLine}`);
|
|
96
85
|
}
|
|
97
86
|
|
|
98
|
-
// PHASE 16: Show evidence completeness
|
|
99
|
-
if (finding.evidencePackage) {
|
|
100
|
-
if (finding.evidencePackage.isComplete) {
|
|
101
|
-
lines.push(` └─ Evidence: Complete`);
|
|
102
|
-
} else {
|
|
103
|
-
lines.push(` └─ Evidence: Incomplete (missing: ${finding.evidencePackage.missingEvidence.join(', ')})`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// PHASE 16: Show downgrade reason if evidence was incomplete
|
|
108
|
-
if (finding.evidenceCompleteness?.downgraded) {
|
|
109
|
-
lines.push(` └─ Downgraded: ${finding.evidenceCompleteness.downgradeReason}`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// PHASE 17/23: Show guardrails applied and reconciliation
|
|
113
|
-
if (finding.guardrails) {
|
|
114
|
-
const guardrails = finding.guardrails;
|
|
115
|
-
if (guardrails.appliedRules && guardrails.appliedRules.length > 0) {
|
|
116
|
-
const ruleCodes = guardrails.appliedRules.map(r => r.code || r.ruleId).filter(Boolean);
|
|
117
|
-
if (ruleCodes.length > 0) {
|
|
118
|
-
lines.push(` └─ Final status due to guardrails: ${ruleCodes.join(', ')}`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (guardrails.contradictions && guardrails.contradictions.length > 0) {
|
|
122
|
-
lines.push(` └─ Contradiction: ${guardrails.contradictions[0].message}`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// PHASE 23: Show confidence reconciliation if present
|
|
126
|
-
if (guardrails.reconciliation) {
|
|
127
|
-
const recon = guardrails.reconciliation;
|
|
128
|
-
if (recon.confidenceBefore !== recon.confidenceAfter) {
|
|
129
|
-
const beforePercent = Math.round(recon.confidenceBefore * 100);
|
|
130
|
-
const afterPercent = Math.round(recon.confidenceAfter * 100);
|
|
131
|
-
lines.push(` └─ Confidence: ${beforePercent}% → ${afterPercent}% (${recon.confidenceLevelBefore} → ${recon.confidenceLevelAfter})`);
|
|
132
|
-
}
|
|
133
|
-
if (recon.reconciliationReasons && recon.reconciliationReasons.length > 0) {
|
|
134
|
-
const topReasons = recon.reconciliationReasons.slice(0, 2);
|
|
135
|
-
lines.push(` └─ Reconciliation: ${topReasons.join(', ')}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
87
|
return lines.join('\n');
|
|
141
88
|
}
|
|
142
89
|
|