@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,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 6A: Artifact Staging, Integrity, & Atomicity
|
|
3
|
+
*
|
|
4
|
+
* Provides production-ready artifact management:
|
|
5
|
+
* - Poison markers to detect incomplete runs
|
|
6
|
+
* - Staging directories for atomic writes
|
|
7
|
+
* - Integrity verification with checksums
|
|
8
|
+
* - Atomic commit of verified artifacts
|
|
9
|
+
* - Rollback with ledger on failures
|
|
10
|
+
*
|
|
11
|
+
* Integration points:
|
|
12
|
+
* 1. initPhase6A() - Called at scan START
|
|
13
|
+
* 2. redirectArtifactWrites() - Intercepts ALL artifact writes
|
|
14
|
+
* 3. completePhase6A() - Called on successful completion
|
|
15
|
+
* 4. rollbackPhase6A() - Called on ANY error
|
|
16
|
+
* 5. checkPoisonMarker() - Called before reading previous runs
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync as _rmSync, renameSync, readdirSync, unlinkSync, statSync } from 'fs';
|
|
20
|
+
import { join, dirname as _dirname } from 'path';
|
|
21
|
+
import { createHash } from 'crypto';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initialize Phase 6A on scan START
|
|
25
|
+
*
|
|
26
|
+
* Actions:
|
|
27
|
+
* 1. Create staging directory
|
|
28
|
+
* 2. Create poison marker
|
|
29
|
+
*
|
|
30
|
+
* @param {string} artifactDir - Artifact directory
|
|
31
|
+
* @returns {Promise<{ success: boolean, poisonMarkerPath?: string, stagingDir?: string, error?: Error }>}
|
|
32
|
+
*/
|
|
33
|
+
export async function initPhase6A(artifactDir) {
|
|
34
|
+
try {
|
|
35
|
+
// Create staging directory if it doesn't exist
|
|
36
|
+
const stagingDir = getStagingPath(artifactDir, '').replace(/\/$/, '');
|
|
37
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
38
|
+
|
|
39
|
+
// Create poison marker indicating scan in progress
|
|
40
|
+
const poisonMarkerPath = getPoisonMarkerPath(artifactDir);
|
|
41
|
+
const poisonContent = JSON.stringify({
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
version: '1.0',
|
|
44
|
+
status: 'in-progress',
|
|
45
|
+
}, null, 2);
|
|
46
|
+
|
|
47
|
+
writeFileSync(poisonMarkerPath, poisonContent, 'utf-8');
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
poisonMarkerPath,
|
|
52
|
+
stagingDir,
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return { success: false, error };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Redirect artifact write to staging directory
|
|
61
|
+
*
|
|
62
|
+
* This function is called for EVERY artifact write.
|
|
63
|
+
* It routes the write to the staging directory instead of the final location.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} artifactDir - Artifact directory
|
|
66
|
+
* @param {string} filename - Artifact filename (e.g., 'summary.json')
|
|
67
|
+
* @returns {string} Path to staging artifact
|
|
68
|
+
*/
|
|
69
|
+
export function redirectArtifactWrites(artifactDir, filename) {
|
|
70
|
+
// Validate artifact name
|
|
71
|
+
validateArtifactPath(artifactDir, filename);
|
|
72
|
+
|
|
73
|
+
// Return staging path
|
|
74
|
+
return getStagingPath(artifactDir, filename);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate artifact path (whitelist approach)
|
|
79
|
+
*
|
|
80
|
+
* @param {string} artifactDir - Artifact directory
|
|
81
|
+
* @param {string} filename - Filename to validate
|
|
82
|
+
* @throws {Error} If filename is not a valid artifact
|
|
83
|
+
*/
|
|
84
|
+
export function validateArtifactPath(artifactDir, filename) {
|
|
85
|
+
const validArtifacts = [
|
|
86
|
+
'summary.json',
|
|
87
|
+
'findings.json',
|
|
88
|
+
'ledger.json',
|
|
89
|
+
'observations.json',
|
|
90
|
+
'report.html',
|
|
91
|
+
'learn.json',
|
|
92
|
+
'manifest.json',
|
|
93
|
+
'observations-legacy.json',
|
|
94
|
+
'observations-legacy-formatted.json',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
if (!validArtifacts.includes(filename)) {
|
|
98
|
+
throw new Error(`Invalid artifact: ${filename}. Must be one of: ${validArtifacts.join(', ')}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Complete Phase 6A on successful run
|
|
104
|
+
*
|
|
105
|
+
* Actions:
|
|
106
|
+
* 1. Generate integrity manifest
|
|
107
|
+
* 2. Verify all artifacts
|
|
108
|
+
* 3. Atomically commit staging to final
|
|
109
|
+
* 4. Remove poison marker
|
|
110
|
+
*
|
|
111
|
+
* @param {string} artifactDir - Artifact directory
|
|
112
|
+
* @returns {Promise<{ success: boolean, verification?: any, error?: Error }>}
|
|
113
|
+
*/
|
|
114
|
+
export async function completePhase6A(artifactDir) {
|
|
115
|
+
try {
|
|
116
|
+
const stagingDir = getStagingPath(artifactDir, '').replace(/\/$/, '');
|
|
117
|
+
|
|
118
|
+
// Check if staging directory exists
|
|
119
|
+
if (!existsSync(stagingDir)) {
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
error: new Error('Staging directory does not exist'),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Discover artifacts in staging
|
|
127
|
+
const allFiles = readdirSync(stagingDir);
|
|
128
|
+
const artifacts = allFiles.filter(f => f.endsWith('.json') && f !== 'integrity.manifest.json');
|
|
129
|
+
|
|
130
|
+
if (artifacts.length === 0) {
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
error: new Error('No artifacts found in staging directory'),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Generate integrity manifest
|
|
138
|
+
const manifestResult = generateIntegrityManifest(stagingDir, artifacts);
|
|
139
|
+
if (manifestResult.errors.length > 0) {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
error: new Error(`Failed to generate integrity manifest: ${manifestResult.errors.join(', ')}`),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Write manifest to staging
|
|
147
|
+
const writeResult = writeIntegrityManifest(stagingDir, manifestResult);
|
|
148
|
+
if (!writeResult.ok) {
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
error: writeResult.error,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Verify all artifacts
|
|
156
|
+
const verification = verifyAllArtifacts(stagingDir, manifestResult);
|
|
157
|
+
|
|
158
|
+
if (!verification.ok) {
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
verification,
|
|
162
|
+
error: new Error(`Artifact integrity verification failed: ${verification.failed.map(f => f.name).join(', ')}`),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Commit staging directory atomically
|
|
167
|
+
await commitStagingDir(artifactDir);
|
|
168
|
+
|
|
169
|
+
// Remove poison marker only after successful commit
|
|
170
|
+
removePoisonMarker(artifactDir);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
success: true,
|
|
174
|
+
verification,
|
|
175
|
+
};
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return { success: false, error };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Rollback Phase 6A on error
|
|
183
|
+
*
|
|
184
|
+
* Actions:
|
|
185
|
+
* 1. Write ledger entry with error details
|
|
186
|
+
* 2. Clean staging artifacts
|
|
187
|
+
* 3. KEEP poison marker (prevents retry)
|
|
188
|
+
*
|
|
189
|
+
* @param {string} artifactDir - Artifact directory
|
|
190
|
+
* @param {Error} error - Error that occurred
|
|
191
|
+
* @returns {Promise<{ success: boolean, ledgerEntry?: any, error?: Error }>}
|
|
192
|
+
*/
|
|
193
|
+
export async function rollbackPhase6A(artifactDir, error) {
|
|
194
|
+
try {
|
|
195
|
+
// Create ledger entry
|
|
196
|
+
const ledgerEntry = createLedgerEntry('error', error);
|
|
197
|
+
|
|
198
|
+
// Append to ledger
|
|
199
|
+
const ledgerPath = join(artifactDir, 'ledger.json');
|
|
200
|
+
let ledger = [];
|
|
201
|
+
|
|
202
|
+
if (existsSync(ledgerPath)) {
|
|
203
|
+
const content = readFileSync(ledgerPath, 'utf-8');
|
|
204
|
+
try {
|
|
205
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
206
|
+
ledger = JSON.parse(content);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
// If ledger is corrupted, start fresh
|
|
209
|
+
ledger = [];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
ledger.push(ledgerEntry);
|
|
214
|
+
writeFileSync(ledgerPath, JSON.stringify(ledger, null, 2), 'utf-8');
|
|
215
|
+
|
|
216
|
+
// Clean staging artifacts but KEEP poison marker
|
|
217
|
+
const stagingDir = getStagingPath(artifactDir, '').replace(/\/$/, '');
|
|
218
|
+
if (existsSync(stagingDir)) {
|
|
219
|
+
const files = readdirSync(stagingDir);
|
|
220
|
+
for (const file of files) {
|
|
221
|
+
const filePath = join(stagingDir, file);
|
|
222
|
+
try {
|
|
223
|
+
if (statSync(filePath).isFile()) {
|
|
224
|
+
unlinkSync(filePath);
|
|
225
|
+
}
|
|
226
|
+
} catch (e) {
|
|
227
|
+
// Ignore file deletion errors
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
success: true,
|
|
234
|
+
ledgerEntry,
|
|
235
|
+
};
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return { success: false, error };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check for poison marker indicating incomplete run
|
|
243
|
+
*
|
|
244
|
+
* This should be called BEFORE reading previous run results to detect
|
|
245
|
+
* incomplete or corrupted previous runs.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} artifactDir - Artifact directory
|
|
248
|
+
* @returns {{ hasPoisonMarker: boolean, entry?: any }}
|
|
249
|
+
*/
|
|
250
|
+
export function checkPoisonMarker(artifactDir) {
|
|
251
|
+
const poisonPath = getPoisonMarkerPath(artifactDir);
|
|
252
|
+
|
|
253
|
+
if (!existsSync(poisonPath)) {
|
|
254
|
+
return { hasPoisonMarker: false };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const content = readFileSync(poisonPath, 'utf-8');
|
|
259
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
260
|
+
const entry = JSON.parse(content);
|
|
261
|
+
return { hasPoisonMarker: true, entry };
|
|
262
|
+
} catch (e) {
|
|
263
|
+
return { hasPoisonMarker: true };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Create ledger entry for a run event
|
|
269
|
+
*
|
|
270
|
+
* @param {string} status - 'success', 'error', or 'partial'
|
|
271
|
+
* @param {Error} error - Error object (if status is 'error')
|
|
272
|
+
* @param {object} metadata - Additional metadata
|
|
273
|
+
* @returns {object} Ledger entry
|
|
274
|
+
*/
|
|
275
|
+
export function createLedgerEntry(status, error, metadata = {}) {
|
|
276
|
+
return {
|
|
277
|
+
timestamp: new Date().toISOString(),
|
|
278
|
+
status,
|
|
279
|
+
error: error ? error.message : undefined,
|
|
280
|
+
stack: error ? error.stack : undefined,
|
|
281
|
+
metadata,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get poison marker path
|
|
287
|
+
*
|
|
288
|
+
* @param {string} artifactDir - Artifact directory
|
|
289
|
+
* @returns {string} Path to poison marker
|
|
290
|
+
*/
|
|
291
|
+
export function getPoisonMarkerPath(artifactDir) {
|
|
292
|
+
return join(artifactDir, '.poison-marker.json');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get staging directory path for artifacts
|
|
297
|
+
*
|
|
298
|
+
* @param {string} artifactDir - Artifact directory
|
|
299
|
+
* @param {string} filename - Optional filename to append
|
|
300
|
+
* @returns {string} Path to staging location
|
|
301
|
+
*/
|
|
302
|
+
export function getStagingPath(artifactDir, filename) {
|
|
303
|
+
const stagingDir = join(artifactDir, '.staging');
|
|
304
|
+
if (filename) {
|
|
305
|
+
return join(stagingDir, filename);
|
|
306
|
+
}
|
|
307
|
+
return stagingDir + '/'; // Return with trailing slash for consistency
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Generate integrity manifest for all artifacts
|
|
312
|
+
*
|
|
313
|
+
* Creates SHA256 checksums for each artifact to detect corruption.
|
|
314
|
+
*
|
|
315
|
+
* @param {string} stagingDir - Staging directory
|
|
316
|
+
* @param {string[]} artifacts - List of artifact filenames
|
|
317
|
+
* @returns {{ checksums: object, generatedAt: string, errors: string[] }}
|
|
318
|
+
*/
|
|
319
|
+
export function generateIntegrityManifest(stagingDir, artifacts) {
|
|
320
|
+
const checksums = {};
|
|
321
|
+
const errors = [];
|
|
322
|
+
|
|
323
|
+
for (const artifact of artifacts) {
|
|
324
|
+
try {
|
|
325
|
+
const filePath = join(stagingDir, artifact);
|
|
326
|
+
if (!existsSync(filePath)) {
|
|
327
|
+
errors.push(`Artifact not found: ${artifact}`);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
332
|
+
if (typeof content !== 'string') {
|
|
333
|
+
errors.push(`Failed to read ${artifact}: content is not a string`);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const hash = createHash('sha256').update(content).digest('hex');
|
|
338
|
+
checksums[artifact] = hash;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
errors.push(`Failed to hash ${artifact}: ${error.message}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
checksums,
|
|
346
|
+
generatedAt: new Date().toISOString(),
|
|
347
|
+
errors,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Write integrity manifest to staging directory
|
|
353
|
+
*
|
|
354
|
+
* @param {string} stagingDir - Staging directory
|
|
355
|
+
* @param {object} manifest - Manifest object
|
|
356
|
+
* @returns {{ ok: boolean, error?: Error }}
|
|
357
|
+
*/
|
|
358
|
+
export function writeIntegrityManifest(stagingDir, manifest) {
|
|
359
|
+
try {
|
|
360
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
361
|
+
return { ok: false, error: new Error('Invalid manifest object') };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const manifestPath = join(stagingDir, 'integrity.manifest.json');
|
|
365
|
+
const manifestContent = JSON.stringify(manifest, null, 2);
|
|
366
|
+
|
|
367
|
+
if (typeof manifestContent !== 'string') {
|
|
368
|
+
return { ok: false, error: new Error('Failed to serialize manifest') };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
writeFileSync(manifestPath, manifestContent, 'utf-8');
|
|
372
|
+
return { ok: true };
|
|
373
|
+
} catch (error) {
|
|
374
|
+
return { ok: false, error };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Verify all artifacts match their checksums in the manifest
|
|
380
|
+
*
|
|
381
|
+
* @param {string} stagingDir - Staging directory
|
|
382
|
+
* @param {object} manifest - Manifest object with checksums
|
|
383
|
+
* @returns {{ ok: boolean, verified: any[], failed: any[] }}
|
|
384
|
+
*/
|
|
385
|
+
export function verifyAllArtifacts(stagingDir, manifest) {
|
|
386
|
+
const verified = [];
|
|
387
|
+
const failed = [];
|
|
388
|
+
|
|
389
|
+
for (const [filename, expectedHash] of Object.entries(manifest.checksums)) {
|
|
390
|
+
try {
|
|
391
|
+
const filePath = join(stagingDir, filename);
|
|
392
|
+
if (!existsSync(filePath)) {
|
|
393
|
+
failed.push({
|
|
394
|
+
name: filename,
|
|
395
|
+
reason: 'File not found',
|
|
396
|
+
});
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
401
|
+
const actualHash = createHash('sha256').update(content).digest('hex');
|
|
402
|
+
|
|
403
|
+
if (actualHash === expectedHash) {
|
|
404
|
+
verified.push({ name: filename });
|
|
405
|
+
} else {
|
|
406
|
+
failed.push({
|
|
407
|
+
name: filename,
|
|
408
|
+
reason: 'Checksum mismatch',
|
|
409
|
+
expected: expectedHash,
|
|
410
|
+
actual: actualHash,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
} catch (error) {
|
|
414
|
+
failed.push({
|
|
415
|
+
name: filename,
|
|
416
|
+
reason: `Verification failed: ${error.message}`,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
ok: failed.length === 0,
|
|
423
|
+
verified,
|
|
424
|
+
failed,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Atomically commit staging directory to final location
|
|
430
|
+
*
|
|
431
|
+
* Uses atomic rename operations to prevent partial writes.
|
|
432
|
+
*
|
|
433
|
+
* @param {string} artifactDir - Artifact directory
|
|
434
|
+
* @returns {Promise<void>}
|
|
435
|
+
*/
|
|
436
|
+
export async function commitStagingDir(artifactDir) {
|
|
437
|
+
const stagingDir = getStagingPath(artifactDir, '').replace(/\/$/, '');
|
|
438
|
+
|
|
439
|
+
if (!existsSync(stagingDir)) {
|
|
440
|
+
throw new Error('Staging directory does not exist');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Get all files in staging
|
|
444
|
+
const files = readdirSync(stagingDir);
|
|
445
|
+
|
|
446
|
+
// Move each file from staging to final location (atomic per-file)
|
|
447
|
+
for (const file of files) {
|
|
448
|
+
const stagingPath = join(stagingDir, file);
|
|
449
|
+
const finalPath = join(artifactDir, file);
|
|
450
|
+
|
|
451
|
+
// Only move actual artifact files, not internal manifest
|
|
452
|
+
if (file === 'integrity.manifest.json') {
|
|
453
|
+
// Manifest stays in staging for verification purposes
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (existsSync(stagingPath)) {
|
|
458
|
+
// Atomic rename: staging -> final
|
|
459
|
+
renameSync(stagingPath, finalPath);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Remove poison marker after successful completion
|
|
466
|
+
*
|
|
467
|
+
* @param {string} artifactDir - Artifact directory
|
|
468
|
+
*/
|
|
469
|
+
export function removePoisonMarker(artifactDir) {
|
|
470
|
+
const poisonPath = getPoisonMarkerPath(artifactDir);
|
|
471
|
+
try {
|
|
472
|
+
if (existsSync(poisonPath)) {
|
|
473
|
+
unlinkSync(poisonPath);
|
|
474
|
+
}
|
|
475
|
+
} catch (error) {
|
|
476
|
+
// Ignore errors removing poison marker
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Discover artifacts in a directory
|
|
482
|
+
*
|
|
483
|
+
* @param {string} dir - Directory to scan
|
|
484
|
+
* @returns {string[]} Array of artifact filenames
|
|
485
|
+
*/
|
|
486
|
+
export function discoverArtifacts(dir) {
|
|
487
|
+
if (!existsSync(dir)) {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
return readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
493
|
+
} catch (error) {
|
|
494
|
+
return [];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 6A Integration Wrapper for Run Command
|
|
3
|
+
*
|
|
4
|
+
* Wraps the run command to add:
|
|
5
|
+
* - Poison markers to detect incomplete runs
|
|
6
|
+
* - Artifact staging for atomic writes
|
|
7
|
+
* - Integrity verification
|
|
8
|
+
* - Atomic commits
|
|
9
|
+
* - Rollback on failure
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { initPhase6A, completePhase6A, rollbackPhase6A, checkPoisonMarker, getStagingPath, redirectArtifactWrites } from './trust-activation-integration.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Wrap run execution with Phase 6A artifact management
|
|
16
|
+
*
|
|
17
|
+
* @param {Function} runFn - Async function that executes the run
|
|
18
|
+
* @param {string} artifactDir - Artifact directory for run
|
|
19
|
+
* @returns {Promise<any>} Result from runFn
|
|
20
|
+
*/
|
|
21
|
+
export async function withPhase6A(runFn, artifactDir) {
|
|
22
|
+
// Check for incomplete previous run
|
|
23
|
+
const poisonCheck = checkPoisonMarker(artifactDir);
|
|
24
|
+
if (poisonCheck.hasPoisonMarker) {
|
|
25
|
+
console.warn('⚠️ WARNING: Incomplete previous run detected (poison marker present)');
|
|
26
|
+
console.warn(' This run may be building on corrupted or incomplete artifacts');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Initialize Phase 6A
|
|
30
|
+
const initResult = await initPhase6A(artifactDir);
|
|
31
|
+
if (!initResult.success) {
|
|
32
|
+
throw new Error(`Phase 6A initialization failed: ${initResult.error.message}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Execute the run function with artifact staging
|
|
37
|
+
const result = await runFn();
|
|
38
|
+
|
|
39
|
+
// Complete Phase 6A on success
|
|
40
|
+
const completeResult = await completePhase6A(artifactDir);
|
|
41
|
+
if (!completeResult.success) {
|
|
42
|
+
throw new Error(`Phase 6A completion failed: ${completeResult.error.message}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
...result,
|
|
47
|
+
phase6a: {
|
|
48
|
+
success: true,
|
|
49
|
+
verification: completeResult.verification,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// Rollback on error
|
|
54
|
+
const rollbackResult = await rollbackPhase6A(artifactDir, error);
|
|
55
|
+
if (!rollbackResult.success) {
|
|
56
|
+
console.error(`Phase 6A rollback failed: ${rollbackResult.error.message}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Re-throw the original error
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create a path redirector for artifact writes
|
|
66
|
+
*
|
|
67
|
+
* This function returns a redirector that can be passed to artifact writers
|
|
68
|
+
* to automatically route writes to staging.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} artifactDir - Artifact directory
|
|
71
|
+
* @returns {Function} Redirector function
|
|
72
|
+
*/
|
|
73
|
+
export function createArtifactPathRedirector(artifactDir) {
|
|
74
|
+
return (filename) => redirectArtifactWrites(artifactDir, filename);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get staging directory path for a run
|
|
79
|
+
*
|
|
80
|
+
* @param {string} runDir - Run directory (e.g., .verax/runs/<runId>)
|
|
81
|
+
* @returns {string} Staging directory path
|
|
82
|
+
*/
|
|
83
|
+
export function getStagingDirectory(runDir) {
|
|
84
|
+
return getStagingPath(runDir, '').replace(/\/$/, '');
|
|
85
|
+
}
|