@neurcode-ai/cli 0.9.49 → 0.9.59
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/dist/commands/fix.d.ts +1 -0
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js +710 -29
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +16 -1
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/patch-apply.d.ts +7 -0
- package/dist/commands/patch-apply.d.ts.map +1 -0
- package/dist/commands/patch-apply.js +85 -0
- package/dist/commands/patch-apply.js.map +1 -0
- package/dist/commands/start-intent.d.ts.map +1 -1
- package/dist/commands/start-intent.js +17 -2
- package/dist/commands/start-intent.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +372 -71
- package/dist/commands/verify.js.map +1 -1
- package/dist/context-engine/graph.d.ts +6 -0
- package/dist/context-engine/graph.d.ts.map +1 -0
- package/dist/context-engine/graph.js +55 -0
- package/dist/context-engine/graph.js.map +1 -0
- package/dist/context-engine/index.d.ts +14 -0
- package/dist/context-engine/index.d.ts.map +1 -0
- package/dist/context-engine/index.js +26 -0
- package/dist/context-engine/index.js.map +1 -0
- package/dist/context-engine/scanner.d.ts +6 -0
- package/dist/context-engine/scanner.d.ts.map +1 -0
- package/dist/context-engine/scanner.js +62 -0
- package/dist/context-engine/scanner.js.map +1 -0
- package/dist/context-engine/scorer.d.ts +9 -0
- package/dist/context-engine/scorer.d.ts.map +1 -0
- package/dist/context-engine/scorer.js +112 -0
- package/dist/context-engine/scorer.js.map +1 -0
- package/dist/context-engine/suggestions.d.ts +12 -0
- package/dist/context-engine/suggestions.d.ts.map +1 -0
- package/dist/context-engine/suggestions.js +22 -0
- package/dist/context-engine/suggestions.js.map +1 -0
- package/dist/daemon/server.d.ts +23 -0
- package/dist/daemon/server.d.ts.map +1 -0
- package/dist/daemon/server.js +222 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -1
- package/dist/intent-engine/coverage.d.ts +69 -0
- package/dist/intent-engine/coverage.d.ts.map +1 -0
- package/dist/intent-engine/coverage.js +140 -0
- package/dist/intent-engine/coverage.js.map +1 -0
- package/dist/intent-engine/flow-rules.d.ts +21 -0
- package/dist/intent-engine/flow-rules.d.ts.map +1 -0
- package/dist/intent-engine/flow-rules.js +83 -0
- package/dist/intent-engine/flow-rules.js.map +1 -0
- package/dist/intent-engine/flow-validator.d.ts +29 -0
- package/dist/intent-engine/flow-validator.d.ts.map +1 -0
- package/dist/intent-engine/flow-validator.js +202 -0
- package/dist/intent-engine/flow-validator.js.map +1 -0
- package/dist/intent-engine/graph.d.ts +33 -0
- package/dist/intent-engine/graph.d.ts.map +1 -0
- package/dist/intent-engine/graph.js +67 -0
- package/dist/intent-engine/graph.js.map +1 -0
- package/dist/intent-engine/index.d.ts +35 -0
- package/dist/intent-engine/index.d.ts.map +1 -0
- package/dist/intent-engine/index.js +94 -0
- package/dist/intent-engine/index.js.map +1 -0
- package/dist/intent-engine/indexer.d.ts +18 -0
- package/dist/intent-engine/indexer.d.ts.map +1 -0
- package/dist/intent-engine/indexer.js +100 -0
- package/dist/intent-engine/indexer.js.map +1 -0
- package/dist/intent-engine/matcher.d.ts +35 -0
- package/dist/intent-engine/matcher.d.ts.map +1 -0
- package/dist/intent-engine/matcher.js +522 -0
- package/dist/intent-engine/matcher.js.map +1 -0
- package/dist/intent-engine/parser.d.ts +12 -0
- package/dist/intent-engine/parser.d.ts.map +1 -0
- package/dist/intent-engine/parser.js +93 -0
- package/dist/intent-engine/parser.js.map +1 -0
- package/dist/intent-engine/regression.d.ts +32 -0
- package/dist/intent-engine/regression.d.ts.map +1 -0
- package/dist/intent-engine/regression.js +166 -0
- package/dist/intent-engine/regression.js.map +1 -0
- package/dist/intent-engine/requirements.d.ts +22 -0
- package/dist/intent-engine/requirements.d.ts.map +1 -0
- package/dist/intent-engine/requirements.js +147 -0
- package/dist/intent-engine/requirements.js.map +1 -0
- package/dist/intent-engine/state.d.ts +44 -0
- package/dist/intent-engine/state.d.ts.map +1 -0
- package/dist/intent-engine/state.js +83 -0
- package/dist/intent-engine/state.js.map +1 -0
- package/dist/patch-engine/diff.d.ts +12 -0
- package/dist/patch-engine/diff.d.ts.map +1 -0
- package/dist/patch-engine/diff.js +74 -0
- package/dist/patch-engine/diff.js.map +1 -0
- package/dist/patch-engine/generator.d.ts +13 -0
- package/dist/patch-engine/generator.d.ts.map +1 -0
- package/dist/patch-engine/generator.js +51 -0
- package/dist/patch-engine/generator.js.map +1 -0
- package/dist/patch-engine/index.d.ts +47 -0
- package/dist/patch-engine/index.d.ts.map +1 -0
- package/dist/patch-engine/index.js +182 -0
- package/dist/patch-engine/index.js.map +1 -0
- package/dist/patch-engine/patterns.d.ts +4 -0
- package/dist/patch-engine/patterns.d.ts.map +1 -0
- package/dist/patch-engine/patterns.js +99 -0
- package/dist/patch-engine/patterns.js.map +1 -0
- package/dist/utils/ai-debt-budget.d.ts +3 -2
- package/dist/utils/ai-debt-budget.d.ts.map +1 -1
- package/dist/utils/ai-debt-budget.js +83 -2
- package/dist/utils/ai-debt-budget.js.map +1 -1
- package/package.json +8 -7
- package/LICENSE +0 -201
package/dist/commands/fix.js
CHANGED
|
@@ -1,10 +1,370 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.fixCommand = fixCommand;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
4
6
|
const cli_json_1 = require("../utils/cli-json");
|
|
5
|
-
const
|
|
7
|
+
const context_engine_1 = require("../context-engine");
|
|
8
|
+
const patch_engine_1 = require("../patch-engine");
|
|
6
9
|
const chalk = (0, cli_json_1.loadChalk)();
|
|
7
10
|
const MAX_SUGGESTIONS = 10;
|
|
11
|
+
// Minimum context-engine score for a target file to be trusted.
|
|
12
|
+
const MIN_TARGET_SCORE = 3;
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Intent issue → FixSuggestion conversion
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const INTENT_ISSUE_ACTIONS = {
|
|
17
|
+
'intent:missing-input-validation': 'Add input validation using zod/joi/yup at the API boundary — validate req.body before processing',
|
|
18
|
+
'intent:missing-role-checks': 'Add role-based access checks — use middleware or inline checks (req.user.role === "admin") before sensitive operations',
|
|
19
|
+
'intent:missing-token-expiry': 'Set token expiry in jwt.sign() and implement refresh-token rotation — e.g., expiresIn: "15m"',
|
|
20
|
+
'intent:missing-api-validation': 'Add a validation schema for all route handlers that read req.body — use zod.parse() or equivalent',
|
|
21
|
+
'intent:missing-error-handling': 'Wrap async handlers in try/catch and return structured error responses — use next(error) for Express',
|
|
22
|
+
'intent:missing-payment-idempotency': 'Add idempotency key handling to payment operations — store and check keys before processing charges',
|
|
23
|
+
'intent:missing-webhook-verification': 'Verify webhook signatures before processing — use stripe.webhooks.constructEvent() or equivalent',
|
|
24
|
+
'intent:db-in-ui': 'Extract the database call into a service/repository module — UI components must not import DB clients directly',
|
|
25
|
+
'intent:auth-logic-in-ui': 'Move JWT signing/verification and bcrypt calls into the API/service layer — UI components must not perform cryptographic operations',
|
|
26
|
+
'intent:partial-auth-no-token': 'Complete the auth flow by adding token issuance — call jwt.sign() with user payload after successful credential validation',
|
|
27
|
+
};
|
|
28
|
+
function intentIssuesToFixSuggestions(intentIssues) {
|
|
29
|
+
return intentIssues.map((issue) => {
|
|
30
|
+
const suggestedAction = INTENT_ISSUE_ACTIONS[issue.rule]
|
|
31
|
+
?? `Resolve intent gap: ${issue.message}`;
|
|
32
|
+
const priority = issue.severity === 'high' ? 'CRITICAL' : 'WARNING';
|
|
33
|
+
const file = issue.files?.[0] ?? 'intent-analysis';
|
|
34
|
+
return {
|
|
35
|
+
file,
|
|
36
|
+
issue: issue.message,
|
|
37
|
+
policy: issue.rule,
|
|
38
|
+
suggestedAction,
|
|
39
|
+
confidence: 'medium',
|
|
40
|
+
source: 'warning',
|
|
41
|
+
priority,
|
|
42
|
+
...(issue.files && issue.files.length > 1 ? { reason: `Also affects: ${issue.files.slice(1).join(', ')}` } : {}),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Flow issue → FixSuggestion conversion (V5)
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
const FLOW_ISSUE_ACTIONS = {
|
|
50
|
+
'flow:token-not-validated-in-middleware': {
|
|
51
|
+
action: 'Add jwt.verify() call in an auth middleware to validate tokens on every protected request',
|
|
52
|
+
targetHint: 'middleware/auth.ts',
|
|
53
|
+
},
|
|
54
|
+
'flow:middleware-not-applied-to-routes': {
|
|
55
|
+
action: 'Wire authMiddleware into route definitions with router.use(authMiddleware) before protected handlers',
|
|
56
|
+
targetHint: 'routes/index.ts',
|
|
57
|
+
},
|
|
58
|
+
'flow:auth-in-ui-layer': {
|
|
59
|
+
action: 'Move JWT signing/verification and bcrypt calls into the API or service layer — UI components must not perform cryptographic operations',
|
|
60
|
+
targetHint: 'services/auth.service.ts',
|
|
61
|
+
},
|
|
62
|
+
'flow:validation-before-handler': {
|
|
63
|
+
action: 'Apply a validation middleware (e.g. validateBody(schema)) as the first argument before your route handler',
|
|
64
|
+
targetHint: 'middleware/validate.ts',
|
|
65
|
+
},
|
|
66
|
+
'flow:service-not-separated': {
|
|
67
|
+
action: 'Extract DB calls from the route handler into a dedicated service or repository module and import it',
|
|
68
|
+
targetHint: 'services/',
|
|
69
|
+
},
|
|
70
|
+
'flow:webhook-handler-without-verification': {
|
|
71
|
+
action: 'Verify webhook signature before processing — call stripe.webhooks.constructEvent() or use createHmac() on the raw body',
|
|
72
|
+
targetHint: 'routes/webhook.ts',
|
|
73
|
+
},
|
|
74
|
+
'flow:payment-no-idempotency-gate': {
|
|
75
|
+
action: 'Add idempotency key handling before executing the charge — check for existing transaction with the same key before calling stripe.paymentIntents.create()',
|
|
76
|
+
targetHint: 'services/payment.service.ts',
|
|
77
|
+
},
|
|
78
|
+
'flow:file-stored-without-type-check': {
|
|
79
|
+
action: 'Validate MIME type before storing — use fileFilter in multer config or check mimetype against an allowlist before writing to disk/S3',
|
|
80
|
+
targetHint: 'middleware/upload.ts',
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
function flowIssuesToFixSuggestions(flowIssues, diffFilePaths) {
|
|
84
|
+
return flowIssues.map((issue) => {
|
|
85
|
+
const guidance = FLOW_ISSUE_ACTIONS[issue.rule];
|
|
86
|
+
const priority = issue.severity === 'high' ? 'CRITICAL' : 'WARNING';
|
|
87
|
+
// Target: first known file from the issue, then fallback to diff paths or hint
|
|
88
|
+
const knownFile = issue.files?.[0];
|
|
89
|
+
const hintBasename = guidance?.targetHint ?? '';
|
|
90
|
+
const targetFile = knownFile ??
|
|
91
|
+
diffFilePaths.find((p) => hintBasename && p.includes(hintBasename.replace(/\//g, ''))) ??
|
|
92
|
+
diffFilePaths[0] ??
|
|
93
|
+
hintBasename;
|
|
94
|
+
const suggestedAction = guidance
|
|
95
|
+
? `${guidance.action}`
|
|
96
|
+
: `Resolve flow gap: ${issue.message}`;
|
|
97
|
+
return {
|
|
98
|
+
file: targetFile,
|
|
99
|
+
issue: issue.message,
|
|
100
|
+
policy: issue.rule,
|
|
101
|
+
suggestedAction,
|
|
102
|
+
confidence: 'medium',
|
|
103
|
+
source: 'warning',
|
|
104
|
+
priority,
|
|
105
|
+
...(issue.files && issue.files.length > 1
|
|
106
|
+
? { reason: `Also affects: ${issue.files.slice(1, 4).join(', ')}${issue.files.length > 4 ? ' ...' : ''}` }
|
|
107
|
+
: {}),
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Regression → FixSuggestion conversion (V6)
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
function regressionToFixSuggestions(regressions, diffFilePaths) {
|
|
115
|
+
return regressions.map((reg) => {
|
|
116
|
+
// Extract the component name from the rule, if present
|
|
117
|
+
// e.g. "regression:component:token-expiry" → "token-expiry"
|
|
118
|
+
const ruleSegments = reg.rule.split(':');
|
|
119
|
+
const componentKey = ruleSegments.length >= 3 ? ruleSegments.slice(2).join(':') : '';
|
|
120
|
+
const label = componentKey
|
|
121
|
+
? componentKey.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')
|
|
122
|
+
: '';
|
|
123
|
+
// Best-effort file target: prefer a file in the diff that matches the component
|
|
124
|
+
const targetFile = (componentKey && diffFilePaths.find((p) => p.includes(componentKey.replace(/-/g, '')))) ??
|
|
125
|
+
(componentKey && diffFilePaths.find((p) => new RegExp(componentKey.split('-')[0], 'i').test(p))) ??
|
|
126
|
+
diffFilePaths[0] ??
|
|
127
|
+
'regression-analysis';
|
|
128
|
+
const suggestedAction = reg.type === 'component-regression'
|
|
129
|
+
? `Restore ${label || componentKey} implementation — check recent commits for accidental removal`
|
|
130
|
+
: reg.type === 'critical-regression'
|
|
131
|
+
? `Immediately restore ${label || componentKey} — this is a security-critical component that must not be absent`
|
|
132
|
+
: reg.type === 'flow-regression'
|
|
133
|
+
? `Fix the newly introduced flow gap: ${reg.message.replace('New flow issue introduced: ', '')}`
|
|
134
|
+
: `Restore system coverage — identify and re-implement the components removed in recent changes`;
|
|
135
|
+
return {
|
|
136
|
+
file: targetFile,
|
|
137
|
+
issue: reg.message,
|
|
138
|
+
policy: reg.rule,
|
|
139
|
+
suggestedAction,
|
|
140
|
+
confidence: 'medium',
|
|
141
|
+
source: 'violation',
|
|
142
|
+
priority: 'CRITICAL',
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Coverage gap → FixSuggestion conversion
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Per-component guidance for missing components discovered via coverage analysis.
|
|
150
|
+
const COMPONENT_ACTIONS = {
|
|
151
|
+
'input-validation': {
|
|
152
|
+
action: 'Add input validation at the API boundary',
|
|
153
|
+
hint: 'Use zod.parse(), joi.validate(), or express-validator on req.body before processing',
|
|
154
|
+
},
|
|
155
|
+
'token-generation': {
|
|
156
|
+
action: 'Implement token generation',
|
|
157
|
+
hint: 'Call jwt.sign(payload, secret, { expiresIn: "15m" }) after successful authentication',
|
|
158
|
+
},
|
|
159
|
+
'token-expiry': {
|
|
160
|
+
action: 'Add token expiry and refresh-token logic',
|
|
161
|
+
hint: 'Set expiresIn in jwt.sign() and implement a /refresh endpoint that issues new access tokens',
|
|
162
|
+
},
|
|
163
|
+
'role-check': {
|
|
164
|
+
action: 'Implement role-based access checks',
|
|
165
|
+
hint: 'Add a checkRole() middleware or inline req.user.role guard before sensitive endpoints',
|
|
166
|
+
},
|
|
167
|
+
'middleware-protection': {
|
|
168
|
+
action: 'Add auth middleware to protected routes',
|
|
169
|
+
hint: 'Apply verifyToken middleware to all routes that require authentication',
|
|
170
|
+
},
|
|
171
|
+
'password-hashing': {
|
|
172
|
+
action: 'Hash passwords before storage',
|
|
173
|
+
hint: 'Use bcrypt.hash(password, 12) on registration and bcrypt.compare() on login',
|
|
174
|
+
},
|
|
175
|
+
'error-handling': {
|
|
176
|
+
action: 'Add consistent error handling to API handlers',
|
|
177
|
+
hint: 'Wrap async handlers in try/catch and call next(error) or return structured error responses',
|
|
178
|
+
},
|
|
179
|
+
'service-layer-separation': {
|
|
180
|
+
action: 'Extract business logic into a service layer',
|
|
181
|
+
hint: 'Create a *Service class or module and import it in route handlers — do not put DB queries in controllers',
|
|
182
|
+
},
|
|
183
|
+
'auth-middleware': {
|
|
184
|
+
action: 'Wire auth middleware to the router',
|
|
185
|
+
hint: 'Use router.use(authMiddleware) or pass the middleware as a route-level guard',
|
|
186
|
+
},
|
|
187
|
+
'response-schema': {
|
|
188
|
+
action: 'Define and enforce a response schema',
|
|
189
|
+
hint: 'Use a zod/joi schema or a response DTO class to ensure consistent API response shape',
|
|
190
|
+
},
|
|
191
|
+
'idempotency': {
|
|
192
|
+
action: 'Add idempotency key handling to payment operations',
|
|
193
|
+
hint: 'Accept an idempotency key header, store it, and reject duplicate requests',
|
|
194
|
+
},
|
|
195
|
+
'webhook-verification': {
|
|
196
|
+
action: 'Verify webhook signatures before processing',
|
|
197
|
+
hint: 'Use stripe.webhooks.constructEvent() or an HMAC check on the raw request body',
|
|
198
|
+
},
|
|
199
|
+
'secure-data-handling': {
|
|
200
|
+
action: 'Ensure sensitive payment data is not logged or stored in plain text',
|
|
201
|
+
hint: 'Mask PAN digits, never log card numbers, and use tokenisation where possible',
|
|
202
|
+
},
|
|
203
|
+
'transaction-handling': {
|
|
204
|
+
action: 'Wrap multi-step DB operations in transactions',
|
|
205
|
+
hint: 'Use prisma.$transaction() or BEGIN/COMMIT to keep multi-step writes atomic',
|
|
206
|
+
},
|
|
207
|
+
'migration-safety': {
|
|
208
|
+
action: 'Add migration rollback support',
|
|
209
|
+
hint: 'Ensure every migration has a corresponding down() function',
|
|
210
|
+
},
|
|
211
|
+
'connection-pooling': {
|
|
212
|
+
action: 'Configure a connection pool',
|
|
213
|
+
hint: 'Set pool.min / pool.max in your DB client config to avoid connection exhaustion',
|
|
214
|
+
},
|
|
215
|
+
'input-sanitization': {
|
|
216
|
+
action: 'Sanitize user input before processing',
|
|
217
|
+
hint: 'Use DOMPurify, sanitize-html, or a validation library to strip unsafe content',
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
function coverageGapsToFixSuggestions(summary, diffFilePaths) {
|
|
221
|
+
if (summary.missing.length === 0)
|
|
222
|
+
return [];
|
|
223
|
+
// Find the most relevant file for the domain (e.g. src/api/auth.ts for auth)
|
|
224
|
+
const domainRe = new RegExp(`\\b${summary.domain}\\b`, 'i');
|
|
225
|
+
const bestFile = diffFilePaths.find((p) => domainRe.test(p)) ??
|
|
226
|
+
diffFilePaths.find((p) => /\/(api|routes?|handler|controller)/i.test(p)) ??
|
|
227
|
+
diffFilePaths[0] ??
|
|
228
|
+
`src/${summary.domain}/index.ts`;
|
|
229
|
+
// V4: critical missing components come first, marked CRITICAL priority
|
|
230
|
+
const criticalMissingSet = new Set(summary.criticalMissing ?? []);
|
|
231
|
+
const criticalKeys = summary.missing.filter((k) => criticalMissingSet.has(k));
|
|
232
|
+
const otherKeys = summary.missing.filter((k) => !criticalMissingSet.has(k));
|
|
233
|
+
const orderedKeys = [...criticalKeys, ...otherKeys];
|
|
234
|
+
return orderedKeys.map((key) => {
|
|
235
|
+
const isCriticalKey = criticalMissingSet.has(key);
|
|
236
|
+
const guidance = COMPONENT_ACTIONS[key];
|
|
237
|
+
const label = key.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
238
|
+
const suggestedAction = guidance
|
|
239
|
+
? `${guidance.action} — ${guidance.hint}`
|
|
240
|
+
: `Implement ${label} for the ${summary.domain} domain`;
|
|
241
|
+
return {
|
|
242
|
+
file: bestFile,
|
|
243
|
+
issue: `${isCriticalKey ? '[CRITICAL] ' : ''}${label} not detected in ${summary.domain} implementation (coverage: ${summary.coveragePct}%)`,
|
|
244
|
+
policy: `intent:coverage:${key}`,
|
|
245
|
+
suggestedAction,
|
|
246
|
+
confidence: 'medium',
|
|
247
|
+
source: 'warning',
|
|
248
|
+
priority: (isCriticalKey ? 'CRITICAL' : 'WARNING'),
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// Reason templates (req 4) — policy-keyed, no runtime string assembly
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
const REASON_TEMPLATES = {
|
|
256
|
+
layering: 'UI layer should not directly access database',
|
|
257
|
+
validation: 'Input must be validated before request handling',
|
|
258
|
+
scope: 'Changes must stay within planned scope to prevent drift',
|
|
259
|
+
default: 'This change violates project architecture guidelines',
|
|
260
|
+
};
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Targeted suggestion helpers
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
function buildRepairIntent(issue, policy) {
|
|
265
|
+
const combined = `${issue} ${policy}`.toLowerCase();
|
|
266
|
+
// Violations with no useful cross-file redirect target
|
|
267
|
+
if (combined.includes('todo') || combined.includes('fixme'))
|
|
268
|
+
return null;
|
|
269
|
+
if (policy === 'scope_guard' || combined.includes('scope'))
|
|
270
|
+
return null;
|
|
271
|
+
if (policy === 'verify_runtime')
|
|
272
|
+
return null;
|
|
273
|
+
if (combined.includes('db') || combined.includes('database') ||
|
|
274
|
+
combined.includes('query') || combined.includes('sql') ||
|
|
275
|
+
combined.includes('data access') || combined.includes('direct access') ||
|
|
276
|
+
policy.includes('layer') || policy.includes('db') || policy.includes('layering')) {
|
|
277
|
+
return 'service layer database repository core';
|
|
278
|
+
}
|
|
279
|
+
if (combined.includes('validation') || combined.includes('validate') ||
|
|
280
|
+
combined.includes('input') || policy.includes('validation')) {
|
|
281
|
+
return 'validation schema middleware validator';
|
|
282
|
+
}
|
|
283
|
+
if (combined.includes('auth') || combined.includes('authentication') ||
|
|
284
|
+
combined.includes('token') || combined.includes('jwt') ||
|
|
285
|
+
policy.includes('auth')) {
|
|
286
|
+
return 'authentication middleware guard auth';
|
|
287
|
+
}
|
|
288
|
+
return issue;
|
|
289
|
+
}
|
|
290
|
+
function getModulePrefix(filePath) {
|
|
291
|
+
const parts = filePath.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
292
|
+
return parts.slice(0, 2).join('/');
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Find the best target file for a repair intent.
|
|
296
|
+
*
|
|
297
|
+
* Applies:
|
|
298
|
+
* - Same-module boost: +2 to candidates sharing a module prefix with any
|
|
299
|
+
* previously selected target (drives coherence across violations).
|
|
300
|
+
* - Safety threshold: only returns a result when adjusted score >= MIN_TARGET_SCORE.
|
|
301
|
+
*/
|
|
302
|
+
function findTargetFile(sourceFile, repairIntent, graph, selectedTargets) {
|
|
303
|
+
const scored = (0, context_engine_1.scoreFiles)(repairIntent, graph);
|
|
304
|
+
const selectedPrefixes = new Set(selectedTargets.map(getModulePrefix));
|
|
305
|
+
// Exclude source file; apply same-module boost
|
|
306
|
+
const adjusted = scored
|
|
307
|
+
.filter((s) => s.file !== sourceFile)
|
|
308
|
+
.map((s) => ({
|
|
309
|
+
file: s.file,
|
|
310
|
+
score: s.score + (selectedPrefixes.has(getModulePrefix(s.file)) ? 2 : 0),
|
|
311
|
+
}));
|
|
312
|
+
adjusted.sort((a, b) => b.score - a.score);
|
|
313
|
+
const best = adjusted.find((s) => s.score >= MIN_TARGET_SCORE);
|
|
314
|
+
if (!best)
|
|
315
|
+
return undefined;
|
|
316
|
+
return { file: best.file, score: best.score };
|
|
317
|
+
}
|
|
318
|
+
function buildReason(issue, policy) {
|
|
319
|
+
const policyLower = policy.toLowerCase();
|
|
320
|
+
const combined = `${issue} ${policy}`.toLowerCase();
|
|
321
|
+
// Policy-name takes precedence
|
|
322
|
+
if (policyLower.includes('layer') || policyLower.includes('db') ||
|
|
323
|
+
policyLower.includes('database') || policyLower.includes('layering')) {
|
|
324
|
+
return REASON_TEMPLATES.layering;
|
|
325
|
+
}
|
|
326
|
+
if (policyLower.includes('validation') || policyLower.includes('validate') ||
|
|
327
|
+
policyLower.includes('input_validation')) {
|
|
328
|
+
return REASON_TEMPLATES.validation;
|
|
329
|
+
}
|
|
330
|
+
if (policyLower === 'scope_guard' || policyLower.includes('scope')) {
|
|
331
|
+
return REASON_TEMPLATES.scope;
|
|
332
|
+
}
|
|
333
|
+
// Issue-text fallback
|
|
334
|
+
if (combined.includes('db') || combined.includes('database') || combined.includes('query') || combined.includes('data access')) {
|
|
335
|
+
return REASON_TEMPLATES.layering;
|
|
336
|
+
}
|
|
337
|
+
if (combined.includes('validation') || combined.includes('validate') || combined.includes('input')) {
|
|
338
|
+
return REASON_TEMPLATES.validation;
|
|
339
|
+
}
|
|
340
|
+
return REASON_TEMPLATES.default;
|
|
341
|
+
}
|
|
342
|
+
function resolveConfidence(score) {
|
|
343
|
+
if (score >= 6)
|
|
344
|
+
return 'high';
|
|
345
|
+
if (score >= MIN_TARGET_SCORE)
|
|
346
|
+
return 'medium';
|
|
347
|
+
return 'low';
|
|
348
|
+
}
|
|
349
|
+
function buildActionVerb(issue, policy) {
|
|
350
|
+
const combined = `${issue} ${policy}`.toLowerCase();
|
|
351
|
+
if (combined.includes('db') || combined.includes('database') || combined.includes('query') || combined.includes('sql')) {
|
|
352
|
+
return 'Move DB query';
|
|
353
|
+
}
|
|
354
|
+
if (combined.includes('data access') || combined.includes('direct access') || policy.includes('layer') || policy.includes('layering')) {
|
|
355
|
+
return 'Move data access logic';
|
|
356
|
+
}
|
|
357
|
+
if (combined.includes('validation') || combined.includes('validate')) {
|
|
358
|
+
return 'Add input validation';
|
|
359
|
+
}
|
|
360
|
+
if (combined.includes('auth') || combined.includes('authentication')) {
|
|
361
|
+
return 'Move authentication logic';
|
|
362
|
+
}
|
|
363
|
+
return 'Move logic';
|
|
364
|
+
}
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// Legacy generic action (used as fallback suggestedAction text)
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
8
368
|
function suggestAction(file, issue, policy, isScopeIssue) {
|
|
9
369
|
const combined = `${issue} ${policy}`.toLowerCase();
|
|
10
370
|
const withContextHint = (message) => `${message} (from current diff)`;
|
|
@@ -92,16 +452,80 @@ function dedupeSuggestions(suggestions) {
|
|
|
92
452
|
}
|
|
93
453
|
return out;
|
|
94
454
|
}
|
|
95
|
-
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
// Core suggestion builder
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
function resolveTargetForViolation(file, message, policy, graph, selectedTargets,
|
|
459
|
+
// repairIntent → cached TargetResult for multi-violation coherence
|
|
460
|
+
targetHistory) {
|
|
461
|
+
const repairIntent = buildRepairIntent(message, policy);
|
|
462
|
+
if (!repairIntent) {
|
|
463
|
+
// No cross-file redirect needed (TODO, scope, etc.) — action is clear.
|
|
464
|
+
return { targetFile: undefined, reason: undefined, confidence: 'high', noStrongTarget: false };
|
|
465
|
+
}
|
|
466
|
+
const cached = targetHistory.get(repairIntent);
|
|
467
|
+
let result;
|
|
468
|
+
if (cached) {
|
|
469
|
+
// Fresh candidate for this specific violation's source file
|
|
470
|
+
const fresh = findTargetFile(file, repairIntent, graph, selectedTargets);
|
|
471
|
+
// Within-1-point tolerance: prefer the cached target for consistency (req 7)
|
|
472
|
+
if (fresh && Math.abs(fresh.score - cached.score) <= 1) {
|
|
473
|
+
result = cached;
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
result = fresh ?? cached;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
result = findTargetFile(file, repairIntent, graph, selectedTargets);
|
|
481
|
+
if (result) {
|
|
482
|
+
targetHistory.set(repairIntent, result);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (!result) {
|
|
486
|
+
return {
|
|
487
|
+
targetFile: undefined,
|
|
488
|
+
reason: undefined,
|
|
489
|
+
confidence: 'low',
|
|
490
|
+
noStrongTarget: true,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
selectedTargets.push(result.file);
|
|
494
|
+
return {
|
|
495
|
+
targetFile: result.file,
|
|
496
|
+
reason: buildReason(message, policy),
|
|
497
|
+
confidence: resolveConfidence(result.score),
|
|
498
|
+
noStrongTarget: false,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function buildSuggestions(verifyOutput, graph) {
|
|
96
502
|
const suggestions = [];
|
|
503
|
+
// Shared state for same-module boost and coherence across all violations
|
|
504
|
+
const selectedTargets = [];
|
|
505
|
+
const targetHistory = new Map();
|
|
97
506
|
for (const violation of verifyOutput.violations) {
|
|
98
507
|
if (suggestions.length >= MAX_SUGGESTIONS)
|
|
99
508
|
break;
|
|
509
|
+
let targetFile;
|
|
510
|
+
let reason;
|
|
511
|
+
let confidence = 'high';
|
|
512
|
+
let noStrongTarget;
|
|
513
|
+
if (graph) {
|
|
514
|
+
const resolved = resolveTargetForViolation(violation.file, violation.message, violation.policy, graph, selectedTargets, targetHistory);
|
|
515
|
+
targetFile = resolved.targetFile;
|
|
516
|
+
reason = resolved.reason;
|
|
517
|
+
confidence = resolved.confidence;
|
|
518
|
+
noStrongTarget = resolved.noStrongTarget || undefined;
|
|
519
|
+
}
|
|
100
520
|
suggestions.push({
|
|
101
521
|
file: violation.file,
|
|
102
522
|
issue: violation.message,
|
|
103
523
|
policy: violation.policy,
|
|
104
524
|
suggestedAction: suggestAction(violation.file, violation.message, violation.policy, false),
|
|
525
|
+
targetFile,
|
|
526
|
+
reason,
|
|
527
|
+
confidence,
|
|
528
|
+
noStrongTarget,
|
|
105
529
|
source: 'violation',
|
|
106
530
|
priority: resolveViolationPriority(violation.severity),
|
|
107
531
|
});
|
|
@@ -109,11 +533,26 @@ function buildSuggestions(verifyOutput) {
|
|
|
109
533
|
for (const warning of verifyOutput.warnings) {
|
|
110
534
|
if (suggestions.length >= MAX_SUGGESTIONS)
|
|
111
535
|
break;
|
|
536
|
+
let targetFile;
|
|
537
|
+
let reason;
|
|
538
|
+
let confidence = 'high';
|
|
539
|
+
let noStrongTarget;
|
|
540
|
+
if (graph) {
|
|
541
|
+
const resolved = resolveTargetForViolation(warning.file, warning.message, warning.policy, graph, selectedTargets, targetHistory);
|
|
542
|
+
targetFile = resolved.targetFile;
|
|
543
|
+
reason = resolved.reason;
|
|
544
|
+
confidence = resolved.confidence;
|
|
545
|
+
noStrongTarget = resolved.noStrongTarget || undefined;
|
|
546
|
+
}
|
|
112
547
|
suggestions.push({
|
|
113
548
|
file: warning.file,
|
|
114
549
|
issue: warning.message,
|
|
115
550
|
policy: warning.policy,
|
|
116
551
|
suggestedAction: suggestAction(warning.file, warning.message, warning.policy, false),
|
|
552
|
+
targetFile,
|
|
553
|
+
reason,
|
|
554
|
+
confidence,
|
|
555
|
+
noStrongTarget,
|
|
117
556
|
source: 'warning',
|
|
118
557
|
priority: 'WARNING',
|
|
119
558
|
});
|
|
@@ -127,6 +566,7 @@ function buildSuggestions(verifyOutput) {
|
|
|
127
566
|
issue: message,
|
|
128
567
|
policy: 'scope_guard',
|
|
129
568
|
suggestedAction: suggestAction(scopeIssue.file, message, 'scope_guard', true),
|
|
569
|
+
confidence: 'high',
|
|
130
570
|
source: 'scope',
|
|
131
571
|
priority: 'SCOPE',
|
|
132
572
|
});
|
|
@@ -145,12 +585,81 @@ function appendExpediteSuggestions(suggestions, expediteItems) {
|
|
|
145
585
|
issue: `[EXPEDITE] ${item.message}`,
|
|
146
586
|
policy: item.policy,
|
|
147
587
|
suggestedAction: suggestExpediteAction(item.file, item.message, item.policy),
|
|
588
|
+
confidence: 'medium',
|
|
148
589
|
source: 'expedite',
|
|
149
590
|
priority: 'WARNING',
|
|
150
591
|
});
|
|
151
592
|
}
|
|
152
593
|
return dedupeSuggestions([...expediteSuggestions, ...suggestions]).slice(0, MAX_SUGGESTIONS);
|
|
153
594
|
}
|
|
595
|
+
// ---------------------------------------------------------------------------
|
|
596
|
+
// Rendering helpers
|
|
597
|
+
// ---------------------------------------------------------------------------
|
|
598
|
+
function capitalize(s) {
|
|
599
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
600
|
+
}
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
// Patch enrichment
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
function enrichSuggestionsWithPatches(suggestions, fileContents) {
|
|
605
|
+
for (const suggestion of suggestions) {
|
|
606
|
+
if (suggestion.file === 'unknown')
|
|
607
|
+
continue;
|
|
608
|
+
const content = fileContents[suggestion.file];
|
|
609
|
+
if (!content)
|
|
610
|
+
continue;
|
|
611
|
+
const patch = (0, patch_engine_1.generatePatchForSuggestion)(suggestion, content);
|
|
612
|
+
if (patch) {
|
|
613
|
+
suggestion.patch = { file: patch.file, diff: patch.diff };
|
|
614
|
+
suggestion.patchConfidence = patch.patchConfidence;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// ---------------------------------------------------------------------------
|
|
619
|
+
// Rendering helpers
|
|
620
|
+
// ---------------------------------------------------------------------------
|
|
621
|
+
function printTargetedAction(item) {
|
|
622
|
+
const verb = buildActionVerb(item.issue, item.policy);
|
|
623
|
+
console.log(` → ${verb} from:`);
|
|
624
|
+
console.log(` ${item.file}`);
|
|
625
|
+
console.log(` To:`);
|
|
626
|
+
console.log(` ${item.targetFile}`);
|
|
627
|
+
console.log(` Reason:`);
|
|
628
|
+
console.log(` ${item.reason}`);
|
|
629
|
+
console.log(` Confidence:`);
|
|
630
|
+
console.log(` ${capitalize(item.confidence)}`);
|
|
631
|
+
}
|
|
632
|
+
function printFallbackAction(item) {
|
|
633
|
+
console.log(' → No strong target file found.');
|
|
634
|
+
console.log(' Suggested action:');
|
|
635
|
+
console.log(` ${item.suggestedAction}`);
|
|
636
|
+
console.log(' Confidence:');
|
|
637
|
+
console.log(' Low');
|
|
638
|
+
}
|
|
639
|
+
function printPatch(patch, confidence) {
|
|
640
|
+
console.log(' Suggested patch (apply manually):');
|
|
641
|
+
for (const line of patch.diff.split('\n')) {
|
|
642
|
+
if (line.startsWith('---') || line.startsWith('+++')) {
|
|
643
|
+
console.log(chalk.dim(` ${line}`));
|
|
644
|
+
}
|
|
645
|
+
else if (line.startsWith('@@')) {
|
|
646
|
+
console.log(chalk.cyan(` ${line}`));
|
|
647
|
+
}
|
|
648
|
+
else if (line.startsWith('-')) {
|
|
649
|
+
console.log(chalk.red(` ${line}`));
|
|
650
|
+
}
|
|
651
|
+
else if (line.startsWith('+')) {
|
|
652
|
+
console.log(chalk.green(` ${line}`));
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
console.log(chalk.dim(` ${line}`));
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (confidence) {
|
|
659
|
+
console.log(chalk.dim(` Patch Confidence: ${capitalize(confidence)}`));
|
|
660
|
+
}
|
|
661
|
+
console.log(chalk.dim(` Run: neurcode patch --file ${patch.file}`));
|
|
662
|
+
}
|
|
154
663
|
function printFixPlan(suggestions, context) {
|
|
155
664
|
console.log(chalk.bold('\nNeurcode Fix Plan (Prioritized)'));
|
|
156
665
|
console.log(chalk.dim('Based on latest Neurcode verify results\n'));
|
|
@@ -213,7 +722,18 @@ function printFixPlan(suggestions, context) {
|
|
|
213
722
|
console.log(`${colorPriority(filePriority)} ${chalk.cyan(file)} (${sortedItems.length} ${issueLabel})`);
|
|
214
723
|
for (const item of sortedItems) {
|
|
215
724
|
console.log(`* [${item.priority}] ${item.issue} (policy: ${item.policy})`);
|
|
216
|
-
|
|
725
|
+
if (item.targetFile && item.reason) {
|
|
726
|
+
printTargetedAction(item);
|
|
727
|
+
}
|
|
728
|
+
else if (item.noStrongTarget) {
|
|
729
|
+
printFallbackAction(item);
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
console.log(` → ${item.suggestedAction}`);
|
|
733
|
+
}
|
|
734
|
+
if (item.patch?.diff) {
|
|
735
|
+
printPatch(item.patch, item.patchConfidence);
|
|
736
|
+
}
|
|
217
737
|
console.log('');
|
|
218
738
|
}
|
|
219
739
|
const showNextHint = index < 2 && index + 1 < grouped.length;
|
|
@@ -227,6 +747,112 @@ function printFixPlan(suggestions, context) {
|
|
|
227
747
|
function emitFixJson(payload) {
|
|
228
748
|
(0, cli_json_1.emitJson)(payload);
|
|
229
749
|
}
|
|
750
|
+
// ---------------------------------------------------------------------------
|
|
751
|
+
// Auto-fix
|
|
752
|
+
// ---------------------------------------------------------------------------
|
|
753
|
+
function isEligibleForAutoFix(suggestion) {
|
|
754
|
+
// Patch must exist and have already passed the safety gate (isPatchSafe is
|
|
755
|
+
// enforced during enrichSuggestionsWithPatches — no need to re-run it here).
|
|
756
|
+
return !!(suggestion.patch?.diff) && suggestion.patchConfidence === 'high';
|
|
757
|
+
}
|
|
758
|
+
async function runAutoFix(suggestions, verifyArgs, json) {
|
|
759
|
+
// Only talk about suggestions that have patches; skip everything else silently.
|
|
760
|
+
const patchable = suggestions.filter((s) => !!s.patch?.diff);
|
|
761
|
+
const results = [];
|
|
762
|
+
for (const suggestion of patchable) {
|
|
763
|
+
if (!isEligibleForAutoFix(suggestion)) {
|
|
764
|
+
results.push({
|
|
765
|
+
file: suggestion.file,
|
|
766
|
+
status: 'skipped',
|
|
767
|
+
reason: suggestion.patchConfidence ? `${suggestion.patchConfidence} confidence` : 'no patch',
|
|
768
|
+
});
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const filePath = (0, path_1.resolve)(process.cwd(), suggestion.file);
|
|
772
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
773
|
+
results.push({ file: suggestion.file, status: 'skipped', reason: 'file not found' });
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
let content;
|
|
777
|
+
try {
|
|
778
|
+
// Re-read from disk so sequential patches in the same run see prior writes.
|
|
779
|
+
content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
results.push({ file: suggestion.file, status: 'skipped', reason: 'could not read file' });
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
// Apply the EXACT diff that was shown to the user — no re-generation.
|
|
786
|
+
const updatedContent = (0, patch_engine_1.applyUnifiedDiff)(content, suggestion.patch.diff);
|
|
787
|
+
if (updatedContent === null) {
|
|
788
|
+
results.push({ file: suggestion.file, status: 'skipped', reason: 'patch mismatch' });
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
try {
|
|
792
|
+
(0, fs_1.writeFileSync)(filePath, updatedContent, 'utf-8');
|
|
793
|
+
results.push({ file: suggestion.file, status: 'applied' });
|
|
794
|
+
}
|
|
795
|
+
catch (err) {
|
|
796
|
+
const msg = err instanceof Error ? err.message : 'write failed';
|
|
797
|
+
results.push({ file: suggestion.file, status: 'skipped', reason: msg });
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const applied = results.filter((r) => r.status === 'applied');
|
|
801
|
+
const skipped = results.filter((r) => r.status === 'skipped');
|
|
802
|
+
// Re-run verify only when something was actually changed.
|
|
803
|
+
let verifyAfter;
|
|
804
|
+
if (applied.length > 0) {
|
|
805
|
+
const reRun = await (0, cli_json_1.runCliJson)(verifyArgs, { cwd: process.cwd() });
|
|
806
|
+
if (reRun.payload) {
|
|
807
|
+
try {
|
|
808
|
+
const reOutput = reRun.payload;
|
|
809
|
+
verifyAfter = {
|
|
810
|
+
exitCode: reRun.exitCode,
|
|
811
|
+
verdict: reOutput.verdict,
|
|
812
|
+
violations: Array.isArray(reOutput.violations) ? reOutput.violations.length : 0,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
verifyAfter = { exitCode: reRun.exitCode, verdict: null, violations: -1 };
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (json) {
|
|
821
|
+
console.log(JSON.stringify({
|
|
822
|
+
success: true,
|
|
823
|
+
applied: applied.length,
|
|
824
|
+
skipped: skipped.length,
|
|
825
|
+
files: results,
|
|
826
|
+
verifyAfter,
|
|
827
|
+
timestamp: new Date().toISOString(),
|
|
828
|
+
}, null, 2));
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
// Human output
|
|
832
|
+
if (applied.length === 0 && skipped.length === 0) {
|
|
833
|
+
console.log(chalk.dim('\nNo patchable suggestions found for --apply-safe.'));
|
|
834
|
+
console.log(chalk.dim('Run `neurcode fix` to see all issues.\n'));
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (applied.length > 0) {
|
|
838
|
+
console.log(chalk.bold('\nAuto-fix applied:'));
|
|
839
|
+
for (const r of applied) {
|
|
840
|
+
console.log(chalk.green(` ✔ Applied patch to ${r.file}`));
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (skipped.length > 0) {
|
|
844
|
+
console.log(chalk.bold('\nSkipped:'));
|
|
845
|
+
for (const r of skipped) {
|
|
846
|
+
console.log(chalk.dim(` ✖ Skipped ${r.file}${r.reason ? ` (${r.reason})` : ''}`));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (verifyAfter) {
|
|
850
|
+
const icon = verifyAfter.exitCode === 0 ? chalk.green('✔') : chalk.yellow('⚠');
|
|
851
|
+
const label = verifyAfter.verdict ?? (verifyAfter.exitCode === 0 ? 'PASS' : 'FAIL');
|
|
852
|
+
console.log(`\n${icon} Verify after auto-fix: ${chalk.bold(label)} — ${verifyAfter.violations} violation(s) remaining`);
|
|
853
|
+
}
|
|
854
|
+
console.log('');
|
|
855
|
+
}
|
|
230
856
|
function buildVerifyArgs(options) {
|
|
231
857
|
const args = ['verify'];
|
|
232
858
|
if (options.planId) {
|
|
@@ -274,36 +900,81 @@ async function fixCommand(options) {
|
|
|
274
900
|
}
|
|
275
901
|
process.exit(1);
|
|
276
902
|
}
|
|
277
|
-
|
|
903
|
+
const verifyOutput = payload;
|
|
904
|
+
// Scan project once; scoring and patch generation reuse this data.
|
|
905
|
+
let graph = null;
|
|
906
|
+
let fileContents = {};
|
|
278
907
|
try {
|
|
279
|
-
|
|
908
|
+
const scan = (0, context_engine_1.scanProject)(process.cwd());
|
|
909
|
+
graph = (0, context_engine_1.buildDependencyGraph)(scan);
|
|
910
|
+
fileContents = scan.fileContents;
|
|
280
911
|
}
|
|
281
|
-
catch
|
|
282
|
-
|
|
283
|
-
if (options.json) {
|
|
284
|
-
emitFixJson({
|
|
285
|
-
success: false,
|
|
286
|
-
message: `Verify output does not match contract: ${message}`,
|
|
287
|
-
timestamp: new Date().toISOString(),
|
|
288
|
-
verifyExitCode: verifyRun.exitCode,
|
|
289
|
-
verdict: null,
|
|
290
|
-
violations: 0,
|
|
291
|
-
scopeIssues: 0,
|
|
292
|
-
suggestions: [],
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
console.log(chalk.bold('\nNeurcode Fix Plan (Prioritized)'));
|
|
297
|
-
console.log(chalk.dim('Based on latest Neurcode verify results\n'));
|
|
298
|
-
console.log(chalk.red(`Verify output does not match contract: ${message}\n`));
|
|
299
|
-
}
|
|
300
|
-
process.exit(1);
|
|
301
|
-
return;
|
|
912
|
+
catch {
|
|
913
|
+
// Non-fatal: fall back to generic suggestions without targeting or patches
|
|
302
914
|
}
|
|
303
915
|
const expediteModeUsed = payload.expediteMode === true || payload.expediteModeUsed === true;
|
|
304
916
|
const expediteItems = extractExpediteItems(payload);
|
|
305
|
-
let suggestions = buildSuggestions(verifyOutput);
|
|
917
|
+
let suggestions = buildSuggestions(verifyOutput, graph);
|
|
306
918
|
suggestions = appendExpediteSuggestions(suggestions, expediteItems);
|
|
919
|
+
enrichSuggestionsWithPatches(suggestions, fileContents);
|
|
920
|
+
// ── Intent-aware suggestions ──────────────────────────────────────────
|
|
921
|
+
// 1. Convert intent issues (missing/misplaced/partial) to fix suggestions.
|
|
922
|
+
const rawIntentIssues = Array.isArray(payload.intentIssues)
|
|
923
|
+
? payload.intentIssues
|
|
924
|
+
: [];
|
|
925
|
+
if (rawIntentIssues.length > 0) {
|
|
926
|
+
const intentSuggestions = intentIssuesToFixSuggestions(rawIntentIssues);
|
|
927
|
+
suggestions = [...suggestions, ...intentSuggestions];
|
|
928
|
+
}
|
|
929
|
+
// 2. Coverage-gap suggestions — one suggestion per missing component.
|
|
930
|
+
// These are distinct from issue-based suggestions: they tell the developer
|
|
931
|
+
// exactly which implementation components are still absent.
|
|
932
|
+
const rawIntentSummary = payload.intentSummary;
|
|
933
|
+
if (rawIntentSummary && rawIntentSummary.missing.length > 0) {
|
|
934
|
+
const diffPaths = [
|
|
935
|
+
...(Array.isArray(verifyOutput.violations) ? verifyOutput.violations.map((v) => v.file).filter(Boolean) : []),
|
|
936
|
+
...(Array.isArray(verifyOutput.warnings) ? verifyOutput.warnings.map((w) => w.file).filter(Boolean) : []),
|
|
937
|
+
...(Array.isArray(verifyOutput.scopeIssues) ? verifyOutput.scopeIssues.map((s) => s.file).filter(Boolean) : []),
|
|
938
|
+
].filter((f) => f && f !== 'unknown' && !f.startsWith('.neurcode'));
|
|
939
|
+
const uniquePaths = [...new Set(diffPaths)];
|
|
940
|
+
const coverageSuggestions = coverageGapsToFixSuggestions(rawIntentSummary, uniquePaths);
|
|
941
|
+
// Only add coverage suggestions that aren't already covered by intent issue suggestions
|
|
942
|
+
const existingPolicies = new Set(suggestions.map((s) => s.policy));
|
|
943
|
+
const newCoverageSuggestions = coverageSuggestions.filter((s) => !existingPolicies.has(s.policy));
|
|
944
|
+
suggestions = [...suggestions, ...newCoverageSuggestions];
|
|
945
|
+
}
|
|
946
|
+
// 3. V6: Regression suggestions — always top-priority (structural degradation).
|
|
947
|
+
const rawRegressions = Array.isArray(payload.regressions)
|
|
948
|
+
? payload.regressions
|
|
949
|
+
: [];
|
|
950
|
+
if (rawRegressions.length > 0) {
|
|
951
|
+
const regressionDiffPaths = [
|
|
952
|
+
...(Array.isArray(verifyOutput.violations) ? verifyOutput.violations.map((v) => v.file).filter(Boolean) : []),
|
|
953
|
+
].filter((f) => f && f !== 'unknown' && !f.startsWith('.neurcode'));
|
|
954
|
+
const uniqueRegressionPaths = [...new Set(regressionDiffPaths)];
|
|
955
|
+
const regressionSuggestions = regressionToFixSuggestions(rawRegressions, uniqueRegressionPaths);
|
|
956
|
+
// Regressions are prepended before everything else
|
|
957
|
+
const existingRegressionPolicies = new Set(suggestions.map((s) => s.policy));
|
|
958
|
+
const newRegressionSuggestions = regressionSuggestions.filter((s) => !existingRegressionPolicies.has(s.policy));
|
|
959
|
+
suggestions = [...newRegressionSuggestions, ...suggestions];
|
|
960
|
+
}
|
|
961
|
+
// 4. V5: Flow issue suggestions — wiring and connectivity gaps.
|
|
962
|
+
const rawFlowIssues = Array.isArray(payload.flowIssues)
|
|
963
|
+
? payload.flowIssues
|
|
964
|
+
: [];
|
|
965
|
+
if (rawFlowIssues.length > 0) {
|
|
966
|
+
const flowDiffPaths = [
|
|
967
|
+
...(Array.isArray(verifyOutput.violations) ? verifyOutput.violations.map((v) => v.file).filter(Boolean) : []),
|
|
968
|
+
...(Array.isArray(verifyOutput.scopeIssues) ? verifyOutput.scopeIssues.map((s) => s.file).filter(Boolean) : []),
|
|
969
|
+
].filter((f) => f && f !== 'unknown' && !f.startsWith('.neurcode'));
|
|
970
|
+
const uniqueFlowPaths = [...new Set(flowDiffPaths)];
|
|
971
|
+
const flowSuggestions = flowIssuesToFixSuggestions(rawFlowIssues, uniqueFlowPaths);
|
|
972
|
+
// Deduplicate: don't re-add if a coverage/intent suggestion already covers the same rule
|
|
973
|
+
const existingFlowPolicies = new Set(suggestions.map((s) => s.policy));
|
|
974
|
+
const newFlowSuggestions = flowSuggestions.filter((s) => !existingFlowPolicies.has(s.policy));
|
|
975
|
+
// Flow suggestions go first (they are structural) before coverage gaps
|
|
976
|
+
suggestions = [...newFlowSuggestions, ...suggestions];
|
|
977
|
+
}
|
|
307
978
|
if (verifyRun.exitCode !== 0 && suggestions.length === 0) {
|
|
308
979
|
suggestions = [
|
|
309
980
|
{
|
|
@@ -311,6 +982,7 @@ async function fixCommand(options) {
|
|
|
311
982
|
issue: 'Verification failed but no actionable items were present in the verify payload',
|
|
312
983
|
policy: 'verify_runtime',
|
|
313
984
|
suggestedAction: 'Re-run `neurcode verify --json` and inspect the emitted payload',
|
|
985
|
+
confidence: 'low',
|
|
314
986
|
source: 'warning',
|
|
315
987
|
priority: 'WARNING',
|
|
316
988
|
},
|
|
@@ -319,14 +991,23 @@ async function fixCommand(options) {
|
|
|
319
991
|
if (verifyOutput.violations.length > 0 && suggestions.length === 0) {
|
|
320
992
|
console.warn('Invariant violation: verify has issues but fix produced none');
|
|
321
993
|
}
|
|
994
|
+
if (options.applySafe) {
|
|
995
|
+
await runAutoFix(suggestions, buildVerifyArgs(options), options.json ?? false);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
322
998
|
if (!options.json) {
|
|
999
|
+
const intentNote = rawIntentIssues.length > 0 ? `, ${rawIntentIssues.length} intent issues` : '';
|
|
1000
|
+
const flowNote = rawFlowIssues.length > 0 ? `, ${rawFlowIssues.length} flow issues` : '';
|
|
1001
|
+
const regressionNote = rawRegressions.length > 0 ? `, ${rawRegressions.length} regressions` : '';
|
|
323
1002
|
console.log(`Fix using verify payload: ${verifyOutput.violations.length} violations, ` +
|
|
324
|
-
`${verifyOutput.warnings.length} warnings, ${verifyOutput.scopeIssues.length} scope issues`);
|
|
1003
|
+
`${verifyOutput.warnings.length} warnings, ${verifyOutput.scopeIssues.length} scope issues${intentNote}${flowNote}${regressionNote}`);
|
|
325
1004
|
}
|
|
326
1005
|
const verifyFailed = verifyRun.exitCode !== 0;
|
|
1006
|
+
const flowSuffix = rawFlowIssues.length > 0 ? `, ${rawFlowIssues.length} flow issues` : '';
|
|
1007
|
+
const intentSuffix = rawIntentIssues.length > 0 ? `, ${rawIntentIssues.length} intent issues` : '';
|
|
327
1008
|
const verifyMessage = verifyFailed
|
|
328
1009
|
? `${verifyOutput.summary.totalViolations} violations, ${verifyOutput.summary.totalWarnings} warnings, ` +
|
|
329
|
-
`${verifyOutput.summary.totalScopeIssues} scope issues`
|
|
1010
|
+
`${verifyOutput.summary.totalScopeIssues} scope issues${intentSuffix}${flowSuffix}`
|
|
330
1011
|
: '';
|
|
331
1012
|
const verifyMessageWithMode = expediteModeUsed
|
|
332
1013
|
? `${verifyMessage}${verifyMessage ? ' | ' : ''}Expedite Mode used`
|