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 6x 6x 6x 6x 1x 5x 53x 53x 1x 52x 52x 4x 3x 3x 3x 39x 39x 3x 4x 4x 4x 1x 3x 3x 3x 3x 3x 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);
Iif (!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) {
Iif (!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`);
}
Iif (!t.electionCycleAnchor) {
throw new Error(`Invalid registry entry "${t.id}": electionCycleAnchor is required`);
}
Iif (!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}"`,
);
}
Iif (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);
}
}
|