euparliamentmonitor 0.8.54 → 0.8.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/package.json +3 -1
  2. package/scripts/aggregator/analysis-aggregator.d.ts +7 -20
  3. package/scripts/aggregator/analysis-aggregator.js +39 -14
  4. package/scripts/aggregator/article-generator.d.ts +6 -0
  5. package/scripts/aggregator/article-generator.js +62 -0
  6. package/scripts/aggregator/article-html.js +52 -8
  7. package/scripts/aggregator/article-meta.d.ts +121 -0
  8. package/scripts/aggregator/article-meta.js +320 -0
  9. package/scripts/aggregator/artifact-order.js +1 -1
  10. package/scripts/aggregator/key-takeaways.d.ts +72 -0
  11. package/scripts/aggregator/key-takeaways.js +213 -0
  12. package/scripts/aggregator/lead-extractor.d.ts +30 -0
  13. package/scripts/aggregator/lead-extractor.js +202 -0
  14. package/scripts/aggregator/markdown-renderer.js +20 -0
  15. package/scripts/aggregator/reader-guide-constants.d.ts +35 -0
  16. package/scripts/aggregator/reader-guide-constants.js +23 -0
  17. package/scripts/aggregator/reader-intelligence-guide.d.ts +46 -0
  18. package/scripts/aggregator/reader-intelligence-guide.js +426 -0
  19. package/scripts/constants/language-ui.d.ts +4 -0
  20. package/scripts/constants/language-ui.js +34 -0
  21. package/scripts/constants/languages.d.ts +1 -1
  22. package/scripts/constants/languages.js +1 -1
  23. package/scripts/generators/news-indexes.js +111 -6
  24. package/scripts/generators/political-intelligence/html.js +68 -11
  25. package/scripts/generators/seo-copy.d.ts +44 -0
  26. package/scripts/generators/seo-copy.js +398 -0
  27. package/scripts/generators/sitemap/html.js +75 -4
  28. package/scripts/lint-prompts.js +59 -0
  29. package/scripts/templates/section-builders.d.ts +7 -0
  30. package/scripts/templates/section-builders.js +29 -4
  31. package/scripts/templates/sync-template-frontmatter.js +385 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "euparliamentmonitor",
3
- "version": "0.8.54",
3
+ "version": "0.8.56",
4
4
  "type": "module",
5
5
  "description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
6
6
  "main": "scripts/index.js",
@@ -55,6 +55,8 @@
55
55
  "build:check-tests": "tsc --project tsconfig.test.json --noEmit",
56
56
  "copy-vendor": "node scripts/copy-vendor.js",
57
57
  "validate-analysis": "node scripts/validate-analysis-completeness.js",
58
+ "sync:templates": "node scripts/templates/sync-template-frontmatter.js",
59
+ "sync:templates:check": "node scripts/templates/sync-template-frontmatter.js --check",
58
60
  "prior-run-diff": "node scripts/aggregator/prior-run-diff.js",
59
61
  "generate-article": "node scripts/aggregator/article-generator.js",
60
62
  "generate-article:all": "node scripts/aggregator/article-generator.js --all",
@@ -1,5 +1,8 @@
1
1
  import { type ArtifactSection } from './artifact-order.js';
2
2
  import { type Manifest, type ManifestFiles } from './manifest/index.js';
3
+ import type { TocSection, IncludedArtifact } from './reader-guide-constants.js';
4
+ export type { TocSection, IncludedArtifact } from './reader-guide-constants.js';
5
+ export { READER_GUIDE_SECTION_ID, READER_GUIDE_SECTION_IDS, READER_GUIDE_SECTION_TITLE, } from './reader-guide-constants.js';
3
6
  /** Result of {@link aggregateAnalysisRun}. */
