forge-workflow 0.1.0-beta.3 → 0.1.0-beta.5
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/AGENTS.md +14 -7
- package/CHANGELOG.md +43 -1
- package/README.md +6 -2
- package/bin/forge-cmd.js +21 -1
- package/bin/forge.js +16 -369
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +9 -4
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +117 -17
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +397 -100
- package/lib/agents-config.js +5 -0
- package/lib/audit-evidence.js +71 -110
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/commands/_issue.js +31 -46
- package/lib/commands/_manifest.js +1 -1
- package/lib/commands/_registry.js +2 -2
- package/lib/commands/_resolve-command-opts.js +36 -29
- package/lib/commands/claim.js +2 -4
- package/lib/commands/clean.js +196 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/hooks.js +358 -13
- package/lib/commands/insights.js +8 -3
- package/lib/commands/merge.js +600 -40
- package/lib/commands/plan.js +23 -115
- package/lib/commands/pr.js +1 -1
- package/lib/commands/preflight.js +11 -2
- package/lib/commands/prime.js +23 -3
- package/lib/commands/push.js +41 -51
- package/lib/commands/recall.js +60 -16
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +18 -4
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +191 -95
- package/lib/commands/shepherd.js +49 -4
- package/lib/commands/ship.js +22 -23
- package/lib/commands/skill.js +383 -0
- package/lib/commands/status.js +54 -33
- package/lib/commands/test.js +56 -34
- package/lib/commands/worktree.js +247 -43
- package/lib/core/runtime-graph.js +89 -15
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +64 -491
- package/lib/git-defaults.js +56 -0
- package/lib/harness-capability-matrix.js +5 -5
- package/lib/hook-renderer.js +147 -16
- package/lib/insights.js +96 -80
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +44 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +292 -18
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/router.js +16 -1
- package/lib/memory-digest.js +47 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +212 -0
- package/lib/merge-rules.js +8 -4
- package/lib/npm-publish-workflow.js +272 -0
- package/lib/orientation.js +371 -49
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +9 -6
- package/lib/pr-monitor/journal.js +18 -2
- package/lib/pr-monitor/reconcile-executor.js +842 -0
- package/lib/pr-monitor/reconcile-tick.js +138 -0
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +196 -0
- package/lib/pr-monitor/shepherd-lease.js +252 -0
- package/lib/pr-monitor/watch-lifecycle.js +14 -2
- package/lib/pr-pull.js +98 -24
- package/lib/pr-shepherd.js +34 -8
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +40 -0
- package/lib/protected-state-authority.js +305 -0
- package/lib/protected-state-surfaces.js +64 -44
- package/lib/release-readiness.js +51 -4
- package/lib/rules-sync.js +4 -0
- package/lib/runtime-health.js +15 -46
- package/lib/shell-utils.js +1 -1
- package/lib/skill-eval.js +750 -0
- package/lib/skills-sync.js +6 -3
- package/lib/smart-merge.js +28 -4
- package/lib/status/identity.js +46 -0
- package/lib/status/presenter.js +0 -35
- package/lib/status/snapshot.js +11 -16
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +47 -9
- package/lib/using-forge.js +328 -0
- package/lib/workflow/enforce-stage.js +5 -5
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +6 -7
- package/rules/using-forge.md +24 -0
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/forge-team/index.sh +0 -5
- package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
- package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +456 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +761 -0
- package/scripts/protected-state-check.js +47 -22
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +249 -37
- package/scripts/test.js +184 -44
- package/skills/claim-safety/SKILL.md +4 -0
- package/skills/claim-safety/evals/scorecard.json +41 -0
- package/skills/coverage.json +83 -0
- package/skills/dev/SKILL.md +4 -0
- package/skills/dev/evals/scorecard.json +41 -0
- package/skills/gates/SKILL.md +80 -0
- package/skills/gates/evals/evals.json +38 -0
- package/skills/gates/evals/scorecard.json +41 -0
- package/skills/hermes-forge/SKILL.md +1 -0
- package/skills/hermes-forge/evals/scorecard.json +41 -0
- package/skills/issue-basics/SKILL.md +1 -0
- package/skills/issue-basics/evals/scorecard.json +41 -0
- package/skills/kernel/SKILL.md +38 -0
- package/skills/kernel/evals/scorecard.json +41 -0
- package/skills/memory/SKILL.md +16 -1
- package/skills/memory/evals/scorecard.json +41 -0
- package/skills/parallel-deep-research/SKILL.md +1 -0
- package/skills/parallel-deep-research/evals/scorecard.json +41 -0
- package/skills/plan/SKILL.md +6 -0
- package/skills/plan/evals/scorecard.json +41 -0
- package/skills/portability/SKILL.md +47 -0
- package/skills/portability/evals/evals.json +34 -0
- package/skills/portability/evals/scorecard.json +41 -0
- package/skills/research/SKILL.md +1 -0
- package/skills/research/evals/scorecard.json +41 -0
- package/skills/review/SKILL.md +10 -11
- package/skills/review/evals/scorecard.json +41 -0
- package/skills/rollback/SKILL.md +5 -11
- package/skills/rollback/evals/scorecard.json +41 -0
- package/skills/setup/SKILL.md +91 -0
- package/skills/setup/evals/evals.json +42 -0
- package/skills/setup/evals/scorecard.json +41 -0
- package/skills/shepherd/SKILL.md +84 -38
- package/skills/shepherd/evals/evals.json +21 -9
- package/skills/shepherd/evals/scorecard.json +41 -0
- package/skills/ship/SKILL.md +10 -12
- package/skills/ship/evals/scorecard.json +41 -0
- package/skills/smith/SKILL.md +8 -0
- package/skills/smith/evals/scorecard.json +41 -0
- package/skills/sonarcloud/SKILL.md +1 -0
- package/skills/sonarcloud/evals/scorecard.json +41 -0
- package/skills/sonarcloud-analysis/SKILL.md +1 -0
- package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
- package/skills/status/SKILL.md +3 -0
- package/skills/status/evals/scorecard.json +41 -0
- package/skills/triage-ready/SKILL.md +2 -0
- package/skills/triage-ready/evals/scorecard.json +41 -0
- package/skills/using-forge/SKILL.md +104 -0
- package/skills/using-forge/evals/scorecard.json +41 -0
- package/skills/validate/SKILL.md +4 -0
- package/skills/validate/evals/scorecard.json +41 -0
- package/skills/verify/SKILL.md +4 -0
- package/skills/verify/evals/scorecard.json +41 -0
- package/skills/worktree/SKILL.md +92 -0
- package/skills/worktree/evals/evals.json +38 -0
- package/skills/worktree/evals/scorecard.json +41 -0
- package/lib/adapters/beads-issue-adapter.js +0 -127
- package/lib/beads-nudge.js +0 -91
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/commands/board.js +0 -64
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -192
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/lib/status/beads-snapshot.js +0 -145
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/forge-team/lib/dashboard.sh +0 -316
- package/scripts/forge-team/tests/dashboard.test.sh +0 -155
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const BOOTSTRAP_SAMPLES = 10000;
|
|
4
|
+
const Z_95 = 1.959963984540054;
|
|
5
|
+
const RISKS = Object.freeze(['low', 'medium', 'high']);
|
|
6
|
+
const STATUSES = new Set(['PASS', 'FAIL', 'INCOMPLETE']);
|
|
7
|
+
const TRIALS = Object.freeze([0, 1, 2]);
|
|
8
|
+
const TIER_PHASES = Object.freeze({ 30: 'instrumentation', 100: 'decision', 300: 'confirmation' });
|
|
9
|
+
|
|
10
|
+
function round(value) {
|
|
11
|
+
return Number(value.toFixed(12));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function wilsonInterval(successes, total) {
|
|
15
|
+
if (!Number.isInteger(successes) || !Number.isInteger(total)
|
|
16
|
+
|| successes < 0 || total < 0 || successes > total) {
|
|
17
|
+
throw new Error('Wilson counts must be valid non-negative integers');
|
|
18
|
+
}
|
|
19
|
+
if (total === 0) return null;
|
|
20
|
+
const rate = successes / total;
|
|
21
|
+
const zSquared = Z_95 ** 2;
|
|
22
|
+
const denominator = 1 + zSquared / total;
|
|
23
|
+
const center = (rate + zSquared / (2 * total)) / denominator;
|
|
24
|
+
const margin = Z_95 * Math.sqrt(
|
|
25
|
+
(rate * (1 - rate) + zSquared / (4 * total)) / total
|
|
26
|
+
) / denominator;
|
|
27
|
+
return { lower: round(center - margin), upper: round(center + margin) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isValidArm(value) {
|
|
31
|
+
return value && typeof value === 'object'
|
|
32
|
+
&& STATUSES.has(value.status)
|
|
33
|
+
&& typeof value.hardFailure === 'boolean'
|
|
34
|
+
&& Number.isFinite(value.latencyMs) && value.latencyMs >= 0
|
|
35
|
+
&& Number.isFinite(value.tokens) && value.tokens >= 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isValidPair(pair) {
|
|
39
|
+
return pair && typeof pair === 'object'
|
|
40
|
+
&& typeof pair.caseId === 'string' && pair.caseId.length > 0
|
|
41
|
+
&& RISKS.includes(pair.risk)
|
|
42
|
+
&& ['DEV', 'TEST'].includes(pair.split)
|
|
43
|
+
&& typeof pair.model === 'string' && pair.model.length > 0
|
|
44
|
+
&& TRIALS.includes(pair.trialIndex)
|
|
45
|
+
&& isValidArm(pair.current)
|
|
46
|
+
&& isValidArm(pair.bounded);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function evidenceShapeError(tier, pairs) {
|
|
50
|
+
if (!Object.hasOwn(TIER_PHASES, tier)) return { error: 'tier_invalid' };
|
|
51
|
+
if (!Array.isArray(pairs) || pairs.length === 0) return { error: 'evidence_missing' };
|
|
52
|
+
if (pairs.some((pair) => !isValidPair(pair))) return { error: 'evidence_malformed' };
|
|
53
|
+
if (pairs.some((pair) => pair.current.status === 'INCOMPLETE' || pair.bounded.status === 'INCOMPLETE')) {
|
|
54
|
+
return { error: 'evidence_incomplete' };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function collectMatrix(pairs) {
|
|
60
|
+
const models = [...new Set(pairs.map((pair) => pair.model))].sort();
|
|
61
|
+
const cases = new Map();
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
for (const pair of pairs) {
|
|
64
|
+
const key = `${pair.caseId}\0${pair.model}\0${pair.trialIndex}`;
|
|
65
|
+
if (seen.has(key)) return { error: 'duplicate_pair' };
|
|
66
|
+
seen.add(key);
|
|
67
|
+
const existing = cases.get(pair.caseId);
|
|
68
|
+
if (existing && (existing.risk !== pair.risk || existing.split !== pair.split)) {
|
|
69
|
+
return { error: 'case_metadata_mismatch' };
|
|
70
|
+
}
|
|
71
|
+
cases.set(pair.caseId, { risk: pair.risk, split: pair.split });
|
|
72
|
+
}
|
|
73
|
+
return { models, cases, seen };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function pairingError(tier, models, cases, seen) {
|
|
77
|
+
if (models.length !== 2) return 'model_pair_incomplete';
|
|
78
|
+
if (cases.size !== tier) return 'case_count_incomplete';
|
|
79
|
+
const splitCounts = { DEV: 0, TEST: 0 };
|
|
80
|
+
for (const { split } of cases.values()) splitCounts[split] += 1;
|
|
81
|
+
if (splitCounts.DEV !== tier * 0.6 || splitCounts.TEST !== tier * 0.4) {
|
|
82
|
+
return 'split_count_incomplete';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const caseId of cases.keys()) {
|
|
86
|
+
for (const model of models) {
|
|
87
|
+
for (const trial of TRIALS) {
|
|
88
|
+
if (!seen.has(`${caseId}\0${model}\0${trial}`)) return 'pairing_incomplete';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validatePairs(tier, pairs) {
|
|
96
|
+
const shapeError = evidenceShapeError(tier, pairs);
|
|
97
|
+
if (shapeError) return shapeError;
|
|
98
|
+
const matrix = collectMatrix(pairs);
|
|
99
|
+
if (matrix.error) return matrix;
|
|
100
|
+
const incompletePairing = pairingError(tier, matrix.models, matrix.cases, matrix.seen);
|
|
101
|
+
if (incompletePairing) return { error: incompletePairing };
|
|
102
|
+
const testRisks = new Set(pairs.filter((pair) => pair.split === 'TEST').map((pair) => pair.risk));
|
|
103
|
+
if (RISKS.some((risk) => !testRisks.has(risk))) return { error: 'risk_stratum_incomplete' };
|
|
104
|
+
return { models: matrix.models };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function passStats(pairs, armName) {
|
|
108
|
+
const passes = pairs.filter((pair) => pair[armName].status === 'PASS').length;
|
|
109
|
+
const total = pairs.length;
|
|
110
|
+
return {
|
|
111
|
+
passes,
|
|
112
|
+
total,
|
|
113
|
+
rate: round(passes / total),
|
|
114
|
+
interval: wilsonInterval(passes, total),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function absoluteStats(testPairs, models) {
|
|
119
|
+
const byModel = Object.create(null);
|
|
120
|
+
for (const model of models) {
|
|
121
|
+
const modelPairs = testPairs.filter((pair) => pair.model === model);
|
|
122
|
+
byModel[model] = {
|
|
123
|
+
current: passStats(modelPairs, 'current'),
|
|
124
|
+
bounded: passStats(modelPairs, 'bounded'),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
pooled: {
|
|
129
|
+
current: passStats(testPairs, 'current'),
|
|
130
|
+
bounded: passStats(testPairs, 'bounded'),
|
|
131
|
+
},
|
|
132
|
+
byModel,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createPrng() {
|
|
137
|
+
let state = 0x5eed1234;
|
|
138
|
+
return () => {
|
|
139
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
140
|
+
let value = state;
|
|
141
|
+
value = Math.imul(value ^ (value >>> 15), value | 1);
|
|
142
|
+
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
|
143
|
+
return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function riskClusters(testPairs) {
|
|
148
|
+
const strata = Object.fromEntries(RISKS.map((risk) => [risk, new Map()]));
|
|
149
|
+
for (const pair of testPairs) {
|
|
150
|
+
const clusters = strata[pair.risk];
|
|
151
|
+
if (!clusters.has(pair.caseId)) clusters.set(pair.caseId, []);
|
|
152
|
+
clusters.get(pair.caseId).push(pair);
|
|
153
|
+
}
|
|
154
|
+
return RISKS.map((risk) => (
|
|
155
|
+
[...strata[risk].entries()]
|
|
156
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
157
|
+
.map(([, rows]) => rows)
|
|
158
|
+
));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function pairedDelta(pairs) {
|
|
162
|
+
const difference = pairs.reduce((sum, pair) => (
|
|
163
|
+
sum + Number(pair.bounded.status === 'PASS') - Number(pair.current.status === 'PASS')
|
|
164
|
+
), 0);
|
|
165
|
+
return round(difference / pairs.length);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function quantile(sorted, probability) {
|
|
169
|
+
const position = (sorted.length - 1) * probability;
|
|
170
|
+
const lowerIndex = Math.floor(position);
|
|
171
|
+
const weight = position - lowerIndex;
|
|
172
|
+
const lower = sorted[lowerIndex];
|
|
173
|
+
const upper = sorted[Math.min(lowerIndex + 1, sorted.length - 1)];
|
|
174
|
+
return round(lower + (upper - lower) * weight);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function pairedBootstrap(testPairs) {
|
|
178
|
+
const strata = riskClusters(testPairs);
|
|
179
|
+
const random = createPrng();
|
|
180
|
+
const deltas = [];
|
|
181
|
+
for (let sample = 0; sample < BOOTSTRAP_SAMPLES; sample += 1) {
|
|
182
|
+
const sampled = [];
|
|
183
|
+
for (const clusters of strata) {
|
|
184
|
+
for (let index = 0; index < clusters.length; index += 1) {
|
|
185
|
+
sampled.push(...clusters[Math.floor(random() * clusters.length)]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
deltas.push(pairedDelta(sampled));
|
|
189
|
+
}
|
|
190
|
+
deltas.sort((left, right) => left - right);
|
|
191
|
+
return {
|
|
192
|
+
samples: BOOTSTRAP_SAMPLES,
|
|
193
|
+
delta: pairedDelta(testPairs),
|
|
194
|
+
interval: { lower: quantile(deltas, 0.025), upper: quantile(deltas, 0.975) },
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function percentile(values, probability) {
|
|
199
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
200
|
+
return sorted[Math.ceil(probability * sorted.length) - 1];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function median(values) {
|
|
204
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
205
|
+
const middle = Math.floor(sorted.length / 2);
|
|
206
|
+
return sorted.length % 2 === 1
|
|
207
|
+
? sorted[middle]
|
|
208
|
+
: (sorted[middle - 1] + sorted[middle]) / 2;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function safeRatio(numerator, denominator) {
|
|
212
|
+
if (denominator === 0) return numerator === 0 ? 1 : Number.POSITIVE_INFINITY;
|
|
213
|
+
return round(numerator / denominator);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function operationalRatios(testPairs) {
|
|
217
|
+
const currentLatency = testPairs.map((pair) => pair.current.latencyMs);
|
|
218
|
+
const boundedLatency = testPairs.map((pair) => pair.bounded.latencyMs);
|
|
219
|
+
const currentTokens = testPairs.map((pair) => pair.current.tokens);
|
|
220
|
+
const boundedTokens = testPairs.map((pair) => pair.bounded.tokens);
|
|
221
|
+
return {
|
|
222
|
+
latencyP95: safeRatio(percentile(boundedLatency, 0.95), percentile(currentLatency, 0.95)),
|
|
223
|
+
tokenMedian: safeRatio(median(boundedTokens), median(currentTokens)),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function evaluateThresholds({ pairs, testPairs, models, absolute, bootstrap, operations }) {
|
|
228
|
+
const perModelDelta = models.every((model) => {
|
|
229
|
+
const stats = absolute.byModel[model];
|
|
230
|
+
return stats.bounded.rate - stats.current.rate >= 0;
|
|
231
|
+
});
|
|
232
|
+
const highRisk = pairs.filter((pair) => pair.risk === 'high');
|
|
233
|
+
const highRiskRegression = models.every((model) => (
|
|
234
|
+
pairedDelta(highRisk.filter((pair) => pair.model === model)) >= -0.02
|
|
235
|
+
));
|
|
236
|
+
return {
|
|
237
|
+
zeroHardFailures: pairs.every((pair) => !pair.current.hardFailure && !pair.bounded.hardFailure),
|
|
238
|
+
highRiskTestPass: testPairs
|
|
239
|
+
.filter((pair) => pair.risk === 'high')
|
|
240
|
+
.every((pair) => pair.bounded.status === 'PASS'),
|
|
241
|
+
pooledDelta: bootstrap.delta >= 0.05 && bootstrap.interval.lower > 0,
|
|
242
|
+
perModelDelta,
|
|
243
|
+
highRiskRegression,
|
|
244
|
+
latency: operations.latencyP95 <= 1.25,
|
|
245
|
+
tokens: operations.tokenMedian <= 1.20,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function thresholdReasons(thresholds) {
|
|
250
|
+
const reasonByThreshold = {
|
|
251
|
+
zeroHardFailures: 'hard_failure',
|
|
252
|
+
highRiskTestPass: 'high_risk_test_pass',
|
|
253
|
+
pooledDelta: 'pooled_delta',
|
|
254
|
+
perModelDelta: 'per_model_delta',
|
|
255
|
+
highRiskRegression: 'high_risk_regression',
|
|
256
|
+
latency: 'latency_cap',
|
|
257
|
+
tokens: 'token_cap',
|
|
258
|
+
};
|
|
259
|
+
return Object.entries(reasonByThreshold)
|
|
260
|
+
.filter(([threshold]) => !thresholds[threshold])
|
|
261
|
+
.map(([, reason]) => reason);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function incomplete(tier, reason) {
|
|
265
|
+
return {
|
|
266
|
+
status: 'INCOMPLETE',
|
|
267
|
+
winner: null,
|
|
268
|
+
phase: TIER_PHASES[tier] ?? null,
|
|
269
|
+
mergeAuthorized: false,
|
|
270
|
+
reasons: [reason],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function scorePromotion({ tier, pairs } = {}) {
|
|
275
|
+
const validation = validatePairs(tier, pairs);
|
|
276
|
+
if (validation.error) return incomplete(tier, validation.error);
|
|
277
|
+
|
|
278
|
+
const testPairs = pairs.filter((pair) => pair.split === 'TEST');
|
|
279
|
+
const absolute = absoluteStats(testPairs, validation.models);
|
|
280
|
+
const bootstrap = pairedBootstrap(testPairs);
|
|
281
|
+
const operations = operationalRatios(testPairs);
|
|
282
|
+
const thresholds = evaluateThresholds({
|
|
283
|
+
pairs,
|
|
284
|
+
testPairs,
|
|
285
|
+
models: validation.models,
|
|
286
|
+
absolute,
|
|
287
|
+
bootstrap,
|
|
288
|
+
operations,
|
|
289
|
+
});
|
|
290
|
+
if (tier === 30) {
|
|
291
|
+
return {
|
|
292
|
+
status: 'INCOMPLETE', winner: null, phase: TIER_PHASES[tier], mergeAuthorized: false,
|
|
293
|
+
reasons: ['instrumentation_only'], absolute, bootstrap, operations, thresholds,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const reasons = thresholdReasons(thresholds);
|
|
297
|
+
return {
|
|
298
|
+
status: reasons.length === 0 ? 'PASS' : 'FAIL',
|
|
299
|
+
winner: reasons.length === 0 ? 'bounded' : null,
|
|
300
|
+
phase: TIER_PHASES[tier],
|
|
301
|
+
mergeAuthorized: false,
|
|
302
|
+
reasons,
|
|
303
|
+
absolute,
|
|
304
|
+
bootstrap,
|
|
305
|
+
operations,
|
|
306
|
+
thresholds,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = {
|
|
311
|
+
BOOTSTRAP_SAMPLES,
|
|
312
|
+
scorePromotion,
|
|
313
|
+
wilsonInterval,
|
|
314
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const { execFileSync } = require('node:child_process');
|
|
7
|
+
|
|
8
|
+
function requiredString(value, name) {
|
|
9
|
+
if (typeof value !== 'string' || value.length === 0 || /[\r\n]/.test(value)) {
|
|
10
|
+
throw new Error(`missing:${name}`);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createReleaseSuiteReceipt(input) {
|
|
16
|
+
const repository = requiredString(input?.repository, 'repository');
|
|
17
|
+
const workflowRef = requiredString(input?.workflowRef, 'workflowRef');
|
|
18
|
+
const runId = requiredString(input?.runId, 'runId');
|
|
19
|
+
const runAttempt = requiredString(input?.runAttempt, 'runAttempt');
|
|
20
|
+
const sha = requiredString(input?.sha, 'sha');
|
|
21
|
+
const receiptSubject = [
|
|
22
|
+
repository,
|
|
23
|
+
workflowRef,
|
|
24
|
+
runId,
|
|
25
|
+
runAttempt,
|
|
26
|
+
'build',
|
|
27
|
+
sha,
|
|
28
|
+
'release-full',
|
|
29
|
+
'true',
|
|
30
|
+
].join('|');
|
|
31
|
+
const receipt = crypto.createHash('sha256').update(receiptSubject, 'utf8').digest('hex');
|
|
32
|
+
return { receipt, receiptSubject };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function verifyReleaseSuiteReceipt(input) {
|
|
36
|
+
const required = [
|
|
37
|
+
'repository',
|
|
38
|
+
'workflowRef',
|
|
39
|
+
'runId',
|
|
40
|
+
'runAttempt',
|
|
41
|
+
'expectedSha',
|
|
42
|
+
'verifiedSha',
|
|
43
|
+
'checkoutSha',
|
|
44
|
+
'receipt',
|
|
45
|
+
'receiptSubject',
|
|
46
|
+
];
|
|
47
|
+
for (const field of required) {
|
|
48
|
+
try {
|
|
49
|
+
requiredString(input?.[field], field);
|
|
50
|
+
} catch (_error) {
|
|
51
|
+
return { allowed: false, reason: `missing:${field}` };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (input.expectedSha !== input.verifiedSha || input.expectedSha !== input.checkoutSha) {
|
|
56
|
+
return { allowed: false, reason: 'sha_mismatch' };
|
|
57
|
+
}
|
|
58
|
+
const expected = createReleaseSuiteReceipt({
|
|
59
|
+
repository: input.repository,
|
|
60
|
+
workflowRef: input.workflowRef,
|
|
61
|
+
runId: input.runId,
|
|
62
|
+
runAttempt: input.runAttempt,
|
|
63
|
+
sha: input.expectedSha,
|
|
64
|
+
});
|
|
65
|
+
if (input.receipt !== expected.receipt || input.receiptSubject !== expected.receiptSubject) {
|
|
66
|
+
return { allowed: false, reason: 'receipt_mismatch' };
|
|
67
|
+
}
|
|
68
|
+
return { allowed: true, reason: null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveCheckoutSha() {
|
|
72
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
73
|
+
encoding: 'utf8',
|
|
74
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
75
|
+
}).trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function attributionFromEnv(env) {
|
|
79
|
+
return {
|
|
80
|
+
repository: env.GITHUB_REPOSITORY,
|
|
81
|
+
workflowRef: env.GITHUB_WORKFLOW_REF,
|
|
82
|
+
runId: env.GITHUB_RUN_ID,
|
|
83
|
+
runAttempt: env.GITHUB_RUN_ATTEMPT,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function main(argv = process.argv.slice(2), env = process.env, options = {}) {
|
|
88
|
+
const mode = argv[0];
|
|
89
|
+
const checkoutSha = (options.resolveCheckoutSha || resolveCheckoutSha)();
|
|
90
|
+
const attribution = attributionFromEnv(env);
|
|
91
|
+
if (mode === 'emit') {
|
|
92
|
+
const expectedSha = requiredString(env.EXPECTED_SHA, 'expectedSha');
|
|
93
|
+
if (checkoutSha !== expectedSha) return { success: false, reason: 'sha_mismatch' };
|
|
94
|
+
const outputPath = requiredString(env.GITHUB_OUTPUT, 'githubOutput');
|
|
95
|
+
const evidence = createReleaseSuiteReceipt({ ...attribution, sha: checkoutSha });
|
|
96
|
+
fs.appendFileSync(
|
|
97
|
+
outputPath,
|
|
98
|
+
`commitSha=${checkoutSha}\nreceipt=${evidence.receipt}\nreceiptSubject=${evidence.receiptSubject}\n`,
|
|
99
|
+
'utf8',
|
|
100
|
+
);
|
|
101
|
+
return { success: true, ...evidence, commitSha: checkoutSha };
|
|
102
|
+
}
|
|
103
|
+
if (mode === 'verify') {
|
|
104
|
+
const verification = verifyReleaseSuiteReceipt({
|
|
105
|
+
...attribution,
|
|
106
|
+
expectedSha: env.EXPECTED_SHA,
|
|
107
|
+
verifiedSha: env.VERIFIED_SHA,
|
|
108
|
+
checkoutSha,
|
|
109
|
+
receipt: env.RECEIPT,
|
|
110
|
+
receiptSubject: env.RECEIPT_SUBJECT,
|
|
111
|
+
});
|
|
112
|
+
return { success: verification.allowed, ...verification };
|
|
113
|
+
}
|
|
114
|
+
return { success: false, reason: 'mode_invalid' };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (require.main === module) {
|
|
118
|
+
try {
|
|
119
|
+
const result = main();
|
|
120
|
+
if (!result.success) {
|
|
121
|
+
console.error(`Release suite receipt denied: ${result.reason}`);
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error(`Release suite receipt denied: ${error.message}`);
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
createReleaseSuiteReceipt,
|
|
132
|
+
verifyReleaseSuiteReceipt,
|
|
133
|
+
main,
|
|
134
|
+
};
|