@dmsdc-ai/aigentry-deliberation 0.0.26 → 0.0.28
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/decision-engine.js +1006 -0
- package/doctor.js +39 -39
- package/i18n.js +40 -0
- package/index.js +283 -283
- package/install.js +50 -50
- package/package.json +3 -1
- package/selectors/role-presets.json +6 -6
- package/selectors/roles/critic.md +9 -9
- package/selectors/roles/free.md +1 -1
- package/selectors/roles/implementer.md +9 -9
- package/selectors/roles/mediator.md +9 -9
- package/selectors/roles/researcher.md +9 -9
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision Engine — Stage-based state machine for structured multi-AI decision making.
|
|
3
|
+
*
|
|
4
|
+
* Implements a pipeline: intake → parallel_opinions → conflict_map → user_probe → synthesis → action_export → done
|
|
5
|
+
*
|
|
6
|
+
* Each stage enforces strict transitions. Multiple LLMs give independent parallel opinions,
|
|
7
|
+
* conflicts are mapped via MCDA score divergence, and the user resolves them before synthesis.
|
|
8
|
+
*
|
|
9
|
+
* Pure ESM, no external dependencies (Node.js built-in only: fs, path, crypto).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
// ── Constants ────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Ordered stages of a decision session.
|
|
21
|
+
* @type {readonly string[]}
|
|
22
|
+
*/
|
|
23
|
+
export const DECISION_STAGES = [
|
|
24
|
+
"intake", // Problem definition, options, criteria collection
|
|
25
|
+
"parallel_opinions", // All LLMs give independent opinions (no cross-visibility)
|
|
26
|
+
"conflict_map", // Extract disagreements between opinions
|
|
27
|
+
"user_probe", // Present conflicts to user, pause for input
|
|
28
|
+
"synthesis", // Combine user input + opinions into final decision
|
|
29
|
+
"action_export", // Convert decision to actionable output
|
|
30
|
+
"done",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Valid stage transitions. Each key maps to the single allowed next stage.
|
|
35
|
+
* @type {Record<string, string>}
|
|
36
|
+
*/
|
|
37
|
+
export const STAGE_TRANSITIONS = {
|
|
38
|
+
intake: "parallel_opinions",
|
|
39
|
+
parallel_opinions: "conflict_map",
|
|
40
|
+
conflict_map: "user_probe",
|
|
41
|
+
user_probe: "synthesis",
|
|
42
|
+
synthesis: "action_export",
|
|
43
|
+
action_export: "done",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Score divergence threshold to flag a criterion as a conflict (1-10 scale). */
|
|
47
|
+
const CONFLICT_DIVERGENCE_THRESHOLD = 3;
|
|
48
|
+
|
|
49
|
+
/** Maximum number of conflicts surfaced to the user to avoid overwhelm. */
|
|
50
|
+
const MAX_CONFLICTS = 5;
|
|
51
|
+
|
|
52
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generate a slug from text: lowercase, strip non-alphanumeric, collapse dashes, truncate.
|
|
58
|
+
* @param {string} text
|
|
59
|
+
* @param {number} [maxLen=48]
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
function slugify(text, maxLen = 48) {
|
|
63
|
+
return (text || "decision")
|
|
64
|
+
.toLowerCase()
|
|
65
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
66
|
+
.replace(/^-+|-+$/g, "")
|
|
67
|
+
.slice(0, maxLen) || "decision";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* ISO timestamp string.
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
function now() {
|
|
75
|
+
return new Date().toISOString();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Core API ─────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @typedef {Object} ModelOpinion
|
|
82
|
+
* @property {string} speaker - LLM identifier
|
|
83
|
+
* @property {string} summary - 1-line conclusion
|
|
84
|
+
* @property {string} reasoning - Full reasoning text
|
|
85
|
+
* @property {Record<string, number>} scores - MCDA scores per criterion (1-10)
|
|
86
|
+
* @property {string} recommendation - Which option the model recommends
|
|
87
|
+
* @property {number} confidence - 0-1 confidence value
|
|
88
|
+
* @property {string} timestamp - ISO timestamp
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @typedef {Object} ConflictItem
|
|
93
|
+
* @property {string} id - conflict-{index}
|
|
94
|
+
* @property {string} criterion - Which criterion the conflict is about
|
|
95
|
+
* @property {Record<string, string>} positions - speaker -> their position text
|
|
96
|
+
* @property {Record<string, number>} scores - speaker -> their score
|
|
97
|
+
* @property {number} divergence - Score spread (max - min)
|
|
98
|
+
* @property {string} question - Auto-generated clarifying question for user
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @typedef {Object} ActionPlan
|
|
103
|
+
* @property {string} decision - The final decision
|
|
104
|
+
* @property {string} rationale - Why this decision was made
|
|
105
|
+
* @property {Array<{id: string, title: string, description: string, priority: string}>} actionItems
|
|
106
|
+
* @property {Array<{description: string, mitigation: string, probability: string}>} risks
|
|
107
|
+
* @property {{checklist: string, githubIssue: string}} exportFormats
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @typedef {Object} DecisionSession
|
|
112
|
+
* @property {string} id
|
|
113
|
+
* @property {"decision"} type
|
|
114
|
+
* @property {string} stage
|
|
115
|
+
* @property {"active"|"completed"|"cancelled"} status
|
|
116
|
+
* @property {string} problem
|
|
117
|
+
* @property {string[]} options
|
|
118
|
+
* @property {string[]} criteria
|
|
119
|
+
* @property {string|null} template
|
|
120
|
+
* @property {string[]} speakers
|
|
121
|
+
* @property {Array} participant_profiles
|
|
122
|
+
* @property {Record<string, ModelOpinion>} opinions
|
|
123
|
+
* @property {ConflictItem[]} conflicts
|
|
124
|
+
* @property {Array} userProbeResponses
|
|
125
|
+
* @property {string|null} synthesis
|
|
126
|
+
* @property {ActionPlan|null} actionPlan
|
|
127
|
+
* @property {Array} log
|
|
128
|
+
* @property {{created: string, updated: string, participants: number, template: string|null}} metadata
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create a new decision session.
|
|
133
|
+
*
|
|
134
|
+
* @param {Object} params
|
|
135
|
+
* @param {string} params.problem - The decision question
|
|
136
|
+
* @param {string[]} [params.options] - Available choices (can be empty)
|
|
137
|
+
* @param {string[]} [params.criteria] - Evaluation criteria
|
|
138
|
+
* @param {string[]} params.speakers - Participating LLMs
|
|
139
|
+
* @param {string|null} [params.template] - Template name if used
|
|
140
|
+
* @param {Array} [params.participant_profiles] - Same format as deliberation
|
|
141
|
+
* @returns {DecisionSession}
|
|
142
|
+
*/
|
|
143
|
+
export function createDecisionSession({
|
|
144
|
+
problem,
|
|
145
|
+
options = [],
|
|
146
|
+
criteria = [],
|
|
147
|
+
speakers = [],
|
|
148
|
+
template = null,
|
|
149
|
+
participant_profiles = [],
|
|
150
|
+
}) {
|
|
151
|
+
if (!problem || typeof problem !== "string") {
|
|
152
|
+
throw new Error("problem is required and must be a non-empty string");
|
|
153
|
+
}
|
|
154
|
+
if (!Array.isArray(speakers) || speakers.length === 0) {
|
|
155
|
+
throw new Error("speakers must be a non-empty array");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const id = `decision-${slugify(problem)}-${randomUUID().slice(0, 8)}`;
|
|
159
|
+
const timestamp = now();
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
id,
|
|
163
|
+
type: "decision",
|
|
164
|
+
stage: "intake",
|
|
165
|
+
status: "active",
|
|
166
|
+
problem,
|
|
167
|
+
options: Array.isArray(options) ? [...options] : [],
|
|
168
|
+
criteria: Array.isArray(criteria) ? [...criteria] : [],
|
|
169
|
+
template: template || null,
|
|
170
|
+
speakers: [...speakers],
|
|
171
|
+
participant_profiles: Array.isArray(participant_profiles) ? [...participant_profiles] : [],
|
|
172
|
+
opinions: {},
|
|
173
|
+
conflicts: [],
|
|
174
|
+
userProbeResponses: [],
|
|
175
|
+
synthesis: null,
|
|
176
|
+
actionPlan: null,
|
|
177
|
+
log: [],
|
|
178
|
+
metadata: {
|
|
179
|
+
created: timestamp,
|
|
180
|
+
updated: timestamp,
|
|
181
|
+
participants: speakers.length,
|
|
182
|
+
template: template || null,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Advance a decision session to the next stage.
|
|
189
|
+
*
|
|
190
|
+
* Validates the current stage has a valid transition, updates the session in place,
|
|
191
|
+
* appends a log entry, and returns the session.
|
|
192
|
+
*
|
|
193
|
+
* @param {DecisionSession} session
|
|
194
|
+
* @returns {DecisionSession} The updated session
|
|
195
|
+
* @throws {Error} If current stage has no valid transition or session is not active
|
|
196
|
+
*/
|
|
197
|
+
export function advanceStage(session) {
|
|
198
|
+
if (!session || typeof session !== "object") {
|
|
199
|
+
throw new Error("session is required");
|
|
200
|
+
}
|
|
201
|
+
if (session.status !== "active") {
|
|
202
|
+
throw new Error(`Cannot advance: session status is "${session.status}", expected "active"`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const nextStage = STAGE_TRANSITIONS[session.stage];
|
|
206
|
+
if (!nextStage) {
|
|
207
|
+
throw new Error(`No valid transition from stage "${session.stage}"`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const prevStage = session.stage;
|
|
211
|
+
session.stage = nextStage;
|
|
212
|
+
session.metadata.updated = now();
|
|
213
|
+
|
|
214
|
+
if (nextStage === "done") {
|
|
215
|
+
session.status = "completed";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
session.log.push({
|
|
219
|
+
event: "stage_transition",
|
|
220
|
+
from: prevStage,
|
|
221
|
+
to: nextStage,
|
|
222
|
+
timestamp: now(),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return session;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Build a conflict map from model opinions and evaluation criteria.
|
|
230
|
+
*
|
|
231
|
+
* For each criterion, collects scores from all opinions, calculates divergence,
|
|
232
|
+
* and flags criteria with divergence >= CONFLICT_DIVERGENCE_THRESHOLD as conflicts.
|
|
233
|
+
* Returns top MAX_CONFLICTS conflicts sorted by divergence descending.
|
|
234
|
+
*
|
|
235
|
+
* @param {Record<string, ModelOpinion>} opinions - speaker -> opinion
|
|
236
|
+
* @param {string[]} criteria - List of evaluation criteria
|
|
237
|
+
* @returns {ConflictItem[]}
|
|
238
|
+
*/
|
|
239
|
+
export function buildConflictMap(opinions, criteria) {
|
|
240
|
+
if (!opinions || typeof opinions !== "object") return [];
|
|
241
|
+
if (!Array.isArray(criteria) || criteria.length === 0) return [];
|
|
242
|
+
|
|
243
|
+
const speakerNames = Object.keys(opinions);
|
|
244
|
+
if (speakerNames.length < 2) return [];
|
|
245
|
+
|
|
246
|
+
const conflicts = [];
|
|
247
|
+
|
|
248
|
+
for (const criterion of criteria) {
|
|
249
|
+
const scores = {};
|
|
250
|
+
const positions = {};
|
|
251
|
+
let hasScores = false;
|
|
252
|
+
|
|
253
|
+
for (const speaker of speakerNames) {
|
|
254
|
+
const opinion = opinions[speaker];
|
|
255
|
+
if (!opinion) continue;
|
|
256
|
+
|
|
257
|
+
// Collect score for this criterion
|
|
258
|
+
const score = opinion.scores?.[criterion];
|
|
259
|
+
if (typeof score === "number" && score >= 1 && score <= 10) {
|
|
260
|
+
scores[speaker] = score;
|
|
261
|
+
hasScores = true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Extract position text: use reasoning or summary as fallback
|
|
265
|
+
positions[speaker] = extractPositionForCriterion(opinion, criterion);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!hasScores) continue;
|
|
269
|
+
|
|
270
|
+
const scoreValues = Object.values(scores);
|
|
271
|
+
if (scoreValues.length < 2) continue;
|
|
272
|
+
|
|
273
|
+
const maxScore = Math.max(...scoreValues);
|
|
274
|
+
const minScore = Math.min(...scoreValues);
|
|
275
|
+
const divergence = maxScore - minScore;
|
|
276
|
+
|
|
277
|
+
if (divergence >= CONFLICT_DIVERGENCE_THRESHOLD) {
|
|
278
|
+
// Find the speakers at max and min for the question
|
|
279
|
+
const maxSpeaker = speakerNames.find(s => scores[s] === maxScore) || speakerNames[0];
|
|
280
|
+
const minSpeaker = speakerNames.find(s => scores[s] === minScore) || speakerNames[1];
|
|
281
|
+
|
|
282
|
+
const question =
|
|
283
|
+
`Models disagree on "${criterion}": ` +
|
|
284
|
+
`${maxSpeaker} says "${truncate(positions[maxSpeaker], 80)}" (score: ${scores[maxSpeaker]}), ` +
|
|
285
|
+
`${minSpeaker} says "${truncate(positions[minSpeaker], 80)}" (score: ${scores[minSpeaker]}). ` +
|
|
286
|
+
`Which perspective aligns more with your priorities?`;
|
|
287
|
+
|
|
288
|
+
conflicts.push({
|
|
289
|
+
id: `conflict-${conflicts.length}`,
|
|
290
|
+
criterion,
|
|
291
|
+
positions,
|
|
292
|
+
scores,
|
|
293
|
+
divergence,
|
|
294
|
+
question,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Sort by divergence descending, cap at MAX_CONFLICTS
|
|
300
|
+
conflicts.sort((a, b) => b.divergence - a.divergence);
|
|
301
|
+
const topConflicts = conflicts.slice(0, MAX_CONFLICTS);
|
|
302
|
+
|
|
303
|
+
// Re-index IDs after sorting/slicing
|
|
304
|
+
for (let i = 0; i < topConflicts.length; i++) {
|
|
305
|
+
topConflicts[i].id = `conflict-${i}`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return topConflicts;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Extract a speaker's position text for a given criterion from their opinion.
|
|
313
|
+
* Searches reasoning for a sentence mentioning the criterion, falls back to summary.
|
|
314
|
+
*
|
|
315
|
+
* @param {ModelOpinion} opinion
|
|
316
|
+
* @param {string} criterion
|
|
317
|
+
* @returns {string}
|
|
318
|
+
*/
|
|
319
|
+
function extractPositionForCriterion(opinion, criterion) {
|
|
320
|
+
if (!opinion) return "(no opinion)";
|
|
321
|
+
|
|
322
|
+
const reasoning = opinion.reasoning || "";
|
|
323
|
+
const criterionLower = criterion.toLowerCase();
|
|
324
|
+
|
|
325
|
+
// Try to find a sentence in reasoning that mentions the criterion
|
|
326
|
+
const sentences = reasoning.split(/[.!?]\s+/);
|
|
327
|
+
for (const sentence of sentences) {
|
|
328
|
+
if (sentence.toLowerCase().includes(criterionLower)) {
|
|
329
|
+
return sentence.trim();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Fall back to summary
|
|
334
|
+
return opinion.summary || opinion.recommendation || "(no position stated)";
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Truncate text to maxLen, appending "..." if truncated.
|
|
339
|
+
* @param {string} text
|
|
340
|
+
* @param {number} maxLen
|
|
341
|
+
* @returns {string}
|
|
342
|
+
*/
|
|
343
|
+
function truncate(text, maxLen) {
|
|
344
|
+
if (!text) return "";
|
|
345
|
+
if (text.length <= maxLen) return text;
|
|
346
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Parse a raw LLM response into a structured ModelOpinion.
|
|
351
|
+
*
|
|
352
|
+
* Handles both structured (markdown headers + score lines) and unstructured responses.
|
|
353
|
+
* Looks for "## Summary", "## Recommendation", score patterns "Criterion: N/10", confidence "Confidence: X%".
|
|
354
|
+
*
|
|
355
|
+
* @param {string} speaker - Speaker identifier
|
|
356
|
+
* @param {string} content - Raw LLM response text
|
|
357
|
+
* @param {string[]} criteria - Expected criteria list
|
|
358
|
+
* @returns {ModelOpinion}
|
|
359
|
+
*/
|
|
360
|
+
export function parseOpinionFromResponse(speaker, content, criteria) {
|
|
361
|
+
if (!content || typeof content !== "string") {
|
|
362
|
+
return {
|
|
363
|
+
speaker,
|
|
364
|
+
summary: "(empty response)",
|
|
365
|
+
reasoning: "",
|
|
366
|
+
scores: {},
|
|
367
|
+
recommendation: "",
|
|
368
|
+
confidence: 0.5,
|
|
369
|
+
timestamp: now(),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const lines = content.split("\n");
|
|
374
|
+
|
|
375
|
+
// Extract sections by markdown headers
|
|
376
|
+
const sections = {};
|
|
377
|
+
let currentSection = "__preamble";
|
|
378
|
+
const sectionLines = { __preamble: [] };
|
|
379
|
+
|
|
380
|
+
for (const line of lines) {
|
|
381
|
+
const headerMatch = line.match(/^#{1,3}\s+(.+)/);
|
|
382
|
+
if (headerMatch) {
|
|
383
|
+
currentSection = headerMatch[1].trim().toLowerCase();
|
|
384
|
+
sectionLines[currentSection] = [];
|
|
385
|
+
} else {
|
|
386
|
+
if (!sectionLines[currentSection]) sectionLines[currentSection] = [];
|
|
387
|
+
sectionLines[currentSection].push(line);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
for (const [key, value] of Object.entries(sectionLines)) {
|
|
392
|
+
sections[key] = value.join("\n").trim();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Extract summary
|
|
396
|
+
const summary =
|
|
397
|
+
sections["summary"] ||
|
|
398
|
+
sections["recommendation"] ||
|
|
399
|
+
sections["conclusion"] ||
|
|
400
|
+
extractFirstNonEmpty(lines, 120);
|
|
401
|
+
|
|
402
|
+
// Extract recommendation
|
|
403
|
+
const recommendation =
|
|
404
|
+
sections["recommendation"] ||
|
|
405
|
+
sections["decision"] ||
|
|
406
|
+
sections["conclusion"] ||
|
|
407
|
+
"";
|
|
408
|
+
|
|
409
|
+
// Extract reasoning
|
|
410
|
+
const reasoning =
|
|
411
|
+
sections["reasoning"] ||
|
|
412
|
+
sections["analysis"] ||
|
|
413
|
+
sections["rationale"] ||
|
|
414
|
+
content;
|
|
415
|
+
|
|
416
|
+
// Extract scores per criterion
|
|
417
|
+
const scores = {};
|
|
418
|
+
if (Array.isArray(criteria)) {
|
|
419
|
+
for (const criterion of criteria) {
|
|
420
|
+
const score = extractScoreForCriterion(content, criterion);
|
|
421
|
+
if (score !== null) {
|
|
422
|
+
scores[criterion] = score;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Extract confidence
|
|
428
|
+
const confidence = extractConfidence(content);
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
speaker,
|
|
432
|
+
summary: truncate(summary, 200),
|
|
433
|
+
reasoning,
|
|
434
|
+
scores,
|
|
435
|
+
recommendation: truncate(recommendation, 200),
|
|
436
|
+
confidence,
|
|
437
|
+
timestamp: now(),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Extract a numeric score (1-10) for a given criterion from text.
|
|
443
|
+
* Handles patterns: "Criterion: N/10", "Criterion: N", "- Criterion — N/10", etc.
|
|
444
|
+
*
|
|
445
|
+
* @param {string} text
|
|
446
|
+
* @param {string} criterion
|
|
447
|
+
* @returns {number|null}
|
|
448
|
+
*/
|
|
449
|
+
function extractScoreForCriterion(text, criterion) {
|
|
450
|
+
if (!text || !criterion) return null;
|
|
451
|
+
|
|
452
|
+
// Escape special regex chars in criterion name
|
|
453
|
+
const escaped = criterion.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
454
|
+
|
|
455
|
+
// Pattern variants:
|
|
456
|
+
// Criterion: 8/10
|
|
457
|
+
// Criterion: 8
|
|
458
|
+
// - Criterion — 8/10
|
|
459
|
+
// **Criterion**: 8
|
|
460
|
+
// | Criterion | 8 |
|
|
461
|
+
const patterns = [
|
|
462
|
+
new RegExp(`(?:^|[\\-*|])\\s*(?:\\*\\*)?${escaped}(?:\\*\\*)?\\s*[:—|]\\s*(\\d{1,2})(?:\\/10)?`, "im"),
|
|
463
|
+
new RegExp(`${escaped}[^\\d]{0,20}(\\d{1,2})(?:\\/10|\\s*(?:out of|of)\\s*10)?`, "im"),
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
for (const pattern of patterns) {
|
|
467
|
+
const match = text.match(pattern);
|
|
468
|
+
if (match) {
|
|
469
|
+
const num = parseInt(match[1], 10);
|
|
470
|
+
if (num >= 1 && num <= 10) return num;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Extract confidence value (0-1) from text.
|
|
479
|
+
* Handles: "Confidence: 85%", "Confidence: 0.85", "confidence level: high"
|
|
480
|
+
*
|
|
481
|
+
* @param {string} text
|
|
482
|
+
* @returns {number}
|
|
483
|
+
*/
|
|
484
|
+
function extractConfidence(text) {
|
|
485
|
+
if (!text) return 0.5;
|
|
486
|
+
|
|
487
|
+
// Percentage pattern: "Confidence: 85%" or "confidence level: 85%"
|
|
488
|
+
const pctMatch = text.match(/confidence[^:]*:\s*(\d{1,3})\s*%/i);
|
|
489
|
+
if (pctMatch) {
|
|
490
|
+
const pct = parseInt(pctMatch[1], 10);
|
|
491
|
+
if (pct >= 0 && pct <= 100) return pct / 100;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Decimal pattern: "Confidence: 0.85"
|
|
495
|
+
const decMatch = text.match(/confidence[^:]*:\s*(0?\.\d+|1\.0?)/i);
|
|
496
|
+
if (decMatch) {
|
|
497
|
+
const val = parseFloat(decMatch[1]);
|
|
498
|
+
if (val >= 0 && val <= 1) return val;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Keyword pattern
|
|
502
|
+
const lower = text.toLowerCase();
|
|
503
|
+
if (/confidence[^.]*\b(very\s+high|extremely\s+high)\b/i.test(lower)) return 0.95;
|
|
504
|
+
if (/confidence[^.]*\bhigh\b/i.test(lower)) return 0.85;
|
|
505
|
+
if (/confidence[^.]*\bmedium\b/i.test(lower)) return 0.65;
|
|
506
|
+
if (/confidence[^.]*\blow\b/i.test(lower)) return 0.35;
|
|
507
|
+
if (/confidence[^.]*\bvery\s+low\b/i.test(lower)) return 0.15;
|
|
508
|
+
|
|
509
|
+
return 0.5; // default mid-confidence
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Return the first non-empty, non-header line from an array, truncated.
|
|
514
|
+
* @param {string[]} lines
|
|
515
|
+
* @param {number} maxLen
|
|
516
|
+
* @returns {string}
|
|
517
|
+
*/
|
|
518
|
+
function extractFirstNonEmpty(lines, maxLen) {
|
|
519
|
+
for (const line of lines) {
|
|
520
|
+
const trimmed = line.trim();
|
|
521
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
522
|
+
return truncate(trimmed, maxLen);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return "";
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Build the prompt sent to each LLM for independent opinion extraction.
|
|
530
|
+
*
|
|
531
|
+
* The prompt clearly states the problem, lists options and criteria,
|
|
532
|
+
* and requests a structured response. Does NOT include any other model's opinion.
|
|
533
|
+
*
|
|
534
|
+
* @param {string} problem - The decision question
|
|
535
|
+
* @param {string[]} options - Available choices
|
|
536
|
+
* @param {string[]} criteria - Evaluation criteria
|
|
537
|
+
* @param {string|null} [template] - Template name (for context)
|
|
538
|
+
* @returns {string}
|
|
539
|
+
*/
|
|
540
|
+
export function buildOpinionPrompt(problem, options, criteria, template = null) {
|
|
541
|
+
const parts = [];
|
|
542
|
+
|
|
543
|
+
parts.push("You are participating in a structured decision-making process.");
|
|
544
|
+
parts.push("Give your INDEPENDENT opinion. Do NOT reference other models or prior opinions.\n");
|
|
545
|
+
|
|
546
|
+
parts.push(`## Decision Problem\n${problem}\n`);
|
|
547
|
+
|
|
548
|
+
if (Array.isArray(options) && options.length > 0) {
|
|
549
|
+
parts.push("## Available Options");
|
|
550
|
+
for (let i = 0; i < options.length; i++) {
|
|
551
|
+
parts.push(`${i + 1}. ${options[i]}`);
|
|
552
|
+
}
|
|
553
|
+
parts.push("");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (Array.isArray(criteria) && criteria.length > 0) {
|
|
557
|
+
parts.push("## Evaluation Criteria");
|
|
558
|
+
parts.push("Score each criterion from 1 (worst) to 10 (best) for your recommended option.\n");
|
|
559
|
+
for (const c of criteria) {
|
|
560
|
+
parts.push(`- ${c}`);
|
|
561
|
+
}
|
|
562
|
+
parts.push("");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (template) {
|
|
566
|
+
parts.push(`*Using decision template: ${template}*\n`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
parts.push("## Required Response Format\n");
|
|
570
|
+
parts.push("Please structure your response with these sections:\n");
|
|
571
|
+
parts.push("### Summary");
|
|
572
|
+
parts.push("(1-2 sentence conclusion)\n");
|
|
573
|
+
parts.push("### Recommendation");
|
|
574
|
+
parts.push("(Which option you recommend and why)\n");
|
|
575
|
+
|
|
576
|
+
if (Array.isArray(criteria) && criteria.length > 0) {
|
|
577
|
+
parts.push("### Scores");
|
|
578
|
+
for (const c of criteria) {
|
|
579
|
+
parts.push(`- ${c}: ?/10`);
|
|
580
|
+
}
|
|
581
|
+
parts.push("");
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
parts.push("### Reasoning");
|
|
585
|
+
parts.push("(Detailed analysis supporting your recommendation)\n");
|
|
586
|
+
parts.push("### Confidence");
|
|
587
|
+
parts.push("(Your confidence level as a percentage, e.g., Confidence: 80%)");
|
|
588
|
+
|
|
589
|
+
return parts.join("\n");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Generate formatted markdown presenting conflicts to the user with numbered questions.
|
|
594
|
+
*
|
|
595
|
+
* @param {ConflictItem[]} conflicts
|
|
596
|
+
* @returns {string}
|
|
597
|
+
*/
|
|
598
|
+
export function generateConflictQuestions(conflicts) {
|
|
599
|
+
if (!Array.isArray(conflicts) || conflicts.length === 0) {
|
|
600
|
+
return "No significant conflicts detected between model opinions. Proceeding to synthesis.";
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const parts = [];
|
|
604
|
+
parts.push("# Decision Conflicts Requiring Your Input\n");
|
|
605
|
+
parts.push(`The participating models disagree on **${conflicts.length}** key area${conflicts.length > 1 ? "s" : ""}.\n`);
|
|
606
|
+
parts.push("Please review each conflict and share your perspective:\n");
|
|
607
|
+
parts.push("---\n");
|
|
608
|
+
|
|
609
|
+
for (let i = 0; i < conflicts.length; i++) {
|
|
610
|
+
const c = conflicts[i];
|
|
611
|
+
parts.push(`## Conflict ${i + 1}: ${c.criterion}`);
|
|
612
|
+
parts.push(`**Divergence score:** ${c.divergence}/10\n`);
|
|
613
|
+
|
|
614
|
+
// Show each speaker's position and score
|
|
615
|
+
const speakers = Object.keys(c.scores);
|
|
616
|
+
for (const speaker of speakers) {
|
|
617
|
+
const score = c.scores[speaker];
|
|
618
|
+
const position = c.positions?.[speaker] || "(no position)";
|
|
619
|
+
parts.push(`- **${speaker}** (score: ${score}/10): ${truncate(position, 150)}`);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
parts.push("");
|
|
623
|
+
parts.push(`**Question ${i + 1}:** ${c.question}\n`);
|
|
624
|
+
parts.push("---\n");
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
parts.push("Please respond with your preference for each numbered question.");
|
|
628
|
+
|
|
629
|
+
return parts.join("\n");
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Build a synthesis report from a complete decision session.
|
|
634
|
+
*
|
|
635
|
+
* Combines opinions, conflicts, and user probe responses into
|
|
636
|
+
* an executive summary, criteria breakdown, and conflict resolution summary.
|
|
637
|
+
*
|
|
638
|
+
* @param {DecisionSession} session
|
|
639
|
+
* @returns {string} Markdown synthesis report
|
|
640
|
+
*/
|
|
641
|
+
export function buildSynthesis(session) {
|
|
642
|
+
if (!session || typeof session !== "object") {
|
|
643
|
+
return "Error: invalid session";
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const parts = [];
|
|
647
|
+
const opinions = session.opinions || {};
|
|
648
|
+
const conflicts = session.conflicts || [];
|
|
649
|
+
const userResponses = session.userProbeResponses || [];
|
|
650
|
+
const speakerNames = Object.keys(opinions);
|
|
651
|
+
|
|
652
|
+
// ── Executive Summary ──
|
|
653
|
+
parts.push("# Decision Synthesis Report\n");
|
|
654
|
+
parts.push(`**Problem:** ${session.problem}\n`);
|
|
655
|
+
|
|
656
|
+
// Determine consensus recommendation
|
|
657
|
+
const recommendationCounts = {};
|
|
658
|
+
for (const opinion of Object.values(opinions)) {
|
|
659
|
+
const rec = opinion.recommendation || "(none)";
|
|
660
|
+
recommendationCounts[rec] = (recommendationCounts[rec] || 0) + 1;
|
|
661
|
+
}
|
|
662
|
+
const sortedRecs = Object.entries(recommendationCounts).sort((a, b) => b[1] - a[1]);
|
|
663
|
+
const topRec = sortedRecs[0];
|
|
664
|
+
|
|
665
|
+
if (topRec) {
|
|
666
|
+
const unanimity = topRec[1] === speakerNames.length ? "unanimous" : `${topRec[1]}/${speakerNames.length}`;
|
|
667
|
+
parts.push(`**Consensus recommendation:** ${topRec[0]} (${unanimity} agreement)\n`);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Average confidence
|
|
671
|
+
const confidences = Object.values(opinions).map(o => o.confidence).filter(c => typeof c === "number");
|
|
672
|
+
if (confidences.length > 0) {
|
|
673
|
+
const avgConf = confidences.reduce((a, b) => a + b, 0) / confidences.length;
|
|
674
|
+
parts.push(`**Average confidence:** ${Math.round(avgConf * 100)}%\n`);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ── Individual Summaries ──
|
|
678
|
+
parts.push("## Model Opinions\n");
|
|
679
|
+
for (const speaker of speakerNames) {
|
|
680
|
+
const op = opinions[speaker];
|
|
681
|
+
parts.push(`### ${speaker}`);
|
|
682
|
+
parts.push(`- **Recommendation:** ${op.recommendation || "(none)"}`);
|
|
683
|
+
parts.push(`- **Summary:** ${op.summary || "(none)"}`);
|
|
684
|
+
parts.push(`- **Confidence:** ${Math.round((op.confidence || 0.5) * 100)}%`);
|
|
685
|
+
parts.push("");
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ── Criteria Breakdown Table ──
|
|
689
|
+
if (Array.isArray(session.criteria) && session.criteria.length > 0 && speakerNames.length > 0) {
|
|
690
|
+
parts.push("## Criteria Scores\n");
|
|
691
|
+
|
|
692
|
+
// Table header
|
|
693
|
+
const header = `| Criterion | ${speakerNames.join(" | ")} | Avg |`;
|
|
694
|
+
const separator = `|${"-".repeat(11)}|${speakerNames.map(() => "-".repeat(7)).join("|")}|${"-".repeat(6)}|`;
|
|
695
|
+
parts.push(header);
|
|
696
|
+
parts.push(separator);
|
|
697
|
+
|
|
698
|
+
for (const criterion of session.criteria) {
|
|
699
|
+
const scores = speakerNames.map(s => opinions[s]?.scores?.[criterion]);
|
|
700
|
+
const validScores = scores.filter(s => typeof s === "number");
|
|
701
|
+
const avg = validScores.length > 0
|
|
702
|
+
? (validScores.reduce((a, b) => a + b, 0) / validScores.length).toFixed(1)
|
|
703
|
+
: "—";
|
|
704
|
+
const row = `| ${truncate(criterion, 30)} | ${scores.map(s => (typeof s === "number" ? String(s) : "—")).join(" | ")} | ${avg} |`;
|
|
705
|
+
parts.push(row);
|
|
706
|
+
}
|
|
707
|
+
parts.push("");
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// ── Conflict Resolution ──
|
|
711
|
+
if (conflicts.length > 0) {
|
|
712
|
+
parts.push("## Conflict Resolution\n");
|
|
713
|
+
for (let i = 0; i < conflicts.length; i++) {
|
|
714
|
+
const c = conflicts[i];
|
|
715
|
+
const userResponse = userResponses[i] || "(no response)";
|
|
716
|
+
parts.push(`### ${c.criterion} (divergence: ${c.divergence})`);
|
|
717
|
+
parts.push(`- **User's resolution:** ${userResponse}`);
|
|
718
|
+
parts.push("");
|
|
719
|
+
}
|
|
720
|
+
} else {
|
|
721
|
+
parts.push("## Conflicts\n");
|
|
722
|
+
parts.push("No significant conflicts were detected.\n");
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return parts.join("\n");
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Convert a decision session's synthesis into an actionable plan.
|
|
730
|
+
*
|
|
731
|
+
* @param {DecisionSession} session
|
|
732
|
+
* @returns {ActionPlan}
|
|
733
|
+
*/
|
|
734
|
+
export function buildActionPlan(session) {
|
|
735
|
+
if (!session || typeof session !== "object") {
|
|
736
|
+
return {
|
|
737
|
+
decision: "(no session)",
|
|
738
|
+
rationale: "",
|
|
739
|
+
actionItems: [],
|
|
740
|
+
risks: [],
|
|
741
|
+
exportFormats: { checklist: "", githubIssue: "" },
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const opinions = session.opinions || {};
|
|
746
|
+
const speakerNames = Object.keys(opinions);
|
|
747
|
+
|
|
748
|
+
// Determine the winning recommendation
|
|
749
|
+
const recommendationCounts = {};
|
|
750
|
+
for (const opinion of Object.values(opinions)) {
|
|
751
|
+
const rec = opinion.recommendation || "";
|
|
752
|
+
if (rec) recommendationCounts[rec] = (recommendationCounts[rec] || 0) + 1;
|
|
753
|
+
}
|
|
754
|
+
const sortedRecs = Object.entries(recommendationCounts).sort((a, b) => b[1] - a[1]);
|
|
755
|
+
const decision = sortedRecs[0]?.[0] || session.problem;
|
|
756
|
+
|
|
757
|
+
// Build rationale from consensus + user input
|
|
758
|
+
const rationale = buildRationale(session, decision);
|
|
759
|
+
|
|
760
|
+
// Generate action items from the decision and criteria
|
|
761
|
+
const actionItems = generateActionItems(session, decision);
|
|
762
|
+
|
|
763
|
+
// Generate risks from low-scored criteria and conflicts
|
|
764
|
+
const risks = generateRisks(session);
|
|
765
|
+
|
|
766
|
+
// Export formats
|
|
767
|
+
const checklist = buildChecklist(decision, actionItems);
|
|
768
|
+
const githubIssue = buildGithubIssue(session, decision, rationale, actionItems, risks);
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
decision,
|
|
772
|
+
rationale,
|
|
773
|
+
actionItems,
|
|
774
|
+
risks,
|
|
775
|
+
exportFormats: {
|
|
776
|
+
checklist,
|
|
777
|
+
githubIssue,
|
|
778
|
+
},
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Build rationale text from session data.
|
|
784
|
+
* @param {DecisionSession} session
|
|
785
|
+
* @param {string} decision
|
|
786
|
+
* @returns {string}
|
|
787
|
+
*/
|
|
788
|
+
function buildRationale(session, decision) {
|
|
789
|
+
const parts = [];
|
|
790
|
+
const opinions = session.opinions || {};
|
|
791
|
+
const speakerNames = Object.keys(opinions);
|
|
792
|
+
|
|
793
|
+
// Count agreement
|
|
794
|
+
const agreeing = speakerNames.filter(s => {
|
|
795
|
+
const rec = opinions[s]?.recommendation || "";
|
|
796
|
+
return rec.toLowerCase().includes(decision.toLowerCase().slice(0, 20));
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
if (agreeing.length > 0) {
|
|
800
|
+
parts.push(`${agreeing.length}/${speakerNames.length} models recommended this approach.`);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// User resolutions
|
|
804
|
+
if (session.userProbeResponses && session.userProbeResponses.length > 0) {
|
|
805
|
+
parts.push("User input was incorporated for conflict resolution.");
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Average confidence
|
|
809
|
+
const confidences = Object.values(opinions).map(o => o.confidence).filter(c => typeof c === "number");
|
|
810
|
+
if (confidences.length > 0) {
|
|
811
|
+
const avg = confidences.reduce((a, b) => a + b, 0) / confidences.length;
|
|
812
|
+
parts.push(`Average model confidence: ${Math.round(avg * 100)}%.`);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return parts.join(" ") || "Decision based on multi-model deliberation.";
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Generate action items from session data.
|
|
820
|
+
* @param {DecisionSession} session
|
|
821
|
+
* @param {string} decision
|
|
822
|
+
* @returns {Array<{id: string, title: string, description: string, priority: string}>}
|
|
823
|
+
*/
|
|
824
|
+
function generateActionItems(session, decision) {
|
|
825
|
+
const items = [];
|
|
826
|
+
|
|
827
|
+
// Primary action: implement the decision
|
|
828
|
+
items.push({
|
|
829
|
+
id: "action-0",
|
|
830
|
+
title: `Implement: ${truncate(decision, 60)}`,
|
|
831
|
+
description: `Execute the decided approach: ${decision}`,
|
|
832
|
+
priority: "high",
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// For each criterion, create a monitoring/validation action
|
|
836
|
+
const criteria = session.criteria || [];
|
|
837
|
+
for (let i = 0; i < criteria.length && i < 5; i++) {
|
|
838
|
+
items.push({
|
|
839
|
+
id: `action-${i + 1}`,
|
|
840
|
+
title: `Validate: ${truncate(criteria[i], 60)}`,
|
|
841
|
+
description: `Ensure the implementation satisfies the "${criteria[i]}" criterion`,
|
|
842
|
+
priority: i < 2 ? "medium" : "low",
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return items;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Generate risk items from low scores and conflicts.
|
|
851
|
+
* @param {DecisionSession} session
|
|
852
|
+
* @returns {Array<{description: string, mitigation: string, probability: string}>}
|
|
853
|
+
*/
|
|
854
|
+
function generateRisks(session) {
|
|
855
|
+
const risks = [];
|
|
856
|
+
const opinions = session.opinions || {};
|
|
857
|
+
const conflicts = session.conflicts || [];
|
|
858
|
+
const criteria = session.criteria || [];
|
|
859
|
+
|
|
860
|
+
// Risks from high-divergence conflicts
|
|
861
|
+
for (const conflict of conflicts) {
|
|
862
|
+
risks.push({
|
|
863
|
+
description: `Disagreement on "${conflict.criterion}" (divergence: ${conflict.divergence}/10)`,
|
|
864
|
+
mitigation: `Monitor and re-evaluate if assumptions about "${conflict.criterion}" change`,
|
|
865
|
+
probability: conflict.divergence >= 6 ? "high" : "medium",
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Risks from criteria with low average scores
|
|
870
|
+
for (const criterion of criteria) {
|
|
871
|
+
const scores = Object.values(opinions)
|
|
872
|
+
.map(o => o.scores?.[criterion])
|
|
873
|
+
.filter(s => typeof s === "number");
|
|
874
|
+
if (scores.length === 0) continue;
|
|
875
|
+
|
|
876
|
+
const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
877
|
+
if (avg <= 4) {
|
|
878
|
+
risks.push({
|
|
879
|
+
description: `Low confidence in "${criterion}" (avg score: ${avg.toFixed(1)}/10)`,
|
|
880
|
+
mitigation: `Investigate alternatives or mitigations for "${criterion}"`,
|
|
881
|
+
probability: avg <= 2 ? "high" : "medium",
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return risks.slice(0, 8); // Cap at 8 risks
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Build a markdown checklist from action items.
|
|
891
|
+
* @param {string} decision
|
|
892
|
+
* @param {Array<{id: string, title: string, description: string, priority: string}>} actionItems
|
|
893
|
+
* @returns {string}
|
|
894
|
+
*/
|
|
895
|
+
function buildChecklist(decision, actionItems) {
|
|
896
|
+
const parts = [];
|
|
897
|
+
parts.push(`# Decision Checklist: ${truncate(decision, 60)}\n`);
|
|
898
|
+
for (const item of actionItems) {
|
|
899
|
+
const priorityTag = item.priority === "high" ? " [HIGH]" : item.priority === "medium" ? " [MED]" : "";
|
|
900
|
+
parts.push(`- [ ]${priorityTag} ${item.title}`);
|
|
901
|
+
if (item.description) {
|
|
902
|
+
parts.push(` - ${item.description}`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return parts.join("\n");
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Build a GitHub issue body from session data.
|
|
910
|
+
* @param {DecisionSession} session
|
|
911
|
+
* @param {string} decision
|
|
912
|
+
* @param {string} rationale
|
|
913
|
+
* @param {Array} actionItems
|
|
914
|
+
* @param {Array} risks
|
|
915
|
+
* @returns {string}
|
|
916
|
+
*/
|
|
917
|
+
function buildGithubIssue(session, decision, rationale, actionItems, risks) {
|
|
918
|
+
const parts = [];
|
|
919
|
+
|
|
920
|
+
parts.push(`## Decision: ${decision}\n`);
|
|
921
|
+
parts.push(`**Problem:** ${session.problem}\n`);
|
|
922
|
+
parts.push(`**Rationale:** ${rationale}\n`);
|
|
923
|
+
|
|
924
|
+
if (actionItems.length > 0) {
|
|
925
|
+
parts.push("### Action Items\n");
|
|
926
|
+
for (const item of actionItems) {
|
|
927
|
+
parts.push(`- [ ] **[${item.priority.toUpperCase()}]** ${item.title}: ${item.description}`);
|
|
928
|
+
}
|
|
929
|
+
parts.push("");
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (risks.length > 0) {
|
|
933
|
+
parts.push("### Risks\n");
|
|
934
|
+
for (const risk of risks) {
|
|
935
|
+
parts.push(`- **[${risk.probability.toUpperCase()}]** ${risk.description}`);
|
|
936
|
+
parts.push(` - Mitigation: ${risk.mitigation}`);
|
|
937
|
+
}
|
|
938
|
+
parts.push("");
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
parts.push("---");
|
|
942
|
+
parts.push(`*Generated by aigentry-deliberation decision engine*`);
|
|
943
|
+
|
|
944
|
+
return parts.join("\n");
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Load decision templates from the selectors directory.
|
|
949
|
+
* Returns an empty array if the file does not exist or is invalid.
|
|
950
|
+
*
|
|
951
|
+
* @returns {Array<{name: string, description: string, criteria: string[], options?: string[], keywords?: string[]}>}
|
|
952
|
+
*/
|
|
953
|
+
export function loadTemplates() {
|
|
954
|
+
const templatePath = join(__dirname, "selectors", "decision-templates.json");
|
|
955
|
+
try {
|
|
956
|
+
const raw = readFileSync(templatePath, "utf-8");
|
|
957
|
+
const parsed = JSON.parse(raw);
|
|
958
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
959
|
+
} catch {
|
|
960
|
+
return [];
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Match a template to a problem description using keyword matching.
|
|
966
|
+
*
|
|
967
|
+
* Scores each template by counting keyword hits in the problem text.
|
|
968
|
+
* Returns the best-matching template, or null if no template matches.
|
|
969
|
+
*
|
|
970
|
+
* @param {string} problemText - The decision problem description
|
|
971
|
+
* @param {Array<{name: string, keywords?: string[], description?: string}>} templates
|
|
972
|
+
* @returns {{name: string, description: string, criteria: string[], options?: string[], keywords?: string[]}|null}
|
|
973
|
+
*/
|
|
974
|
+
export function matchTemplate(problemText, templates) {
|
|
975
|
+
if (!problemText || !Array.isArray(templates) || templates.length === 0) {
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const lower = problemText.toLowerCase();
|
|
980
|
+
let bestMatch = null;
|
|
981
|
+
let bestScore = 0;
|
|
982
|
+
|
|
983
|
+
for (const template of templates) {
|
|
984
|
+
let score = 0;
|
|
985
|
+
const keywords = template.keywords || [];
|
|
986
|
+
|
|
987
|
+
for (const keyword of keywords) {
|
|
988
|
+
if (lower.includes(keyword.toLowerCase())) {
|
|
989
|
+
score++;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Also match against template name and description
|
|
994
|
+
if (template.name && lower.includes(template.name.toLowerCase())) {
|
|
995
|
+
score += 2;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (score > bestScore) {
|
|
999
|
+
bestScore = score;
|
|
1000
|
+
bestMatch = template;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Require at least 1 keyword match
|
|
1005
|
+
return bestScore >= 1 ? bestMatch : null;
|
|
1006
|
+
}
|