Skip to content

Commit b826379

Browse files
committed
feat(core): add fast path execution gate
1 parent ce322bf commit b826379

8 files changed

Lines changed: 617 additions & 6 deletions

File tree

.aiox-core/core/config/schemas/framework-config.schema.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,34 @@
9797
"type": "boolean",
9898
"description": "Require orchestrator review before story state mutation",
9999
"default": true
100+
},
101+
"fast_path": {
102+
"type": "object",
103+
"description": "Fast-path gate defaults for mechanical, batchable, or parallelizable tasks",
104+
"properties": {
105+
"enabled": {
106+
"type": "boolean",
107+
"default": true
108+
},
109+
"min_confidence": {
110+
"type": "number",
111+
"minimum": 0,
112+
"maximum": 1,
113+
"default": 0.58
114+
},
115+
"min_batch_items": {
116+
"type": "integer",
117+
"minimum": 1,
118+
"default": 3
119+
},
120+
"external_executor_threshold": {
121+
"type": "number",
122+
"minimum": 0,
123+
"maximum": 1,
124+
"default": 0.78
125+
}
126+
},
127+
"additionalProperties": false
100128
}
101129
},
102130
"additionalProperties": false
Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
'use strict';
2+
3+
const DEFAULT_FAST_PATH_CONFIG = Object.freeze({
4+
enabled: true,
5+
externalExecutorsEnabled: false,
6+
minConfidence: 0.58,
7+
minBatchItems: 3,
8+
externalExecutorThreshold: 0.78,
9+
});
10+
11+
const STRUCTURED_FILE_EXTENSIONS_FROZEN = Object.freeze([
12+
'.csv',
13+
'.json',
14+
'.jsonl',
15+
'.md',
16+
'.toml',
17+
'.tsv',
18+
'.txt',
19+
'.yaml',
20+
'.yml',
21+
]);
22+
23+
const STRUCTURED_FILE_EXTENSION_SET = new Set(STRUCTURED_FILE_EXTENSIONS_FROZEN);
24+
25+
const AUTOMATION_PATTERNS_FROZEN = Object.freeze([
26+
{
27+
id: 'bulk-edit',
28+
weight: 3,
29+
pattern: /\b(batch|bulk|many files|multiple files|all files|in one shot|one shot)\b/i,
30+
},
31+
{
32+
id: 'structured-transform',
33+
weight: 3,
34+
pattern: /\b(yaml|json|csv|markdown|frontmatter|schema|variable|variables|field|fields)\b/i,
35+
},
36+
{
37+
id: 'mechanical-edit',
38+
weight: 3,
39+
pattern: /\b(replace|rename|populate|fill|complete|update|convert|transform|normalize|format)\b/i,
40+
},
41+
{
42+
id: 'map-then-apply',
43+
weight: 2,
44+
pattern: /\b(map|extract|derive|template|codemod|script)\b/i,
45+
},
46+
{
47+
id: 'repetition',
48+
weight: 2,
49+
pattern: /\b(repeated|repetitive|same change|similar change|dumb task|tedious)\b/i,
50+
},
51+
{
52+
id: 'parallelizable',
53+
weight: 2,
54+
pattern: /\b(parallel|independent|per file|per item|per record)\b/i,
55+
},
56+
].map(Object.freeze));
57+
58+
const RISK_PATTERNS_FROZEN = Object.freeze([
59+
{
60+
id: 'architecture',
61+
weight: 3,
62+
pattern: /\b(architecture|architectural|design decision|adr|contract)\b/i,
63+
},
64+
{
65+
id: 'security',
66+
weight: 3,
67+
pattern: /\b(security|secret|token|credential|auth|permission|pii|rls)\b/i,
68+
},
69+
{
70+
id: 'destructive',
71+
weight: 3,
72+
pattern: /\b(delete|remove data|drop|reset|rewrite history|destructive)\b/i,
73+
},
74+
{
75+
id: 'production',
76+
weight: 2,
77+
pattern: /\b(production|prod|release|billing|payment|customer)\b/i,
78+
},
79+
{
80+
id: 'migration',
81+
weight: 2,
82+
pattern: /\b(migration|migrate|schema change|breaking change)\b/i,
83+
},
84+
].map(Object.freeze));
85+
86+
function clonePatternDefinition(patternDefinition) {
87+
return {
88+
id: patternDefinition.id,
89+
weight: patternDefinition.weight,
90+
pattern: new RegExp(patternDefinition.pattern.source, patternDefinition.pattern.flags),
91+
};
92+
}
93+
94+
function getStructuredFileExtensions() {
95+
return new Set(STRUCTURED_FILE_EXTENSIONS_FROZEN);
96+
}
97+
98+
function getAutomationPatterns() {
99+
return AUTOMATION_PATTERNS_FROZEN.map(clonePatternDefinition);
100+
}
101+
102+
function getRiskPatterns() {
103+
return RISK_PATTERNS_FROZEN.map(clonePatternDefinition);
104+
}
105+
106+
function parseBoolean(value, fallback) {
107+
if (typeof value === 'boolean') {
108+
return value;
109+
}
110+
if (typeof value === 'string') {
111+
const normalized = value.trim().toLowerCase();
112+
if (normalized === 'true') {
113+
return true;
114+
}
115+
if (normalized === 'false') {
116+
return false;
117+
}
118+
}
119+
return fallback;
120+
}
121+
122+
function normalizeConfig(config = {}) {
123+
const clamp01 = (value, fallback) => {
124+
const numericValue = Number(value);
125+
if (!Number.isFinite(numericValue)) {
126+
return fallback;
127+
}
128+
return Math.min(1, Math.max(0, numericValue));
129+
};
130+
const positiveInteger = (value, fallback) => {
131+
const numericValue = Number(value);
132+
if (!Number.isFinite(numericValue)) {
133+
return fallback;
134+
}
135+
return Math.max(1, Math.floor(numericValue));
136+
};
137+
138+
return {
139+
enabled: parseBoolean(config.enabled, DEFAULT_FAST_PATH_CONFIG.enabled),
140+
externalExecutorsEnabled: parseBoolean(
141+
config.externalExecutorsEnabled ?? config.external_executors_enabled,
142+
DEFAULT_FAST_PATH_CONFIG.externalExecutorsEnabled,
143+
),
144+
minConfidence: clamp01(
145+
config.minConfidence ?? config.min_confidence,
146+
DEFAULT_FAST_PATH_CONFIG.minConfidence,
147+
),
148+
minBatchItems: positiveInteger(
149+
config.minBatchItems ?? config.min_batch_items,
150+
DEFAULT_FAST_PATH_CONFIG.minBatchItems,
151+
),
152+
externalExecutorThreshold: clamp01(
153+
config.externalExecutorThreshold ?? config.external_executor_threshold,
154+
DEFAULT_FAST_PATH_CONFIG.externalExecutorThreshold,
155+
),
156+
};
157+
}
158+
159+
function normalizeTask(input = {}) {
160+
const task = input.task || input;
161+
return {
162+
description: String(task.description || task.summary || task.title || ''),
163+
files: Array.isArray(task.files) ? task.files : [],
164+
acceptanceCriteria: Array.isArray(task.acceptanceCriteria)
165+
? task.acceptanceCriteria
166+
: Array.isArray(task.acceptance_criteria)
167+
? task.acceptance_criteria
168+
: [],
169+
itemCount: Number.isFinite(task.itemCount)
170+
? task.itemCount
171+
: Number.isFinite(task.item_count)
172+
? task.item_count
173+
: null,
174+
};
175+
}
176+
177+
function normalizePathExtension(filePath) {
178+
const match = String(filePath || '').toLowerCase().match(/(\.[a-z0-9]+)$/);
179+
return match ? match[1] : '';
180+
}
181+
182+
function collectSignals(patterns, text) {
183+
return patterns
184+
.filter(({ pattern }) => pattern.test(text))
185+
.map(({ id, weight }) => ({ id, weight }));
186+
}
187+
188+
function getTaskText(task) {
189+
return [
190+
task.description,
191+
...task.acceptanceCriteria.map((criterion) => String(criterion || '')),
192+
...task.files.map((file) => String(file || '')),
193+
].join('\n');
194+
}
195+
196+
function scoreFastPath({ automationSignals, riskSignals, files, structuredFileCount, batchSize }) {
197+
const automationWeight = automationSignals.reduce((sum, signal) => sum + signal.weight, 0);
198+
const riskWeight = riskSignals.reduce((sum, signal) => sum + signal.weight, 0);
199+
const fileWeight = Math.min(files.length, 8) * 0.45;
200+
const structuredWeight = Math.min(structuredFileCount, 6) * 0.55;
201+
const batchWeight = Math.min(batchSize, 10) * 0.35;
202+
203+
const rawScore = automationWeight + fileWeight + structuredWeight + batchWeight - riskWeight * 1.35;
204+
return Math.max(0, Math.min(1, rawScore / 13));
205+
}
206+
207+
function chooseMode({
208+
confidence,
209+
config,
210+
externalExecutorsEnabled,
211+
parallelizable,
212+
structuredFileCount,
213+
batchSize,
214+
}) {
215+
if (externalExecutorsEnabled && confidence >= config.externalExecutorThreshold) {
216+
return 'external_executor';
217+
}
218+
219+
if (parallelizable && batchSize >= config.minBatchItems) {
220+
return 'parallel_batch';
221+
}
222+
223+
if (structuredFileCount > 0 || batchSize >= config.minBatchItems) {
224+
return 'deterministic_batch';
225+
}
226+
227+
return 'standard';
228+
}
229+
230+
function buildActions(mode) {
231+
if (mode === 'external_executor') {
232+
return [
233+
'Prepare a bounded prompt with target files, schema, acceptance criteria, and validation commands.',
234+
'Run a dry-run plan first, then delegate with the configured external executor sandbox.',
235+
'Review the executor diff before mutating story or issue state.',
236+
];
237+
}
238+
239+
if (mode === 'parallel_batch') {
240+
return [
241+
'Map target files or records once before editing.',
242+
'Group independent changes by file or record and apply them as a batch.',
243+
'Run targeted validation on the changed surface before broader checks.',
244+
];
245+
}
246+
247+
if (mode === 'deterministic_batch') {
248+
return [
249+
'Extract the data shape and replacement rules before editing.',
250+
'Use a deterministic transform or structured parser instead of conversational one-by-one edits.',
251+
'Validate output syntax and diff size before continuing.',
252+
];
253+
}
254+
255+
return [
256+
'Use the standard story/task workflow.',
257+
'Keep changes sequential when risk or ambiguity is higher than the automation signal.',
258+
];
259+
}
260+
261+
function evaluateFastPath(input = {}) {
262+
const config = normalizeConfig(input.config || input.fastPath || {});
263+
const task = normalizeTask(input);
264+
const text = getTaskText(task);
265+
const automationSignals = collectSignals(AUTOMATION_PATTERNS_FROZEN, text);
266+
const riskSignals = collectSignals(RISK_PATTERNS_FROZEN, text);
267+
const structuredFileCount = task.files.filter((file) => (
268+
STRUCTURED_FILE_EXTENSION_SET.has(normalizePathExtension(file))
269+
)).length;
270+
const batchSize = task.itemCount ?? Math.max(task.files.length, structuredFileCount);
271+
const parallelizable = (
272+
task.files.length >= config.minBatchItems ||
273+
automationSignals.some((signal) => signal.id === 'parallelizable')
274+
);
275+
276+
if (!config.enabled) {
277+
return {
278+
gate: 'fast_path',
279+
enabled: false,
280+
passed: false,
281+
mode: 'standard',
282+
confidence: 0,
283+
parallelizable: false,
284+
riskLevel: 'unknown',
285+
reasons: ['fast path gate disabled by configuration'],
286+
evidence: { automationSignals: [], riskSignals: [], fileCount: task.files.length, structuredFileCount, batchSize },
287+
actions: buildActions('standard'),
288+
};
289+
}
290+
291+
const confidence = scoreFastPath({
292+
automationSignals,
293+
riskSignals,
294+
files: task.files,
295+
structuredFileCount,
296+
batchSize,
297+
});
298+
const passed = confidence >= config.minConfidence && riskSignals.length === 0;
299+
const mode = passed
300+
? chooseMode({
301+
confidence,
302+
config,
303+
externalExecutorsEnabled: parseBoolean(
304+
input.externalExecutorsEnabled ?? input.external_executors_enabled,
305+
config.externalExecutorsEnabled,
306+
),
307+
parallelizable,
308+
structuredFileCount,
309+
batchSize,
310+
})
311+
: 'standard';
312+
const riskLevel = riskSignals.length >= 2 ? 'high' : riskSignals.length === 1 ? 'medium' : 'low';
313+
const reasons = [
314+
...automationSignals.map((signal) => `automation signal: ${signal.id}`),
315+
...riskSignals.map((signal) => `risk signal: ${signal.id}`),
316+
];
317+
318+
if (structuredFileCount > 0) {
319+
reasons.push(`structured files detected: ${structuredFileCount}`);
320+
}
321+
if (batchSize >= config.minBatchItems) {
322+
reasons.push(`batch size meets threshold: ${batchSize}`);
323+
}
324+
if (!passed && reasons.length === 0) {
325+
reasons.push('insufficient automation signal for fast path');
326+
}
327+
328+
return {
329+
gate: 'fast_path',
330+
enabled: true,
331+
passed,
332+
mode,
333+
confidence,
334+
parallelizable: passed && parallelizable,
335+
riskLevel,
336+
reasons,
337+
evidence: {
338+
automationSignals,
339+
riskSignals,
340+
fileCount: task.files.length,
341+
structuredFileCount,
342+
batchSize,
343+
},
344+
actions: buildActions(mode),
345+
};
346+
}
347+
348+
module.exports = {
349+
DEFAULT_FAST_PATH_CONFIG,
350+
evaluateFastPath,
351+
getAutomationPatterns,
352+
getRiskPatterns,
353+
getStructuredFileExtensions,
354+
normalizeConfig,
355+
normalizeTask,
356+
};

0 commit comments

Comments
 (0)