All files / scripts/agentic/gate-checks devils-advocate.ts

92% Statements 23/25
75% Branches 9/12
100% Functions 3/3
91.3% Lines 21/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104                                                      8x 8x 8x   6x 6x     6x   8x               6x                     6x       6x 1x           1x     5x       5x   34x 14x 5x             5x 1x             4x               5x    
/**
 * @module scripts/agentic/gate-checks/devils-advocate
 * @description Check 7d — Validate devils-advocate.md (≥3 competing
 *              hypotheses + KJ Coverage Matrix completeness).
 *
 * @see .github/prompts/05-analysis-gate.md §Check 7 (devils-advocate)
 * @author Hack23 AB
 * @license Apache-2.0
 */
 
import { readFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
 
import {
  extractSection,
  hasHeading,
} from '../gate-shared/markdown-helpers.js';
import type { GateCheckResult } from '../gate-shared/types.js';
 
/**
 * Check devils-advocate.md for ≥3 competing hypotheses and the KJ
 * Coverage Matrix (mandatory in the template).
 */
export async function checkDevilsAdvocate(
  analysisDir: string,
): Promise<GateCheckResult[]> {
  const results: GateCheckResult[] = [];
  const filePath = join(analysisDir, 'devils-advocate.md');
  if (!existsSync(filePath)) return results;
 
  const content = await readFile(filePath, 'utf-8');
  const hyMatches = content.match(
    /^#{2,4}\s*(Hypothesis|H[0-9]+\s*[:.—-])/gm,
  );
  const count = hyMatches ? hyMatches.length : 0;
 
  Iif (count < 3) {
    results.push({
      checkId: 'family-c-structure',
      passed: false,
      message: `devils-advocate.md: fewer than 3 competing hypotheses (found ${count})`,
      artifact: 'devils-advocate.md',
    });
  } else {
    results.push({
      checkId: 'family-c-structure',
      passed: true,
      message: `devils-advocate.md: ${count} hypotheses found`,
      artifact: 'devils-advocate.md',
    });
  }
 
  // KJ Coverage Matrix enforcement — the template documents this as a
  // required gate; verify the matching ## heading and that every KJ row is
  // covered (no ❌ markers on KJ rows).
  const kjMatrixHeadingPresent = hasHeading(
    content,
    /Key\s+Judgment\s+Coverage\s+Matrix/i,
  );
  if (!kjMatrixHeadingPresent) {
    results.push({
      checkId: 'family-c-structure',
      passed: false,
      message: "devils-advocate.md: missing '## Key Judgment Coverage Matrix' section",
      artifact: 'devils-advocate.md',
    });
    return results;
  }
 
  const matrixSection = extractSection(
    content,
    /Key\s+Judgment\s+Coverage\s+Matrix/i,
  );
  const kjRows = matrixSection
    .split('\n')
    .filter((line) => /^\|\s*KJ[-\s]?\d/i.test(line.trim()));
  const uncoveredRow = kjRows.find((row) => /❌/.test(row));
  Iif (kjRows.length === 0) {
    results.push({
      checkId: 'family-c-structure',
      passed: false,
      message: 'devils-advocate.md: KJ Coverage Matrix has no KJ rows (expected ≥1 row mapping each intelligence-assessment KJ to a hypothesis)',
      artifact: 'devils-advocate.md',
    });
  } else if (uncoveredRow) {
    results.push({
      checkId: 'family-c-structure',
      passed: false,
      message: 'devils-advocate.md: KJ Coverage Matrix has ❌ row(s); coverage must be 100%',
      artifact: 'devils-advocate.md',
    });
  } else {
    results.push({
      checkId: 'family-c-structure',
      passed: true,
      message: `devils-advocate.md: KJ Coverage Matrix has ${kjRows.length} covered KJ row(s)`,
      artifact: 'devils-advocate.md',
    });
  }
 
  return results;
}