mcp-council-server 4.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/src/tools.js ADDED
@@ -0,0 +1,556 @@
1
+ import { z } from 'zod';
2
+ import { CONFIG } from './config.js';
3
+ import { createSession, getSession, updateSessionStatus, updateSessionPhase } from './memory/session.js';
4
+ import { getActivePhase, getAllPhases, savePhaseOutput, validatePhaseTransition, unlockNextPhase } from './memory/phase.js';
5
+ import { saveDecision } from './memory/decision.js';
6
+ import { composeRecallContext } from './memory/recall.js';
7
+ import { parseTaskStructure, extractDeliverables, extractUnresolved } from './analysis/parser.js';
8
+ import { assessComplexity, generatePipeline, getPhaseInstructions } from './prinsip/adaptive.js';
9
+ import { validateCertainty, extractKnowledgeBoundary, createTransparencyLogEntry } from './prinsip/transparency.js';
10
+ import { getPersona, validateAuthority, buildHandshake } from './prinsip/agentic.js';
11
+ import { detectContradictions } from './verify/contradiction.js';
12
+ import { checkGrounding } from './verify/grounding.js';
13
+ import { calculatePhaseScore } from './score/index.js';
14
+ import { scoreCompleteness } from './score/completeness.js';
15
+ import { scoreConsistency } from './score/consistency.js';
16
+ import { scoreTransparency } from './score/transparency.js';
17
+ import { scoreAgentic } from './score/agentic.js';
18
+ import { insert } from './db/json-store.js';
19
+
20
+ function sanitize(str) {
21
+ if (typeof str !== 'string') return '';
22
+ return str.replace(/\0/g, '').normalize('NFKC').trim();
23
+ }
24
+
25
+ function buildError(message) {
26
+ return { content: [{ type: 'text', text: JSON.stringify({ error: message }) }], isError: true };
27
+ }
28
+
29
+ function buildResponse(data) {
30
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
31
+ }
32
+
33
+ const InitSchema = z.object({
34
+ task: z.string().min(10).max(CONFIG.MAX_TASK_LENGTH),
35
+ context: z.string().max(CONFIG.MAX_CONTEXT_LENGTH).optional().default(''),
36
+ title: z.string().max(200).optional().default('')
37
+ });
38
+
39
+ const SaveSchema = z.object({
40
+ sessionId: z.string().uuid(),
41
+ phase: z.enum(['decompile', 'design', 'critique', 'synthesis', 'verify']),
42
+ output: z.object({
43
+ summary: z.string().min(1).max(20000),
44
+ decisions: z.array(z.object({
45
+ description: z.string(),
46
+ rationale: z.string().optional().default(''),
47
+ alternatives: z.array(z.string()).optional().default([]),
48
+ rejectedReasons: z.record(z.string()).optional().default({})
49
+ })).optional().default([]),
50
+ constraints: z.array(z.string()).optional().default([]),
51
+ confidence: z.number().min(0).max(1),
52
+ claims: z.array(z.object({
53
+ text: z.string(),
54
+ certainty: z.enum(['confirmed', 'likely', 'uncertain', 'unknown', 'assumption']).optional().default('unknown'),
55
+ source: z.string().optional().default('assumption')
56
+ })).optional().default([]),
57
+ knowledgeBoundary: z.object({
58
+ whatIKnow: z.array(z.string()).optional().default([]),
59
+ whatIAssume: z.array(z.string()).optional().default([]),
60
+ whatIDontKnow: z.array(z.string()).optional().default([]),
61
+ whatNeedsVerification: z.array(z.string()).optional().default([])
62
+ }).optional().default({}),
63
+ approaches: z.array(z.any()).optional(),
64
+ justification: z.string().optional(),
65
+ tradeoffs: z.array(z.any()).optional(),
66
+ recommended: z.string().optional(),
67
+ vulnerabilities: z.array(z.any()).optional(),
68
+ riskLevel: z.string().optional(),
69
+ mitigation: z.array(z.any()).optional(),
70
+ implementation: z.array(z.any()).optional(),
71
+ testing: z.array(z.any()).optional(),
72
+ warnings: z.array(z.string()).optional().default([]),
73
+ checks: z.array(z.any()).optional(),
74
+ passedCount: z.number().optional(),
75
+ failedCount: z.number().optional(),
76
+ finalConfidence: z.number().optional(),
77
+ criticalQuestions: z.array(z.string()).optional().default([])
78
+ })
79
+ });
80
+
81
+ const RecallSchema = z.object({
82
+ sessionId: z.string().uuid(),
83
+ focus: z.array(z.enum(['decompile', 'design', 'critique', 'synthesis', 'verify'])).optional(),
84
+ format: z.enum(['full', 'summary', 'decisions']).optional().default('full')
85
+ });
86
+
87
+ const VerifySchema = z.object({
88
+ sessionId: z.string().uuid(),
89
+ claims: z.array(z.object({
90
+ text: z.string(),
91
+ source: z.string().optional().default('assumption'),
92
+ type: z.string().optional().default('')
93
+ })),
94
+ mode: z.enum(['contradiction', 'grounding', 'all']).optional().default('all')
95
+ });
96
+
97
+ const ContinueSchema = z.object({
98
+ sessionId: z.string().uuid()
99
+ });
100
+
101
+ const ScoreSchema = z.object({
102
+ sessionId: z.string().uuid()
103
+ });
104
+
105
+ export function registerCouncilTools(server) {
106
+ // ─── Tool 1: council_init ───────────────────────────────────────
107
+ server.tool(
108
+ 'council_init',
109
+ 'Memulai sesi reasoning council. Panggil pertama kali saat menerima prompt kompleks dari user. ' +
110
+ 'Mengembalikan adaptive pipeline, persona, phase plan, dan memory slots. ' +
111
+ 'Pipeline menyesuaikan kompleksitas task (light/standard/full).',
112
+ {
113
+ task: z.string().min(10).max(CONFIG.MAX_TASK_LENGTH).describe('Prompt kompleks dari user'),
114
+ context: z.string().max(CONFIG.MAX_CONTEXT_LENGTH).optional().describe('Konteks tambahan'),
115
+ title: z.string().max(200).optional().describe('Nama sesi untuk identifikasi')
116
+ },
117
+ async ({ task, context, title }) => {
118
+ try {
119
+ const cleanTask = sanitize(task);
120
+ const cleanContext = sanitize(context || '');
121
+
122
+ const structure = parseTaskStructure(cleanTask);
123
+ const complexity = assessComplexity(cleanTask, structure);
124
+ const pipeline = generatePipeline(complexity);
125
+ const sessionId = createSession(cleanTask, cleanContext, title, complexity, pipeline);
126
+ const activePhase = getActivePhase(sessionId);
127
+ const persona = getPersona(activePhase.name);
128
+
129
+ return buildResponse({
130
+ type: 'council_init',
131
+ sessionId,
132
+ adaptation: {
133
+ complexity,
134
+ pipeline: pipeline.map((p, i) => ({ ...p, order: i + 1 })),
135
+ adaptiveReason: `Complexity score ${complexity.normalized}/100 (${complexity.level}). Pipeline: ${complexity.pipelineDepth}.`
136
+ },
137
+ personas: { current: { name: persona.name, identity: persona.identity, authority: persona.authority, notAuthority: persona.notAuthority } },
138
+ phase: activePhase.name,
139
+ phasePlan: pipeline.map((p, i) => ({
140
+ name: p.name, persona: p.persona, order: i + 1, status: i === 0 ? 'active' : 'locked'
141
+ })),
142
+ instructions: getPhaseInstructions(activePhase.name, activePhase.depth),
143
+ memorySlots: {
144
+ originalTask: cleanTask,
145
+ decompositions: [],
146
+ decisions: [],
147
+ constraints: [],
148
+ criticalQuestions: []
149
+ },
150
+ transparencyLog: { entries: [], count: 0 }
151
+ });
152
+ } catch (err) {
153
+ return buildError(`Gagal inisialisasi: ${err.message}`);
154
+ }
155
+ }
156
+ );
157
+
158
+ // ─── Tool 2: council_save ─────────────────────────────────────────
159
+ server.tool(
160
+ 'council_save',
161
+ 'Simpan hasil fase ke memory. Fase harus sesuai urutan (tidak bisa skip). ' +
162
+ 'Setelah save: server menghitung scoring, deteksi kontradiksi, validasi authority, ' +
163
+ 'dan menyusun handoff ke fase berikutnya. Jika score < 70, AI disarankan refine fase ini.',
164
+ {
165
+ sessionId: z.string().uuid().describe('ID sesi dari council_init'),
166
+ phase: z.enum(['decompile', 'design', 'critique', 'synthesis', 'verify']).describe('Nama fase yang akan disimpan'),
167
+ output: z.any().describe('Output fase — object dengan summary, decisions, claims, dll')
168
+ },
169
+ async ({ sessionId, phase, output }) => {
170
+
171
+ try {
172
+ // Validasi session
173
+ const session = getSession(sessionId);
174
+ if (!session) return buildError('Session tidak ditemukan. Gunakan council_init untuk memulai sesi baru.');
175
+
176
+ // Validasi phase transition
177
+ const transition = validatePhaseTransition(sessionId, session.current_phase, phase);
178
+ if (!transition.valid) return buildError(transition.error);
179
+
180
+ // Authority validation
181
+ const persona = getPersona(phase);
182
+ const authorityWarnings = validateAuthority(phase, output);
183
+
184
+ // Get previous phases for scoring
185
+ const allPhases = getAllPhases(sessionId);
186
+ const previousPhases = allPhases.filter(p => p.status === 'done');
187
+
188
+ // Scoring
189
+ const score = calculatePhaseScore(phase, output, previousPhases);
190
+
191
+ // Transparency validation
192
+ const certaintyResult = validateCertainty(output.claims || []);
193
+ const kb = extractKnowledgeBoundary(output);
194
+
195
+ // Contradiction detection
196
+ const conflicts = detectContradictions(output, previousPhases);
197
+
198
+ // Save to DB
199
+ savePhaseOutput(sessionId, phase, output, output.confidence, kb, certaintyResult.stats, authorityWarnings);
200
+
201
+ // Save decisions
202
+ if (output.decisions) {
203
+ for (const d of output.decisions) {
204
+ saveDecision(sessionId, phase, d.description, d.rationale, d.alternatives, d.rejectedReasons);
205
+ }
206
+ }
207
+
208
+ // Save claims to store
209
+ if (output.claims) {
210
+ for (const c of output.claims) {
211
+ insert('claims', {
212
+ id: CONFIG.uuidv4(),
213
+ session_id: sessionId,
214
+ phase_name: phase,
215
+ text: c.text,
216
+ certainty: c.certainty || 'unknown',
217
+ source: c.source || 'assumption',
218
+ status: 'unverified',
219
+ created_at: new Date().toISOString()
220
+ });
221
+ }
222
+ }
223
+
224
+ // Transparency log for issues
225
+ if (!certaintyResult.valid) {
226
+ createTransparencyLogEntry(sessionId, phase, 'uncertainty',
227
+ `${certaintyResult.unlabeled.length} claims tanpa certainty`, certaintyResult.unlabeled, 'unknown');
228
+ }
229
+ if (authorityWarnings.length > 0) {
230
+ createTransparencyLogEntry(sessionId, phase, 'boundary',
231
+ `${authorityWarnings.length} authority violations`, authorityWarnings, 'unknown');
232
+ }
233
+
234
+ // Handoff
235
+ const nextPhase = unlockNextPhase(sessionId, phase);
236
+ if (nextPhase) {
237
+ updateSessionPhase(sessionId, nextPhase.name);
238
+ } else {
239
+ updateSessionPhase(sessionId, 'done');
240
+ updateSessionStatus(sessionId, 'done');
241
+ }
242
+
243
+ const deliverables = extractDeliverables(output);
244
+ const unresolved = extractUnresolved(output);
245
+ const handshake = buildHandshake(phase, nextPhase?.name || 'done', deliverables, authorityWarnings, unresolved, conflicts);
246
+
247
+ // Context for next phase
248
+ let contextForNext = null;
249
+ if (nextPhase && score.canProceed) {
250
+ const nextPersona = getPersona(nextPhase.name);
251
+ contextForNext = {
252
+ currentPersona: {
253
+ name: nextPersona.name,
254
+ identity: nextPersona.identity,
255
+ authority: nextPersona.authority,
256
+ notAuthority: nextPersona.notAuthority
257
+ },
258
+ originalTask: session.task,
259
+ previousResults: allPhases.filter(p => p.status === 'done').map(p => ({
260
+ phase: p.name,
261
+ summary: p.output?.summary || '',
262
+ confidence: p.confidence
263
+ })),
264
+ phaseInstructions: getPhaseInstructions(nextPhase.name, nextPhase.depth)
265
+ };
266
+ }
267
+
268
+ return buildResponse({
269
+ type: 'council_save',
270
+ status: 'saved',
271
+ phase,
272
+ scoring: {
273
+ finalScore: score.finalScore,
274
+ verdict: score.verdict,
275
+ dimensions: score.dimensions,
276
+ suggestions: score.suggestions
277
+ },
278
+ canProceed: score.canProceed,
279
+ handoff: handshake,
280
+ nextPhase: nextPhase?.name || null,
281
+ phasePlan: allPhases.map(p => ({
282
+ name: p.name,
283
+ status: p.status === phase ? 'done' : p.name === nextPhase?.name ? 'active' : p.status === 'done' ? 'done' : 'locked'
284
+ })),
285
+ contextForNext,
286
+ sessionComplete: !nextPhase
287
+ });
288
+ } catch (err) {
289
+ return buildError(`Gagal menyimpan fase: ${err.message}`);
290
+ }
291
+ }
292
+ );
293
+
294
+ // ─── Tool 3: council_recall ───────────────────────────────────────
295
+ server.tool(
296
+ 'council_recall',
297
+ 'Ambil memory kapan saja. Berguna saat AI lupa konteks, mau review keputusan, ' +
298
+ 'atau resume session. Tersedia format: full (default), summary, atau decisions-only.',
299
+ {
300
+ sessionId: z.string().uuid().describe('ID sesi'),
301
+ focus: z.array(z.enum(['decompile', 'design', 'critique', 'synthesis', 'verify'])).optional()
302
+ .describe('Fase spesifik yang ingin dilihat'),
303
+ format: z.enum(['full', 'summary', 'decisions']).optional().default('full')
304
+ .describe('Format output: full (default), summary, atau decisions-only')
305
+ },
306
+ async ({ sessionId, focus, format }) => {
307
+ try {
308
+ const context = composeRecallContext(sessionId, focus, format);
309
+ if (!context) return buildError('Session tidak ditemukan.');
310
+ return buildResponse({ type: 'council_recall', ...context });
311
+ } catch (err) {
312
+ return buildError(`Gagal recall: ${err.message}`);
313
+ }
314
+ }
315
+ );
316
+
317
+ // ─── Tool 4: council_verify ───────────────────────────────────────
318
+ server.tool(
319
+ 'council_verify',
320
+ 'Verifikasi klaim terhadap memory sesi. Deteksi kontradiksi numerik, polaritas, ' +
321
+ 'dan grounding source. Mode: contradiction, grounding, atau all (default).',
322
+ {
323
+ sessionId: z.string().uuid().describe('ID sesi'),
324
+ claims: z.any().describe('Daftar klaim yang akan diverifikasi — array of {text, source?, type?}'),
325
+ mode: z.enum(['contradiction', 'grounding', 'all']).optional().default('all')
326
+ .describe('Mode verifikasi')
327
+ },
328
+ async ({ sessionId, claims, mode }) => {
329
+ try {
330
+ const session = getSession(sessionId);
331
+ if (!session) return buildError('Session tidak ditemukan.');
332
+
333
+ const allPhases = getAllPhases(sessionId);
334
+ const completedPhases = allPhases.filter(p => p.status === 'done');
335
+
336
+ const results = [];
337
+ let contradictionResults = [];
338
+ let groundingResults = null;
339
+
340
+ if (mode === 'contradiction' || mode === 'all') {
341
+ const claimsObj = { claims };
342
+ contradictionResults = detectContradictions(claimsObj, completedPhases);
343
+ }
344
+
345
+ if (mode === 'grounding' || mode === 'all') {
346
+ groundingResults = checkGrounding(claims);
347
+ }
348
+
349
+ let total = 0, passed = 0, warning = 0, contradicted = 0;
350
+
351
+ if (contradictionResults.length > 0) {
352
+ contradicted = contradictionResults.length;
353
+ }
354
+
355
+ if (groundingResults) {
356
+ passed = groundingResults.verified;
357
+ warning = groundingResults.unverified;
358
+ total = groundingResults.total;
359
+ }
360
+
361
+ return buildResponse({
362
+ type: 'council_verify',
363
+ results: {
364
+ contradictions: contradictionResults,
365
+ grounding: groundingResults
366
+ },
367
+ summary: {
368
+ total: Math.max(total, contradictionResults.length),
369
+ passed,
370
+ warning,
371
+ contradicted
372
+ }
373
+ });
374
+ } catch (err) {
375
+ return buildError(`Gagal verifikasi: ${err.message}`);
376
+ }
377
+ }
378
+ );
379
+
380
+ // ─── Tool 5: council_continue ─────────────────────────────────────
381
+ server.tool(
382
+ 'council_continue',
383
+ 'Resume session yang terhenti (karena restart atau interupsi). ' +
384
+ 'Mengembalikan full context + fase terakhir yang aktif + instruksi resume.',
385
+ {
386
+ sessionId: z.string().uuid().describe('ID sesi yang akan dilanjutkan')
387
+ },
388
+ async ({ sessionId }) => {
389
+ try {
390
+ const session = getSession(sessionId);
391
+ if (!session) return buildError('Session tidak ditemukan.');
392
+
393
+ if (session.status === 'done') {
394
+ return buildError('Session sudah selesai. Gunakan council_recall untuk review.');
395
+ }
396
+
397
+ const activePhase = getActivePhase(sessionId);
398
+ if (!activePhase) return buildError('Tidak ada fase aktif. Session mungkin sudah selesai.');
399
+
400
+ const allPhases = getAllPhases(sessionId);
401
+ const lastDone = allPhases.filter(p => p.status === 'done').pop();
402
+ const persona = getPersona(activePhase.name);
403
+ const context = composeRecallContext(sessionId);
404
+
405
+ return buildResponse({
406
+ type: 'council_continue',
407
+ session: {
408
+ id: session.id,
409
+ task: session.task,
410
+ status: session.status,
411
+ currentPhase: session.current_phase,
412
+ progress: `${allPhases.filter(p => p.status === 'done').length}/${allPhases.length}`
413
+ },
414
+ currentPhase: {
415
+ name: activePhase.name,
416
+ persona: persona.name,
417
+ personaIdentity: persona.identity,
418
+ personaAuthority: persona.authority,
419
+ personaNotAuthority: persona.notAuthority,
420
+ instructions: getPhaseInstructions(activePhase.name, activePhase.depth)
421
+ },
422
+ lastCompletedPhase: lastDone ? { name: lastDone.name, summary: lastDone.output?.summary || '' } : null,
423
+ context,
424
+ resumeInstructions: `Lanjutkan dari fase ${activePhase.name} sebagai ${persona.name}. ${getPhaseInstructions(activePhase.name, activePhase.depth)}`
425
+ });
426
+ } catch (err) {
427
+ return buildError(`Gagal melanjutkan session: ${err.message}`);
428
+ }
429
+ }
430
+ );
431
+
432
+ // ─── Tool 6: council_score ────────────────────────────────────────
433
+ server.tool(
434
+ 'council_score',
435
+ 'Nilai kualitas seluruh sesi council. Menganalisis completeness, consistency, ' +
436
+ 'transparency, dan agentic alignment dari SEMUA fase. Memberi overall score + rekomendasi.',
437
+ {
438
+ sessionId: z.string().uuid().describe('ID sesi yang akan dinilai')
439
+ },
440
+ async ({ sessionId }) => {
441
+ try {
442
+ const session = getSession(sessionId);
443
+ if (!session) return buildError('Session tidak ditemukan.');
444
+
445
+ const allPhases = getAllPhases(sessionId);
446
+ const completedPhases = allPhases.filter(p => p.status === 'done');
447
+
448
+ if (completedPhases.length === 0) {
449
+ return buildError('Belum ada fase yang selesai. Selesaikan minimal 1 fase terlebih dahulu.');
450
+ }
451
+
452
+ const perPhase = [];
453
+ let totalQuality = 0;
454
+ let totalTransparency = 0;
455
+ let totalAgentic = 0;
456
+ let totalAdaptability = 0;
457
+
458
+ for (let i = 0; i < completedPhases.length; i++) {
459
+ const phase = completedPhases[i];
460
+ const prevPhases = completedPhases.slice(0, i);
461
+ const persona = getPersona(phase.name);
462
+ const output = phase.output || {};
463
+
464
+ const compScore = scoreCompleteness(phase.name, output);
465
+ const consScore = scoreConsistency(phase.name, output, prevPhases);
466
+ const transScore = scoreTransparency(phase.name, output);
467
+ const agScore = scoreAgentic(phase.name, output, persona);
468
+
469
+ const phaseFinal = Math.round(
470
+ compScore.score * 0.25 + consScore.score * 0.25 +
471
+ (output.decisions?.length > 0 ? 80 : 0) * 0.15 +
472
+ transScore.score * 0.15 + agScore.score * 0.10 + 100 * 0.10
473
+ );
474
+
475
+ totalQuality += phaseFinal;
476
+ totalTransparency += transScore.score;
477
+ totalAgentic += agScore.score;
478
+
479
+ perPhase.push({
480
+ phase: phase.name,
481
+ persona: persona.name,
482
+ finalScore: phaseFinal,
483
+ verdict: phaseFinal >= 90 ? 'excellent' : phaseFinal >= 70 ? 'good' : phaseFinal >= 50 ? 'needs_improvement' : 'insufficient',
484
+ dimensions: {
485
+ completeness: compScore,
486
+ consistency: { score: consScore.score, contradictions: consScore.contradictions },
487
+ transparency: transScore,
488
+ agentic: agScore
489
+ }
490
+ });
491
+ }
492
+
493
+ // Cross-phase contradictions
494
+ const crossPhaseIssues = [];
495
+ for (let i = 1; i < completedPhases.length; i++) {
496
+ const curr = completedPhases[i];
497
+ const prevs = completedPhases.slice(0, i);
498
+ if (curr.output) {
499
+ const contradictions = detectContradictions(curr.output, prevs);
500
+ if (contradictions.length > 0) {
501
+ crossPhaseIssues.push({
502
+ phase: curr.name,
503
+ contradictions: contradictions.slice(0, 5)
504
+ });
505
+ }
506
+ }
507
+ }
508
+
509
+ const avgQuality = Math.round(totalQuality / completedPhases.length);
510
+ const avgTransparency = Math.round(totalTransparency / completedPhases.length);
511
+ const avgAgentic = Math.round(totalAgentic / completedPhases.length);
512
+ const adaptabilityScore = session.adaptation_plan?.length > 0 ? 85 : 70;
513
+
514
+ const overall = Math.round(avgQuality * 0.4 + adaptabilityScore * 0.2 + avgTransparency * 0.2 + avgAgentic * 0.2);
515
+ const finalVerdict = overall >= 90 ? 'excellent' : overall >= 70 ? 'good' : overall >= 50 ? 'needs_improvement' : 'insufficient';
516
+
517
+ // Suggestions
518
+ const suggestions = [];
519
+ if (crossPhaseIssues.length > 0) {
520
+ suggestions.push(`Resolve ${crossPhaseIssues.length} cross-phase contradiction(s)`);
521
+ }
522
+ const lowScorePhases = perPhase.filter(p => p.finalScore < 70);
523
+ if (lowScorePhases.length > 0) {
524
+ suggestions.push(`Perbaiki fase: ${lowScorePhases.map(p => `${p.phase}(${p.finalScore})`).join(', ')}`);
525
+ }
526
+ if (avgTransparency < 70) {
527
+ suggestions.push('Tingkatkan transparansi: tambahkan certainty level dan knowledge boundary');
528
+ }
529
+ if (avgAgentic < 70) {
530
+ suggestions.push('Tingkatkan agentic alignment: ikuti batasan authority persona');
531
+ }
532
+ if (suggestions.length === 0) {
533
+ suggestions.push('Semua fase berkualitas baik. Tidak ada rekomendasi perbaikan.');
534
+ }
535
+
536
+ return buildResponse({
537
+ type: 'council_score',
538
+ sessionId,
539
+ overall: {
540
+ finalScore: overall,
541
+ verdict: finalVerdict,
542
+ qualityScore: avgQuality,
543
+ adaptabilityScore,
544
+ transparencyScore: avgTransparency,
545
+ agenticScore: avgAgentic
546
+ },
547
+ perPhase,
548
+ crossPhaseIssues,
549
+ improvementSuggestions: suggestions
550
+ });
551
+ } catch (err) {
552
+ return buildError(`Gagal menghitung score: ${err.message}`);
553
+ }
554
+ }
555
+ );
556
+ }
@@ -0,0 +1,125 @@
1
+ const POLARITY_PAIRS = [
2
+ ['increase', 'decrease'], ['use', 'avoid'], ['sync', 'async'],
3
+ ['monolith', 'microservice'], ['sql', 'nosql'],
4
+ ['paid', 'free'], ['fast', 'slow'],
5
+ ['secure', 'insecure'], ['include', 'exclude'],
6
+ ['start', 'stop'], ['enable', 'disable'],
7
+ ['accept', 'reject'], ['allow', 'deny'],
8
+ ['optimistic', 'pessimistic'], ['vertical', 'horizontal'],
9
+ ['centralized', 'decentralized'], ['synchronous', 'asynchronous'],
10
+ ['blocking', 'non-blocking'], ['stateful', 'stateless']
11
+ ];
12
+
13
+ export function detectContradictions(newOutput, previousPhases) {
14
+ if (!previousPhases || previousPhases.length === 0) {
15
+ return [];
16
+ }
17
+
18
+ const contradictions = [];
19
+ const newText = JSON.stringify(newOutput).toLowerCase();
20
+
21
+ // 1. Numerical contradiction detection
22
+ const numberPattern = /\b(\d+\.?\d*)\s*(ms|s|gb|mb|kb|%|usd|rb|rps|qps|tps|menit|jam|hari|gbps|mbps)?\b/g;
23
+
24
+ const currentNumbers = [];
25
+ let match;
26
+ while ((match = numberPattern.exec(newText)) !== null) {
27
+ currentNumbers.push({ value: parseFloat(match[1]), unit: (match[2] || '').toLowerCase(), raw: match[0] });
28
+ }
29
+
30
+ for (const prev of previousPhases) {
31
+ if (!prev.output) continue;
32
+ const prevText = JSON.stringify(prev.output).toLowerCase();
33
+
34
+ const prevNumbers = [];
35
+ while ((match = numberPattern.exec(prevText)) !== null) {
36
+ prevNumbers.push({ value: parseFloat(match[1]), unit: (match[2] || '').toLowerCase(), raw: match[0] });
37
+ }
38
+
39
+ for (const curr of currentNumbers) {
40
+ if (isNaN(curr.value) || curr.value < 1) continue;
41
+
42
+ for (const prevN of prevNumbers) {
43
+ if (isNaN(prevN.value) || prevN.value < 1) continue;
44
+ if (curr.unit !== prevN.unit) continue;
45
+ if (!curr.unit || !prevN.unit) continue; // require explicit units
46
+
47
+ const deviation = Math.abs(curr.value - prevN.value) / Math.max(Math.abs(prevN.value), 0.001);
48
+ if (deviation > 0.2) {
49
+ const key = `${curr.raw}-vs-${prevN.raw}-${prev.name}`;
50
+ if (!contradictions.some(c => c.key === key)) {
51
+ contradictions.push({
52
+ key,
53
+ type: 'numerical',
54
+ detail: `Angka "${curr.raw}" (fase saat ini) vs "${prevN.raw}" (${prev.name}) — deviasi ${Math.round(deviation * 100)}%`,
55
+ between: [prev.name, 'current']
56
+ });
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ // 2. Polarity contradiction detection
64
+ for (const [pos, neg] of POLARITY_PAIRS) {
65
+ const currentHasPos = newText.includes(pos);
66
+ const currentHasNeg = newText.includes(neg);
67
+
68
+ for (const prev of previousPhases) {
69
+ if (!prev.output) continue;
70
+ const prevText = JSON.stringify(prev.output).toLowerCase();
71
+ const prevHasPos = prevText.includes(pos);
72
+ const prevHasNeg = prevText.includes(neg);
73
+
74
+ if (currentHasPos && prevHasNeg) {
75
+ const key = `polarity-${pos}-${neg}-${prev.name}`;
76
+ if (!contradictions.some(c => c.key === key)) {
77
+ contradictions.push({
78
+ key,
79
+ type: 'polarity',
80
+ detail: `Fase ini bilang "${pos}" tapi ${prev.name} bilang "${neg}"`,
81
+ between: [prev.name, 'current']
82
+ });
83
+ }
84
+ }
85
+
86
+ if (currentHasNeg && prevHasPos) {
87
+ const key = `polarity-${neg}-${pos}-${prev.name}`;
88
+ if (!contradictions.some(c => c.key === key)) {
89
+ contradictions.push({
90
+ key,
91
+ type: 'polarity',
92
+ detail: `Fase ini bilang "${neg}" tapi ${prev.name} bilang "${pos}"`,
93
+ between: [prev.name, 'current']
94
+ });
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ // 3. Constraint compliance check (soft warning)
101
+ for (const prev of previousPhases) {
102
+ if (!prev.output?.constraints) continue;
103
+ for (const constraint of prev.output.constraints) {
104
+ const keywords = String(constraint).toLowerCase().split(/[\s_]+/).filter(k => k.length > 3);
105
+ const found = keywords.some(k => newText.includes(k));
106
+ if (!found && keywords.length > 0) {
107
+ contradictions.push({
108
+ type: 'constraint_not_referenced',
109
+ detail: `Constraint "${constraint}" dari fase ${prev.name} tidak direferensi di fase ini`,
110
+ between: [prev.name, 'current'],
111
+ severity: 'warning'
112
+ });
113
+ break;
114
+ }
115
+ }
116
+ }
117
+
118
+ // Remove duplicates and clean up
119
+ return contradictions.map(c => ({
120
+ type: c.type,
121
+ detail: c.detail,
122
+ between: c.between,
123
+ severity: c.severity || 'contradiction'
124
+ }));
125
+ }