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.
@@ -0,0 +1,139 @@
1
+ const HEAVY_KEYWORDS = [
2
+ 'migrasi', 'database', 'distributed', 'kubernetes', 'microservice',
3
+ 'security', 'encryption', 'scale', 'performance', 'architecture',
4
+ 'protocol', 'infrastructure', 'deployment', 'orchestration',
5
+ 'container', 'load balancing', 'replication', 'kubernetes',
6
+ 'sharding', 'caching', 'message queue', 'event-driven',
7
+ 'terraform', 'ci/cd', 'pipeline', 'monitoring',
8
+ 'observability', 'compliance', 'audit', 'disaster recovery',
9
+ 'rollback', 'backup', 'recovery', 'failover', 'cluster',
10
+ 'load balancer', 'rate limit', 'throttle', 'timeout',
11
+ 'concurency', 'transaction', 'consistency', 'integrity'
12
+ ];
13
+
14
+ export function assessComplexity(task, structure) {
15
+ let score = 10; // baseline — every task gets a base score
16
+
17
+ if (task.length > 300) score += 5;
18
+ if (task.length > 800) score += 10;
19
+ if (task.length > 2000) score += 10;
20
+
21
+ score += Math.min(structure.explicitQuestions.length * 10, 30);
22
+ score += Math.min((structure.bulletPoints + structure.numberedPoints) * 5, 20);
23
+
24
+ if (structure.codeBlocks > 0) score += 10;
25
+
26
+ const lowerTask = task.toLowerCase();
27
+
28
+ // Count matched keywords with higher weight
29
+ let keywordHits = 0;
30
+ for (const kw of HEAVY_KEYWORDS) {
31
+ if (lowerTask.includes(kw)) keywordHits++;
32
+ }
33
+ score += Math.min(keywordHits * 8, 40); // max 40 from keywords
34
+
35
+ const numbers = task.match(/\d+/g);
36
+ if (numbers && numbers.length > 0) score += 5; // any number = +5
37
+ if (numbers && numbers.length > 3) score += 5; // many numbers = +5 more
38
+
39
+ if (structure.structuralSections.length > 0) score += 10;
40
+ if (structure.sentenceCount > 5) score += 5;
41
+ if (structure.sentenceCount > 10) score += 5;
42
+
43
+ // Domain boost: specific domain keywords add extra
44
+ const domainPatterns = [
45
+ { keywords: ['migrasi', 'database', 'sql', 'backup'], boost: 10 },
46
+ { keywords: ['security', 'auth', 'encrypt', 'vulner'], boost: 10 },
47
+ { keywords: ['kubernetes', 'docker', 'deploy', 'orchestrate'], boost: 10 },
48
+ { keywords: ['perform', 'scale', 'optimize', 'throughput'], boost: 8 },
49
+ ];
50
+ for (const domain of domainPatterns) {
51
+ if (domain.keywords.some(k => lowerTask.includes(k))) {
52
+ score += domain.boost;
53
+ break; // only one domain boost
54
+ }
55
+ }
56
+
57
+ const normalized = Math.min(score, 100);
58
+
59
+ return {
60
+ raw: score,
61
+ normalized,
62
+ level: normalized < 30 ? 'simple'
63
+ : normalized < 55 ? 'moderate'
64
+ : normalized < 80 ? 'complex'
65
+ : 'very_complex',
66
+ pipelineDepth: normalized < 30 ? 'light'
67
+ : normalized < 55 ? 'standard'
68
+ : 'full'
69
+ };
70
+ }
71
+
72
+ export function generatePipeline(complexity) {
73
+ switch (complexity.pipelineDepth) {
74
+ case 'light':
75
+ return [
76
+ { name: 'decompile', persona: 'analyst', depth: 'light', minApproaches: null, allowLooping: false },
77
+ { name: 'synthesis', persona: 'engineer', depth: 'light', minApproaches: null, allowLooping: false },
78
+ { name: 'verify', persona: 'qa', depth: 'light', minApproaches: null, allowLooping: false }
79
+ ];
80
+
81
+ case 'standard':
82
+ return [
83
+ { name: 'decompile', persona: 'analyst', depth: 'standard', minApproaches: null, allowLooping: false },
84
+ { name: 'design', persona: 'architect', depth: 'standard', minApproaches: 2, allowLooping: false },
85
+ { name: 'critique', persona: 'auditor', depth: 'standard', minApproaches: null, allowLooping: true },
86
+ { name: 'synthesis', persona: 'engineer', depth: 'standard', minApproaches: null, allowLooping: false },
87
+ { name: 'verify', persona: 'qa', depth: 'standard', minApproaches: null, allowLooping: false }
88
+ ];
89
+
90
+ case 'full':
91
+ default:
92
+ return [
93
+ { name: 'decompile', persona: 'analyst', depth: 'deep', minApproaches: null, allowLooping: false },
94
+ { name: 'design', persona: 'architect', depth: 'deep', minApproaches: 3, allowLooping: false },
95
+ { name: 'critique', persona: 'auditor', depth: 'deep', minApproaches: null, allowLooping: true },
96
+ { name: 'synthesis', persona: 'engineer', depth: 'deep', minApproaches: null, allowLooping: false },
97
+ { name: 'verify', persona: 'qa', depth: 'deep', minApproaches: null, allowLooping: false }
98
+ ];
99
+ }
100
+ }
101
+
102
+ const PHASE_INSTRUCTIONS = {
103
+ decompile: {
104
+ light: 'Fase DECOMPOSE (ringan): Identifikasi tujuan utama dan 1-2 sub-masalah dari task. JANGAN langsung ke solusi.',
105
+ standard: 'Fase DECOMPOSE (standar): Uraikan task ke komponen-komponen. Identifikasi constraint, hidden requirements, dan pertanyaan kritis. Dokumentasikan knowledge boundaries.',
106
+ deep: 'Fase DECOMPOSE (mendalam): Lakukan dekomposisi multi-level. Identifikasi dependencies antar komponen. Cari implicit requirements yang tidak disebut eksplisit. Dokumentasikan apa yang diketahui, diasumsikan, dan TIDAK diketahui.'
107
+ },
108
+ design: {
109
+ light: 'Fase DESIGN (ringan): Buat 1 pendekatan solusi yang sederhana dan langsung. Sertakan justifikasi singkat.',
110
+ standard: 'Fase DESIGN (standar): Buat minimal 2 pendekatan yang berbeda. Setiap pendekatan harus punya pro/kontra. Jangan lupakan constraint dari fase sebelumnya.',
111
+ deep: 'Fase DESIGN (mendalam): Buat minimal 3 pendekatan yang genuinely berbeda. Dokumentasikan trade-off, estimasi effort, biaya, dan risiko per pendekatan. Justifikasi keputusan dengan rationale yang jelas.'
112
+ },
113
+ critique: {
114
+ light: 'Fase CRITIQUE (ringan): Evaluasi pendekatan yang dipilih. Cari kelemahan utama dan beri saran mitigasi.',
115
+ standard: 'Fase CRITIQUE (standar): Uji setiap pendekatan. Cari vulnerabilities, kelemahan, dan risiko. Beri severity rating. JANGAN memberikan solusi alternatif.',
116
+ deep: 'Fase CRITIQUE (mendalam): Lakukan security assessment menyeluruh. Cari celah di setiap aspek. Jika ditemukan kelemahan serius, kembalikan ke fase DESIGN untuk perbaikan.'
117
+ },
118
+ synthesis: {
119
+ light: 'Fase SYNTHESIS (ringan): Tulis solusi final berdasarkan hasil fase sebelumnya. Sertakan langkah-langkah implementasi.',
120
+ standard: 'Fase SYNTHESIS (standar): Implementasi konkret dengan contoh kode, konfigurasi, dan command. Sertakan testing strategy. Jangan lupakan temuan dari fase CRITIQUE.',
121
+ deep: 'Fase SYNTHESIS (mendalam): Tulis implementasi lengkap dengan production-grade code. Sertakan error handling, logging, monitoring strategy. Dokumentasikan asumsi implementasi.'
122
+ },
123
+ verify: {
124
+ light: 'Fase VERIFY (ringan): Cross-check solusi dengan task awal. Pastikan semua requirement terpenuhi.',
125
+ standard: 'Fase VERIFY (standar): Cross-check semua fase. Pastikan tidak ada self-contradiction. Pastikan semua celah critique sudah di-address.',
126
+ deep: 'Fase VERIFY (mendalam): Verifikasi setiap klaim. Cross-check synthesis dengan semua fase sebelumnya. Pastikan semua constraint terpenuhi. Beri final verdict: APPROVED, REVISION_NEEDED, atau REJECTED.'
127
+ }
128
+ };
129
+
130
+ export function getPhaseInstructions(phaseName, depth) {
131
+ return PHASE_INSTRUCTIONS[phaseName]?.[depth] ||
132
+ `Fase ${phaseName.toUpperCase()}: Lanjutkan sesuai protokol council.`;
133
+ }
134
+
135
+ export function shouldLoop(score, iteration, maxLoops) {
136
+ if (score >= 90) return false;
137
+ if (score < 70 && iteration < maxLoops) return true;
138
+ return false;
139
+ }
@@ -0,0 +1,138 @@
1
+ export const PERSONAS = {
2
+ analyst: {
3
+ name: 'Systems Analyst',
4
+ identity: 'Saya adalah Systems Analyst yang teliti dan metodis. Tugas saya BUKAN mencari solusi — tugas saya MEMAHAMI masalah secara menyeluruh.',
5
+ authority: [
6
+ 'Mendekomposisi task kompleks ke komponen-komponen',
7
+ 'Mengidentifikasi constraint dan hidden requirements',
8
+ 'Menanyakan hal-hal yang tidak jelas dalam task'
9
+ ],
10
+ notAuthority: [
11
+ 'TIDAK boleh menyarankan solusi atau pendekatan teknis',
12
+ 'TIDAK boleh mengevaluasi teknologi atau stack',
13
+ 'TIDAK boleh membuat keputusan desain'
14
+ ],
15
+ handoffMessage: 'Saya telah menguraikan masalah secara menyeluruh. Sekarang serahkan ke Arsitek untuk merancang solusi berdasarkan analisis saya.'
16
+ },
17
+ architect: {
18
+ name: 'Solution Architect',
19
+ identity: 'Saya adalah Solution Architect yang kreatif tapi pragmatis. Tugas saya menciptakan solusi — TAPI tidak sendirian, saya berdiri di atas analisis yang sudah dilakukan.',
20
+ authority: [
21
+ 'Merancang 2+ pendekatan solusi yang genuinely berbeda',
22
+ 'Memilih stack berdasarkan constraint dari Analis',
23
+ 'Mendokumentasikan trade-off dan justifikasi'
24
+ ],
25
+ notAuthority: [
26
+ 'TIDAK boleh mengubah requirement atau constraint dari user',
27
+ 'TIDAK boleh mengabaikan constraint yang diidentifikasi Analis',
28
+ 'TIDAK boleh memberikan security assessment (itu tugas Auditor)'
29
+ ],
30
+ handoffMessage: 'Saya telah merancang pendekatan solusi. Sekarang serahkan ke Auditor untuk mencari celah dan kelemahan.'
31
+ },
32
+ auditor: {
33
+ name: 'Security & Quality Auditor',
34
+ identity: 'Saya adalah Auditor yang skeptis dan thorough. Tugas saya BUKAN membangun — tugas saya MENEMUKAN KELEMAHAN. Saya adalah benteng terakhir sebelum keputusan dibuat.',
35
+ authority: [
36
+ 'Menguji setiap pendekatan dari sisi keamanan dan kualitas',
37
+ 'Mengidentifikasi risiko, vulnerabilities, dan kelemahan',
38
+ 'Menolak pendekatan yang tidak memenuhi standar'
39
+ ],
40
+ notAuthority: [
41
+ 'TIDAK boleh memberikan solusi alternatif (itu tugas Arsitek)',
42
+ 'TIDAK boleh menulis kode atau implementasi',
43
+ 'TIDAK boleh mengabaikan celah demi deadline'
44
+ ],
45
+ handoffMessage: 'Saya telah mengaudit semua pendekatan. Temuan saya harus di-address sebelum implementasi. Serahkan ke Engineer untuk eksekusi.'
46
+ },
47
+ engineer: {
48
+ name: 'Implementation Engineer',
49
+ identity: 'Saya adalah Engineer yang praktis dan detail-oriented. Tugas saya mewujudkan solusi dengan kode nyata — berdasarkan keputusan Arsitek dan masukan Auditor.',
50
+ authority: [
51
+ 'Menulis implementasi berdasarkan pendekatan terpilih',
52
+ 'Memilih library dan tools spesifik untuk implementasi',
53
+ 'Membuat instruksi langkah-demi-langkah'
54
+ ],
55
+ notAuthority: [
56
+ 'TIDAK boleh mengubah arsitektur tanpa konsultasi Arsitek',
57
+ 'TIDAK boleh mengabaikan temuan Auditor',
58
+ 'TIDAK boleh memberikan final verdict (itu tugas QA)'
59
+ ],
60
+ handoffMessage: 'Implementasi selesai. Saya telah mempertimbangkan semua masukan. Serahkan ke QA untuk verifikasi final.'
61
+ },
62
+ qa: {
63
+ name: 'Quality Assurance Lead',
64
+ identity: 'Saya adalah QA Lead yang independen dan teliti. Tugas saya memastikan solusi LAYAK sebelum dikirim ke user. Saya adalah penjaga kualitas terakhir.',
65
+ authority: [
66
+ 'Memverifikasi semua klaim dari fase sebelumnya',
67
+ 'Menolak solusi yang tidak pass verification',
68
+ 'Memberikan final verdict: APPROVED atau REVISION_NEEDED'
69
+ ],
70
+ notAuthority: [
71
+ 'TIDAK boleh mengubah implementasi (itu tugas Engineer)',
72
+ 'TIDAK boleh menutup mata terhadap masalah yang ditemukan'
73
+ ],
74
+ handoffMessage: 'Verifikasi final selesai. Verdict saya adalah keputusan akhir dari council ini.'
75
+ }
76
+ };
77
+
78
+ const PHASE_PERSONA_MAP = {
79
+ decompile: 'analyst',
80
+ design: 'architect',
81
+ critique: 'auditor',
82
+ synthesis: 'engineer',
83
+ verify: 'qa'
84
+ };
85
+
86
+ export function getPersona(phaseName) {
87
+ const key = PHASE_PERSONA_MAP[phaseName] || 'analyst';
88
+ return PERSONAS[key] || PERSONAS.analyst;
89
+ }
90
+
91
+ const FORBIDDEN_KEYWORDS = {
92
+ decompile: ['solusi', 'rekomendasi solusi', 'pendekatan teknis', 'implementasi', 'code', 'kode', 'gunakan library', 'saya sarankan'],
93
+ design: ['vulnerability', 'security flaw', 'celah keamanan', 'kerentanan', 'tidak aman'],
94
+ critique: ['implementasi', 'code', 'kode', 'cara implementasi', 'langkah implementasi', 'saya akan buat'],
95
+ synthesis: ['verdict', 'approved', 'rejected', 'final decision', 'keputusan akhir', 'diterima', 'ditolak'],
96
+ verify: ['desain baru', 'pendekatan baru', 'redesign', 'restrukturisasi']
97
+ };
98
+
99
+ export function validateAuthority(phaseName, output) {
100
+ if (!output || typeof output !== 'object') return [];
101
+
102
+ const warnings = [];
103
+ const forbidden = FORBIDDEN_KEYWORDS[phaseName];
104
+ if (!forbidden) return warnings;
105
+
106
+ const outputStr = JSON.stringify(output).toLowerCase();
107
+ const persona = getPersona(phaseName);
108
+
109
+ for (const keyword of forbidden) {
110
+ if (outputStr.includes(keyword.toLowerCase())) {
111
+ warnings.push(
112
+ `[BOUNDARY] Fase '${phaseName}' sebagai ${persona.name}: terdeteksi keyword '${keyword}' yang melampaui wewenang fase ini. ${persona.notAuthority[0] || ''}`
113
+ );
114
+ }
115
+ }
116
+
117
+ return warnings;
118
+ }
119
+
120
+ export function buildHandshake(fromPhase, toPhase, keyDeliverables, warnings, unresolved, conflicts) {
121
+ const fromPersona = getPersona(fromPhase);
122
+ const toPersona = getPersona(toPhase);
123
+
124
+ return {
125
+ from: fromPhase,
126
+ to: toPhase,
127
+ personaTransition: {
128
+ fromPersona: fromPersona.name,
129
+ toPersona: toPersona.name,
130
+ advice: `[${fromPersona.name}] menyerahkan ke [${toPersona.name}]. Key deliverables: ${keyDeliverables.length} item. Warnings: ${warnings.length}. Conflicts: ${conflicts.length}.`
131
+ },
132
+ keyDeliverables,
133
+ warnings: warnings.slice(0, 5),
134
+ unresolvedIssues: unresolved.slice(0, 5),
135
+ conflicts: conflicts.slice(0, 5),
136
+ message: fromPersona.handoffMessage
137
+ };
138
+ }
@@ -0,0 +1,87 @@
1
+ import { CONFIG } from '../config.js';
2
+ import { insert } from '../db/json-store.js';
3
+
4
+ const VALID_CERTAINTIES = new Set(['confirmed', 'likely', 'uncertain', 'unknown', 'assumption']);
5
+
6
+ export function validateCertainty(claims) {
7
+ if (!Array.isArray(claims) || claims.length === 0) {
8
+ return { valid: true, unlabeled: [], stats: { total: 0, labeled: 0, unlabeled: 0, distribution: {} } };
9
+ }
10
+
11
+ const unlabeled = [];
12
+ const distribution = { confirmed: 0, likely: 0, uncertain: 0, unknown: 0, assumption: 0 };
13
+
14
+ for (let i = 0; i < claims.length; i++) {
15
+ const c = claims[i];
16
+ if (!c.certainty || !VALID_CERTAINTIES.has(c.certainty)) {
17
+ unlabeled.push({ index: i, text: (c.text || '').slice(0, 80) });
18
+ } else {
19
+ distribution[c.certainty] = (distribution[c.certainty] || 0) + 1;
20
+ }
21
+ }
22
+
23
+ return {
24
+ valid: unlabeled.length === 0,
25
+ unlabeled,
26
+ stats: {
27
+ total: claims.length,
28
+ labeled: claims.length - unlabeled.length,
29
+ unlabeled: unlabeled.length,
30
+ distribution
31
+ }
32
+ };
33
+ }
34
+
35
+ export function extractKnowledgeBoundary(output) {
36
+ if (!output || typeof output !== 'object') {
37
+ return { whatIKnow: [], whatIAssume: [], whatIDontKnow: [], whatNeedsVerification: [] };
38
+ }
39
+
40
+ const kb = output.knowledgeBoundary || output.knowledge_boundary || {};
41
+ return {
42
+ whatIKnow: Array.isArray(kb.whatIKnow) ? kb.whatIKnow : [],
43
+ whatIAssume: Array.isArray(kb.whatIAssume) ? kb.whatIAssume : [],
44
+ whatIDontKnow: Array.isArray(kb.whatIDontKnow) ? kb.whatIDontKnow : [],
45
+ whatNeedsVerification: Array.isArray(kb.whatNeedsVerification) ? kb.whatNeedsVerification : []
46
+ };
47
+ }
48
+
49
+ export function checkAssumptionFlagging(claims) {
50
+ if (!Array.isArray(claims)) {
51
+ return { allFlagged: true, unflagged: [] };
52
+ }
53
+
54
+ const unflagged = [];
55
+ for (let i = 0; i < claims.length; i++) {
56
+ const c = claims[i];
57
+ const isAssumption = c.certainty === 'assumption' || c.source === 'assumption';
58
+ if (isAssumption) {
59
+ const hasLabel = c.text && c.text.length > 0;
60
+ if (!hasLabel) {
61
+ unflagged.push({ index: i });
62
+ }
63
+ }
64
+ }
65
+
66
+ return {
67
+ allFlagged: unflagged.length === 0,
68
+ unflagged
69
+ };
70
+ }
71
+
72
+ export function createTransparencyLogEntry(sessionId, phaseName, type, description, detail, certainty) {
73
+ const id = CONFIG.uuidv4();
74
+ insert('transparency_log', {
75
+ id,
76
+ session_id: sessionId,
77
+ phase_name: phaseName,
78
+ type,
79
+ description,
80
+ detail: JSON.stringify(detail || {}),
81
+ certainty: certainty || 'unknown',
82
+ resolved: 0,
83
+ resolved_at: null,
84
+ created_at: new Date().toISOString()
85
+ });
86
+ return id;
87
+ }
@@ -0,0 +1,28 @@
1
+ import { validateAuthority } from '../prinsip/agentic.js';
2
+
3
+ export function scoreAgentic(phaseName, output, persona) {
4
+ if (!output || typeof output !== 'object') {
5
+ return { score: 0, violations: ['no_output'], personaFit: 0 };
6
+ }
7
+
8
+ const violations = validateAuthority(phaseName, output);
9
+ const violationPenalty = violations.length * 25;
10
+
11
+ let personaFit = 0;
12
+ const outputStr = JSON.stringify(output).toLowerCase();
13
+
14
+ if (persona) {
15
+ const identityKeywords = persona.identity ? persona.identity.toLowerCase().split(/\s+/).filter(w => w.length > 4) : [];
16
+ const matched = identityKeywords.filter(kw => outputStr.includes(kw)).length;
17
+ personaFit = Math.min(100, Math.round((matched / Math.max(identityKeywords.length, 1)) * 100));
18
+ }
19
+
20
+ const baseScore = Math.max(0, 100 - violationPenalty);
21
+ const finalScore = Math.round(baseScore * 0.7 + personaFit * 0.3);
22
+
23
+ return {
24
+ score: finalScore,
25
+ violations,
26
+ personaFit
27
+ };
28
+ }
@@ -0,0 +1,76 @@
1
+ const PHASE_REQUIREMENTS = {
2
+ decompile: {
3
+ required: ['summary', 'decisions', 'constraints', 'confidence', 'criticalQuestions', 'claims'],
4
+ types: { summary: 'string', decisions: 'array', constraints: 'array', confidence: 'number', criticalQuestions: 'array', claims: 'array' },
5
+ minLengths: { summary: 20, decisions: 1, constraints: 1, criticalQuestions: 1 }
6
+ },
7
+ design: {
8
+ required: ['summary', 'approaches', 'justification', 'tradeoffs', 'recommended', 'decisions'],
9
+ types: { summary: 'string', approaches: 'array', justification: 'string', tradeoffs: 'array', recommended: 'string' },
10
+ minLengths: { summary: 20, approaches: 2, justification: 20, tradeoffs: 1 }
11
+ },
12
+ critique: {
13
+ required: ['summary', 'vulnerabilities', 'riskLevel', 'mitigation', 'confidence'],
14
+ types: { summary: 'string', vulnerabilities: 'array', riskLevel: 'string', mitigation: 'array', confidence: 'number' },
15
+ minLengths: { summary: 20, vulnerabilities: 1, mitigation: 1 },
16
+ enums: { riskLevel: ['low', 'medium', 'high', 'critical'] }
17
+ },
18
+ synthesis: {
19
+ required: ['summary', 'implementation', 'testing', 'warnings'],
20
+ types: { summary: 'string', implementation: 'array', testing: 'array', warnings: 'array' },
21
+ minLengths: { summary: 30, implementation: 1, testing: 1 }
22
+ },
23
+ verify: {
24
+ required: ['summary', 'checks', 'passedCount', 'failedCount', 'finalConfidence'],
25
+ types: { summary: 'string', checks: 'array', passedCount: 'number', failedCount: 'number', finalConfidence: 'number' },
26
+ minLengths: { summary: 20, checks: 1 }
27
+ }
28
+ };
29
+
30
+ export function scoreCompleteness(phaseName, output) {
31
+ const reqs = PHASE_REQUIREMENTS[phaseName];
32
+ if (!reqs || !output || typeof output !== 'object') {
33
+ return { score: 0, passed: [], failed: ['output tidak valid atau fase tidak dikenal'] };
34
+ }
35
+
36
+ const passed = [];
37
+ const failed = [];
38
+
39
+ for (const field of reqs.required) {
40
+ if (!(field in output)) {
41
+ failed.push(`${field} (missing)`);
42
+ continue;
43
+ }
44
+
45
+ const value = output[field];
46
+ const expectedType = reqs.types[field];
47
+ const actualType = Array.isArray(value) ? 'array' : typeof value;
48
+
49
+ if (expectedType !== undefined && actualType !== expectedType) {
50
+ failed.push(`${field} (type: ${actualType}, expected: ${expectedType})`);
51
+ continue;
52
+ }
53
+
54
+ if (reqs.minLengths[field]) {
55
+ const len = Array.isArray(value) ? value.length : String(value).length;
56
+ if (len < reqs.minLengths[field]) {
57
+ failed.push(`${field} (length: ${len}, min: ${reqs.minLengths[field]})`);
58
+ continue;
59
+ }
60
+ }
61
+
62
+ if (reqs.enums && reqs.enums[field]) {
63
+ if (!reqs.enums[field].includes(value)) {
64
+ failed.push(`${field} (value: "${value}", expected one of: ${reqs.enums[field].join(', ')})`);
65
+ continue;
66
+ }
67
+ }
68
+
69
+ passed.push(field);
70
+ }
71
+
72
+ const totalChecks = reqs.required.length;
73
+ const score = totalChecks > 0 ? Math.round((passed.length / totalChecks) * 100) : 0;
74
+
75
+ return { score, passed, failed };
76
+ }
@@ -0,0 +1,15 @@
1
+ import { detectContradictions } from '../verify/contradiction.js';
2
+
3
+ export function scoreConsistency(phaseName, output, previousPhases) {
4
+ if (!previousPhases || previousPhases.length === 0) {
5
+ return { score: 100, contradictions: [], warnings: [] };
6
+ }
7
+
8
+ const contradictions = detectContradictions(output, previousPhases);
9
+ const warnings = contradictions.filter(c => c.severity === 'warning');
10
+ const realContradictions = contradictions.filter(c => c.severity !== 'warning');
11
+
12
+ const score = Math.max(0, Math.round(100 - (realContradictions.length * 25 + warnings.length * 10)));
13
+
14
+ return { score, contradictions, warnings };
15
+ }
@@ -0,0 +1,101 @@
1
+ import { scoreCompleteness } from './completeness.js';
2
+ import { scoreConsistency } from './consistency.js';
3
+ import { scoreTransparency } from './transparency.js';
4
+ import { scoreAgentic } from './agentic.js';
5
+ import { CONFIG } from '../config.js';
6
+ import { getPersona } from '../prinsip/agentic.js';
7
+
8
+ function scoreDecisionQuality(output) {
9
+ if (!output?.decisions || !Array.isArray(output.decisions) || output.decisions.length === 0) {
10
+ return 0;
11
+ }
12
+
13
+ const decisions = output.decisions;
14
+ let totalScore = 0;
15
+
16
+ for (const d of decisions) {
17
+ let dScore = 0;
18
+ if (d.description && d.description.length > 5) dScore += 40;
19
+ if (d.rationale && d.rationale.length > 10) dScore += 35;
20
+ if (d.alternatives && Array.isArray(d.alternatives) && d.alternatives.length > 0) dScore += 25;
21
+ totalScore += dScore;
22
+ }
23
+
24
+ return Math.round(totalScore / decisions.length);
25
+ }
26
+
27
+ function generateSuggestions(verdict, dimensions) {
28
+ const suggestions = [];
29
+
30
+ if (dimensions.completeness.failed?.length > 0) {
31
+ suggestions.push(`Lengkapi field: ${dimensions.completeness.failed.join(', ')}`);
32
+ }
33
+
34
+ if (dimensions.consistency.contradictions?.length > 0) {
35
+ suggestions.push(`Resolve ${dimensions.consistency.contradictions.length} kontradiksi: ${dimensions.consistency.contradictions.slice(0, 3).map(c => c.detail).join('; ')}`);
36
+ }
37
+
38
+ if (dimensions.transparency.issues?.length > 0) {
39
+ suggestions.push(`Tingkatkan transparansi: ${dimensions.transparency.issues.slice(0, 3).join('; ')}`);
40
+ }
41
+
42
+ if (dimensions.agentic.violations?.length > 0) {
43
+ suggestions.push(`Perbaiki boundary: ${dimensions.agentic.violations.slice(0, 2).join('; ')}`);
44
+ }
45
+
46
+ if (verdict === 'insufficient') {
47
+ suggestions.push('Score sangat rendah. Sebaiknya ulangi fase ini dari awal dengan mengikuti protokol persona.');
48
+ } else if (verdict === 'needs_improvement') {
49
+ suggestions.push('Perbaiki item di atas sebelum melanjutkan ke fase berikutnya.');
50
+ } else if (verdict === 'excellent') {
51
+ suggestions.push('Kualitas sangat baik. Silakan lanjut ke fase berikutnya.');
52
+ }
53
+
54
+ return suggestions;
55
+ }
56
+
57
+ export function calculatePhaseScore(phaseName, output, previousPhases) {
58
+ const completeness = scoreCompleteness(phaseName, output);
59
+ const consistency = scoreConsistency(phaseName, output, previousPhases);
60
+ const decisionQuality = scoreDecisionQuality(output);
61
+ const transparency = scoreTransparency(phaseName, output);
62
+ const persona = getPersona(phaseName);
63
+ const agentic = scoreAgentic(phaseName, output, persona);
64
+
65
+ const weights = CONFIG.SCORE_WEIGHTS;
66
+
67
+ const weightedScore =
68
+ completeness.score * weights.completeness +
69
+ consistency.score * weights.consistency +
70
+ decisionQuality * weights.decisionQuality +
71
+ 100 * weights.structure +
72
+ transparency.score * weights.transparency +
73
+ agentic.score * weights.agentic;
74
+
75
+ const finalScore = Math.round(weightedScore);
76
+ const verdict = finalScore >= 90 ? 'excellent'
77
+ : finalScore >= 70 ? 'good'
78
+ : finalScore >= 50 ? 'needs_improvement'
79
+ : 'insufficient';
80
+
81
+ const suggestions = generateSuggestions(verdict, {
82
+ completeness,
83
+ consistency,
84
+ transparency,
85
+ agentic
86
+ });
87
+
88
+ return {
89
+ finalScore,
90
+ verdict,
91
+ canProceed: finalScore >= CONFIG.SCORE_PASS_THRESHOLD,
92
+ dimensions: {
93
+ completeness,
94
+ consistency,
95
+ decisionQuality: { score: decisionQuality },
96
+ transparency,
97
+ agentic
98
+ },
99
+ suggestions
100
+ };
101
+ }
@@ -0,0 +1,51 @@
1
+ import { validateCertainty, extractKnowledgeBoundary, checkAssumptionFlagging } from '../prinsip/transparency.js';
2
+
3
+ export function scoreTransparency(phaseName, output) {
4
+ if (!output || typeof output !== 'object') {
5
+ return { score: 0, detail: 'Output tidak valid', issues: ['no_output'] };
6
+ }
7
+
8
+ const issues = [];
9
+ const claims = output.claims || [];
10
+
11
+ // 1. Certainty score (40% weight in transparency)
12
+ const certaintyResult = validateCertainty(claims);
13
+ const certaintyScore = certaintyResult.stats.total > 0
14
+ ? Math.round((certaintyResult.stats.labeled / certaintyResult.stats.total) * 40)
15
+ : 0;
16
+
17
+ if (certaintyResult.stats.total === 0) {
18
+ issues.push('Tidak ada claims yang dideklarasikan');
19
+ }
20
+ if (!certaintyResult.valid) {
21
+ issues.push(`${certaintyResult.unlabeled.length} claims tanpa certainty level`);
22
+ }
23
+
24
+ // 2. Knowledge boundary score (40% weight)
25
+ const kb = extractKnowledgeBoundary(output);
26
+ let boundaryScore = 0;
27
+ if (kb.whatIKnow.length > 0) boundaryScore += 10;
28
+ if (kb.whatIAssume.length > 0) boundaryScore += 10;
29
+ if (kb.whatIDontKnow.length > 0) boundaryScore += 10;
30
+ if (kb.whatNeedsVerification.length > 0) boundaryScore += 10;
31
+
32
+ if (boundaryScore === 0) {
33
+ issues.push('Knowledge boundary tidak didefinisikan');
34
+ }
35
+
36
+ // 3. Assumption flagging score (20% weight)
37
+ const assumptionResult = checkAssumptionFlagging(claims);
38
+ const assumptionScore = assumptionResult.allFlagged ? 20 : 10;
39
+
40
+ if (!assumptionResult.allFlagged) {
41
+ issues.push(`${assumptionResult.unflagged.length} asumsi tidak di-flag dengan benar`);
42
+ }
43
+
44
+ const totalScore = certaintyScore + boundaryScore + assumptionScore;
45
+
46
+ return {
47
+ score: totalScore,
48
+ detail: `Certainty: ${certaintyScore}/40, Knowledge Boundary: ${boundaryScore}/40, Assumptions: ${assumptionScore}/20`,
49
+ issues
50
+ };
51
+ }