@claude-flow/cli 3.32.24 → 3.32.26
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/.claude/helpers/.helpers-version +1 -1
- package/.claude/helpers/auto-memory-hook.mjs +430 -430
- package/.claude/helpers/helpers.manifest.json +6 -6
- package/.claude/helpers/hook-handler.cjs +565 -565
- package/.claude/helpers/intelligence.cjs +1058 -1058
- package/.claude/helpers/statusline.cjs +1060 -1060
- package/catalog-manifest.json +4 -4
- package/dist/src/commands/metaharness.js +80 -2
- package/dist/src/mcp-tools/metaharness-tools.js +81 -2
- package/dist/src/services/distill-oracle.d.ts +1 -0
- package/dist/src/services/distill-oracle.js +11 -3
- package/dist/src/services/flywheel-proposer.d.ts +87 -0
- package/dist/src/services/flywheel-proposer.js +165 -0
- package/dist/src/services/flywheel-receipt.d.ts +136 -0
- package/dist/src/services/flywheel-receipt.js +309 -0
- package/dist/src/services/flywheel-transaction.d.ts +77 -0
- package/dist/src/services/flywheel-transaction.js +378 -0
- package/dist/src/services/harness-flywheel-runtime.d.ts +9 -0
- package/dist/src/services/harness-flywheel-runtime.js +67 -2
- package/dist/src/services/harness-flywheel.d.ts +23 -0
- package/dist/src/services/harness-flywheel.js +111 -21
- package/package.json +4 -1
- package/.claude/.proven-config-version +0 -1
- package/.claude/proven-config.json +0 -42
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export declare const RECEIPT_SCHEMA = "ruflo.flywheel-receipt/v1";
|
|
2
|
+
export declare const RECEIPT_DOMAIN = "ruflo/flywheel-receipt/v1";
|
|
3
|
+
export declare const GENESIS_LEDGER_HEAD: string;
|
|
4
|
+
export type VerificationKind = 'recomputed' | 'signature-verified' | 'trusted-assertion';
|
|
5
|
+
export type ProposerName = 'local' | 'darwin';
|
|
6
|
+
export interface ReceiptSignature {
|
|
7
|
+
algorithm: 'ed25519';
|
|
8
|
+
domain: typeof RECEIPT_DOMAIN;
|
|
9
|
+
publicKeyPem: string;
|
|
10
|
+
signatureBase64: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ResourceEvidence {
|
|
13
|
+
p95LatencyMicros: number;
|
|
14
|
+
costMicrosPerTask: number;
|
|
15
|
+
tokensPerTask: number;
|
|
16
|
+
failureRate: string;
|
|
17
|
+
evaluationCostMicros: number;
|
|
18
|
+
energyMicrojoules?: number;
|
|
19
|
+
currency: string;
|
|
20
|
+
}
|
|
21
|
+
export interface PromotionStatistics {
|
|
22
|
+
ruleVersion: 'ruflo.flywheel-gate/v1';
|
|
23
|
+
relativeLift: string;
|
|
24
|
+
pairedBootstrapProbability: string;
|
|
25
|
+
pairedBootstrapDeltaCILow95: string;
|
|
26
|
+
frozenAnchorRegression: string;
|
|
27
|
+
iterations: number;
|
|
28
|
+
seedHex: string;
|
|
29
|
+
significant: boolean;
|
|
30
|
+
accepted: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface TermVerification {
|
|
33
|
+
term: string;
|
|
34
|
+
verification: VerificationKind;
|
|
35
|
+
evidenceRef: string;
|
|
36
|
+
attestor?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface EvaluationEvidence {
|
|
39
|
+
corpusRoles: {
|
|
40
|
+
selectionTaskIds: string[];
|
|
41
|
+
promotionHoldoutTaskIds: string[];
|
|
42
|
+
guardTaskIds: string[];
|
|
43
|
+
};
|
|
44
|
+
verification: Record<string, unknown>;
|
|
45
|
+
canary: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
export interface FlywheelReceiptPayload {
|
|
48
|
+
schemaVersion: typeof RECEIPT_SCHEMA;
|
|
49
|
+
receiptId: string;
|
|
50
|
+
lineageId: string;
|
|
51
|
+
candidateId: string;
|
|
52
|
+
evaluationRunId: string;
|
|
53
|
+
baselineRef: string;
|
|
54
|
+
expectedLedgerHead: string;
|
|
55
|
+
candidatePolicy: Record<string, unknown>;
|
|
56
|
+
gateVersion: string;
|
|
57
|
+
policySchemaVersion: string;
|
|
58
|
+
safetyEnvelopeRef: string;
|
|
59
|
+
requestedProposer: 'auto' | ProposerName;
|
|
60
|
+
effectiveProposer: ProposerName;
|
|
61
|
+
proposerSubstitution?: string;
|
|
62
|
+
corpusVersion: string;
|
|
63
|
+
corpusHash: string;
|
|
64
|
+
baselineScore: string;
|
|
65
|
+
candidateScore: string;
|
|
66
|
+
heldOutDeltas: string[];
|
|
67
|
+
statistics: PromotionStatistics;
|
|
68
|
+
gates: Record<string, boolean>;
|
|
69
|
+
resourceEvidence: ResourceEvidence;
|
|
70
|
+
evidence: EvaluationEvidence;
|
|
71
|
+
termVerification: TermVerification[];
|
|
72
|
+
decision: 'accepted' | 'rejected';
|
|
73
|
+
issuedAt: string;
|
|
74
|
+
expiresAt: string;
|
|
75
|
+
}
|
|
76
|
+
export interface FlywheelEvaluationReceipt {
|
|
77
|
+
payload: FlywheelReceiptPayload;
|
|
78
|
+
signature?: ReceiptSignature;
|
|
79
|
+
}
|
|
80
|
+
export interface CreateReceiptInput {
|
|
81
|
+
lineageId?: string;
|
|
82
|
+
evaluationRunId?: string;
|
|
83
|
+
baselineRef: string;
|
|
84
|
+
expectedLedgerHead?: string;
|
|
85
|
+
candidatePolicy: Record<string, unknown>;
|
|
86
|
+
gateVersion?: string;
|
|
87
|
+
policySchemaVersion?: string;
|
|
88
|
+
safetyEnvelopeRef: string;
|
|
89
|
+
requestedProposer?: 'auto' | ProposerName;
|
|
90
|
+
effectiveProposer?: ProposerName;
|
|
91
|
+
proposerSubstitution?: string;
|
|
92
|
+
corpusVersion: string;
|
|
93
|
+
corpusHash: string;
|
|
94
|
+
baselineScore: number;
|
|
95
|
+
candidateScore: number;
|
|
96
|
+
heldOutDeltas: number[];
|
|
97
|
+
frozenAnchorRegression: number;
|
|
98
|
+
gates: Record<string, boolean>;
|
|
99
|
+
resourceEvidence?: Partial<ResourceEvidence>;
|
|
100
|
+
evidence?: EvaluationEvidence;
|
|
101
|
+
termVerification?: TermVerification[];
|
|
102
|
+
now?: number;
|
|
103
|
+
ttlMs?: number;
|
|
104
|
+
privateKeyPem?: string;
|
|
105
|
+
publicKeyPem?: string;
|
|
106
|
+
bootstrapIterations?: number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* RFC-8785-compatible for the JSON domain accepted above: ECMAScript primitive
|
|
110
|
+
* serialization plus recursively sorted UTF-16 property names.
|
|
111
|
+
*/
|
|
112
|
+
export declare function canonicalizeJcs(value: unknown): string;
|
|
113
|
+
export declare function sha256Ref(value: string | Buffer): string;
|
|
114
|
+
export declare function policyCandidateId(policy: Record<string, unknown>): string;
|
|
115
|
+
/** UUIDv7 with a 48-bit millisecond timestamp and RFC-4122 variant bits. */
|
|
116
|
+
export declare function uuidV7(now?: number): string;
|
|
117
|
+
export declare function computePromotionStatistics(input: {
|
|
118
|
+
baselineScore: number;
|
|
119
|
+
candidateScore: number;
|
|
120
|
+
heldOutDeltas: number[];
|
|
121
|
+
frozenAnchorRegression: number;
|
|
122
|
+
corpusHash: string;
|
|
123
|
+
candidateId: string;
|
|
124
|
+
baselineRef: string;
|
|
125
|
+
evaluationRunId: string;
|
|
126
|
+
iterations?: number;
|
|
127
|
+
metricEpsilon?: number;
|
|
128
|
+
}): PromotionStatistics;
|
|
129
|
+
export declare function createFlywheelReceipt(input: CreateReceiptInput): FlywheelEvaluationReceipt;
|
|
130
|
+
export interface ReceiptVerification {
|
|
131
|
+
valid: boolean;
|
|
132
|
+
signed: boolean;
|
|
133
|
+
errors: string[];
|
|
134
|
+
}
|
|
135
|
+
export declare function verifyFlywheelReceipt(receipt: FlywheelEvaluationReceipt, trustedPublicKeys?: Set<string>): ReceiptVerification;
|
|
136
|
+
//# sourceMappingURL=flywheel-receipt.d.ts.map
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-322C receipt protocol.
|
|
3
|
+
*
|
|
4
|
+
* Provides deterministic JCS-style canonicalization, distinct policy/run/receipt
|
|
5
|
+
* identities, deterministic paired-bootstrap statistics, and domain-separated
|
|
6
|
+
* Ed25519 signatures. Signed receipts are immutable; promotion consumption is
|
|
7
|
+
* tracked separately by flywheel-transaction.ts.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash, randomBytes, sign as edSign, verify as edVerify, } from 'node:crypto';
|
|
10
|
+
export const RECEIPT_SCHEMA = 'ruflo.flywheel-receipt/v1';
|
|
11
|
+
export const RECEIPT_DOMAIN = 'ruflo/flywheel-receipt/v1';
|
|
12
|
+
export const GENESIS_LEDGER_HEAD = `sha256:${'0'.repeat(64)}`;
|
|
13
|
+
function assertJsonValue(value, path = '$') {
|
|
14
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
15
|
+
return;
|
|
16
|
+
if (typeof value === 'number') {
|
|
17
|
+
if (!Number.isFinite(value) || Object.is(value, -0)) {
|
|
18
|
+
throw new Error(`non-canonical number at ${path}`);
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
value.forEach((v, i) => {
|
|
24
|
+
if (v === undefined)
|
|
25
|
+
throw new Error(`undefined array member at ${path}[${i}]`);
|
|
26
|
+
assertJsonValue(v, `${path}[${i}]`);
|
|
27
|
+
});
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === 'object') {
|
|
31
|
+
for (const [key, child] of Object.entries(value)) {
|
|
32
|
+
if (child === undefined)
|
|
33
|
+
throw new Error(`undefined property at ${path}.${key}`);
|
|
34
|
+
assertJsonValue(child, `${path}.${key}`);
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`unsupported JSON value at ${path}`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* RFC-8785-compatible for the JSON domain accepted above: ECMAScript primitive
|
|
42
|
+
* serialization plus recursively sorted UTF-16 property names.
|
|
43
|
+
*/
|
|
44
|
+
export function canonicalizeJcs(value) {
|
|
45
|
+
assertJsonValue(value);
|
|
46
|
+
const encode = (v) => {
|
|
47
|
+
if (v === null || typeof v !== 'object')
|
|
48
|
+
return JSON.stringify(v);
|
|
49
|
+
if (Array.isArray(v))
|
|
50
|
+
return `[${v.map(encode).join(',')}]`;
|
|
51
|
+
const obj = v;
|
|
52
|
+
return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${encode(obj[k])}`).join(',')}}`;
|
|
53
|
+
};
|
|
54
|
+
return encode(value);
|
|
55
|
+
}
|
|
56
|
+
export function sha256Ref(value) {
|
|
57
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
58
|
+
}
|
|
59
|
+
export function policyCandidateId(policy) {
|
|
60
|
+
return sha256Ref(canonicalizeJcs(policy));
|
|
61
|
+
}
|
|
62
|
+
/** UUIDv7 with a 48-bit millisecond timestamp and RFC-4122 variant bits. */
|
|
63
|
+
export function uuidV7(now = Date.now()) {
|
|
64
|
+
const bytes = randomBytes(16);
|
|
65
|
+
const timestamp = BigInt(now);
|
|
66
|
+
for (let i = 5; i >= 0; i--)
|
|
67
|
+
bytes[5 - i] = Number((timestamp >> BigInt(i * 8)) & 0xffn);
|
|
68
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x70;
|
|
69
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
70
|
+
const hex = bytes.toString('hex');
|
|
71
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
72
|
+
}
|
|
73
|
+
const decimal = (value, scale = 12) => {
|
|
74
|
+
if (!Number.isFinite(value))
|
|
75
|
+
throw new Error('metric must be finite');
|
|
76
|
+
const normalized = value.toFixed(scale).replace(/\.?0+$/, '');
|
|
77
|
+
return normalized === '-0' || normalized === '' ? '0' : normalized;
|
|
78
|
+
};
|
|
79
|
+
function seedFrom(parts) {
|
|
80
|
+
const digest = createHash('sha256').update(parts.join('')).digest();
|
|
81
|
+
return { seed: digest.readUInt32BE(0), hex: digest.toString('hex') };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Deterministic three-way quickselect. Bootstrap promotion needs one quantile,
|
|
85
|
+
* not a fully sorted distribution; selecting it in expected O(n) removes the
|
|
86
|
+
* O(n log n) sort from every evaluation. Three-way partitioning also keeps the
|
|
87
|
+
* common all-equal bootstrap case linear.
|
|
88
|
+
*/
|
|
89
|
+
function selectKth(values, k) {
|
|
90
|
+
let left = 0;
|
|
91
|
+
let right = values.length - 1;
|
|
92
|
+
while (left <= right) {
|
|
93
|
+
const pivot = values[(left + right) >>> 1];
|
|
94
|
+
let lower = left;
|
|
95
|
+
let scan = left;
|
|
96
|
+
let upper = right;
|
|
97
|
+
while (scan <= upper) {
|
|
98
|
+
if (values[scan] < pivot) {
|
|
99
|
+
[values[lower], values[scan]] = [values[scan], values[lower]];
|
|
100
|
+
lower++;
|
|
101
|
+
scan++;
|
|
102
|
+
}
|
|
103
|
+
else if (values[scan] > pivot) {
|
|
104
|
+
[values[scan], values[upper]] = [values[upper], values[scan]];
|
|
105
|
+
upper--;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
scan++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (k < lower)
|
|
112
|
+
right = lower - 1;
|
|
113
|
+
else if (k > upper)
|
|
114
|
+
left = upper + 1;
|
|
115
|
+
else
|
|
116
|
+
return values[k];
|
|
117
|
+
}
|
|
118
|
+
return values[k] ?? 0;
|
|
119
|
+
}
|
|
120
|
+
export function computePromotionStatistics(input) {
|
|
121
|
+
const iterations = input.iterations ?? 10_000;
|
|
122
|
+
if (!Number.isInteger(iterations) || iterations < 100)
|
|
123
|
+
throw new Error('bootstrap iterations must be >= 100');
|
|
124
|
+
const n = input.heldOutDeltas.length;
|
|
125
|
+
const metricEpsilon = input.metricEpsilon ?? 1e-12;
|
|
126
|
+
const relativeLift = (input.candidateScore - input.baselineScore) / Math.max(Math.abs(input.baselineScore), metricEpsilon);
|
|
127
|
+
const seeded = seedFrom([
|
|
128
|
+
'ruflo/bootstrap/v1',
|
|
129
|
+
input.corpusHash,
|
|
130
|
+
input.candidateId,
|
|
131
|
+
input.baselineRef,
|
|
132
|
+
input.evaluationRunId,
|
|
133
|
+
]);
|
|
134
|
+
let state = seeded.seed >>> 0;
|
|
135
|
+
const rnd = () => {
|
|
136
|
+
state = (1664525 * state + 1013904223) >>> 0;
|
|
137
|
+
return state / 4294967296;
|
|
138
|
+
};
|
|
139
|
+
const means = new Array(iterations);
|
|
140
|
+
let positiveMeans = 0;
|
|
141
|
+
if (n === 0) {
|
|
142
|
+
means.fill(0);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
for (let b = 0; b < iterations; b++) {
|
|
146
|
+
let total = 0;
|
|
147
|
+
for (let i = 0; i < n; i++)
|
|
148
|
+
total += input.heldOutDeltas[Math.floor(rnd() * n)];
|
|
149
|
+
const mean = total / n;
|
|
150
|
+
means[b] = mean;
|
|
151
|
+
if (mean > 0)
|
|
152
|
+
positiveMeans++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const probability = positiveMeans / iterations;
|
|
156
|
+
const ciLow = selectKth(means, Math.floor(0.025 * iterations));
|
|
157
|
+
const significant = probability >= 0.95 && ciLow > 0;
|
|
158
|
+
const accepted = relativeLift >= 0.02 && significant && input.frozenAnchorRegression <= 0;
|
|
159
|
+
return {
|
|
160
|
+
ruleVersion: 'ruflo.flywheel-gate/v1',
|
|
161
|
+
relativeLift: decimal(relativeLift),
|
|
162
|
+
pairedBootstrapProbability: decimal(probability),
|
|
163
|
+
pairedBootstrapDeltaCILow95: decimal(ciLow),
|
|
164
|
+
frozenAnchorRegression: decimal(input.frozenAnchorRegression),
|
|
165
|
+
iterations,
|
|
166
|
+
seedHex: seeded.hex,
|
|
167
|
+
significant,
|
|
168
|
+
accepted,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function receiptIdentityPayload(payload) {
|
|
172
|
+
return payload;
|
|
173
|
+
}
|
|
174
|
+
function signedBytes(payload) {
|
|
175
|
+
return Buffer.concat([
|
|
176
|
+
Buffer.from(RECEIPT_DOMAIN, 'utf8'),
|
|
177
|
+
Buffer.from([0]),
|
|
178
|
+
Buffer.from(canonicalizeJcs(payload), 'utf8'),
|
|
179
|
+
]);
|
|
180
|
+
}
|
|
181
|
+
export function createFlywheelReceipt(input) {
|
|
182
|
+
const now = input.now ?? Date.now();
|
|
183
|
+
const evaluationRunId = input.evaluationRunId ?? uuidV7(now);
|
|
184
|
+
const lineageId = input.lineageId ?? uuidV7(now);
|
|
185
|
+
const candidateId = policyCandidateId(input.candidatePolicy);
|
|
186
|
+
const statistics = computePromotionStatistics({
|
|
187
|
+
baselineScore: input.baselineScore,
|
|
188
|
+
candidateScore: input.candidateScore,
|
|
189
|
+
heldOutDeltas: input.heldOutDeltas,
|
|
190
|
+
frozenAnchorRegression: input.frozenAnchorRegression,
|
|
191
|
+
corpusHash: input.corpusHash,
|
|
192
|
+
candidateId,
|
|
193
|
+
baselineRef: input.baselineRef,
|
|
194
|
+
evaluationRunId,
|
|
195
|
+
iterations: input.bootstrapIterations,
|
|
196
|
+
});
|
|
197
|
+
const decision = statistics.accepted && Object.values(input.gates).every(Boolean) ? 'accepted' : 'rejected';
|
|
198
|
+
const base = {
|
|
199
|
+
schemaVersion: RECEIPT_SCHEMA,
|
|
200
|
+
lineageId,
|
|
201
|
+
candidateId,
|
|
202
|
+
evaluationRunId,
|
|
203
|
+
baselineRef: input.baselineRef,
|
|
204
|
+
expectedLedgerHead: input.expectedLedgerHead ?? GENESIS_LEDGER_HEAD,
|
|
205
|
+
candidatePolicy: input.candidatePolicy,
|
|
206
|
+
gateVersion: input.gateVersion ?? statistics.ruleVersion,
|
|
207
|
+
policySchemaVersion: input.policySchemaVersion ?? 'ruflo.retrieval-policy/v1',
|
|
208
|
+
safetyEnvelopeRef: input.safetyEnvelopeRef,
|
|
209
|
+
requestedProposer: input.requestedProposer ?? 'local',
|
|
210
|
+
effectiveProposer: input.effectiveProposer ?? 'local',
|
|
211
|
+
...(input.proposerSubstitution ? { proposerSubstitution: input.proposerSubstitution } : {}),
|
|
212
|
+
corpusVersion: input.corpusVersion,
|
|
213
|
+
corpusHash: input.corpusHash,
|
|
214
|
+
baselineScore: decimal(input.baselineScore),
|
|
215
|
+
candidateScore: decimal(input.candidateScore),
|
|
216
|
+
heldOutDeltas: input.heldOutDeltas.map((v) => decimal(v)),
|
|
217
|
+
statistics,
|
|
218
|
+
gates: input.gates,
|
|
219
|
+
resourceEvidence: {
|
|
220
|
+
p95LatencyMicros: input.resourceEvidence?.p95LatencyMicros ?? 0,
|
|
221
|
+
costMicrosPerTask: input.resourceEvidence?.costMicrosPerTask ?? 0,
|
|
222
|
+
tokensPerTask: input.resourceEvidence?.tokensPerTask ?? 0,
|
|
223
|
+
failureRate: input.resourceEvidence?.failureRate ?? '0',
|
|
224
|
+
evaluationCostMicros: input.resourceEvidence?.evaluationCostMicros ?? 0,
|
|
225
|
+
...(input.resourceEvidence?.energyMicrojoules === undefined
|
|
226
|
+
? {}
|
|
227
|
+
: { energyMicrojoules: input.resourceEvidence.energyMicrojoules }),
|
|
228
|
+
currency: input.resourceEvidence?.currency ?? 'USD',
|
|
229
|
+
},
|
|
230
|
+
evidence: input.evidence ?? {
|
|
231
|
+
corpusRoles: {
|
|
232
|
+
selectionTaskIds: [],
|
|
233
|
+
promotionHoldoutTaskIds: [],
|
|
234
|
+
guardTaskIds: [],
|
|
235
|
+
},
|
|
236
|
+
verification: {},
|
|
237
|
+
canary: {},
|
|
238
|
+
},
|
|
239
|
+
termVerification: input.termVerification ?? [],
|
|
240
|
+
decision,
|
|
241
|
+
issuedAt: new Date(now).toISOString(),
|
|
242
|
+
expiresAt: new Date(now + (input.ttlMs ?? 24 * 60 * 60 * 1000)).toISOString(),
|
|
243
|
+
};
|
|
244
|
+
const receiptId = sha256Ref(canonicalizeJcs(receiptIdentityPayload(base)));
|
|
245
|
+
const payload = { ...base, receiptId };
|
|
246
|
+
const receipt = { payload };
|
|
247
|
+
if (input.privateKeyPem || input.publicKeyPem) {
|
|
248
|
+
if (!input.privateKeyPem || !input.publicKeyPem)
|
|
249
|
+
throw new Error('both Ed25519 private and public PEM keys are required');
|
|
250
|
+
receipt.signature = {
|
|
251
|
+
algorithm: 'ed25519',
|
|
252
|
+
domain: RECEIPT_DOMAIN,
|
|
253
|
+
publicKeyPem: input.publicKeyPem,
|
|
254
|
+
signatureBase64: edSign(null, signedBytes(payload), input.privateKeyPem).toString('base64'),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return receipt;
|
|
258
|
+
}
|
|
259
|
+
export function verifyFlywheelReceipt(receipt, trustedPublicKeys) {
|
|
260
|
+
const errors = [];
|
|
261
|
+
try {
|
|
262
|
+
if (receipt.payload.schemaVersion !== RECEIPT_SCHEMA)
|
|
263
|
+
errors.push('unsupported receipt schema');
|
|
264
|
+
const { receiptId: _receiptId, ...base } = receipt.payload;
|
|
265
|
+
const expectedId = sha256Ref(canonicalizeJcs(receiptIdentityPayload(base)));
|
|
266
|
+
if (expectedId !== receipt.payload.receiptId)
|
|
267
|
+
errors.push('receipt content ID mismatch');
|
|
268
|
+
if (policyCandidateId(receipt.payload.candidatePolicy) !== receipt.payload.candidateId)
|
|
269
|
+
errors.push('candidate content ID mismatch');
|
|
270
|
+
const recomputedStatistics = computePromotionStatistics({
|
|
271
|
+
baselineScore: Number(receipt.payload.baselineScore),
|
|
272
|
+
candidateScore: Number(receipt.payload.candidateScore),
|
|
273
|
+
heldOutDeltas: receipt.payload.heldOutDeltas.map(Number),
|
|
274
|
+
frozenAnchorRegression: Number(receipt.payload.statistics.frozenAnchorRegression),
|
|
275
|
+
corpusHash: receipt.payload.corpusHash,
|
|
276
|
+
candidateId: receipt.payload.candidateId,
|
|
277
|
+
baselineRef: receipt.payload.baselineRef,
|
|
278
|
+
evaluationRunId: receipt.payload.evaluationRunId,
|
|
279
|
+
iterations: receipt.payload.statistics.iterations,
|
|
280
|
+
});
|
|
281
|
+
if (canonicalizeJcs(recomputedStatistics) !== canonicalizeJcs(receipt.payload.statistics)) {
|
|
282
|
+
errors.push('statistical decision does not recompute');
|
|
283
|
+
}
|
|
284
|
+
const recomputedDecision = recomputedStatistics.accepted && Object.values(receipt.payload.gates).every(Boolean)
|
|
285
|
+
? 'accepted'
|
|
286
|
+
: 'rejected';
|
|
287
|
+
if (receipt.payload.decision !== recomputedDecision)
|
|
288
|
+
errors.push('receipt decision does not recompute');
|
|
289
|
+
if (!receipt.signature) {
|
|
290
|
+
errors.push('receipt is unsigned');
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
if (receipt.signature.algorithm !== 'ed25519' || receipt.signature.domain !== RECEIPT_DOMAIN) {
|
|
294
|
+
errors.push('unsupported signature metadata');
|
|
295
|
+
}
|
|
296
|
+
else if (trustedPublicKeys && !trustedPublicKeys.has(receipt.signature.publicKeyPem)) {
|
|
297
|
+
errors.push('receipt signer is not trusted');
|
|
298
|
+
}
|
|
299
|
+
else if (!edVerify(null, signedBytes(receipt.payload), receipt.signature.publicKeyPem, Buffer.from(receipt.signature.signatureBase64, 'base64'))) {
|
|
300
|
+
errors.push('receipt signature invalid');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
errors.push(`verification error: ${error.message}`);
|
|
306
|
+
}
|
|
307
|
+
return { valid: errors.length === 0, signed: !!receipt.signature, errors };
|
|
308
|
+
}
|
|
309
|
+
//# sourceMappingURL=flywheel-receipt.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type ApplyResult } from '../config/harness-feedback-applier.js';
|
|
2
|
+
import { type FlywheelEvaluationReceipt } from './flywheel-receipt.js';
|
|
3
|
+
declare const STATE_VERSION: 1;
|
|
4
|
+
export interface ReceiptState {
|
|
5
|
+
receiptId: string;
|
|
6
|
+
status: 'evaluated' | 'consumed' | 'expired' | 'revoked';
|
|
7
|
+
registeredAt: number;
|
|
8
|
+
promotedAt: number | null;
|
|
9
|
+
promotionTransactionId: string | null;
|
|
10
|
+
}
|
|
11
|
+
export interface PromotionCommit {
|
|
12
|
+
commitId: string;
|
|
13
|
+
transactionId: string;
|
|
14
|
+
receiptId: string;
|
|
15
|
+
lineageId: string;
|
|
16
|
+
sequence: number;
|
|
17
|
+
previousLedgerHead: string;
|
|
18
|
+
baselineRef: string;
|
|
19
|
+
candidateId: string;
|
|
20
|
+
servingEpoch: number;
|
|
21
|
+
proposer: string;
|
|
22
|
+
proposerSubstitution?: string;
|
|
23
|
+
promotedAt: number;
|
|
24
|
+
}
|
|
25
|
+
export interface FlywheelTransactionState {
|
|
26
|
+
version: typeof STATE_VERSION;
|
|
27
|
+
activeChampionRef: string | null;
|
|
28
|
+
activePolicy: Record<string, unknown> | null;
|
|
29
|
+
activeGateVersion: string | null;
|
|
30
|
+
activePolicySchemaVersion: string | null;
|
|
31
|
+
activeSafetyEnvelopeRef: string | null;
|
|
32
|
+
ledgerHead: string;
|
|
33
|
+
servingEpoch: number;
|
|
34
|
+
materializedServingEpoch: number;
|
|
35
|
+
servedChampionRef: string | null;
|
|
36
|
+
receiptStates: Record<string, ReceiptState>;
|
|
37
|
+
commits: PromotionCommit[];
|
|
38
|
+
}
|
|
39
|
+
export interface PromotionResult {
|
|
40
|
+
success: boolean;
|
|
41
|
+
idempotent: boolean;
|
|
42
|
+
reason: string;
|
|
43
|
+
transactionId?: string;
|
|
44
|
+
receiptId?: string;
|
|
45
|
+
championRef?: string;
|
|
46
|
+
ledgerHead?: string;
|
|
47
|
+
servingEpoch?: number;
|
|
48
|
+
materialized?: boolean;
|
|
49
|
+
materializeReason?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface PromoteOptions {
|
|
52
|
+
confirm: boolean;
|
|
53
|
+
now?: number;
|
|
54
|
+
trustedPublicKeys?: Set<string>;
|
|
55
|
+
approvedAttestors?: Set<string>;
|
|
56
|
+
allowedProposerSubstitutions?: Set<string>;
|
|
57
|
+
applyFn?: (root: string, policy: Record<string, unknown>, championRef: string, previous: string | null, now: number) => ApplyResult;
|
|
58
|
+
/** Test-only crash hook; production callers leave unset. */
|
|
59
|
+
faultAt?: 'before-commit' | 'after-commit-before-materialize';
|
|
60
|
+
}
|
|
61
|
+
export declare function readFlywheelTransactionState(root: string): FlywheelTransactionState;
|
|
62
|
+
export declare function readFlywheelReceipt(root: string, receiptId: string): FlywheelEvaluationReceipt | null;
|
|
63
|
+
export declare function registerFlywheelReceipt(root: string, receipt: FlywheelEvaluationReceipt, now?: number): Promise<ReceiptState>;
|
|
64
|
+
export declare function recoverFlywheelMaterialization(root: string, opts?: Pick<PromoteOptions, 'now' | 'applyFn'>): Promise<PromotionResult>;
|
|
65
|
+
export declare function promoteFlywheelCandidate(root: string, receiptId: string, options: PromoteOptions): Promise<PromotionResult>;
|
|
66
|
+
export declare function listFlywheelReceipts(root: string): Array<{
|
|
67
|
+
receipt: FlywheelEvaluationReceipt;
|
|
68
|
+
state: ReceiptState | null;
|
|
69
|
+
}>;
|
|
70
|
+
export declare function verifyFlywheelLedger(root: string): {
|
|
71
|
+
valid: boolean;
|
|
72
|
+
errors: string[];
|
|
73
|
+
commits: number;
|
|
74
|
+
head: string;
|
|
75
|
+
};
|
|
76
|
+
export {};
|
|
77
|
+
//# sourceMappingURL=flywheel-transaction.d.ts.map
|