4
7
  export interface AggregatedRun {
5
8
  /** Final Markdown document (provenance + sections + appendices). */
@@ -23,26 +26,6 @@ export interface AggregatedRun {
23
26
  */
24
27
  readonly sectionToc: readonly TocSection[];
25
28
  }
26
- /** One entry in the article-level table of contents (H2 level). */
27
- export interface TocSection {
28
- /** Fragment identifier — matches the `id="…"` on the rendered H2. */
29
- readonly id: string;
30
- /** Display title shown in the sidebar nav. */
31
- readonly title: string;
32
- }
33
- /** Metadata for one artifact included in the aggregate. */
34
- export interface IncludedArtifact {
35
- /** Path relative to the run dir. */
36
- readonly runRelPath: string;
37
- /** Path relative to the repo root. */
38
- readonly repoRelPath: string;
39
- /** Id of the section this artifact belongs to. */
40
- readonly sectionId: string;
41
- }
42
- /** Id of the generated reader guide section. */
43
- export declare const READER_GUIDE_SECTION_ID = "reader-intelligence-guide";
44
- /** Display title of the generated reader guide section. */
45
- export declare const READER_GUIDE_SECTION_TITLE = "Reader Intelligence Guide";
46
29
  /** Options for {@link aggregateAnalysisRun}. */
47
30
  export interface AggregateOptions {
48
31
  /** Absolute path to the analysis run directory. */
@@ -146,6 +129,10 @@ export declare function renderAnalysisIndex(included: readonly IncludedArtifact[
146
129
  * artifact sections. It gives readers a Riksdagsmonitor-style navigation layer
147
130
  * without requiring agents to hand-author another artifact.
148
131
  *
132
+ * Section membership is checked against `READER_GUIDE_SECTION_IDS` (the
133
+ * canonical list shared with the HTML renderer in `reader-intelligence-guide.ts`)
134
+ * to prevent drift between the two renderers.
135
+ *
149
136
  * @param sections - Emitted section TOC entries, in document order
150
137
  * @param included - Included artifacts, used to name each section's source
151
138
  * @returns Markdown block containing the guide table
@@ -14,11 +14,10 @@ import path from 'path';
14
14
  import { ARTIFACT_SECTIONS, MANIFEST_SECTION_ID, MANIFEST_SECTION_TITLE, SUPPLEMENTARY_SECTION_ID, SUPPLEMENTARY_SECTION_TITLE, TRADECRAFT_SECTION_ID, TRADECRAFT_SECTION_TITLE, } from './artifact-order.js';
15
15
  import { cleanArtifact, githubBlobUrl } from './clean-artifact.js';
16
16
  import { treeUrl } from './infra/github-urls.js';
17
+ import { buildKeyTakeaways, KEY_TAKEAWAYS_SECTION_ID, KEY_TAKEAWAYS_SECTION_TITLE, } from './key-takeaways.js';
17
18
  import { flattenManifestFiles as _flattenManifestFiles, latestGateResult as _latestGateResult, resolveArticleType as _resolveArticleType, resolveRunId as _resolveRunId, } from './manifest/index.js';
18
- /** Id of the generated reader guide section. */
19
- export const READER_GUIDE_SECTION_ID = 'reader-intelligence-guide';
20
- /** Display title of the generated reader guide section. */
21
- export const READER_GUIDE_SECTION_TITLE = 'Reader Intelligence Guide';
19
+ import { READER_GUIDE_SECTION_ID, READER_GUIDE_SECTION_IDS, READER_GUIDE_SECTION_TITLE, } from './reader-guide-constants.js';
20
+ export { READER_GUIDE_SECTION_ID, READER_GUIDE_SECTION_IDS, READER_GUIDE_SECTION_TITLE, } from './reader-guide-constants.js';
22
21
  /**
23
22
  * Normalise `manifest.files` into a flat list of `runRelPath` strings.
24
23
  *
@@ -258,8 +257,13 @@ export function renderAnalysisIndex(included, manifestRelPath) {
258
257
  '',
259
258
  ].join('\n');
260
259
  }
261
- /** Reader-guide copy for high-value intelligence sections. */
262
- const READER_GUIDE_VALUES = {
260
+ /**
261
+ * English-only reader-guide copy for the Markdown guide embedded in the
262
+ * aggregated source document. Section membership is gated by
263
+ * `READER_GUIDE_SECTION_IDS` (imported from `reader-guide-constants.ts`)
264
+ * so both renderers stay in sync automatically.
265
+ */
266
+ const READER_GUIDE_EN = {
263
267
  'section-executive-brief': {
264
268
  need: 'BLUF and editorial decisions',
265
269
  value: 'fast answer to what happened, why it matters, who is accountable, and the next dated trigger',
@@ -298,6 +302,10 @@ const READER_GUIDE_VALUES = {
298
302
  * artifact sections. It gives readers a Riksdagsmonitor-style navigation layer
299
303
  * without requiring agents to hand-author another artifact.
300
304
  *
305
+ * Section membership is checked against `READER_GUIDE_SECTION_IDS` (the
306
+ * canonical list shared with the HTML renderer in `reader-intelligence-guide.ts`)
307
+ * to prevent drift between the two renderers.
308
+ *
301
309
  * @param sections - Emitted section TOC entries, in document order
302
310
  * @param included - Included artifacts, used to name each section's source
303
311
  * @returns Markdown block containing the guide table
@@ -305,7 +313,10 @@ const READER_GUIDE_VALUES = {
305
313
  export function renderReaderIntelligenceGuide(sections, included) {
306
314
  const rows = sections
307
315
  .map((section) => {
308
- const copy = Object.getOwnPropertyDescriptor(READER_GUIDE_VALUES, section.id)?.value;
316
+ // Guard: only include sections whose IDs are in the canonical list
317
+ if (!READER_GUIDE_SECTION_IDS.includes(section.id))
318
+ return '';
319
+ const copy = Object.getOwnPropertyDescriptor(READER_GUIDE_EN, section.id)?.value;
309
320
  if (!copy)
310
321
  return '';
311
322
  const source = included.find((artifact) => artifact.sectionId === section.id)?.runRelPath;
@@ -538,16 +549,29 @@ export function aggregateAnalysisRun(options) {
538
549
  const tradecraft = renderTradecraftAppendix(tradecraftFiles);
539
550
  const analysisIndex = renderAnalysisIndex(includedArtifacts, manifestRelPath);
540
551
  const readerGuide = renderReaderIntelligenceGuide(emittedSections, includedArtifacts);
552
+ // Deterministic 3–7 bullet "Key takeaways" block, harvested from the
553
+ // synthesis-summary / intelligence-assessment artifacts. Placed
554
+ // immediately after the Executive Brief so the reader gets the BLUF
555
+ // followed by a digest of the strongest findings before being handed
556
+ // off to the Reader Intelligence Guide and the deeper sections.
557
+ const keyTakeaways = buildKeyTakeaways({ runDir });
541
558
  // TOC ordering reflects the rendered document:
542
559
  // Executive Brief (already first in emittedSections via appendSection) →
543
- // Reader Intelligence Guide (inserted at position 1, after Exec Brief) →
544
- // remaining sections → audit appendices.
560
+ // Key Takeaways (inserted right after the brief when present) →
561
+ // Reader Intelligence Guide → remaining sections → audit appendices.
562
+ let postBriefIdx = emittedSections.length > 0 &&
563
+ emittedSections[0]?.id === namespacedSectionId(execBriefSection?.id ?? '')
564
+ ? 1
565
+ : 0;
566
+ if (keyTakeaways) {
567
+ emittedSections.splice(postBriefIdx, 0, {
568
+ id: KEY_TAKEAWAYS_SECTION_ID,
569
+ title: KEY_TAKEAWAYS_SECTION_TITLE,
570
+ });
571
+ postBriefIdx += 1;
572
+ }
545
573
  if (readerGuide) {
546
- const insertIdx = emittedSections.length > 0 &&
547
- emittedSections[0]?.id === namespacedSectionId(execBriefSection?.id ?? '')
548
- ? 1
549
- : 0;
550
- emittedSections.splice(insertIdx, 0, {
574
+ emittedSections.splice(postBriefIdx, 0, {
551
575
  id: READER_GUIDE_SECTION_ID,
552
576
  title: READER_GUIDE_SECTION_TITLE,
553
577
  });
@@ -559,6 +583,7 @@ export function aggregateAnalysisRun(options) {
559
583
  '',
560
584
  ...execBriefMarkdown,
561
585
  '',
586
+ ...(keyTakeaways ? [keyTakeaways, ''] : []),
562
587
  readerGuide,
563
588
  '',
564
589
  ...sectionMarkdown,
@@ -45,6 +45,12 @@ export interface GenerateResult {
45
45
  * the artifacts that produced it (riksdagsmonitor pattern).
46
46
  */
47
47
  readonly runArticleMdRelPath: string;
48
+ /**
49
+ * Repo-relative path of the `article-meta.json` sidecar written next to
50
+ * `article.md` — structured data consumed by HTML SEO, news indexes,
51
+ * and RSS rendering. Always emitted, deterministic.
52
+ */
53
+ readonly runArticleMetaRelPath: string;
48
54
  /** Filenames written under `outDir`, relative to `outDir`. */
49
55
  readonly writtenFiles: readonly string[];
50
56
  /** Metadata from {@link aggregateAnalysisRun}. */
@@ -20,9 +20,12 @@ import fs from 'fs';
20
20
  import path from 'path';
21
21
  import { pathToFileURL } from 'url';
22
22
  import { aggregateAnalysisRun, resolveArticleTypeFromManifest, } from './analysis-aggregator.js';
23
+ import { resolveRunId as _resolveRunId } from './manifest/index.js';
23
24
  import { resolveArticleMetadata, extractStrongProseLine, } from './article-metadata.js';
25
+ import { buildArticleMeta, serializeArticleMeta } from './article-meta.js';
24
26
  import { renderMarkdown } from './markdown-renderer.js';
25
27
  import { wrapArticleHtml, getArticleFilename } from './article-html.js';
28
+ import { buildReaderIntelligenceGuideHtml, stripInlineReaderGuide, } from './reader-intelligence-guide.js';
26
29
  import { ALL_LANGUAGES } from '../constants/language-core.js';
27
30
  import { blobUrl } from './infra/github-urls.js';
28
31
  import { buildArticleSlug as _buildArticleSlug, sanitizeRunSuffix as _sanitizeRunSuffix, } from './slug/index.js';
@@ -335,6 +338,22 @@ function writeLanguageVariant(lang, slug, aggregated, englishHtml, chromeOptions
335
338
  metaSource = fs.readFileSync(langMdAbs, 'utf8');
336
339
  bodyHtml = renderMarkdown(metaSource).html;
337
340
  }
341
+ // Strip any AI-authored inline Reader Intelligence Guide and inject the
342
+ // renderer-owned, language-aware version so exactly one guide appears.
343
+ bodyHtml = stripInlineReaderGuide(bodyHtml);
344
+ // The article chrome (wrapArticleHtml) renders its own <h1> in the hero
345
+ // header. Strip the in-body <h1> emitted from the Markdown `# Title` to
346
+ // avoid a duplicate H1 and broken heading hierarchy (H2 preceding H1).
347
+ bodyHtml = bodyHtml.replace(/<h1[^>]*>[\s\S]*?<\/h1>\s*/, '');
348
+ const guideHtml = buildReaderIntelligenceGuideHtml(lang, aggregated.sectionToc, aggregated.includedArtifacts);
349
+ if (guideHtml) {
350
+ // Prepend the guide to the body so it always appears at the top of
351
+ // the rendered content, immediately after the chrome header. The
352
+ // article chrome in wrapArticleHtml wraps the body in an <article>
353
+ // with its own <header>/<h1>, so prepending here is deterministic
354
+ // and avoids fragile in-body heading searches.
355
+ bodyHtml = guideHtml + '\n' + bodyHtml;
356
+ }
338
357
  // When a per-language translated source exists, prefer a summary derived
339
358
  // from it so the `<meta description>` matches the visible prose. The
340
359
  // editorial title still comes from the English resolver (per-language
@@ -455,6 +474,25 @@ export function generateArticle(opts, runSuffix, articleCountOverride) {
455
474
  .relative(opts.repoRoot, runArticleMdAbs)
456
475
  .split(path.sep)
457
476
  .join('/');
477
+ // Emit `article-meta.json` next to `article.md` — a deterministic
478
+ // structured-data sidecar consumed by HTML SEO, news indexes, and RSS.
479
+ // Same artifact bytes in → same JSON bytes out (asserted by the
480
+ // determinism test).
481
+ const runArticleMetaAbs = path.join(opts.runDir, 'article-meta.json');
482
+ const articleMeta = buildArticleMeta({
483
+ runDir: opts.runDir,
484
+ repoRoot: opts.repoRoot,
485
+ date: aggregated.date,
486
+ articleType: aggregated.articleType,
487
+ runId: readManifestRunId(opts.runDir, path.basename(opts.runDir)),
488
+ gateResult: aggregated.gateResult,
489
+ slug,
490
+ });
491
+ fs.writeFileSync(runArticleMetaAbs, serializeArticleMeta(articleMeta), 'utf8');
492
+ const runArticleMetaRelPath = path
493
+ .relative(opts.repoRoot, runArticleMetaAbs)
494
+ .split(path.sep)
495
+ .join('/');
458
496
  // Also write source Markdown under <outDir>/<slug>.en.md for search
459
497
  // indexing and backwards compatibility with existing news-index scripts.
460
498
  ensureDir(opts.outDir);
@@ -479,6 +517,7 @@ export function generateArticle(opts, runSuffix, articleCountOverride) {
479
517
  return {
480
518
  sourceMarkdownRelPath: runArticleMdRelPath,
481
519
  runArticleMdRelPath,
520
+ runArticleMetaRelPath,
482
521
  writtenFiles: written,
483
522
  aggregated,
484
523
  };
@@ -536,6 +575,29 @@ export function generateAllArticles(opts) {
536
575
  }
537
576
  return results;
538
577
  }
578
+ /**
579
+ * Read the run identifier from `manifest.json`, falling back to the
580
+ * directory basename when the manifest is missing or unparsable. Wraps
581
+ * the canonical resolver from `aggregator/manifest/index.ts` so callers
582
+ * outside the aggregator core (here, the article-meta sidecar emitter)
583
+ * stay decoupled from the internal manifest schema.
584
+ *
585
+ * @param runDir - Absolute run directory path
586
+ * @param defaultRunId - Fall-back run id (typically the directory basename)
587
+ * @returns Resolved run id, never empty
588
+ */
589
+ function readManifestRunId(runDir, defaultRunId) {
590
+ const manifestPath = path.join(runDir, 'manifest.json');
591
+ if (!fs.existsSync(manifestPath))
592
+ return defaultRunId;
593
+ try {
594
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
595
+ return _resolveRunId(parsed, defaultRunId);
596
+ }
597
+ catch {
598
+ return defaultRunId;
599
+ }
600
+ }
539
601
  /**
540
602
  * Read the raw manifest.json from a run directory and return the subset
541
603
  * of fields consumed by {@link resolveArticleMetadata}. Returns an empty
@@ -23,6 +23,12 @@ import { buildHeadFreshnessTags } from '../constants/build-info-meta.js';
23
23
  import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, SKIP_LINK_TEXTS, TOC_ARIA_LABELS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, getLocalizedString, getTextDirection, } from '../constants/languages.js';
24
24
  import { escapeHTML } from '../utils/file-utils.js';
25
25
  import { buildSiteFooter, buildSiteHeader, buildPageBanner, } from '../templates/section-builders.js';
26
+ import { READER_GUIDE_SECTION_ID } from './reader-guide-constants.js';
27
+ import { READER_GUIDE_TITLE_LABELS } from './reader-intelligence-guide.js';
28
+ /** Publisher organization name used in JSON-LD, meta tags. */
29
+ const PUBLISHER_NAME = 'Hack23 AB';
30
+ /** Site name used across meta tags and structured data. */
31
+ const SITE_NAME = 'EU Parliament Monitor';
26
32
  /**
27
33
  * Build the canonical filename for an article in a given language. English
28
34
  * uses the bare stem (`2026-01-15-breaking-en.html`); other languages share
@@ -85,7 +91,13 @@ export function buildArticleToc(entries, lang) {
85
91
  return '';
86
92
  const label = escapeHTML(getLocalizedString(TOC_ARIA_LABELS, lang));
87
93
  const items = entries
88
- .map((e) => ` <li><a href="#${escapeHTML(e.id)}">${escapeHTML(e.title)}</a></li>`)
94
+ .map((e) => {
95
+ // Translate the Reader Intelligence Guide title into the target language
96
+ const displayTitle = e.id === READER_GUIDE_SECTION_ID
97
+ ? getLocalizedString(READER_GUIDE_TITLE_LABELS, lang)
98
+ : e.title;
99
+ return ` <li><a href="#${escapeHTML(e.id)}">${escapeHTML(displayTitle)}</a></li>`;
100
+ })
89
101
  .join('\n');
90
102
  return [
91
103
  ` <aside class="article-toc-container" aria-label="${label}">`,
@@ -111,7 +123,7 @@ export function buildArticleToc(entries, lang) {
111
123
  export function wrapArticleHtml(options) {
112
124
  const safeLang = ALL_LANGUAGES.includes(options.lang) ? options.lang : 'en';
113
125
  const dir = getTextDirection(safeLang);
114
- const siteTitle = getLocalizedString(PAGE_TITLES, safeLang).split(' - ')[0] ?? 'EU Parliament Monitor';
126
+ const siteTitle = getLocalizedString(PAGE_TITLES, safeLang).split(' - ')[0] ?? SITE_NAME;
115
127
  const skipLinkText = getLocalizedString(SKIP_LINK_TEXTS, safeLang);
116
128
  const canonicalUrl = `${BASE_URL}/news/${getArticleFilename(options.articleSlug, safeLang)}`;
117
129
  const indexHref = safeLang === 'en' ? '../index.html' : `../index-${safeLang}.html`;
@@ -127,14 +139,21 @@ export function wrapArticleHtml(options) {
127
139
  headline: options.title,
128
140
  description: options.description,
129
141
  datePublished: options.date,
142
+ dateModified: options.date,
130
143
  inLanguage: safeLang,
131
144
  url: canonicalUrl,
132
- author: { '@type': 'Organization', name: 'Hack23 AB', url: 'https://hack23.com' },
133
- publisher: { '@type': 'Organization', name: 'Hack23 AB', url: 'https://hack23.com' },
145
+ image: `${BASE_URL}/images/og-image.jpg`,
146
+ author: { '@type': 'Organization', name: PUBLISHER_NAME, url: 'https://hack23.com' },
147
+ publisher: {
148
+ '@type': 'Organization',
149
+ name: PUBLISHER_NAME,
150
+ url: 'https://hack23.com',
151
+ logo: { '@type': 'ImageObject', url: `${BASE_URL}/images/apple-touch-icon.png` },
152
+ },
134
153
  articleSection: options.articleType,
135
154
  isPartOf: {
136
155
  '@type': 'WebSite',
137
- name: 'EU Parliament Monitor',
156
+ name: SITE_NAME,
138
157
  url: BASE_URL,
139
158
  },
140
159
  ...(options.isBasedOn && options.isBasedOn.length > 0
@@ -143,7 +162,32 @@ export function wrapArticleHtml(options) {
143
162
  }
144
163
  : {}),
145
164
  };
146
- const jsonLdString = JSON.stringify(jsonLd).replace(/</g, '\\u003c');
165
+ const breadcrumbLd = {
166
+ '@context': 'https://schema.org',
167
+ '@type': 'BreadcrumbList',
168
+ itemListElement: [
169
+ {
170
+ '@type': 'ListItem',
171
+ position: 1,
172
+ name: SITE_NAME,
173
+ item: BASE_URL,
174
+ },
175
+ {
176
+ '@type': 'ListItem',
177
+ position: 2,
178
+ name: options.articleType.replace(/-/g, ' '),
179
+ item: `${BASE_URL}/news/`,
180
+ },
181
+ {
182
+ '@type': 'ListItem',
183
+ position: 3,
184
+ name: options.title,
185
+ item: canonicalUrl,
186
+ },
187
+ ],
188
+ };
189
+ const structuredData = [jsonLd, breadcrumbLd];
190
+ const jsonLdString = JSON.stringify(structuredData).replace(/</g, '\\u003c');
147
191
  const pageTitle = `${options.title} — ${siteTitle}`;
148
192
  const header = buildSiteHeader({
149
193
  lang: safeLang,
@@ -163,8 +207,8 @@ export function wrapArticleHtml(options) {
163
207
  <title>${escapeHTML(pageTitle)}</title>
164
208
  <meta name="description" content="${escapeHTML(options.description)}">
165
209
  <meta name="robots" content="index, follow, max-image-preview:large">
166
- <meta name="author" content="Hack23 AB">
167
- <meta name="publisher" content="Hack23 AB">
210
+ <meta name="author" content="${PUBLISHER_NAME}">
211
+ <meta name="publisher" content="${PUBLISHER_NAME}">
168
212
  <meta name="date" content="${options.date}">
169
213
  <meta name="article:published_time" content="${options.date}">
170
214
  <link rel="canonical" href="${canonicalUrl}">
@@ -0,0 +1,121 @@
1
+ import { buildKeyTakeaways as _buildKeyTakeaways } from './key-takeaways.js';
2
+ /** Shape of `article-meta.json`. */
3
+ export interface ArticleMeta {
4
+ /** ISO date of the run (`YYYY-MM-DD`). */
5
+ readonly date: string;
6
+ /** Article type slug (e.g. `breaking`). */
7
+ readonly articleType: string;
8
+ /** Stable run identifier from the manifest. */
9
+ readonly runId: string;
10
+ /** Latest non-PENDING gate result. */
11
+ readonly gateResult: string;
12
+ /** Article slug used by the news pages (`<date>-<type>[-<suffix>]`). */
13
+ readonly slug: string;
14
+ /** Run-relative path of the canonical `article.md`. */
15
+ readonly articlePath: string;
16
+ /** One-sentence executive lead — the strongest finding, distilled. */
17
+ readonly topFinding: string;
18
+ /** 3–7 deterministic key takeaways harvested from synthesis-summary. */
19
+ readonly keyTakeaways: readonly string[];
20
+ /** Top political risks (artifact-driven, may be empty). */
21
+ readonly topRisks: readonly string[];
22
+ /** Key dated triggers / "what to watch" items. */
23
+ readonly keyDates: readonly string[];
24
+ /** Key actors / political groups identified by the artifacts. */
25
+ readonly keyActors: readonly string[];
26
+ /** Optional IMF / WorldBank macro hook surfaced as a sidebar callout. */
27
+ readonly macroContext: string;
28
+ /**
29
+ * Run-relative paths of every artifact whose content fed into this meta
30
+ * record — emitted so the HTML SEO layer can build `isBasedOn` arrays
31
+ * without re-walking the run directory.
32
+ */
33
+ readonly sources: readonly string[];
34
+ }
35
+ /** Options for {@link buildArticleMeta}. */
36
+ export interface BuildArticleMetaOptions {
37
+ /** Absolute path to the analysis run directory. */
38
+ readonly runDir: string;
39
+ /** Absolute path to the repository root (used for repo-relative paths). */
40
+ readonly repoRoot: string;
41
+ /** ISO date of the run (`YYYY-MM-DD`). */
42
+ readonly date: string;
43
+ /** Article type slug. */
44
+ readonly articleType: string;
45
+ /** Stable run identifier from the manifest. */
46
+ readonly runId: string;
47
+ /** Latest non-PENDING gate result. */
48
+ readonly gateResult: string;
49
+ /** Article slug used by the news pages. */
50
+ readonly slug: string;
51
+ }
52
+ /**
53
+ * Mine top political risks from `risk-scoring/risk-matrix.md` (or its
54
+ * historic variants under the same directory). Falls back to the first
55
+ * bullets in `risk-scoring/quantitative-swot.md` when the matrix is
56
+ * absent. Returns at most {@link MAX_LIST_ENTRIES} bullets.
57
+ *
58
+ * @param runDir - Absolute path to the analysis run directory
59
+ * @returns Ordered list of risk bullet bodies
60
+ */
61
+ export declare function extractTopRisks(runDir: string): string[];
62
+ /**
63
+ * Mine forward-looking dated items from
64
+ * `intelligence/parliamentary-calendar-projection.md` and
65
+ * `extended/forward-indicators.md`. Returns at most
66
+ * {@link MAX_LIST_ENTRIES} bullets, de-duplicated across the two sources.
67
+ *
68
+ * @param runDir - Absolute path to the analysis run directory
69
+ * @returns Ordered list of dated trigger bullet bodies
70
+ */
71
+ export declare function extractKeyDates(runDir: string): string[];
72
+ /**
73
+ * Mine key actors / political groups from
74
+ * `classification/actor-mapping.md` and `intelligence/stakeholder-map.md`.
75
+ * Falls through to coalition-dynamics when the canonical actor map is
76
+ * missing. Returns at most {@link MAX_LIST_ENTRIES} bullets.
77
+ *
78
+ * @param runDir - Absolute path to the analysis run directory
79
+ * @returns Ordered list of actor bullet bodies
80
+ */
81
+ export declare function extractKeyActors(runDir: string): string[];
82
+ /**
83
+ * Resolve a one-line IMF / WorldBank macro context callout from
84
+ * `intelligence/economic-context.md`. Returns the trimmed lead sentence
85
+ * of the artifact, or `''` when the artifact is missing.
86
+ *
87
+ * @param runDir - Absolute path to the analysis run directory
88
+ * @returns IMF-backed macro hook, or `''`
89
+ */
90
+ export declare function extractMacroContext(runDir: string): string;
91
+ /**
92
+ * Resolve the deterministic 3–7 key-takeaway bullets used in both the
93
+ * Markdown article body and `article-meta.json`.
94
+ *
95
+ * @param runDir - Absolute path to the analysis run directory
96
+ * @returns Ordered list of takeaway bodies (3–7 entries)
97
+ */
98
+ export declare function extractKeyTakeaways(runDir: string): string[];
99
+ /**
100
+ * Build the deterministic `ArticleMeta` record for one run. Pure function
101
+ * of the on-disk artifacts plus the resolved manifest fields.
102
+ *
103
+ * @param options - Run-level metadata + absolute run directory
104
+ * @returns Frozen, JSON-serialisable {@link ArticleMeta}
105
+ */
106
+ export declare function buildArticleMeta(options: BuildArticleMetaOptions): ArticleMeta;
107
+ /**
108
+ * Serialise an {@link ArticleMeta} as a stable JSON string with a trailing
109
+ * newline. Keys are emitted in declaration order (insertion-order, matching
110
+ * the interface layout). Determinism guarantee: same input → same bytes.
111
+ *
112
+ * @param meta - Article meta record
113
+ * @returns JSON string ready to be written next to `article.md`
114
+ */
115
+ export declare function serializeArticleMeta(meta: ArticleMeta): string;
116
+ /**
117
+ * Convenience wrapper that re-exports {@link _buildKeyTakeaways} so the
118
+ * aggregator can import the rendered Markdown block from a single module.
119
+ */
120
+ export { _buildKeyTakeaways as buildKeyTakeawaysMarkdown };
121
+ //# sourceMappingURL=article-meta.d.ts.map