# 06 — Article Generation (aggregate + render)

Articles are 100% rendered from the analysis artifacts produced in [`04-analysis-pipeline.md`](04-analysis-pipeline.md). No AI-written HTML, no scaffold, no `AI_MUST_REPLACE` markers — the analysis folder is the article, the renderer wraps it in chrome. End-to-end architecture and UI/UX export map: [`Article-Generation.md`](../../Article-Generation.md).

## Preconditions

- [`05-analysis-gate.md`](05-analysis-gate.md) has passed — every required artifact exists, every `dok_id` in the manifest has a `documents/{dok_id}-analysis.md`, Mermaid/colour/ICD 203 audits pass.
- `$ARTICLE_DATE` (YYYY-MM-DD) and `$SUBFOLDER` (`propositions`, `motions`, `committee-reports`, `evening-analysis`, …) are exported.

## Generation steps (exactly three)

> ⏱  **Time-budget telemetry (mandatory).** Print `agent_minute` before aggregation. Target `agent_minute ≤ 38` here (see [`00-base-contract.md §Session timing → Phase budget`](00-base-contract.md)). At `agent_minute ≥ 40`, the renderer is fast but the safe-outputs runner still needs ~3 min — commit whatever exists and proceed; do not loop on render fixes past minute 41.
>
> ```bash
> AGENT_START_EPOCH="$(cat /tmp/gh-aw/agent-start.epoch 2>/dev/null || date -u +%s)"
> ELAPSED_MIN=$(( ($(date -u +%s) - AGENT_START_EPOCH) / 60 ))
> echo "⏱  render-entry agent_minute=$ELAPSED_MIN  remaining_to_pr_deadline=$(( 45 - ELAPSED_MIN )) min"
> ```

### Step 1 — Headline QA

Run the content-side headline validator against the current subfolder before aggregation so boilerplate/date/scaffolding never reaches `article.md` or the rendered HTML:

```bash
npx tsx scripts/check-headline-quality.ts \
  "analysis/daily/$ARTICLE_DATE/$SUBFOLDER"
```

Non-zero exit → fix `analysis/daily/$ARTICLE_DATE/$SUBFOLDER/executive-brief.md` (or the English source brief inside the scoped subtree), then re-run the validator. The path is intentionally scoped to the current article subtree, so the workflow blocks on newly generated content without scanning the full historical archive.

### Step 2 — Aggregate

```bash
npx tsx scripts/aggregate-analysis.ts \
  --date     "$ARTICLE_DATE" \
  --subfolder "$SUBFOLDER"
```

What the aggregator does:

1. Reads every committed `.md` under `analysis/daily/$ARTICLE_DATE/$SUBFOLDER/` (including `documents/*.md` and any `ext/*.md`).
2. Strips each file's YAML front-matter, duplicate H1, and admin/footer boilerplate (`Document control`, `Audit trail`, `Generated by …`, `Run ID …`, `— End of template —`).
3. Rewrites relative `[label](path.md)` links to absolute GitHub blob URLs so every claim stays auditable.
4. Emits the **Executive Brief first** (journalistic lede / so-what up front), then injects a deterministic `## Reader Intelligence Guide` immediately after. Article types that produce a subset of artifacts (realtime, week-ahead, monthly-review) are fully supported: missing artifacts are skipped silently and the guide lists only the lenses generated.
5. Concatenates files in the deterministic political-intelligence order below, injecting one `## <Section title>` heading and a `_Source: [\`file.md\`](GitHub URL)_` attribution above each block. Mermaid code fences pass through untouched.
6. Emits one canonical markdown file with YAML front-matter:

   `analysis/daily/$ARTICLE_DATE/$SUBFOLDER/article.md`

**Canonical order** (see `scripts/render-lib/index.ts:AGGREGATION_ORDER`):

