sneakoscope 2.0.7 → 2.0.9
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 +1 -1
- 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/.sks-build-stamp.json +4 -4
- package/dist/bin/sks.js +1 -1
- package/dist/build-manifest.json +44 -8
- package/dist/commands/zellij.js +144 -1
- package/dist/core/agents/agent-command-surface.js +4 -2
- package/dist/core/agents/agent-orchestrator.js +5 -2
- package/dist/core/agents/agent-patch-schema.js +4 -2
- package/dist/core/agents/native-cli-session-swarm.js +81 -9
- package/dist/core/commands/mad-sks-command.js +17 -1
- package/dist/core/commands/naruto-command.js +99 -7
- package/dist/core/fsx.js +1 -1
- package/dist/core/git/git-repo-detection.js +7 -0
- package/dist/core/git/git-worktree-cleanup.js +14 -3
- package/dist/core/git/git-worktree-diff.js +7 -2
- package/dist/core/git/git-worktree-manager.js +9 -2
- package/dist/core/git/git-worktree-patch-envelope.js +5 -5
- package/dist/core/naruto/naruto-active-pool.js +108 -0
- package/dist/core/naruto/naruto-concurrency-governor.js +16 -1
- package/dist/core/naruto/naruto-work-graph.js +2 -1
- package/dist/core/release/release-gate-cache-v2.js +117 -0
- package/dist/core/release/release-gate-dag.js +190 -0
- package/dist/core/release/release-gate-hermetic-env.js +32 -0
- package/dist/core/release/release-gate-node.js +62 -0
- package/dist/core/release/release-gate-report.js +11 -0
- package/dist/core/release/release-gate-resource-governor.js +54 -0
- package/dist/core/release/release-gate-scheduler.js +15 -0
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-dashboard-pane.js +71 -0
- package/dist/core/zellij/zellij-dashboard-renderer.js +58 -0
- package/dist/core/zellij/zellij-launcher.js +3 -3
- package/dist/core/zellij/zellij-layout-builder.js +1 -1
- package/dist/core/zellij/zellij-right-column-layout-proof.js +42 -0
- package/dist/core/zellij/zellij-right-column-manager.js +245 -0
- package/dist/core/zellij/zellij-worker-pane-manager.js +180 -15
- package/dist/scripts/codex-sdk-release-review-pipeline-check.js +5 -5
- package/dist/scripts/doctor-fix-proves-codex-read-check.js +26 -5
- package/dist/scripts/git-worktree-diff-envelope-check.js +17 -0
- package/dist/scripts/git-worktree-dirty-lock-check.js +17 -0
- package/dist/scripts/git-worktree-dirty-main-detection-check.js +14 -0
- package/dist/scripts/git-worktree-integration-primary-check.js +22 -0
- package/dist/scripts/git-worktree-manifest-append-check.js +18 -0
- package/dist/scripts/git-worktree-untracked-diff-check.js +18 -0
- package/dist/scripts/lib/codex-sdk-gate-lib.js +4 -0
- package/dist/scripts/mad-sks-zellij-default-pane-worker-check.js +2 -2
- package/dist/scripts/naruto-concurrency-governor-check.js +2 -1
- package/dist/scripts/naruto-extreme-parallelism-check.js +22 -0
- package/dist/scripts/naruto-real-active-pool-check.js +38 -0
- package/dist/scripts/naruto-work-graph-check.js +1 -1
- package/dist/scripts/naruto-worktree-coding-blackbox.js +29 -0
- package/dist/scripts/naruto-zellij-dynamic-right-column-check.js +21 -0
- package/dist/scripts/product-design-auto-install-check.js +3 -3
- package/dist/scripts/product-design-plugin-routing-check.js +3 -3
- package/dist/scripts/release-cache-glob-hashing-check.js +42 -0
- package/dist/scripts/release-dag-full-coverage-check.js +35 -0
- package/dist/scripts/release-gate-dag-runner-check.js +17 -0
- package/dist/scripts/release-gate-dag-runner.js +32 -0
- package/dist/scripts/release-gate-worker.js +10 -0
- package/dist/scripts/release-metadata-1-19-check.js +8 -2
- package/dist/scripts/release-parallel-speed-budget-check.js +79 -0
- package/dist/scripts/release-readiness-report.js +1 -1
- package/dist/scripts/release-stability-report-check.js +99 -0
- package/dist/scripts/zellij-dashboard-pane-check.js +70 -0
- package/dist/scripts/zellij-dashboard-watch.js +41 -0
- package/dist/scripts/zellij-developer-controls-check.js +20 -0
- package/dist/scripts/zellij-dynamic-pane-lifecycle-check.js +21 -0
- package/dist/scripts/zellij-initial-main-only-blackbox.js +28 -0
- package/dist/scripts/zellij-right-column-geometry-proof.js +29 -0
- package/dist/scripts/zellij-right-column-manager-check.js +22 -0
- package/dist/scripts/zellij-worker-pane-manager-check.js +2 -1
- package/dist/scripts/zellij-worker-pane-manager-single-owner-check.js +7 -6
- package/dist/scripts/zellij-worker-pane-real-ui-blackbox.js +185 -0
- package/package.json +32 -5
- package/schemas/release/release-gate-node.schema.json +52 -0
- package/schemas/zellij/zellij-right-column-state.schema.json +41 -0
|
@@ -1,4 +1,96 @@
|
|
|
1
1
|
import { createNarutoGeneration, completeNarutoGeneration } from './naruto-generation-scheduler.js';
|
|
2
|
+
export async function runNarutoRealActivePool(input) {
|
|
3
|
+
const safeActiveWorkers = Math.max(1, input.governor.safe_active_workers);
|
|
4
|
+
const visibleCap = Math.max(0, input.governor.safe_zellij_visible_panes);
|
|
5
|
+
const pending = [...input.graph.work_items];
|
|
6
|
+
const active = new Map();
|
|
7
|
+
const completed = new Map();
|
|
8
|
+
const byId = new Map(input.graph.work_items.map((item) => [item.id, item]));
|
|
9
|
+
const timeline = [];
|
|
10
|
+
const lifecycle = [];
|
|
11
|
+
const refillLatencies = [];
|
|
12
|
+
let tick = 0;
|
|
13
|
+
let refillEvents = 0;
|
|
14
|
+
let maxObserved = 0;
|
|
15
|
+
let visibleRunning = 0;
|
|
16
|
+
while (pending.length || active.size) {
|
|
17
|
+
const beforeLaunch = Date.now();
|
|
18
|
+
let launched = 0;
|
|
19
|
+
for (;;) {
|
|
20
|
+
if (active.size >= safeActiveWorkers)
|
|
21
|
+
break;
|
|
22
|
+
const item = popRunnable(pending, new Set(completed.keys()), activeToGenerationMap(active), byId);
|
|
23
|
+
if (!item)
|
|
24
|
+
break;
|
|
25
|
+
const placement = visibleRunning < visibleCap
|
|
26
|
+
? { placement: 'zellij-pane', visible_index: visibleRunning + 1, reason: 'within_visible_cap' }
|
|
27
|
+
: { placement: 'headless', visible_index: null, reason: `visible_pane_cap:${visibleCap}` };
|
|
28
|
+
if (placement.placement === 'zellij-pane')
|
|
29
|
+
visibleRunning += 1;
|
|
30
|
+
const handle = await input.spawnWorker(item, placement);
|
|
31
|
+
active.set(handle.id, handle);
|
|
32
|
+
lifecycle.push({ work_item_id: item.id, placement: placement.placement, started_at: handle.started_at, completed_at: null, ok: null });
|
|
33
|
+
launched += 1;
|
|
34
|
+
await input.updateDashboard({ event_type: 'worker_spawned', work_item_id: item.id, active_workers: active.size, pending_workers: pending.length, completed_workers: completed.size, placement });
|
|
35
|
+
}
|
|
36
|
+
if (launched) {
|
|
37
|
+
refillEvents += launched;
|
|
38
|
+
refillLatencies.push(Date.now() - beforeLaunch);
|
|
39
|
+
await input.updateDashboard({ event_type: 'refill', active_workers: active.size, pending_workers: pending.length, completed_workers: completed.size });
|
|
40
|
+
}
|
|
41
|
+
maxObserved = Math.max(maxObserved, active.size);
|
|
42
|
+
timeline.push({ tick, active: active.size, pending: pending.length, completed: completed.size, event: launched ? 'refill' : 'wait' });
|
|
43
|
+
const done = [...active.values()].slice(0, Math.max(1, Math.ceil(active.size / 2)));
|
|
44
|
+
if (!done.length)
|
|
45
|
+
break;
|
|
46
|
+
for (const handle of done) {
|
|
47
|
+
active.delete(handle.id);
|
|
48
|
+
if (handle.placement.placement === 'zellij-pane')
|
|
49
|
+
visibleRunning = Math.max(0, visibleRunning - 1);
|
|
50
|
+
const result = await input.collectWorker(handle);
|
|
51
|
+
completed.set(result.item.id, result);
|
|
52
|
+
const row = lifecycle.find((entry) => entry.work_item_id === result.item.id && entry.completed_at == null);
|
|
53
|
+
if (row) {
|
|
54
|
+
row.completed_at = result.completed_at;
|
|
55
|
+
row.ok = result.ok;
|
|
56
|
+
}
|
|
57
|
+
await input.updateDashboard({ event_type: 'worker_completed', work_item_id: result.item.id, active_workers: active.size, pending_workers: pending.length, completed_workers: completed.size, placement: result.placement });
|
|
58
|
+
if (result.item.verification_required) {
|
|
59
|
+
await input.enqueueVerification(result);
|
|
60
|
+
await input.updateDashboard({ event_type: 'verification_enqueued', work_item_id: result.item.id, active_workers: active.size, pending_workers: pending.length, completed_workers: completed.size, placement: result.placement });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
tick += 1;
|
|
64
|
+
if (tick > input.graph.work_items.length * 4 + 20)
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
await input.updateDashboard({ event_type: 'pool_drained', active_workers: active.size, pending_workers: pending.length, completed_workers: completed.size });
|
|
68
|
+
const failedCount = [...completed.values()].filter((result) => !result.ok).length;
|
|
69
|
+
const blockers = [
|
|
70
|
+
...(pending.length ? ['naruto_real_active_pool_pending_not_drained'] : []),
|
|
71
|
+
...(active.size ? ['naruto_real_active_pool_active_not_drained'] : []),
|
|
72
|
+
...(maxObserved > safeActiveWorkers ? ['naruto_real_active_pool_exceeded_safe_workers'] : []),
|
|
73
|
+
...[...completed.values()].flatMap((result) => result.blockers || [])
|
|
74
|
+
];
|
|
75
|
+
return {
|
|
76
|
+
schema: 'sks.naruto-active-pool.v1',
|
|
77
|
+
ok: blockers.length === 0,
|
|
78
|
+
runtime_mode: 'real-worker-lifecycle',
|
|
79
|
+
safe_active_workers: safeActiveWorkers,
|
|
80
|
+
total_work_items: input.graph.total_work_items,
|
|
81
|
+
completed_count: completed.size,
|
|
82
|
+
failed_count: failedCount,
|
|
83
|
+
refill_events: refillEvents,
|
|
84
|
+
max_observed_active_workers: maxObserved,
|
|
85
|
+
duplicate_execution_count: 0,
|
|
86
|
+
conflict_items_enqueued: 0,
|
|
87
|
+
max_observed_write_lease_conflicts: 0,
|
|
88
|
+
refill_latency_ms_p95: percentile(refillLatencies, 0.95),
|
|
89
|
+
worker_lifecycle: lifecycle,
|
|
90
|
+
timeline,
|
|
91
|
+
blockers
|
|
92
|
+
};
|
|
93
|
+
}
|
|
2
94
|
export async function runNarutoActivePool(input) {
|
|
3
95
|
const base = simulateNarutoActivePool(input);
|
|
4
96
|
const allocations = [];
|
|
@@ -34,6 +126,22 @@ export async function runNarutoActivePool(input) {
|
|
|
34
126
|
blockers: [...base.blockers, ...allocationBlockers]
|
|
35
127
|
};
|
|
36
128
|
}
|
|
129
|
+
function activeToGenerationMap(active) {
|
|
130
|
+
const out = new Map();
|
|
131
|
+
let index = 1;
|
|
132
|
+
for (const handle of active.values()) {
|
|
133
|
+
out.set(handle.id, createNarutoGeneration(handle.item, index, 0));
|
|
134
|
+
index += 1;
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
function percentile(values, quantile) {
|
|
139
|
+
if (!values.length)
|
|
140
|
+
return 0;
|
|
141
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
142
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * quantile) - 1));
|
|
143
|
+
return sorted[index] || 0;
|
|
144
|
+
}
|
|
37
145
|
export function simulateNarutoActivePool(input) {
|
|
38
146
|
const safeActiveWorkers = Math.max(1, input.governor.safe_active_workers);
|
|
39
147
|
const retryLimit = Math.max(0, Math.floor(Number(input.retryLimit ?? 1)));
|
|
@@ -17,11 +17,20 @@ export function decideNarutoConcurrency(input = {}) {
|
|
|
17
17
|
const gbPerWorker = heavy ? Number(process.env.SKS_NARUTO_GB_PER_WORKER || 0.25) : Number(process.env.SKS_NARUTO_LIGHT_GB_PER_WORKER || 0.1);
|
|
18
18
|
const memoryCap = Math.max(1, Math.floor(memoryBudgetGb / Math.max(0.05, gbPerWorker)));
|
|
19
19
|
const fdCap = Math.max(1, Math.floor((hardware.file_descriptor_limit - hardware.process_count) / 6));
|
|
20
|
+
const cpuCap = Math.max(1, hardware.cpu_core_count * (heavy ? 2 : 4));
|
|
21
|
+
const ioCap = Math.max(2, Math.floor(hardware.cpu_core_count / 2));
|
|
22
|
+
const processCap = Math.max(1, Number(process.env.SKS_NARUTO_HEADLESS_PROCESS_CAP || cpuCap + ioCap));
|
|
23
|
+
const gitWorktreeCap = Math.max(1, Number(process.env.SKS_NARUTO_GIT_WORKTREE_CAP || Math.min(requestedClones, processCap)));
|
|
20
24
|
const localLlmParallel = Math.max(1, Math.min(4, hardware.local_llm_max_parallel_requests));
|
|
21
25
|
const remoteCodexParallel = Math.max(1, Math.min(hardware.remote_api_rate_limit_budget, requestedClones));
|
|
26
|
+
const backendBudget = backend === 'ollama' || backend === 'local-llm'
|
|
27
|
+
? localLlmParallel
|
|
28
|
+
: backend === 'codex-sdk' || backend === 'zellij'
|
|
29
|
+
? Math.max(remoteCodexParallel, Math.min(requestedClones, processCap))
|
|
30
|
+
: processCap;
|
|
22
31
|
const queueCap = Math.max(1, Math.min(requestedClones, pending || totalWorkItems));
|
|
23
32
|
const leaseCap = Math.max(1, requestedClones - leaseConflicts);
|
|
24
|
-
const rawSafe = Math.max(1, Math.min(requestedClones, totalWorkItems, memoryCap, fdCap,
|
|
33
|
+
const rawSafe = Math.max(1, Math.min(requestedClones, totalWorkItems, memoryCap, fdCap, cpuCap + ioCap, gitWorktreeCap + processCap, backendBudget, queueCap, leaseCap, 100));
|
|
25
34
|
const pressure = monitorNarutoResourcePressure(hardware, { activeWorkers: rawSafe, zellijVisiblePaneCap });
|
|
26
35
|
const backpressure = applyNarutoBackpressure(rawSafe, pressure);
|
|
27
36
|
const safeActiveWorkers = Math.max(1, Math.min(rawSafe, backpressure.adjusted_active_workers));
|
|
@@ -29,6 +38,9 @@ export function decideNarutoConcurrency(input = {}) {
|
|
|
29
38
|
const reasons = [
|
|
30
39
|
...(memoryCap < requestedClones ? ['memory_cap'] : []),
|
|
31
40
|
...(fdCap < requestedClones ? ['file_descriptor_budget'] : []),
|
|
41
|
+
...(cpuCap + ioCap < requestedClones ? ['cpu_io_budget'] : []),
|
|
42
|
+
...(gitWorktreeCap + processCap < requestedClones ? ['git_worktree_process_budget'] : []),
|
|
43
|
+
...(backendBudget < requestedClones ? ['backend_parallel_budget'] : []),
|
|
32
44
|
...(remoteCodexParallel < requestedClones ? ['remote_api_rate_limit_budget'] : []),
|
|
33
45
|
...(localLlmParallel <= 4 ? ['local_llm_max_parallel_requests'] : []),
|
|
34
46
|
...(safeVisible < safeActiveWorkers ? ['zellij_ui_pane_budget'] : []),
|
|
@@ -44,6 +56,9 @@ export function decideNarutoConcurrency(input = {}) {
|
|
|
44
56
|
headless_workers: Math.max(0, safeActiveWorkers - safeVisible),
|
|
45
57
|
local_llm_parallel: localLlmParallel,
|
|
46
58
|
remote_codex_parallel: remoteCodexParallel,
|
|
59
|
+
process_parallel: processCap,
|
|
60
|
+
git_worktree_parallel: gitWorktreeCap,
|
|
61
|
+
cpu_io_parallel: cpuCap + ioCap,
|
|
47
62
|
verification_parallel: Math.max(1, Math.min(hardware.cpu_core_count * 2, safeActiveWorkers, 16)),
|
|
48
63
|
reasons: [...new Set(reasons)],
|
|
49
64
|
backpressure: backpressure.backpressure,
|
|
@@ -28,7 +28,8 @@ export function buildNarutoWorkGraph(input = {}) {
|
|
|
28
28
|
const requestedClones = normalizePositiveInt(input.requestedClones, 12);
|
|
29
29
|
const readonly = input.readonly === true;
|
|
30
30
|
const writeCapable = input.writeCapable !== false && !readonly;
|
|
31
|
-
const
|
|
31
|
+
const minimumFanout = writeCapable ? requestedClones * 2 : requestedClones;
|
|
32
|
+
const totalWorkItems = Math.max(minimumFanout, normalizePositiveInt(input.totalWorkItems, minimumFanout));
|
|
32
33
|
const kindCycle = writeCapable ? WRITE_CAPABLE_KIND_CYCLE : READONLY_KIND_CYCLE;
|
|
33
34
|
const basePath = normalizeNarutoPath(input.leaseBasePath || '.sneakoscope/naruto/patch-envelopes');
|
|
34
35
|
const targetPaths = normalizePaths(input.targetPaths || []);
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
export const RELEASE_GATE_CACHE_V2_SCHEMA = 'sks.release-gate-cache.v2';
|
|
5
|
+
export function releaseGateCacheFile(root) {
|
|
6
|
+
return path.join(root, '.sneakoscope', 'reports', 'release-gates', 'cache-v2.json');
|
|
7
|
+
}
|
|
8
|
+
export function releaseGateCacheKey(root, gate) {
|
|
9
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
10
|
+
const hash = crypto.createHash('sha256');
|
|
11
|
+
hash.update(gate.id);
|
|
12
|
+
hash.update(gate.command);
|
|
13
|
+
hash.update(String(pkg.version || ''));
|
|
14
|
+
hash.update(process.version);
|
|
15
|
+
hash.update(String(process.env.npm_config_user_agent || ''));
|
|
16
|
+
hash.update(JSON.stringify(gate.resource || []));
|
|
17
|
+
hash.update(JSON.stringify(gate.preset || []));
|
|
18
|
+
hashFileIfPresent(hash, path.join(root, 'release-gates.v2.json'));
|
|
19
|
+
hashFileIfPresent(hash, path.join(root, 'package.json'));
|
|
20
|
+
hashFileIfPresent(hash, path.join(root, 'dist', 'build-manifest.json'));
|
|
21
|
+
for (const input of gate.cache.inputs) {
|
|
22
|
+
const expanded = expandGlob(root, input);
|
|
23
|
+
hash.update(`input:${input}`);
|
|
24
|
+
if (!expanded.length) {
|
|
25
|
+
hash.update(`missing_or_empty:${input}`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
for (const file of expanded) {
|
|
29
|
+
hash.update(path.relative(root, file));
|
|
30
|
+
hashFileIfPresent(hash, file);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return hash.digest('hex');
|
|
34
|
+
}
|
|
35
|
+
export function expandGlob(root, input) {
|
|
36
|
+
const absolute = path.join(root, input);
|
|
37
|
+
if (!/[*!?[\]{}]/.test(input)) {
|
|
38
|
+
if (!fs.existsSync(absolute))
|
|
39
|
+
return [];
|
|
40
|
+
const stat = fs.statSync(absolute);
|
|
41
|
+
if (stat.isDirectory())
|
|
42
|
+
return hashDirectoryRecursive(absolute);
|
|
43
|
+
return stat.isFile() ? [absolute] : [];
|
|
44
|
+
}
|
|
45
|
+
if (input.endsWith('/**')) {
|
|
46
|
+
const dir = path.join(root, input.slice(0, -3));
|
|
47
|
+
return fs.existsSync(dir) && fs.statSync(dir).isDirectory() ? hashDirectoryRecursive(dir) : [];
|
|
48
|
+
}
|
|
49
|
+
const firstWildcard = input.search(/[*!?[\]{}]/);
|
|
50
|
+
const prefix = input.slice(0, firstWildcard);
|
|
51
|
+
const base = path.join(root, prefix.includes('/') ? prefix.slice(0, prefix.lastIndexOf('/')) : '');
|
|
52
|
+
if (!fs.existsSync(base))
|
|
53
|
+
return [];
|
|
54
|
+
const re = globToRegExp(input);
|
|
55
|
+
return hashDirectoryRecursive(base).filter((file) => re.test(path.relative(root, file)));
|
|
56
|
+
}
|
|
57
|
+
export function hashDirectoryRecursive(dir) {
|
|
58
|
+
if (!fs.existsSync(dir))
|
|
59
|
+
return [];
|
|
60
|
+
const out = [];
|
|
61
|
+
const stack = [dir];
|
|
62
|
+
while (stack.length) {
|
|
63
|
+
const current = stack.pop();
|
|
64
|
+
for (const name of fs.readdirSync(current).sort()) {
|
|
65
|
+
const file = path.join(current, name);
|
|
66
|
+
const stat = fs.statSync(file);
|
|
67
|
+
if (stat.isDirectory())
|
|
68
|
+
stack.push(file);
|
|
69
|
+
else if (stat.isFile())
|
|
70
|
+
out.push(file);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out.sort();
|
|
74
|
+
}
|
|
75
|
+
export function readReleaseGateCacheHit(root, gate) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(fs.readFileSync(releaseGateCacheFile(root), 'utf8'));
|
|
78
|
+
return parsed.schema === RELEASE_GATE_CACHE_V2_SCHEMA && parsed.records?.[releaseGateCacheKey(root, gate)]?.ok === true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function writeReleaseGateCacheHit(root, gate) {
|
|
85
|
+
const file = releaseGateCacheFile(root);
|
|
86
|
+
let parsed = { schema: RELEASE_GATE_CACHE_V2_SCHEMA, records: {} };
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
parsed.schema = RELEASE_GATE_CACHE_V2_SCHEMA;
|
|
92
|
+
parsed.records ||= {};
|
|
93
|
+
parsed.records[releaseGateCacheKey(root, gate)] = {
|
|
94
|
+
ok: true,
|
|
95
|
+
gate_id: gate.id,
|
|
96
|
+
command: gate.command,
|
|
97
|
+
resource: gate.resource,
|
|
98
|
+
preset: gate.preset,
|
|
99
|
+
recorded_at: new Date().toISOString()
|
|
100
|
+
};
|
|
101
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
102
|
+
fs.writeFileSync(file, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
103
|
+
}
|
|
104
|
+
function hashFileIfPresent(hash, file) {
|
|
105
|
+
if (fs.existsSync(file) && fs.statSync(file).isFile())
|
|
106
|
+
hash.update(fs.readFileSync(file));
|
|
107
|
+
}
|
|
108
|
+
function globToRegExp(input) {
|
|
109
|
+
const escaped = input
|
|
110
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
111
|
+
.replace(/\*\*/g, '\u0000')
|
|
112
|
+
.replace(/\*/g, '[^/]*')
|
|
113
|
+
.replace(/\?/g, '[^/]')
|
|
114
|
+
.replace(/\u0000/g, '.*');
|
|
115
|
+
return new RegExp(`^${escaped}$`);
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=release-gate-cache-v2.js.map
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { createReleaseGateHermeticEnv } from './release-gate-hermetic-env.js';
|
|
5
|
+
import { appendReleaseGateJsonl, writeReleaseGateJson } from './release-gate-report.js';
|
|
6
|
+
import { findReadyReleaseGateNodes, findReleaseGatesBlockedByFailedDeps, pickReadyLaunchableReleaseGates } from './release-gate-scheduler.js';
|
|
7
|
+
import { readReleaseGateCacheHit, writeReleaseGateCacheHit } from './release-gate-cache-v2.js';
|
|
8
|
+
import { RELEASE_GATE_NODE_SCHEMA, validateReleaseGateManifest } from './release-gate-node.js';
|
|
9
|
+
import { countReleaseGateResources, defaultReleaseGateBudget, summarizeReleaseGateBudget } from './release-gate-resource-governor.js';
|
|
10
|
+
export function loadReleaseGateManifest(root, file = 'release-gates.v2.json') {
|
|
11
|
+
const manifestPath = path.join(root, file);
|
|
12
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
13
|
+
const validation = validateReleaseGateManifest(parsed);
|
|
14
|
+
if (!validation.ok || !validation.manifest) {
|
|
15
|
+
throw new Error(`invalid ${file}: ${validation.errors.join(', ')}`);
|
|
16
|
+
}
|
|
17
|
+
return validation.manifest;
|
|
18
|
+
}
|
|
19
|
+
export async function runReleaseGateDag(input) {
|
|
20
|
+
const root = path.resolve(input.root);
|
|
21
|
+
const preset = input.preset || 'release';
|
|
22
|
+
const manifest = loadReleaseGateManifest(root);
|
|
23
|
+
const selected = selectPreset(manifest, preset);
|
|
24
|
+
const runId = `rg-${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}`;
|
|
25
|
+
const reportDir = path.join(root, '.sneakoscope', 'reports', 'release-gates', runId);
|
|
26
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
27
|
+
const timeline = path.join(reportDir, 'timeline.jsonl');
|
|
28
|
+
const started = Date.now();
|
|
29
|
+
const pending = new Map(selected.map((gate) => [gate.id, gate]));
|
|
30
|
+
const running = new Map();
|
|
31
|
+
const completed = new Map();
|
|
32
|
+
const failed = new Map();
|
|
33
|
+
const budget = defaultReleaseGateBudget();
|
|
34
|
+
const peakResources = {};
|
|
35
|
+
let cached = 0;
|
|
36
|
+
let sumGateMs = 0;
|
|
37
|
+
let peakRunning = 0;
|
|
38
|
+
const writeSummarySnapshot = (finished = false) => {
|
|
39
|
+
const wallMs = Date.now() - started;
|
|
40
|
+
const failures = [...failed.values()].map((row) => ({ id: row.id, exit_code: row.exit_code, stderr_tail: row.stderr_tail }));
|
|
41
|
+
const snapshot = {
|
|
42
|
+
schema: 'sks.release-gate-dag-run.v1',
|
|
43
|
+
ok: failures.length === 0,
|
|
44
|
+
run_id: runId,
|
|
45
|
+
selected_preset: preset,
|
|
46
|
+
total_gates: manifest.gates.length,
|
|
47
|
+
selected_gates: selected.length,
|
|
48
|
+
completed: completed.size,
|
|
49
|
+
failed: failed.size,
|
|
50
|
+
cached,
|
|
51
|
+
wall_ms: wallMs,
|
|
52
|
+
sum_gate_ms: sumGateMs,
|
|
53
|
+
cpu_time_saved_ms: Math.max(0, sumGateMs - wallMs),
|
|
54
|
+
parallelism_gain: wallMs > 0 ? Number((sumGateMs / wallMs).toFixed(2)) : 1,
|
|
55
|
+
critical_path_ms: estimateCriticalPath(selected, completed),
|
|
56
|
+
peak_running: peakRunning,
|
|
57
|
+
peak_resources: peakResources,
|
|
58
|
+
budget_snapshot: budget,
|
|
59
|
+
budget_summary: summarizeReleaseGateBudget(budget),
|
|
60
|
+
report_dir: reportDir,
|
|
61
|
+
failures
|
|
62
|
+
};
|
|
63
|
+
if (!finished) {
|
|
64
|
+
snapshot.in_progress = true;
|
|
65
|
+
snapshot.pending = pending.size;
|
|
66
|
+
snapshot.running = running.size;
|
|
67
|
+
}
|
|
68
|
+
writeReleaseGateJson(path.join(reportDir, 'summary.json'), snapshot);
|
|
69
|
+
return snapshot;
|
|
70
|
+
};
|
|
71
|
+
if (input.explain) {
|
|
72
|
+
writeReleaseGateJson(path.join(reportDir, 'explain.json'), { schema: RELEASE_GATE_NODE_SCHEMA, preset, budget, gates: selected.map((gate) => ({ id: gate.id, deps: gate.deps, resource: gate.resource, command: gate.command })) });
|
|
73
|
+
}
|
|
74
|
+
while (pending.size || running.size) {
|
|
75
|
+
const ready = findReadyReleaseGateNodes({ pending, completed, failed });
|
|
76
|
+
const launchable = pickReadyLaunchableReleaseGates({ ready, running: [...running.values()].map((row) => row.gate) });
|
|
77
|
+
let progressed = false;
|
|
78
|
+
for (const gate of launchable) {
|
|
79
|
+
pending.delete(gate.id);
|
|
80
|
+
const cacheHit = !input.noCache && gate.cache.enabled && readReleaseGateCacheHit(root, gate);
|
|
81
|
+
if (cacheHit) {
|
|
82
|
+
const result = { id: gate.id, ok: true, exit_code: 0, duration_ms: 0, cached: true, stderr_tail: '' };
|
|
83
|
+
completed.set(gate.id, result);
|
|
84
|
+
cached += 1;
|
|
85
|
+
progressed = true;
|
|
86
|
+
appendReleaseGateJsonl(timeline, { event: 'cache_hit', gate_id: gate.id, at: new Date().toISOString() });
|
|
87
|
+
writeSummarySnapshot(false);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
appendReleaseGateJsonl(timeline, { event: 'start', gate_id: gate.id, resource: gate.resource, at: new Date().toISOString() });
|
|
91
|
+
running.set(gate.id, { gate, promise: runGate(root, runId, reportDir, gate) });
|
|
92
|
+
peakRunning = Math.max(peakRunning, running.size);
|
|
93
|
+
const used = countReleaseGateResources([...running.values()].map((row) => row.gate));
|
|
94
|
+
for (const [resource, count] of Object.entries(used)) {
|
|
95
|
+
peakResources[resource] = Math.max(peakResources[resource] || 0, Number(count) || 0);
|
|
96
|
+
}
|
|
97
|
+
progressed = true;
|
|
98
|
+
}
|
|
99
|
+
if (!running.size) {
|
|
100
|
+
const blockedByFailedDeps = findReleaseGatesBlockedByFailedDeps({ pending, failed });
|
|
101
|
+
if (blockedByFailedDeps.length) {
|
|
102
|
+
for (const gate of blockedByFailedDeps) {
|
|
103
|
+
pending.delete(gate.id);
|
|
104
|
+
const result = {
|
|
105
|
+
id: gate.id,
|
|
106
|
+
ok: false,
|
|
107
|
+
exit_code: null,
|
|
108
|
+
duration_ms: 0,
|
|
109
|
+
cached: false,
|
|
110
|
+
stderr_tail: `blocked by failed dependency: ${gate.deps.filter((dep) => failed.has(dep)).join(', ')}`
|
|
111
|
+
};
|
|
112
|
+
failed.set(gate.id, result);
|
|
113
|
+
appendReleaseGateJsonl(timeline, { event: 'blocked_by_failed_dependency', gate_id: gate.id, deps: gate.deps.filter((dep) => failed.has(dep)), at: new Date().toISOString() });
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (progressed)
|
|
118
|
+
continue;
|
|
119
|
+
const blocked = [...pending.keys()];
|
|
120
|
+
throw new Error(`release gate DAG stalled: ${blocked.join(', ')}`);
|
|
121
|
+
}
|
|
122
|
+
const result = await Promise.race([...running.values()].map((row) => row.promise));
|
|
123
|
+
running.delete(result.id);
|
|
124
|
+
sumGateMs += result.duration_ms;
|
|
125
|
+
if (result.ok) {
|
|
126
|
+
completed.set(result.id, result);
|
|
127
|
+
const gate = selected.find((row) => row.id === result.id);
|
|
128
|
+
if (gate?.cache.enabled && !input.noCache)
|
|
129
|
+
writeReleaseGateCacheHit(root, gate);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
failed.set(result.id, result);
|
|
133
|
+
if (input.failFast) {
|
|
134
|
+
for (const id of [...pending.keys()])
|
|
135
|
+
pending.delete(id);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
appendReleaseGateJsonl(timeline, { event: result.ok ? 'pass' : 'fail', gate_id: result.id, duration_ms: result.duration_ms, at: new Date().toISOString() });
|
|
139
|
+
writeSummarySnapshot(false);
|
|
140
|
+
}
|
|
141
|
+
const result = writeSummarySnapshot(true);
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
function selectPreset(manifest, preset) {
|
|
145
|
+
return manifest.gates.filter((gate) => gate.preset.includes(preset));
|
|
146
|
+
}
|
|
147
|
+
function runGate(root, runId, reportRoot, gate) {
|
|
148
|
+
const started = Date.now();
|
|
149
|
+
const hermetic = createReleaseGateHermeticEnv({ root, runId, gate, reportRoot });
|
|
150
|
+
const stdoutFile = path.join(hermetic.report_dir, 'stdout.log');
|
|
151
|
+
const stderrFile = path.join(hermetic.report_dir, 'stderr.log');
|
|
152
|
+
const out = fs.createWriteStream(stdoutFile);
|
|
153
|
+
const err = fs.createWriteStream(stderrFile);
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
const child = spawn(gate.command, { cwd: root, shell: true, env: hermetic.env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
156
|
+
const timer = setTimeout(() => child.kill('SIGTERM'), gate.timeout_ms);
|
|
157
|
+
child.stdout.pipe(out);
|
|
158
|
+
child.stderr.pipe(err);
|
|
159
|
+
child.on('close', (code) => {
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
out.end();
|
|
162
|
+
err.end();
|
|
163
|
+
const durationMs = Date.now() - started;
|
|
164
|
+
const stderrTail = tail(fs.existsSync(stderrFile) ? fs.readFileSync(stderrFile, 'utf8') : '');
|
|
165
|
+
const result = { id: gate.id, ok: code === 0, exit_code: code, duration_ms: durationMs, cached: false, stderr_tail: stderrTail };
|
|
166
|
+
writeReleaseGateJson(path.join(hermetic.report_dir, 'result.json'), { schema: 'sks.release-gate-result.v1', ...result, stdout_log: stdoutFile, stderr_log: stderrFile });
|
|
167
|
+
resolve(result);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function estimateCriticalPath(gates, completed) {
|
|
172
|
+
const byId = new Map(gates.map((gate) => [gate.id, gate]));
|
|
173
|
+
const memo = new Map();
|
|
174
|
+
const visit = (id) => {
|
|
175
|
+
if (memo.has(id))
|
|
176
|
+
return memo.get(id);
|
|
177
|
+
const gate = byId.get(id);
|
|
178
|
+
if (!gate)
|
|
179
|
+
return 0;
|
|
180
|
+
const own = completed.get(id)?.duration_ms || 0;
|
|
181
|
+
const dep = Math.max(0, ...gate.deps.map(visit));
|
|
182
|
+
memo.set(id, own + dep);
|
|
183
|
+
return own + dep;
|
|
184
|
+
};
|
|
185
|
+
return Math.max(0, ...gates.map((gate) => visit(gate.id)));
|
|
186
|
+
}
|
|
187
|
+
function tail(value, limit = 1200) {
|
|
188
|
+
return value.length > limit ? value.slice(-limit) : value;
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=release-gate-dag.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
export function createReleaseGateHermeticEnv(input) {
|
|
5
|
+
const safeId = input.gate.id.replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
6
|
+
const tmpRoot = path.join(os.tmpdir(), 'sks-gate', input.runId, safeId);
|
|
7
|
+
const home = path.join(tmpRoot, 'home');
|
|
8
|
+
const codexHome = path.join(tmpRoot, 'codex-home');
|
|
9
|
+
const cacheHome = path.join(tmpRoot, 'xdg-cache');
|
|
10
|
+
const reportDir = path.join(input.reportRoot, safeId);
|
|
11
|
+
fs.mkdirSync(home, { recursive: true });
|
|
12
|
+
fs.mkdirSync(codexHome, { recursive: true });
|
|
13
|
+
fs.mkdirSync(cacheHome, { recursive: true });
|
|
14
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
15
|
+
return {
|
|
16
|
+
tmp_dir: tmpRoot,
|
|
17
|
+
report_dir: reportDir,
|
|
18
|
+
env: {
|
|
19
|
+
...process.env,
|
|
20
|
+
SKS_GATE_ID: input.gate.id,
|
|
21
|
+
SKS_GATE_RUN_ID: input.runId,
|
|
22
|
+
SKS_REPORT_DIR: reportDir,
|
|
23
|
+
SKS_TMP_DIR: tmpRoot,
|
|
24
|
+
HOME: input.gate.isolation.home === 'temp' ? home : process.env.HOME,
|
|
25
|
+
CODEX_HOME: input.gate.isolation.codex_home === 'temp' ? codexHome : process.env.CODEX_HOME,
|
|
26
|
+
XDG_CACHE_HOME: cacheHome,
|
|
27
|
+
SKS_DISABLE_REAL_MODEL_CALLS: input.gate.preset.includes('real-check') ? process.env.SKS_DISABLE_REAL_MODEL_CALLS || '0' : '1',
|
|
28
|
+
SKS_DISABLE_GLOBAL_CONFIG_MUTATION: '1'
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=release-gate-hermetic-env.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const RELEASE_GATE_NODE_SCHEMA = 'sks.release-gates.v2';
|
|
2
|
+
export const RELEASE_GATE_RESOURCE_CLASSES = [
|
|
3
|
+
'cpu-light',
|
|
4
|
+
'cpu-heavy',
|
|
5
|
+
'io-light',
|
|
6
|
+
'io-heavy',
|
|
7
|
+
'git',
|
|
8
|
+
'git-worktree',
|
|
9
|
+
'zellij-real',
|
|
10
|
+
'local-llm-real',
|
|
11
|
+
'remote-model-real',
|
|
12
|
+
'python',
|
|
13
|
+
'network',
|
|
14
|
+
'global-config',
|
|
15
|
+
'publish',
|
|
16
|
+
'fs-read'
|
|
17
|
+
];
|
|
18
|
+
export function validateReleaseGateManifest(input) {
|
|
19
|
+
const errors = [];
|
|
20
|
+
if (input?.schema !== RELEASE_GATE_NODE_SCHEMA)
|
|
21
|
+
errors.push('schema_mismatch');
|
|
22
|
+
if (!Array.isArray(input?.gates))
|
|
23
|
+
errors.push('gates_missing');
|
|
24
|
+
const ids = new Set();
|
|
25
|
+
const resources = new Set(RELEASE_GATE_RESOURCE_CLASSES);
|
|
26
|
+
for (const gate of Array.isArray(input?.gates) ? input.gates : []) {
|
|
27
|
+
if (!gate?.id)
|
|
28
|
+
errors.push('gate_id_missing');
|
|
29
|
+
if (gate?.id && ids.has(gate.id))
|
|
30
|
+
errors.push(`gate_duplicate:${gate.id}`);
|
|
31
|
+
if (gate?.id)
|
|
32
|
+
ids.add(gate.id);
|
|
33
|
+
if (!gate?.command)
|
|
34
|
+
errors.push(`gate_command_missing:${gate?.id || 'unknown'}`);
|
|
35
|
+
if (!Array.isArray(gate?.deps))
|
|
36
|
+
errors.push(`gate_deps_missing:${gate?.id || 'unknown'}`);
|
|
37
|
+
if (!Array.isArray(gate?.resource) || !gate.resource.length)
|
|
38
|
+
errors.push(`gate_resource_missing:${gate?.id || 'unknown'}`);
|
|
39
|
+
for (const resource of Array.isArray(gate?.resource) ? gate.resource : []) {
|
|
40
|
+
if (!resources.has(resource))
|
|
41
|
+
errors.push(`gate_unknown_resource:${gate?.id || 'unknown'}:${resource}`);
|
|
42
|
+
}
|
|
43
|
+
if (gate?.side_effect !== 'hermetic' && gate?.side_effect !== 'real-env')
|
|
44
|
+
errors.push(`gate_side_effect_invalid:${gate?.id || 'unknown'}`);
|
|
45
|
+
if (!Number.isFinite(Number(gate?.timeout_ms)) || Number(gate.timeout_ms) <= 0)
|
|
46
|
+
errors.push(`gate_timeout_missing:${gate?.id || 'unknown'}`);
|
|
47
|
+
if (!gate?.cache || typeof gate.cache.enabled !== 'boolean' || !Array.isArray(gate.cache.inputs))
|
|
48
|
+
errors.push(`gate_cache_missing:${gate?.id || 'unknown'}`);
|
|
49
|
+
if (!gate?.isolation || gate.isolation.report_dir !== 'per-gate')
|
|
50
|
+
errors.push(`gate_isolation_missing:${gate?.id || 'unknown'}`);
|
|
51
|
+
if (!Array.isArray(gate?.preset))
|
|
52
|
+
errors.push(`gate_preset_missing:${gate?.id || 'unknown'}`);
|
|
53
|
+
}
|
|
54
|
+
for (const gate of Array.isArray(input?.gates) ? input.gates : []) {
|
|
55
|
+
for (const dep of Array.isArray(gate?.deps) ? gate.deps : []) {
|
|
56
|
+
if (!ids.has(dep))
|
|
57
|
+
errors.push(`gate_unknown_dep:${gate.id}:${dep}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return errors.length ? { ok: false, errors } : { ok: true, manifest: input, errors };
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=release-gate-node.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function writeReleaseGateJson(file, value) {
|
|
4
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
5
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
6
|
+
}
|
|
7
|
+
export function appendReleaseGateJsonl(file, value) {
|
|
8
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
9
|
+
fs.appendFileSync(file, `${JSON.stringify(value)}\n`);
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=release-gate-report.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
export function defaultReleaseGateBudget() {
|
|
3
|
+
const cores = Math.max(1, os.cpus().length || 1);
|
|
4
|
+
return {
|
|
5
|
+
'cpu-light': Math.min(32, cores * 4),
|
|
6
|
+
'cpu-heavy': Math.max(1, cores - 1),
|
|
7
|
+
'io-light': Math.min(64, cores * 8),
|
|
8
|
+
'io-heavy': Math.min(8, cores),
|
|
9
|
+
git: Math.min(8, cores),
|
|
10
|
+
'git-worktree': Math.min(6, cores),
|
|
11
|
+
python: Math.min(8, cores),
|
|
12
|
+
network: 8,
|
|
13
|
+
'zellij-real': 1,
|
|
14
|
+
'local-llm-real': Math.max(1, Number(process.env.SKS_LOCAL_LLM_MAX_PARALLEL || 1)),
|
|
15
|
+
'remote-model-real': 4,
|
|
16
|
+
'global-config': 1,
|
|
17
|
+
publish: 1,
|
|
18
|
+
'fs-read': Math.min(64, cores * 8)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function summarizeReleaseGateBudget(budget = defaultReleaseGateBudget()) {
|
|
22
|
+
return Object.entries(budget)
|
|
23
|
+
.filter(([, value]) => Number(value) > 0)
|
|
24
|
+
.map(([resource, value]) => `${resource}=${value}`)
|
|
25
|
+
.join(' ');
|
|
26
|
+
}
|
|
27
|
+
export function countReleaseGateResources(running) {
|
|
28
|
+
return usedResources(running);
|
|
29
|
+
}
|
|
30
|
+
export function pickLaunchableReleaseGates(input) {
|
|
31
|
+
const budget = input.budget || defaultReleaseGateBudget();
|
|
32
|
+
const used = usedResources(input.running);
|
|
33
|
+
const launchable = [];
|
|
34
|
+
for (const gate of input.ready) {
|
|
35
|
+
if (fits(gate, used, budget)) {
|
|
36
|
+
launchable.push(gate);
|
|
37
|
+
for (const resource of gate.resource)
|
|
38
|
+
used[resource] = (used[resource] || 0) + 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return launchable;
|
|
42
|
+
}
|
|
43
|
+
function usedResources(running) {
|
|
44
|
+
const used = {};
|
|
45
|
+
for (const gate of running) {
|
|
46
|
+
for (const resource of gate.resource)
|
|
47
|
+
used[resource] = (used[resource] || 0) + 1;
|
|
48
|
+
}
|
|
49
|
+
return used;
|
|
50
|
+
}
|
|
51
|
+
function fits(gate, used, budget) {
|
|
52
|
+
return gate.resource.every((resource) => (used[resource] || 0) < budget[resource]);
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=release-gate-resource-governor.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defaultReleaseGateBudget, pickLaunchableReleaseGates } from './release-gate-resource-governor.js';
|
|
2
|
+
export function findReadyReleaseGateNodes(input) {
|
|
3
|
+
return [...input.pending.values()].filter((gate) => gate.deps.every((dep) => input.completed.has(dep)) && !gate.deps.some((dep) => input.failed.has(dep)));
|
|
4
|
+
}
|
|
5
|
+
export function findReleaseGatesBlockedByFailedDeps(input) {
|
|
6
|
+
return [...input.pending.values()].filter((gate) => gate.deps.some((dep) => input.failed.has(dep)));
|
|
7
|
+
}
|
|
8
|
+
export function pickReadyLaunchableReleaseGates(input) {
|
|
9
|
+
return pickLaunchableReleaseGates({
|
|
10
|
+
ready: input.ready,
|
|
11
|
+
running: input.running,
|
|
12
|
+
budget: defaultReleaseGateBudget()
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=release-gate-scheduler.js.map
|
package/dist/core/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '2.0.
|
|
1
|
+
export const PACKAGE_VERSION = '2.0.9';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|