All files / scripts/validators/article/rules landmarks.ts

100% Statements 17/17
93.75% Branches 15/16
100% Functions 1/1
100% Lines 17/17

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                                                      2x                                                     4x 4x 16x 4x             4x 4x 1x             4x 4x 3x 3x 3x 3x 3x 2x             4x    
/**
 * @module scripts/validators/article/rules/landmarks
 * @description Reader-facing landmark guard — every aggregated article
 *              must contain Reader Intelligence Guide, Executive Brief,
 *              BLUF, and Article Sources sections.
 *
 *              Rule census: extracted from
 *              `scripts/validate-article.ts` lines 83–104
 *              (`REQUIRED_LANDMARKS`). Logic is byte-identical to the
 *              original.
 *
 * @author Hack23 AB
 * @license Apache-2.0
 */
 
/**
 * Reader-Facing Output Contract — every aggregated article must contain
 * these reader-facing landmarks. The aggregator generates the Reader
 * Intelligence Guide and the Article Sources appendix automatically;
 * the BLUF and Executive Brief sections come from the
 * `executive-brief.md` artifact and are guarded here so a malformed
 * template can't ship a headless article.
 */
export const REQUIRED_LANDMARKS: ReadonlyArray<{
  pattern: RegExp;
  label: string;
  code: string;
}> = [
  {
    pattern: /^##\s+Reader Intelligence Guide\s*$/m,
    label: 'Reader Intelligence Guide',
    code: 'missing-reader-guide',
  },
  {
    pattern: /^##\s+Executive Brief\s*$/m,
    label: '## Executive Brief section',
    code: 'missing-executive-brief',
  },
  {
    pattern: /^#{2,6}\s+(?:[^\n]*?\s)?BLUF\b/im,
    label: 'BLUF heading inside the executive brief',
    code: 'missing-bluf',
  },
  {
    pattern: /^##\s+Article Sources\s*$/m,
    label: 'Article Sources appendix',
    code: 'missing-sources-appendix',
  },
];
 
import type { ArticleViolation } from '../types.js';
 
/** Required-landmark + duplicate-Reader-Guide rule. */
export function checkLandmarks(rel: string, text: string): ArticleViolation[] {
  const out: ArticleViolation[] = [];
  for (const landmark of REQUIRED_LANDMARKS) {
    if (!landmark.pattern.test(text)) {
      out.push({
        file: rel,
        code: landmark.code,
        message: `Aggregated article is missing the required "${landmark.label}". The aggregator generates Reader Intelligence Guide and Article Sources automatically; missing Executive Brief / BLUF means the source artifact is malformed.`,
      });
    }
  }
  const guideMatches = text.match(/^##\s+Reader Intelligence Guide\s*$/gm);
  if (guideMatches && guideMatches.length > 1) {
    out.push({
      file: rel,
      code: 'duplicate-reader-guide',
      message: `Reader Intelligence Guide heading appears ${guideMatches.length} times — must be exactly once. The cleaning pipeline should strip inline duplicates before the aggregator emits the canonical instance.`,
    });
  }
  // Reader-guide table empty-row guard
  const guideHeadingMatch = text.match(/^##\s+Reader Intelligence Guide\s*$/m);
  if (guideHeadingMatch && guideHeadingMatch.index !== undefined) {
    const after = text.slice(guideHeadingMatch.index + guideHeadingMatch[0].length);
    const stop = after.search(/^##\s+\S/m);
    const region = stop === -1 ? after : after.slice(0, stop);
    const dataRows = region.match(/^\|\s*(?:[^|\n]+\|\s*)?\[/gm) ?? [];
    if (dataRows.length === 0) {
      out.push({
        file: rel,
        code: 'reader-guide-empty-table',
        message: `Reader Intelligence Guide table has zero data rows — the aggregator should emit at least one row per available artifact lens. Verify the artifact set is non-empty and re-aggregate.`,
      });
    }
  }
  return out;
}