1. `executive-brief.md` ← journalistic lede (title + meta description come from its H1 + first real paragraph; admin bylines like `**Author**`/`**Run ID**`/`**Classification**`/`**Confidence**` are stripped before description selection)
2. `synthesis-summary.md`
3. `intelligence-assessment.md` ← Key Judgments + PIRs (ICD-203 — placed immediately after the synthesis so readers meet the thesis before the evidence stack)
4. `significance-scoring.md`
5. `media-framing-analysis.md` ← reader-facing narrative contestation, manipulation surface (DISARM TTPs · CIB ABCDE · narrative-laundering chain), Outlet Bias Audit (no outlet is neutral — ownership / funding / board / lean / Reuters Institute Trust / PO-PON complaints / foreign-actor link), comparative-international frame lineage, frame lifecycle / longevity, RRPA impact, L1–L5 counter-resilience ladder
6. `stakeholder-perspectives.md`
7. `forward-indicators.md`
8. `scenario-analysis.md`
9. `risk-assessment.md`
10. `swot-analysis.md`
11. `threat-analysis.md`
12. **Per-document intelligence** — every `documents/*-analysis.md`, one `### {dok_id}` subsection each
13. `election-2026-analysis.md`
14. `coalition-mathematics.md`
15. `voter-segmentation.md`
16. `comparative-international.md`
17. `historical-parallels.md`
18. `implementation-feasibility.md`
19. `devils-advocate.md`
20. `classification-results.md`
21. `cross-reference-map.md`
22. `methodology-reflection.md`
23. `data-download-manifest.md` (appendix)
24. Any supplementary `*.md` not listed above (alphabetical)

### AI self-audit "Pass 2" sections are not published

`cleanArtifactBody()` strips any heading like `## Pass 2 …` / `### Pass 2 refinements` / `## 🔁 Pass 2 addendum` (and everything after it in the same artifact) — the aggregator treats it as process metadata. Keep Pass-2 notes in the artifact for the next iteration, but land real Pass-2 improvements *inside* the original section, not in a trailing appendix.

When a required artifact is missing, the aggregator aborts with a non-zero exit code — return to [`04-analysis-pipeline.md`](04-analysis-pipeline.md) and produce the missing file. `article.md` is never hand-edited.

#### Step 2b — Editorial QA re-check (post-aggregation)

The inline analysis gate ([`05-analysis-gate.md`](05-analysis-gate.md) Check 12) runs **before** aggregation, so the editorial validator is informational on the first pass. Immediately after `aggregate-analysis.ts` writes `article.md`, re-invoke the article validator to make editorial checks (banned phrases, citation density per `reference-quality-thresholds.json → aiFirst.citationDensity.perArticle`, `economicProvenance` ≤ 6-month vintage) blocking before staging:

```bash
npx tsx scripts/validate-article.ts \
  "analysis/daily/$ARTICLE_DATE/$SUBFOLDER/article.md"
```

Non-zero exit → fix the offending claims in the upstream analysis artifacts, re-run `aggregate-analysis.ts`, re-validate. `article.md` is never hand-edited.

### Step 3 — (No-op) Per-language Markdown translation is not performed

Per-type workflows do not produce `article.<lang>.md` for any non-English language. The agent stops after writing the canonical English `article.md` from Step 1. Non-English HTML pages are produced by `scripts/render-articles.ts` via the localized executive-brief cascade — the renderer keeps the detailed **article body in English** and only swaps in the localized hero/SEO overlay from `executive-brief_<lang>.md` (H1, dek, BLUF, JSON-LD `headline`/`description`, `<title>`, `<meta name="description">`, `og:title`/`og:description`). `<html lang>` / JSON-LD `inLanguage` are forced to the target language even though body prose stays English. Contract: `scripts/render-lib/article-merge.ts` (`mergeLocalizedWithEnglish`).

> ⚠️ **Workflow ordering**: per-type workflows render HTML in the same run that produces the English `executive-brief.md`. The dedicated `news-translate` workflow runs on a separate schedule and back-fills `executive-brief_<lang>.md` *after the fact*. On the first HTML render the cascade falls through to the English brief title/description for every non-EN language (`language: <lang>` is still forced so `<html lang>` / JSON-LD `inLanguage` are correct). Newly translated briefs appear in the localized HTML only on the **next** per-type re-render of the same subfolder (next scheduled run, `force_generation=true`, or explicit `npm run render-articles`). `news-translate` is forbidden from touching `news/*.html` (see `validate-file-ownership.ts`) to keep the file-ownership contract conflict-free.

