All files / src/aggregator/run tradecraft.ts

100% Statements 35/35
86.66% Branches 13/15
100% Functions 7/7
100% Lines 32/32

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 105 106 107 108 109 110 111                              17x                           56x 56x 112x 112x 80x 80x 1962x 1962x 1815x       56x                   2270x     5383x                         1737x 1737x 58x             58x 34x 34x 34x 1235x 1235x   34x   58x 41x 41x 41x 502x 502x   41x   58x                           533x    
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
 
/**
 * @module Aggregator/Run/Tradecraft
 * @description Tradecraft-appendix rendering plus the canonical `humanize`
 * helper used across the aggregator. Owns discovery of methodology and
 * template files referenced from every aggregated run.
 */
 
import fs from 'fs';
import path from 'path';
import { githubBlobUrl } from '../clean-artifact.js';
import { TRADECRAFT_SECTION_ID, TRADECRAFT_SECTION_TITLE } from '../artifact-order.js';
 
const TRADECRAFT_EXCLUDED_FILES = new Set([
  'analysis/methodologies/executive-brief-translation-guide.md',
  'analysis/templates/executive-brief-translation-template.md',
]);
 
/**
 * Discover tradecraft files (methodologies + templates) under a repo root.
 * Returned paths are repo-relative with POSIX separators and sorted
 * lexically.
 *
 * @param repoRoot - Absolute path of the repo root
 * @returns Sorted list of `analysis/methodologies/*.md` + `analysis/templates/*.md`
 */
export function discoverTradecraftFiles(repoRoot: string): string[] {
  const result: string[] = [];
  for (const sub of ['analysis/methodologies', 'analysis/templates']) {
    const dir = path.join(repoRoot, sub);
    if (!fs.existsSync(dir)) continue;
    const entries = fs.readdirSync(dir, { withFileTypes: true });
    for (const entry of entries) {
      const rel = `${sub}/${entry.name}`;
      if (entry.isFile() && entry.name.endsWith('.md') && !TRADECRAFT_EXCLUDED_FILES.has(rel)) {
        result.push(rel);
      }
    }
  }
  return result.sort();
}
 
/**
 * Human-friendly title derived from a file stem (kebab/snake → Title Case).
 *
 * @param stem - File stem (e.g. `synthesis-summary.md`)
 * @returns Humanised title (e.g. `Synthesis Summary`)
 */
function humanize(stem: string): string {
  return stem
    .replace(/[-_]+/g, ' ')
    .replace(/\.md$/i, '')
    .replace(/\b([a-z])/g, (_, c: string) => c.toUpperCase())
    .trim();
}
 
/**
 * Render the tradecraft-references appendix — one bullet per
 * methodology/template file with a GitHub blob link.
 *
 * @param files - Repo-relative paths under `analysis/methodologies/` and
 *                `analysis/templates/`
 * @returns Markdown block with two subsections (methodologies, templates)
 */
export function renderTradecraftAppendix(files: readonly string[]): string {
  const methods = files.filter((f) => f.startsWith('analysis/methodologies/'));
  const templates = files.filter((f) => f.startsWith('analysis/templates/'));
  const block: string[] = [
    `<h2 id="${TRADECRAFT_SECTION_ID}">${TRADECRAFT_SECTION_TITLE}</h2>`,
    '',
    'This article is produced under the [Hack23 AB](https://hack23.com) intelligence tradecraft library. Every methodology and artifact template applied to this run is linked below.',
    '',
    '',
  ];
  if (templates.length > 0) {
    block.push('### Artifact templates');
    block.push('');
    for (const rel of templates) {
      const stem = rel.split('/').pop()?.replace(/\.md$/i, '') ?? rel;
      block.push(`- [${humanize(stem)}](${githubBlobUrl(rel)})`);
    }
    block.push('');
  }
  if (methods.length > 0) {
    block.push('### Methodologies');
    block.push('');
    for (const rel of methods) {
      const stem = rel.split('/').pop()?.replace(/\.md$/i, '') ?? rel;
      block.push(`- [${humanize(stem)}](${githubBlobUrl(rel)})`);
    }
    block.push('');
  }
  return block.join('\n');
}
 
/**
 * Public re-export of the internal `humanize` helper so other aggregator
 * modules (in particular `article-html.ts`) can derive the same display
 * title from a file stem when no curated title is available. Keeping the
 * single canonical implementation here avoids duplicate humanisation
 * rules drifting across modules.
 *
 * @param stem - File stem (e.g. `electoral-cycle-methodology`)
 * @returns Humanised title (e.g. `Electoral Cycle Methodology`)
 */
export function humanizeStem(stem: string): string {
  return humanize(stem);
}