sinapse-ai 1.4.2 → 1.5.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.
@@ -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
+ };
@@ -145,6 +145,20 @@ const {
145
145
  PHASE_1_SEQUENCE,
146
146
  } = require('./greenfield-handler');
147
147
 
148
+ // v1.5.0: Fast-Path Gate
149
+ // Heuristic gate to decide when a task can be executed via a faster mode
150
+ // (parallel_batch / deterministic_batch / external_executor) versus the
151
+ // standard sequential workflow. Used as input to orchestrator decisions.
152
+ const {
153
+ DEFAULT_FAST_PATH_CONFIG,
154
+ evaluateFastPath,
155
+ getAutomationPatterns,
156
+ getRiskPatterns,
157
+ getStructuredFileExtensions,
158
+ normalizeConfig: normalizeFastPathConfig,
159
+ normalizeTask: normalizeFastPathTask,
160
+ } = require('./fast-path-gate');
161
+
148
162
  module.exports = {
149
163
  // Main orchestrators
150
164
  WorkflowOrchestrator,
@@ -319,5 +333,14 @@ module.exports = {
319
333
  GreenfieldPhaseFailureAction,
320
334
  DEFAULT_GREENFIELD_INDICATORS,
321
335
  PHASE_1_SEQUENCE,
336
+
337
+ // v1.5.0: Fast-Path Gate
338
+ DEFAULT_FAST_PATH_CONFIG,
339
+ evaluateFastPath,
340
+ getAutomationPatterns,
341
+ getRiskPatterns,
342
+ getStructuredFileExtensions,
343
+ normalizeFastPathConfig,
344
+ normalizeFastPathTask,
322
345
  };
323
346