@neurcode-ai/cli 0.9.63 → 0.9.64

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.
Files changed (70) hide show
  1. package/dist/commands/control-plane.js +7 -7
  2. package/dist/commands/control-plane.js.map +1 -1
  3. package/dist/commands/fix.d.ts.map +1 -1
  4. package/dist/commands/fix.js +108 -1
  5. package/dist/commands/fix.js.map +1 -1
  6. package/dist/commands/patch-apply.d.ts +2 -0
  7. package/dist/commands/patch-apply.d.ts.map +1 -1
  8. package/dist/commands/patch-apply.js +331 -19
  9. package/dist/commands/patch-apply.js.map +1 -1
  10. package/dist/commands/replay.js +5 -5
  11. package/dist/commands/replay.js.map +1 -1
  12. package/dist/commands/verify.d.ts.map +1 -1
  13. package/dist/commands/verify.js +29 -1
  14. package/dist/commands/verify.js.map +1 -1
  15. package/dist/commands/workspace.js +7 -7
  16. package/dist/commands/workspace.js.map +1 -1
  17. package/dist/daemon/server.d.ts +2 -2
  18. package/dist/daemon/server.d.ts.map +1 -1
  19. package/dist/daemon/server.js +1035 -32
  20. package/dist/daemon/server.js.map +1 -1
  21. package/dist/index.js +15 -4
  22. package/dist/index.js.map +1 -1
  23. package/dist/intent-engine/matcher.d.ts.map +1 -1
  24. package/dist/intent-engine/matcher.js +2 -0
  25. package/dist/intent-engine/matcher.js.map +1 -1
  26. package/dist/patch-engine/diff.d.ts +1 -1
  27. package/dist/patch-engine/diff.js +1 -1
  28. package/dist/patch-engine/generator.d.ts +9 -0
  29. package/dist/patch-engine/generator.d.ts.map +1 -1
  30. package/dist/patch-engine/generator.js +375 -17
  31. package/dist/patch-engine/generator.js.map +1 -1
  32. package/dist/patch-engine/index.d.ts +25 -25
  33. package/dist/patch-engine/index.d.ts.map +1 -1
  34. package/dist/patch-engine/index.js +134 -87
  35. package/dist/patch-engine/index.js.map +1 -1
  36. package/dist/patch-engine/patterns.d.ts +1 -1
  37. package/dist/patch-engine/patterns.d.ts.map +1 -1
  38. package/dist/patch-engine/patterns.js +277 -40
  39. package/dist/patch-engine/patterns.js.map +1 -1
  40. package/dist/patch-engine/rollback.d.ts +31 -0
  41. package/dist/patch-engine/rollback.d.ts.map +1 -0
  42. package/dist/patch-engine/rollback.js +275 -0
  43. package/dist/patch-engine/rollback.js.map +1 -0
  44. package/dist/patch-engine/safety.d.ts +28 -0
  45. package/dist/patch-engine/safety.d.ts.map +1 -0
  46. package/dist/patch-engine/safety.js +122 -0
  47. package/dist/patch-engine/safety.js.map +1 -0
  48. package/dist/patch-engine/transaction.d.ts +52 -0
  49. package/dist/patch-engine/transaction.d.ts.map +1 -0
  50. package/dist/patch-engine/transaction.js +93 -0
  51. package/dist/patch-engine/transaction.js.map +1 -0
  52. package/dist/utils/advisory-signals.d.ts +5 -0
  53. package/dist/utils/advisory-signals.d.ts.map +1 -1
  54. package/dist/utils/advisory-signals.js +50 -12
  55. package/dist/utils/advisory-signals.js.map +1 -1
  56. package/dist/utils/ai-debt-budget.d.ts.map +1 -1
  57. package/dist/utils/ai-debt-budget.js +5 -2
  58. package/dist/utils/ai-debt-budget.js.map +1 -1
  59. package/dist/utils/cli-json.d.ts.map +1 -1
  60. package/dist/utils/cli-json.js +80 -12
  61. package/dist/utils/cli-json.js.map +1 -1
  62. package/dist/utils/execution-bus.d.ts +10 -0
  63. package/dist/utils/execution-bus.d.ts.map +1 -1
  64. package/dist/utils/execution-bus.js +16 -0
  65. package/dist/utils/execution-bus.js.map +1 -1
  66. package/dist/utils/policy-compiler.d.ts +6 -0
  67. package/dist/utils/policy-compiler.d.ts.map +1 -1
  68. package/dist/utils/policy-compiler.js +20 -0
  69. package/dist/utils/policy-compiler.js.map +1 -1
  70. package/package.json +2 -2
@@ -39,20 +39,181 @@ exports.startDaemon = startDaemon;
39
39
  const http = __importStar(require("node:http"));
40
40
  const path = __importStar(require("node:path"));
41
41
  const fs = __importStar(require("node:fs"));
42
+ const node_crypto_1 = require("node:crypto");
43
+ const node_child_process_1 = require("node:child_process");
44
+ const patch_engine_1 = require("../patch-engine");
45
+ const diff_1 = require("../patch-engine/diff");
42
46
  const execution_bus_1 = require("../utils/execution-bus");
43
47
  const runtime_events_1 = require("../utils/runtime-events");
44
48
  const control_plane_1 = require("../utils/control-plane");
45
49
  const workspace_runtime_1 = require("../utils/workspace-runtime");
46
50
  const replay_runtime_1 = require("../utils/replay-runtime");
51
+ const contracts_1 = require("@neurcode-ai/contracts");
47
52
  // ── Configuration ──────────────────────────────────────────────────────────────
48
- exports.DAEMON_PORT = 4321;
49
- exports.DAEMON_HOST = '127.0.0.1';
53
+ exports.DAEMON_PORT = Number.parseInt(process.env.NEURCODE_DAEMON_PORT || '4321', 10) || 4321;
54
+ exports.DAEMON_HOST = process.env.NEURCODE_DAEMON_HOST || '127.0.0.1';
50
55
  const SSE_RETRY_MS = 3000;
56
+ const REQUEST_ID_HEADER = 'x-neurcode-request-id';
57
+ const ALLOW_NON_LOOPBACK = ['1', 'true', 'yes', 'on'].includes(String(process.env.NEURCODE_DAEMON_ALLOW_REMOTE || '').trim().toLowerCase());
51
58
  const runtimeEventClients = new Map();
52
59
  let runtimeEventClientSeq = 0;
53
60
  let runtimeEventUnsubscribe = null;
54
61
  let runtimeEventTailTimer = null;
55
62
  let runtimeEventTailCursor = null;
