cipher-security 2.1.0 → 2.2.0
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/bin/cipher.js +10 -0
- package/lib/analyze/consistency.js +566 -0
- package/lib/analyze/constitution.js +110 -0
- package/lib/analyze/sharding.js +251 -0
- package/lib/autonomous/agent-tool.js +165 -0
- package/lib/autonomous/framework.js +17 -0
- package/lib/autonomous/handoff.js +506 -0
- package/lib/autonomous/modes/blue.js +26 -0
- package/lib/autonomous/modes/red.js +28 -0
- package/lib/benchmark/agent.js +88 -26
- package/lib/benchmark/baselines.js +3 -0
- package/lib/benchmark/claude-code-solver.js +254 -0
- package/lib/benchmark/cognitive.js +283 -0
- package/lib/benchmark/index.js +12 -2
- package/lib/benchmark/knowledge.js +281 -0
- package/lib/benchmark/llm.js +156 -15
- package/lib/benchmark/models.js +5 -2
- package/lib/benchmark/nyu-ctf.js +192 -0
- package/lib/benchmark/overthewire.js +347 -0
- package/lib/benchmark/picoctf.js +281 -0
- package/lib/benchmark/prompts.js +280 -0
- package/lib/benchmark/registry.js +219 -0
- package/lib/benchmark/remote-solver.js +356 -0
- package/lib/benchmark/remote-target.js +263 -0
- package/lib/benchmark/reporter.js +35 -0
- package/lib/benchmark/runner.js +174 -10
- package/lib/benchmark/sandbox.js +35 -0
- package/lib/benchmark/scorer.js +22 -4
- package/lib/benchmark/solver.js +34 -1
- package/lib/benchmark/tools.js +262 -16
- package/lib/commands.js +9 -0
- package/lib/execution/council.js +434 -0
- package/lib/execution/parallel.js +292 -0
- package/lib/gates/circuit-breaker.js +135 -0
- package/lib/gates/confidence.js +302 -0
- package/lib/gates/corrections.js +219 -0
- package/lib/gates/self-check.js +245 -0
- package/lib/gateway/commands.js +727 -0
- package/lib/guardrails/engine.js +364 -0
- package/lib/mcp/server.js +349 -3
- package/lib/memory/compressor.js +94 -7
- package/lib/pipeline/hooks.js +288 -0
- package/lib/pipeline/index.js +11 -0
- package/lib/review/budget.js +210 -0
- package/lib/review/engine.js +526 -0
- package/lib/review/layers/acceptance-auditor.js +279 -0
- package/lib/review/layers/blind-hunter.js +500 -0
- package/lib/review/layers/defense-in-depth.js +209 -0
- package/lib/review/layers/edge-case-hunter.js +266 -0
- package/lib/review/panel.js +519 -0
- package/lib/review/two-stage.js +244 -0
- package/lib/session/cost-tracker.js +203 -0
- package/lib/session/logger.js +349 -0
- package/package.json +1 -1
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
// CIPHER is a trademark of defconxt.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CIPHER Pipeline Hook System
|
|
7
|
+
*
|
|
8
|
+
* EventEmitter-based hooks for pipeline stages. Supports before/after
|
|
9
|
+
* callbacks with context passing, finding transformation, and abort.
|
|
10
|
+
*
|
|
11
|
+
* Zero overhead when no hooks are registered.
|
|
12
|
+
*
|
|
13
|
+
* @module pipeline/hooks
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { EventEmitter } from 'node:events';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Hook Events
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/** @enum {string} */
|
|
23
|
+
export const HookEvent = Object.freeze({
|
|
24
|
+
PIPELINE_START: 'pipeline:start',
|
|
25
|
+
PIPELINE_COMPLETE: 'pipeline:complete',
|
|
26
|
+
PIPELINE_ERROR: 'pipeline:error',
|
|
27
|
+
BEFORE_CRAWL: 'before:crawl',
|
|
28
|
+
AFTER_CRAWL: 'after:crawl',
|
|
29
|
+
BEFORE_SCAN: 'before:scan',
|
|
30
|
+
AFTER_SCAN: 'after:scan',
|
|
31
|
+
BEFORE_REVIEW: 'before:review',
|
|
32
|
+
AFTER_REVIEW: 'after:review',
|
|
33
|
+
BEFORE_ANALYZE: 'before:analyze',
|
|
34
|
+
AFTER_ANALYZE: 'after:analyze',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Hook Context
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Context object passed to hook callbacks.
|
|
43
|
+
* Mutable — hooks can add metadata or transform findings.
|
|
44
|
+
*/
|
|
45
|
+
export class HookContext {
|
|
46
|
+
/**
|
|
47
|
+
* @param {object} opts
|
|
48
|
+
* @param {string} opts.stage - Current pipeline stage
|
|
49
|
+
* @param {string} [opts.target] - Target being processed
|
|
50
|
+
* @param {object} [opts.options] - Stage options
|
|
51
|
+
* @param {any[]} [opts.findings] - Findings accumulated so far
|
|
52
|
+
* @param {object} [opts.result] - Stage result (for after hooks)
|
|
53
|
+
* @param {number} [opts.startTime] - Stage start timestamp
|
|
54
|
+
* @param {number} [opts.duration] - Stage duration ms (for after hooks)
|
|
55
|
+
* @param {object} [opts.meta] - Arbitrary metadata
|
|
56
|
+
*/
|
|
57
|
+
constructor(opts = {}) {
|
|
58
|
+
this.stage = opts.stage ?? '';
|
|
59
|
+
this.target = opts.target ?? '';
|
|
60
|
+
this.options = opts.options ?? {};
|
|
61
|
+
this.findings = opts.findings ?? [];
|
|
62
|
+
this.result = opts.result ?? null;
|
|
63
|
+
this.startTime = opts.startTime ?? Date.now();
|
|
64
|
+
this.duration = opts.duration ?? 0;
|
|
65
|
+
this.meta = opts.meta ?? {};
|
|
66
|
+
this._aborted = false;
|
|
67
|
+
this._abortReason = '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Signal that the pipeline should abort after this hook.
|
|
72
|
+
* @param {string} [reason]
|
|
73
|
+
*/
|
|
74
|
+
abort(reason = '') {
|
|
75
|
+
this._aborted = true;
|
|
76
|
+
this._abortReason = reason;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get aborted() { return this._aborted; }
|
|
80
|
+
get abortReason() { return this._abortReason; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// PipelineHooks
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Pipeline hook manager. Wraps EventEmitter with typed hook registration
|
|
89
|
+
* and context-aware execution.
|
|
90
|
+
*/
|
|
91
|
+
export class PipelineHooks extends EventEmitter {
|
|
92
|
+
constructor() {
|
|
93
|
+
super();
|
|
94
|
+
this.setMaxListeners(50); // Allow many hooks
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Register a hook callback.
|
|
99
|
+
* @param {string} event - HookEvent value
|
|
100
|
+
* @param {function} fn - async (ctx: HookContext) => void
|
|
101
|
+
* @param {object} [opts]
|
|
102
|
+
* @param {string} [opts.name] - Hook name for logging
|
|
103
|
+
* @param {number} [opts.priority] - Lower runs first (default: 100)
|
|
104
|
+
* @returns {PipelineHooks} this (chainable)
|
|
105
|
+
*/
|
|
106
|
+
hook(event, fn, opts = {}) {
|
|
107
|
+
fn._hookName = opts.name ?? fn.name ?? 'anonymous';
|
|
108
|
+
fn._hookPriority = opts.priority ?? 100;
|
|
109
|
+
this.on(event, fn);
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Execute all hooks for an event, in priority order.
|
|
115
|
+
* Returns the context (possibly mutated by hooks).
|
|
116
|
+
*
|
|
117
|
+
* @param {string} event
|
|
118
|
+
* @param {HookContext} ctx
|
|
119
|
+
* @returns {Promise<HookContext>}
|
|
120
|
+
*/
|
|
121
|
+
async run(event, ctx) {
|
|
122
|
+
const listeners = this.listeners(event);
|
|
123
|
+
if (listeners.length === 0) return ctx;
|
|
124
|
+
|
|
125
|
+
// Sort by priority (stable sort)
|
|
126
|
+
const sorted = [...listeners].sort(
|
|
127
|
+
(a, b) => (a._hookPriority ?? 100) - (b._hookPriority ?? 100),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
for (const fn of sorted) {
|
|
131
|
+
try {
|
|
132
|
+
await fn(ctx);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
ctx.abort(`Hook "${fn._hookName}" threw: ${err.message}`);
|
|
135
|
+
}
|
|
136
|
+
if (ctx.aborted) break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return ctx;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if any hooks are registered for an event.
|
|
144
|
+
* @param {string} event
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
*/
|
|
147
|
+
hasHooks(event) {
|
|
148
|
+
return this.listenerCount(event) > 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get all registered hook names grouped by event.
|
|
153
|
+
* @returns {object}
|
|
154
|
+
*/
|
|
155
|
+
listHooks() {
|
|
156
|
+
const result = {};
|
|
157
|
+
for (const event of Object.values(HookEvent)) {
|
|
158
|
+
const listeners = this.listeners(event);
|
|
159
|
+
if (listeners.length > 0) {
|
|
160
|
+
result[event] = listeners.map((fn) => fn._hookName ?? 'anonymous');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// HookablePipeline — wraps any stage function with hooks
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Create a hookable wrapper around a pipeline stage function.
|
|
173
|
+
*
|
|
174
|
+
* @param {PipelineHooks} hooks - Hook manager
|
|
175
|
+
* @param {string} stageName - Stage name (e.g. 'crawl', 'scan', 'review')
|
|
176
|
+
* @param {function} stageFn - async (target, options) => result
|
|
177
|
+
* @returns {function} - async (target, options) => result (with hooks)
|
|
178
|
+
*/
|
|
179
|
+
export function hookableStage(hooks, stageName, stageFn) {
|
|
180
|
+
const beforeEvent = `before:${stageName}`;
|
|
181
|
+
const afterEvent = `after:${stageName}`;
|
|
182
|
+
|
|
183
|
+
return async function hookedStage(target, options = {}) {
|
|
184
|
+
// Skip hook overhead if none registered
|
|
185
|
+
if (!hooks.hasHooks(beforeEvent) && !hooks.hasHooks(afterEvent)) {
|
|
186
|
+
return stageFn(target, options);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const t0 = Date.now();
|
|
190
|
+
|
|
191
|
+
// Before hook
|
|
192
|
+
const beforeCtx = new HookContext({
|
|
193
|
+
stage: stageName,
|
|
194
|
+
target,
|
|
195
|
+
options,
|
|
196
|
+
startTime: t0,
|
|
197
|
+
});
|
|
198
|
+
await hooks.run(beforeEvent, beforeCtx);
|
|
199
|
+
|
|
200
|
+
if (beforeCtx.aborted) {
|
|
201
|
+
throw new Error(`Pipeline aborted before ${stageName}: ${beforeCtx.abortReason}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Execute stage
|
|
205
|
+
const result = await stageFn(target, beforeCtx.options);
|
|
206
|
+
const duration = Date.now() - t0;
|
|
207
|
+
|
|
208
|
+
// After hook
|
|
209
|
+
const afterCtx = new HookContext({
|
|
210
|
+
stage: stageName,
|
|
211
|
+
target,
|
|
212
|
+
options: beforeCtx.options,
|
|
213
|
+
result,
|
|
214
|
+
startTime: t0,
|
|
215
|
+
duration,
|
|
216
|
+
meta: beforeCtx.meta, // Pass through metadata from before hook
|
|
217
|
+
});
|
|
218
|
+
await hooks.run(afterEvent, afterCtx);
|
|
219
|
+
|
|
220
|
+
if (afterCtx.aborted) {
|
|
221
|
+
throw new Error(`Pipeline aborted after ${stageName}: ${afterCtx.abortReason}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return afterCtx.result ?? result;
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Built-in hooks
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Timing hook — logs stage duration. Attach to after:* events.
|
|
234
|
+
* @param {function} logger - (message: string) => void
|
|
235
|
+
* @returns {function}
|
|
236
|
+
*/
|
|
237
|
+
export function timingHook(logger = console.log) {
|
|
238
|
+
const fn = async (ctx) => {
|
|
239
|
+
logger(`[hook:timing] ${ctx.stage} completed in ${ctx.duration}ms`);
|
|
240
|
+
};
|
|
241
|
+
fn._hookName = 'timing';
|
|
242
|
+
fn._hookPriority = 999; // Run last
|
|
243
|
+
return fn;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Finding filter hook — removes findings below a severity threshold.
|
|
248
|
+
* Attach to after:review or after:scan events.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} minSeverity - Minimum severity to keep
|
|
251
|
+
* @returns {function}
|
|
252
|
+
*/
|
|
253
|
+
export function severityFilterHook(minSeverity) {
|
|
254
|
+
const RANK = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
|
|
255
|
+
const minRank = RANK[minSeverity] ?? 0;
|
|
256
|
+
|
|
257
|
+
const fn = async (ctx) => {
|
|
258
|
+
if (ctx.result && ctx.result.findings) {
|
|
259
|
+
ctx.result.findings = ctx.result.findings.filter(
|
|
260
|
+
(f) => (RANK[f.severity] ?? 0) >= minRank,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
fn._hookName = `severity-filter:${minSeverity}`;
|
|
265
|
+
fn._hookPriority = 50;
|
|
266
|
+
return fn;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Abort hook — aborts pipeline if findings exceed a threshold.
|
|
271
|
+
* Attach to after:review or after:scan events.
|
|
272
|
+
*
|
|
273
|
+
* @param {string} severity - Severity level to count
|
|
274
|
+
* @param {number} max - Maximum allowed findings of that severity
|
|
275
|
+
* @returns {function}
|
|
276
|
+
*/
|
|
277
|
+
export function thresholdAbortHook(severity, max) {
|
|
278
|
+
const fn = async (ctx) => {
|
|
279
|
+
if (!ctx.result?.findings) return;
|
|
280
|
+
const count = ctx.result.findings.filter((f) => f.severity === severity).length;
|
|
281
|
+
if (count > max) {
|
|
282
|
+
ctx.abort(`${count} ${severity} findings exceed threshold of ${max}`);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
fn._hookName = `threshold-abort:${severity}>${max}`;
|
|
286
|
+
fn._hookPriority = 80;
|
|
287
|
+
return fn;
|
|
288
|
+
}
|
package/lib/pipeline/index.js
CHANGED
|
@@ -124,3 +124,14 @@ export {
|
|
|
124
124
|
TemplateCollection,
|
|
125
125
|
NucleiTemplateManager,
|
|
126
126
|
} from './template-manager.js';
|
|
127
|
+
|
|
128
|
+
// Pipeline hooks — before/after callbacks for pipeline stages
|
|
129
|
+
export {
|
|
130
|
+
HookEvent,
|
|
131
|
+
HookContext,
|
|
132
|
+
PipelineHooks,
|
|
133
|
+
hookableStage,
|
|
134
|
+
timingHook,
|
|
135
|
+
severityFilterHook,
|
|
136
|
+
thresholdAbortHook,
|
|
137
|
+
} from './hooks.js';
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
// CIPHER is a trademark of defconxt.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CIPHER Token Budget Manager
|
|
7
|
+
*
|
|
8
|
+
* Analyzes input complexity and allocates context tokens across
|
|
9
|
+
* phases (research, planning, execution, review) based on
|
|
10
|
+
* file count, LOC, language diversity, and max file size.
|
|
11
|
+
*
|
|
12
|
+
* @module review/budget
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { resolveInput, detectLanguage } from './engine.js';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Complexity levels
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/** @enum {string} */
|
|
22
|
+
export const Complexity = Object.freeze({
|
|
23
|
+
SIMPLE: 'simple',
|
|
24
|
+
MEDIUM: 'medium',
|
|
25
|
+
COMPLEX: 'complex',
|
|
26
|
+
EXTREME: 'extreme',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Thresholds for complexity classification.
|
|
31
|
+
*/
|
|
32
|
+
const THRESHOLDS = {
|
|
33
|
+
simple: { maxFiles: 3, maxLoc: 500, maxLanguages: 1 },
|
|
34
|
+
medium: { maxFiles: 15, maxLoc: 3000, maxLanguages: 3 },
|
|
35
|
+
complex: { maxFiles: 50, maxLoc: 15000, maxLanguages: 5 },
|
|
36
|
+
// anything above complex is extreme
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Default token budgets per complexity level.
|
|
41
|
+
*/
|
|
42
|
+
const DEFAULT_BUDGETS = {
|
|
43
|
+
simple: { total: 4000, weights: { research: 0.15, planning: 0.15, execution: 0.50, review: 0.20 } },
|
|
44
|
+
medium: { total: 8000, weights: { research: 0.20, planning: 0.20, execution: 0.40, review: 0.20 } },
|
|
45
|
+
complex: { total: 12000, weights: { research: 0.25, planning: 0.20, execution: 0.35, review: 0.20 } },
|
|
46
|
+
extreme: { total: 16000, weights: { research: 0.25, planning: 0.25, execution: 0.30, review: 0.20 } },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Complexity Analysis
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Complexity metrics for a set of source files.
|
|
55
|
+
*/
|
|
56
|
+
export class ComplexityMetrics {
|
|
57
|
+
constructor(opts = {}) {
|
|
58
|
+
this.fileCount = opts.fileCount ?? 0;
|
|
59
|
+
this.totalLoc = opts.totalLoc ?? 0;
|
|
60
|
+
this.languages = opts.languages ?? [];
|
|
61
|
+
this.languageCount = opts.languageCount ?? 0;
|
|
62
|
+
this.maxFileSize = opts.maxFileSize ?? 0;
|
|
63
|
+
this.avgFileSize = opts.avgFileSize ?? 0;
|
|
64
|
+
this.complexity = opts.complexity ?? Complexity.SIMPLE;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Analyze complexity of resolved source files.
|
|
70
|
+
*
|
|
71
|
+
* @param {import('./engine.js').SourceFile[]} sources
|
|
72
|
+
* @returns {ComplexityMetrics}
|
|
73
|
+
*/
|
|
74
|
+
export function analyzeComplexity(sources) {
|
|
75
|
+
if (!sources.length) {
|
|
76
|
+
return new ComplexityMetrics({ complexity: Complexity.SIMPLE });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const langSet = new Set();
|
|
80
|
+
let totalLoc = 0;
|
|
81
|
+
let maxFileSize = 0;
|
|
82
|
+
|
|
83
|
+
for (const src of sources) {
|
|
84
|
+
const loc = src.content.split('\n').length;
|
|
85
|
+
totalLoc += loc;
|
|
86
|
+
if (loc > maxFileSize) maxFileSize = loc;
|
|
87
|
+
langSet.add(src.language);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const fileCount = sources.length;
|
|
91
|
+
const languageCount = langSet.size;
|
|
92
|
+
const avgFileSize = Math.round(totalLoc / fileCount);
|
|
93
|
+
const languages = [...langSet].sort();
|
|
94
|
+
|
|
95
|
+
// Classify complexity
|
|
96
|
+
let complexity;
|
|
97
|
+
if (fileCount <= THRESHOLDS.simple.maxFiles &&
|
|
98
|
+
totalLoc <= THRESHOLDS.simple.maxLoc &&
|
|
99
|
+
languageCount <= THRESHOLDS.simple.maxLanguages) {
|
|
100
|
+
complexity = Complexity.SIMPLE;
|
|
101
|
+
} else if (fileCount <= THRESHOLDS.medium.maxFiles &&
|
|
102
|
+
totalLoc <= THRESHOLDS.medium.maxLoc &&
|
|
103
|
+
languageCount <= THRESHOLDS.medium.maxLanguages) {
|
|
104
|
+
complexity = Complexity.MEDIUM;
|
|
105
|
+
} else if (fileCount <= THRESHOLDS.complex.maxFiles &&
|
|
106
|
+
totalLoc <= THRESHOLDS.complex.maxLoc &&
|
|
107
|
+
languageCount <= THRESHOLDS.complex.maxLanguages) {
|
|
108
|
+
complexity = Complexity.COMPLEX;
|
|
109
|
+
} else {
|
|
110
|
+
complexity = Complexity.EXTREME;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return new ComplexityMetrics({
|
|
114
|
+
fileCount,
|
|
115
|
+
totalLoc,
|
|
116
|
+
languages,
|
|
117
|
+
languageCount,
|
|
118
|
+
maxFileSize,
|
|
119
|
+
avgFileSize,
|
|
120
|
+
complexity,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Token Budget
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Structured token budget with per-phase allocations.
|
|
130
|
+
*/
|
|
131
|
+
export class TokenBudget {
|
|
132
|
+
/**
|
|
133
|
+
* @param {object} opts
|
|
134
|
+
* @param {number} opts.total
|
|
135
|
+
* @param {object} opts.phases - { research, planning, execution, review }
|
|
136
|
+
* @param {ComplexityMetrics} opts.metrics
|
|
137
|
+
*/
|
|
138
|
+
constructor(opts = {}) {
|
|
139
|
+
this.total = opts.total ?? 0;
|
|
140
|
+
this.phases = opts.phases ?? {};
|
|
141
|
+
this.metrics = opts.metrics ?? new ComplexityMetrics();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
toReport() {
|
|
145
|
+
const lines = [
|
|
146
|
+
`Token Budget (${this.metrics.complexity})`,
|
|
147
|
+
` Total: ${this.total} tokens`,
|
|
148
|
+
` Research: ${this.phases.research} tokens`,
|
|
149
|
+
` Planning: ${this.phases.planning} tokens`,
|
|
150
|
+
` Execution: ${this.phases.execution} tokens`,
|
|
151
|
+
` Review: ${this.phases.review} tokens`,
|
|
152
|
+
'',
|
|
153
|
+
`Complexity: ${this.metrics.complexity}`,
|
|
154
|
+
` Files: ${this.metrics.fileCount}`,
|
|
155
|
+
` LOC: ${this.metrics.totalLoc}`,
|
|
156
|
+
` Languages: ${this.metrics.languages.join(', ')} (${this.metrics.languageCount})`,
|
|
157
|
+
` Max file: ${this.metrics.maxFileSize} lines`,
|
|
158
|
+
` Avg file: ${this.metrics.avgFileSize} lines`,
|
|
159
|
+
];
|
|
160
|
+
return lines.join('\n');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
toJSON() {
|
|
164
|
+
return {
|
|
165
|
+
total: this.total,
|
|
166
|
+
phases: this.phases,
|
|
167
|
+
complexity: this.metrics.complexity,
|
|
168
|
+
metrics: {
|
|
169
|
+
fileCount: this.metrics.fileCount,
|
|
170
|
+
totalLoc: this.metrics.totalLoc,
|
|
171
|
+
languages: this.metrics.languages,
|
|
172
|
+
languageCount: this.metrics.languageCount,
|
|
173
|
+
maxFileSize: this.metrics.maxFileSize,
|
|
174
|
+
avgFileSize: this.metrics.avgFileSize,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Factory
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create a token budget for the given input.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} input - File path, directory, or code string
|
|
188
|
+
* @param {object} [options]
|
|
189
|
+
* @param {string} [options.language] - Override language detection
|
|
190
|
+
* @param {number} [options.total] - Override total token budget
|
|
191
|
+
* @param {object} [options.weights] - Override phase weights { research, planning, execution, review }
|
|
192
|
+
* @returns {Promise<TokenBudget>}
|
|
193
|
+
*/
|
|
194
|
+
export async function createTokenBudget(input, options = {}) {
|
|
195
|
+
const sources = await resolveInput(input, { language: options.language });
|
|
196
|
+
const metrics = analyzeComplexity(sources);
|
|
197
|
+
|
|
198
|
+
const budgetConfig = DEFAULT_BUDGETS[metrics.complexity];
|
|
199
|
+
const total = options.total ?? budgetConfig.total;
|
|
200
|
+
const weights = { ...budgetConfig.weights, ...options.weights };
|
|
201
|
+
|
|
202
|
+
// Normalize weights
|
|
203
|
+
const weightSum = Object.values(weights).reduce((a, b) => a + b, 0);
|
|
204
|
+
const phases = {};
|
|
205
|
+
for (const [phase, weight] of Object.entries(weights)) {
|
|
206
|
+
phases[phase] = Math.round((weight / weightSum) * total);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return new TokenBudget({ total, phases, metrics });
|
|
210
|
+
}
|