@claude-flow/cli 3.32.25 → 3.32.29
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/index.d.ts +1 -0
- package/dist/src/commands/index.js +5 -3
- package/dist/src/commands/memory.js +49 -5
- package/dist/src/commands/metaharness.js +100 -2
- package/dist/src/commands/policy.d.ts +4 -0
- package/dist/src/commands/policy.js +107 -0
- package/dist/src/index.js +18 -0
- package/dist/src/mcp-client.js +25 -1
- package/dist/src/mcp-tools/capability-brain.d.ts +134 -0
- package/dist/src/mcp-tools/capability-brain.js +697 -0
- package/dist/src/mcp-tools/guidance-tools.d.ts +2 -0
- package/dist/src/mcp-tools/guidance-tools.js +369 -37
- package/dist/src/mcp-tools/index.d.ts +4 -1
- package/dist/src/mcp-tools/index.js +3 -1
- package/dist/src/mcp-tools/memory-tools.js +26 -0
- package/dist/src/mcp-tools/metaharness-tools.js +106 -1
- package/dist/src/mcp-tools/policy-tools.d.ts +3 -0
- package/dist/src/mcp-tools/policy-tools.js +121 -0
- package/dist/src/memory/memory-bridge.d.ts +11 -0
- package/dist/src/memory/memory-bridge.js +100 -21
- package/dist/src/memory/memory-initializer.d.ts +22 -1
- package/dist/src/memory/memory-initializer.js +184 -39
- package/dist/src/services/bounded-worker-pool.d.ts +28 -0
- package/dist/src/services/bounded-worker-pool.js +90 -0
- 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 +11 -0
- package/dist/src/services/harness-flywheel-runtime.js +85 -2
- package/dist/src/services/harness-flywheel.d.ts +27 -1
- package/dist/src/services/harness-flywheel.js +138 -27
- package/dist/src/services/policy-runtime.d.ts +38 -0
- package/dist/src/services/policy-runtime.js +340 -0
- package/package.json +23 -5
- package/plugins/ruflo-metaharness/scripts/smoke.sh +22 -14
- package/plugins/ruflo-metaharness/scripts/test-mcp-tools.mjs +3 -1
- package/.claude/.proven-config-version +0 -1
- package/.claude/proven-config.json +0 -42
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-322B proposer adapter.
|
|
3
|
+
*
|
|
4
|
+
* Proposers are untrusted candidate generators. This adapter normalizes local
|
|
5
|
+
* and Darwin output, enforces hard policy/resource admissibility before Pareto
|
|
6
|
+
* ranking, and makes auto-mode substitution evaluation-only by default.
|
|
7
|
+
*/
|
|
8
|
+
import { type ProposerName } from './flywheel-receipt.js';
|
|
9
|
+
export type ProposerMode = 'auto' | ProposerName;
|
|
10
|
+
export interface SafetyEnvelope {
|
|
11
|
+
ref: string;
|
|
12
|
+
allowedPolicyKeys: string[];
|
|
13
|
+
numericBounds?: Record<string, {
|
|
14
|
+
min?: number;
|
|
15
|
+
max?: number;
|
|
16
|
+
}>;
|
|
17
|
+
maxP95LatencyMicros: number;
|
|
18
|
+
maxCostMicrosPerTask: number;
|
|
19
|
+
maxTokensPerTask: number;
|
|
20
|
+
maxFailureRate: number;
|
|
21
|
+
maxEvaluationCostMicros: number;
|
|
22
|
+
}
|
|
23
|
+
export interface CandidateResources {
|
|
24
|
+
p95LatencyMicros: number;
|
|
25
|
+
costMicrosPerTask: number;
|
|
26
|
+
tokensPerTask: number;
|
|
27
|
+
failureRate: number;
|
|
28
|
+
evaluationCostMicros: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ProposedCandidate {
|
|
31
|
+
policy: Record<string, unknown>;
|
|
32
|
+
resources: CandidateResources;
|
|
33
|
+
score?: number;
|
|
34
|
+
mutation?: string;
|
|
35
|
+
parentIds?: string[];
|
|
36
|
+
}
|
|
37
|
+
export interface NormalizedCandidate extends ProposedCandidate {
|
|
38
|
+
candidateId: string;
|
|
39
|
+
admissible: boolean;
|
|
40
|
+
rejectionReasons: string[];
|
|
41
|
+
}
|
|
42
|
+
export interface CandidateArchive {
|
|
43
|
+
archiveVersion: 'ruflo.candidate-archive/v1';
|
|
44
|
+
archiveId: string;
|
|
45
|
+
requestedProposer: ProposerMode;
|
|
46
|
+
effectiveProposer: ProposerName;
|
|
47
|
+
proposerSubstitution?: string;
|
|
48
|
+
promotionAllowed: boolean;
|
|
49
|
+
baselineRef: string;
|
|
50
|
+
safetyEnvelopeRef: string;
|
|
51
|
+
seed: number;
|
|
52
|
+
candidates: NormalizedCandidate[];
|
|
53
|
+
admissibleCandidates: NormalizedCandidate[];
|
|
54
|
+
paretoCandidates: NormalizedCandidate[];
|
|
55
|
+
completed: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface DarwinRunInput {
|
|
58
|
+
baselinePolicy: Record<string, unknown>;
|
|
59
|
+
seed: number;
|
|
60
|
+
budget: {
|
|
61
|
+
maxEvaluationCostMicros: number;
|
|
62
|
+
maxConcurrency: number;
|
|
63
|
+
maxWallTimeMs: number;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export type DarwinInvoker = (input: DarwinRunInput) => Promise<{
|
|
67
|
+
completed: boolean;
|
|
68
|
+
candidates: ProposedCandidate[];
|
|
69
|
+
reason?: string;
|
|
70
|
+
}>;
|
|
71
|
+
export interface ProposeCandidatesInput {
|
|
72
|
+
mode: ProposerMode;
|
|
73
|
+
baselinePolicy: Record<string, unknown>;
|
|
74
|
+
safetyEnvelope: SafetyEnvelope;
|
|
75
|
+
seed: number;
|
|
76
|
+
localProposer: (baseline: Record<string, unknown>, seed: number) => ProposedCandidate[] | Promise<ProposedCandidate[]>;
|
|
77
|
+
darwinInvoker?: DarwinInvoker;
|
|
78
|
+
allowSubstitutionPromotion?: boolean;
|
|
79
|
+
maxConcurrency?: number;
|
|
80
|
+
maxWallTimeMs?: number;
|
|
81
|
+
maxCandidates?: number;
|
|
82
|
+
}
|
|
83
|
+
export declare class DarwinUnavailableError extends Error {
|
|
84
|
+
constructor(message: string);
|
|
85
|
+
}
|
|
86
|
+
export declare function proposeFlywheelCandidates(input: ProposeCandidatesInput): Promise<CandidateArchive>;
|
|
87
|
+
//# sourceMappingURL=flywheel-proposer.d.ts.map
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-322B proposer adapter.
|
|
3
|
+
*
|
|
4
|
+
* Proposers are untrusted candidate generators. This adapter normalizes local
|
|
5
|
+
* and Darwin output, enforces hard policy/resource admissibility before Pareto
|
|
6
|
+
* ranking, and makes auto-mode substitution evaluation-only by default.
|
|
7
|
+
*/
|
|
8
|
+
import { canonicalizeJcs, policyCandidateId, sha256Ref } from './flywheel-receipt.js';
|
|
9
|
+
export class DarwinUnavailableError extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'DarwinUnavailableError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function validateCandidate(candidate, envelope) {
|
|
16
|
+
const reasons = [];
|
|
17
|
+
const allowed = new Set(envelope.allowedPolicyKeys);
|
|
18
|
+
for (const [key, value] of Object.entries(candidate.policy)) {
|
|
19
|
+
if (!allowed.has(key)) {
|
|
20
|
+
reasons.push(`policy key not allowed: ${key}`);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const bounds = envelope.numericBounds?.[key];
|
|
24
|
+
if (bounds && typeof value === 'number') {
|
|
25
|
+
if (bounds.min !== undefined && value < bounds.min)
|
|
26
|
+
reasons.push(`${key} below minimum`);
|
|
27
|
+
if (bounds.max !== undefined && value > bounds.max)
|
|
28
|
+
reasons.push(`${key} above maximum`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const r = candidate.resources;
|
|
32
|
+
if (r.p95LatencyMicros > envelope.maxP95LatencyMicros)
|
|
33
|
+
reasons.push('p95 latency exceeds envelope');
|
|
34
|
+
if (r.costMicrosPerTask > envelope.maxCostMicrosPerTask)
|
|
35
|
+
reasons.push('cost per task exceeds envelope');
|
|
36
|
+
if (r.tokensPerTask > envelope.maxTokensPerTask)
|
|
37
|
+
reasons.push('tokens per task exceeds envelope');
|
|
38
|
+
if (r.failureRate > envelope.maxFailureRate)
|
|
39
|
+
reasons.push('failure rate exceeds envelope');
|
|
40
|
+
if (r.evaluationCostMicros > envelope.maxEvaluationCostMicros)
|
|
41
|
+
reasons.push('evaluation cost exceeds envelope');
|
|
42
|
+
return {
|
|
43
|
+
...candidate,
|
|
44
|
+
candidateId: policyCandidateId(candidate.policy),
|
|
45
|
+
admissible: reasons.length === 0,
|
|
46
|
+
rejectionReasons: reasons,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function finalizeArchive(input) {
|
|
50
|
+
const normalized = input.candidates.map((candidate) => validateCandidate(candidate, input.safetyEnvelope));
|
|
51
|
+
const admissible = normalized.filter((candidate) => candidate.admissible);
|
|
52
|
+
const dominates = (a, b) => {
|
|
53
|
+
const av = [
|
|
54
|
+
a.score ?? 0,
|
|
55
|
+
-a.resources.p95LatencyMicros,
|
|
56
|
+
-a.resources.costMicrosPerTask,
|
|
57
|
+
-a.resources.tokensPerTask,
|
|
58
|
+
-a.resources.failureRate,
|
|
59
|
+
-a.resources.evaluationCostMicros,
|
|
60
|
+
];
|
|
61
|
+
const bv = [
|
|
62
|
+
b.score ?? 0,
|
|
63
|
+
-b.resources.p95LatencyMicros,
|
|
64
|
+
-b.resources.costMicrosPerTask,
|
|
65
|
+
-b.resources.tokensPerTask,
|
|
66
|
+
-b.resources.failureRate,
|
|
67
|
+
-b.resources.evaluationCostMicros,
|
|
68
|
+
];
|
|
69
|
+
return av.every((value, index) => value >= bv[index])
|
|
70
|
+
&& av.some((value, index) => value > bv[index]);
|
|
71
|
+
};
|
|
72
|
+
const paretoCandidates = admissible.filter((candidate, index) => !admissible.some((other, otherIndex) => otherIndex !== index && dominates(other, candidate)));
|
|
73
|
+
const core = {
|
|
74
|
+
archiveVersion: 'ruflo.candidate-archive/v1',
|
|
75
|
+
requestedProposer: input.requested,
|
|
76
|
+
effectiveProposer: input.effective,
|
|
77
|
+
...(input.substitution ? { proposerSubstitution: input.substitution } : {}),
|
|
78
|
+
promotionAllowed: input.promotionAllowed,
|
|
79
|
+
baselineRef: input.baselineRef,
|
|
80
|
+
safetyEnvelopeRef: input.safetyEnvelope.ref,
|
|
81
|
+
seed: input.seed,
|
|
82
|
+
candidates: normalized,
|
|
83
|
+
completed: input.completed,
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
...core,
|
|
87
|
+
archiveId: sha256Ref(canonicalizeJcs(core)),
|
|
88
|
+
admissibleCandidates: admissible,
|
|
89
|
+
paretoCandidates,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export async function proposeFlywheelCandidates(input) {
|
|
93
|
+
const baselineRef = policyCandidateId(input.baselinePolicy);
|
|
94
|
+
const runLocal = async (substitution) => finalizeArchive({
|
|
95
|
+
requested: input.mode,
|
|
96
|
+
effective: 'local',
|
|
97
|
+
substitution,
|
|
98
|
+
promotionAllowed: substitution ? input.allowSubstitutionPromotion === true : true,
|
|
99
|
+
baselineRef,
|
|
100
|
+
safetyEnvelope: input.safetyEnvelope,
|
|
101
|
+
seed: input.seed,
|
|
102
|
+
candidates: await input.localProposer(input.baselinePolicy, input.seed),
|
|
103
|
+
completed: true,
|
|
104
|
+
});
|
|
105
|
+
if (input.mode === 'local')
|
|
106
|
+
return runLocal();
|
|
107
|
+
if (!input.darwinInvoker) {
|
|
108
|
+
if (input.mode === 'darwin')
|
|
109
|
+
throw new DarwinUnavailableError('Darwin explicitly requested but unavailable');
|
|
110
|
+
return runLocal('darwin-unavailable');
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const maxWallTimeMs = input.maxWallTimeMs ?? 60_000;
|
|
114
|
+
let timeout;
|
|
115
|
+
const result = await Promise.race([
|
|
116
|
+
input.darwinInvoker({
|
|
117
|
+
baselinePolicy: input.baselinePolicy,
|
|
118
|
+
seed: input.seed,
|
|
119
|
+
budget: {
|
|
120
|
+
maxEvaluationCostMicros: input.safetyEnvelope.maxEvaluationCostMicros,
|
|
121
|
+
maxConcurrency: input.maxConcurrency ?? 2,
|
|
122
|
+
maxWallTimeMs,
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
new Promise((_, reject) => {
|
|
126
|
+
timeout = setTimeout(() => reject(new DarwinUnavailableError('Darwin exceeded wall-time budget')), maxWallTimeMs);
|
|
127
|
+
timeout.unref?.();
|
|
128
|
+
}),
|
|
129
|
+
]).finally(() => {
|
|
130
|
+
if (timeout)
|
|
131
|
+
clearTimeout(timeout);
|
|
132
|
+
});
|
|
133
|
+
if (!result.completed) {
|
|
134
|
+
if (input.mode === 'darwin')
|
|
135
|
+
throw new DarwinUnavailableError(result.reason ?? 'Darwin archive incomplete');
|
|
136
|
+
return runLocal(`darwin-incomplete:${result.reason ?? 'unknown'}`);
|
|
137
|
+
}
|
|
138
|
+
if (result.candidates.length > (input.maxCandidates ?? 256)) {
|
|
139
|
+
throw new DarwinUnavailableError('Darwin archive exceeded candidate-count budget');
|
|
140
|
+
}
|
|
141
|
+
const evaluationCost = result.candidates.reduce((sum, candidate) => sum + candidate.resources.evaluationCostMicros, 0);
|
|
142
|
+
if (evaluationCost > input.safetyEnvelope.maxEvaluationCostMicros) {
|
|
143
|
+
throw new DarwinUnavailableError('Darwin archive exceeded evaluation-cost budget');
|
|
144
|
+
}
|
|
145
|
+
return finalizeArchive({
|
|
146
|
+
requested: input.mode,
|
|
147
|
+
effective: 'darwin',
|
|
148
|
+
promotionAllowed: true,
|
|
149
|
+
baselineRef,
|
|
150
|
+
safetyEnvelope: input.safetyEnvelope,
|
|
151
|
+
seed: input.seed,
|
|
152
|
+
candidates: result.candidates,
|
|
153
|
+
completed: true,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (input.mode === 'darwin') {
|
|
158
|
+
if (error instanceof DarwinUnavailableError)
|
|
159
|
+
throw error;
|
|
160
|
+
throw new DarwinUnavailableError(`Darwin failed closed: ${error.message}`);
|
|
161
|
+
}
|
|
162
|
+
return runLocal(`darwin-error:${error.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=flywheel-proposer.js.map
|
|
@@ -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
|