Skill Tracer
Skills › gstack › gstack › cso-native-evidence.ts

cso-native-evidence.ts

131 lines · scripts/cso-native-evidence.ts at 3f79d81

1#!/usr/bin/env -S bun --no-env-file --config=/dev/null
2/** Derive native runtime gate evidence from the named Docker tests that actually executed and passed. */
3import * as fs from 'node:fs';
4import * as path from 'node:path';
5import { sha256 } from '../lib/cso/contracts';
6import type { ImageBuildRow } from './cso-image-matrix';
7import { requiredChecks } from './cso-runtime-promotion';
8
9type Stack = ImageBuildRow['stack'];
10export interface TestRef { file: string; name: string }
11export interface JUnitCase extends TestRef { status: 'passed' | 'failed' | 'skipped'; assertions: number }
13const MAX_REPORT_BYTES = 4 * 1024 * 1024;
14const DOCKER = 'test/cso-docker-integration.test.ts';
15const CONTAINMENT: TestRef[] = [
16 { file: DOCKER, name: 'hard fails when local daemon enforcement prerequisites are absent' },
17 { file: DOCKER, name: 'rejects image-declared writable volumes before container creation' },
18 { file: DOCKER, name: 'shares only loopback while denying egress, privileges, and daemon logs, and reads private source/policy mounts' },
19 { file: DOCKER, name: 'machine-wide admission allows only two groups per endpoint' },
20];
21const STAGED: TestRef = { file: DOCKER, name: 'staged runtime executes its trusted verifier and checks every declared tool version' };
22const NODE_LIFECYCLE: TestRef = {
23 file: 'test/cso-node-lifecycle-integration.test.ts',
24 name: 'finds, reproduces, bundles, replays, and closes against current source through the command dispatcher',
25};
26const COLD_JOURNEY: TestRef = {
27 file: 'test/cso-stack-cold-integration.test.ts',
28 name: 'acquires, prepares, boots, controls, and tests the requested staged stack with no target egress',
29};
31/** Each native check key names the tests whose execution proves it for one stack. */
32export function nativeTestRequirements(stack: Stack): Record<string, TestRef[]> {
33 if (stack === 'postgresql') return {
34 containmentPassed: [...CONTAINMENT, STAGED],
35 coldStartPassed: [STAGED],
36 multiDatabasePassed: [STAGED],
37 readinessPassed: [STAGED],
38 };
39 const journey = stack === 'node' ? NODE_LIFECYCLE : COLD_JOURNEY;
40 const application = {
41 containmentPassed: [...CONTAINMENT, STAGED],
42 coldStartPassed: [journey],
43 positiveNegativeAssertionsPassed: [STAGED],
44 acquisitionPublicOnlyPassed: [journey],
45 offlineLifecyclePassed: [journey],
46 };
47 return stack === 'rails'
48 ? { ...application, railsSqlitePassed: [COLD_JOURNEY], railsPostgresqlPassed: [COLD_JOURNEY], nativeExtensionsPassed: [COLD_JOURNEY] }
49 : application;
52function decode(value: string): string {
53 return value.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|amp|lt|gt|quot|apos);/g, (_, entity: string) => {
54 if (entity.startsWith('#x')) return String.fromCodePoint(Number.parseInt(entity.slice(2), 16));
55 if (entity.startsWith('#')) return String.fromCodePoint(Number.parseInt(entity.slice(1), 10));
56 return { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" }[entity]!;
57 });
60/** Parse the testcases of Bun's `--reporter=junit` output. */
61export function parseJUnit(xml: string): JUnitCase[] {
62 if (!/^<\?xml[^>]*\?>\s*<testsuites\b/.test(xml)) throw new Error('INVALID_JUNIT_REPORT');
63 const cases: JUnitCase[] = [];
64 for (const match of xml.matchAll(/<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g)) {
65 const attributes = Object.fromEntries([...match[1].matchAll(/([A-Za-z_:][\w:.-]*)="([^"]*)"/g)].map(([, key, value]) => [key, decode(value)]));
66 const body = match[2] ?? '';
67 const status = /<(?:failure|error)\b/.test(body) ? 'failed' : /<skipped\b/.test(body) ? 'skipped' : 'passed';
68 const assertions = Number(attributes.assertions ?? '0');
69 if (typeof attributes.name !== 'string' || typeof attributes.file !== 'string' || !Number.isInteger(assertions) || assertions < 0) {
70 throw new Error('INVALID_JUNIT_TESTCASE');
71 }
72 cases.push({ file: attributes.file, name: attributes.name, status, assertions });
73 }
74 if (cases.length === 0) throw new Error('EMPTY_JUNIT_REPORT');
75 return cases;
78/** A check passes only when the whole report has no failure and every named test ran once, passed, and asserted. */
79export function deriveNativeChecks(stack: Stack, cases: JUnitCase[]): Record<string, boolean> {
80 const clean = cases.some(item => item.status === 'passed') && !cases.some(item => item.status === 'failed');
81 const passed = (ref: TestRef) => {
82 const matches = cases.filter(item => item.file === ref.file && item.name === ref.name);
83 return matches.length === 1 && matches[0].status === 'passed' && matches[0].assertions > 0;
84 };
85 return Object.fromEntries(Object.entries(nativeTestRequirements(stack))
86 .map(([key, refs]) => [key, clean && refs.every(passed)]));
89/** Native evidence for one staged row; private checks are every required release gate the native job cannot prove. */
90export function nativeEvidence(staged: unknown, stack: Stack, platform: string, report: string): Record<string, unknown> {
91 const record = staged as Record<string, unknown>;
92 if (!record || typeof record !== 'object' || record.state !== 'staged' || record.stack !== stack || record.platform !== platform) {
93 throw new Error('STAGED_EVIDENCE_MISMATCH');
94 }
95 const required = requiredChecks(stack);
96 const nativeChecks = deriveNativeChecks(stack, parseJUnit(report));
97 const failed = Object.entries(nativeChecks).filter(([key, value]) => !value || !required.includes(key)).map(([key]) => key);
98 if (failed.length) throw new Error(`NATIVE_GATE_NOT_PASSED: ${failed.sort().join(', ')}`);
99 return {
100 ...record,
101 state: 'native-gates-passed',
102 nativeChecks,
103 nativeTestReport: `sha256:${sha256(report)}`,
104 privateChecks: Object.fromEntries(required.filter(key => !(key in nativeChecks)).map(key => [key, 'pending'])),
105 qualified: false,
106 promotion: 'prohibited until authenticated private qualification evidence completes every release gate',
107 };
110function readFile(file: string): string {
111 const stat = fs.lstatSync(path.resolve(file));
112 if (!stat.isFile() || stat.isSymbolicLink() || stat.size <= 0 || stat.size > MAX_REPORT_BYTES) throw new Error('UNSAFE_NATIVE_EVIDENCE_INPUT');
113 return fs.readFileSync(path.resolve(file), 'utf8');
116if (import.meta.main) {
117 try {
118 const args = process.argv.slice(2);
119 const names = ['--stack', '--platform', '--junit', '--staged', '--output'];
120 if (args.length !== names.length * 2 || names.some((name, index) => args[index * 2] !== name)) {
121 throw new Error('Usage: cso-native-evidence.ts --stack STACK --platform PLATFORM --junit REPORT.xml --staged staged-image.json --output EVIDENCE.json');
122 }
123 const [stack, platform, junit, staged, output] = names.map((_, index) => args[index * 2 + 1]);
124 if (!['node', 'bun', 'python', 'rails', 'postgresql'].includes(stack)) throw new Error('INVALID_STACK');
125 const evidence = nativeEvidence(JSON.parse(readFile(staged)), stack as Stack, platform, readFile(junit));
126 fs.writeFileSync(path.resolve(output), JSON.stringify(evidence, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
127 } catch (error) {
128 process.stderr.write((error instanceof Error ? error.message : 'NATIVE_EVIDENCE_FAILED') + '\n');
129 process.exitCode = 1;
130 }