@runhalo/engine 0.5.0 → 0.6.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.
Files changed (40) hide show
  1. package/dist/context-analyzer.js +38 -31
  2. package/dist/context-analyzer.js.map +1 -1
  3. package/dist/fp-patterns.d.ts +36 -0
  4. package/dist/fp-patterns.js +426 -0
  5. package/dist/fp-patterns.js.map +1 -0
  6. package/dist/frameworks/angular.d.ts +11 -0
  7. package/dist/frameworks/angular.js +41 -0
  8. package/dist/frameworks/angular.js.map +1 -0
  9. package/dist/frameworks/index.js +6 -0
  10. package/dist/frameworks/index.js.map +1 -1
  11. package/dist/frameworks/react.d.ts +13 -0
  12. package/dist/frameworks/react.js +36 -0
  13. package/dist/frameworks/react.js.map +1 -0
  14. package/dist/frameworks/vue.d.ts +9 -0
  15. package/dist/frameworks/vue.js +39 -0
  16. package/dist/frameworks/vue.js.map +1 -0
  17. package/dist/graduation/fp-verdict-logger.d.ts +81 -0
  18. package/dist/graduation/fp-verdict-logger.js +130 -0
  19. package/dist/graduation/fp-verdict-logger.js.map +1 -0
  20. package/dist/graduation/graduation-codifier.d.ts +37 -0
  21. package/dist/graduation/graduation-codifier.js +205 -0
  22. package/dist/graduation/graduation-codifier.js.map +1 -0
  23. package/dist/graduation/graduation-validator.d.ts +73 -0
  24. package/dist/graduation/graduation-validator.js +204 -0
  25. package/dist/graduation/graduation-validator.js.map +1 -0
  26. package/dist/graduation/index.d.ts +71 -0
  27. package/dist/graduation/index.js +105 -0
  28. package/dist/graduation/index.js.map +1 -0
  29. package/dist/graduation/pattern-aggregator.d.ts +77 -0
  30. package/dist/graduation/pattern-aggregator.js +154 -0
  31. package/dist/graduation/pattern-aggregator.js.map +1 -0
  32. package/dist/index.d.ts +75 -0
  33. package/dist/index.js +632 -73
  34. package/dist/index.js.map +1 -1
  35. package/dist/review-board/two-agent-review.d.ts +152 -0
  36. package/dist/review-board/two-agent-review.js +463 -0
  37. package/dist/review-board/two-agent-review.js.map +1 -0
  38. package/package.json +5 -2
  39. package/rules/coppa-tier-1.yaml +17 -10
  40. package/rules/rules.json +408 -40