63
+ const DAEMON_MAX_ERROR_HISTORY = 40;
64
+ const DAEMON_MAX_ROUTE_SAMPLE = 1000;
65
+ const daemonOpsMetrics = {
66
+ startedAt: new Date().toISOString(),
67
+ requestsTotal: 0,
68
+ requestsByMethod: {},
69
+ requestsByRoute: {},
70
+ failuresTotal: 0,
71
+ retriableFailuresTotal: 0,
72
+ stalePreviewRejections: 0,
73
+ rollbackStaleRejections: 0,
74
+ patchApplied: 0,
75
+ patchPartial: 0,
76
+ patchRejected: 0,
77
+ rollbackApplied: 0,
78
+ rollbackRejected: 0,
79
+ recentErrors: [],
80
+ };
81
+ function normalizeRoutePath(url) {
82
+ const pathOnly = url.split('?')[0]?.trim() || '/';
83
+ return pathOnly.startsWith('/') ? pathOnly : `/${pathOnly}`;
84
+ }
85
+ function incrementMetricCounter(record, key) {
86
+ record[key] = (record[key] || 0) + 1;
87
+ }
88
+ function recordDaemonRequest(url, method) {
89
+ daemonOpsMetrics.requestsTotal += 1;
90
+ incrementMetricCounter(daemonOpsMetrics.requestsByMethod, method.toUpperCase());
91
+ const route = normalizeRoutePath(url);
92
+ incrementMetricCounter(daemonOpsMetrics.requestsByRoute, route);
93
+ // Keep route cardinality bounded for long-lived daemon sessions.
94
+ const routeKeys = Object.keys(daemonOpsMetrics.requestsByRoute);
95
+ if (routeKeys.length > DAEMON_MAX_ROUTE_SAMPLE) {
96
+ const overflow = routeKeys
97
+ .sort((left, right) => (daemonOpsMetrics.requestsByRoute[left] || 0) - (daemonOpsMetrics.requestsByRoute[right] || 0))
98
+ .slice(0, routeKeys.length - DAEMON_MAX_ROUTE_SAMPLE);
99
+ for (const key of overflow) {
100
+ delete daemonOpsMetrics.requestsByRoute[key];
101
+ }
102
+ }
103
+ }
104
+ function recordDaemonFailure(sample) {
105
+ daemonOpsMetrics.failuresTotal += 1;
106
+ if (sample.retriable) {
107
+ daemonOpsMetrics.retriableFailuresTotal += 1;
108
+ }
109
+ daemonOpsMetrics.recentErrors.unshift({
110
+ at: new Date().toISOString(),
111
+ ...sample,
112
+ });
113
+ if (daemonOpsMetrics.recentErrors.length > DAEMON_MAX_ERROR_HISTORY) {
114
+ daemonOpsMetrics.recentErrors.length = DAEMON_MAX_ERROR_HISTORY;
115
+ }
116
+ }
117
+ function recordPatchOutcome(status) {
118
+ if (status === 'applied') {
119
+ daemonOpsMetrics.patchApplied += 1;
120
+ return;
121
+ }
122
+ if (status === 'partial') {
123
+ daemonOpsMetrics.patchPartial += 1;
124
+ return;
125
+ }
126
+ if (status === 'stale_preview') {
127
+ daemonOpsMetrics.patchRejected += 1;
128
+ daemonOpsMetrics.stalePreviewRejections += 1;
129
+ return;
130
+ }
131
+ if (status === 'rollback_applied') {
132
+ daemonOpsMetrics.rollbackApplied += 1;
133
+ return;
134
+ }
135
+ if (status === 'rollback_stale') {
136
+ daemonOpsMetrics.rollbackRejected += 1;
137
+ daemonOpsMetrics.rollbackStaleRejections += 1;
138
+ return;
139
+ }
140
+ if (status === 'rollback_rejected') {
141
+ daemonOpsMetrics.rollbackRejected += 1;
142
+ return;
143
+ }
144
+ daemonOpsMetrics.patchRejected += 1;
145
+ }
146
+ function buildDaemonOperationalSummary(cwd) {
147
+ const uptimeSeconds = Math.max(0, Math.floor((Date.now() - new Date(daemonOpsMetrics.startedAt).getTime()) / 1000));
148
+ const lockPath = path.resolve(cwd, '.neurcode', 'executions', '.lock');
149
+ let lockPresent = false;
150
+ let lockAgeMs = null;
151
+ try {
152
+ const stat = fs.statSync(lockPath);
153
+ lockPresent = stat.isFile();
154
+ lockAgeMs = Date.now() - stat.mtimeMs;
155
+ }
156
+ catch {
157
+ lockPresent = false;
158
+ lockAgeMs = null;
159
+ }
160
+ const executionQuery = (0, execution_bus_1.queryExecutions)(cwd, {
161
+ limit: 200,
162
+ status: 'all',
163
+ });
164
+ const items = executionQuery.items || [];
165
+ const activeExecutions = items.filter((item) => item.status !== 'completed' && item.status !== 'failed').length;
166
+ const recentFailures = items
167
+ .filter((item) => item.status === 'failed')
168
+ .slice(0, 10)
169
+ .map((item) => ({
170
+ id: item.id,
171
+ type: item.type,
172
+ source: item.source,
173
+ actor: item.actor,
174
+ completedAt: item.completedAt,
175
+ message: item.result?.message || null,
176
+ }));
177
+ const patchAttempts = daemonOpsMetrics.patchApplied + daemonOpsMetrics.patchPartial + daemonOpsMetrics.patchRejected;
178
+ const rollbackAttempts = daemonOpsMetrics.rollbackApplied + daemonOpsMetrics.rollbackRejected;
179
+ return {
180
+ uptimeSeconds,
181
+ activeExecutions,
182
+ sseClients: runtimeEventClients.size,
183
+ requestTotals: {
184
+ total: daemonOpsMetrics.requestsTotal,
185
+ failures: daemonOpsMetrics.failuresTotal,
186
+ retriableFailures: daemonOpsMetrics.retriableFailuresTotal,
187
+ byMethod: daemonOpsMetrics.requestsByMethod,
188
+ topRoutes: Object.entries(daemonOpsMetrics.requestsByRoute)
189
+ .sort((left, right) => right[1] - left[1])
190
+ .slice(0, 12)
191
+ .map(([route, count]) => ({ route, count })),
192
+ },
193
+ patchStats: {
194
+ attempts: patchAttempts,
195
+ applied: daemonOpsMetrics.patchApplied,
196
+ partial: daemonOpsMetrics.patchPartial,
197
+ rejected: daemonOpsMetrics.patchRejected,
198
+ stalePreviewRejections: daemonOpsMetrics.stalePreviewRejections,
199
+ successRate: patchAttempts > 0 ? Number((daemonOpsMetrics.patchApplied / patchAttempts).toFixed(4)) : null,
200
+ },
201
+ rollbackStats: {
202
+ attempts: rollbackAttempts,
203
+ applied: daemonOpsMetrics.rollbackApplied,
204
+ rejected: daemonOpsMetrics.rollbackRejected,
205
+ staleRejections: daemonOpsMetrics.rollbackStaleRejections,
206
+ successRate: rollbackAttempts > 0 ? Number((daemonOpsMetrics.rollbackApplied / rollbackAttempts).toFixed(4)) : null,
207
+ },
208
+ executionLock: {
209
+ path: lockPath,
210
+ present: lockPresent,
211
+ ageMs: lockAgeMs,
212
+ },
213
+ recentFailures,
214
+ recentErrors: daemonOpsMetrics.recentErrors,
215
+ };
216
+ }
56
217
  // ── Request helpers ────────────────────────────────────────────────────────────
