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 122 123 124 | 1x 1x 1x 1x 1x 11x 11x 11x 1x 10x 1x 9x 69x 1x 68x 1x 67x 1x 66x 1x 5x 4x 4x 4x 52x 52x 4x 6x 6x 6x 1x 5x 1x 4x 4x 4x 4x 1x 1x 1x 1x 1x 1x 1x | /**
* @module scripts/generate-article-types-doc
* @description Reads `analysis/article-types.json` and replaces the content
* between `<!-- ARTICLE-TYPES:BEGIN -->` / `<!-- ARTICLE-TYPES:END -->`
* sentinels in `Article-Generation.md` with an auto-generated
* Markdown table.
*
* Invoked as part of `prebuild` in `package.json`.
* Idempotent — running twice produces the same file.
* Exits non-zero if the registry fails structural validation.
*
* @author Hack23 AB
* @license Apache-2.0
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { ArticleTypesRegistry, ArticleTypeEntry } from './horizon-context.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const repoRoot = resolve(__dirname, '..');
const SENTINEL_BEGIN = '<!-- ARTICLE-TYPES:BEGIN -->';
const SENTINEL_END = '<!-- ARTICLE-TYPES:END -->';
/**
* Load and validate the article-types registry.
*/
export function loadAndValidateRegistry(registryPath: string): ArticleTypesRegistry {
const raw = readFileSync(registryPath, 'utf8');
const registry: ArticleTypesRegistry = JSON.parse(raw);
if (!registry.version || !registry.types || !Array.isArray(registry.types)) {
throw new Error('Invalid registry: missing version or types array');
}
if (registry.types.length === 0) {
throw new Error('Invalid registry: types array is empty');
}
for (const t of registry.types) {
if (!t.id || !t.family || t.horizonDays == null || t.tierCMultiplier == null) {
throw new Error(`Invalid registry entry: missing required fields in type "${t.id ?? '(unknown)'}"`);
}
if (t.articleWordFloor == null || typeof t.articleWordFloor !== 'number') {
throw new Error(`Invalid registry entry "${t.id}": articleWordFloor must be a number`);
}
if (!t.electionCycleAnchor) {
throw new Error(`Invalid registry entry "${t.id}": electionCycleAnchor is required`);
}
if (!t.dispatchOnly && !t.cronExpression) {
throw new Error(`Invalid registry entry "${t.id}": cronExpression required when dispatchOnly is not true`);
}
}
return registry;
}
/**
* Render the article-types Markdown table from the registry.
*/
export function renderTable(types: readonly ArticleTypeEntry[]): string {
const header = '| id | family | horizonDays | tierCMultiplier | articleWordFloor | electionCycleAnchor | cronExpression |';
const separator = '|---|---|---|---|---|---|---|';
const rows = types.map((t) => {
const cron = t.dispatchOnly ? '_dispatch-only_' : `\`${t.cronExpression ?? '—'}\``;
return `| ${t.id} | ${t.family} | ${t.horizonDays} | ${t.tierCMultiplier} | ${t.articleWordFloor} | ${t.electionCycleAnchor} | ${cron} |`;
});
return [header, separator, ...rows].join('\n');
}
/**
* Replace content between sentinels in the target document.
* Returns the updated document content.
*/
export function replaceBetweenSentinels(doc: string, table: string): string {
const beginIdx = doc.indexOf(SENTINEL_BEGIN);
const endIdx = doc.indexOf(SENTINEL_END);
if (beginIdx === -1 || endIdx === -1) {
throw new Error(
`Sentinels not found in document. Expected "${SENTINEL_BEGIN}" and "${SENTINEL_END}"`,
);
}
if (endIdx <= beginIdx) {
throw new Error('ARTICLE-TYPES:END sentinel appears before ARTICLE-TYPES:BEGIN');
}
const before = doc.slice(0, beginIdx + SENTINEL_BEGIN.length);
const after = doc.slice(endIdx);
const warning = '<!-- ⚠️ AUTO-GENERATED from analysis/article-types.json — do NOT edit manually -->';
return `${before}\n${warning}\n\n${table}\n\n${after}`;
}
/**
* Main entry point — generates the table and writes the doc.
*/
export function generate(
registryPath: string = resolve(repoRoot, 'analysis/article-types.json'),
docPath: string = resolve(repoRoot, 'Article-Generation.md'),
): void {
const registry = loadAndValidateRegistry(registryPath);
const table = renderTable(registry.types);
const doc = readFileSync(docPath, 'utf8');
const updated = replaceBetweenSentinels(doc, table);
writeFileSync(docPath, updated, 'utf8');
}
// Run when executed directly
const isMain = process.argv[1] && resolve(process.argv[1]) === __filename;
Iif (isMain) {
try {
generate();
console.log('✅ Article-Generation.md article-types table regenerated.');
} catch (err: unknown) {
console.error('❌', (err as Error).message);
process.exit(1);
}
}
|