Historical `article.<lang>.md` files in the repo are forbidden artifacts per `scripts/validate-file-ownership.ts` (category-independent reject) — per-type workflows do not recreate them.

### Step 4 — Render

```bash
npx tsx scripts/render-articles.ts \
  --date     "$ARTICLE_DATE" \
  --subfolder "$SUBFOLDER" \
  --lang     all              # always render every supported language
```

What the renderer does:

1. Reads `analysis/daily/$ARTICLE_DATE/$SUBFOLDER/article.md`. For each non-English language it composes the English body with the localized executive brief (`executive-brief_<lang>.md`) via `mergeLocalizedWithEnglish` when present; historical `article.<lang>.md` files are ignored (forbidden artifact — `validate-file-ownership.ts`).
2. Parses through `unified` → `remark-parse` → `remark-gfm` → `remark-rehype` → `rehype-raw` → `rehype-slug` → `rehype-autolink-headings` → `rehype-sanitize` → `rehype-stringify`. Mermaid ```` ```mermaid ```` fences are preserved as `<pre class="mermaid">` and upgraded to SVG client-side by `js/lib/mermaid-init.mjs`.
3. Wraps the sanitised body in the shared site chrome (`scripts/render-lib/index.ts:buildChrome`): full `<head>` with hreflang × all 14 supported languages, Open Graph / Twitter / JSON-LD `NewsArticle` (with `isBasedOn` citing every source artifact), cyberpunk header with skip-link + language switcher, article dek + provenance badges, footer with "Analysis sources" linking every source `.md` / `.json` back to GitHub. Generated `article.md`, translated `article.<lang>.md`, and `pass1/` snapshots are excluded from the public source list.
4. Writes one HTML file per supported language — **always all 14**:

   `news/$ARTICLE_DATE-$SUBFOLDER-$LANG.html`   (e.g. `news/2026-04-23-propositions-en.html` … `news/2026-04-23-propositions-zh.html`)

Every per-type workflow renders the full 14-language set in one agentic run. The dedicated `news-translate` workflow is re-scoped to produce `executive-brief_<lang>.md` markdown translations of every `analysis/daily/**/executive-brief.md` for the 13 non-English target languages (3 runs/day, default batch 2 sources — lowered from 3 after run [#26008347629](https://github.com/Hack23/riksdagsmonitor/actions/runs/26008347629) hit Timer A). It does not touch `news/*.html` and does not back-fill `article.<lang>.md`. See `.github/workflows/news-translate.md`.

## What the AI does **not** do here

- HTML scaffolding, per-section templates, `AI_MUST_REPLACE` markers — all removed with the old scaffold pipeline.
- Article prose directly in `.html` files — the HTML body is a pure projection of the `.md` analysis.
- Banned-patterns gate (`scripts/check-banned-patterns.ts`), HTML-level Swedish-leakage detector, HTML-level manifest check — replaced by the analysis gate in [`05-analysis-gate.md`](05-analysis-gate.md).

## What the AI does

> 🛠 **File-write contract**: `article.md` is a script-generated projection of upstream `.md` artifacts — fix upstream artifacts (with the `edit` tool) and re-aggregate. The aggregator + renderer (`scripts/aggregate-analysis.ts`, `scripts/render-articles.ts`) are the only writers for `article.md` and `news/*.html`. For any upstream edit between aggregate and render (e.g. last-minute SEO H1/dek tightening in `executive-brief.md`, fixing a stale `dok_id` cross-reference in `cross-reference-map.md`), use the `edit` tool. Banned for agent-generated content writes under `analysis/daily/**`: `python3`, `node -e`, `sed -i`, `echo … > file`, `tee file`, unquoted heredocs (`<<EOF`). **Sole exception**: the env-var-only pre-flight scaffold in [`03-data-download.md`](03-data-download.md) that creates the initial `data-download-manifest.md` marker when missing. See [`01-bash-and-shell-safety.md` §File creation & overwrite strategy](01-bash-and-shell-safety.md).

- Produce every analysis artifact required by [`04-analysis-pipeline.md`](04-analysis-pipeline.md) at publication quality (AI-FIRST minimum 2 passes — [`00-base-contract.md`](00-base-contract.md) §5). Prose quality is set here, not later.
- Verify the aggregator output before commit:

  ```bash
  ART_MD="analysis/daily/$ARTICLE_DATE/$SUBFOLDER/article.md"
  [ -s "$ART_MD" ] || { echo "❌ article.md empty — aggregator did not run"; exit 1; }
  # At least the core analytical sections must be present.
  for h in "## Executive Brief" "## Synthesis Summary" "## Intelligence Assessment" "## Classification Results" "## Methodology Reflection"; do
    grep -qF "$h" "$ART_MD" || { echo "❌ $h missing from $ART_MD"; exit 1; }
  done
  # Every dok_id cited in the article must have a source file.
  CITED=$(grep -hoE 'H[A-Z0-9]{2}[A-Z]{2,}[0-9]+' "$ART_MD" | sort -u)
  for d in $CITED; do
    [ -e "analysis/daily/$ARTICLE_DATE/$SUBFOLDER/documents/${d}-analysis.md" ] \
      || { echo "❌ article.md cites $d but no documents/${d}-analysis.md"; exit 1; }
  done
  ```
- Verify the rendered HTML exists for every supported language (always all 14):

  ```bash
  ALL_LANGS="en sv da no fi de fr es nl ar he ja ko zh"
  for LANG in $ALL_LANGS; do
    OUT="news/${ARTICLE_DATE}-${SUBFOLDER}-${LANG}.html"
    [ -s "$OUT" ] || { echo "❌ $OUT missing"; exit 1; }
    grep -q '<article class="rm-article"' "$OUT" || { echo "❌ $OUT lacks article wrapper"; exit 1; }
    grep -q 'rm-article-sources' "$OUT" || { echo "❌ $OUT lacks analysis-sources footer"; exit 1; }
  done
  ```

## Chrome + SEO

The renderer embeds:

- `<title>` = aggregated article title (H1 of `executive-brief.md`), rewritten to satisfy [`seo-metadata-contract.md`](seo-metadata-contract.md): 55–70 characters, actor-first, active news verb, no literal date, no boilerplate.
- `<meta name="description">` = first BLUF paragraph of `executive-brief.md`, rewritten to satisfy [`seo-metadata-contract.md`](seo-metadata-contract.md): 140–200 characters, one complete sentence, concrete actor/number/instrument, no admin metadata.
- `<link rel="canonical">` + `<link rel="alternate" hreflang="…">` × all requested languages + `x-default`.
- Open Graph (`og:type=article`), Twitter summary card.
- JSON-LD `NewsArticle` with `isBasedOn` listing every source `.md` / `.json` artifact on GitHub — the article is self-documenting.
- Cyberpunk site header with skip-link, nav (Home, Political Intelligence, Sitemap), language switcher.
- Footer: brand, navigation, direct link to `analysis/daily/` and the repo root, Apache-2.0 + GDPR Art 9(2)(e,g) notice, client-side Mermaid loader (`js/lib/mermaid-init.mjs`).

Before staging, read the generated `article.md` once and verify it reads as a coherent political-intelligence article (not an artifact dump): journalistic lede first, Key Judgments early, concrete evidence density, IMF-first economic provenance where applicable, Statskontoret agency-capacity evidence where applicable, source links in every high-impact claim.

## Translations

Every per-type workflow renders the full **14-language set** (English + Swedish + 12 additional) in one agentic run. `news-translate` is re-scoped to the **executive-brief markdown pipeline**: each of three daily runs (09 / 14 / 19 UTC) picks up untranslated `analysis/daily/**/executive-brief.md` files and produces `executive-brief_<lang>.md` siblings for the 13 non-English target languages. Per-type workflows own the primary HTML translation hand-off (canonical 14-language render inside the per-type run). `executive-brief_<lang>.md` is owned exclusively by `news-translate` — the file-ownership validator enforces this boundary.

## Quality floor

The aggregator refuses to run if `executive-brief.md` is missing or empty. Beyond that, quality is inherited from the analysis artifacts — if [`05-analysis-gate.md`](05-analysis-gate.md) passed, the article is publishable.

## Next step

Stage the aggregated `analysis/daily/$ARTICLE_DATE/$SUBFOLDER/article.md` alongside the HTML outputs under `news/`, then proceed to [`07-commit-and-pr.md`](07-commit-and-pr.md).
