sneakoscope 4.0.0 → 4.0.1
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 +2 -2
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/bin/sks.js +1 -1
- package/dist/core/build/build-once-runner.js +5 -1
- package/dist/core/commands/check-command.js +3 -1
- package/dist/core/commands/mad-sks-command.js +7 -2
- package/dist/core/daemon/sksd-ipc.js +9 -0
- package/dist/core/daemon/sksd.js +37 -5
- package/dist/core/doctor/doctor-dirty-planner.js +70 -5
- package/dist/core/fsx.js +1 -1
- package/dist/core/probes/probe-memoization.js +40 -2
- package/dist/core/release/critical-path-ledger.js +12 -0
- package/dist/core/release/extreme-parallel-scheduler.js +53 -0
- package/dist/core/release/gate-pack-fixture-cache.js +39 -0
- package/dist/core/release/gate-pack-runner.js +145 -1
- package/dist/core/release/release-gate-cache-v2.js +6 -1
- package/dist/core/release/release-gate-dag.js +9 -2
- package/dist/core/triwiki/triwiki-affected-graph.js +63 -7
- package/dist/core/triwiki/triwiki-cache-key.js +27 -7
- package/dist/core/triwiki/triwiki-gate-impact-map.js +47 -2
- package/dist/core/triwiki/triwiki-invalidation.js +81 -0
- package/dist/core/triwiki/triwiki-module-card.js +48 -11
- package/dist/core/triwiki/triwiki-proof-bank.js +82 -4
- package/dist/core/triwiki/triwiki-proof-card.js +34 -3
- package/dist/core/triwiki/triwiki-sla-certificate.js +25 -2
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-launcher.js +13 -0
- package/dist/core/zellij/zellij-worker-pane-manager.js +68 -17
- package/dist/scripts/release-4001-required-gates.js +13 -0
- package/dist/scripts/sizecheck.js +4 -2
- package/package.json +11 -2
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { computeTriWikiCacheKey } from '../triwiki/triwiki-cache-key.js';
|
|
5
5
|
import { createTriWikiProofCard } from '../triwiki/triwiki-proof-card.js';
|
|
6
6
|
import { readReusableTriWikiProofCard, writeTriWikiProofCard } from '../triwiki/triwiki-proof-bank.js';
|
|
7
7
|
import { buildGatePackManifest } from './gate-pack-manifest.js';
|
|
8
|
+
import { prepareGatePackFixture } from './gate-pack-fixture-cache.js';
|
|
8
9
|
import { loadPackageScripts, loadReleaseGateManifest } from '../triwiki/triwiki-gate-impact-map.js';
|
|
9
10
|
export const GATE_PACK_RUNNER_SCHEMA = 'sks.gate-pack-runner.v1';
|
|
10
11
|
export function runGatePack(input) {
|
|
@@ -63,6 +64,11 @@ export function runGatePack(input) {
|
|
|
63
64
|
cache_key: cacheKey.key,
|
|
64
65
|
input_hash: cacheKey.input_hash,
|
|
65
66
|
implementation_hash: cacheKey.implementation_hash,
|
|
67
|
+
gate_impl_hash: cacheKey.implementation_hash,
|
|
68
|
+
package_lock_hash: cacheKey.package_lock_hash,
|
|
69
|
+
release_gates_hash: cacheKey.release_gates_hash,
|
|
70
|
+
env_allowlist_hash: cacheKey.env_allowlist_hash,
|
|
71
|
+
tool_versions: cacheKey.tool_versions,
|
|
66
72
|
tool_version: cacheKey.tool_version,
|
|
67
73
|
fixture_version: cacheKey.fixture_version,
|
|
68
74
|
result: passed ? 'passed' : 'failed',
|
|
@@ -92,6 +98,130 @@ export function runGatePack(input) {
|
|
|
92
98
|
writeGatePackReport(input.root, report);
|
|
93
99
|
return report;
|
|
94
100
|
}
|
|
101
|
+
export async function executeGatePack(input) {
|
|
102
|
+
const mode = input.mode || 'execute';
|
|
103
|
+
const manifest = buildGatePackManifest(input.root);
|
|
104
|
+
const pack = manifest.packs.find((candidate) => candidate.id === input.packId);
|
|
105
|
+
if (!pack) {
|
|
106
|
+
return { schema: GATE_PACK_RUNNER_SCHEMA, ok: false, root: input.root, pack_id: input.packId, mode: mode === 'plan' ? 'plan' : 'execute', reused: 0, executed: 0, failed: 0, proof_paths: [], blockers: ['pack_missing'] };
|
|
107
|
+
}
|
|
108
|
+
const gates = loadReleaseGateManifest(input.root).gates.filter((gate) => pack.gate_ids.includes(gate.id));
|
|
109
|
+
if (mode === 'plan') {
|
|
110
|
+
return { schema: GATE_PACK_RUNNER_SCHEMA, ok: true, root: input.root, pack_id: input.packId, mode: 'plan', reused: 0, executed: 0, failed: 0, proof_paths: [], blockers: [], shared_setup_count: 0, parallelism_gain: 1, critical_path_ms: pack.estimated_ms, reused_proof_count: 0, executed_gate_count: 0 };
|
|
111
|
+
}
|
|
112
|
+
const fixture = await prepareGatePackFixture({ root: input.root, packId: input.packId, fixtureVersion: 'sks-4.0.1' });
|
|
113
|
+
const scripts = loadPackageScripts(input.root);
|
|
114
|
+
const blockers = [];
|
|
115
|
+
const proofPaths = [];
|
|
116
|
+
let reused = 0;
|
|
117
|
+
let executed = 0;
|
|
118
|
+
let failed = 0;
|
|
119
|
+
let sumMs = 0;
|
|
120
|
+
let criticalPathMs = 0;
|
|
121
|
+
const pending = gates.slice();
|
|
122
|
+
const maxParallel = Math.max(1, Math.min(input.maxParallel || 4, pending.length || 1));
|
|
123
|
+
const workers = Array.from({ length: maxParallel }, async () => {
|
|
124
|
+
while (pending.length) {
|
|
125
|
+
const gate = pending.shift();
|
|
126
|
+
if (!gate)
|
|
127
|
+
return;
|
|
128
|
+
const cacheKey = computeTriWikiCacheKey({
|
|
129
|
+
root: input.root,
|
|
130
|
+
id: gate.id,
|
|
131
|
+
inputs: gate.cache.inputs,
|
|
132
|
+
implementationFiles: [`src/scripts/${scriptFileForCommand(gate.command) || ''}`].filter(Boolean),
|
|
133
|
+
envAllowlist: ['CI', 'SKS_FAST_MODE', 'SKS_RELEASE_PRESET'],
|
|
134
|
+
fixtureVersion: fixture.fixture_version
|
|
135
|
+
});
|
|
136
|
+
const hit = readReusableTriWikiProofCard({ root: input.root, subjectId: gate.id, cacheKey: cacheKey.key });
|
|
137
|
+
if (hit.hit) {
|
|
138
|
+
reused += 1;
|
|
139
|
+
if (hit.path)
|
|
140
|
+
proofPaths.push(hit.path);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (mode === 'assert-only') {
|
|
144
|
+
failed += 1;
|
|
145
|
+
blockers.push(`proof_missing:${gate.id}`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const scriptName = scriptNameForCommand(gate.command);
|
|
149
|
+
if (!scriptName || !scripts[scriptName]) {
|
|
150
|
+
failed += 1;
|
|
151
|
+
blockers.push(`script_missing:${gate.id}`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const started = Date.now();
|
|
155
|
+
const run = await spawnNpmScript(input.root, scriptName, input.env);
|
|
156
|
+
const duration = Math.max(0, Date.now() - started);
|
|
157
|
+
sumMs += duration;
|
|
158
|
+
criticalPathMs = Math.max(criticalPathMs, duration);
|
|
159
|
+
executed += 1;
|
|
160
|
+
const passed = run.status === 0;
|
|
161
|
+
if (!passed) {
|
|
162
|
+
failed += 1;
|
|
163
|
+
blockers.push(`gate_failed:${gate.id}`);
|
|
164
|
+
}
|
|
165
|
+
const card = createTriWikiProofCard({
|
|
166
|
+
subject_type: 'gate',
|
|
167
|
+
subject_id: gate.id,
|
|
168
|
+
cache_key: cacheKey.key,
|
|
169
|
+
input_hash: cacheKey.input_hash,
|
|
170
|
+
implementation_hash: cacheKey.implementation_hash,
|
|
171
|
+
gate_impl_hash: cacheKey.implementation_hash,
|
|
172
|
+
package_lock_hash: cacheKey.package_lock_hash,
|
|
173
|
+
release_gates_hash: cacheKey.release_gates_hash,
|
|
174
|
+
env_allowlist_hash: cacheKey.env_allowlist_hash,
|
|
175
|
+
tool_versions: cacheKey.tool_versions,
|
|
176
|
+
tool_version: cacheKey.tool_version,
|
|
177
|
+
fixture_version: cacheKey.fixture_version,
|
|
178
|
+
result: passed ? 'passed' : 'failed',
|
|
179
|
+
reusable: passed,
|
|
180
|
+
duration_ms: duration,
|
|
181
|
+
evidence: { status: run.status, stdout_tail: tail(run.stdout), stderr_tail: tail(run.stderr), fixture_path: fixture.run_path },
|
|
182
|
+
invalidation_reasons: passed ? [] : ['gate_failed']
|
|
183
|
+
});
|
|
184
|
+
proofPaths.push(writeTriWikiProofCard(input.root, card));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
await Promise.all(workers);
|
|
188
|
+
const packCard = createTriWikiProofCard({
|
|
189
|
+
subject_type: 'gate-pack',
|
|
190
|
+
subject_id: input.packId,
|
|
191
|
+
cache_key: `pack:${input.packId}:${gates.map((gate) => gate.id).sort().join(',')}`,
|
|
192
|
+
input_hash: computeTriWikiCacheKey({ root: input.root, id: `pack:${input.packId}`, inputs: gates.flatMap((gate) => gate.cache.inputs), fixtureVersion: fixture.fixture_version }).input_hash,
|
|
193
|
+
gate_impl_hash: `pack-runner:${GATE_PACK_RUNNER_SCHEMA}`,
|
|
194
|
+
package_lock_hash: computeTriWikiCacheKey({ root: input.root, id: `pack:${input.packId}:meta`, inputs: [] }).package_lock_hash,
|
|
195
|
+
release_gates_hash: computeTriWikiCacheKey({ root: input.root, id: `pack:${input.packId}:meta`, inputs: [] }).release_gates_hash,
|
|
196
|
+
env_allowlist_hash: computeTriWikiCacheKey({ root: input.root, id: `pack:${input.packId}:meta`, inputs: [], envAllowlist: ['CI', 'SKS_FAST_MODE'] }).env_allowlist_hash,
|
|
197
|
+
tool_versions: { sks: computeTriWikiCacheKey({ root: input.root, id: `pack:${input.packId}:meta`, inputs: [] }).tool_version },
|
|
198
|
+
fixture_version: fixture.fixture_version,
|
|
199
|
+
result: failed === 0 ? 'passed' : 'failed',
|
|
200
|
+
reusable: failed === 0,
|
|
201
|
+
evidence: { gate_count: gates.length, reused, executed, failed },
|
|
202
|
+
invalidation_reasons: failed === 0 ? [] : ['pack_gate_failed']
|
|
203
|
+
});
|
|
204
|
+
proofPaths.push(writeTriWikiProofCard(input.root, packCard));
|
|
205
|
+
const report = {
|
|
206
|
+
schema: GATE_PACK_RUNNER_SCHEMA,
|
|
207
|
+
ok: blockers.length === 0,
|
|
208
|
+
root: input.root,
|
|
209
|
+
pack_id: input.packId,
|
|
210
|
+
mode: 'execute',
|
|
211
|
+
reused,
|
|
212
|
+
executed,
|
|
213
|
+
failed,
|
|
214
|
+
proof_paths: proofPaths,
|
|
215
|
+
blockers,
|
|
216
|
+
shared_setup_count: fixture.setup_count,
|
|
217
|
+
parallelism_gain: criticalPathMs > 0 ? Number((sumMs / criticalPathMs).toFixed(2)) : 1,
|
|
218
|
+
critical_path_ms: criticalPathMs,
|
|
219
|
+
reused_proof_count: reused,
|
|
220
|
+
executed_gate_count: executed
|
|
221
|
+
};
|
|
222
|
+
writeGatePackReport(input.root, report);
|
|
223
|
+
return report;
|
|
224
|
+
}
|
|
95
225
|
function writeGatePackReport(root, report) {
|
|
96
226
|
const file = path.join(root, '.sneakoscope', 'reports', 'gate-pack-runner.json');
|
|
97
227
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -110,4 +240,18 @@ function scriptFileForCommand(command) {
|
|
|
110
240
|
function tail(value, limit = 2000) {
|
|
111
241
|
return value.length > limit ? value.slice(-limit) : value;
|
|
112
242
|
}
|
|
243
|
+
function spawnNpmScript(root, scriptName, env) {
|
|
244
|
+
return new Promise((resolve) => {
|
|
245
|
+
const child = spawn('npm', ['run', scriptName, '--silent'], {
|
|
246
|
+
cwd: root,
|
|
247
|
+
env: { ...process.env, ...(env || {}), CI: process.env.CI || 'true' },
|
|
248
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
249
|
+
});
|
|
250
|
+
let stdout = '';
|
|
251
|
+
let stderr = '';
|
|
252
|
+
child.stdout?.on('data', (chunk) => { stdout = tail(stdout + String(chunk)); });
|
|
253
|
+
child.stderr?.on('data', (chunk) => { stderr = tail(stderr + String(chunk)); });
|
|
254
|
+
child.on('close', (status) => resolve({ status, stdout, stderr }));
|
|
255
|
+
});
|
|
256
|
+
}
|
|
113
257
|
//# sourceMappingURL=gate-pack-runner.js.map
|
|
@@ -171,7 +171,7 @@ export function writeReleaseGateCacheHit(root, gate, durationMs = 0) {
|
|
|
171
171
|
inputs: gate.cache.inputs,
|
|
172
172
|
implementationFiles: ['release-gates.v2.json', `src/scripts/${gate.id.replace(/[:]/g, '-')}-check.ts`],
|
|
173
173
|
envAllowlist: ['CI', 'SKS_FAST_MODE', 'SKS_RELEASE_PRESET'],
|
|
174
|
-
fixtureVersion: 'sks-4.0.
|
|
174
|
+
fixtureVersion: 'sks-4.0.1',
|
|
175
175
|
salt: key
|
|
176
176
|
});
|
|
177
177
|
writeTriWikiProofCard(root, createTriWikiProofCard({
|
|
@@ -180,6 +180,11 @@ export function writeReleaseGateCacheHit(root, gate, durationMs = 0) {
|
|
|
180
180
|
cache_key: key,
|
|
181
181
|
input_hash: triKey.input_hash,
|
|
182
182
|
implementation_hash: triKey.implementation_hash,
|
|
183
|
+
gate_impl_hash: triKey.implementation_hash,
|
|
184
|
+
package_lock_hash: triKey.package_lock_hash,
|
|
185
|
+
release_gates_hash: triKey.release_gates_hash,
|
|
186
|
+
env_allowlist_hash: triKey.env_allowlist_hash,
|
|
187
|
+
tool_versions: triKey.tool_versions,
|
|
183
188
|
tool_version: triKey.tool_version,
|
|
184
189
|
fixture_version: triKey.fixture_version,
|
|
185
190
|
result: 'passed',
|
|
@@ -8,6 +8,7 @@ import { readReleaseGateCacheRecord, releaseGateProofBankFile, writeReleaseGateC
|
|
|
8
8
|
import { RELEASE_GATE_NODE_SCHEMA, validateReleaseGateManifest } from './release-gate-node.js';
|
|
9
9
|
import { countReleaseGateResources, defaultReleaseGateBudget, summarizeReleaseGateBudget } from './release-gate-resource-governor.js';
|
|
10
10
|
import { selectAffectedReleaseGates } from './release-gate-affected-selector.js';
|
|
11
|
+
import { computeTriWikiAffectedGraph } from '../triwiki/triwiki-affected-graph.js';
|
|
11
12
|
import { guardedProcessKill, guardContextForRoute } from '../safety/mutation-guard.js';
|
|
12
13
|
import { createRequestedScopeContract } from '../safety/requested-scope-contract.js';
|
|
13
14
|
import { rmrf } from '../fsx.js';
|
|
@@ -25,7 +26,10 @@ export async function runReleaseGateDag(input) {
|
|
|
25
26
|
const preset = input.preset || 'release';
|
|
26
27
|
const manifest = loadReleaseGateManifest(root);
|
|
27
28
|
const presetGates = selectReleaseGatePreset(manifest, preset);
|
|
28
|
-
const
|
|
29
|
+
const triwikiGraph = input.triwiki !== false && (preset === 'affected' || preset === 'fast' || preset === 'confidence') && input.full !== true
|
|
30
|
+
? computeTriWikiAffectedGraph({ root, tier: preset === 'fast' ? 'affected' : 'confidence', changedSince: input.changedSince || 'auto' })
|
|
31
|
+
: null;
|
|
32
|
+
const affected = (preset === 'affected' || preset === 'fast' || preset === 'confidence') && input.full !== true
|
|
29
33
|
? selectAffectedReleaseGates(root, manifest, presetGates, { changedSince: input.changedSince || 'auto', preset })
|
|
30
34
|
: selectAffectedReleaseGates(root, manifest, presetGates, { full: true, preset });
|
|
31
35
|
const selected = affected.gates;
|
|
@@ -105,7 +109,8 @@ export async function runReleaseGateDag(input) {
|
|
|
105
109
|
report_dir: reportDir,
|
|
106
110
|
failures,
|
|
107
111
|
affected_graph: affectedGraph,
|
|
108
|
-
completion_certificate: completionCertificate
|
|
112
|
+
completion_certificate: completionCertificate,
|
|
113
|
+
triwiki_affected_graph: triwikiGraph
|
|
109
114
|
};
|
|
110
115
|
if (!finished) {
|
|
111
116
|
snapshot.in_progress = true;
|
|
@@ -114,6 +119,8 @@ export async function runReleaseGateDag(input) {
|
|
|
114
119
|
}
|
|
115
120
|
writeReleaseGateJson(path.join(reportDir, 'summary.json'), snapshot);
|
|
116
121
|
writeReleaseGateJson(affectedGraphFile, affectedGraph);
|
|
122
|
+
if (triwikiGraph)
|
|
123
|
+
writeReleaseGateJson(path.join(reportDir, 'triwiki-affected-graph.json'), triwikiGraph);
|
|
117
124
|
writeReleaseGateJson(completionCertificateFile, completionCertificate);
|
|
118
125
|
if (finished) {
|
|
119
126
|
writeReleaseGateJson(path.join(root, '.sneakoscope', 'reports', 'affected-gate-graph.json'), affectedGraph);
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import { DEFAULT_TRIWIKI_MODULE_CARDS, moduleIdsForPath } from './triwiki-module-card.js';
|
|
3
5
|
import { buildTriWikiGateImpactMap } from './triwiki-gate-impact-map.js';
|
|
4
6
|
export const TRIWIKI_AFFECTED_GRAPH_SCHEMA = 'sks.triwiki-affected-graph.v1';
|
|
5
7
|
export function computeTriWikiAffectedGraph(input) {
|
|
6
8
|
const cards = input.cards || DEFAULT_TRIWIKI_MODULE_CARDS;
|
|
7
|
-
const changedFiles = normalizeChangedFiles(
|
|
9
|
+
const changedFiles = normalizeChangedFiles(resolveChangedFiles(input));
|
|
8
10
|
const affectedModules = new Set();
|
|
9
11
|
let conservativeReason = null;
|
|
12
|
+
if (input.full) {
|
|
13
|
+
for (const card of cards)
|
|
14
|
+
affectedModules.add(card.module_id);
|
|
15
|
+
conservativeReason = 'full_release_requested';
|
|
16
|
+
}
|
|
10
17
|
for (const file of changedFiles) {
|
|
11
18
|
const ids = moduleIdsForPath(file, cards);
|
|
12
19
|
for (const id of ids)
|
|
13
20
|
affectedModules.add(id);
|
|
14
|
-
if (ids.includes('unknown'))
|
|
21
|
+
if (!input.full && ids.includes('unknown'))
|
|
15
22
|
conservativeReason = 'unknown_changed_file';
|
|
16
23
|
}
|
|
17
|
-
if (!changedFiles.length)
|
|
24
|
+
if (!input.full && !changedFiles.length)
|
|
18
25
|
affectedModules.add('release');
|
|
19
26
|
if (changedFiles.some((file) => file === 'package.json' || file === 'package-lock.json' || file === 'release-gates.v2.json')) {
|
|
20
27
|
conservativeReason = conservativeReason || 'root_release_surface_changed';
|
|
@@ -22,7 +29,7 @@ export function computeTriWikiAffectedGraph(input) {
|
|
|
22
29
|
affectedModules.add(card.module_id);
|
|
23
30
|
}
|
|
24
31
|
const impactMap = buildTriWikiGateImpactMap(input.root, cards);
|
|
25
|
-
const selected = impactMap.impacts.filter((impact) => impact.modules.some((moduleId) => affectedModules.has(moduleId)) || conservativeReason === 'root_release_surface_changed');
|
|
32
|
+
const selected = impactMap.impacts.filter((impact) => input.full || impact.modules.some((moduleId) => affectedModules.has(moduleId)) || conservativeReason === 'root_release_surface_changed');
|
|
26
33
|
const gatePacks = new Set();
|
|
27
34
|
for (const impact of selected)
|
|
28
35
|
gatePacks.add(impact.gate_pack);
|
|
@@ -37,9 +44,30 @@ export function computeTriWikiAffectedGraph(input) {
|
|
|
37
44
|
gates: selected.map((impact) => impact.gate_id).sort(),
|
|
38
45
|
release_equivalent_within_scope: tier !== 'instant',
|
|
39
46
|
confidence: tier === 'release' ? 'full-release' : tier === 'instant' ? 'instant' : 'affected-release-equivalent',
|
|
40
|
-
conservative_reason: conservativeReason
|
|
47
|
+
conservative_reason: conservativeReason,
|
|
48
|
+
reused_proofs: [],
|
|
49
|
+
invalidated_proofs: selected.map((impact) => impact.gate_id).sort(),
|
|
50
|
+
required_new_proofs: selected.map((impact) => impact.gate_id).sort()
|
|
41
51
|
};
|
|
42
52
|
}
|
|
53
|
+
function resolveChangedFiles(input) {
|
|
54
|
+
if (input.full)
|
|
55
|
+
return ['*full-release*'];
|
|
56
|
+
if (input.changedFiles && input.changedFiles.length)
|
|
57
|
+
return input.changedFiles;
|
|
58
|
+
if (input.baseRef && input.headRef) {
|
|
59
|
+
const diff = gitDiffNameStatus(input.root, input.baseRef, input.headRef);
|
|
60
|
+
if (diff.length)
|
|
61
|
+
return diff;
|
|
62
|
+
}
|
|
63
|
+
if (input.changedSince && input.changedSince !== 'auto') {
|
|
64
|
+
const diff = gitDiffNameStatus(input.root, input.changedSince, 'HEAD');
|
|
65
|
+
if (diff.length)
|
|
66
|
+
return diff;
|
|
67
|
+
}
|
|
68
|
+
const status = gitChangedFiles(input.root);
|
|
69
|
+
return status.length ? status : [];
|
|
70
|
+
}
|
|
43
71
|
function normalizeChangedFiles(files) {
|
|
44
72
|
return [...new Set(files.map((file) => file.trim().replace(/\\/g, '/')).filter(Boolean))].sort();
|
|
45
73
|
}
|
|
@@ -49,8 +77,36 @@ function gitChangedFiles(root) {
|
|
|
49
77
|
return [];
|
|
50
78
|
return String(result.stdout || '')
|
|
51
79
|
.split('\n')
|
|
52
|
-
.
|
|
80
|
+
.flatMap((line) => parseGitStatusPathLine(line.slice(3).trim()))
|
|
53
81
|
.filter(Boolean)
|
|
54
|
-
.
|
|
82
|
+
.filter((file) => file !== '*full-release*');
|
|
83
|
+
}
|
|
84
|
+
function gitDiffNameStatus(root, base, head) {
|
|
85
|
+
const result = spawnSync('git', ['diff', '--name-status', '--find-renames', base, head], { cwd: root, encoding: 'utf8' });
|
|
86
|
+
if (result.status !== 0)
|
|
87
|
+
return [];
|
|
88
|
+
return String(result.stdout || '').split('\n').flatMap((line) => {
|
|
89
|
+
const parts = line.trim().split(/\t+/).filter(Boolean);
|
|
90
|
+
if (!parts.length)
|
|
91
|
+
return [];
|
|
92
|
+
const status = parts[0] || '';
|
|
93
|
+
if (status.startsWith('R') && parts.length >= 3)
|
|
94
|
+
return [parts[1], parts[2]];
|
|
95
|
+
if (status === 'D' && parts[1])
|
|
96
|
+
return [previousPathOrConservative(root, parts[1])];
|
|
97
|
+
return parts[1] ? [parts[1]] : [];
|
|
98
|
+
}).filter(Boolean);
|
|
99
|
+
}
|
|
100
|
+
function parseGitStatusPathLine(line) {
|
|
101
|
+
if (!line)
|
|
102
|
+
return [];
|
|
103
|
+
if (line.includes(' -> ')) {
|
|
104
|
+
const [oldPath, newPath] = line.split(' -> ');
|
|
105
|
+
return [oldPath, newPath].filter((value) => Boolean(value));
|
|
106
|
+
}
|
|
107
|
+
return [line];
|
|
108
|
+
}
|
|
109
|
+
function previousPathOrConservative(root, file) {
|
|
110
|
+
return fs.existsSync(path.join(root, file)) ? file : file;
|
|
55
111
|
}
|
|
56
112
|
//# sourceMappingURL=triwiki-affected-graph.js.map
|
|
@@ -16,9 +16,12 @@ export function computeTriWikiCacheKey(input) {
|
|
|
16
16
|
const sourceFiles = collectInputFiles(root, input.inputs);
|
|
17
17
|
const implementationFiles = collectInputFiles(root, input.implementationFiles || []);
|
|
18
18
|
const packageLock = hashPathIfPresent(root, 'package-lock.json');
|
|
19
|
-
const
|
|
19
|
+
const releaseGates = hashPathIfPresent(root, 'release-gates.v2.json');
|
|
20
|
+
const env = envFingerprint(input.envAllowlist || []);
|
|
21
|
+
const envHash = hashJson(env.records);
|
|
20
22
|
const fixtureVersion = input.fixtureVersion || 'fixture-v1';
|
|
21
23
|
const toolVersion = input.toolVersion || readPackageVersion(root);
|
|
24
|
+
const toolVersions = { sks: toolVersion, ...(input.toolVersions || {}) };
|
|
22
25
|
const inputHash = hashJson(sourceFiles.records);
|
|
23
26
|
const implementationHash = hashJson(implementationFiles.records);
|
|
24
27
|
const key = hashJson({
|
|
@@ -27,9 +30,10 @@ export function computeTriWikiCacheKey(input) {
|
|
|
27
30
|
input_hash: inputHash,
|
|
28
31
|
implementation_hash: implementationHash,
|
|
29
32
|
package_lock_hash: packageLock.hash,
|
|
33
|
+
release_gates_hash: releaseGates.hash,
|
|
30
34
|
env_allowlist_hash: envHash,
|
|
31
35
|
fixture_version: fixtureVersion,
|
|
32
|
-
|
|
36
|
+
tool_versions: toolVersions,
|
|
33
37
|
salt: input.salt || ''
|
|
34
38
|
});
|
|
35
39
|
return {
|
|
@@ -39,11 +43,15 @@ export function computeTriWikiCacheKey(input) {
|
|
|
39
43
|
input_hash: inputHash,
|
|
40
44
|
implementation_hash: implementationHash,
|
|
41
45
|
package_lock_hash: packageLock.hash,
|
|
46
|
+
release_gates_hash: releaseGates.hash,
|
|
42
47
|
env_allowlist_hash: envHash,
|
|
43
48
|
fixture_version: fixtureVersion,
|
|
44
49
|
tool_version: toolVersion,
|
|
50
|
+
tool_versions: toolVersions,
|
|
45
51
|
file_count: sourceFiles.records.length,
|
|
46
|
-
missing_inputs: [...sourceFiles.missing, ...packageLock.missing]
|
|
52
|
+
missing_inputs: [...sourceFiles.missing, ...packageLock.missing, ...releaseGates.missing],
|
|
53
|
+
redacted_env_keys: env.redacted_keys,
|
|
54
|
+
unsupported_globs: [...sourceFiles.unsupported, ...implementationFiles.unsupported]
|
|
47
55
|
};
|
|
48
56
|
}
|
|
49
57
|
export function hashText(value) {
|
|
@@ -55,7 +63,12 @@ export function hashJson(value) {
|
|
|
55
63
|
export function collectInputFiles(root, patterns) {
|
|
56
64
|
const files = new Set();
|
|
57
65
|
const missing = [];
|
|
66
|
+
const unsupported = [];
|
|
58
67
|
for (const pattern of patterns) {
|
|
68
|
+
if (/[{}]/.test(pattern)) {
|
|
69
|
+
unsupported.push(`unsupported_brace_glob:${pattern}`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
59
72
|
const matches = expandInputPattern(root, pattern);
|
|
60
73
|
if (!matches.length) {
|
|
61
74
|
const literal = path.resolve(root, pattern);
|
|
@@ -77,7 +90,7 @@ export function collectInputFiles(root, patterns) {
|
|
|
77
90
|
else if (hashed.record)
|
|
78
91
|
records.push(hashed.record);
|
|
79
92
|
}
|
|
80
|
-
return { records, missing: [...new Set(missing)].sort() };
|
|
93
|
+
return { records, missing: [...new Set(missing)].sort(), unsupported: [...new Set(unsupported)].sort() };
|
|
81
94
|
}
|
|
82
95
|
function expandInputPattern(root, pattern) {
|
|
83
96
|
const normalized = normalizePattern(pattern);
|
|
@@ -158,7 +171,9 @@ function hashPathIfPresent(root, rel) {
|
|
|
158
171
|
}
|
|
159
172
|
if (stat.isSymbolicLink()) {
|
|
160
173
|
const target = fs.readlinkSync(absolute);
|
|
161
|
-
const
|
|
174
|
+
const resolved = path.resolve(path.dirname(absolute), target);
|
|
175
|
+
const outsideRoot = !resolved.startsWith(path.resolve(root) + path.sep);
|
|
176
|
+
const hash = hashJson({ rel, target, mode, outside_root: outsideRoot });
|
|
162
177
|
return { hash, missing: [], record: { path: normalizePattern(rel), hash, size: target.length, mode } };
|
|
163
178
|
}
|
|
164
179
|
const hash = hashFileChunked(absolute, stat.size);
|
|
@@ -188,14 +203,19 @@ function hashFileChunked(file, size) {
|
|
|
188
203
|
return h.digest('hex');
|
|
189
204
|
}
|
|
190
205
|
function envFingerprint(keys) {
|
|
191
|
-
|
|
206
|
+
const redacted = [];
|
|
207
|
+
const records = [...new Set(keys)].sort().map((key) => {
|
|
192
208
|
const value = process.env[key];
|
|
209
|
+
const secret = /SECRET|TOKEN|PASSWORD|KEY|CREDENTIAL/i.test(key);
|
|
210
|
+
if (secret)
|
|
211
|
+
redacted.push(key);
|
|
193
212
|
return {
|
|
194
213
|
key,
|
|
195
214
|
present: value !== undefined,
|
|
196
|
-
fingerprint: value === undefined ? 'missing' : hashText(`${key}:${value}`)
|
|
215
|
+
fingerprint: value === undefined ? 'missing' : secret ? 'redacted-secret' : hashText(`${key}:${hashText(value)}`)
|
|
197
216
|
};
|
|
198
217
|
});
|
|
218
|
+
return { records, redacted_keys: redacted };
|
|
199
219
|
}
|
|
200
220
|
function readPackageVersion(root) {
|
|
201
221
|
try {
|
|
@@ -14,17 +14,27 @@ export function buildTriWikiGateImpactMap(root, cards = DEFAULT_TRIWIKI_MODULE_C
|
|
|
14
14
|
gate_pack: gatePackForGate(gate, modules, cards),
|
|
15
15
|
cache_inputs: gate.cache.inputs,
|
|
16
16
|
resources: gate.resource,
|
|
17
|
+
semantic_dependencies: semanticDependenciesForGate(gate),
|
|
18
|
+
fixture_dependencies: fixtureDependenciesForGate(gate),
|
|
19
|
+
resource_class: gate.resource,
|
|
20
|
+
expected_duration_ms_p50: expectedDurationForGate(gate, 0.5),
|
|
21
|
+
expected_duration_ms_p95: expectedDurationForGate(gate, 0.95),
|
|
17
22
|
orphan: !scriptExistsForGateCommand(gate.command, scripts),
|
|
18
23
|
command: gate.command
|
|
19
24
|
};
|
|
20
25
|
});
|
|
21
|
-
|
|
26
|
+
const packageScriptOrphans = findPackageScriptOrphans(manifest, scripts);
|
|
27
|
+
const map = {
|
|
22
28
|
schema: TRIWIKI_GATE_IMPACT_MAP_SCHEMA,
|
|
23
29
|
root,
|
|
24
30
|
gate_count: impacts.length,
|
|
25
31
|
orphan_count: impacts.filter((impact) => impact.orphan).length,
|
|
26
|
-
|
|
32
|
+
package_script_orphan_count: packageScriptOrphans.length,
|
|
33
|
+
impacts,
|
|
34
|
+
package_script_orphans: packageScriptOrphans
|
|
27
35
|
};
|
|
36
|
+
writeImpactMap(root, map);
|
|
37
|
+
return map;
|
|
28
38
|
}
|
|
29
39
|
export function loadReleaseGateManifest(root) {
|
|
30
40
|
const manifestPath = path.join(root, 'release-gates.v2.json');
|
|
@@ -51,6 +61,9 @@ export function modulesForGate(gate, cards = DEFAULT_TRIWIKI_MODULE_CARDS) {
|
|
|
51
61
|
return [...modules].sort();
|
|
52
62
|
}
|
|
53
63
|
export function gatePackForGate(gate, modules, cards = DEFAULT_TRIWIKI_MODULE_CARDS) {
|
|
64
|
+
const explicitPack = gate.x_sks_pack;
|
|
65
|
+
if (explicitPack)
|
|
66
|
+
return explicitPack;
|
|
54
67
|
if (gate.id.startsWith('triwiki:'))
|
|
55
68
|
return 'triwiki';
|
|
56
69
|
if (gate.id.startsWith('gate-pack:') || gate.id.startsWith('release:'))
|
|
@@ -76,4 +89,36 @@ function inputMatches(pattern, input) {
|
|
|
76
89
|
const cleanInput = input.replace(/\/\*\*.*$/, '');
|
|
77
90
|
return cleanInput === cleanPattern || cleanInput.startsWith(`${cleanPattern}/`);
|
|
78
91
|
}
|
|
92
|
+
function semanticDependenciesForGate(gate) {
|
|
93
|
+
const deps = new Set(gate.deps || []);
|
|
94
|
+
for (const input of gate.cache.inputs || []) {
|
|
95
|
+
if (input.includes('package'))
|
|
96
|
+
deps.add('package-metadata');
|
|
97
|
+
if (input.includes('release-gates'))
|
|
98
|
+
deps.add('release-gate-manifest');
|
|
99
|
+
if (input.includes('src/core/triwiki'))
|
|
100
|
+
deps.add('triwiki-runtime');
|
|
101
|
+
}
|
|
102
|
+
return [...deps].sort();
|
|
103
|
+
}
|
|
104
|
+
function fixtureDependenciesForGate(gate) {
|
|
105
|
+
return (gate.cache.inputs || []).filter((input) => input.includes('fixture') || input.includes('test/')).sort();
|
|
106
|
+
}
|
|
107
|
+
function expectedDurationForGate(gate, quantile) {
|
|
108
|
+
const resources = new Set(gate.resource || []);
|
|
109
|
+
const base = resources.has('cpu-heavy') ? 45_000 : resources.has('remote-model-real') ? 90_000 : 12_000;
|
|
110
|
+
return quantile === 0.95 ? Math.round(base * 2.5) : base;
|
|
111
|
+
}
|
|
112
|
+
function findPackageScriptOrphans(manifest, scripts) {
|
|
113
|
+
const releaseScripts = new Set(manifest.gates.map((gate) => gate.command.match(/^npm run ([^ ]+)/)?.[1]).filter((value) => Boolean(value)));
|
|
114
|
+
return Object.keys(scripts)
|
|
115
|
+
.filter((name) => /^(triwiki|gate-pack|scheduler|release|doctor|legacy|orphan|sks:401|certificate|build-once|sksd|probes):/.test(name))
|
|
116
|
+
.filter((name) => !releaseScripts.has(name))
|
|
117
|
+
.sort();
|
|
118
|
+
}
|
|
119
|
+
function writeImpactMap(root, map) {
|
|
120
|
+
const file = path.join(root, '.sneakoscope', 'reports', 'triwiki-gate-impact-map.json');
|
|
121
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
122
|
+
fs.writeFileSync(file, `${JSON.stringify(map, null, 2)}\n`);
|
|
123
|
+
}
|
|
79
124
|
//# sourceMappingURL=triwiki-gate-impact-map.js.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { markTriWikiProofInvalidated, triWikiProofBankDir } from './triwiki-proof-bank.js';
|
|
4
|
+
export const TRIWIKI_PROOF_INVALIDATION_SCHEMA = 'sks.triwiki-proof-invalidation.v1';
|
|
5
|
+
export async function invalidateTriWikiProofsForChange(input) {
|
|
6
|
+
const broad = input.changedFiles.some((file) => ['package.json', 'package-lock.json', 'release-gates.v2.json'].includes(file));
|
|
7
|
+
const affectedGates = new Set(input.affectedGates);
|
|
8
|
+
const affectedPacks = new Set();
|
|
9
|
+
const invalidated = [];
|
|
10
|
+
let unaffected = 0;
|
|
11
|
+
for (const file of walkProofs(input.root)) {
|
|
12
|
+
const parsed = readProofIdentity(file);
|
|
13
|
+
if (!parsed)
|
|
14
|
+
continue;
|
|
15
|
+
const hit = broad || affectedGates.has(parsed.subject_id) || input.affectedModules.includes(parsed.subject_id);
|
|
16
|
+
if (!hit) {
|
|
17
|
+
unaffected += 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (parsed.subject_type === 'gate-pack')
|
|
21
|
+
affectedPacks.add(parsed.subject_id);
|
|
22
|
+
if (markTriWikiProofInvalidated(input.root, parsed.subject_id, parsed.proof_id, input.reason, pluralSubject(parsed.subject_type))) {
|
|
23
|
+
invalidated.push(parsed.proof_id);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const report = {
|
|
27
|
+
schema: TRIWIKI_PROOF_INVALIDATION_SCHEMA,
|
|
28
|
+
root: input.root,
|
|
29
|
+
reason: input.reason,
|
|
30
|
+
broad,
|
|
31
|
+
affected_gates: [...affectedGates].sort(),
|
|
32
|
+
affected_packs: [...affectedPacks].sort(),
|
|
33
|
+
invalidated_proofs: invalidated.sort(),
|
|
34
|
+
unaffected_proofs: unaffected
|
|
35
|
+
};
|
|
36
|
+
const out = path.join(input.root, '.sneakoscope', 'reports', 'triwiki-proof-invalidation.json');
|
|
37
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
38
|
+
fs.writeFileSync(out, `${JSON.stringify(report, null, 2)}\n`);
|
|
39
|
+
return report;
|
|
40
|
+
}
|
|
41
|
+
function walkProofs(root) {
|
|
42
|
+
const base = triWikiProofBankDir(root);
|
|
43
|
+
if (!fs.existsSync(base))
|
|
44
|
+
return [];
|
|
45
|
+
const out = [];
|
|
46
|
+
const stack = [base];
|
|
47
|
+
while (stack.length) {
|
|
48
|
+
const dir = stack.pop();
|
|
49
|
+
if (!dir)
|
|
50
|
+
continue;
|
|
51
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
52
|
+
const absolute = path.join(dir, entry.name);
|
|
53
|
+
if (entry.isDirectory())
|
|
54
|
+
stack.push(absolute);
|
|
55
|
+
else if (entry.isFile() && entry.name.endsWith('.json'))
|
|
56
|
+
out.push(absolute);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function readProofIdentity(file) {
|
|
62
|
+
try {
|
|
63
|
+
const json = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
64
|
+
if (!json.subject_type || !json.subject_id || !json.proof_id)
|
|
65
|
+
return null;
|
|
66
|
+
return { subject_type: json.subject_type, subject_id: json.subject_id, proof_id: json.proof_id };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function pluralSubject(value) {
|
|
73
|
+
if (value === 'gate')
|
|
74
|
+
return 'gates';
|
|
75
|
+
if (value === 'gate-pack')
|
|
76
|
+
return 'gate-packs';
|
|
77
|
+
if (value === 'module')
|
|
78
|
+
return 'modules';
|
|
79
|
+
return 'pipelines';
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=triwiki-invalidation.js.map
|