All files / src/aggregator article-generator.ts

21.42% Statements 6/28
18.18% Branches 2/11
33.33% Functions 1/3
23.07% Lines 6/26

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 112 113 114 115 116 117 118 119 120 121                                                                                                                                                                                                              7x 7x 7x 7x 7x           7x              
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
 
/**
 * @module Aggregator/ArticleGenerator
 * @description Barrel + thin CLI entry point for the article generator.
 *
 * The library was split (Refactor 7/8) into focused sub-modules under
 * `./generator/` so the public API is importable without CLI
 * side-effects:
 *
 *   - `./generator/cli.ts`                   — `parseCliArgs`, `CliOptions`
 *   - `./generator/slug.ts`                  — slug + default-description helpers
 *   - `./generator/discovery.ts`             — `discoverAnalysisRuns`, grouping
 *   - `./generator/reader-guide-insertion.ts`— Reader guide splice helper
 *   - `./generator/render-one.ts`            — `generateArticle(...)` single-run
 *   - `./generator/render-batch.ts`          — `generateAllArticles(opts)`
 *
 * This file remains the compiled CLI entry — `npm run generate-article`
 * resolves to `scripts/aggregator/article-generator.js`. The
 * `process.exit`, `console.log`, and `process.argv` side-effects live
 * inside the `isMain` block below so that programmatic importers
 * (production aggregator code, the test suite, downstream curators)
 * see only the pure barrel exports.
 *
 * **Always-14-languages-always-HTML contract**: every CLI invocation
 * renders every supported language to HTML. The legacy `--lang` /
 * `--language` / `--markdown-only` flags have been removed. The
 * programmatic `generateArticle()` API still accepts `langs` and
 * `markdownOnly` for tests that need to scope a render for speed.
 */
 
import path from 'path';
import { pathToFileURL } from 'url';
 
// CLI parsing
export { parseCliArgs, type CliOptions } from './generator/cli.js';
 
// Slug + default description
export {
  buildArticleSlug,
  sanitizeRunSuffix,
  extractDefaultDescription,
} from './generator/slug.js';
 
// Discovery + grouping
export {
  discoverAnalysisRuns,
  groupRunsForCollision,
  type DiscoveredRun,
} from './generator/discovery.js';
 
// Reader guide insertion
export { insertReaderGuideAfterExecutiveBrief } from './generator/reader-guide-insertion.js';
 
// Progressive disclosure reading-time helpers
export {
  estimateReadingMinutes,
  buildLayerReadingTimes,
  splitBodyIntoDisclosureLayers,
} from './progressive-disclosure.js';
 
// Single-run + batch orchestrators
export { generateArticle, type GenerateResult } from './generator/render-one.js';
export { generateAllArticles } from './generator/render-batch.js';
 
import { parseCliArgs } from './generator/cli.js';
import { generateArticle } from './generator/render-one.js';
import { generateAllArticles } from './generator/render-batch.js';
 
/**
 * Main entry when invoked as a script. Uses `process.argv.slice(2)` and the
 * current working directory as repo root unless overridden by `REPO_ROOT`.
 *
 * @param argv - Argument list (defaults to `process.argv.slice(2)`)
 */
export async function main(argv: readonly string[] = process.argv.slice(2)): Promise<void> {
  const repoRoot = process.env.REPO_ROOT ? path.resolve(process.env.REPO_ROOT) : process.cwd();
  const opts = parseCliArgs(argv, repoRoot);
  if (opts.all) {
    const results = generateAllArticles(opts);
    let totalFiles = 0;
    let totalArtifacts = 0;
    for (const r of results) {
      totalFiles += r.writtenFiles.length;
      totalArtifacts += r.aggregated.includedArtifacts.length;
      process.stdout.write(
        `  [${r.aggregated.date}/${r.aggregated.articleType}] ${r.writtenFiles.length} file(s) · ${r.aggregated.includedArtifacts.length} artifact(s) · gate ${r.aggregated.gateResult}\n`
      );
    }
    process.stdout.write(
      `Generated ${totalFiles} file(s) across ${results.length} run(s) from ${totalArtifacts} total artifact(s)\n`
    );
    return;
  }
  const result = generateArticle(opts);
  process.stdout.write(
    `Generated ${result.writtenFiles.length} file(s) from ${result.aggregated.includedArtifacts.length} artifact(s) — gate: ${result.aggregated.gateResult}\n`
  );
  for (const f of result.writtenFiles) process.stdout.write(`  ${f}\n`);
}
 
// Only run when invoked directly, not when imported by tests.
const isMain = (() => {
  const entry = process.argv[1];
  Iif (!entry) return false;
  try {
    return import.meta.url === pathToFileURL(entry).href;
  } catch {
    return false;
  }
})();
 
Iif (isMain) {
  main().catch((err: unknown) => {
    const msg = err instanceof Error ? err.message : String(err);
    process.stderr.write(`generate-article: ${msg}\n`);
    process.exit(1);
  });
}