watchfix 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/fix.js +2 -2
- package/dist/cli/commands/show.js +7 -0
- package/dist/cli/commands/status.js +15 -1
- package/dist/cli/commands/stop.js +1 -1
- package/dist/cli/commands/watch.js +1 -1
- package/dist/config/schema.d.ts +5 -0
- package/dist/config/schema.js +1 -0
- package/dist/db/queries.d.ts +1 -1
- package/dist/fixer/context.d.ts +24 -1
- package/dist/fixer/context.js +117 -18
- package/dist/fixer/index.d.ts +23 -0
- package/dist/fixer/index.js +97 -7
- package/dist/fixer/output.d.ts +15 -1
- package/dist/fixer/output.js +43 -0
- package/dist/utils/errors.d.ts +1 -1
- package/dist/utils/process.d.ts +1 -1
- package/dist/utils/process.js +3 -2
- package/dist/watcher/index.js +31 -5
- package/package.json +1 -1
package/dist/cli/commands/fix.js
CHANGED
|
@@ -152,7 +152,7 @@ const formatStatusLine = (result, maxAttempts) => {
|
|
|
152
152
|
return 'Status: resolved (issue already fixed)';
|
|
153
153
|
}
|
|
154
154
|
const retryInfo = result.attempts < maxAttempts ? ', will retry' : ', max attempts reached';
|
|
155
|
-
return `Status: ${result.status} (attempt ${result.attempts} of ${maxAttempts}${retryInfo})`;
|
|
155
|
+
return `Status: ${result.status} (attempt ${result.attempts + 1} of ${maxAttempts}${retryInfo})`;
|
|
156
156
|
};
|
|
157
157
|
const formatFixOutcome = (result, verbosity, maxAttempts) => {
|
|
158
158
|
if (verbosity === 'quiet') {
|
|
@@ -211,7 +211,7 @@ const checkDaemonConflict = (db, logger) => {
|
|
|
211
211
|
if (!state) {
|
|
212
212
|
return true;
|
|
213
213
|
}
|
|
214
|
-
if (!isOurProcess(state.pid
|
|
214
|
+
if (!isOurProcess(state.pid)) {
|
|
215
215
|
clearWatcherState(db);
|
|
216
216
|
return true;
|
|
217
217
|
}
|
|
@@ -56,6 +56,9 @@ const formatAnalysis = (analysis) => {
|
|
|
56
56
|
return ['Analysis (raw):', ` ${analysis}`];
|
|
57
57
|
}
|
|
58
58
|
const lines = ['Analysis:'];
|
|
59
|
+
if (analysis.category) {
|
|
60
|
+
lines.push(` Category: ${analysis.category}`);
|
|
61
|
+
}
|
|
59
62
|
if (analysis.summary) {
|
|
60
63
|
lines.push(` Summary: ${analysis.summary}`);
|
|
61
64
|
}
|
|
@@ -73,6 +76,10 @@ const formatAnalysis = (analysis) => {
|
|
|
73
76
|
lines.push(` - ${file}`);
|
|
74
77
|
}
|
|
75
78
|
}
|
|
79
|
+
if (analysis.remediation_guidance) {
|
|
80
|
+
lines.push(' Remediation guidance:');
|
|
81
|
+
lines.push(...analysis.remediation_guidance.split('\n').map((line) => ` ${line}`));
|
|
82
|
+
}
|
|
76
83
|
if (analysis.confidence) {
|
|
77
84
|
lines.push(` Confidence: ${analysis.confidence}`);
|
|
78
85
|
}
|
|
@@ -13,6 +13,7 @@ const STATUS_ORDER = [
|
|
|
13
13
|
'fixed',
|
|
14
14
|
'failed',
|
|
15
15
|
'ignored',
|
|
16
|
+
'deferred',
|
|
16
17
|
];
|
|
17
18
|
const buildDatabasePath = (rootDir) => path.join(rootDir, '.watchfix', 'errors.db');
|
|
18
19
|
const getWatcherState = (db) => db.get('SELECT pid, started_at, autonomous, project_root, command_line FROM watcher_state WHERE id = 1');
|
|
@@ -62,6 +63,16 @@ const formatActionableErrors = (errors) => {
|
|
|
62
63
|
}
|
|
63
64
|
return lines;
|
|
64
65
|
};
|
|
66
|
+
const formatDeferredErrors = (errors) => {
|
|
67
|
+
if (errors.length === 0) {
|
|
68
|
+
return ['Deferred errors: none'];
|
|
69
|
+
}
|
|
70
|
+
const lines = ['Deferred errors (manual action required):'];
|
|
71
|
+
for (const error of errors) {
|
|
72
|
+
lines.push(` #${error.id} ${error.errorType} (${error.source}): ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
return lines;
|
|
75
|
+
};
|
|
65
76
|
export const statusCommand = async (options) => {
|
|
66
77
|
const config = loadConfig(options.config);
|
|
67
78
|
const dbPath = buildDatabasePath(config.project.root);
|
|
@@ -71,6 +82,7 @@ export const statusCommand = async (options) => {
|
|
|
71
82
|
'Errors:',
|
|
72
83
|
...STATUS_ORDER.map((status) => ` ${status}: 0`),
|
|
73
84
|
'Actionable errors: none',
|
|
85
|
+
'Deferred errors: none',
|
|
74
86
|
];
|
|
75
87
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
76
88
|
return;
|
|
@@ -81,7 +93,7 @@ export const statusCommand = async (options) => {
|
|
|
81
93
|
checkSchemaVersion(db);
|
|
82
94
|
const lines = [];
|
|
83
95
|
const state = getWatcherState(db);
|
|
84
|
-
if (state && isOurProcess(state.pid
|
|
96
|
+
if (state && isOurProcess(state.pid)) {
|
|
85
97
|
const mode = state.autonomous ? 'autonomous' : 'manual';
|
|
86
98
|
const uptime = formatUptime(state.started_at);
|
|
87
99
|
lines.push(`Watcher: running (pid ${state.pid}, ${mode} mode, uptime ${uptime}).`);
|
|
@@ -102,6 +114,8 @@ export const statusCommand = async (options) => {
|
|
|
102
114
|
}
|
|
103
115
|
const actionable = getErrorsByStatus(db, ['pending', 'suggested']);
|
|
104
116
|
lines.push(...formatActionableErrors(actionable));
|
|
117
|
+
const deferred = getErrorsByStatus(db, ['deferred']);
|
|
118
|
+
lines.push(...formatDeferredErrors(deferred));
|
|
105
119
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
106
120
|
}
|
|
107
121
|
finally {
|
|
@@ -58,7 +58,7 @@ export const stopCommand = async (options) => {
|
|
|
58
58
|
process.exitCode = EXIT_CODES.WATCHER_CONFLICT;
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
|
-
if (!isOurProcess(state.pid
|
|
61
|
+
if (!isOurProcess(state.pid)) {
|
|
62
62
|
process.stdout.write('Stale watcher state (process no longer exists).\n');
|
|
63
63
|
clearWatcherState(db);
|
|
64
64
|
process.exitCode = EXIT_CODES.WATCHER_CONFLICT;
|
|
@@ -31,7 +31,7 @@ const ensureWatcherAvailable = (db, projectRoot, logger) => {
|
|
|
31
31
|
if (!existing) {
|
|
32
32
|
return true;
|
|
33
33
|
}
|
|
34
|
-
if (isOurProcess(existing.pid
|
|
34
|
+
if (isOurProcess(existing.pid)) {
|
|
35
35
|
const mode = existing.autonomous ? 'autonomous' : 'manual';
|
|
36
36
|
const message = `Watcher already running (pid ${existing.pid}, ${mode} mode). Use 'watchfix stop'.`;
|
|
37
37
|
if (logger) {
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -261,10 +261,13 @@ declare const configSchema: z.ZodObject<{
|
|
|
261
261
|
}>>;
|
|
262
262
|
deduplication: z.ZodDefault<z.ZodObject<{
|
|
263
263
|
fixed_grace_period: z.ZodDefault<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
264
|
+
deferred_grace_period: z.ZodDefault<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
264
265
|
}, "strip", z.ZodTypeAny, {
|
|
265
266
|
fixed_grace_period: string;
|
|
267
|
+
deferred_grace_period: string;
|
|
266
268
|
}, {
|
|
267
269
|
fixed_grace_period?: string | undefined;
|
|
270
|
+
deferred_grace_period?: string | undefined;
|
|
268
271
|
}>>;
|
|
269
272
|
patterns: z.ZodDefault<z.ZodObject<{
|
|
270
273
|
match: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -324,6 +327,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
324
327
|
};
|
|
325
328
|
deduplication: {
|
|
326
329
|
fixed_grace_period: string;
|
|
330
|
+
deferred_grace_period: string;
|
|
327
331
|
};
|
|
328
332
|
patterns: {
|
|
329
333
|
ignore: string[];
|
|
@@ -377,6 +381,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
377
381
|
} | undefined;
|
|
378
382
|
deduplication?: {
|
|
379
383
|
fixed_grace_period?: string | undefined;
|
|
384
|
+
deferred_grace_period?: string | undefined;
|
|
380
385
|
} | undefined;
|
|
381
386
|
patterns?: {
|
|
382
387
|
ignore?: string[] | undefined;
|
package/dist/config/schema.js
CHANGED
package/dist/db/queries.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export type ErrorInsert = {
|
|
|
35
35
|
createdAt?: string;
|
|
36
36
|
updatedAt?: string;
|
|
37
37
|
};
|
|
38
|
-
export type ActivityAction = 'watcher_start' | 'watcher_stop' | 'error_detected' | 'error_deduplicated' | 'analysis_start' | 'analysis_complete' | 'analysis_failed' | 'analysis_timeout' | 'already_fixed_detected' | 'fix_start' | 'fix_complete' | 'fix_failed' | 'fix_timeout' | 'verification_start' | 'verification_pass' | 'verification_fail' | 'error_ignored' | 'lock_acquired' | 'lock_released' | 'lock_expired' | 'stale_recovery';
|
|
38
|
+
export type ActivityAction = 'watcher_start' | 'watcher_stop' | 'error_detected' | 'error_deduplicated' | 'analysis_start' | 'analysis_complete' | 'analysis_failed' | 'analysis_timeout' | 'already_fixed_detected' | 'error_deferred' | 'deferred_reanalyzed' | 'fix_start' | 'fix_complete' | 'fix_failed' | 'fix_timeout' | 'verification_start' | 'verification_pass' | 'verification_fail' | 'error_ignored' | 'lock_acquired' | 'lock_released' | 'lock_expired' | 'stale_recovery';
|
|
39
39
|
export declare function insertError(db: Database, error: ErrorInsert): number;
|
|
40
40
|
export declare function getError(db: Database, id: number): ErrorRecord | null;
|
|
41
41
|
export declare function getErrorByHash(db: Database, hash: string): ErrorRecord | null;
|
package/dist/fixer/context.d.ts
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import type { Config } from '../config/schema.js';
|
|
2
2
|
import type { ErrorRecord } from '../db/queries.js';
|
|
3
|
+
type RetryContext = {
|
|
4
|
+
previousAttempt: {
|
|
5
|
+
analysis?: {
|
|
6
|
+
summary: string;
|
|
7
|
+
suggested_fix: string;
|
|
8
|
+
files_to_modify: string[];
|
|
9
|
+
};
|
|
10
|
+
fix?: {
|
|
11
|
+
success: boolean;
|
|
12
|
+
summary: string;
|
|
13
|
+
files_changed?: Array<{
|
|
14
|
+
path: string;
|
|
15
|
+
change: string;
|
|
16
|
+
}>;
|
|
17
|
+
};
|
|
18
|
+
verification_failure?: {
|
|
19
|
+
type: string;
|
|
20
|
+
command?: string;
|
|
21
|
+
message?: string;
|
|
22
|
+
stderr?: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
3
26
|
type GeneratedContext = {
|
|
4
27
|
path: string;
|
|
5
28
|
content: string;
|
|
6
29
|
};
|
|
7
|
-
export declare const generateAnalyzeContext: (error: ErrorRecord, config: Config, attempt: number) => GeneratedContext;
|
|
30
|
+
export declare const generateAnalyzeContext: (error: ErrorRecord, config: Config, attempt: number, retryContext?: RetryContext) => GeneratedContext;
|
|
8
31
|
export declare const generateFixContext: (error: ErrorRecord, analysis: string, config: Config, attempt: number) => GeneratedContext;
|
|
9
32
|
export type { GeneratedContext };
|
package/dist/fixer/context.js
CHANGED
|
@@ -97,9 +97,52 @@ const truncateStackTraceToBytes = (stackTrace, maxBytes) => {
|
|
|
97
97
|
const head = headBytes > 0 ? sliceUtf8ByBytes(stackTrace, headBytes, 'start') : '';
|
|
98
98
|
return head ? `${head}\n${STACK_TRACE_TRUNCATION_MARKER}` : STACK_TRACE_TRUNCATION_MARKER;
|
|
99
99
|
};
|
|
100
|
+
const buildRetrySection = (retryContext, attempt) => {
|
|
101
|
+
const { previousAttempt } = retryContext;
|
|
102
|
+
const lines = [
|
|
103
|
+
`## IMPORTANT: This is a RETRY (Attempt ${attempt + 1})`,
|
|
104
|
+
'',
|
|
105
|
+
'The previous fix attempt was applied but **verification failed**.',
|
|
106
|
+
'',
|
|
107
|
+
'### What was tried:',
|
|
108
|
+
];
|
|
109
|
+
if (previousAttempt.analysis) {
|
|
110
|
+
lines.push(`- Analysis: ${previousAttempt.analysis.summary}`);
|
|
111
|
+
if (previousAttempt.analysis.files_to_modify.length > 0) {
|
|
112
|
+
lines.push(`- Files modified: ${previousAttempt.analysis.files_to_modify.join(', ')}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (previousAttempt.fix) {
|
|
116
|
+
lines.push(`- Fix applied: ${previousAttempt.fix.summary}`);
|
|
117
|
+
}
|
|
118
|
+
lines.push('');
|
|
119
|
+
lines.push('### Why it failed:');
|
|
120
|
+
if (previousAttempt.verification_failure) {
|
|
121
|
+
const vf = previousAttempt.verification_failure;
|
|
122
|
+
lines.push(`**Verification command failed**: ${vf.command || 'unknown'}`);
|
|
123
|
+
lines.push(`**Message**: ${vf.message || 'unknown'}`);
|
|
124
|
+
if (vf.stderr) {
|
|
125
|
+
lines.push('**Test output**:');
|
|
126
|
+
lines.push('```');
|
|
127
|
+
lines.push(vf.stderr.slice(0, 2000));
|
|
128
|
+
lines.push('```');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
lines.push('Verification failed (details not available)');
|
|
133
|
+
}
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push('### Instructions for retry:');
|
|
136
|
+
lines.push('1. The code has ALREADY been modified by the previous attempt');
|
|
137
|
+
lines.push('2. Do NOT report already_fixed unless the verification test is actually passing');
|
|
138
|
+
lines.push('3. Focus on the verification failure output - it shows what\'s still broken');
|
|
139
|
+
lines.push('4. The original error message may not appear in code anymore, but the test is still failing');
|
|
140
|
+
lines.push('');
|
|
141
|
+
return lines.join('\n');
|
|
142
|
+
};
|
|
100
143
|
const buildAnalyzeContent = (options) => {
|
|
101
144
|
const { projectName, projectRoot, error, attempt, date, stackTrace } = options;
|
|
102
|
-
const analysisPath = path.posix.join(CONTEXT_DIR, `${date}-error-${error.id}-attempt-${attempt}-analysis.yaml`);
|
|
145
|
+
const analysisPath = path.posix.join(CONTEXT_DIR, `${date}-error-${error.id}-attempt-${attempt + 1}-analysis.yaml`);
|
|
103
146
|
return `# WatchFix Task
|
|
104
147
|
|
|
105
148
|
## Mode
|
|
@@ -114,7 +157,7 @@ analyze
|
|
|
114
157
|
- Source: ${error.source}
|
|
115
158
|
- Type: ${error.errorType}
|
|
116
159
|
- Detected: ${error.timestamp}
|
|
117
|
-
- Fix Attempts: ${error.fixAttempts}
|
|
160
|
+
- Fix Attempts: ${error.fixAttempts + 1}
|
|
118
161
|
|
|
119
162
|
### Message
|
|
120
163
|
${error.message}
|
|
@@ -124,27 +167,69 @@ ${stackTrace}
|
|
|
124
167
|
|
|
125
168
|
### Context (surrounding log lines)
|
|
126
169
|
${options.contextBlock}
|
|
170
|
+
${options.retryContext ? `
|
|
171
|
+
${buildRetrySection(options.retryContext, options.attempt)}` : ''}
|
|
172
|
+
## Error Classification
|
|
173
|
+
|
|
174
|
+
First, classify this error into one of three categories:
|
|
175
|
+
|
|
176
|
+
**code** - Bugs in source code fixable by modifying code files:
|
|
177
|
+
- Logic errors, null pointer exceptions, type mismatches
|
|
178
|
+
- Missing error handling, incorrect API usage
|
|
179
|
+
- Syntax errors, import/export issues
|
|
180
|
+
|
|
181
|
+
**infrastructure** - Environment/deployment issues NOT fixable by code changes:
|
|
182
|
+
- Database/Redis/queue unavailable or connection refused
|
|
183
|
+
- Network connectivity issues, DNS resolution failures
|
|
184
|
+
- Resource exhaustion (disk full, out of memory)
|
|
185
|
+
- Container/orchestration problems, service not running
|
|
186
|
+
|
|
187
|
+
**configuration** - Settings or config issues NOT fixable by code changes:
|
|
188
|
+
- Missing or incorrect environment variables
|
|
189
|
+
- Wrong config file values, invalid credentials
|
|
190
|
+
- Permission issues, file/directory access denied
|
|
191
|
+
- SSL/TLS certificate problems
|
|
127
192
|
|
|
128
193
|
## Instructions
|
|
129
194
|
|
|
130
|
-
1. **
|
|
195
|
+
1. **Classify the error category first**
|
|
196
|
+
- If infrastructure or configuration: provide remediation_guidance for the user
|
|
197
|
+
- Only code errors will proceed to the fix phase
|
|
198
|
+
|
|
199
|
+
2. **Check if this issue still exists in the code** (for code errors)
|
|
131
200
|
- Look at the file(s) mentioned in the stack trace
|
|
132
|
-
-
|
|
133
|
-
|
|
201
|
+
- If the code has been fixed, report already_fixed: true
|
|
202
|
+
|
|
203
|
+
3. **Trace the root cause** (not just the symptom):
|
|
204
|
+
- Ask WHY the error occurs, not just WHERE
|
|
205
|
+
- Common root causes to check:
|
|
206
|
+
- Type mismatches in comparisons (e.g., comparing incompatible types)
|
|
207
|
+
- Failed lookups due to incorrect comparison logic
|
|
208
|
+
- Missing type conversions on input values
|
|
209
|
+
- Follow the data flow from source to error location
|
|
210
|
+
- The fix should address the underlying cause, not just guard against the symptom
|
|
134
211
|
|
|
135
|
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
-
-
|
|
139
|
-
-
|
|
212
|
+
4. **Determine the minimal fix location**:
|
|
213
|
+
- Fix at the point where the bug originates, not where it manifests
|
|
214
|
+
- For type issues: convert types at the source
|
|
215
|
+
- For unhandled errors: add error handling at the CALL SITE only
|
|
216
|
+
- Do NOT modify functions that throw errors - handle errors where they are called
|
|
217
|
+
|
|
218
|
+
## Anti-patterns to Avoid
|
|
219
|
+
- Adding null/undefined checks that mask the real bug (e.g., the check passes but the lookup logic is still wrong)
|
|
220
|
+
- Modifying error-throwing functions instead of handling at call sites
|
|
221
|
+
- Adding environment variables, feature flags, or extra parameters
|
|
222
|
+
- Refactoring or improving code beyond the specific fix
|
|
140
223
|
|
|
141
224
|
Write your analysis to: \`${analysisPath}\`
|
|
142
225
|
|
|
143
226
|
Use this exact YAML format:
|
|
144
227
|
\`\`\`yaml
|
|
145
228
|
already_fixed: true | false
|
|
229
|
+
category: code | infrastructure | configuration
|
|
146
230
|
summary: One sentence summary of the problem (or that it was already fixed)
|
|
147
|
-
|
|
231
|
+
|
|
232
|
+
# For code errors (category: code), include these fields:
|
|
148
233
|
root_cause: |
|
|
149
234
|
Detailed explanation of root cause
|
|
150
235
|
Can be multiple lines
|
|
@@ -155,6 +240,13 @@ files_to_modify:
|
|
|
155
240
|
- path/to/file1
|
|
156
241
|
- path/to/file2
|
|
157
242
|
confidence: high | medium | low
|
|
243
|
+
|
|
244
|
+
# For non-code errors (category: infrastructure or configuration), include:
|
|
245
|
+
remediation_guidance: |
|
|
246
|
+
Steps the user should take to resolve this issue.
|
|
247
|
+
Be specific about what to check or change.
|
|
248
|
+
Example: "Start the PostgreSQL container with: docker-compose up -d postgres"
|
|
249
|
+
confidence: high | medium | low
|
|
158
250
|
\`\`\`
|
|
159
251
|
|
|
160
252
|
## Constraints
|
|
@@ -163,11 +255,12 @@ confidence: high | medium | low
|
|
|
163
255
|
- Be specific about file paths relative to project root
|
|
164
256
|
- Set already_fixed to true if the issue no longer exists in the code (e.g., fixed by a previous error fix)
|
|
165
257
|
- WARNING: If a fix fails and is retried, any file modifications from previous attempts will persist
|
|
258
|
+
- For infrastructure/configuration errors, provide actionable remediation_guidance
|
|
166
259
|
`;
|
|
167
260
|
};
|
|
168
261
|
const buildFixContent = (options) => {
|
|
169
262
|
const { projectName, projectRoot, error, attempt, date, stackTrace, analysis } = options;
|
|
170
|
-
const resultPath = path.posix.join(CONTEXT_DIR, `${date}-error-${error.id}-attempt-${attempt}-result.yaml`);
|
|
263
|
+
const resultPath = path.posix.join(CONTEXT_DIR, `${date}-error-${error.id}-attempt-${attempt + 1}-result.yaml`);
|
|
171
264
|
return `# WatchFix Task
|
|
172
265
|
|
|
173
266
|
## Mode
|
|
@@ -182,7 +275,7 @@ fix
|
|
|
182
275
|
- Source: ${error.source}
|
|
183
276
|
- Type: ${error.errorType}
|
|
184
277
|
- Detected: ${error.timestamp}
|
|
185
|
-
- Fix Attempts: ${error.fixAttempts}
|
|
278
|
+
- Fix Attempts: ${error.fixAttempts + 1}
|
|
186
279
|
|
|
187
280
|
### Message
|
|
188
281
|
${error.message}
|
|
@@ -215,8 +308,13 @@ notes: |
|
|
|
215
308
|
\`\`\`
|
|
216
309
|
|
|
217
310
|
## Constraints
|
|
218
|
-
- Make the
|
|
219
|
-
-
|
|
311
|
+
- Make the SMALLEST change that fixes the ROOT CAUSE
|
|
312
|
+
- Fix the bug where it originates, not where symptoms appear
|
|
313
|
+
- Do NOT add defensive checks that mask the real bug
|
|
314
|
+
- Do NOT modify functions that throw errors - add handling at call sites
|
|
315
|
+
- Do NOT add environment variables, feature flags, or new parameters
|
|
316
|
+
- Do NOT refactor, improve, or clean up code beyond the fix
|
|
317
|
+
- If touching more than 1-2 files or 10 lines, reconsider your approach
|
|
220
318
|
- If the fix cannot be applied, set success to false and explain in notes
|
|
221
319
|
- WARNING: If this fix fails verification, the modified files will remain changed for the next retry attempt
|
|
222
320
|
`;
|
|
@@ -263,9 +361,9 @@ const ensureSizeLimit = (options) => {
|
|
|
263
361
|
}
|
|
264
362
|
return { content, truncatedLines, beforeLines, afterLines, stackTrace };
|
|
265
363
|
};
|
|
266
|
-
export const generateAnalyzeContext = (error, config, attempt) => {
|
|
364
|
+
export const generateAnalyzeContext = (error, config, attempt, retryContext) => {
|
|
267
365
|
const date = formatDate();
|
|
268
|
-
const contextPath = path.posix.join(CONTEXT_DIR, `${date}-error-${error.id}-attempt-${attempt}-analyze.md`);
|
|
366
|
+
const contextPath = path.posix.join(CONTEXT_DIR, `${date}-error-${error.id}-attempt-${attempt + 1}-analyze.md`);
|
|
269
367
|
const maxBytes = config.cleanup.context_max_size_kb * 1024;
|
|
270
368
|
const { before, after } = splitRawLog(error);
|
|
271
369
|
const buildContent = (stackTraceValue, beforeLines, afterLines, truncated) => {
|
|
@@ -281,6 +379,7 @@ export const generateAnalyzeContext = (error, config, attempt) => {
|
|
|
281
379
|
date,
|
|
282
380
|
stackTrace: stackTraceValue,
|
|
283
381
|
contextBlock: buildContextBlock(beforeLines, errorLines, afterLines, truncated),
|
|
382
|
+
retryContext,
|
|
284
383
|
});
|
|
285
384
|
};
|
|
286
385
|
const stackTrace = truncateStackTrace(error.stackTrace ?? '');
|
|
@@ -345,7 +444,7 @@ const ensureFixSizeLimit = (options) => {
|
|
|
345
444
|
};
|
|
346
445
|
export const generateFixContext = (error, analysis, config, attempt) => {
|
|
347
446
|
const date = formatDate();
|
|
348
|
-
const contextPath = path.posix.join(CONTEXT_DIR, `${date}-error-${error.id}-attempt-${attempt}-fix.md`);
|
|
447
|
+
const contextPath = path.posix.join(CONTEXT_DIR, `${date}-error-${error.id}-attempt-${attempt + 1}-fix.md`);
|
|
349
448
|
const maxBytes = config.cleanup.context_max_size_kb * 1024;
|
|
350
449
|
const stackTrace = truncateStackTrace(error.stackTrace ?? '');
|
|
351
450
|
const render = (analysisValue, stackTraceValue) => buildFixContent({
|
package/dist/fixer/index.d.ts
CHANGED
|
@@ -25,6 +25,29 @@ type FixOrchestratorOptions = {
|
|
|
25
25
|
logger?: Logger;
|
|
26
26
|
terminalEnabled?: boolean;
|
|
27
27
|
};
|
|
28
|
+
export type RetryContext = {
|
|
29
|
+
previousAttempt: {
|
|
30
|
+
analysis?: {
|
|
31
|
+
summary: string;
|
|
32
|
+
suggested_fix: string;
|
|
33
|
+
files_to_modify: string[];
|
|
34
|
+
};
|
|
35
|
+
fix?: {
|
|
36
|
+
success: boolean;
|
|
37
|
+
summary: string;
|
|
38
|
+
files_changed?: Array<{
|
|
39
|
+
path: string;
|
|
40
|
+
change: string;
|
|
41
|
+
}>;
|
|
42
|
+
};
|
|
43
|
+
verification_failure?: {
|
|
44
|
+
type: string;
|
|
45
|
+
command?: string;
|
|
46
|
+
message?: string;
|
|
47
|
+
stderr?: string;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
};
|
|
28
51
|
export declare class FixOrchestrator {
|
|
29
52
|
private readonly db;
|
|
30
53
|
private readonly config;
|
package/dist/fixer/index.js
CHANGED
|
@@ -51,9 +51,24 @@ const parseSuggestion = (value) => {
|
|
|
51
51
|
suggested_fix: parsed.suggested_fix ?? '',
|
|
52
52
|
files_to_modify: parsed.files_to_modify ?? [],
|
|
53
53
|
confidence: parsed.confidence,
|
|
54
|
+
category: parsed.category,
|
|
55
|
+
remediation_guidance: parsed.remediation_guidance,
|
|
54
56
|
};
|
|
55
57
|
}
|
|
56
|
-
// For
|
|
58
|
+
// For non-code categories, other fields may be empty
|
|
59
|
+
if (parsed.category && parsed.category !== 'code') {
|
|
60
|
+
return {
|
|
61
|
+
already_fixed: false,
|
|
62
|
+
summary: parsed.summary,
|
|
63
|
+
root_cause: parsed.root_cause ?? '',
|
|
64
|
+
suggested_fix: parsed.suggested_fix ?? '',
|
|
65
|
+
files_to_modify: parsed.files_to_modify ?? [],
|
|
66
|
+
confidence: parsed.confidence,
|
|
67
|
+
category: parsed.category,
|
|
68
|
+
remediation_guidance: parsed.remediation_guidance,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// For regular code analyses, ensure all fields are present
|
|
57
72
|
if (typeof parsed.root_cause !== 'string' ||
|
|
58
73
|
typeof parsed.suggested_fix !== 'string' ||
|
|
59
74
|
!Array.isArray(parsed.files_to_modify)) {
|
|
@@ -66,8 +81,41 @@ const parseSuggestion = (value) => {
|
|
|
66
81
|
suggested_fix: parsed.suggested_fix,
|
|
67
82
|
files_to_modify: parsed.files_to_modify,
|
|
68
83
|
confidence: parsed.confidence,
|
|
84
|
+
category: parsed.category,
|
|
85
|
+
remediation_guidance: parsed.remediation_guidance,
|
|
69
86
|
};
|
|
70
87
|
};
|
|
88
|
+
const buildRetryContext = (error) => {
|
|
89
|
+
if (error.fixAttempts === 0)
|
|
90
|
+
return undefined;
|
|
91
|
+
const previousAttempt = {};
|
|
92
|
+
// Extract analysis from suggestion
|
|
93
|
+
if (error.suggestion) {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(error.suggestion);
|
|
96
|
+
if (!parsed.error && parsed.summary) {
|
|
97
|
+
previousAttempt.analysis = {
|
|
98
|
+
summary: parsed.summary,
|
|
99
|
+
suggested_fix: parsed.suggested_fix || '',
|
|
100
|
+
files_to_modify: parsed.files_to_modify || [],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch { /* ignore */ }
|
|
105
|
+
}
|
|
106
|
+
// Extract fix result and verification failure
|
|
107
|
+
if (error.fixResult) {
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(error.fixResult);
|
|
110
|
+
if (parsed.fix)
|
|
111
|
+
previousAttempt.fix = parsed.fix;
|
|
112
|
+
if (parsed.verification_failure)
|
|
113
|
+
previousAttempt.verification_failure = parsed.verification_failure;
|
|
114
|
+
}
|
|
115
|
+
catch { /* ignore */ }
|
|
116
|
+
}
|
|
117
|
+
return Object.keys(previousAttempt).length > 0 ? { previousAttempt } : undefined;
|
|
118
|
+
};
|
|
71
119
|
export class FixOrchestrator {
|
|
72
120
|
db;
|
|
73
121
|
config;
|
|
@@ -151,8 +199,9 @@ export class FixOrchestrator {
|
|
|
151
199
|
throw new UserError(`Failed to transition error ${errorId} into analyzing status`);
|
|
152
200
|
}
|
|
153
201
|
logActivity(this.db, 'analysis_start', errorId, JSON.stringify({ attempt: attempts, lockId }));
|
|
154
|
-
this.logger.info(`Analyzing error ${errorId} (attempt ${attempts})...`);
|
|
155
|
-
const
|
|
202
|
+
this.logger.info(`Analyzing error ${errorId} (attempt ${attempts + 1})...`);
|
|
203
|
+
const retryContext = buildRetryContext(error);
|
|
204
|
+
const context = generateAnalyzeContext(error, this.config, attempts, retryContext);
|
|
156
205
|
const contextPath = path.resolve(this.config.project.root, context.path);
|
|
157
206
|
await fs.mkdir(path.dirname(contextPath), { recursive: true });
|
|
158
207
|
await fs.writeFile(contextPath, context.content, 'utf8');
|
|
@@ -185,6 +234,34 @@ export class FixOrchestrator {
|
|
|
185
234
|
message: 'Issue already fixed',
|
|
186
235
|
};
|
|
187
236
|
}
|
|
237
|
+
// Handle non-code errors by transitioning to deferred status
|
|
238
|
+
const category = analysisOutput.category ?? 'code';
|
|
239
|
+
if (category !== 'code') {
|
|
240
|
+
if (!transitionStatus(this.db, errorId, 'analyzing', 'deferred', lockId)) {
|
|
241
|
+
throw new UserError(`Failed to transition error ${errorId} into deferred status`);
|
|
242
|
+
}
|
|
243
|
+
this.db.run('UPDATE errors SET suggestion = ?, updated_at = ? WHERE id = ?', [
|
|
244
|
+
JSON.stringify(analysisOutput),
|
|
245
|
+
new Date().toISOString(),
|
|
246
|
+
errorId,
|
|
247
|
+
]);
|
|
248
|
+
logActivity(this.db, 'error_deferred', errorId, JSON.stringify({
|
|
249
|
+
attempt: attempts,
|
|
250
|
+
category,
|
|
251
|
+
summary: analysisOutput.summary,
|
|
252
|
+
}));
|
|
253
|
+
this.logger.info(`Error ${errorId} deferred (category=${category}): ${analysisOutput.summary}`);
|
|
254
|
+
await releaseLock(this.db, errorId, lockId);
|
|
255
|
+
lockHeld = false;
|
|
256
|
+
return {
|
|
257
|
+
errorId,
|
|
258
|
+
status: 'deferred',
|
|
259
|
+
lockAcquired: true,
|
|
260
|
+
attempts,
|
|
261
|
+
analysis: analysisOutput,
|
|
262
|
+
message: `Error deferred (${category})`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
188
265
|
if (!transitionStatus(this.db, errorId, 'analyzing', 'suggested', lockId)) {
|
|
189
266
|
throw new UserError(`Failed to transition error ${errorId} into suggested status`);
|
|
190
267
|
}
|
|
@@ -279,7 +356,7 @@ export class FixOrchestrator {
|
|
|
279
356
|
throw new UserError(`Failed to transition error ${errorId} into fixing status`);
|
|
280
357
|
}
|
|
281
358
|
logActivity(this.db, 'fix_start', errorId, JSON.stringify({ attempt: attempts, lockId }));
|
|
282
|
-
this.logger.info(`Applying fix for error ${errorId} (attempt ${attempts})...`);
|
|
359
|
+
this.logger.info(`Applying fix for error ${errorId} (attempt ${attempts + 1})...`);
|
|
283
360
|
const analysisYaml = analysisToYaml(analysisOutput);
|
|
284
361
|
const fixContext = generateFixContext(error, analysisYaml, this.config, attempts);
|
|
285
362
|
const fixContextPath = path.resolve(this.config.project.root, fixContext.path);
|
|
@@ -362,14 +439,16 @@ export class FixOrchestrator {
|
|
|
362
439
|
if (!transitionStatus(this.db, errorId, 'fixing', 'fixed', lockId)) {
|
|
363
440
|
throw new UserError(`Failed to transition error ${errorId} into fixed status`);
|
|
364
441
|
}
|
|
365
|
-
|
|
442
|
+
const newAttempts = attempts + 1;
|
|
443
|
+
this.db.run('UPDATE errors SET fix_attempts = ?, updated_at = ? WHERE id = ?', [newAttempts, new Date().toISOString(), errorId]);
|
|
444
|
+
logActivity(this.db, 'verification_pass', errorId, JSON.stringify({ attempt: newAttempts }));
|
|
366
445
|
await releaseLock(this.db, errorId, lockId);
|
|
367
446
|
lockHeld = false;
|
|
368
447
|
return {
|
|
369
448
|
errorId,
|
|
370
449
|
status: 'fixed',
|
|
371
450
|
lockAcquired: true,
|
|
372
|
-
attempts,
|
|
451
|
+
attempts: newAttempts,
|
|
373
452
|
analysis: analysisOutput,
|
|
374
453
|
fix: fixOutput,
|
|
375
454
|
verification: verificationResult,
|
|
@@ -382,7 +461,18 @@ export class FixOrchestrator {
|
|
|
382
461
|
if (!transitionStatus(this.db, errorId, 'fixing', nextStatus, lockId)) {
|
|
383
462
|
throw new UserError(`Failed to transition error ${errorId} after verification failure`);
|
|
384
463
|
}
|
|
385
|
-
|
|
464
|
+
const failureRecord = {
|
|
465
|
+
fix: fixOutput,
|
|
466
|
+
verification_failure: verificationResult.failure ? {
|
|
467
|
+
type: verificationResult.failure.type,
|
|
468
|
+
command: verificationResult.failure.type === 'command'
|
|
469
|
+
? verificationResult.failure.command : undefined,
|
|
470
|
+
message: verificationResult.failure.message,
|
|
471
|
+
stderr: verificationResult.failure.type === 'command'
|
|
472
|
+
? verificationResult.failure.stderr?.slice(0, 4096) : undefined,
|
|
473
|
+
} : undefined,
|
|
474
|
+
};
|
|
475
|
+
this.db.run('UPDATE errors SET fix_result = ?, fix_attempts = ?, updated_at = ? WHERE id = ?', [JSON.stringify(failureRecord), newAttempts, new Date().toISOString(), errorId]);
|
|
386
476
|
logActivity(this.db, 'verification_fail', errorId, JSON.stringify({ attempt: attempts, failure: verificationResult.failure }));
|
|
387
477
|
await releaseLock(this.db, errorId, lockId);
|
|
388
478
|
lockHeld = false;
|
package/dist/fixer/output.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
type ConfidenceLevel = 'high' | 'medium' | 'low';
|
|
2
|
+
type ErrorCategory = 'code' | 'infrastructure' | 'configuration';
|
|
2
3
|
type AnalysisOutput = {
|
|
3
4
|
already_fixed: boolean;
|
|
4
5
|
summary: string;
|
|
@@ -6,6 +7,8 @@ type AnalysisOutput = {
|
|
|
6
7
|
suggested_fix: string;
|
|
7
8
|
files_to_modify: string[];
|
|
8
9
|
confidence: ConfidenceLevel;
|
|
10
|
+
category?: ErrorCategory;
|
|
11
|
+
remediation_guidance?: string;
|
|
9
12
|
};
|
|
10
13
|
type FixOutput = {
|
|
11
14
|
success: boolean;
|
|
@@ -16,7 +19,18 @@ type FixOutput = {
|
|
|
16
19
|
}>;
|
|
17
20
|
notes?: string;
|
|
18
21
|
};
|
|
22
|
+
type StoredFixResult = {
|
|
23
|
+
fix?: FixOutput;
|
|
24
|
+
verification_failure?: {
|
|
25
|
+
type: 'command' | 'health_check';
|
|
26
|
+
command?: string;
|
|
27
|
+
message?: string;
|
|
28
|
+
stderr?: string;
|
|
29
|
+
};
|
|
30
|
+
error?: boolean;
|
|
31
|
+
diagnostic?: string;
|
|
32
|
+
};
|
|
19
33
|
declare const parseAnalysisOutput: (content: string) => AnalysisOutput;
|
|
20
34
|
declare const parseFixOutput: (content: string) => FixOutput;
|
|
21
|
-
export type { AnalysisOutput, FixOutput, ConfidenceLevel };
|
|
35
|
+
export type { AnalysisOutput, FixOutput, ConfidenceLevel, ErrorCategory, StoredFixResult };
|
|
22
36
|
export { parseAnalysisOutput, parseFixOutput };
|
package/dist/fixer/output.js
CHANGED
|
@@ -66,12 +66,30 @@ const validateConfidence = (value) => {
|
|
|
66
66
|
}
|
|
67
67
|
throw new UserError(`Invalid confidence value: ${value ?? 'undefined'} (expected high|medium|low)`);
|
|
68
68
|
};
|
|
69
|
+
const validateCategory = (value) => {
|
|
70
|
+
if (value === 'code' || value === 'infrastructure' || value === 'configuration') {
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
throw new UserError(`Invalid category value: ${value ?? 'undefined'} (expected code|infrastructure|configuration)`);
|
|
74
|
+
};
|
|
69
75
|
const parseAnalysisOutput = (content) => {
|
|
70
76
|
const raw = parseYaml(content);
|
|
71
77
|
const data = assertRecord(raw);
|
|
72
78
|
const already_fixed = data.already_fixed === true;
|
|
73
79
|
const summary = requireStringField(data, 'summary');
|
|
74
80
|
const confidence = validateConfidence(data.confidence);
|
|
81
|
+
// Parse category, defaulting to 'code' if not specified (backwards compat)
|
|
82
|
+
const category = data.category !== undefined
|
|
83
|
+
? validateCategory(data.category)
|
|
84
|
+
: 'code';
|
|
85
|
+
// Parse remediation_guidance if present
|
|
86
|
+
let remediation_guidance;
|
|
87
|
+
if (data.remediation_guidance !== undefined) {
|
|
88
|
+
if (typeof data.remediation_guidance !== 'string') {
|
|
89
|
+
throw new UserError('Invalid field: remediation_guidance must be a string');
|
|
90
|
+
}
|
|
91
|
+
remediation_guidance = data.remediation_guidance;
|
|
92
|
+
}
|
|
75
93
|
if (already_fixed) {
|
|
76
94
|
return {
|
|
77
95
|
already_fixed: true,
|
|
@@ -80,8 +98,31 @@ const parseAnalysisOutput = (content) => {
|
|
|
80
98
|
suggested_fix: '',
|
|
81
99
|
files_to_modify: [],
|
|
82
100
|
confidence,
|
|
101
|
+
category,
|
|
102
|
+
remediation_guidance,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// For non-code categories, require remediation_guidance but allow empty files_to_modify
|
|
106
|
+
if (category !== 'code') {
|
|
107
|
+
if (!remediation_guidance || remediation_guidance.trim() === '') {
|
|
108
|
+
throw new UserError(`Missing required field: remediation_guidance (required for ${category} errors)`);
|
|
109
|
+
}
|
|
110
|
+
// files_to_modify can be empty for non-code errors
|
|
111
|
+
const files_to_modify = data.files_to_modify !== undefined
|
|
112
|
+
? requireStringArrayField(data, 'files_to_modify')
|
|
113
|
+
: [];
|
|
114
|
+
return {
|
|
115
|
+
already_fixed: false,
|
|
116
|
+
summary,
|
|
117
|
+
root_cause: typeof data.root_cause === 'string' ? data.root_cause : '',
|
|
118
|
+
suggested_fix: typeof data.suggested_fix === 'string' ? data.suggested_fix : '',
|
|
119
|
+
files_to_modify,
|
|
120
|
+
confidence,
|
|
121
|
+
category,
|
|
122
|
+
remediation_guidance,
|
|
83
123
|
};
|
|
84
124
|
}
|
|
125
|
+
// For code category, require root_cause, suggested_fix, files_to_modify
|
|
85
126
|
const root_cause = requireStringField(data, 'root_cause');
|
|
86
127
|
const suggested_fix = requireStringField(data, 'suggested_fix');
|
|
87
128
|
const files_to_modify = requireStringArrayField(data, 'files_to_modify');
|
|
@@ -92,6 +133,8 @@ const parseAnalysisOutput = (content) => {
|
|
|
92
133
|
suggested_fix,
|
|
93
134
|
files_to_modify,
|
|
94
135
|
confidence,
|
|
136
|
+
category,
|
|
137
|
+
remediation_guidance,
|
|
95
138
|
};
|
|
96
139
|
};
|
|
97
140
|
const parseFixOutput = (content) => {
|
package/dist/utils/errors.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare class InternalError extends Error {
|
|
|
6
6
|
cause?: unknown;
|
|
7
7
|
});
|
|
8
8
|
}
|
|
9
|
-
export type ErrorStatus = 'pending' | 'analyzing' | 'suggested' | 'fixing' | 'fixed' | 'failed' | 'ignored' | 'resolved';
|
|
9
|
+
export type ErrorStatus = 'pending' | 'analyzing' | 'suggested' | 'fixing' | 'fixed' | 'failed' | 'ignored' | 'resolved' | 'deferred';
|
|
10
10
|
export declare const EXIT_CODES: {
|
|
11
11
|
readonly SUCCESS: 0;
|
|
12
12
|
readonly GENERAL_ERROR: 1;
|
package/dist/utils/process.d.ts
CHANGED
|
@@ -13,4 +13,4 @@ export interface SpawnResult {
|
|
|
13
13
|
}
|
|
14
14
|
export declare function checkCliExists(command: string): CliCheckResult;
|
|
15
15
|
export declare function spawnWithTimeout(command: string, args: string[], options: SpawnOptions | undefined, timeoutMs: number): Promise<SpawnResult>;
|
|
16
|
-
export declare function isOurProcess(pid: number
|
|
16
|
+
export declare function isOurProcess(pid: number): boolean;
|
package/dist/utils/process.js
CHANGED
|
@@ -126,7 +126,7 @@ function readCommandLine(pid) {
|
|
|
126
126
|
});
|
|
127
127
|
return `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
|
|
128
128
|
}
|
|
129
|
-
export function isOurProcess(pid
|
|
129
|
+
export function isOurProcess(pid) {
|
|
130
130
|
try {
|
|
131
131
|
process.kill(pid, 0);
|
|
132
132
|
}
|
|
@@ -138,7 +138,8 @@ export function isOurProcess(pid, expectedRoot) {
|
|
|
138
138
|
if (!cmdline) {
|
|
139
139
|
return false;
|
|
140
140
|
}
|
|
141
|
-
|
|
141
|
+
// Check for 'watchfix' in command line, or 'index.js' for dev mode
|
|
142
|
+
return cmdline.includes('watchfix') || cmdline.includes('index.js');
|
|
142
143
|
}
|
|
143
144
|
catch {
|
|
144
145
|
return false;
|
package/dist/watcher/index.js
CHANGED
|
@@ -203,6 +203,25 @@ export class WatcherOrchestrator {
|
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
|
+
// Handle deferred errors with grace period
|
|
207
|
+
// Within grace period: deduplicate
|
|
208
|
+
// After grace period: create new error for re-analysis
|
|
209
|
+
if (existing && existing.status === 'deferred') {
|
|
210
|
+
const gracePeriodMs = parseDuration(this.config.deduplication.deferred_grace_period);
|
|
211
|
+
const deferredAt = new Date(existing.updatedAt).getTime();
|
|
212
|
+
if (Date.now() - deferredAt < gracePeriodMs) {
|
|
213
|
+
logActivity(this.db, 'error_deduplicated', existing.id, `status=${existing.status} grace_period=true`);
|
|
214
|
+
this.logger.info(`Deduplicated error ${error.hash} (within grace period after deferral)`);
|
|
215
|
+
this.emitter.emit('error_deduplicated', {
|
|
216
|
+
errorId: existing.id,
|
|
217
|
+
error,
|
|
218
|
+
status: existing.status,
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Grace period expired - fall through to create new error for re-analysis
|
|
223
|
+
this.logger.info(`Deferred error ${error.hash} reappeared after grace period - creating new error for re-analysis`);
|
|
224
|
+
}
|
|
206
225
|
const newId = insertError(this.db, {
|
|
207
226
|
hash: error.hash,
|
|
208
227
|
source: error.source,
|
|
@@ -216,11 +235,18 @@ export class WatcherOrchestrator {
|
|
|
216
235
|
suggestion: null,
|
|
217
236
|
fixResult: null,
|
|
218
237
|
});
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
// Log appropriate activity based on previous status
|
|
239
|
+
if (existing && existing.status === 'deferred') {
|
|
240
|
+
logActivity(this.db, 'deferred_reanalyzed', newId, `previous_id=${existing.id}`);
|
|
241
|
+
this.logger.info(`Deferred error re-analyzed (${newId}) from ${error.source}`);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const details = existing ? `recurrence_of=${existing.id}` : undefined;
|
|
245
|
+
logActivity(this.db, 'error_detected', newId, details);
|
|
246
|
+
this.logger.info(existing
|
|
247
|
+
? `Recurring error detected (${newId}) from ${error.source}`
|
|
248
|
+
: `New error detected (${newId}) from ${error.source}`);
|
|
249
|
+
}
|
|
224
250
|
this.emitter.emit('error_detected', {
|
|
225
251
|
errorId: newId,
|
|
226
252
|
error,
|