@lumenflow/agent 1.0.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/LICENSE +190 -0
- package/README.md +192 -0
- package/dist/agent-incidents.d.ts +55 -0
- package/dist/agent-incidents.js +91 -0
- package/dist/agent-session.d.ts +66 -0
- package/dist/agent-session.js +155 -0
- package/dist/agent-verification.d.ts +31 -0
- package/dist/agent-verification.js +118 -0
- package/dist/auto-session-integration.d.ts +106 -0
- package/dist/auto-session-integration.js +173 -0
- package/dist/feedback-promote-core.d.ts +137 -0
- package/dist/feedback-promote-core.js +337 -0
- package/dist/feedback-review-core.d.ts +116 -0
- package/dist/feedback-review-core.js +358 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/package.json +63 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Promote Core Logic (WU-1599)
|
|
3
|
+
*
|
|
4
|
+
* Generates draft WU specs from feedback patterns and promotes them to actual WUs.
|
|
5
|
+
* Tracks incident-to-WU mappings in feedback-index.ndjson.
|
|
6
|
+
*
|
|
7
|
+
* @see {@link tools/__tests__/feedback-promote.test.mjs} - Tests
|
|
8
|
+
* @see {@link tools/feedback-promote.mjs} - CLI entry point
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Directory for draft WU specs
|
|
12
|
+
*/
|
|
13
|
+
export declare const DRAFT_DIRECTORY = ".beacon/feedback-drafts";
|
|
14
|
+
/**
|
|
15
|
+
* Path to feedback index (incident-to-WU mappings)
|
|
16
|
+
*/
|
|
17
|
+
export declare const FEEDBACK_INDEX_PATH = ".beacon/feedback-index.ndjson";
|
|
18
|
+
/**
|
|
19
|
+
* Feedback index entry status
|
|
20
|
+
*/
|
|
21
|
+
export declare const FEEDBACK_STATUS: {
|
|
22
|
+
PENDING_RESOLUTION: string;
|
|
23
|
+
RESOLVED: string;
|
|
24
|
+
WONT_FIX: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Pattern example structure
|
|
28
|
+
*/
|
|
29
|
+
interface PatternExample {
|
|
30
|
+
id: string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Pattern structure from feedback:review
|
|
35
|
+
*/
|
|
36
|
+
interface Pattern {
|
|
37
|
+
title: string;
|
|
38
|
+
category?: string;
|
|
39
|
+
frequency?: number;
|
|
40
|
+
score?: number;
|
|
41
|
+
firstSeen?: string;
|
|
42
|
+
lastSeen?: string;
|
|
43
|
+
examples?: PatternExample[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Draft WU spec structure
|
|
47
|
+
*/
|
|
48
|
+
interface DraftSpec {
|
|
49
|
+
title: string;
|
|
50
|
+
lane: string;
|
|
51
|
+
description: string;
|
|
52
|
+
acceptance: string[];
|
|
53
|
+
source_incidents: string[];
|
|
54
|
+
pattern_metadata: {
|
|
55
|
+
frequency: number | undefined;
|
|
56
|
+
category: string | undefined;
|
|
57
|
+
score: number | undefined;
|
|
58
|
+
firstSeen: string | undefined;
|
|
59
|
+
lastSeen: string | undefined;
|
|
60
|
+
};
|
|
61
|
+
filePath?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Options for generating a draft
|
|
65
|
+
*/
|
|
66
|
+
interface GenerateDraftOptions {
|
|
67
|
+
writeFile?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Generate draft WU spec from a pattern
|
|
71
|
+
*
|
|
72
|
+
* @param baseDir - Base directory
|
|
73
|
+
* @param pattern - Pattern from feedback:review
|
|
74
|
+
* @param options - Options
|
|
75
|
+
* @returns Draft WU spec
|
|
76
|
+
*/
|
|
77
|
+
export declare function generateDraft(baseDir: string, pattern: Pattern, options?: GenerateDraftOptions): Promise<DraftSpec>;
|
|
78
|
+
/**
|
|
79
|
+
* Load all draft files from .beacon/feedback-drafts/
|
|
80
|
+
*
|
|
81
|
+
* @param baseDir - Base directory
|
|
82
|
+
* @returns Array of draft objects with filePath
|
|
83
|
+
*/
|
|
84
|
+
export declare function loadDrafts(baseDir: string): Promise<DraftSpec[]>;
|
|
85
|
+
/**
|
|
86
|
+
* Options for promoting a draft
|
|
87
|
+
*/
|
|
88
|
+
interface PromoteDraftOptions {
|
|
89
|
+
dryRun?: boolean;
|
|
90
|
+
wuIdOverride?: string;
|
|
91
|
+
removeDraft?: boolean;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Result of promoting a draft
|
|
95
|
+
*/
|
|
96
|
+
interface PromoteDraftResult {
|
|
97
|
+
success: boolean;
|
|
98
|
+
wuId: string;
|
|
99
|
+
command: string;
|
|
100
|
+
draft?: DraftSpec;
|
|
101
|
+
error?: string;
|
|
102
|
+
draftRemoved?: boolean;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Promote a draft to a WU via wu:create
|
|
106
|
+
*
|
|
107
|
+
* @param baseDir - Base directory
|
|
108
|
+
* @param draft - Draft object
|
|
109
|
+
* @param options - Options
|
|
110
|
+
* @returns Result with success, wuId, command
|
|
111
|
+
*/
|
|
112
|
+
export declare function promoteDraft(baseDir: string, draft: DraftSpec, options?: PromoteDraftOptions): Promise<PromoteDraftResult>;
|
|
113
|
+
/**
|
|
114
|
+
* Feedback index entry
|
|
115
|
+
*/
|
|
116
|
+
interface FeedbackIndexEntry {
|
|
117
|
+
incident_id: string;
|
|
118
|
+
wu_id: string;
|
|
119
|
+
status: string;
|
|
120
|
+
timestamp: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Update feedback index with incident-to-WU mappings
|
|
124
|
+
*
|
|
125
|
+
* @param baseDir - Base directory
|
|
126
|
+
* @param wuId - WU ID
|
|
127
|
+
* @param incidentIds - Array of incident IDs
|
|
128
|
+
*/
|
|
129
|
+
export declare function updateFeedbackIndex(baseDir: string, wuId: string, incidentIds: string[]): Promise<void>;
|
|
130
|
+
/**
|
|
131
|
+
* Load feedback index entries
|
|
132
|
+
*
|
|
133
|
+
* @param baseDir - Base directory
|
|
134
|
+
* @returns Array of index entries
|
|
135
|
+
*/
|
|
136
|
+
export declare function loadFeedbackIndex(baseDir: string): Promise<FeedbackIndexEntry[]>;
|
|
137
|
+
export {};
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Promote Core Logic (WU-1599)
|
|
3
|
+
*
|
|
4
|
+
* Generates draft WU specs from feedback patterns and promotes them to actual WUs.
|
|
5
|
+
* Tracks incident-to-WU mappings in feedback-index.ndjson.
|
|
6
|
+
*
|
|
7
|
+
* @see {@link tools/__tests__/feedback-promote.test.mjs} - Tests
|
|
8
|
+
* @see {@link tools/feedback-promote.mjs} - CLI entry point
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { stringifyYAML, parseYAML } from '@lumenflow/core/lib/wu-yaml.js';
|
|
13
|
+
/**
|
|
14
|
+
* Directory for draft WU specs
|
|
15
|
+
*/
|
|
16
|
+
export const DRAFT_DIRECTORY = '.beacon/feedback-drafts';
|
|
17
|
+
/**
|
|
18
|
+
* Path to feedback index (incident-to-WU mappings)
|
|
19
|
+
*/
|
|
20
|
+
export const FEEDBACK_INDEX_PATH = '.beacon/feedback-index.ndjson';
|
|
21
|
+
/**
|
|
22
|
+
* Feedback index entry status
|
|
23
|
+
*/
|
|
24
|
+
export const FEEDBACK_STATUS = {
|
|
25
|
+
PENDING_RESOLUTION: 'pending_resolution',
|
|
26
|
+
RESOLVED: 'resolved',
|
|
27
|
+
WONT_FIX: 'wont_fix',
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Lane constants for DRY compliance
|
|
31
|
+
*/
|
|
32
|
+
const LANES = {
|
|
33
|
+
OPERATIONS_TOOLING: 'Operations: Tooling',
|
|
34
|
+
OPERATIONS_DOCUMENTATION: 'Operations: Documentation',
|
|
35
|
+
OPERATIONS_SECURITY: 'Operations: Security',
|
|
36
|
+
OPERATIONS_COMPLIANCE: 'Operations: Compliance',
|
|
37
|
+
OPERATIONS: 'Operations',
|
|
38
|
+
CORE_SYSTEMS: 'Core Systems',
|
|
39
|
+
EXPERIENCE: 'Experience',
|
|
40
|
+
INTELLIGENCE: 'Intelligence',
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Lane inference mapping from category to suggested lane
|
|
44
|
+
*/
|
|
45
|
+
const CATEGORY_TO_LANE = {
|
|
46
|
+
test: LANES.OPERATIONS_TOOLING,
|
|
47
|
+
tooling: LANES.OPERATIONS_TOOLING,
|
|
48
|
+
docs: LANES.OPERATIONS_DOCUMENTATION,
|
|
49
|
+
documentation: LANES.OPERATIONS_DOCUMENTATION,
|
|
50
|
+
infrastructure: LANES.CORE_SYSTEMS,
|
|
51
|
+
database: LANES.CORE_SYSTEMS,
|
|
52
|
+
api: LANES.CORE_SYSTEMS,
|
|
53
|
+
ui: LANES.EXPERIENCE,
|
|
54
|
+
frontend: LANES.EXPERIENCE,
|
|
55
|
+
ux: LANES.EXPERIENCE,
|
|
56
|
+
llm: LANES.INTELLIGENCE,
|
|
57
|
+
prompt: LANES.INTELLIGENCE,
|
|
58
|
+
ai: LANES.INTELLIGENCE,
|
|
59
|
+
security: LANES.OPERATIONS_SECURITY,
|
|
60
|
+
compliance: LANES.OPERATIONS_COMPLIANCE,
|
|
61
|
+
uncategorized: LANES.OPERATIONS,
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Infer lane from pattern category
|
|
65
|
+
*
|
|
66
|
+
* @param category - Pattern category
|
|
67
|
+
* @returns Suggested lane
|
|
68
|
+
*/
|
|
69
|
+
function inferLane(category) {
|
|
70
|
+
const normalizedCategory = (category ?? 'uncategorized').toLowerCase();
|
|
71
|
+
// Safe lookup using Object.hasOwn to prevent prototype pollution
|
|
72
|
+
if (Object.hasOwn(CATEGORY_TO_LANE, normalizedCategory)) {
|
|
73
|
+
// eslint-disable-next-line security/detect-object-injection -- Safe: hasOwn validates key exists
|
|
74
|
+
return CATEGORY_TO_LANE[normalizedCategory];
|
|
75
|
+
}
|
|
76
|
+
return CATEGORY_TO_LANE.uncategorized;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Generate description from pattern
|
|
80
|
+
*
|
|
81
|
+
* @param pattern - Pattern object
|
|
82
|
+
* @returns Description with Context/Problem/Solution structure
|
|
83
|
+
*/
|
|
84
|
+
function generateDescription(pattern) {
|
|
85
|
+
const frequency = pattern.frequency ?? 1;
|
|
86
|
+
const category = pattern.category ?? 'uncategorized';
|
|
87
|
+
const firstSeen = pattern.firstSeen
|
|
88
|
+
? new Date(pattern.firstSeen).toISOString().slice(0, 10)
|
|
89
|
+
: 'unknown';
|
|
90
|
+
const lastSeen = pattern.lastSeen
|
|
91
|
+
? new Date(pattern.lastSeen).toISOString().slice(0, 10)
|
|
92
|
+
: 'unknown';
|
|
93
|
+
return [
|
|
94
|
+
`Context: Pattern detected from ${frequency} incident(s) in category "${category}". First seen: ${firstSeen}, last seen: ${lastSeen}.`,
|
|
95
|
+
'',
|
|
96
|
+
`Problem: ${pattern.title}`,
|
|
97
|
+
'',
|
|
98
|
+
'Solution: [To be defined by implementer]',
|
|
99
|
+
].join('\n');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Generate acceptance criteria from pattern
|
|
103
|
+
*
|
|
104
|
+
* @param pattern - Pattern object
|
|
105
|
+
* @returns Acceptance criteria
|
|
106
|
+
*/
|
|
107
|
+
function generateAcceptance(pattern) {
|
|
108
|
+
return [
|
|
109
|
+
'Root cause identified and documented',
|
|
110
|
+
'Fix implemented',
|
|
111
|
+
'Tests pass (pnpm gates)',
|
|
112
|
+
`No recurrence of "${pattern.title}" pattern`,
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Generate draft WU spec from a pattern
|
|
117
|
+
*
|
|
118
|
+
* @param baseDir - Base directory
|
|
119
|
+
* @param pattern - Pattern from feedback:review
|
|
120
|
+
* @param options - Options
|
|
121
|
+
* @returns Draft WU spec
|
|
122
|
+
*/
|
|
123
|
+
export async function generateDraft(baseDir, pattern, options = {}) {
|
|
124
|
+
const { writeFile: shouldWrite = false } = options;
|
|
125
|
+
const draft = {
|
|
126
|
+
title: pattern.title,
|
|
127
|
+
lane: inferLane(pattern.category),
|
|
128
|
+
description: generateDescription(pattern),
|
|
129
|
+
acceptance: generateAcceptance(pattern),
|
|
130
|
+
source_incidents: (pattern.examples ?? []).map((e) => e.id),
|
|
131
|
+
pattern_metadata: {
|
|
132
|
+
frequency: pattern.frequency,
|
|
133
|
+
category: pattern.category,
|
|
134
|
+
score: pattern.score,
|
|
135
|
+
firstSeen: pattern.firstSeen,
|
|
136
|
+
lastSeen: pattern.lastSeen,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
if (shouldWrite) {
|
|
140
|
+
const draftsDir = path.join(baseDir, DRAFT_DIRECTORY);
|
|
141
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
|
|
142
|
+
await fs.mkdir(draftsDir, { recursive: true });
|
|
143
|
+
const timestamp = Date.now();
|
|
144
|
+
const filename = `draft-${timestamp}.yaml`;
|
|
145
|
+
const filePath = path.join(draftsDir, filename);
|
|
146
|
+
const yamlContent = stringifyYAML(draft, { lineWidth: 100 });
|
|
147
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes draft file
|
|
148
|
+
await fs.writeFile(filePath, yamlContent, 'utf8');
|
|
149
|
+
draft.filePath = path.join(DRAFT_DIRECTORY, filename);
|
|
150
|
+
}
|
|
151
|
+
return draft;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Load all draft files from .beacon/feedback-drafts/
|
|
155
|
+
*
|
|
156
|
+
* @param baseDir - Base directory
|
|
157
|
+
* @returns Array of draft objects with filePath
|
|
158
|
+
*/
|
|
159
|
+
export async function loadDrafts(baseDir) {
|
|
160
|
+
const draftsDir = path.join(baseDir, DRAFT_DIRECTORY);
|
|
161
|
+
let files;
|
|
162
|
+
try {
|
|
163
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known directory
|
|
164
|
+
files = await fs.readdir(draftsDir);
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
const yamlFiles = files.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'));
|
|
173
|
+
const drafts = [];
|
|
174
|
+
for (const file of yamlFiles) {
|
|
175
|
+
const filePath = path.join(draftsDir, file);
|
|
176
|
+
try {
|
|
177
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads draft files
|
|
178
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
179
|
+
// Parse YAML or JSON content
|
|
180
|
+
let draft;
|
|
181
|
+
try {
|
|
182
|
+
// Try JSON first (for test files that might use JSON.stringify)
|
|
183
|
+
draft = JSON.parse(content);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Fall back to YAML parsing
|
|
187
|
+
draft = parseYAML(content);
|
|
188
|
+
}
|
|
189
|
+
draft.filePath = path.join(DRAFT_DIRECTORY, file);
|
|
190
|
+
drafts.push(draft);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
// Skip malformed files
|
|
194
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
195
|
+
console.warn(`Warning: Could not load draft ${file}: ${errorMessage}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return drafts;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Promote a draft to a WU via wu:create
|
|
202
|
+
*
|
|
203
|
+
* @param baseDir - Base directory
|
|
204
|
+
* @param draft - Draft object
|
|
205
|
+
* @param options - Options
|
|
206
|
+
* @returns Result with success, wuId, command
|
|
207
|
+
*/
|
|
208
|
+
export async function promoteDraft(baseDir, draft, options = {}) {
|
|
209
|
+
const { dryRun = false, wuIdOverride, removeDraft = false } = options;
|
|
210
|
+
// Generate WU ID if not provided
|
|
211
|
+
const wuId = wuIdOverride ?? `WU-${Date.now()}`;
|
|
212
|
+
// Build wu:create command
|
|
213
|
+
const command = buildWuCreateCommand(wuId, draft);
|
|
214
|
+
const result = {
|
|
215
|
+
success: true,
|
|
216
|
+
wuId,
|
|
217
|
+
command,
|
|
218
|
+
draft,
|
|
219
|
+
};
|
|
220
|
+
if (!dryRun) {
|
|
221
|
+
// Execute wu:create command
|
|
222
|
+
const { execSync } = await import('node:child_process');
|
|
223
|
+
try {
|
|
224
|
+
execSync(command, { cwd: baseDir, stdio: 'pipe' });
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
wuId,
|
|
231
|
+
command,
|
|
232
|
+
error: errorMessage,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
// Update feedback index with incident mappings
|
|
236
|
+
if (draft.source_incidents && draft.source_incidents.length > 0) {
|
|
237
|
+
await updateFeedbackIndex(baseDir, wuId, draft.source_incidents);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Remove draft file if requested
|
|
241
|
+
if (removeDraft && draft.filePath) {
|
|
242
|
+
const absolutePath = draft.filePath.startsWith('/')
|
|
243
|
+
? draft.filePath
|
|
244
|
+
: path.join(baseDir, draft.filePath);
|
|
245
|
+
try {
|
|
246
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool removes draft file
|
|
247
|
+
await fs.unlink(absolutePath);
|
|
248
|
+
result.draftRemoved = true;
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
// Ignore errors if file doesn't exist
|
|
252
|
+
if (err instanceof Error && 'code' in err && err.code !== 'ENOENT') {
|
|
253
|
+
console.warn(`Warning: Could not remove draft ${draft.filePath}: ${err.message}`);
|
|
254
|
+
}
|
|
255
|
+
result.draftRemoved = true; // Mark as removed even if it didn't exist
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Build wu:create command from draft
|
|
262
|
+
*
|
|
263
|
+
* @param wuId - WU ID
|
|
264
|
+
* @param draft - Draft object
|
|
265
|
+
* @returns Command string
|
|
266
|
+
*/
|
|
267
|
+
function buildWuCreateCommand(wuId, draft) {
|
|
268
|
+
const parts = ['pnpm wu:create'];
|
|
269
|
+
parts.push(`--id ${wuId}`);
|
|
270
|
+
parts.push(`--lane "${draft.lane}"`);
|
|
271
|
+
parts.push(`--title "${draft.title.replace(/"/g, '\\"')}"`);
|
|
272
|
+
if (draft.description) {
|
|
273
|
+
parts.push(`--description "${draft.description.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`);
|
|
274
|
+
}
|
|
275
|
+
if (draft.acceptance && draft.acceptance.length > 0) {
|
|
276
|
+
for (const criterion of draft.acceptance) {
|
|
277
|
+
parts.push(`--acceptance "${criterion.replace(/"/g, '\\"')}"`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return parts.join(' ');
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Update feedback index with incident-to-WU mappings
|
|
284
|
+
*
|
|
285
|
+
* @param baseDir - Base directory
|
|
286
|
+
* @param wuId - WU ID
|
|
287
|
+
* @param incidentIds - Array of incident IDs
|
|
288
|
+
*/
|
|
289
|
+
export async function updateFeedbackIndex(baseDir, wuId, incidentIds) {
|
|
290
|
+
const indexPath = path.join(baseDir, FEEDBACK_INDEX_PATH);
|
|
291
|
+
const timestamp = new Date().toISOString();
|
|
292
|
+
// Ensure directory exists
|
|
293
|
+
const indexDir = path.dirname(indexPath);
|
|
294
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
|
|
295
|
+
await fs.mkdir(indexDir, { recursive: true });
|
|
296
|
+
// Build NDJSON entries
|
|
297
|
+
const entries = incidentIds.map((incidentId) => ({
|
|
298
|
+
incident_id: incidentId,
|
|
299
|
+
wu_id: wuId,
|
|
300
|
+
status: FEEDBACK_STATUS.PENDING_RESOLUTION,
|
|
301
|
+
timestamp,
|
|
302
|
+
}));
|
|
303
|
+
const content = `${entries.map((e) => JSON.stringify(e)).join('\n')}\n`;
|
|
304
|
+
// Append to index file
|
|
305
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes index file
|
|
306
|
+
await fs.appendFile(indexPath, content, 'utf8');
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Load feedback index entries
|
|
310
|
+
*
|
|
311
|
+
* @param baseDir - Base directory
|
|
312
|
+
* @returns Array of index entries
|
|
313
|
+
*/
|
|
314
|
+
export async function loadFeedbackIndex(baseDir) {
|
|
315
|
+
const indexPath = path.join(baseDir, FEEDBACK_INDEX_PATH);
|
|
316
|
+
try {
|
|
317
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads index file
|
|
318
|
+
const content = await fs.readFile(indexPath, 'utf8');
|
|
319
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
320
|
+
return lines
|
|
321
|
+
.map((line) => {
|
|
322
|
+
try {
|
|
323
|
+
return JSON.parse(line);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
.filter((entry) => entry !== null);
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Review Core Logic (WU-1598)
|
|
3
|
+
*
|
|
4
|
+
* Aggregates .beacon/incidents/*.ndjson and .beacon/memory/memory.jsonl,
|
|
5
|
+
* clusters by title similarity, scores patterns (frequency x severity x recency),
|
|
6
|
+
* and outputs prioritised patterns for human review.
|
|
7
|
+
*
|
|
8
|
+
* @see {@link tools/__tests__/feedback-review.test.mjs} - Tests
|
|
9
|
+
* @see {@link tools/feedback-review.mjs} - CLI entry point
|
|
10
|
+
*/
|
|
11
|
+
import { INCIDENT_SEVERITY } from '@lumenflow/core/lib/wu-constants.js';
|
|
12
|
+
/**
|
|
13
|
+
* Severity weights for scoring
|
|
14
|
+
*
|
|
15
|
+
* Higher severity = higher weight in scoring formula
|
|
16
|
+
*/
|
|
17
|
+
export declare const SEVERITY_WEIGHTS: {
|
|
18
|
+
[INCIDENT_SEVERITY.BLOCKER]: number;
|
|
19
|
+
[INCIDENT_SEVERITY.MAJOR]: number;
|
|
20
|
+
[INCIDENT_SEVERITY.MINOR]: number;
|
|
21
|
+
[INCIDENT_SEVERITY.INFO]: number;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Node with ID and optional title/content
|
|
25
|
+
*/
|
|
26
|
+
interface NodeWithTitle {
|
|
27
|
+
id: string;
|
|
28
|
+
title?: string;
|
|
29
|
+
content?: string;
|
|
30
|
+
category?: string;
|
|
31
|
+
severity?: string;
|
|
32
|
+
created_at?: string;
|
|
33
|
+
source?: string;
|
|
34
|
+
metadata?: Record<string, unknown>;
|
|
35
|
+
type?: string;
|
|
36
|
+
tags?: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Cluster of nodes grouped by title similarity
|
|
40
|
+
*/
|
|
41
|
+
interface Cluster {
|
|
42
|
+
title: string;
|
|
43
|
+
nodes: NodeWithTitle[];
|
|
44
|
+
category: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Cluster nodes by title similarity
|
|
48
|
+
*
|
|
49
|
+
* Uses simple greedy clustering with Jaccard similarity.
|
|
50
|
+
*
|
|
51
|
+
* @param nodes - Nodes to cluster
|
|
52
|
+
* @param threshold - Similarity threshold
|
|
53
|
+
* @returns Array of cluster objects
|
|
54
|
+
*/
|
|
55
|
+
export declare function clusterByTitle(nodes: NodeWithTitle[], threshold?: number): Cluster[];
|
|
56
|
+
/**
|
|
57
|
+
* Score a pattern cluster
|
|
58
|
+
*
|
|
59
|
+
* Formula: frequency x average_severity x recency_factor
|
|
60
|
+
*
|
|
61
|
+
* @param cluster - Cluster with nodes
|
|
62
|
+
* @returns Score value
|
|
63
|
+
*/
|
|
64
|
+
export declare function scorePattern(cluster: Cluster): number;
|
|
65
|
+
/**
|
|
66
|
+
* Options for reviewing feedback
|
|
67
|
+
*/
|
|
68
|
+
interface ReviewOptions {
|
|
69
|
+
since?: string;
|
|
70
|
+
minFrequency?: number;
|
|
71
|
+
category?: string;
|
|
72
|
+
json?: boolean;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Pattern example for output
|
|
76
|
+
*/
|
|
77
|
+
interface PatternExample {
|
|
78
|
+
id: string;
|
|
79
|
+
severity: string | undefined;
|
|
80
|
+
source: string | undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Pattern in review result
|
|
84
|
+
*/
|
|
85
|
+
interface ReviewPattern {
|
|
86
|
+
title: string;
|
|
87
|
+
frequency: number;
|
|
88
|
+
category: string;
|
|
89
|
+
score: number;
|
|
90
|
+
firstSeen: string | undefined;
|
|
91
|
+
lastSeen: string | undefined;
|
|
92
|
+
examples: PatternExample[];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Review result
|
|
96
|
+
*/
|
|
97
|
+
interface ReviewResult {
|
|
98
|
+
success: boolean;
|
|
99
|
+
patterns: ReviewPattern[];
|
|
100
|
+
summary: {
|
|
101
|
+
totalNodes: number;
|
|
102
|
+
totalClusters: number;
|
|
103
|
+
topCategory: string | null;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Review feedback from incidents and memory nodes
|
|
108
|
+
*
|
|
109
|
+
* Main entry point for feedback review logic.
|
|
110
|
+
*
|
|
111
|
+
* @param baseDir - Base directory containing .beacon
|
|
112
|
+
* @param options - Review options
|
|
113
|
+
* @returns Review result
|
|
114
|
+
*/
|
|
115
|
+
export declare function reviewFeedback(baseDir: string, options?: ReviewOptions): Promise<ReviewResult>;
|
|
116
|
+
export {};
|