@claude-flow/cli 3.32.25 → 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.
@@ -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