57
218
  function readBody(req) {
58
219
  return new Promise((resolve, reject) => {
@@ -63,7 +224,18 @@ function readBody(req) {
63
224
  });
64
225
  }
65
226
  function send(res, status, body) {
66
- const payload = JSON.stringify(body);
227
+ const requestIdRaw = res.getHeader(REQUEST_ID_HEADER);
228
+ const requestId = typeof requestIdRaw === 'string' && requestIdRaw.trim().length > 0
229
+ ? requestIdRaw.trim()
230
+ : null;
231
+ const payloadBody = (requestId
232
+ && body
233
+ && typeof body === 'object'
234
+ && !Array.isArray(body)
235
+ && !Object.prototype.hasOwnProperty.call(body, 'requestId'))
236
+ ? { ...body, requestId }
237
+ : body;
238
+ const payload = JSON.stringify(payloadBody);
67
239
  res.writeHead(status, {
68
240
  'Content-Type': 'application/json',
69
241
  'Content-Length': Buffer.byteLength(payload),
@@ -73,8 +245,52 @@ function send(res, status, body) {
73
245
  function success(res, data) {
74
246
  send(res, 200, { success: true, data });
75
247
  }
76
- function failure(res, error, status = 200) {
77
- send(res, status, { success: false, error });
248
+ function defaultFailureCode(status, error) {
249
+ if (status === 400)
250
+ return contracts_1.DAEMON_ERROR_CODES.badRequest;
251
+ if (status === 401)
252
+ return contracts_1.DAEMON_ERROR_CODES.unauthorized;
253
+ if (status === 403)
254
+ return contracts_1.DAEMON_ERROR_CODES.forbidden;
255
+ if (status === 404) {
256
+ if (/no route for/i.test(error))
257
+ return contracts_1.DAEMON_ERROR_CODES.routeNotFound;
258
+ return contracts_1.DAEMON_ERROR_CODES.notFound;
259
+ }
260
+ if (status === 408)
261
+ return contracts_1.DAEMON_ERROR_CODES.timeout;
262
+ if (status === 409)
263
+ return contracts_1.DAEMON_ERROR_CODES.conflict;
264
+ if (status === 422)
265
+ return contracts_1.DAEMON_ERROR_CODES.validationFailed;
266
+ if (status === 429)
267
+ return contracts_1.DAEMON_ERROR_CODES.rateLimited;
268
+ if (status >= 500)
269
+ return contracts_1.DAEMON_ERROR_CODES.internalError;
270
+ return contracts_1.DAEMON_ERROR_CODES.unknown;
271
+ }
272
+ function failure(res, error, status = 500, options = {}) {
273
+ const code = options.code ?? defaultFailureCode(status, error);
274
+ const retriable = options.retriable ?? status >= 500;
275
+ const requestIdRaw = res.getHeader(REQUEST_ID_HEADER);
276
+ const requestId = typeof requestIdRaw === 'string' && requestIdRaw.trim().length > 0
277
+ ? requestIdRaw.trim()
278
+ : null;
279
+ const route = res.__neurcodeRoutePath || '/unknown';
280
+ recordDaemonFailure({
281
+ route,
282
+ requestId,
283
+ code,
284
+ message: error,
285
+ retriable,
286
+ });
287
+ send(res, status, {
288
+ success: false,
289
+ error,
290
+ code,
291
+ retriable,
292
+ details: options.details ?? null,
293
+ });
78
294
  }
79
295
  function addCorsHeaders(res, req) {
80
296
  // Wildcard is safe: daemon binds to 127.0.0.1 only and isLoopback() rejects
@@ -84,13 +300,294 @@ function addCorsHeaders(res, req) {
84
300
  const requestedHeaders = Array.isArray(requestedHeadersRaw)
85
301
  ? requestedHeadersRaw.join(',')
86
302
  : (requestedHeadersRaw || '');
87
- const allowedHeaders = new Set(['content-type', 'x-neurcode-source', 'x-neurcode-actor']
303
+ const allowedHeaders = new Set(['content-type', 'x-neurcode-source', 'x-neurcode-actor', REQUEST_ID_HEADER]
88
304
  .concat(requestedHeaders.split(',').map((entry) => entry.trim().toLowerCase()).filter(Boolean)));
89
305
  res.setHeader('Access-Control-Allow-Origin', '*');
90
306
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
91
307
  res.setHeader('Access-Control-Allow-Headers', [...allowedHeaders].join(', '));
92
308
  res.setHeader('Access-Control-Max-Age', '86400');
93
309
  }
310
+ function resolveGitRoot(cwd) {
311
+ const result = (0, node_child_process_1.spawnSync)('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], {
312
+ encoding: 'utf-8',
313
+ stdio: ['ignore', 'pipe', 'ignore'],
314
+ });
315
+ if (result.status !== 0)
316
+ return null;
317
+ const value = typeof result.stdout === 'string' ? result.stdout.trim() : '';
318
+ return value.length > 0 ? value : null;
319
+ }
320
+ function captureGitDirtyPaths(cwd) {
321
+ const gitRoot = resolveGitRoot(cwd);
322
+ if (!gitRoot)
323
+ return null;
324
+ const statusResult = (0, node_child_process_1.spawnSync)('git', ['-C', cwd, 'status', '--porcelain=1', '-z', '--untracked-files=all'], {
325
+ encoding: 'utf-8',
326
+ stdio: ['ignore', 'pipe', 'ignore'],
327
+ });
328
+ if (statusResult.status !== 0 || typeof statusResult.stdout !== 'string')
329
+ return null;
330
+ const tokens = statusResult.stdout.split('\0').filter((entry) => entry.length > 0);
331
+ const dirty = new Set();
332
+ for (let index = 0; index < tokens.length; index += 1) {
333
+ const token = tokens[index];
334
+ if (token.length < 4)
335
+ continue;
336
+ const status = token.slice(0, 2);
337
+ const filePath = token.slice(3).trim();
338
+ if (filePath.length > 0) {
339
+ dirty.add(path.resolve(gitRoot, filePath));
340
+ }
341
+ const renamedOrCopied = status.includes('R') || status.includes('C');
342
+ if (renamedOrCopied && index + 1 < tokens.length) {
343
+ index += 1;
344
+ }
345
+ }
346
+ return dirty;
347
+ }
348
+ function hashFileForDiff(absPath) {
349
+ try {
350
+ const stat = fs.statSync(absPath);
351
+ if (!stat.isFile())
352
+ return '<non-file>';
353
+ const content = fs.readFileSync(absPath);
354
+ return (0, node_crypto_1.createHash)('sha256').update(content).digest('hex');
355
+ }
356
+ catch {
357
+ return '<missing>';
358
+ }
359
+ }
360
+ function captureDirtyFileFingerprints(cwd) {
361
+ const dirtyPaths = captureGitDirtyPaths(cwd);
362
+ if (!dirtyPaths)
363
+ return null;
364
+ const map = new Map();
365
+ for (const dirtyPath of dirtyPaths) {
366
+ map.set(dirtyPath, hashFileForDiff(dirtyPath));
367
+ }
368
+ return map;
369
+ }
370
+ function isAllowedPatchSideEffect(absPath, targetAbsPath, cwd) {
371
+ if (absPath === targetAbsPath)
372
+ return true;
373
+ const rel = path.relative(cwd, absPath);
374
+ if (!rel || rel.startsWith('..'))
375
+ return false;
376
+ if (rel === 'neurcode.policy.compiled.json')
377
+ return true;
378
+ return rel === '.neurcode' || rel.startsWith(`.neurcode${path.sep}`);
379
+ }
380
+ function collectUnexpectedPatchSideEffects(before, after, targetAbsPath, cwd) {
381
+ if (!before || !after)
382
+ return [];
383
+ const added = [...after].filter((entry) => !before.has(entry));
384
+ const unexpected = added
385
+ .filter((entry) => !isAllowedPatchSideEffect(entry, targetAbsPath, cwd))
386
+ .map((entry) => path.relative(cwd, entry).replace(/\\/g, '/'))
387
+ .filter((entry) => entry.length > 0)
388
+ .sort();
389
+ return unexpected;
390
+ }
391
+ function collectUnexpectedPatchMutations(before, after, targetAbsPath, cwd) {
392
+ if (!before || !after)
393
+ return [];
394
+ const keys = new Set([...before.keys(), ...after.keys()]);
395
+ const unexpected = [];
396
+ for (const key of keys) {
397
+ const beforeHash = before.get(key) ?? '<missing>';
398
+ const afterHash = after.get(key) ?? '<missing>';
399
+ if (beforeHash === afterHash)
400
+ continue;
401
+ if (isAllowedPatchSideEffect(key, targetAbsPath, cwd))
402
+ continue;
403
+ const rel = path.relative(cwd, key).replace(/\\/g, '/');
404
+ if (rel.length > 0 && !rel.startsWith('..')) {
405
+ unexpected.push(rel);
406
+ }
407
+ }
408
+ return unexpected.sort();
409
+ }
410
+ function patternDescriptor(kind, confidence, manualReviewRequired) {
411
+ const labelByKind = {
412
+ missing_validation: 'API input validation guard',
413
+ missing_timeout_handling: 'Outbound request timeout guard',
414
+ unsafe_fetch_without_retries: 'Outbound request retry guard',
415
+ missing_idempotency_keys: 'Mutation idempotency-key guard',
416
+ unsafe_file_uploads: 'Upload MIME/size validation guard',
417
+ missing_auth_middleware: 'Route authentication middleware',
418
+ missing_rate_limiting: 'Route rate limiting middleware',
419
+ missing_token_expiry: 'JWT expiry enforcement',
420
+ unsafe_inner_html_usage: 'Unsafe DOM sink replacement',
421
+ unsafe_sensitive_logging: 'Sensitive log redaction',
422
+ db_in_ui: 'Service-layer boundary placeholder',
423
+ todo_fixme: 'TODO/FIXME debt marker removal',
424
+ };
425
+ const confidenceModel = confidence === 'high'
426
+ ? 'high'
427
+ : confidence === 'medium'
428
+ ? 'medium'
429
+ : 'low';
430
+ return {
431
+ kind,
432
+ label: labelByKind[kind] || kind,
433
+ deterministic: true,
434
+ confidenceModel,
435
+ advisoryOnly: confidenceModel === 'low',
436
+ manualReviewRequired,
437
+ };
438
+ }
439
+ function summarizeDiff(diff) {
440
+ let addedLines = 0;
441
+ let removedLines = 0;
442
+ for (const line of diff.split('\n')) {
443
+ if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('@@'))
444
+ continue;
445
+ if (line.startsWith('+'))
446
+ addedLines += 1;
447
+ if (line.startsWith('-'))
448
+ removedLines += 1;
449
+ }
450
+ const changedLines = addedLines + removedLines;
451
+ return {
452
+ addedLines,
453
+ removedLines,
454
+ changedLines,
455
+ summary: `${changedLines} changed line(s): +${addedLines} / -${removedLines}`,
456
+ };
457
+ }
458
+ function extractRequestInputUsage(content) {
459
+ const accessMatch = content.match(/\b(req|request)\.(body|params|query)\b/);
460
+ if (!accessMatch)
461
+ return null;
462
+ const receiver = accessMatch[1];
463
+ const field = accessMatch[2];
464
+ const fieldRegex = new RegExp(`\\b${receiver}\\.${field}\\.([A-Za-z_$][\\w$]*)\\b`, 'g');
465
+ const fields = [];
466
+ const seen = new Set();
467
+ let match = fieldRegex.exec(content);
468
+ while (match) {
469
+ const fieldName = match[1];
470
+ if (!seen.has(fieldName)) {
471
+ seen.add(fieldName);
472
+ fields.push(fieldName);
473
+ }
474
+ match = fieldRegex.exec(content);
475
+ }
476
+ return { receiver, field, fields };
477
+ }
478
+ function buildPatchPreviewReasoning(patternKind, targetPath, beforeContent) {
479
+ if (!patternKind)
480
+ return null;
481
+ if (patternKind === 'missing_validation') {
482
+ const usage = extractRequestInputUsage(beforeContent);
483
+ if (!usage) {
484
+ return {
485
+ summary: 'Adds deterministic API input validation guard.',
486
+ why: `This file accesses request input without a validation boundary check.`,
487
+ risk: 'Malformed input can cause runtime errors or unsafe processing paths.',
488
+ expectedOutcome: 'Invalid requests fail fast and valid requests continue unchanged.',
489
+ };
490
+ }
491
+ const noun = usage.field === 'body' ? 'request body' : usage.field === 'params' ? 'route params' : 'query params';
492
+ const fieldSummary = usage.fields.length > 0 ? usage.fields.join(', ') : 'no explicit property access detected';
493
+ return {
494
+ summary: `Adds deterministic validation before reading ${usage.receiver}.${usage.field}.`,
495
+ why: `${targetPath} reads ${noun} fields (${fieldSummary}) before validation.`,
496
+ risk: `Without boundary validation, malformed ${noun} may propagate into handler logic.`,
497
+ expectedOutcome: `Invalid ${noun} returns HTTP 400 early; valid requests keep existing behavior.`,
498
+ fields: usage.fields,
499
+ };
500
+ }
501
+ if (patternKind === 'db_in_ui') {
502
+ return {
503
+ summary: 'Suggests moving direct DB access behind a service boundary.',
504
+ why: `${targetPath} appears to perform direct data access in a non-service layer.`,
505
+ risk: 'Layering violations increase coupling and make behavior harder to govern.',
506
+ expectedOutcome: 'Patch inserts a deterministic placeholder to redirect to service-layer logic.',
507
+ };
508
+ }
509
+ if (patternKind === 'missing_auth_middleware') {
510
+ return {
511
+ summary: 'Adds deterministic authentication middleware to the route definition.',
512
+ why: `${targetPath} appears to expose a request handler without an auth middleware guard.`,
513
+ risk: 'Unauthenticated routes can expose sensitive behavior to unauthorized clients.',
514
+ expectedOutcome: 'Route execution is gated by requireAuth before handler logic runs.',
515
+ };
516
+ }
517
+ if (patternKind === 'missing_rate_limiting') {
518
+ return {
519
+ summary: 'Adds deterministic rate-limit middleware to the route definition.',
520
+ why: `${targetPath} appears to expose a request handler without rate limiting controls.`,
521
+ risk: 'Unbounded request rates can increase abuse, cost, and availability risks.',
522
+ expectedOutcome: 'Route applies rateLimitGuard before handler execution.',
523
+ };
524
+ }
525
+ if (patternKind === 'missing_timeout_handling') {
526
+ return {
527
+ summary: 'Adds deterministic timeout guard to outbound fetch call.',
528
+ why: `${targetPath} issues a fetch request without timeout protection.`,
529
+ risk: 'Unbounded network calls can hang request execution and degrade reliability under upstream latency.',
530
+ expectedOutcome: 'Fetch call aborts after timeout and fails fast instead of hanging.',
531
+ };
532
+ }
533
+ if (patternKind === 'unsafe_fetch_without_retries') {
534
+ return {
535
+ summary: 'Wraps outbound fetch call in deterministic retry guard.',
536
+ why: `${targetPath} makes outbound network calls without transient failure retry handling.`,
537
+ risk: 'Single transient failures can become user-facing errors and increase instability.',
538
+ expectedOutcome: 'Transient upstream failures retry deterministically before failing.',
539
+ };
540
+ }
541
+ if (patternKind === 'missing_idempotency_keys') {
542
+ return {
543
+ summary: 'Adds deterministic idempotency-key guard for side-effecting requests.',
544
+ why: `${targetPath} appears to process payment/order-like mutations without idempotency key enforcement.`,
545
+ risk: 'Duplicate requests can cause repeated side effects (double charges/orders).',
546
+ expectedOutcome: 'Requests missing idempotency key fail early with explicit error.',
547
+ };
548
+ }
549
+ if (patternKind === 'unsafe_file_uploads') {
550
+ return {
551
+ summary: 'Adds deterministic MIME and size guards for uploaded files.',
552
+ why: `${targetPath} appears to process uploaded files without boundary checks.`,
553
+ risk: 'Unbounded or unsafe uploads increase security and stability risk.',
554
+ expectedOutcome: 'Invalid upload payloads are rejected before processing.',
555
+ };
556
+ }
557
+ if (patternKind === 'missing_token_expiry') {
558
+ return {
559
+ summary: 'Adds deterministic token expiry to JWT signing call.',
560
+ why: `${targetPath} signs JWT tokens without an expiresIn option.`,
561
+ risk: 'Long-lived tokens increase replay and account-compromise blast radius.',
562
+ expectedOutcome: 'Tokens gain explicit expiry to enforce credential rotation windows.',
563
+ };
564
+ }
565
+ if (patternKind === 'unsafe_inner_html_usage') {
566
+ return {
567
+ summary: 'Replaces unsafe innerHTML assignment with textContent.',
568
+ why: `${targetPath} writes HTML content directly into the DOM using innerHTML.`,
569
+ risk: 'innerHTML assignments can expose XSS vectors when input is not trusted.',
570
+ expectedOutcome: 'DOM assignment becomes text-only rendering with reduced injection risk.',
571
+ };
572
+ }
573
+ if (patternKind === 'unsafe_sensitive_logging') {
574
+ return {
575
+ summary: 'Removes deterministic sensitive logging line.',
576
+ why: `${targetPath} appears to log secret-bearing fields (token/authorization/password).`,
577
+ risk: 'Sensitive log content can leak credentials to observability or audit sinks.',
578
+ expectedOutcome: 'Sensitive logging path is replaced with a neutral warning placeholder.',
579
+ };
580
+ }
581
+ if (patternKind === 'todo_fixme') {
582
+ return {
583
+ summary: 'Removes TODO/FIXME marker matched by policy.',
584
+ why: `${targetPath} includes TODO/FIXME comments tracked as governance debt.`,
585
+ risk: 'Unresolved TODO markers can hide missing implementation or review debt.',
586
+ expectedOutcome: 'Patch removes the marker; implementation must still be verified separately.',
587
+ };
588
+ }
589
+ return null;
590
+ }
94
591
  function isExecutionActionType(value) {
95
592
  if (typeof value !== 'string')
96
593
  return false;
@@ -102,7 +599,119 @@ function isExecutionActionType(value) {
102
599
  || value === 'policy-sync'
103
600
  || value === 'intent-update');
104
601
  }
602
+ function asObjectRecord(value) {
603
+ if (!value || typeof value !== 'object' || Array.isArray(value))
604
+ return null;
605
+ return value;
606
+ }
607
+ function asObjectArray(value) {
608
+ if (!Array.isArray(value))
609
+ return [];
610
+ return value
611
+ .map((entry) => asObjectRecord(entry))
612
+ .filter((entry) => entry !== null);
613
+ }
614
+ function toLegacyViolation(entry, fallbackSeverity) {
615
+ const file = typeof entry.file === 'string' && entry.file.trim().length > 0
616
+ ? entry.file.trim()
617
+ : '';
618
+ const message = typeof entry.message === 'string' && entry.message.trim().length > 0
619
+ ? entry.message.trim()
620
+ : '';
621
+ if (!file || !message)
622
+ return null;
623
+ const severity = typeof entry.severity === 'string' && entry.severity.trim().length > 0
624
+ ? entry.severity.trim()
625
+ : fallbackSeverity;
626
+ const rule = typeof entry.rule === 'string' && entry.rule.trim().length > 0
627
+ ? entry.rule.trim()
628
+ : typeof entry.policy === 'string' && entry.policy.trim().length > 0
629
+ ? entry.policy.trim()
630
+ : '';
631
+ return { file, message, severity, rule };
632
+ }
633
+ function normalizeVerifyPayloadForLegacyClients(payload) {
634
+ if (!payload)
635
+ return null;
636
+ const existingViolations = asObjectArray(payload.violations)
637
+ .map((entry) => toLegacyViolation(entry, 'warn'))
638
+ .filter((entry) => entry !== null);
639
+ const blockingItems = asObjectArray(payload.blockingItems)
640
+ .map((entry) => toLegacyViolation(entry, 'block'))
641
+ .filter((entry) => entry !== null);
642
+ const advisoryItems = asObjectArray(payload.advisoryItems)
643
+ .map((entry) => toLegacyViolation(entry, 'warn'))
644
+ .filter((entry) => entry !== null);
645
+ const warnings = asObjectArray(payload.warnings)
646
+ .map((entry) => toLegacyViolation(entry, 'warn'))
647
+ .filter((entry) => entry !== null);
648
+ const merged = [...existingViolations];
649
+ const canonicalSeverity = (value) => {
650
+ const normalized = value.trim().toLowerCase();
651
+ if (normalized === 'block' || normalized === 'critical' || normalized === 'high')
652
+ return 'block';
653
+ if (normalized === 'warn' || normalized === 'warning' || normalized === 'advisory' || normalized === 'medium' || normalized === 'low')
654
+ return 'warn';
655
+ return normalized;
656
+ };
657
+ const canonicalKey = (entry) => `${entry.file}::${entry.rule}::${entry.message}::${canonicalSeverity(entry.severity)}`;
658
+ const seen = new Set(merged.map((entry) => canonicalKey(entry)));
659
+ for (const item of [...blockingItems, ...advisoryItems, ...warnings]) {
660
+ const key = canonicalKey(item);
661
+ if (seen.has(key))
662
+ continue;
663
+ seen.add(key);
664
+ merged.push(item);
665
+ }
666
+ if (merged.length === 0)
667
+ return payload;
668
+ return {
669
+ ...payload,
670
+ violations: merged,
671
+ };
672
+ }
673
+ function normalizeFixPayloadForLegacyClients(payload) {
674
+ if (!payload)
675
+ return null;
676
+ const suggestions = asObjectArray(payload.suggestions);
677
+ if (suggestions.length === 0)
678
+ return payload;
679
+ const deduped = [];
680
+ const seen = new Set();
681
+ for (const suggestion of suggestions) {
682
+ const file = typeof suggestion.file === 'string' ? suggestion.file.trim() : '';
683
+ const line = typeof suggestion.line === 'number' && Number.isFinite(suggestion.line)
684
+ ? String(Math.floor(suggestion.line))
685
+ : '';
686
+ const message = typeof suggestion.message === 'string' ? suggestion.message.trim() : '';
687
+ const rule = typeof suggestion.rule === 'string'
688
+ ? suggestion.rule.trim()
689
+ : typeof suggestion.policy === 'string'
690
+ ? suggestion.policy.trim()
691
+ : '';
692
+ const confidence = typeof suggestion.confidence === 'string' ? suggestion.confidence.trim().toLowerCase() : '';
693
+ const patch = asObjectRecord(suggestion.patch);
694
+ const patchDiff = patch && typeof patch.diff === 'string' ? patch.diff : '';
695
+ const key = `${file}::${line}::${rule}::${message}::${confidence}::${(0, node_crypto_1.createHash)('sha1').update(patchDiff).digest('hex')}`;
696
+ if (seen.has(key))
697
+ continue;
698
+ seen.add(key);
699
+ deduped.push(suggestion);
700
+ }
701
+ if (deduped.length === suggestions.length)
702
+ return payload;
703
+ return {
704
+ ...payload,
705
+ suggestions: deduped,
706
+ _normalization: {
707
+ ...(asObjectRecord(payload._normalization) || {}),
708
+ suggestionsDeduped: suggestions.length - deduped.length,
709
+ },
710
+ };
711
+ }
105
712
  function isLoopback(req) {
713
+ if (ALLOW_NON_LOOPBACK)
714
+ return true;
106
715
  const addr = req.socket.remoteAddress ?? '';
107
716
  return addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1';
108
717
  }
@@ -350,8 +959,9 @@ async function handleVerify(req, res) {
350
959
  failure(res, run.execution.result?.message || 'verify execution produced no payload');
351
960
  return;
352
961
  }
962
+ const normalizedPayload = normalizeVerifyPayloadForLegacyClients(run.primaryPayload ?? null) ?? run.primaryPayload;
353
963
  success(res, {
354
- ...run.primaryPayload,
964
+ ...normalizedPayload,
355
965
  _execution: {
356
966
  id: run.execution.id,
357
967
  type: run.execution.type,
@@ -376,9 +986,11 @@ async function handleFix(req, res) {
376
986
  failure(res, run.execution.result?.message || 'fix execution produced no payload');
377
987
  return;
378
988
  }
989
+ const normalizedFixPayload = normalizeFixPayloadForLegacyClients(run.primaryPayload) ?? run.primaryPayload;
990
+ const normalizedVerifyAfter = normalizeVerifyPayloadForLegacyClients(run.verificationPayload);
379
991
  success(res, {
380
- ...run.primaryPayload,
381
- verifyAfter: run.verificationPayload ?? null,
992
+ ...normalizedFixPayload,
993
+ verifyAfter: normalizedVerifyAfter ?? null,
382
994
  _execution: {
383
995
  id: run.execution.id,
384
996
  type: run.execution.type,
@@ -403,9 +1015,11 @@ async function handleFixApplySafe(req, res) {
403
1015
  failure(res, run.execution.result?.message || 'fix --apply-safe execution produced no payload');
404
1016
  return;
405
1017
  }
1018
+ const normalizedFixPayload = normalizeFixPayloadForLegacyClients(run.primaryPayload) ?? run.primaryPayload;
1019
+ const normalizedVerifyAfter = normalizeVerifyPayloadForLegacyClients(run.verificationPayload);
406
1020
  success(res, {
407
- ...run.primaryPayload,
408
- verifyAfter: run.verificationPayload ?? null,
1021
+ ...normalizedFixPayload,
1022
+ verifyAfter: normalizedVerifyAfter ?? null,
409
1023
  execution: run.execution,
410
1024
  });
411
1025
  }
@@ -423,24 +1037,36 @@ async function handlePatch(req, res) {
423
1037
  failure(res, 'Missing or unsafe "file" field', 400);
424
1038
  return;
425
1039
  }
1040
+ const previewToken = typeof body.previewToken === 'string' && body.previewToken.trim().length > 0
1041
+ ? body.previewToken.trim()
1042
+ : undefined;
1043
+ const cwd = process.cwd();
1044
+ const targetPath = file.trim();
1045
+ const absPath = path.resolve(cwd, targetPath);
1046
+ const beforeDirtyPaths = captureGitDirtyPaths(cwd);
1047
+ const beforeDirtyFingerprints = captureDirtyFileFingerprints(cwd);
426
1048
  // Capture file content before patch to detect real change
427
- const absPath = path.resolve(process.cwd(), file);
428
1049
  let contentBefore = null;
429
1050
  try {
430
1051
  contentBefore = fs.readFileSync(absPath, 'utf-8');
431
1052
  }
432
1053
  catch { /* file may not exist */ }
1054
+ const primaryArgs = ['patch', '--file', targetPath];
1055
+ if (previewToken) {
1056
+ primaryArgs.push('--preview-token', previewToken);
1057
+ }
433
1058
  const run = await (0, execution_bus_1.runExecution)({
434
1059
  type: 'patch',
435
1060
  source: toSource(req),
436
1061
  actor: toActor(req),
437
- target: file,
438
- cwd: process.cwd(),
1062
+ target: targetPath,
1063
+ cwd,
439
1064
  reverify: true,
1065
+ primaryArgs,
440
1066
  });
441
1067
  const patchData = run.primaryPayload ?? {
442
1068
  success: false,
443
- file,
1069
+ file: targetPath,
444
1070
  message: run.execution.result?.message || 'No applicable patch found',
445
1071
  };
446
1072
  // Validate that the file actually changed on disk
@@ -452,12 +1078,298 @@ async function handlePatch(req, res) {
452
1078
  }
453
1079
  catch { /* ignore read error */ }
454
1080
  }
1081
+ const afterDirtyPaths = captureGitDirtyPaths(cwd);
1082
+ const afterDirtyFingerprints = captureDirtyFileFingerprints(cwd);
1083
+ const sideEffects = collectUnexpectedPatchSideEffects(beforeDirtyPaths, afterDirtyPaths, absPath, cwd);
1084
+ const mutatedSideEffects = collectUnexpectedPatchMutations(beforeDirtyFingerprints, afterDirtyFingerprints, absPath, cwd);
1085
+ const combinedSideEffects = [...new Set([...sideEffects, ...mutatedSideEffects])].sort();
1086
+ const payloadFile = typeof patchData.file === 'string' ? patchData.file : '';
1087
+ const payloadTargetMatch = payloadFile.length > 0
1088
+ ? path.resolve(cwd, payloadFile) === absPath
1089
+ : true;
1090
+ const patchSucceeded = patchData.success === true;
1091
+ const rawPatchStatus = typeof patchData.status === 'string' ? patchData.status : '';
1092
+ const patchStatus = rawPatchStatus === 'filesystem_changed_since_preview'
1093
+ ? 'stale_preview'
1094
+ : !patchSucceeded
1095
+ ? 'rejected'
1096
+ : changed && payloadTargetMatch && combinedSideEffects.length === 0
1097
+ ? 'applied'
1098
+ : changed
1099
+ ? 'partial'
1100
+ : 'rejected';
1101
+ const patchMessage = (() => {
1102
+ if (patchStatus === 'applied') {
1103
+ return (typeof patchData.message === 'string' && patchData.message.trim().length > 0)
1104
+ ? patchData.message
1105
+ : `${contracts_1.STATUS_TERMS.safePatchApplied}`;
1106
+ }
1107
+ if (patchStatus === 'partial') {
1108
+ if (!payloadTargetMatch) {
1109
+ return `${contracts_1.STATUS_TERMS.patchRejected}: patch target mismatch detected between requested file and daemon payload file.`;
1110
+ }
1111
+ if (combinedSideEffects.length > 0) {
1112
+ return `${contracts_1.STATUS_TERMS.patchRejected}: patch introduced side effects in ${combinedSideEffects.length} additional file(s).`;
1113
+ }
1114
+ return `${contracts_1.STATUS_TERMS.safePatchApplied}. ${contracts_1.STATUS_TERMS.manualReviewRecommended}.`;
1115
+ }
1116
+ if (patchStatus === 'stale_preview') {
1117
+ return `${contracts_1.STATUS_TERMS.filesystemChangedSincePreview}. Regenerate patch preview and retry. ${contracts_1.STATUS_TERMS.retrySafe}.`;
1118
+ }
1119
+ return (typeof patchData.message === 'string' && patchData.message.trim().length > 0)
1120
+ ? patchData.message
1121
+ : `${contracts_1.STATUS_TERMS.patchRejected}; no deterministic file-scoped change applied`;
1122
+ })();
1123
+ const reverifyRequired = patchStatus === 'applied' || patchStatus === 'partial';
1124
+ const stateLabel = patchStatus === 'stale_preview'
1125
+ ? contracts_1.STATUS_TERMS.filesystemChangedSincePreview.toLowerCase()
1126
+ : (0, contracts_1.toPatchStateLabel)(patchStatus).toLowerCase();
1127
+ recordPatchOutcome(patchStatus);
1128
+ const normalizedVerifyPayload = normalizeVerifyPayloadForLegacyClients(run.verificationPayload);
455
1129
  success(res, {
456
- patch: { ...patchData, changed },
457
- verify: run.verificationPayload ?? null,
1130
+ patch: {
1131
+ ...patchData,
1132
+ file: payloadFile || targetPath,
1133
+ success: patchStatus === 'applied',
1134
+ rawSuccess: patchData.success === true,
1135
+ changed,
1136
+ status: patchStatus,
1137
+ targetMatch: payloadTargetMatch,
1138
+ sideEffects: combinedSideEffects,
1139
+ message: patchMessage,
1140
+ reverifyRequired,
1141
+ stateLabel,
1142
+ previewTokenUsed: previewToken ? true : false,
1143
+ },
1144
+ verify: normalizedVerifyPayload ?? null,
458
1145
  execution: run.execution,
459
1146
  });
460
1147
  }
1148
+ async function handlePatchRollback(req, res) {
1149
+ let body = {};
1150
+ try {
1151
+ body = JSON.parse(await readBody(req));
1152
+ }
1153
+ catch {
1154
+ failure(res, 'Invalid JSON body', 400);
1155
+ return;
1156
+ }
1157
+ const file = body.file;
1158
+ const receiptId = typeof body.receiptId === 'string' ? body.receiptId.trim() : '';
1159
+ if (!file || typeof file !== 'string' || file.includes('..')) {
1160
+ failure(res, 'Missing or unsafe "file" field', 400);
1161
+ return;
1162
+ }
1163
+ if (!receiptId) {
1164
+ failure(res, 'Missing "receiptId" field', 400);
1165
+ return;
1166
+ }
1167
+ const cwd = process.cwd();
1168
+ const targetPath = file.trim();
1169
+ const absPath = path.resolve(cwd, targetPath);
1170
+ const beforeDirtyPaths = captureGitDirtyPaths(cwd);
1171
+ const beforeDirtyFingerprints = captureDirtyFileFingerprints(cwd);
1172
+ let contentBefore = null;
1173
+ try {
1174
+ contentBefore = fs.readFileSync(absPath, 'utf-8');
1175
+ }
1176
+ catch { /* file may not exist */ }
1177
+ const run = await (0, execution_bus_1.runExecution)({
1178
+ type: 'patch',
1179
+ source: toSource(req),
1180
+ actor: toActor(req),
1181
+ target: targetPath,
1182
+ cwd,
1183
+ reverify: true,
1184
+ primaryArgs: ['patch', '--file', targetPath, '--rollback-receipt', receiptId, '--json'],
1185
+ });
1186
+ const patchData = run.primaryPayload ?? {
1187
+ success: false,
1188
+ file: targetPath,
1189
+ message: run.execution.result?.message || 'No rollback receipt could be applied',
1190
+ };
1191
+ let changed = false;
1192
+ if (patchData.success && contentBefore !== null) {
1193
+ try {
1194
+ const contentAfter = fs.readFileSync(absPath, 'utf-8');
1195
+ changed = contentAfter !== contentBefore;
1196
+ }
1197
+ catch {
1198
+ // ignore read error
1199
+ }
1200
+ }
1201
+ const afterDirtyPaths = captureGitDirtyPaths(cwd);
1202
+ const afterDirtyFingerprints = captureDirtyFileFingerprints(cwd);
1203
+ const sideEffects = collectUnexpectedPatchSideEffects(beforeDirtyPaths, afterDirtyPaths, absPath, cwd);
1204
+ const mutatedSideEffects = collectUnexpectedPatchMutations(beforeDirtyFingerprints, afterDirtyFingerprints, absPath, cwd);
1205
+ const combinedSideEffects = [...new Set([...sideEffects, ...mutatedSideEffects])].sort();
1206
+ const payloadFile = typeof patchData.file === 'string' ? patchData.file : '';
1207
+ const payloadTargetMatch = payloadFile.length > 0
1208
+ ? path.resolve(cwd, payloadFile) === absPath
1209
+ : true;
1210
+ const rawStatus = typeof patchData.status === 'string' ? patchData.status : '';
1211
+ const rollbackStatus = rawStatus === 'rollback_applied'
1212
+ ? 'rollback_applied'
1213
+ : rawStatus === 'rollback_stale' || rawStatus === 'filesystem_changed_since_patch'
1214
+ ? 'rollback_stale'
1215
+ : 'rollback_rejected';
1216
+ const rollbackSucceeded = patchData.success === true && rollbackStatus === 'rollback_applied' && payloadTargetMatch && combinedSideEffects.length === 0;
1217
+ const rollbackMessage = (() => {
1218
+ if (rollbackSucceeded) {
1219
+ return (typeof patchData.message === 'string' && patchData.message.trim().length > 0)
1220
+ ? patchData.message
1221
+ : contracts_1.STATUS_TERMS.rollbackApplied;
1222
+ }
1223
+ if (!payloadTargetMatch) {
1224
+ return `${contracts_1.STATUS_TERMS.patchRejected}: rollback receipt target mismatch detected.`;
1225
+ }
1226
+ if (combinedSideEffects.length > 0) {
1227
+ return `${contracts_1.STATUS_TERMS.patchRejected}: rollback side effects detected in ${combinedSideEffects.length} additional file(s).`;
1228
+ }
1229
+ return (typeof patchData.message === 'string' && patchData.message.trim().length > 0)
1230
+ ? patchData.message
1231
+ : contracts_1.STATUS_TERMS.patchRejected;
1232
+ })();
1233
+ recordPatchOutcome(rollbackStatus);
1234
+ success(res, {
1235
+ patch: {
1236
+ ...patchData,
1237
+ file: payloadFile || targetPath,
1238
+ success: rollbackSucceeded,
1239
+ rawSuccess: patchData.success === true,
1240
+ changed,
1241
+ status: rollbackStatus,
1242
+ targetMatch: payloadTargetMatch,
1243
+ sideEffects: combinedSideEffects,
1244
+ message: rollbackMessage,
1245
+ reverifyRequired: rollbackSucceeded,
1246
+ stateLabel: rollbackSucceeded
1247
+ ? contracts_1.STATUS_TERMS.rollbackApplied.toLowerCase()
1248
+ : rollbackStatus === 'rollback_stale'
1249
+ ? contracts_1.STATUS_TERMS.filesystemChangedSincePreview.toLowerCase()
1250
+ : contracts_1.STATUS_TERMS.patchRejected.toLowerCase(),
1251
+ previewTokenUsed: false,
1252
+ },
1253
+ verify: normalizeVerifyPayloadForLegacyClients(run.verificationPayload) ?? null,
1254
+ execution: run.execution,
1255
+ });
1256
+ }
1257
+ async function handlePatchPreview(req, res) {
1258
+ let body = {};
1259
+ try {
1260
+ body = JSON.parse(await readBody(req));
1261
+ }
1262
+ catch {
1263
+ failure(res, 'Invalid JSON body', 400);
1264
+ return;
1265
+ }
1266
+ const file = body.file;
1267
+ if (!file || typeof file !== 'string' || file.includes('..')) {
1268
+ failure(res, 'Missing or unsafe "file" field', 400);
1269
+ return;
1270
+ }
1271
+ const cwd = process.cwd();
1272
+ const targetPath = file.trim();
1273
+ const absPath = path.resolve(cwd, targetPath);
1274
+ let contentBefore = '';
1275
+ try {
1276
+ contentBefore = fs.readFileSync(absPath, 'utf-8');
1277
+ }
1278
+ catch {
1279
+ failure(res, `File not found: ${targetPath}`, 404);
1280
+ return;
1281
+ }
1282
+ const preview = (0, patch_engine_1.applyFirstMatchingPatch)(targetPath, contentBefore);
1283
+ if (!preview) {
1284
+ success(res, {
1285
+ success: false,
1286
+ file: targetPath,
1287
+ status: 'rejected',
1288
+ message: `No deterministic patch preview available for ${targetPath}`,
1289
+ beforeContent: contentBefore,
1290
+ afterContent: null,
1291
+ diff: null,
1292
+ changed: false,
1293
+ patternKind: null,
1294
+ patchConfidence: null,
1295
+ patchHash: null,
1296
+ previewToken: null,
1297
+ validation: null,
1298
+ recipe: null,
1299
+ pattern: null,
1300
+ whatChanges: null,
1301
+ rollbackPreviewDiff: null,
1302
+ whySafe: null,
1303
+ manualReviewRequired: true,
1304
+ supportedDeterministicPattern: false,
1305
+ reasoning: null,
1306
+ });
1307
+ return;
1308
+ }
1309
+ const reasoning = buildPatchPreviewReasoning(preview.patternKind, targetPath, contentBefore);
1310
+ const rollbackPreviewDiff = (0, diff_1.generateUnifiedDiff)(targetPath, preview.updatedContent, contentBefore);
1311
+ const changeSummary = summarizeDiff(preview.diff);
1312
+ const manualReviewRequired = preview.patchConfidence === 'low'
1313
+ || preview.validation.safe !== true
1314
+ || preview.recipe.requiresManualReview === true;
1315
+ const pattern = patternDescriptor(preview.patternKind, preview.patchConfidence, manualReviewRequired);
1316
+ const whySafe = {
1317
+ deterministic: true,
1318
+ validationPassed: preview.validation.safe === true,
1319
+ confidence: preview.patchConfidence,
1320
+ checks: preview.validation.checks,
1321
+ reasonCodes: preview.validation.reasonCodes,
1322
+ };
1323
+ if (!preview.validation.safe) {
1324
+ success(res, {
1325
+ success: false,
1326
+ file: targetPath,
1327
+ status: 'rejected',
1328
+ message: `Patch preview rejected by deterministic safety validation (${preview.validation.reasonCodes.join(', ') || 'unknown'}).`,
1329
+ beforeContent: contentBefore,
1330
+ afterContent: null,
1331
+ diff: preview.diff,
1332
+ changed: false,
1333
+ patternKind: preview.patternKind,
1334
+ patchConfidence: preview.patchConfidence,
1335
+ patchHash: preview.patchHash,
1336
+ previewToken: preview.previewToken,
1337
+ validation: preview.validation,
1338
+ recipe: preview.recipe,
1339
+ pattern,
1340
+ whatChanges: changeSummary,
1341
+ rollbackPreviewDiff,
1342
+ whySafe,
1343
+ manualReviewRequired,
1344
+ supportedDeterministicPattern: true,
1345
+ reasoning,
1346
+ });
1347
+ return;
1348
+ }
1349
+ success(res, {
1350
+ success: true,
1351
+ file: targetPath,
1352
+ status: 'preview',
1353
+ message: 'Patch preview generated',
1354
+ beforeContent: contentBefore,
1355
+ afterContent: preview.updatedContent,
1356
+ diff: preview.diff,
1357
+ changed: contentBefore !== preview.updatedContent,
1358
+ patternKind: preview.patternKind,
1359
+ patchConfidence: preview.patchConfidence,
1360
+ patchHash: preview.patchHash,
1361
+ previewToken: preview.previewToken,
1362
+ validation: preview.validation,
1363
+ recipe: preview.recipe,
1364
+ pattern,
1365
+ whatChanges: changeSummary,
1366
+ rollbackPreviewDiff,
1367
+ whySafe,
1368
+ manualReviewRequired,
1369
+ supportedDeterministicPattern: true,
1370
+ reasoning,
1371
+ });
1372
+ }
461
1373
  async function handleExecute(req, res) {
462
1374
  let body = {};
463
1375
  try {
@@ -966,6 +1878,15 @@ async function handleRuntimeEventStream(req, res) {
966
1878
  // ── Server factory ─────────────────────────────────────────────────────────────
967
1879
  function createDaemonServer() {
968
1880
  const server = http.createServer(async (req, res) => {
1881
+ const incomingRequestIdRaw = req.headers[REQUEST_ID_HEADER];
1882
+ const incomingRequestId = Array.isArray(incomingRequestIdRaw)
1883
+ ? incomingRequestIdRaw[0]
1884
+ : incomingRequestIdRaw;
1885
+ const requestId = (typeof incomingRequestId === 'string'
1886
+ && incomingRequestId.trim().length > 0)
1887
+ ? incomingRequestId.trim().slice(0, 128)
1888
+ : `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
1889
+ res.setHeader(REQUEST_ID_HEADER, requestId);
969
1890
  addCorsHeaders(res, req);
970
1891
  if (req.method === 'OPTIONS') {
971
1892
  res.writeHead(204);
@@ -978,6 +1899,9 @@ function createDaemonServer() {
978
1899
  }
979
1900
  const url = req.url ?? '/';
980
1901
  const method = req.method ?? 'GET';
1902
+ const normalizedRoutePath = normalizeRoutePath(url);
1903
+ recordDaemonRequest(normalizedRoutePath, method);
1904
+ res.__neurcodeRoutePath = normalizedRoutePath;
981
1905
  try {
982
1906
  if (method === 'GET' && url === '/health') {
983
1907
  let version = '0.0.0';
@@ -986,10 +1910,12 @@ function createDaemonServer() {
986
1910
  version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version ?? version;
987
1911
  }
988
1912
  catch { /* ignore */ }
1913
+ const operational = buildDaemonOperationalSummary(process.cwd());
989
1914
  send(res, 200, {
990
1915
  ok: true,
991
1916
  version,
992
1917
  cwd: process.cwd(),
1918
+ operational,
993
1919
  executionBus: {
994
1920
  schemaVersion: 'neurcode.execution.v1',
995
1921
  supportedActions: ['verify', 'fix', 'patch', 'apply-safe', 'reverify', 'policy-sync', 'intent-update'],
@@ -998,6 +1924,11 @@ function createDaemonServer() {
998
1924
  schemaVersion: 'neurcode.runtime-event.v1',
999
1925
  streamPath: '/events/stream',
1000
1926
  },
1927
+ compatibility: {
1928
+ runtimeContractVersion: contracts_1.RUNTIME_COMPATIBILITY_CONTRACT_VERSION,
1929
+ cliJsonContractVersion: contracts_1.CLI_JSON_CONTRACT_VERSION,
1930
+ statusVocabularyVersion: contracts_1.STATUS_VOCABULARY_VERSION,
1931
+ },
1001
1932
  controlPlane: {
1002
1933
  schemaVersion: 'neurcode.control-plane.v1',
1003
1934
  path: '/control-plane',
@@ -1013,6 +1944,14 @@ function createDaemonServer() {
1013
1944
  });
1014
1945
  return;
1015
1946
  }
1947
+ if (method === 'GET' && (url === '/ops/summary' || url.startsWith('/ops/summary?'))) {
1948
+ success(res, {
1949
+ schemaVersion: 'neurcode.daemon.ops.v1',
1950
+ generatedAt: new Date().toISOString(),
1951
+ operational: buildDaemonOperationalSummary(process.cwd()),
1952
+ });
1953
+ return;
1954
+ }
1016
1955
  if (method === 'GET' && url.startsWith('/executions')) {
1017
1956
  if (url === '/executions' || url.startsWith('/executions?')) {
1018
1957
  await handleListExecutions(req, res);
@@ -1053,11 +1992,11 @@ function createDaemonServer() {
1053
1992
  await handleGetControlPlane(req, res);
1054
1993
  return;
1055
1994
  }
1056
- if (method === 'POST' && url === '/control-plane/preview') {
1995
+ if (method === 'POST' && (url === '/control-plane/preview' || url.startsWith('/control-plane/preview?'))) {
1057
1996
  await handlePreviewControlPlaneUpdate(req, res);
1058
1997
  return;
1059
1998
  }
1060
- if (method === 'PUT' && url === '/control-plane') {
1999
+ if (method === 'PUT' && (url === '/control-plane' || url.startsWith('/control-plane?'))) {
1061
2000
  await handleApplyControlPlaneUpdate(req, res);
1062
2001
  return;
1063
2002
  }
@@ -1081,26 +2020,26 @@ function createDaemonServer() {
1081
2020
  return;
1082
2021
  }
1083
2022
  }
1084
- if (method === 'POST' && url === '/workspaces') {
2023
+ if (method === 'POST' && (url === '/workspaces' || url.startsWith('/workspaces?'))) {
1085
2024
  await handleCreateWorkspace(req, res);
1086
2025
  return;
1087
2026
  }
1088
- const activateMatch = url.match(/^\/workspaces\/([^/]+)\/activate$/);
2027
+ const activateMatch = url.match(/^\/workspaces\/([^/]+)\/activate(?:\?.*)?$/);
1089
2028
  if (method === 'POST' && activateMatch) {
1090
2029
  await handleActivateWorkspace(req, res, decodeURIComponent(activateMatch[1]));
1091
2030
  return;
1092
2031
  }
1093
- const addRepoMatch = url.match(/^\/workspaces\/([^/]+)\/repositories$/);
2032
+ const addRepoMatch = url.match(/^\/workspaces\/([^/]+)\/repositories(?:\?.*)?$/);
1094
2033
  if (method === 'POST' && addRepoMatch) {
1095
2034
  await handleAddWorkspaceRepository(req, res, decodeURIComponent(addRepoMatch[1]));
1096
2035
  return;
1097
2036
  }
1098
- const updateWorkspaceMatch = url.match(/^\/workspaces\/([^/?]+)$/);
2037
+ const updateWorkspaceMatch = url.match(/^\/workspaces\/([^/?]+)(?:\?.*)?$/);
1099
2038
  if (method === 'PUT' && updateWorkspaceMatch) {
1100
2039
  await handleUpdateWorkspace(req, res, decodeURIComponent(updateWorkspaceMatch[1]));
1101
2040
  return;
1102
2041
  }
1103
- if (method === 'POST' && url === '/workspaces/execute') {
2042
+ if (method === 'POST' && (url === '/workspaces/execute' || url.startsWith('/workspaces/execute?'))) {
1104
2043
  await handleExecuteWorkspace(req, res);
1105
2044
  return;
1106
2045
  }
@@ -1128,38 +2067,99 @@ function createDaemonServer() {
1128
2067
  return;
1129
2068
  }
1130
2069
  }
1131
- if (method === 'POST' && url === '/execute') {
2070
+ if (method === 'POST' && (url === '/execute' || url.startsWith('/execute?'))) {
1132
2071
  await handleExecute(req, res);
1133
2072
  return;
1134
2073
  }
1135
- if (method === 'POST' && url === '/verify') {
2074
+ if (method === 'POST' && (url === '/verify' || url.startsWith('/verify?'))) {
1136
2075
  await handleVerify(req, res);
1137
2076
  return;
1138
2077
  }
1139
- if (method === 'POST' && url === '/fix') {
2078
+ if (method === 'POST' && (url === '/fix' || url.startsWith('/fix?'))) {
1140
2079
  await handleFix(req, res);
1141
2080
  return;
1142
2081
  }
1143
- if (method === 'POST' && url === '/fix/apply-safe') {
2082
+ if (method === 'POST' && (url === '/fix/apply-safe' || url.startsWith('/fix/apply-safe?'))) {
1144
2083
  await handleFixApplySafe(req, res);
1145
2084
  return;
1146
2085
  }
1147
- if (method === 'POST' && url === '/patch') {
2086
+ if (method === 'POST' && (url === '/patch/preview' || url.startsWith('/patch/preview?'))) {
2087
+ await handlePatchPreview(req, res);
2088
+ return;
2089
+ }
2090
+ if (method === 'POST' && (url === '/patch/rollback' || url.startsWith('/patch/rollback?'))) {
2091
+ await handlePatchRollback(req, res);
2092
+ return;
2093
+ }
2094
+ if (method === 'POST' && (url === '/patch' || url.startsWith('/patch?'))) {
1148
2095
  await handlePatch(req, res);
1149
2096
  return;
1150
2097
  }
1151
2098
  failure(res, `No route for ${method} ${url}`, 404);
1152
2099
  }
1153
2100
  catch (err) {
1154
- failure(res, err instanceof Error ? err.message : String(err), 500);
2101
+ const message = err instanceof Error ? err.message : String(err);
2102
+ if (/execution lock busy|EEXIST: file already exists, open '.*\/\.lock'/.test(message)) {
2103
+ failure(res, 'Execution lock busy. Another daemon action is running; retry this request.', 409, {
2104
+ code: 'daemon.execution_lock_busy',
2105
+ retriable: true,
2106
+ details: { cause: message },
2107
+ });
2108
+ return;
2109
+ }
2110
+ failure(res, message, 500);
1155
2111
  }
1156
2112
  });
1157
2113
  return server;
1158
2114
  }
1159
2115
  // ── Start function ─────────────────────────────────────────────────────────────
2116
+ function validateDaemonStartup(cwd) {
2117
+ const errors = [];
2118
+ const warnings = [];
2119
+ const nodeMajor = Number.parseInt(process.versions.node.split('.')[0] || '0', 10);
2120
+ if (!Number.isFinite(nodeMajor) || nodeMajor < 18) {
2121
+ errors.push(`Node.js >= 18 is required (detected ${process.versions.node}).`);
2122
+ }
2123
+ try {
2124
+ const stat = fs.statSync(cwd);
2125
+ if (!stat.isDirectory()) {
2126
+ errors.push(`Current working directory is not a directory: ${cwd}`);
2127
+ }
2128
+ }
2129
+ catch (error) {
2130
+ errors.push(`Cannot access working directory ${cwd}: ${error instanceof Error ? error.message : String(error)}`);
2131
+ }
2132
+ try {
2133
+ const runtimeDir = path.resolve(cwd, '.neurcode');
2134
+ fs.mkdirSync(runtimeDir, { recursive: true });
2135
+ fs.accessSync(runtimeDir, fs.constants.R_OK | fs.constants.W_OK);
2136
+ }
2137
+ catch (error) {
2138
+ errors.push(`Runtime state directory .neurcode is not writable in ${cwd}: ${error instanceof Error ? error.message : String(error)}`);
2139
+ }
2140
+ const apiUrl = process.env.NEURCODE_API_URL || process.env.VITE_API_URL;
2141
+ if (!apiUrl) {
2142
+ warnings.push('NEURCODE_API_URL is not set; cloud compatibility probes may rely on default localhost API settings.');
2143
+ }
2144
+ if (ALLOW_NON_LOOPBACK) {
2145
+ warnings.push('Remote daemon access is enabled via NEURCODE_DAEMON_ALLOW_REMOTE. Ensure network access is restricted and trusted.');
2146
+ }
2147
+ return { errors, warnings };
2148
+ }
1160
2149
  function startDaemon() {
2150
+ const cwd = process.cwd();
2151
+ const startupValidation = validateDaemonStartup(cwd);
2152
+ for (const warning of startupValidation.warnings) {
2153
+ console.warn(`⚠️ Daemon startup warning: ${warning}`);
2154
+ }
2155
+ if (startupValidation.errors.length > 0) {
2156
+ for (const error of startupValidation.errors) {
2157
+ console.error(`❌ Daemon startup validation error: ${error}`);
2158
+ }
2159
+ process.exit(1);
2160
+ }
1161
2161
  const server = createDaemonServer();
1162
- startRuntimeEventTailer(process.cwd());
2162
+ startRuntimeEventTailer(cwd);
1163
2163
  if (!runtimeEventUnsubscribe) {
1164
2164
  runtimeEventUnsubscribe = (0, runtime_events_1.onRuntimeEvent)((event) => {
1165
2165
  broadcastRuntimeEvent(event);
@@ -1181,6 +2181,8 @@ function startDaemon() {
1181
2181
  console.log(` POST /verify → execution bus: verify`);
1182
2182
  console.log(` POST /fix → execution bus: fix + reverify`);
1183
2183
  console.log(` POST /fix/apply-safe → execution bus: apply-safe + reverify`);
2184
+ console.log(` POST /patch/preview → deterministic patch preview (before/after diff)`);
2185
+ console.log(` POST /patch/rollback → deterministic rollback apply by receipt`);
1184
2186
  console.log(` POST /patch → execution bus: patch + reverify`);
1185
2187
  console.log(` POST /execute → unified execution endpoint`);
1186
2188
  console.log(` GET /executions → execution history`);
@@ -1189,6 +2191,7 @@ function startDaemon() {
1189
2191
  console.log(` GET /executions/:id/diff → verification + patch inspection`);
1190
2192
  console.log(` GET /events → runtime event history`);
1191
2193
  console.log(` GET /events/stream → SSE deterministic governance runtime`);
2194
+ console.log(` GET /ops/summary → daemon operational health + reliability metrics`);
1192
2195
  console.log(` GET /control-plane → governance control-plane state + snapshots`);
1193
2196
  console.log(` POST /control-plane/preview → deterministic config impact preview`);
1194
2197
  console.log(` PUT /control-plane → apply deterministic governance config update`);
@@ -1205,7 +2208,7 @@ function startDaemon() {
1205
2208
  console.log(` GET /replay/execution/:id → deterministic execution replay`);
1206
2209
  console.log(` GET /replay/workspace/:id → deterministic workspace replay`);
1207
2210
  console.log(` GET /replay/timeline → deterministic governance timeline replay`);
1208
- console.log(`\n CWD: ${process.cwd()}`);
2211
+ console.log(`\n CWD: ${cwd}`);
1209
2212
  console.log(` Press Ctrl+C to stop.\n`);
1210
2213
  });
1211
2214
  const stop = () => {