@@ -0,0 +1,463 @@
1
+ "use strict";
2
+ /**
3
+ * Sprint 13a Week 2: Two-Agent Review Board Prototype
4
+ *
5
+ * Two parallel AI agents review each violation from different perspectives:
6
+ * - Agent 1 (Regulatory): Legal/compliance risk lens, errs toward confirming
7
+ * - Agent 2 (Code Context): Code quality lens, errs toward precision
8
+ *
9
+ * Consensus logic: Both must agree to dismiss (consensus-to-dismiss).
10
+ * Cost: 2x single-agent (two parallel API calls), not 3x.
11
+ *
12
+ * Guardrail: False dismiss rate must be below 20% on ground truth dataset.
13
+ * If not achieved in 1.5 days, ship single-agent with calibration protocol.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.resolveConsensus = resolveConsensus;
17
+ exports.buildRegulatorySystemPrompt = buildRegulatorySystemPrompt;
18
+ exports.buildCodeContextSystemPrompt = buildCodeContextSystemPrompt;
19
+ exports.buildViolationPrompt = buildViolationPrompt;
20
+ exports.runTwoAgentReview = runTwoAgentReview;
21
+ exports.scoreConsensusVerdicts = scoreConsensusVerdicts;
22
+ exports.formatAccuracyReport = formatAccuracyReport;
23
+ // ─── Verdict Priority ───────────────────────────────────────────────────────
24
+ // Higher number = higher severity. Used for consensus resolution.
25
+ const VERDICT_PRIORITY = {
26
+ dismissed: 0,
27
+ downgraded: 1,
28
+ confirmed: 2,
29
+ escalated: 3,
30
+ };
31
+ // ─── Consensus Logic ────────────────────────────────────────────────────────
32
+ /**
33
+ * Resolve two agent verdicts into a consensus result.
34
+ *
35
+ * Rules (consensus-to-dismiss):
36
+ * 1. Both dismiss → dismissed (high confidence both agree it's FP)
37
+ * 2. Both confirm → confirmed (high agreement)
38
+ * 3. Both escalate → escalated
39
+ * 4. Both downgrade → downgraded
40
+ * 5. One confirms/escalates, one dismisses → confirmed (safety bias)
41
+ * 6. One downgrades, one confirms → confirmed at lower confidence
42
+ * 7. One downgrades, one dismisses → downgraded (middle ground)
43
+ */
44
+ function resolveConsensus(regulatory, codeContext) {
45
+ const regPriority = VERDICT_PRIORITY[regulatory.verdict] ?? 2;
46
+ const codePriority = VERDICT_PRIORITY[codeContext.verdict] ?? 2;
47
+ let finalVerdict;
48
+ let finalConfidence;
49
+ let agreementLevel;
50
+ let consensusReason;
51
+ let fpReason;
52
+ let fpPatternId;
53
+ if (regulatory.verdict === codeContext.verdict) {
54
+ // Full agreement
55
+ finalVerdict = regulatory.verdict;
56
+ finalConfidence = Math.round((regulatory.confidence + codeContext.confidence) / 2);
57
+ agreementLevel = 'full';
58
+ consensusReason = `Both agents agree: ${finalVerdict}`;
59
+ if (finalVerdict === 'dismissed') {
60
+ fpReason = regulatory.fpReason || codeContext.fpReason;
61
+ fpPatternId = regulatory.fpPatternId || codeContext.fpPatternId;
62
+ }
63
+ }
64
+ else if (Math.abs(regPriority - codePriority) === 1) {
65
+ // Partial agreement (adjacent verdicts)
66
+ agreementLevel = 'partial';
67
+ if (regPriority > codePriority) {
68
+ // Regulatory is stricter → use regulatory verdict (safety bias)
69
+ finalVerdict = regulatory.verdict;
70
+ finalConfidence = Math.max(Math.round((regulatory.confidence + codeContext.confidence) / 2) - 1, 1);
71
+ consensusReason = `Regulatory agent stricter (${regulatory.verdict}) vs code context (${codeContext.verdict}). Safety bias applied.`;
72
+ }
73
+ else {
74
+ // Code context is stricter → use code context verdict
75
+ finalVerdict = codeContext.verdict;
76
+ finalConfidence = Math.max(Math.round((regulatory.confidence + codeContext.confidence) / 2) - 1, 1);
77
+ consensusReason = `Code context agent stricter (${codeContext.verdict}) vs regulatory (${regulatory.verdict}).`;
78
+ }
79
+ }
80
+ else {
81
+ // Split verdict (non-adjacent, e.g., confirm vs dismiss)
82
+ agreementLevel = 'split';
83
+ // Consensus-to-dismiss: if EITHER agent flags concern, confirm
84
+ if (regPriority >= VERDICT_PRIORITY.confirmed || codePriority >= VERDICT_PRIORITY.confirmed) {
85
+ finalVerdict = 'confirmed';
86
+ const confirmer = regPriority >= codePriority ? regulatory : codeContext;
87
+ finalConfidence = Math.max(confirmer.confidence - 1, 1);
88
+ consensusReason = `Split verdict: ${regulatory.verdict} (regulatory) vs ${codeContext.verdict} (code). Confirmed per consensus-to-dismiss policy.`;
89
+ }
90
+ else {
91
+ // Both below confirmed (e.g., both downgraded or one dismissed + one downgraded)
92
+ finalVerdict = regPriority > codePriority ? regulatory.verdict : codeContext.verdict;
93
+ finalConfidence = Math.round((regulatory.confidence + codeContext.confidence) / 2);
94
+ consensusReason = `Split verdict resolved to ${finalVerdict}.`;
95
+ }
96
+ }
97
+ return {
98
+ ruleId: regulatory.ruleId || codeContext.ruleId,
99
+ finalVerdict,
100
+ finalConfidence: Math.max(1, Math.min(10, finalConfidence)),
101
+ regulatoryVerdict: regulatory,
102
+ codeContextVerdict: codeContext,
103
+ agreementLevel,
104
+ consensusReason,
105
+ fpReason,
106
+ fpPatternId,
107
+ };
108
+ }
109
+ // ─── System Prompts ─────────────────────────────────────────────────────────
110
+ /**
111
+ * Regulatory Compliance Agent system prompt.
112
+ * Focuses on legal risk, enforcement precedent, statutory requirements.
113
+ * Bias: Err toward confirming (safety first — missing a real violation is worse).
114
+ */
115
+ function buildRegulatorySystemPrompt(fpChecklists, repoMetadata) {
116
+ const ageContext = repoMetadata?.target_age_range
117
+ ? `The target audience is children aged ${repoMetadata.target_age_range}.`
118
+ : 'Assume the product may serve children of any age under 18.';
119
+ return `You are Agent 1 of the Halo Two-Agent Review Board: the REGULATORY COMPLIANCE SPECIALIST.
120
+
121
+ YOUR ROLE: Evaluate each violation through a legal and regulatory lens. Your priority is CHILD SAFETY. You represent the perspective of a regulator at the FTC Children's Privacy Bureau or the UK ICO.
122
+
123
+ ${ageContext}
124
+
125
+ == YOUR EVALUATION CRITERIA ==
126
+
127
+ 1. STATUTORY COMPLIANCE: Does this code violate COPPA, AADCA, UK AADC, or EU AI Act requirements as written in the statute?
128
+ 2. ENFORCEMENT PRECEDENT: Would the FTC, ICO, or a state AG pursue this pattern based on recent enforcement actions?
129
+ 3. CHILD HARM POTENTIAL: Even if technically legal, does this create risk of developmental harm to children?
130
+ 4. DATA LIFECYCLE: Does this code handle children's data in a way that creates retention, exposure, or consent gaps?
131
+
132
+ == DECISION RULES ==
133
+
134
+ 1. Check the FP checklist FIRST. If a pattern clearly matches (vendor code, test file, doc generator) → DISMISS.
135
+ 2. If NO FP pattern matches, CONFIRM. A regulator would flag this.
136
+ 3. When the violation is technically present but with mitigating factors (framework escaping, admin-only) → DOWNGRADE.
137
+ 4. When in doubt, CONFIRM. A missed real violation in a children's product is worse than a false alarm.
138
+ 5. You may ONLY dismiss if you can cite a specific FP pattern from the checklist below.
139
+
140
+ == CRITICAL CONSTRAINTS (Sprint 13b) ==
141
+ - You may ONLY use fpPatternId values that appear in the FP CHECKLISTS below. Do NOT invent new pattern IDs.
142
+ - "admin-internal" is NOT a valid dismiss reason when child/student data (email, name, PII) is being processed.
143
+ - "vendor-library" is NOT a valid dismiss reason when vendor code is bundled in a child-facing production app.
144
+ - "generic-code-pattern" or "generic-boolean-assignment" are NEVER valid dismiss reasons. Evaluate what the code DOES.
145
+ - If a code snippet handles student_email, child data, account management, or personal data → CONFIRM regardless of context.
146
+ - ~60% of violations should be confirmed. If you dismiss >50%, you are being too aggressive.
147
+
148
+ == CONFIDENCE SCALE (1-10) ==
149
+
150
+ 1-2: Clear false positive (vendor, test, doc generator) → dismiss
151
+ 3-4: Likely false positive with specific evidence → dismiss with explanation
152
+ 5-6: Ambiguous but with mitigating factors → downgrade
153
+ 7-8: Genuine compliance concern → confirm
154
+ 9-10: Critical child safety risk, potential enforcement action → confirm/escalate
155
+
156
+ == FP CHECKLISTS ==
157
+ ${fpChecklists}
158
+
159
+ Respond with ONLY a JSON array. Each element:
160
+ {
161
+ "ruleId": "string",
162
+ "verdict": "confirmed" | "downgraded" | "escalated" | "dismissed",
163
+ "confidence": <1-10>,
164
+ "reasoning": "2-3 sentences explaining your regulatory assessment",
165
+ "fpReason": "pattern name if dismissed, null otherwise",
166
+ "fpPatternId": "pattern ID if dismissed, null otherwise"
167
+ }`;
168
+ }
169
+ /**
170
+ * Code Context Agent system prompt.
171
+ * Focuses on code semantics, framework behavior, false positive detection.
172
+ * Bias: Err toward precision (fewer false positives, more accurate analysis).
173
+ */
174
+ function buildCodeContextSystemPrompt(fpChecklists, repoMetadata) {
175
+ const frameworkContext = repoMetadata?.framework
176
+ ? `The codebase uses ${repoMetadata.framework}. Consider framework-specific behaviors (auto-escaping, middleware, built-in protections).`
177
+ : '';
178
+ return `You are Agent 2 of the Halo Two-Agent Review Board: the CODE CONTEXT SPECIALIST.
179
+
180
+ YOUR ROLE: Evaluate each violation through a software engineering lens. Your priority is ACCURACY. You understand frameworks, libraries, and code patterns deeply. The regex scanner produces ~25% false positives — your job is to distinguish real violations from scanner noise.
181
+
182
+ ${frameworkContext}
183
+
184
+ == YOUR EVALUATION CRITERIA ==
185
+
186
+ 1. CODE SEMANTICS: Does the flagged code ACTUALLY do what the rule claims? Or did regex match something benign?
187
+ 2. FRAMEWORK BEHAVIOR: Does the framework handle this automatically? (e.g., React auto-escaping, Django CSRF)
188
+ 3. EXECUTION CONTEXT: Is this code reachable? Dead code, comments, examples, and config should be dismissed.
189
+ 4. PATTERN RECOGNITION: Match against known FP patterns. If the pattern clearly matches a checklist item → DISMISS.
190
+
191
+ == DECISION RULES ==
192
+
193
+ 1. Check the FP checklist FIRST. If a specific pattern matches → DISMISS with the pattern ID.
194
+ 2. Analyze the surrounding code context (5 lines before + after). Does the code ACTUALLY implement what the rule flags?
195
+ 3. Consider the framework: React JSX auto-escapes, Django templates auto-escape, etc. → DOWNGRADE if framework handles it.
196
+ 4. If the code genuinely does what the rule says with no mitigation → CONFIRM.
197
+ 5. You may ONLY dismiss if you can cite a specific FP pattern from the checklist below.
198
+
199
+ == CRITICAL CONSTRAINTS (Sprint 13b) ==
200
+ - You may ONLY use fpPatternId values that appear in the FP CHECKLISTS below. Do NOT invent new pattern IDs like "vendor-library", "generic-boolean-assignment", "auth-token-pattern", or "non-social-media-context".
201
+ - VENDOR CODE RULE: Bundled third-party code (TinyMCE, Chart.js, jQuery plugins) that executes in a child-facing production app IS in scope. Only dismiss vendor code if it is in a separate, non-bundled package.
202
+ - ADMIN CODE RULE: Admin/staff code that processes child data (student emails, account management, enrollment) is compliance-relevant. Do NOT dismiss as "admin-internal".
203
+ - GENERIC CODE RULE: Simple-looking code (boolean assignments, data assignments, function calls) can still be compliance violations. Evaluate what the code DOES in context, not how simple it looks.
204
+ - If no checklist item matches, you MUST confirm or downgrade. Never dismiss without a checklist match.
205
+ - ~60% of violations should be confirmed. If you dismiss >50%, you are being too aggressive.
206
+
207
+ == PRECISION FOCUS ==
208
+
209
+ Your value is catching FALSE POSITIVES the regex scanner produces. Key FP categories:
210
+ - Vendor/third-party code that is NOT bundled in the child-facing app
211
+ - Test/fixture/mock code that doesn't run in production
212
+ - Framework auto-mitigations (auto-escaping, CSRF protection)
213
+ - Sanitized inputs (DOMPurify, bleach, htmlspecialchars)
214
+ - Comments, documentation strings, TODO notes
215
+ - Cookie consent implementations (privacy-protective code flagged as violation)
216
+ - Media playback flagged as media capture
217
+
218
+ IMPORTANT: Do NOT dismiss admin routes or bundled vendor code automatically. These are common false-dismiss triggers.
219
+
220
+ == CONFIDENCE SCALE (1-10) ==
221
+
222
+ 1-2: Obvious FP (standalone test, comment, doc generator) → dismiss
223
+ 3-4: Clear FP pattern match → dismiss
224
+ 5-6: Framework mitigates but technically present → downgrade
225
+ 7-8: Real issue in production code → confirm
226
+ 9-10: Serious vulnerability with no mitigation → confirm/escalate
227
+
228
+ == FP CHECKLISTS ==
229
+ ${fpChecklists}
230
+
231
+ Respond with ONLY a JSON array. Each element:
232
+ {
233
+ "ruleId": "string",
234
+ "verdict": "confirmed" | "downgraded" | "escalated" | "dismissed",
235
+ "confidence": <1-10>,
236
+ "reasoning": "2-3 sentences explaining your code analysis",
237
+ "fpReason": "pattern name if dismissed, null otherwise",
238
+ "fpPatternId": "pattern ID if dismissed, null otherwise"
239
+ }`;
240
+ }
241
+ // ─── Violation Prompt Builder ───────────────────────────────────────────────
242
+ /**
243
+ * Build the violation prompt (shared by both agents — same data, different lens).
244
+ */
245
+ function buildViolationPrompt(violations) {
246
+ const items = violations.map((v, i) => {
247
+ const metaFlags = v.fileMetadata
248
+ ? `Context Flags: isVendor=${v.fileMetadata.isVendor}, isTest=${v.fileMetadata.isTest}, isAdmin=${v.fileMetadata.isAdmin}, isConsent=${v.fileMetadata.isConsent}, isDocGenerator=${v.fileMetadata.isDocGenerator}`
249
+ : 'Context Flags: not available';
250
+ const langInfo = v.fileMetadata?.language || v.language || 'unknown';
251
+ const framework = v.fileMetadata?.detectedFramework || 'unknown';
252
+ const codeBlock = v.surroundingCode || v.codeSnippet || 'No code available';
253
+ return `--- Violation ${i + 1} ---
254
+ Rule: ${v.ruleId}${v.ruleName ? ` (${v.ruleName})` : ''}
255
+ Severity: ${v.severity}
256
+ File: ${v.filePath}
257
+ Line: ${v.line}
258
+ Language: ${langInfo}
259
+ Framework: ${framework}
260
+ ${metaFlags}
261
+ AST Verdict: ${v.astVerdict || 'regex_only'}
262
+ Context Confidence: ${v.contextConfidence ?? 'N/A'}
263
+
264
+ Code:
265
+ \`\`\`
266
+ ${codeBlock}
267
+ \`\`\`
268
+
269
+ Message: ${v.message}`;
270
+ });
271
+ return `Review these ${violations.length} violation(s).
272
+
273
+ YOUR PROCESS for each violation:
274
+ 1. Check file path and metadata flags FIRST.
275
+ 2. Read the code context. Does the code ACTUALLY do what the rule says?
276
+ 3. Check the rule-specific FP checklist. Does any pattern match?
277
+ 4. Only if no dismissal reason found → CONFIRM.
278
+
279
+ ${items.join('\n\n')}
280
+
281
+ Respond with ONLY a JSON array. No other text.`;
282
+ }
283
+ async function callAnthropicAPI(systemPrompt, userPrompt, config) {
284
+ const model = config.model || 'claude-sonnet-4-20250514';
285
+ const maxTokens = config.maxTokens || 4096;
286
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
287
+ method: 'POST',
288
+ headers: {
289
+ 'Content-Type': 'application/json',
290
+ 'x-api-key': config.anthropicApiKey,
291
+ 'anthropic-version': '2023-06-01',
292
+ },
293
+ body: JSON.stringify({
294
+ model,
295
+ max_tokens: maxTokens,
296
+ system: systemPrompt,
297
+ messages: [{ role: 'user', content: userPrompt }],
298
+ }),
299
+ });
300
+ if (!response.ok) {
301
+ const errText = await response.text();
302
+ throw new Error(`Anthropic API error ${response.status}: ${errText}`);
303
+ }
304
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
305
+ const result = await response.json();
306
+ const responseText = result.content?.[0]?.text || '[]';
307
+ let verdicts;
308
+ try {
309
+ const cleaned = responseText.replace(/^```json?\n?/i, '').replace(/\n?```$/i, '').trim();
310
+ verdicts = JSON.parse(cleaned);
311
+ }
312
+ catch {
313
+ throw new Error(`Failed to parse agent response: ${responseText.substring(0, 200)}`);
314
+ }
315
+ return {
316
+ verdicts,
317
+ inputTokens: result.usage?.input_tokens || 0,
318
+ outputTokens: result.usage?.output_tokens || 0,
319
+ };
320
+ }
321
+ /**
322
+ * Run the two-agent review on a batch of violations.
323
+ * Both agents run in parallel, then consensus is resolved.
324
+ */
325
+ async function runTwoAgentReview(violations, fpChecklists, config, repoMetadata) {
326
+ const startTime = Date.now();
327
+ const regulatoryPrompt = buildRegulatorySystemPrompt(fpChecklists, repoMetadata);
328
+ const codeContextPrompt = buildCodeContextSystemPrompt(fpChecklists, repoMetadata);
329
+ const violationPrompt = buildViolationPrompt(violations);
330
+ // Run both agents in parallel
331
+ const [regulatoryResult, codeContextResult] = await Promise.all([
332
+ callAnthropicAPI(regulatoryPrompt, violationPrompt, config),
333
+ callAnthropicAPI(codeContextPrompt, violationPrompt, config),
334
+ ]);
335
+ // Resolve consensus for each violation
336
+ const consensusResults = [];
337
+ for (let i = 0; i < violations.length; i++) {
338
+ const regVerdict = regulatoryResult.verdicts[i] || {
339
+ ruleId: violations[i].ruleId,
340
+ verdict: 'confirmed',
341
+ confidence: 7,
342
+ reasoning: 'Regulatory agent could not assess. Defaulting to confirmed.',
343
+ };
344
+ const codeVerdict = codeContextResult.verdicts[i] || {
345
+ ruleId: violations[i].ruleId,
346
+ verdict: 'confirmed',
347
+ confidence: 7,
348
+ reasoning: 'Code context agent could not assess. Defaulting to confirmed.',
349
+ };
350
+ consensusResults.push(resolveConsensus(regVerdict, codeVerdict));
351
+ }
352
+ // Compute stats
353
+ const totalInputTokens = regulatoryResult.inputTokens + codeContextResult.inputTokens;
354
+ const totalOutputTokens = regulatoryResult.outputTokens + codeContextResult.outputTokens;
355
+ const costUsd = (totalInputTokens * 3.0 / 1000000) + (totalOutputTokens * 15.0 / 1000000);
356
+ const agreementStats = {
357
+ full_agreement: consensusResults.filter(r => r.agreementLevel === 'full').length,
358
+ partial_agreement: consensusResults.filter(r => r.agreementLevel === 'partial').length,
359
+ split_decisions: consensusResults.filter(r => r.agreementLevel === 'split').length,
360
+ total: consensusResults.length,
361
+ };
362
+ return {
363
+ consensusResults,
364
+ regulatoryVerdicts: regulatoryResult.verdicts,
365
+ codeContextVerdicts: codeContextResult.verdicts,
366
+ cost: {
367
+ input_tokens: totalInputTokens,
368
+ output_tokens: totalOutputTokens,
369
+ estimated_usd: costUsd,
370
+ },
371
+ latency_ms: Date.now() - startTime,
372
+ agreement_stats: agreementStats,
373
+ };
374
+ }
375
+ /**
376
+ * Score two-agent consensus verdicts against ground truth.
377
+ */
378
+ function scoreConsensusVerdicts(groundTruth, verdicts) {
379
+ let fpTotal = 0, fpDismissed = 0;
380
+ let tpTotal = 0, tpFalseDismissed = 0;
381
+ let confirmedTp = 0, confirmedTotal = 0;
382
+ let fullAgreement = 0, totalWithVerdicts = 0;
383
+ const perCategory = {};
384
+ for (const entry of groundTruth) {
385
+ const verdict = verdicts.get(entry.id);
386
+ if (!verdict)
387
+ continue;
388
+ totalWithVerdicts++;
389
+ if (verdict.agreementLevel === 'full')
390
+ fullAgreement++;
391
+ const isDismissed = verdict.finalVerdict === 'dismissed';
392
+ const isConfirmed = verdict.finalVerdict === 'confirmed' || verdict.finalVerdict === 'escalated';
393
+ if (entry.label === 'fp') {
394
+ fpTotal++;
395
+ if (isDismissed)
396
+ fpDismissed++;
397
+ }
398
+ else {
399
+ tpTotal++;
400
+ if (isDismissed)
401
+ tpFalseDismissed++;
402
+ }
403
+ if (isConfirmed) {
404
+ confirmedTotal++;
405
+ if (entry.label === 'tp')
406
+ confirmedTp++;
407
+ }
408
+ // Per-category tracking
409
+ const cat = entry.ruleCategory || entry.ruleId;
410
+ if (!perCategory[cat]) {
411
+ perCategory[cat] = { total: 0, correct: 0, accuracy: 0 };
412
+ }
413
+ perCategory[cat].total++;
414
+ const correct = (entry.label === 'fp' && isDismissed) || (entry.label === 'tp' && !isDismissed);
415
+ if (correct)
416
+ perCategory[cat].correct++;
417
+ }
418
+ // Compute per-category accuracy
419
+ for (const cat of Object.keys(perCategory)) {
420
+ perCategory[cat].accuracy = perCategory[cat].total > 0
421
+ ? Math.round((perCategory[cat].correct / perCategory[cat].total) * 1000) / 10
422
+ : 0;
423
+ }
424
+ return {
425
+ totalEntries: totalWithVerdicts,
426
+ fpDismissRate: fpTotal > 0 ? Math.round((fpDismissed / fpTotal) * 1000) / 10 : 0,
427
+ tpFalseDismissRate: tpTotal > 0 ? Math.round((tpFalseDismissed / tpTotal) * 1000) / 10 : 0,
428
+ precision: confirmedTotal > 0 ? Math.round((confirmedTp / confirmedTotal) * 1000) / 10 : 0,
429
+ agreementRate: totalWithVerdicts > 0 ? Math.round((fullAgreement / totalWithVerdicts) * 1000) / 10 : 0,
430
+ perCategory,
431
+ };
432
+ }
433
+ /**
434
+ * Format accuracy metrics as a readable report.
435
+ */
436
+ function formatAccuracyReport(metrics) {
437
+ const lines = [
438
+ '═══ Two-Agent Review Board — Accuracy Report ═══',
439
+ '',
440
+ `Total entries scored: ${metrics.totalEntries}`,
441
+ `FP dismiss rate: ${metrics.fpDismissRate}% (target: >80%)`,
442
+ `TP false dismiss: ${metrics.tpFalseDismissRate}% (target: <20%, HARD GATE)`,
443
+ `Precision: ${metrics.precision}%`,
444
+ `Agent agreement: ${metrics.agreementRate}%`,
445
+ '',
446
+ '── Per-Category Breakdown ──',
447
+ ];
448
+ const sorted = Object.entries(metrics.perCategory)
449
+ .sort(([, a], [, b]) => b.total - a.total);
450
+ for (const [cat, data] of sorted) {
451
+ lines.push(` ${cat.padEnd(25)} ${data.correct}/${data.total} (${data.accuracy}%)`);
452
+ }
453
+ lines.push('');
454
+ if (metrics.tpFalseDismissRate >= 20) {
455
+ lines.push('FAIL: TP false dismiss rate exceeds 20% guardrail.');
456
+ lines.push('ACTION: Revert to single-agent. Revisit multi-agent in Sprint 13b.');
457
+ }
458
+ else {
459
+ lines.push('PASS: TP false dismiss rate within guardrail.');
460
+ }
461
+ return lines.join('\n');
462
+ }
463
+ //# sourceMappingURL=two-agent-review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"two-agent-review.js","sourceRoot":"","sources":["../../src/review-board/two-agent-review.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@runhalo/engine",
3
- "version": "0.5.0",
4
- "description": "Halo rule engine — child online safety compliance detection with tree-sitter AST analysis. 114 rules across 10 jurisdictions including COPPA, UK AADC, EU DSA, EU AI Act, and more.",
3
+ "version": "0.6.0",
4
+ "description": "Halo rule engine — child online safety compliance detection. 130 rules across 10 packs covering COPPA, UK AADC, EU DSA, EU AI Act, and more.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
@@ -37,6 +37,9 @@
37
37
  },
38
38
  "author": "Mindful Media <hello@mindfulmedia.org> (https://mindfulmedia.org)",
39
39
  "license": "Apache-2.0",
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
40
43
  "dependencies": {
41
44
  "glob": "^10.3.10",
42
45
  "js-yaml": "^3.14.2",
@@ -13,7 +13,7 @@ rules:
13
13
  name: Unverified Social Login Providers
14
14
  coppaSection: "§ 312.5 (Parental Consent) & COPPA 2.0 § 312.11 (Mixed Audience)"
15
15
  severity: critical
16
- penaltyRange: "$51,744 per violation"
16
+ penaltyRange: "$53,088 per violation"
17
17
  languages:
18
18
  - typescript
19
19
  - javascript
@@ -46,7 +46,7 @@ rules:
46
46
  name: Sensitive Data in GET Requests
47
47
  coppaSection: "§ 312.2 (Definitions - Personal Information)"
48
48
  severity: high
49
- penaltyRange: "$51,744 per violation"
49
+ penaltyRange: "$53,088 per violation"
50
50
  languages:
51
51
  - typescript
52
52
  - javascript
@@ -80,7 +80,7 @@ rules:
80
80
  name: Restricted Third-Party Ad Trackers
81
81
  coppaSection: "§ 312.2 (Definitions - Support for Internal Operations) - COPPA 2.0 Focus"
82
82
  severity: critical
83
- penaltyRange: "$51,744 per violation"
83
+ penaltyRange: "$53,088 per violation"
84
84
  languages:
85
85
  - typescript
86
86
  - javascript
@@ -123,7 +123,7 @@ rules:
123
123
  name: Precise Geolocation Without Parental Consent
124
124
  coppaSection: "§ 312.2 (Personal Information)"
125
125
  severity: high
126
- penaltyRange: "$51,744 per violation"
126
+ penaltyRange: "$53,088 per violation"
127
127
  languages:
128
128
  - typescript
129
129
  - javascript
@@ -159,18 +159,23 @@ rules:
159
159
 
160
160
  # ============================================
161
161
  # 5. coppa-retention-005: Missing Data Retention Policy
162
+ # Updated: 2026-03-12 — COPPA 2025 final rule explicitly
163
+ # prohibits indefinite retention (§ 312.10)
162
164
  # ============================================
163
165
  - metadata:
164
166
  id: coppa-retention-005
165
167
  name: Missing Data Retention Policy in Database Schemas
166
- coppaSection: "§ 312.10 (Data Retention and Deletion) - COPPA 2.0 Focus"
168
+ coppaSection: "§ 312.10 (Data Retention and Deletion) - COPPA 2025 Final Rule"
167
169
  severity: medium
168
- penaltyRange: "Regulatory scrutiny / Audit failure"
170
+ penaltyRange: "$53,088 per violation (indefinite retention prohibition)"
169
171
  languages:
170
172
  - typescript
171
173
  - javascript
172
174
  - python
173
175
  - sql
176
+ - go
177
+ - java
178
+ - kotlin
174
179
  falsePositiveRisk: high
175
180
  patterns:
176
181
  - regex:
@@ -186,17 +191,19 @@ rules:
186
191
  pattern: "class\\s+\\w+.*extends\\s+Model(?!.*deletedAt)"
187
192
  flags: "g"
188
193
  autoFix:
189
- description: "Add TTL index or expiration field to schema"
194
+ description: "Add explicit retention period, TTL index, and deletion mechanism per COPPA 2025 § 312.10"
190
195
  replacement: |
191
- // Mongoose - Add TTL index
196
+ // Mongoose - Add TTL index with explicit retention period
192
197
  new Schema({
193
198
  createdAt: { type: Date, expires: '365d' },
199
+ retentionDays: { type: Number, default: 365 },
194
200
  email: String
195
201
  });
196
-
197
- // Sequelize - Add deletedAt
202
+
203
+ // Sequelize - Add deletedAt with retention config
198
204
  sequelize.define('User', {
199
205
  deletedAt: { type: DataTypes.DATE },
206
+ retentionDays: { type: DataTypes.INTEGER, defaultValue: 365 },
200
207
  // ... other fields
201
208
  }, {
202
209
  paranoid: true