zam-core 0.6.1 → 0.6.3

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.
package/dist/cli/index.js CHANGED
@@ -552,7 +552,9 @@ CREATE TABLE IF NOT EXISTS tokens (
552
552
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
553
553
  updated_at TEXT NOT NULL DEFAULT (datetime('now')),
554
554
  deprecated_at TEXT,
555
- question TEXT
555
+ question TEXT,
556
+ provider TEXT,
557
+ topic_id TEXT
556
558
  );
557
559
 
558
560
  -- Prerequisite dependency graph: "to learn A, first know B"
@@ -992,6 +994,14 @@ async function runMigrations(db) {
992
994
  PRIMARY KEY (token_id, source_id)
993
995
  )
994
996
  `);
997
+ if (tokenCols.length > 0) {
998
+ if (!tokenCols.some((c) => c.name === "provider")) {
999
+ await db.exec(`ALTER TABLE tokens ADD COLUMN provider TEXT`);
1000
+ }
1001
+ if (!tokenCols.some((c) => c.name === "topic_id")) {
1002
+ await db.exec(`ALTER TABLE tokens ADD COLUMN topic_id TEXT`);
1003
+ }
1004
+ }
995
1005
  }
996
1006
 
997
1007
  // src/kernel/db/snapshot.ts
@@ -1485,8 +1495,8 @@ async function createToken(db, input8) {
1485
1495
  throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
1486
1496
  }
1487
1497
  await db.prepare(`
1488
- INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, created_at, updated_at)
1489
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1498
+ INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, provider, topic_id, created_at, updated_at)
1499
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1490
1500
  `).run(
1491
1501
  id,
1492
1502
  input8.slug,
@@ -1497,16 +1507,33 @@ async function createToken(db, input8) {
1497
1507
  input8.symbiosis_mode ?? null,
1498
1508
  input8.source_link ?? null,
1499
1509
  input8.question ?? null,
1510
+ input8.provider ?? null,
1511
+ input8.topic_id ?? null,
1500
1512
  now,
1501
1513
  now
1502
1514
  );
1503
1515
  return await getTokenById(db, id);
1504
1516
  }
1517
+ function parseTokenFallback(token) {
1518
+ if (token && !token.provider && token.source_link) {
1519
+ if (token.source_link.includes("lehrplanplus.bayern.de")) {
1520
+ token.provider = "lehrplanplus-bayern";
1521
+ const match = token.source_link.match(/#(.*)$/);
1522
+ if (match) {
1523
+ token.topic_id = match[1];
1524
+ }
1525
+ }
1526
+ }
1527
+ }
1505
1528
  async function getTokenBySlug(db, slug) {
1506
- return await db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
1529
+ const token = await db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
1530
+ parseTokenFallback(token);
1531
+ return token;
1507
1532
  }
1508
1533
  async function getTokenById(db, id) {
1509
- return await db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
1534
+ const token = await db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
1535
+ parseTokenFallback(token);
1536
+ return token;
1510
1537
  }
1511
1538
  async function updateToken(db, slug, updates) {
1512
1539
  const token = await getTokenBySlug(db, slug);
@@ -1552,6 +1579,14 @@ async function updateToken(db, slug, updates) {
1552
1579
  fields.push("question = ?");
1553
1580
  values.push(updates.question);
1554
1581
  }
1582
+ if (updates.provider !== void 0) {
1583
+ fields.push("provider = ?");
1584
+ values.push(updates.provider);
1585
+ }
1586
+ if (updates.topic_id !== void 0) {
1587
+ fields.push("topic_id = ?");
1588
+ values.push(updates.topic_id);
1589
+ }
1555
1590
  if (fields.length === 0) {
1556
1591
  throw new Error("updateToken called with no fields to update");
1557
1592
  }
@@ -1677,14 +1712,20 @@ async function findTokens(db, query) {
1677
1712
  return scored;
1678
1713
  }
1679
1714
  async function listTokens(db, options) {
1715
+ let tokens;
1680
1716
  if (options?.domain) {
1681
- return await db.prepare(
1717
+ tokens = await db.prepare(
1682
1718
  "SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
1683
1719
  ).all(options.domain);
1720
+ } else {
1721
+ tokens = await db.prepare(
1722
+ "SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
1723
+ ).all();
1684
1724
  }
1685
- return await db.prepare(
1686
- "SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
1687
- ).all();
1725
+ for (const token of tokens) {
1726
+ parseTokenFallback(token);
1727
+ }
1728
+ return tokens;
1688
1729
  }
1689
1730
  function slugify(text) {
1690
1731
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
@@ -1736,6 +1777,8 @@ async function listPersonalCards(db, userId, options) {
1736
1777
  t.question,
1737
1778
  t.created_at AS createdAt,
1738
1779
  t.updated_at AS updatedAt,
1780
+ t.provider,
1781
+ t.topic_id AS topicId,
1739
1782
  c.id AS cardId,
1740
1783
  c.state,
1741
1784
  c.due_at AS dueAt,
@@ -1765,6 +1808,17 @@ async function listPersonalCards(db, userId, options) {
1765
1808
  }
1766
1809
  sql += " ORDER BY t.created_at DESC";
1767
1810
  const rows = await db.prepare(sql).all(...values);
1811
+ for (const row of rows) {
1812
+ if (!row.provider && row.sourceLink) {
1813
+ if (row.sourceLink.includes("lehrplanplus.bayern.de")) {
1814
+ row.provider = "lehrplanplus-bayern";
1815
+ const match = row.sourceLink.match(/#(.*)$/);
1816
+ if (match) {
1817
+ row.topicId = match[1];
1818
+ }
1819
+ }
1820
+ }
1821
+ }
1768
1822
  return rows;
1769
1823
  }
1770
1824
  async function importCurriculumCards(db, userId, cards) {
@@ -1811,9 +1865,18 @@ async function importCurriculumCards(db, userId, cards) {
1811
1865
  context: card.context || "",
1812
1866
  symbiosis_mode: symbiosisMode,
1813
1867
  source_link: card.source_link || null,
1814
- question: card.question || null
1868
+ question: card.question || null,
1869
+ provider: card.provider || null,
1870
+ topic_id: card.topic_id || null
1815
1871
  });
1816
1872
  createdCount++;
1873
+ } else {
1874
+ if (!token.provider && card.provider) {
1875
+ await updateToken(tx, token.slug, {
1876
+ provider: card.provider,
1877
+ topic_id: card.topic_id || null
1878
+ });
1879
+ }
1817
1880
  }
1818
1881
  const existingCard = await getCard(tx, token.id, userId);
1819
1882
  if (!existingCard) {
@@ -2035,11 +2098,12 @@ async function confirmSourceImport(db, userId, sourceId, proposals) {
2035
2098
  let createdCount = 0;
2036
2099
  let linkedCount = 0;
2037
2100
  await db.transaction(async (tx) => {
2038
- const source = await tx.prepare("SELECT id FROM sources WHERE id = ?").get(sourceId);
2039
- if (!source) {
2040
- throw new Error(`Source not found: ${sourceId}`);
2041
- }
2042
2101
  for (const card of proposals) {
2102
+ const cardSourceId = card.source_id || sourceId;
2103
+ const source = await tx.prepare("SELECT id FROM sources WHERE id = ?").get(cardSourceId);
2104
+ if (!source) {
2105
+ throw new Error(`Source not found: ${cardSourceId}`);
2106
+ }
2043
2107
  const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
2044
2108
  const cleanDomain = slugify(card.domain || "");
2045
2109
  const cleanBase = slugify(baseText);
@@ -2070,11 +2134,19 @@ async function confirmSourceImport(db, userId, sourceId, proposals) {
2070
2134
  bloom_level: bloom,
2071
2135
  context: card.excerpt || "",
2072
2136
  symbiosis_mode: symbiosisMode,
2073
- question: card.question || null
2137
+ question: card.question || null,
2138
+ provider: card.provider || null,
2139
+ topic_id: card.topic_id || null
2074
2140
  });
2075
2141
  createdCount++;
2076
2142
  } else {
2077
2143
  linkedCount++;
2144
+ if (!token.provider && card.provider) {
2145
+ await updateToken(tx, token.slug, {
2146
+ provider: card.provider,
2147
+ topic_id: card.topic_id || null
2148
+ });
2149
+ }
2078
2150
  }
2079
2151
  const existingCard = await getCard(tx, token.id, userId);
2080
2152
  if (!existingCard) {
@@ -2086,7 +2158,12 @@ async function confirmSourceImport(db, userId, sourceId, proposals) {
2086
2158
  ON CONFLICT(token_id, source_id) DO UPDATE SET
2087
2159
  excerpt = excluded.excerpt,
2088
2160
  page_number = excluded.page_number`
2089
- ).run(token.id, sourceId, card.excerpt || "", card.page_number || null);
2161
+ ).run(
2162
+ token.id,
2163
+ cardSourceId,
2164
+ card.excerpt || "",
2165
+ card.page_number || null
2166
+ );
2090
2167
  }
2091
2168
  });
2092
2169
  return { createdCount, linkedCount };
@@ -6069,6 +6146,14 @@ var BLOOM_VERBS2 = {
6069
6146
  4: "Analyze",
6070
6147
  5: "Synthesize"
6071
6148
  };
6149
+ var LlmResponseTruncatedError = class extends Error {
6150
+ constructor(label) {
6151
+ super(
6152
+ `${label}: the model's response was cut off before producing usable output (finish_reason=length). The prompt likely left no room in the model's context window for a reply.`
6153
+ );
6154
+ this.name = "LlmResponseTruncatedError";
6155
+ }
6156
+ };
6072
6157
  async function readChatContent(res, label) {
6073
6158
  if (!res.ok) {
6074
6159
  const errorText = await res.text().catch(() => "");
@@ -6077,11 +6162,16 @@ async function readChatContent(res, label) {
6077
6162
  );
6078
6163
  }
6079
6164
  const data = await res.json();
6080
- const content = data.choices?.[0]?.message?.content;
6081
- if (!content) {
6165
+ const choice = data.choices?.[0];
6166
+ const content = choice?.message?.content;
6167
+ const trimmed = content?.trim() ?? "";
6168
+ if (choice?.finish_reason === "length") {
6169
+ throw new LlmResponseTruncatedError(label);
6170
+ }
6171
+ if (!trimmed) {
6082
6172
  throw new Error("Empty response from LLM");
6083
6173
  }
6084
- return content.trim();
6174
+ return trimmed;
6085
6175
  }
6086
6176
  async function generateQuestionViaLLM(db, input8) {
6087
6177
  const cfg = await getProviderForRole(db, "recall");
@@ -6248,15 +6338,42 @@ function parseGeneratedCardArray(responseText, label, limits) {
6248
6338
  };
6249
6339
  });
6250
6340
  }
6251
- async function importCurriculumViaLLM(db, text, targetCategory, sourceUrl) {
6252
- if (text.length > MAX_IMPORT_TEXT_CHARS) {
6253
- throw new Error(
6254
- `Curriculum text exceeds the ${MAX_IMPORT_TEXT_CHARS.toLocaleString()} character limit`
6255
- );
6341
+ var CURRICULUM_CHUNK_TEXT_CHARS = 3e3;
6342
+ function splitCurriculumText(text, maxChars) {
6343
+ if (text.length <= maxChars) return [text];
6344
+ const chunks = [];
6345
+ let start = 0;
6346
+ while (start < text.length) {
6347
+ let end = Math.min(start + maxChars, text.length);
6348
+ if (end < text.length) {
6349
+ const minimumBoundary = start + Math.floor(maxChars * 0.6);
6350
+ const sentenceWindow = text.slice(minimumBoundary, end);
6351
+ let sentenceBoundary = -1;
6352
+ for (const match of sentenceWindow.matchAll(/[.!?;:]\s+/g)) {
6353
+ sentenceBoundary = (match.index ?? 0) + match[0].length;
6354
+ }
6355
+ if (sentenceBoundary > 0) {
6356
+ end = minimumBoundary + sentenceBoundary;
6357
+ } else {
6358
+ const whitespaceBoundary = text.lastIndexOf(" ", end);
6359
+ if (whitespaceBoundary >= minimumBoundary) end = whitespaceBoundary + 1;
6360
+ }
6361
+ }
6362
+ chunks.push(text.slice(start, end));
6363
+ start = end;
6256
6364
  }
6257
- const cfg = await getProviderForRole(db, "text");
6258
- const endpoint = await resolveUsableTextEndpoint(db);
6259
- const langName = LANGUAGE_NAMES[cfg.locale] || "English";
6365
+ return chunks;
6366
+ }
6367
+ function deduplicateGeneratedCards(cards) {
6368
+ const seen = /* @__PURE__ */ new Set();
6369
+ return cards.filter((card) => {
6370
+ const key = `${card.question.trim().toLocaleLowerCase()}\0${card.concept.trim().toLocaleLowerCase()}`;
6371
+ if (seen.has(key)) return false;
6372
+ seen.add(key);
6373
+ return true;
6374
+ });
6375
+ }
6376
+ async function requestCurriculumCards(endpoint, locale, langName, curriculumText, targetCategory, sourceUrl) {
6260
6377
  const systemPrompt = `You are ZAM, a highly precise agentic curriculum parser.
6261
6378
  Your task is to analyze curriculum objectives, syllabus requirements, or textbook notes, and extract atomic facts, concepts, or skills as structured learning cards in ${langName}.
6262
6379
 
@@ -6285,7 +6402,7 @@ Guidelines:
6285
6402
  curriculum section.
6286
6403
  - Output ONLY a raw valid JSON array of objects. Do NOT wrap the JSON in markdown code blocks, HTML, or include any preamble, headers, or conversational filler.`;
6287
6404
  const userPrompt = `Curriculum Text to Parse:
6288
- ${text}
6405
+ ${curriculumText}
6289
6406
 
6290
6407
  Target Category: ${targetCategory}
6291
6408
  ${sourceUrl ? `Source Reference Link: ${sourceUrl}` : ""}
@@ -6308,10 +6425,73 @@ JSON Array Output:`;
6308
6425
  temperature: 0.1,
6309
6426
  max_tokens: DEFAULT_LLM_MAX_TOKENS
6310
6427
  }),
6311
- locale: cfg.locale
6428
+ locale
6312
6429
  }
6313
6430
  );
6314
- const responseText = await readChatContent(res, "LLM curriculum import");
6431
+ return readChatContent(res, "LLM curriculum import");
6432
+ }
6433
+ async function importCurriculumViaLLM(db, text, targetCategory, sourceUrl) {
6434
+ if (text.length > MAX_IMPORT_TEXT_CHARS) {
6435
+ throw new Error(
6436
+ `Curriculum text exceeds the ${MAX_IMPORT_TEXT_CHARS.toLocaleString()} character limit`
6437
+ );
6438
+ }
6439
+ const cfg = await getProviderForRole(db, "text");
6440
+ const endpoint = await resolveUsableTextEndpoint(db);
6441
+ const langName = LANGUAGE_NAMES[cfg.locale] || "English";
6442
+ let responseText;
6443
+ try {
6444
+ responseText = await requestCurriculumCards(
6445
+ endpoint,
6446
+ cfg.locale,
6447
+ langName,
6448
+ text,
6449
+ targetCategory,
6450
+ sourceUrl
6451
+ );
6452
+ } catch (err) {
6453
+ if (!(err instanceof LlmResponseTruncatedError) || text.length <= CURRICULUM_CHUNK_TEXT_CHARS) {
6454
+ throw err;
6455
+ }
6456
+ const chunks = splitCurriculumText(text, CURRICULUM_CHUNK_TEXT_CHARS);
6457
+ const cards = [];
6458
+ for (const [index, chunk] of chunks.entries()) {
6459
+ let chunkResponse;
6460
+ try {
6461
+ chunkResponse = await requestCurriculumCards(
6462
+ endpoint,
6463
+ cfg.locale,
6464
+ langName,
6465
+ chunk,
6466
+ targetCategory,
6467
+ sourceUrl
6468
+ );
6469
+ } catch (chunkError) {
6470
+ if (chunkError instanceof LlmResponseTruncatedError) {
6471
+ throw new Error(
6472
+ `Curriculum chunk ${index + 1} of ${chunks.length} is still too large for the configured model's context window. Configure a model with a larger context window, or import a smaller source.`
6473
+ );
6474
+ }
6475
+ throw chunkError;
6476
+ }
6477
+ cards.push(
6478
+ ...parseGeneratedCardArray(chunkResponse, "curriculum import", {
6479
+ min: 0,
6480
+ max: 200
6481
+ })
6482
+ );
6483
+ }
6484
+ const deduplicated = deduplicateGeneratedCards(cards);
6485
+ if (deduplicated.length > 200) {
6486
+ throw new Error(
6487
+ `Invalid curriculum import response: expected 0-200 cards, got ${deduplicated.length}`
6488
+ );
6489
+ }
6490
+ return deduplicated.map((card) => ({
6491
+ ...card,
6492
+ source_link: sourceUrl || null
6493
+ }));
6494
+ }
6315
6495
  return parseGeneratedCardArray(responseText, "curriculum import", {
6316
6496
  min: 0,
6317
6497
  max: 200
@@ -7206,7 +7386,7 @@ async function readWebLink(url) {
7206
7386
  redirect: "manual",
7207
7387
  signal: controller.signal,
7208
7388
  headers: {
7209
- "User-Agent": "ZAM-Content-Studio/0.6.1"
7389
+ "User-Agent": "ZAM-Content-Studio/0.6.3"
7210
7390
  }
7211
7391
  });
7212
7392
  if (res.status >= 300 && res.status < 400) {
@@ -7269,114 +7449,2775 @@ async function readImageOCR(db, imagePath) {
7269
7449
  return extractTextFromScanViaLLM(db, imagePath);
7270
7450
  }
7271
7451
 
7272
- // src/cli/llm/vision.ts
7273
- import { randomBytes } from "crypto";
7274
- import { readFileSync as readFileSync10 } from "fs";
7275
- import { tmpdir as tmpdir2 } from "os";
7276
- import { basename as basename3, join as join14 } from "path";
7277
- var LANGUAGE_NAMES2 = {
7278
- en: "English",
7279
- de: "German",
7280
- es: "Spanish",
7281
- fr: "French",
7282
- pt: "Portuguese",
7283
- zh: "Chinese",
7284
- ja: "Japanese"
7285
- };
7286
- var OBSERVATION_KINDS2 = /* @__PURE__ */ new Set([
7287
- "progress",
7288
- "step-completed",
7289
- "error",
7290
- "help-seeking",
7291
- "uncertain",
7292
- "privacy-pause",
7293
- "heartbeat"
7294
- ]);
7295
- var ACTION_TYPES2 = /* @__PURE__ */ new Set([
7296
- "click",
7297
- "shortcut",
7298
- "typing",
7299
- "scroll",
7300
- "window-change"
7301
- ]);
7302
- async function observeUiSnapshotViaLLM(db, input8) {
7303
- const cfg = await getProviderForRole(db, "vision");
7304
- if (!cfg.enabled) {
7305
- throw new Error(
7306
- "Vision observation is disabled in settings (llm.vision.enabled)"
7307
- );
7452
+ // src/cli/curriculum/breadcrumb.ts
7453
+ var LAST_SELECTION_KEY = "curriculum.lastSelection";
7454
+ async function getLastCurriculumSelection(db) {
7455
+ const raw = await getSetting(db, LAST_SELECTION_KEY);
7456
+ if (!raw) return void 0;
7457
+ try {
7458
+ return JSON.parse(raw);
7459
+ } catch {
7460
+ return void 0;
7308
7461
  }
7309
- const imageUrls = [];
7310
- const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
7311
- if (isVideo) {
7312
- const { mkdirSync: mkdirSync14, readdirSync: readdirSync3, rmSync: rmSync4 } = await import("fs");
7313
- const { execSync: execSync6 } = await import("child_process");
7314
- const tempDir = join14(
7315
- tmpdir2(),
7316
- `zam-frames-${randomBytes(4).toString("hex")}`
7462
+ }
7463
+ async function setLastCurriculumSelection(db, breadcrumb) {
7464
+ await setSetting(db, LAST_SELECTION_KEY, JSON.stringify(breadcrumb));
7465
+ }
7466
+
7467
+ // src/cli/curriculum/providers/bildungsplan-bremen/manifest.ts
7468
+ var BILDUNGSPLAN_BREMEN_MANIFEST = {
7469
+ schoolYear: "2025/2026",
7470
+ capturedOn: "2026-07-02",
7471
+ sourceRevision: "Bildungspl\xE4ne Bremen",
7472
+ schoolTypes: [
7473
+ { id: "oberschule", label: "Oberschule" },
7474
+ { id: "gymnasium", label: "Gymnasium" }
7475
+ ],
7476
+ grades: {
7477
+ oberschule: ["7", "8", "9", "10"],
7478
+ gymnasium: ["7", "8", "9", "10"]
7479
+ },
7480
+ subjects: {
7481
+ oberschule: [
7482
+ { id: "mathematik", label: "Mathematik" },
7483
+ { id: "informatik", label: "Informatik" },
7484
+ { id: "physik", label: "Physik" },
7485
+ { id: "chemie", label: "Chemie" },
7486
+ { id: "biologie", label: "Biologie" }
7487
+ ],
7488
+ gymnasium: [
7489
+ { id: "mathematik", label: "Mathematik" },
7490
+ { id: "informatik", label: "Informatik" },
7491
+ { id: "physik", label: "Physik" },
7492
+ { id: "chemie", label: "Chemie" },
7493
+ { id: "biologie", label: "Biologie" }
7494
+ ]
7495
+ },
7496
+ tracks: {},
7497
+ topics: {
7498
+ "oberschule|10|mathematik": [
7499
+ { id: "funktionen", label: "Funktionen" },
7500
+ { id: "geometrie", label: "Geometrie" }
7501
+ ],
7502
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
7503
+ "oberschule|9|informatik": [{ id: "algorithmen", label: "Algorithmen" }],
7504
+ "oberschule|9|physik": [{ id: "mechanik", label: "Mechanik" }],
7505
+ "oberschule|9|chemie": [
7506
+ { id: "reaktionen", label: "Chemische Reaktionen" }
7507
+ ],
7508
+ "oberschule|9|biologie": [{ id: "zellen", label: "Zellen" }],
7509
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
7510
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Chemische Bindungen" }],
7511
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
7512
+ },
7513
+ contentUrls: {
7514
+ "oberschule|10|mathematik": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7515
+ "gymnasium|10|mathematik": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7516
+ "oberschule|9|informatik": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7517
+ "oberschule|9|physik": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7518
+ "oberschule|9|chemie": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7519
+ "oberschule|9|biologie": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7520
+ "gymnasium|9|physik": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7521
+ "gymnasium|9|chemie": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7522
+ "gymnasium|9|biologie": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942"
7523
+ }
7524
+ };
7525
+
7526
+ // src/cli/curriculum/providers/bildungsplan-bremen/index.ts
7527
+ var bildungsplanBremenProvider = {
7528
+ id: "bildungsplan-bremen",
7529
+ country: "DE",
7530
+ countryLabel: "Deutschland",
7531
+ region: "HB",
7532
+ regionLabel: "Bremen",
7533
+ label: "Bildungsplan (Bremen)",
7534
+ listSchoolTypes() {
7535
+ return BILDUNGSPLAN_BREMEN_MANIFEST.schoolTypes;
7536
+ },
7537
+ listGrades(schoolType) {
7538
+ return (BILDUNGSPLAN_BREMEN_MANIFEST.grades[schoolType] || []).map((id) => ({
7539
+ id,
7540
+ label: `Klasse ${id}`
7541
+ }));
7542
+ },
7543
+ listSubjects(schoolType, _grade) {
7544
+ return BILDUNGSPLAN_BREMEN_MANIFEST.subjects[schoolType] || [];
7545
+ },
7546
+ listTracks(schoolType, grade, subject) {
7547
+ const key = `${schoolType}|${grade}|${subject}`;
7548
+ return BILDUNGSPLAN_BREMEN_MANIFEST.tracks[key] || [];
7549
+ },
7550
+ listTopics(selection) {
7551
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
7552
+ const list = BILDUNGSPLAN_BREMEN_MANIFEST.topics[key] || [];
7553
+ return list.map((t2) => ({
7554
+ ...t2,
7555
+ sourceRef: key
7556
+ }));
7557
+ },
7558
+ resolveTopic(topic) {
7559
+ const uri = BILDUNGSPLAN_BREMEN_MANIFEST.contentUrls[topic.sourceRef];
7560
+ if (!uri) {
7561
+ throw new Error(
7562
+ `Bildungsplan Bremen: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
7563
+ );
7564
+ }
7565
+ return {
7566
+ provider: "bildungsplan-bremen",
7567
+ topicId: `${topic.sourceRef}#${topic.id}`,
7568
+ uri
7569
+ };
7570
+ },
7571
+ extractTopics(html, topicIds) {
7572
+ const results = {};
7573
+ const headingMatches = Array.from(
7574
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
7317
7575
  );
7318
- mkdirSync14(tempDir, { recursive: true });
7319
- try {
7320
- execSync6(
7321
- `ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
7322
- { stdio: "ignore" }
7576
+ const sections = [];
7577
+ for (const m of headingMatches) {
7578
+ const headerText = cleanHtmlText(m[1]).trim();
7579
+ const start = m.index + m[0].length;
7580
+ const nextHeading = html.indexOf("<h", start);
7581
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
7582
+ const content = cleanHtmlText(rawContent).slice(0, 1500);
7583
+ if (headerText) sections.push({ header: headerText, content });
7584
+ }
7585
+ for (const topicId of topicIds) {
7586
+ const hashIdx = topicId.indexOf("#");
7587
+ if (hashIdx === -1) continue;
7588
+ const key = topicId.substring(0, hashIdx);
7589
+ const shortId = topicId.substring(hashIdx + 1);
7590
+ const list = BILDUNGSPLAN_BREMEN_MANIFEST.topics[key] ?? [];
7591
+ const match = list.find((t2) => t2.id === shortId);
7592
+ if (!match) continue;
7593
+ const label = match.label;
7594
+ const normalizedLabel = normalizeForComparison(label);
7595
+ let section = sections.find(
7596
+ (s) => normalizeForComparison(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
7323
7597
  );
7324
- let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
7325
- if (files.length === 0) {
7326
- execSync6(
7327
- `ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
7328
- { stdio: "ignore" }
7329
- );
7330
- files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
7331
- }
7332
- const maxFrames = cfg.maxFrames ?? 100;
7333
- let sampledFiles = files;
7334
- if (files.length > maxFrames) {
7335
- if (maxFrames <= 1) {
7336
- sampledFiles = [files[0]];
7337
- } else {
7338
- const step = (files.length - 1) / (maxFrames - 1);
7339
- sampledFiles = [];
7340
- for (let i = 0; i < maxFrames; i++) {
7341
- const index = Math.round(i * step);
7342
- sampledFiles.push(files[index]);
7343
- }
7344
- }
7345
- }
7346
- for (const file of sampledFiles) {
7347
- const bytes = readFileSync10(join14(tempDir, file));
7348
- imageUrls.push(`data:image/png;base64,${bytes.toString("base64")}`);
7598
+ if (!section && sections.length > 0) {
7599
+ section = sections.find((s) => s.content.length > 80) || sections[0];
7349
7600
  }
7350
- } finally {
7351
- try {
7352
- rmSync4(tempDir, { recursive: true, force: true });
7353
- } catch {
7601
+ if (section) {
7602
+ results[topicId] = `${label}
7603
+
7604
+ ${section.content}`.trim();
7605
+ } else {
7606
+ results[topicId] = label;
7354
7607
  }
7355
7608
  }
7356
- } else {
7357
- const imageBytes = readFileSync10(input8.imagePath);
7358
- const ext = input8.imagePath.split(".").pop()?.toLowerCase();
7359
- const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
7360
- imageUrls.push(`data:${mime};base64,${imageBytes.toString("base64")}`);
7609
+ return results;
7361
7610
  }
7362
- if (imageUrls.length === 0) {
7363
- throw new Error("No image data available for vision analysis");
7611
+ };
7612
+ function cleanHtmlText(html) {
7613
+ let text = html.replace(
7614
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
7615
+ " "
7616
+ );
7617
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
7618
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
7619
+ text = text.replace(/<p[^>]*>/gi, "\n");
7620
+ text = text.replace(/<br\s*\/?>/gi, "\n");
7621
+ text = text.replace(/<[^>]+>/g, " ");
7622
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
7623
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
7624
+ }
7625
+ function normalizeForComparison(str) {
7626
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
7627
+ }
7628
+
7629
+ // src/cli/curriculum/providers/bildungsplan-bw/manifest.ts
7630
+ var BILDUNGSPLAN_BW_MANIFEST = {
7631
+ schoolYear: "2026/2027",
7632
+ capturedOn: "2026-07-02",
7633
+ sourceRevision: "Bildungsplan Baden-W\xFCrttemberg Gymnasium 2016",
7634
+ schoolTypes: [{ id: "gymnasium", label: "Gymnasium" }],
7635
+ grades: {
7636
+ gymnasium: ["9", "10", "11", "12"]
7637
+ },
7638
+ subjects: {
7639
+ gymnasium: [
7640
+ { id: "mathematik", label: "Mathematik" },
7641
+ { id: "informatik", label: "Informatik" },
7642
+ { id: "physik", label: "Physik" },
7643
+ { id: "chemie", label: "Chemie" },
7644
+ { id: "biologie", label: "Biologie" }
7645
+ ]
7646
+ },
7647
+ tracks: {},
7648
+ topics: {
7649
+ "gymnasium|10|mathematik": [
7650
+ { id: "leitidee-zahl", label: "Leitidee Zahl - Variable - Operation" },
7651
+ { id: "leitidee-messung", label: "Leitidee Messen" },
7652
+ { id: "leitidee-raum", label: "Leitidee Raum und Form" },
7653
+ { id: "leitidee-funktion", label: "Leitidee Funktionaler Zusammenhang" },
7654
+ { id: "leitidee-daten", label: "Leitidee Daten und Zufall" }
7655
+ ],
7656
+ "gymnasium|10|informatik": [
7657
+ { id: "ik-daten-codierung", label: "Daten und Codierung" },
7658
+ { id: "ik-algorithmen", label: "Algorithmen" },
7659
+ { id: "ik-rechner-netze", label: "Rechner und Netze" },
7660
+ {
7661
+ id: "ik-informationsgesellschaft",
7662
+ label: "Informationsgesellschaft und Datensicherheit"
7663
+ }
7664
+ ],
7665
+ "gymnasium|10|physik": [
7666
+ { id: "optik-akustik", label: "Optik und Akustik" },
7667
+ { id: "mechanik", label: "Mechanik" },
7668
+ {
7669
+ id: "elektrizitaet-magnetismus",
7670
+ label: "Elektrizit\xE4t und Magnetismus"
7671
+ },
7672
+ { id: "energie-waerme", label: "Energie und W\xE4rmelehre" }
7673
+ ],
7674
+ "gymnasium|10|chemie": [
7675
+ { id: "stoffe-reaktionen", label: "Stoffe und chemische Reaktionen" },
7676
+ { id: "atome-molekuele", label: "Atome und Molek\xFCle" },
7677
+ { id: "saeuren-basen", label: "S\xE4uren, Basen und Salze" },
7678
+ { id: "organische-chemie", label: "Organische Chemie" }
7679
+ ],
7680
+ "gymnasium|10|biologie": [
7681
+ { id: "zellen", label: "Zellen und Gewebe" },
7682
+ { id: "genetik", label: "Genetik und Vererbung" },
7683
+ { id: "oekologie", label: "\xD6kologie und Umwelt" },
7684
+ { id: "evolution", label: "Evolution und Vielfalt" }
7685
+ ]
7686
+ },
7687
+ contentUrls: {
7688
+ "gymnasium|10|mathematik": "http://www.bildungsplaene-bw.de/,Lde/LS/BP2016BW/ALLG/GYM/M/bp/klasse-10",
7689
+ "gymnasium|10|informatik": "https://www.bildungsplaene-bw.de/,Lde/BP2016BW_ALLG_SEK1_INFWF",
7690
+ "gymnasium|10|physik": "https://www.bildungsplaene-bw.de/,Lde/BP2016BW_ALLG_GYM_PH",
7691
+ "gymnasium|10|chemie": "https://www.bildungsplaene-bw.de/,Lde/BP2016BW_ALLG_GYM_CH",
7692
+ "gymnasium|10|biologie": "https://www.bildungsplaene-bw.de/,Lde/BP2016BW_ALLG_GYM_BIO"
7364
7693
  }
7365
- const endpoints = [
7366
- {
7367
- url: cfg.url,
7368
- apiKey: cfg.apiKey || DEFAULT_LLM_API_KEY,
7369
- model: input8.model ?? cfg.model,
7370
- apiFlavor: cfg.apiFlavor
7694
+ };
7695
+
7696
+ // src/cli/curriculum/providers/bildungsplan-bw/index.ts
7697
+ var bildungsplanBwProvider = {
7698
+ id: "bildungsplan-bw",
7699
+ country: "DE",
7700
+ countryLabel: "Deutschland",
7701
+ region: "BW",
7702
+ regionLabel: "Baden-W\xFCrttemberg",
7703
+ label: "Bildungsplan (Baden-W\xFCrttemberg)",
7704
+ listSchoolTypes() {
7705
+ return BILDUNGSPLAN_BW_MANIFEST.schoolTypes;
7706
+ },
7707
+ listGrades(schoolType) {
7708
+ return (BILDUNGSPLAN_BW_MANIFEST.grades[schoolType] || []).map((id) => ({
7709
+ id,
7710
+ label: `Klasse ${id}`
7711
+ }));
7712
+ },
7713
+ listSubjects(schoolType, _grade) {
7714
+ return BILDUNGSPLAN_BW_MANIFEST.subjects[schoolType] || [];
7715
+ },
7716
+ listTracks(schoolType, grade, subject) {
7717
+ const key = `${schoolType}|${grade}|${subject}`;
7718
+ return BILDUNGSPLAN_BW_MANIFEST.tracks[key] || [];
7719
+ },
7720
+ listTopics(selection) {
7721
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
7722
+ const list = BILDUNGSPLAN_BW_MANIFEST.topics[key] || [];
7723
+ return list.map((t2) => ({
7724
+ ...t2,
7725
+ sourceRef: key
7726
+ }));
7727
+ },
7728
+ resolveTopic(topic) {
7729
+ const uri = BILDUNGSPLAN_BW_MANIFEST.contentUrls[topic.sourceRef];
7730
+ if (!uri) {
7731
+ throw new Error(
7732
+ `Bildungsplan Baden-W\xFCrttemberg: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
7733
+ );
7371
7734
  }
7372
- ];
7373
- if (cfg.fallback) {
7374
- endpoints.push({
7375
- url: cfg.fallback.url,
7376
- apiKey: cfg.fallback.apiKey || DEFAULT_LLM_API_KEY,
7377
- model: cfg.fallback.model,
7378
- apiFlavor: cfg.fallback.apiFlavor
7379
- });
7735
+ return {
7736
+ provider: "bildungsplan-bw",
7737
+ topicId: `${topic.sourceRef}#${topic.id}`,
7738
+ uri
7739
+ };
7740
+ },
7741
+ extractTopics(html, topicIds) {
7742
+ const chunks = html.split('<div class="bp-topic-block" id="bp_topic_');
7743
+ const sections = [];
7744
+ for (let i = 1; i < chunks.length; i++) {
7745
+ const chunk = chunks[i];
7746
+ const quoteIdx = chunk.indexOf('"');
7747
+ if (quoteIdx === -1) continue;
7748
+ const id = chunk.slice(0, quoteIdx);
7749
+ const contentHtml = chunk.slice(quoteIdx + 1);
7750
+ const headerMatch = contentHtml.match(
7751
+ /<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/i
7752
+ );
7753
+ const headerText = headerMatch ? cleanHtmlText2(headerMatch[1]).trim() : "";
7754
+ sections.push({
7755
+ id,
7756
+ headerText,
7757
+ contentHtml
7758
+ });
7759
+ }
7760
+ const results = {};
7761
+ for (const topicId of topicIds) {
7762
+ const hashIdx = topicId.indexOf("#");
7763
+ if (hashIdx === -1) continue;
7764
+ const key = topicId.substring(0, hashIdx);
7765
+ const shortId = topicId.substring(hashIdx + 1);
7766
+ const list = BILDUNGSPLAN_BW_MANIFEST.topics[key] ?? [];
7767
+ const match = list.find((t2) => t2.id === shortId);
7768
+ if (!match) continue;
7769
+ const label = match.label;
7770
+ const normalizedLabel = normalizeForComparison2(label);
7771
+ const sectionIndex = sections.findIndex((s) => {
7772
+ const normalizedHeader = normalizeForComparison2(s.headerText);
7773
+ return normalizedHeader.includes(normalizedLabel) || s.id === shortId;
7774
+ });
7775
+ if (sectionIndex === -1) {
7776
+ continue;
7777
+ }
7778
+ results[topicId] = cleanHtmlText2(sections[sectionIndex].contentHtml);
7779
+ }
7780
+ return results;
7781
+ }
7782
+ };
7783
+ function cleanHtmlText2(html) {
7784
+ let text = html.replace(
7785
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
7786
+ " "
7787
+ );
7788
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
7789
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
7790
+ text = text.replace(/<p[^>]*>/gi, "\n");
7791
+ text = text.replace(/<br\s*\/?>/gi, "\n");
7792
+ text = text.replace(/<[^>]+>/g, " ");
7793
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&ndash;/g, "\u2013").replace(/&mdash;/g, "\u2014").replace(/&middot;/g, "\xB7");
7794
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
7795
+ }
7796
+ function normalizeForComparison2(str) {
7797
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
7798
+ }
7799
+
7800
+ // src/cli/curriculum/providers/bildungsplan-hamburg/manifest.ts
7801
+ var BILDUNGSPLAN_HAMBURG_MANIFEST = {
7802
+ schoolYear: "2025/2026",
7803
+ capturedOn: "2026-07-02",
7804
+ sourceRevision: "Bildungspl\xE4ne Hamburg",
7805
+ schoolTypes: [
7806
+ { id: "stadtteilschule", label: "Stadtteilschule (Realschule)" },
7807
+ { id: "gymnasium", label: "Gymnasium" }
7808
+ ],
7809
+ grades: {
7810
+ stadtteilschule: ["7", "8", "9", "10"],
7811
+ gymnasium: ["7", "8", "9", "10"]
7812
+ },
7813
+ subjects: {
7814
+ stadtteilschule: [
7815
+ { id: "mathematik", label: "Mathematik" },
7816
+ { id: "informatik", label: "Informatik" },
7817
+ { id: "physik", label: "Physik" },
7818
+ { id: "chemie", label: "Chemie" },
7819
+ { id: "biologie", label: "Biologie" }
7820
+ ],
7821
+ gymnasium: [
7822
+ { id: "mathematik", label: "Mathematik" },
7823
+ { id: "informatik", label: "Informatik" },
7824
+ { id: "physik", label: "Physik" },
7825
+ { id: "chemie", label: "Chemie" },
7826
+ { id: "biologie", label: "Biologie" }
7827
+ ]
7828
+ },
7829
+ tracks: {},
7830
+ topics: {
7831
+ "stadtteilschule|10|mathematik": [
7832
+ { id: "funktionen", label: "Funktionen" },
7833
+ { id: "geometrie", label: "Geometrie" }
7834
+ ],
7835
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
7836
+ "stadtteilschule|9|physik": [{ id: "mechanik", label: "Mechanik" }],
7837
+ "stadtteilschule|9|chemie": [{ id: "reaktionen", label: "Reaktionen" }],
7838
+ "stadtteilschule|9|biologie": [{ id: "zellen", label: "Zellen" }],
7839
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
7840
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Bindungen" }],
7841
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
7842
+ },
7843
+ contentUrls: {
7844
+ "stadtteilschule|10|mathematik": "https://www.hamburg.de/bildungsplaene",
7845
+ "gymnasium|10|mathematik": "https://www.hamburg.de/bildungsplaene",
7846
+ "stadtteilschule|9|physik": "https://www.hamburg.de/bildungsplaene",
7847
+ "stadtteilschule|9|chemie": "https://www.hamburg.de/bildungsplaene",
7848
+ "stadtteilschule|9|biologie": "https://www.hamburg.de/bildungsplaene",
7849
+ "gymnasium|9|physik": "https://www.hamburg.de/bildungsplaene",
7850
+ "gymnasium|9|chemie": "https://www.hamburg.de/bildungsplaene",
7851
+ "gymnasium|9|biologie": "https://www.hamburg.de/bildungsplaene"
7852
+ }
7853
+ };
7854
+
7855
+ // src/cli/curriculum/providers/bildungsplan-hamburg/index.ts
7856
+ var bildungsplanHamburgProvider = {
7857
+ id: "bildungsplan-hamburg",
7858
+ country: "DE",
7859
+ countryLabel: "Deutschland",
7860
+ region: "HH",
7861
+ regionLabel: "Hamburg",
7862
+ label: "Bildungsplan (Hamburg)",
7863
+ listSchoolTypes() {
7864
+ return BILDUNGSPLAN_HAMBURG_MANIFEST.schoolTypes;
7865
+ },
7866
+ listGrades(schoolType) {
7867
+ return (BILDUNGSPLAN_HAMBURG_MANIFEST.grades[schoolType] || []).map((id) => ({
7868
+ id,
7869
+ label: `Klasse ${id}`
7870
+ }));
7871
+ },
7872
+ listSubjects(schoolType, _grade) {
7873
+ return BILDUNGSPLAN_HAMBURG_MANIFEST.subjects[schoolType] || [];
7874
+ },
7875
+ listTracks(schoolType, grade, subject) {
7876
+ const key = `${schoolType}|${grade}|${subject}`;
7877
+ return BILDUNGSPLAN_HAMBURG_MANIFEST.tracks[key] || [];
7878
+ },
7879
+ listTopics(selection) {
7880
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
7881
+ const list = BILDUNGSPLAN_HAMBURG_MANIFEST.topics[key] || [];
7882
+ return list.map((t2) => ({
7883
+ ...t2,
7884
+ sourceRef: key
7885
+ }));
7886
+ },
7887
+ resolveTopic(topic) {
7888
+ const uri = BILDUNGSPLAN_HAMBURG_MANIFEST.contentUrls[topic.sourceRef];
7889
+ if (!uri) {
7890
+ throw new Error(
7891
+ `Bildungsplan Hamburg: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
7892
+ );
7893
+ }
7894
+ return {
7895
+ provider: "bildungsplan-hamburg",
7896
+ topicId: `${topic.sourceRef}#${topic.id}`,
7897
+ uri
7898
+ };
7899
+ },
7900
+ extractTopics(html, topicIds) {
7901
+ const results = {};
7902
+ const headingMatches = Array.from(
7903
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
7904
+ );
7905
+ const sections = [];
7906
+ for (const m of headingMatches) {
7907
+ const headerText = cleanHtmlText3(m[1]).trim();
7908
+ const start = m.index + m[0].length;
7909
+ const nextHeading = html.indexOf("<h", start);
7910
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
7911
+ const content = cleanHtmlText3(rawContent).slice(0, 1500);
7912
+ if (headerText) sections.push({ header: headerText, content });
7913
+ }
7914
+ for (const topicId of topicIds) {
7915
+ const hashIdx = topicId.indexOf("#");
7916
+ if (hashIdx === -1) continue;
7917
+ const key = topicId.substring(0, hashIdx);
7918
+ const shortId = topicId.substring(hashIdx + 1);
7919
+ const list = BILDUNGSPLAN_HAMBURG_MANIFEST.topics[key] ?? [];
7920
+ const match = list.find((t2) => t2.id === shortId);
7921
+ if (!match) continue;
7922
+ const label = match.label;
7923
+ const normalizedLabel = normalizeForComparison3(label);
7924
+ let section = sections.find(
7925
+ (s) => normalizeForComparison3(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
7926
+ );
7927
+ if (!section && sections.length > 0) {
7928
+ section = sections.find((s) => s.content.length > 80) || sections[0];
7929
+ }
7930
+ if (section) {
7931
+ results[topicId] = `${label}
7932
+
7933
+ ${section.content}`.trim();
7934
+ } else {
7935
+ results[topicId] = label;
7936
+ }
7937
+ }
7938
+ return results;
7939
+ }
7940
+ };
7941
+ function cleanHtmlText3(html) {
7942
+ let text = html.replace(
7943
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
7944
+ " "
7945
+ );
7946
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
7947
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
7948
+ text = text.replace(/<p[^>]*>/gi, "\n");
7949
+ text = text.replace(/<br\s*\/?>/gi, "\n");
7950
+ text = text.replace(/<[^>]+>/g, " ");
7951
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
7952
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
7953
+ }
7954
+ function normalizeForComparison3(str) {
7955
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
7956
+ }
7957
+
7958
+ // src/cli/curriculum/providers/fachanforderungen-sh/manifest.ts
7959
+ var FACHANFORDERUNGEN_SH_MANIFEST = {
7960
+ schoolYear: "2025/2026",
7961
+ capturedOn: "2026-07-02",
7962
+ sourceRevision: "Fachanforderungen Schleswig-Holstein",
7963
+ schoolTypes: [
7964
+ { id: "gemeinschaftsschule", label: "Gemeinschaftsschule" },
7965
+ { id: "gymnasium", label: "Gymnasium" }
7966
+ ],
7967
+ grades: {
7968
+ gemeinschaftsschule: ["7", "8", "9", "10"],
7969
+ gymnasium: ["7", "8", "9", "10"]
7970
+ },
7971
+ subjects: {
7972
+ gemeinschaftsschule: [
7973
+ { id: "mathematik", label: "Mathematik" },
7974
+ { id: "informatik", label: "Informatik" },
7975
+ { id: "physik", label: "Physik" },
7976
+ { id: "chemie", label: "Chemie" },
7977
+ { id: "biologie", label: "Biologie" }
7978
+ ],
7979
+ gymnasium: [
7980
+ { id: "mathematik", label: "Mathematik" },
7981
+ { id: "informatik", label: "Informatik" },
7982
+ { id: "physik", label: "Physik" },
7983
+ { id: "chemie", label: "Chemie" },
7984
+ { id: "biologie", label: "Biologie" }
7985
+ ]
7986
+ },
7987
+ tracks: {},
7988
+ topics: {
7989
+ "gemeinschaftsschule|10|mathematik": [
7990
+ { id: "funktionen", label: "Funktionen" }
7991
+ ],
7992
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
7993
+ "gemeinschaftsschule|9|physik": [{ id: "mechanik", label: "Mechanik" }],
7994
+ "gemeinschaftsschule|9|chemie": [{ id: "reaktionen", label: "Reaktionen" }],
7995
+ "gemeinschaftsschule|9|biologie": [{ id: "zellen", label: "Zellen" }],
7996
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
7997
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Bindungen" }],
7998
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
7999
+ },
8000
+ contentUrls: {
8001
+ "gemeinschaftsschule|10|mathematik": "https://fachportal.lernnetz.de/sh/fachanforderungen.html",
8002
+ "gymnasium|10|mathematik": "https://fachportal.lernnetz.de/sh/fachanforderungen.html",
8003
+ "gemeinschaftsschule|9|physik": "https://fachportal.lernnetz.de/sh/fachanforderungen.html",
8004
+ "gemeinschaftsschule|9|chemie": "https://fachportal.lernnetz.de/sh/fachanforderungen.html",
8005
+ "gemeinschaftsschule|9|biologie": "https://fachportal.lernnetz.de/sh/fachanforderungen.html",
8006
+ "gymnasium|9|physik": "https://fachportal.lernnetz.de/sh/fachanforderungen.html",
8007
+ "gymnasium|9|chemie": "https://fachportal.lernnetz.de/sh/fachanforderungen.html",
8008
+ "gymnasium|9|biologie": "https://fachportal.lernnetz.de/sh/fachanforderungen.html"
8009
+ }
8010
+ };
8011
+
8012
+ // src/cli/curriculum/providers/fachanforderungen-sh/index.ts
8013
+ var fachanforderungenShProvider = {
8014
+ id: "fachanforderungen-sh",
8015
+ country: "DE",
8016
+ countryLabel: "Deutschland",
8017
+ region: "SH",
8018
+ regionLabel: "Schleswig-Holstein",
8019
+ label: "Fachanforderungen (Schleswig-Holstein)",
8020
+ listSchoolTypes() {
8021
+ return FACHANFORDERUNGEN_SH_MANIFEST.schoolTypes;
8022
+ },
8023
+ listGrades(schoolType) {
8024
+ return (FACHANFORDERUNGEN_SH_MANIFEST.grades[schoolType] || []).map((id) => ({
8025
+ id,
8026
+ label: `Klasse ${id}`
8027
+ }));
8028
+ },
8029
+ listSubjects(schoolType, _grade) {
8030
+ return FACHANFORDERUNGEN_SH_MANIFEST.subjects[schoolType] || [];
8031
+ },
8032
+ listTracks(schoolType, grade, subject) {
8033
+ const key = `${schoolType}|${grade}|${subject}`;
8034
+ return FACHANFORDERUNGEN_SH_MANIFEST.tracks[key] || [];
8035
+ },
8036
+ listTopics(selection) {
8037
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
8038
+ const list = FACHANFORDERUNGEN_SH_MANIFEST.topics[key] || [];
8039
+ return list.map((t2) => ({
8040
+ ...t2,
8041
+ sourceRef: key
8042
+ }));
8043
+ },
8044
+ resolveTopic(topic) {
8045
+ const uri = FACHANFORDERUNGEN_SH_MANIFEST.contentUrls[topic.sourceRef];
8046
+ if (!uri) {
8047
+ throw new Error(
8048
+ `Fachanforderungen SH: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
8049
+ );
8050
+ }
8051
+ return {
8052
+ provider: "fachanforderungen-sh",
8053
+ topicId: `${topic.sourceRef}#${topic.id}`,
8054
+ uri
8055
+ };
8056
+ },
8057
+ extractTopics(html, topicIds) {
8058
+ const results = {};
8059
+ const headingMatches = Array.from(
8060
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
8061
+ );
8062
+ const sections = [];
8063
+ for (const m of headingMatches) {
8064
+ const headerText = cleanHtmlText4(m[1]).trim();
8065
+ const start = m.index + m[0].length;
8066
+ const nextHeading = html.indexOf("<h", start);
8067
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
8068
+ const content = cleanHtmlText4(rawContent).slice(0, 1500);
8069
+ if (headerText) sections.push({ header: headerText, content });
8070
+ }
8071
+ for (const topicId of topicIds) {
8072
+ const hashIdx = topicId.indexOf("#");
8073
+ if (hashIdx === -1) continue;
8074
+ const key = topicId.substring(0, hashIdx);
8075
+ const shortId = topicId.substring(hashIdx + 1);
8076
+ const list = FACHANFORDERUNGEN_SH_MANIFEST.topics[key] ?? [];
8077
+ const match = list.find((t2) => t2.id === shortId);
8078
+ if (!match) continue;
8079
+ const label = match.label;
8080
+ const normalizedLabel = normalizeForComparison4(label);
8081
+ let section = sections.find(
8082
+ (s) => normalizeForComparison4(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
8083
+ );
8084
+ if (!section && sections.length > 0) {
8085
+ section = sections.find((s) => s.content.length > 80) || sections[0];
8086
+ }
8087
+ if (section) {
8088
+ results[topicId] = `${label}
8089
+
8090
+ ${section.content}`.trim();
8091
+ } else {
8092
+ results[topicId] = label;
8093
+ }
8094
+ }
8095
+ return results;
8096
+ }
8097
+ };
8098
+ function cleanHtmlText4(html) {
8099
+ let text = html.replace(
8100
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
8101
+ " "
8102
+ );
8103
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
8104
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
8105
+ text = text.replace(/<p[^>]*>/gi, "\n");
8106
+ text = text.replace(/<br\s*\/?>/gi, "\n");
8107
+ text = text.replace(/<[^>]+>/g, " ");
8108
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
8109
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8110
+ }
8111
+ function normalizeForComparison4(str) {
8112
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8113
+ }
8114
+
8115
+ // src/cli/curriculum/providers/kerncurriculum-hessen/manifest.ts
8116
+ var KERNCURRICULUM_HESSEN_MANIFEST = {
8117
+ schoolYear: "2025/2026",
8118
+ capturedOn: "2026-07-02",
8119
+ sourceRevision: "Kerncurricula Hessen (Sekundarstufe I)",
8120
+ schoolTypes: [
8121
+ { id: "realschule", label: "Realschule" },
8122
+ { id: "gymnasium", label: "Gymnasium" }
8123
+ ],
8124
+ grades: {
8125
+ realschule: ["7", "8", "9", "10"],
8126
+ gymnasium: ["7", "8", "9", "10"]
8127
+ },
8128
+ subjects: {
8129
+ realschule: [
8130
+ { id: "mathematik", label: "Mathematik" },
8131
+ { id: "informatik", label: "Informatik" },
8132
+ { id: "physik", label: "Physik" },
8133
+ { id: "chemie", label: "Chemie" },
8134
+ { id: "biologie", label: "Biologie" }
8135
+ ],
8136
+ gymnasium: [
8137
+ { id: "mathematik", label: "Mathematik" },
8138
+ { id: "informatik", label: "Informatik" },
8139
+ { id: "physik", label: "Physik" },
8140
+ { id: "chemie", label: "Chemie" },
8141
+ { id: "biologie", label: "Biologie" }
8142
+ ]
8143
+ },
8144
+ tracks: {},
8145
+ topics: {
8146
+ // Minimal for Hessen Realschule / Gym - based on Kerncurricula Inhaltsfelder
8147
+ "realschule|10|mathematik": [
8148
+ { id: "funktionen", label: "Funktionen" },
8149
+ { id: "geometrie", label: "Geometrie" },
8150
+ { id: "wahrscheinlichkeit", label: "Wahrscheinlichkeit und Statistik" }
8151
+ ],
8152
+ "gymnasium|10|mathematik": [
8153
+ { id: "analysis", label: "Analysis" },
8154
+ { id: "vektoren", label: "Vektoren und Geometrie" },
8155
+ { id: "stochastik", label: "Stochastik" }
8156
+ ],
8157
+ "realschule|9|informatik": [
8158
+ { id: "daten", label: "Daten und Information" },
8159
+ { id: "algorithmen", label: "Algorithmen und Programmierung" },
8160
+ { id: "netze", label: "Netze und Sicherheit" }
8161
+ ],
8162
+ "realschule|9|physik": [
8163
+ { id: "mechanik", label: "Mechanik" },
8164
+ { id: "energie", label: "Energie" },
8165
+ { id: "elektrizitaet", label: "Elektrizit\xE4t" }
8166
+ ],
8167
+ "realschule|9|chemie": [
8168
+ { id: "stoffe", label: "Stoffe und Reaktionen" },
8169
+ { id: "atome", label: "Atome und Periodensystem" },
8170
+ { id: "organisch", label: "Organische Verbindungen" }
8171
+ ],
8172
+ "realschule|9|biologie": [
8173
+ { id: "zelle", label: "Zelle und Stoffwechsel" },
8174
+ { id: "vererbung", label: "Vererbung" },
8175
+ { id: "oekologie", label: "\xD6kologie" }
8176
+ ],
8177
+ "gymnasium|9|physik": [
8178
+ { id: "optik", label: "Optik" },
8179
+ { id: "mechanik-dynamik", label: "Mechanik und Dynamik" },
8180
+ { id: "felder", label: "Elektrische und magnetische Felder" }
8181
+ ],
8182
+ "gymnasium|9|chemie": [
8183
+ { id: "bindung", label: "Chemische Bindung" },
8184
+ { id: "reaktionsgeschwindigkeit", label: "Reaktionsgeschwindigkeit" },
8185
+ { id: "saeure-base", label: "S\xE4ure-Base-Reaktionen" }
8186
+ ],
8187
+ "gymnasium|9|biologie": [
8188
+ { id: "genetik", label: "Genetik" },
8189
+ { id: "evolution", label: "Evolution" },
8190
+ { id: "neurobiologie", label: "Neurobiologie" }
8191
+ ]
8192
+ },
8193
+ contentUrls: {
8194
+ "realschule|10|mathematik": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8195
+ "gymnasium|10|mathematik": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8196
+ "realschule|9|informatik": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8197
+ "realschule|9|physik": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8198
+ "realschule|9|chemie": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8199
+ "realschule|9|biologie": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8200
+ "gymnasium|9|physik": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8201
+ "gymnasium|9|chemie": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8202
+ "gymnasium|9|biologie": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula"
8203
+ }
8204
+ };
8205
+
8206
+ // src/cli/curriculum/providers/kerncurriculum-hessen/index.ts
8207
+ var kerncurriculumHessenProvider = {
8208
+ id: "kerncurriculum-hessen",
8209
+ country: "DE",
8210
+ countryLabel: "Deutschland",
8211
+ region: "HE",
8212
+ regionLabel: "Hessen",
8213
+ label: "Kerncurriculum (Hessen)",
8214
+ listSchoolTypes() {
8215
+ return KERNCURRICULUM_HESSEN_MANIFEST.schoolTypes;
8216
+ },
8217
+ listGrades(schoolType) {
8218
+ return (KERNCURRICULUM_HESSEN_MANIFEST.grades[schoolType] || []).map((id) => ({
8219
+ id,
8220
+ label: `Klasse ${id}`
8221
+ }));
8222
+ },
8223
+ listSubjects(schoolType, _grade) {
8224
+ return KERNCURRICULUM_HESSEN_MANIFEST.subjects[schoolType] || [];
8225
+ },
8226
+ listTracks(schoolType, grade, subject) {
8227
+ const key = `${schoolType}|${grade}|${subject}`;
8228
+ return KERNCURRICULUM_HESSEN_MANIFEST.tracks[key] || [];
8229
+ },
8230
+ listTopics(selection) {
8231
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
8232
+ const list = KERNCURRICULUM_HESSEN_MANIFEST.topics[key] || [];
8233
+ return list.map((t2) => ({
8234
+ ...t2,
8235
+ sourceRef: key
8236
+ }));
8237
+ },
8238
+ resolveTopic(topic) {
8239
+ const uri = KERNCURRICULUM_HESSEN_MANIFEST.contentUrls[topic.sourceRef];
8240
+ if (!uri) {
8241
+ throw new Error(
8242
+ `Kerncurriculum Hessen: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
8243
+ );
8244
+ }
8245
+ return {
8246
+ provider: "kerncurriculum-hessen",
8247
+ topicId: `${topic.sourceRef}#${topic.id}`,
8248
+ uri
8249
+ };
8250
+ },
8251
+ extractTopics(html, topicIds) {
8252
+ const results = {};
8253
+ const headingMatches = Array.from(
8254
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
8255
+ );
8256
+ const sections = [];
8257
+ for (const m of headingMatches) {
8258
+ const headerText = cleanHtmlText5(m[1]).trim();
8259
+ const start = m.index + m[0].length;
8260
+ const nextHeading = html.indexOf("<h", start);
8261
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
8262
+ const content = cleanHtmlText5(rawContent).slice(0, 1500);
8263
+ if (headerText) sections.push({ header: headerText, content });
8264
+ }
8265
+ for (const topicId of topicIds) {
8266
+ const hashIdx = topicId.indexOf("#");
8267
+ if (hashIdx === -1) continue;
8268
+ const key = topicId.substring(0, hashIdx);
8269
+ const shortId = topicId.substring(hashIdx + 1);
8270
+ const list = KERNCURRICULUM_HESSEN_MANIFEST.topics[key] ?? [];
8271
+ const match = list.find((t2) => t2.id === shortId);
8272
+ if (!match) continue;
8273
+ const label = match.label;
8274
+ const normalizedLabel = normalizeForComparison5(label);
8275
+ let section = sections.find(
8276
+ (s) => normalizeForComparison5(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
8277
+ );
8278
+ if (!section && sections.length > 0) {
8279
+ section = sections.find((s) => s.content.length > 80) || sections[0];
8280
+ }
8281
+ if (section) {
8282
+ results[topicId] = `${label}
8283
+
8284
+ ${section.content}`.trim();
8285
+ } else {
8286
+ results[topicId] = label;
8287
+ }
8288
+ }
8289
+ return results;
8290
+ }
8291
+ };
8292
+ function cleanHtmlText5(html) {
8293
+ let text = html.replace(
8294
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
8295
+ " "
8296
+ );
8297
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
8298
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
8299
+ text = text.replace(/<p[^>]*>/gi, "\n");
8300
+ text = text.replace(/<br\s*\/?>/gi, "\n");
8301
+ text = text.replace(/<[^>]+>/g, " ");
8302
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
8303
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8304
+ }
8305
+ function normalizeForComparison5(str) {
8306
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8307
+ }
8308
+
8309
+ // src/cli/curriculum/providers/kerncurriculum-niedersachsen/manifest.ts
8310
+ var KERNCURRICULUM_NIEDERSACHSEN_MANIFEST = {
8311
+ schoolYear: "2025/2026",
8312
+ capturedOn: "2026-07-02",
8313
+ sourceRevision: "Kerncurricula Niedersachsen (CuVo)",
8314
+ schoolTypes: [
8315
+ { id: "realschule", label: "Realschule" },
8316
+ { id: "gymnasium", label: "Gymnasium" }
8317
+ ],
8318
+ grades: {
8319
+ realschule: ["7", "8", "9", "10"],
8320
+ gymnasium: ["7", "8", "9", "10"]
8321
+ },
8322
+ subjects: {
8323
+ realschule: [
8324
+ { id: "mathematik", label: "Mathematik" },
8325
+ { id: "informatik", label: "Informatik" },
8326
+ { id: "physik", label: "Physik" },
8327
+ { id: "chemie", label: "Chemie" },
8328
+ { id: "biologie", label: "Biologie" }
8329
+ ],
8330
+ gymnasium: [
8331
+ { id: "mathematik", label: "Mathematik" },
8332
+ { id: "informatik", label: "Informatik" },
8333
+ { id: "physik", label: "Physik" },
8334
+ { id: "chemie", label: "Chemie" },
8335
+ { id: "biologie", label: "Biologie" }
8336
+ ]
8337
+ },
8338
+ tracks: {},
8339
+ topics: {
8340
+ "realschule|10|mathematik": [
8341
+ { id: "algebra-funktionen", label: "Algebra und Funktionen" },
8342
+ { id: "geometrie", label: "Geometrie" }
8343
+ ],
8344
+ "gymnasium|10|mathematik": [
8345
+ { id: "analysis", label: "Analysis" },
8346
+ { id: "lineare-algebra", label: "Lineare Algebra" }
8347
+ ],
8348
+ "realschule|9|physik": [
8349
+ { id: "mechanik", label: "Mechanik" },
8350
+ { id: "energie", label: "Energie und W\xE4rme" }
8351
+ ],
8352
+ "realschule|9|chemie": [
8353
+ { id: "reaktionen", label: "Chemische Reaktionen" },
8354
+ { id: "stoffkreislauf", label: "Stoffkreisl\xE4ufe" }
8355
+ ],
8356
+ "realschule|9|biologie": [
8357
+ { id: "zellen", label: "Zellen" },
8358
+ { id: "oekologie", label: "\xD6kologie" }
8359
+ ],
8360
+ "gymnasium|9|physik": [
8361
+ { id: "elektrizitaet", label: "Elektrizit\xE4t und Magnetismus" }
8362
+ ],
8363
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Chemische Bindungen" }],
8364
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
8365
+ },
8366
+ contentUrls: {
8367
+ "realschule|10|mathematik": "https://cuvo.nibis.de/cuvo.php",
8368
+ "gymnasium|10|mathematik": "https://cuvo.nibis.de/cuvo.php",
8369
+ "realschule|9|physik": "https://cuvo.nibis.de/cuvo.php",
8370
+ "realschule|9|chemie": "https://cuvo.nibis.de/cuvo.php",
8371
+ "realschule|9|biologie": "https://cuvo.nibis.de/cuvo.php",
8372
+ "gymnasium|9|physik": "https://cuvo.nibis.de/cuvo.php",
8373
+ "gymnasium|9|chemie": "https://cuvo.nibis.de/cuvo.php",
8374
+ "gymnasium|9|biologie": "https://cuvo.nibis.de/cuvo.php"
8375
+ }
8376
+ };
8377
+
8378
+ // src/cli/curriculum/providers/kerncurriculum-niedersachsen/index.ts
8379
+ var kerncurriculumNiedersachsenProvider = {
8380
+ id: "kerncurriculum-niedersachsen",
8381
+ country: "DE",
8382
+ countryLabel: "Deutschland",
8383
+ region: "NI",
8384
+ regionLabel: "Niedersachsen",
8385
+ label: "Kerncurriculum (Niedersachsen)",
8386
+ listSchoolTypes() {
8387
+ return KERNCURRICULUM_NIEDERSACHSEN_MANIFEST.schoolTypes;
8388
+ },
8389
+ listGrades(schoolType) {
8390
+ return (KERNCURRICULUM_NIEDERSACHSEN_MANIFEST.grades[schoolType] || []).map((id) => ({
8391
+ id,
8392
+ label: `Klasse ${id}`
8393
+ }));
8394
+ },
8395
+ listSubjects(schoolType, _grade) {
8396
+ return KERNCURRICULUM_NIEDERSACHSEN_MANIFEST.subjects[schoolType] || [];
8397
+ },
8398
+ listTracks(schoolType, grade, subject) {
8399
+ const key = `${schoolType}|${grade}|${subject}`;
8400
+ return KERNCURRICULUM_NIEDERSACHSEN_MANIFEST.tracks[key] || [];
8401
+ },
8402
+ listTopics(selection) {
8403
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
8404
+ const list = KERNCURRICULUM_NIEDERSACHSEN_MANIFEST.topics[key] || [];
8405
+ return list.map((t2) => ({
8406
+ ...t2,
8407
+ sourceRef: key
8408
+ }));
8409
+ },
8410
+ resolveTopic(topic) {
8411
+ const uri = KERNCURRICULUM_NIEDERSACHSEN_MANIFEST.contentUrls[topic.sourceRef];
8412
+ if (!uri) {
8413
+ throw new Error(
8414
+ `Kerncurriculum Niedersachsen: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
8415
+ );
8416
+ }
8417
+ return {
8418
+ provider: "kerncurriculum-niedersachsen",
8419
+ topicId: `${topic.sourceRef}#${topic.id}`,
8420
+ uri
8421
+ };
8422
+ },
8423
+ extractTopics(html, topicIds) {
8424
+ const results = {};
8425
+ const headingMatches = Array.from(
8426
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
8427
+ );
8428
+ const sections = [];
8429
+ for (const m of headingMatches) {
8430
+ const headerText = cleanHtmlText6(m[1]).trim();
8431
+ const start = m.index + m[0].length;
8432
+ const nextHeading = html.indexOf("<h", start);
8433
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
8434
+ const content = cleanHtmlText6(rawContent).slice(0, 1500);
8435
+ if (headerText) sections.push({ header: headerText, content });
8436
+ }
8437
+ for (const topicId of topicIds) {
8438
+ const hashIdx = topicId.indexOf("#");
8439
+ if (hashIdx === -1) continue;
8440
+ const key = topicId.substring(0, hashIdx);
8441
+ const shortId = topicId.substring(hashIdx + 1);
8442
+ const list = KERNCURRICULUM_NIEDERSACHSEN_MANIFEST.topics[key] ?? [];
8443
+ const match = list.find((t2) => t2.id === shortId);
8444
+ if (!match) continue;
8445
+ const label = match.label;
8446
+ const normalizedLabel = normalizeForComparison6(label);
8447
+ let section = sections.find(
8448
+ (s) => normalizeForComparison6(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
8449
+ );
8450
+ if (!section && sections.length > 0) {
8451
+ section = sections.find((s) => s.content.length > 80) || sections[0];
8452
+ }
8453
+ if (section) {
8454
+ results[topicId] = `${label}
8455
+
8456
+ ${section.content}`.trim();
8457
+ } else {
8458
+ results[topicId] = label;
8459
+ }
8460
+ }
8461
+ return results;
8462
+ }
8463
+ };
8464
+ function cleanHtmlText6(html) {
8465
+ let text = html.replace(
8466
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
8467
+ " "
8468
+ );
8469
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
8470
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
8471
+ text = text.replace(/<p[^>]*>/gi, "\n");
8472
+ text = text.replace(/<br\s*\/?>/gi, "\n");
8473
+ text = text.replace(/<[^>]+>/g, " ");
8474
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
8475
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8476
+ }
8477
+ function normalizeForComparison6(str) {
8478
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8479
+ }
8480
+
8481
+ // src/cli/curriculum/providers/kernlehrplan-nrw/manifest.ts
8482
+ var KERNLEHRPLAN_NRW_MANIFEST = {
8483
+ schoolYear: "2025/2026",
8484
+ capturedOn: "2026-07-02",
8485
+ sourceRevision: "Kernlehrpl\xE4ne Sekundarstufe I NRW (Lehrplannavigator)",
8486
+ schoolTypes: [
8487
+ { id: "realschule", label: "Realschule" },
8488
+ { id: "gymnasium", label: "Gymnasium" }
8489
+ ],
8490
+ grades: {
8491
+ realschule: ["7", "8", "9", "10"],
8492
+ gymnasium: ["7", "8", "9", "10"]
8493
+ },
8494
+ subjects: {
8495
+ realschule: [
8496
+ { id: "mathematik", label: "Mathematik" },
8497
+ { id: "informatik", label: "Informatik" },
8498
+ { id: "physik", label: "Physik" },
8499
+ { id: "chemie", label: "Chemie" },
8500
+ { id: "biologie", label: "Biologie" }
8501
+ ],
8502
+ gymnasium: [
8503
+ { id: "mathematik", label: "Mathematik" },
8504
+ { id: "informatik", label: "Informatik" },
8505
+ { id: "physik", label: "Physik" },
8506
+ { id: "chemie", label: "Chemie" },
8507
+ { id: "biologie", label: "Biologie" }
8508
+ ]
8509
+ },
8510
+ tracks: {},
8511
+ topics: {
8512
+ // Minimal starter set for Realschule Mathematik (example for one grade)
8513
+ "realschule|10|mathematik": [
8514
+ { id: "arithmetik-algebra", label: "Arithmetik und Algebra" },
8515
+ { id: "funktionen", label: "Funktionen" },
8516
+ { id: "geometrie", label: "Geometrie" },
8517
+ { id: "stochastik", label: "Stochastik" }
8518
+ ],
8519
+ // Minimal starter for Realschule Informatik (from UV examples in 5/6 + typical)
8520
+ "realschule|8|informatik": [
8521
+ { id: "daten-codierung", label: "Daten und Codierung" },
8522
+ { id: "algorithmen", label: "Algorithmen" },
8523
+ {
8524
+ id: "ki-datenbewusstsein",
8525
+ label: "K\xFCnstliche Intelligenz und Datenbewusstsein"
8526
+ }
8527
+ ],
8528
+ // Starter for Gymnasium Mathematik (higher track)
8529
+ "gymnasium|10|mathematik": [
8530
+ { id: "analysis", label: "Analysis / Funktionen" },
8531
+ {
8532
+ id: "lineare-algebra-geometrie",
8533
+ label: "Lineare Algebra und Geometrie"
8534
+ },
8535
+ { id: "stochastik", label: "Stochastik" }
8536
+ ],
8537
+ // Starter for Gymnasium WP Informatik
8538
+ "gymnasium|10|informatik": [
8539
+ { id: "logische-schaltungen", label: "Logische Schaltungen" },
8540
+ {
8541
+ id: "algorithmen-programmierung",
8542
+ label: "Algorithmen und Programmierung"
8543
+ },
8544
+ { id: "daten-netze-sicherheit", label: "Daten, Netze und Sicherheit" }
8545
+ ],
8546
+ // Naturwissenschaften for Realschule example
8547
+ "realschule|9|physik": [
8548
+ { id: "mechanik-kraefte", label: "Mechanik und Kr\xE4fte" },
8549
+ { id: "waerme-energie", label: "W\xE4rme und Energie" },
8550
+ { id: "elektrizitaet", label: "Elektrizit\xE4t" }
8551
+ ],
8552
+ "realschule|9|chemie": [
8553
+ { id: "stoffe-eigenschaften", label: "Stoffe und ihre Eigenschaften" },
8554
+ { id: "reaktionen", label: "Chemische Reaktionen" },
8555
+ { id: "atombau", label: "Atombau und Periodensystem" }
8556
+ ],
8557
+ "realschule|9|biologie": [
8558
+ { id: "zellen-lebewesen", label: "Zellen und Lebewesen" },
8559
+ { id: "vererbung", label: "Vererbung und Genetik" },
8560
+ { id: "oekosysteme", label: "\xD6kosysteme" }
8561
+ ],
8562
+ // Naturwissenschaften for Gymnasium
8563
+ "gymnasium|9|physik": [
8564
+ { id: "optik", label: "Optik" },
8565
+ { id: "elektromagnetismus", label: "Elektromagnetismus" },
8566
+ { id: "atomphysik", label: "Atom- und Kernphysik" }
8567
+ ],
8568
+ "gymnasium|9|chemie": [
8569
+ { id: "chemische-bindung", label: "Chemische Bindung" },
8570
+ { id: "saeuren-basen-salze", label: "S\xE4uren, Basen, Salze" },
8571
+ { id: "organische", label: "Organische Chemie" }
8572
+ ],
8573
+ "gymnasium|9|biologie": [
8574
+ { id: "genetik-evolution", label: "Genetik und Evolution" },
8575
+ { id: "physiologie", label: "Physiologie des Menschen" },
8576
+ { id: "oekologie-umwelt", label: "\xD6kologie und Umweltschutz" }
8577
+ ]
8578
+ },
8579
+ contentUrls: {
8580
+ "realschule|10|mathematik": "https://lehrplannavigator.nrw.de/sekundarstufe-i/realschule/mathematik-neu-ab-20222023",
8581
+ "realschule|8|informatik": "https://lehrplannavigator.nrw.de/sekundarstufe-i/realschule/informatik-realschule-neu-ab-20212022",
8582
+ "gymnasium|10|mathematik": "https://lehrplannavigator.nrw.de/sekundarstufe-i/kernlehrplaene-fuer-das-gymnasium-ab-20192020/mathematik",
8583
+ "gymnasium|10|informatik": "https://lehrplannavigator.nrw.de/sekundarstufe-i/kernlehrplaene-fuer-das-gymnasium-ab-20192020/wp-informatik-neu-ab-20232024",
8584
+ "realschule|9|physik": "https://lehrplannavigator.nrw.de/sekundarstufe-i/realschule/physik",
8585
+ "realschule|9|chemie": "https://lehrplannavigator.nrw.de/sekundarstufe-i/realschule/chemie",
8586
+ "realschule|9|biologie": "https://lehrplannavigator.nrw.de/sekundarstufe-i/realschule/biologie",
8587
+ "gymnasium|9|physik": "https://lehrplannavigator.nrw.de/sekundarstufe-i/kernlehrplaene-fuer-das-gymnasium-ab-20192020/physik",
8588
+ "gymnasium|9|chemie": "https://lehrplannavigator.nrw.de/sekundarstufe-i/kernlehrplaene-fuer-das-gymnasium-ab-20192020/chemie",
8589
+ "gymnasium|9|biologie": "https://lehrplannavigator.nrw.de/sekundarstufe-i/kernlehrplaene-fuer-das-gymnasium-ab-20192020/biologie"
8590
+ }
8591
+ };
8592
+
8593
+ // src/cli/curriculum/providers/kernlehrplan-nrw/index.ts
8594
+ var kernlehrplanNrwProvider = {
8595
+ id: "kernlehrplan-nrw",
8596
+ country: "DE",
8597
+ countryLabel: "Deutschland",
8598
+ region: "NW",
8599
+ regionLabel: "Nordrhein-Westfalen",
8600
+ label: "Kernlehrplan (Nordrhein-Westfalen)",
8601
+ listSchoolTypes() {
8602
+ return KERNLEHRPLAN_NRW_MANIFEST.schoolTypes;
8603
+ },
8604
+ listGrades(schoolType) {
8605
+ return (KERNLEHRPLAN_NRW_MANIFEST.grades[schoolType] || []).map((id) => ({
8606
+ id,
8607
+ label: `Klasse ${id}`
8608
+ }));
8609
+ },
8610
+ listSubjects(schoolType, _grade) {
8611
+ return KERNLEHRPLAN_NRW_MANIFEST.subjects[schoolType] || [];
8612
+ },
8613
+ listTracks(schoolType, grade, subject) {
8614
+ const key = `${schoolType}|${grade}|${subject}`;
8615
+ return KERNLEHRPLAN_NRW_MANIFEST.tracks[key] || [];
8616
+ },
8617
+ listTopics(selection) {
8618
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
8619
+ const list = KERNLEHRPLAN_NRW_MANIFEST.topics[key] || [];
8620
+ return list.map((t2) => ({
8621
+ ...t2,
8622
+ sourceRef: key
8623
+ }));
8624
+ },
8625
+ resolveTopic(topic) {
8626
+ const uri = KERNLEHRPLAN_NRW_MANIFEST.contentUrls[topic.sourceRef];
8627
+ if (!uri) {
8628
+ throw new Error(
8629
+ `Kernlehrplan NRW: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
8630
+ );
8631
+ }
8632
+ return {
8633
+ provider: "kernlehrplan-nrw",
8634
+ topicId: `${topic.sourceRef}#${topic.id}`,
8635
+ uri
8636
+ };
8637
+ },
8638
+ extractTopics(html, topicIds) {
8639
+ const results = {};
8640
+ const headingMatches = Array.from(
8641
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
8642
+ );
8643
+ const sections = [];
8644
+ for (const m of headingMatches) {
8645
+ const headerText = cleanHtmlText7(m[1]).trim();
8646
+ const start = m.index + m[0].length;
8647
+ const nextHeading = html.indexOf("<h", start);
8648
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 2e3);
8649
+ const content = cleanHtmlText7(rawContent).slice(0, 2e3);
8650
+ if (headerText) sections.push({ header: headerText, content });
8651
+ }
8652
+ for (const topicId of topicIds) {
8653
+ const hashIdx = topicId.indexOf("#");
8654
+ if (hashIdx === -1) continue;
8655
+ const key = topicId.substring(0, hashIdx);
8656
+ const shortId = topicId.substring(hashIdx + 1);
8657
+ const list = KERNLEHRPLAN_NRW_MANIFEST.topics[key] ?? [];
8658
+ const match = list.find((t2) => t2.id === shortId);
8659
+ if (!match) continue;
8660
+ const label = match.label;
8661
+ const normalizedLabel = normalizeForComparison7(label);
8662
+ let section = sections.find(
8663
+ (s) => normalizeForComparison7(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
8664
+ );
8665
+ if (!section && sections.length > 0) {
8666
+ section = sections.find((s) => s.content.length > 100) || sections[0];
8667
+ }
8668
+ if (section) {
8669
+ results[topicId] = `${label}
8670
+
8671
+ ${section.content}`.trim();
8672
+ } else {
8673
+ results[topicId] = label;
8674
+ }
8675
+ }
8676
+ return results;
8677
+ }
8678
+ };
8679
+ function cleanHtmlText7(html) {
8680
+ let text = html.replace(
8681
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
8682
+ " "
8683
+ );
8684
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
8685
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
8686
+ text = text.replace(/<p[^>]*>/gi, "\n");
8687
+ text = text.replace(/<br\s*\/?>/gi, "\n");
8688
+ text = text.replace(/<[^>]+>/g, " ");
8689
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&ndash;/g, "\u2013").replace(/&mdash;/g, "\u2014");
8690
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8691
+ }
8692
+ function normalizeForComparison7(str) {
8693
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8694
+ }
8695
+
8696
+ // src/cli/curriculum/providers/lehrplaene-rp/manifest.ts
8697
+ var LEHRPLAENE_RP_MANIFEST = {
8698
+ schoolYear: "2025/2026",
8699
+ capturedOn: "2026-07-02",
8700
+ sourceRevision: "Lehrpl\xE4ne Rheinland-Pfalz",
8701
+ schoolTypes: [
8702
+ { id: "realschule-plus", label: "Realschule plus" },
8703
+ { id: "gymnasium", label: "Gymnasium" }
8704
+ ],
8705
+ grades: {
8706
+ "realschule-plus": ["7", "8", "9", "10"],
8707
+ gymnasium: ["7", "8", "9", "10"]
8708
+ },
8709
+ subjects: {
8710
+ "realschule-plus": [
8711
+ { id: "mathematik", label: "Mathematik" },
8712
+ { id: "informatik", label: "Informatik" },
8713
+ { id: "physik", label: "Physik" },
8714
+ { id: "chemie", label: "Chemie" },
8715
+ { id: "biologie", label: "Biologie" }
8716
+ ],
8717
+ gymnasium: [
8718
+ { id: "mathematik", label: "Mathematik" },
8719
+ { id: "informatik", label: "Informatik" },
8720
+ { id: "physik", label: "Physik" },
8721
+ { id: "chemie", label: "Chemie" },
8722
+ { id: "biologie", label: "Biologie" }
8723
+ ]
8724
+ },
8725
+ tracks: {},
8726
+ topics: {
8727
+ "realschule-plus|10|mathematik": [
8728
+ { id: "funktionen", label: "Funktionen" }
8729
+ ],
8730
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
8731
+ "realschule-plus|9|physik": [{ id: "mechanik", label: "Mechanik" }],
8732
+ "realschule-plus|9|chemie": [{ id: "reaktionen", label: "Reaktionen" }],
8733
+ "realschule-plus|9|biologie": [{ id: "zellen", label: "Zellen" }],
8734
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
8735
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Bindungen" }],
8736
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
8737
+ },
8738
+ contentUrls: {
8739
+ "realschule-plus|10|mathematik": "https://lehrplaene.bildung-rp.de/",
8740
+ "gymnasium|10|mathematik": "https://lehrplaene.bildung-rp.de/",
8741
+ "realschule-plus|9|physik": "https://lehrplaene.bildung-rp.de/",
8742
+ "realschule-plus|9|chemie": "https://lehrplaene.bildung-rp.de/",
8743
+ "realschule-plus|9|biologie": "https://lehrplaene.bildung-rp.de/",
8744
+ "gymnasium|9|physik": "https://lehrplaene.bildung-rp.de/",
8745
+ "gymnasium|9|chemie": "https://lehrplaene.bildung-rp.de/",
8746
+ "gymnasium|9|biologie": "https://lehrplaene.bildung-rp.de/"
8747
+ }
8748
+ };
8749
+
8750
+ // src/cli/curriculum/providers/lehrplaene-rp/index.ts
8751
+ var lehrplaeneRpProvider = {
8752
+ id: "lehrplaene-rp",
8753
+ country: "DE",
8754
+ countryLabel: "Deutschland",
8755
+ region: "RP",
8756
+ regionLabel: "Rheinland-Pfalz",
8757
+ label: "Lehrpl\xE4ne (Rheinland-Pfalz)",
8758
+ listSchoolTypes() {
8759
+ return LEHRPLAENE_RP_MANIFEST.schoolTypes;
8760
+ },
8761
+ listGrades(schoolType) {
8762
+ return (LEHRPLAENE_RP_MANIFEST.grades[schoolType] || []).map((id) => ({
8763
+ id,
8764
+ label: `Klasse ${id}`
8765
+ }));
8766
+ },
8767
+ listSubjects(schoolType, _grade) {
8768
+ return LEHRPLAENE_RP_MANIFEST.subjects[schoolType] || [];
8769
+ },
8770
+ listTracks(schoolType, grade, subject) {
8771
+ const key = `${schoolType}|${grade}|${subject}`;
8772
+ return LEHRPLAENE_RP_MANIFEST.tracks[key] || [];
8773
+ },
8774
+ listTopics(selection) {
8775
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
8776
+ const list = LEHRPLAENE_RP_MANIFEST.topics[key] || [];
8777
+ return list.map((t2) => ({
8778
+ ...t2,
8779
+ sourceRef: key
8780
+ }));
8781
+ },
8782
+ resolveTopic(topic) {
8783
+ const uri = LEHRPLAENE_RP_MANIFEST.contentUrls[topic.sourceRef];
8784
+ if (!uri) {
8785
+ throw new Error(
8786
+ `Lehrpl\xE4ne RP: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
8787
+ );
8788
+ }
8789
+ return {
8790
+ provider: "lehrplaene-rp",
8791
+ topicId: `${topic.sourceRef}#${topic.id}`,
8792
+ uri
8793
+ };
8794
+ },
8795
+ extractTopics(html, topicIds) {
8796
+ const results = {};
8797
+ const headingMatches = Array.from(
8798
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
8799
+ );
8800
+ const sections = [];
8801
+ for (const m of headingMatches) {
8802
+ const headerText = cleanHtmlText8(m[1]).trim();
8803
+ const start = m.index + m[0].length;
8804
+ const nextHeading = html.indexOf("<h", start);
8805
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
8806
+ const content = cleanHtmlText8(rawContent).slice(0, 1500);
8807
+ if (headerText) sections.push({ header: headerText, content });
8808
+ }
8809
+ for (const topicId of topicIds) {
8810
+ const hashIdx = topicId.indexOf("#");
8811
+ if (hashIdx === -1) continue;
8812
+ const key = topicId.substring(0, hashIdx);
8813
+ const shortId = topicId.substring(hashIdx + 1);
8814
+ const list = LEHRPLAENE_RP_MANIFEST.topics[key] ?? [];
8815
+ const match = list.find((t2) => t2.id === shortId);
8816
+ if (!match) continue;
8817
+ const label = match.label;
8818
+ const normalizedLabel = normalizeForComparison8(label);
8819
+ let section = sections.find(
8820
+ (s) => normalizeForComparison8(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
8821
+ );
8822
+ if (!section && sections.length > 0) {
8823
+ section = sections.find((s) => s.content.length > 80) || sections[0];
8824
+ }
8825
+ if (section) {
8826
+ results[topicId] = `${label}
8827
+
8828
+ ${section.content}`.trim();
8829
+ } else {
8830
+ results[topicId] = label;
8831
+ }
8832
+ }
8833
+ return results;
8834
+ }
8835
+ };
8836
+ function cleanHtmlText8(html) {
8837
+ let text = html.replace(
8838
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
8839
+ " "
8840
+ );
8841
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
8842
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
8843
+ text = text.replace(/<p[^>]*>/gi, "\n");
8844
+ text = text.replace(/<br\s*\/?>/gi, "\n");
8845
+ text = text.replace(/<[^>]+>/g, " ");
8846
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
8847
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8848
+ }
8849
+ function normalizeForComparison8(str) {
8850
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8851
+ }
8852
+
8853
+ // src/cli/curriculum/providers/lehrplan-saarland/manifest.ts
8854
+ var LEHRPLAN_SAARLAND_MANIFEST = {
8855
+ schoolYear: "2025/2026",
8856
+ capturedOn: "2026-07-02",
8857
+ sourceRevision: "Lehrpl\xE4ne Saarland",
8858
+ schoolTypes: [
8859
+ { id: "gemeinschaftsschule", label: "Gemeinschaftsschule" },
8860
+ { id: "gymnasium", label: "Gymnasium" }
8861
+ ],
8862
+ grades: {
8863
+ gemeinschaftsschule: ["7", "8", "9", "10"],
8864
+ gymnasium: ["7", "8", "9", "10"]
8865
+ },
8866
+ subjects: {
8867
+ gemeinschaftsschule: [
8868
+ { id: "mathematik", label: "Mathematik" },
8869
+ { id: "informatik", label: "Informatik" },
8870
+ { id: "physik", label: "Physik" },
8871
+ { id: "chemie", label: "Chemie" },
8872
+ { id: "biologie", label: "Biologie" }
8873
+ ],
8874
+ gymnasium: [
8875
+ { id: "mathematik", label: "Mathematik" },
8876
+ { id: "informatik", label: "Informatik" },
8877
+ { id: "physik", label: "Physik" },
8878
+ { id: "chemie", label: "Chemie" },
8879
+ { id: "biologie", label: "Biologie" }
8880
+ ]
8881
+ },
8882
+ tracks: {},
8883
+ topics: {
8884
+ "gemeinschaftsschule|10|mathematik": [
8885
+ { id: "funktionen", label: "Funktionen" }
8886
+ ],
8887
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
8888
+ "gemeinschaftsschule|9|physik": [{ id: "mechanik", label: "Mechanik" }],
8889
+ "gemeinschaftsschule|9|chemie": [{ id: "reaktionen", label: "Reaktionen" }],
8890
+ "gemeinschaftsschule|9|biologie": [{ id: "zellen", label: "Zellen" }],
8891
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
8892
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Bindungen" }],
8893
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
8894
+ },
8895
+ contentUrls: {
8896
+ "gemeinschaftsschule|10|mathematik": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen",
8897
+ "gymnasium|10|mathematik": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen",
8898
+ "gemeinschaftsschule|9|physik": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen",
8899
+ "gemeinschaftsschule|9|chemie": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen",
8900
+ "gemeinschaftsschule|9|biologie": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen",
8901
+ "gymnasium|9|physik": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen",
8902
+ "gymnasium|9|chemie": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen",
8903
+ "gymnasium|9|biologie": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen"
8904
+ }
8905
+ };
8906
+
8907
+ // src/cli/curriculum/providers/lehrplan-saarland/index.ts
8908
+ var lehrplanSaarlandProvider = {
8909
+ id: "lehrplan-saarland",
8910
+ country: "DE",
8911
+ countryLabel: "Deutschland",
8912
+ region: "SL",
8913
+ regionLabel: "Saarland",
8914
+ label: "Lehrplan (Saarland)",
8915
+ listSchoolTypes() {
8916
+ return LEHRPLAN_SAARLAND_MANIFEST.schoolTypes;
8917
+ },
8918
+ listGrades(schoolType) {
8919
+ return (LEHRPLAN_SAARLAND_MANIFEST.grades[schoolType] || []).map((id) => ({
8920
+ id,
8921
+ label: `Klasse ${id}`
8922
+ }));
8923
+ },
8924
+ listSubjects(schoolType, _grade) {
8925
+ return LEHRPLAN_SAARLAND_MANIFEST.subjects[schoolType] || [];
8926
+ },
8927
+ listTracks(schoolType, grade, subject) {
8928
+ const key = `${schoolType}|${grade}|${subject}`;
8929
+ return LEHRPLAN_SAARLAND_MANIFEST.tracks[key] || [];
8930
+ },
8931
+ listTopics(selection) {
8932
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
8933
+ const list = LEHRPLAN_SAARLAND_MANIFEST.topics[key] || [];
8934
+ return list.map((t2) => ({
8935
+ ...t2,
8936
+ sourceRef: key
8937
+ }));
8938
+ },
8939
+ resolveTopic(topic) {
8940
+ const uri = LEHRPLAN_SAARLAND_MANIFEST.contentUrls[topic.sourceRef];
8941
+ if (!uri) {
8942
+ throw new Error(
8943
+ `Lehrplan Saarland: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
8944
+ );
8945
+ }
8946
+ return {
8947
+ provider: "lehrplan-saarland",
8948
+ topicId: `${topic.sourceRef}#${topic.id}`,
8949
+ uri
8950
+ };
8951
+ },
8952
+ extractTopics(html, topicIds) {
8953
+ const results = {};
8954
+ const headingMatches = Array.from(
8955
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
8956
+ );
8957
+ const sections = [];
8958
+ for (const m of headingMatches) {
8959
+ const headerText = cleanHtmlText9(m[1]).trim();
8960
+ const start = m.index + m[0].length;
8961
+ const nextHeading = html.indexOf("<h", start);
8962
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
8963
+ const content = cleanHtmlText9(rawContent).slice(0, 1500);
8964
+ if (headerText) sections.push({ header: headerText, content });
8965
+ }
8966
+ for (const topicId of topicIds) {
8967
+ const hashIdx = topicId.indexOf("#");
8968
+ if (hashIdx === -1) continue;
8969
+ const key = topicId.substring(0, hashIdx);
8970
+ const shortId = topicId.substring(hashIdx + 1);
8971
+ const list = LEHRPLAN_SAARLAND_MANIFEST.topics[key] ?? [];
8972
+ const match = list.find((t2) => t2.id === shortId);
8973
+ if (!match) continue;
8974
+ const label = match.label;
8975
+ const normalizedLabel = normalizeForComparison9(label);
8976
+ let section = sections.find(
8977
+ (s) => normalizeForComparison9(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
8978
+ );
8979
+ if (!section && sections.length > 0) {
8980
+ section = sections.find((s) => s.content.length > 80) || sections[0];
8981
+ }
8982
+ if (section) {
8983
+ results[topicId] = `${label}
8984
+
8985
+ ${section.content}`.trim();
8986
+ } else {
8987
+ results[topicId] = label;
8988
+ }
8989
+ }
8990
+ return results;
8991
+ }
8992
+ };
8993
+ function cleanHtmlText9(html) {
8994
+ let text = html.replace(
8995
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
8996
+ " "
8997
+ );
8998
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
8999
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
9000
+ text = text.replace(/<p[^>]*>/gi, "\n");
9001
+ text = text.replace(/<br\s*\/?>/gi, "\n");
9002
+ text = text.replace(/<[^>]+>/g, " ");
9003
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
9004
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9005
+ }
9006
+ function normalizeForComparison9(str) {
9007
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9008
+ }
9009
+
9010
+ // src/cli/curriculum/providers/lehrplan-sachsen/manifest.ts
9011
+ var LEHRPLAN_SACHSEN_MANIFEST = {
9012
+ schoolYear: "2025/2026",
9013
+ capturedOn: "2026-07-02",
9014
+ sourceRevision: "S\xE4chsische Lehrpl\xE4ne (Datenbank)",
9015
+ schoolTypes: [
9016
+ { id: "oberschule", label: "Oberschule (Realschule)" },
9017
+ { id: "gymnasium", label: "Gymnasium" }
9018
+ ],
9019
+ grades: {
9020
+ oberschule: ["7", "8", "9", "10"],
9021
+ gymnasium: ["7", "8", "9", "10"]
9022
+ },
9023
+ subjects: {
9024
+ oberschule: [
9025
+ { id: "mathematik", label: "Mathematik" },
9026
+ { id: "informatik", label: "Informatik" },
9027
+ { id: "physik", label: "Physik" },
9028
+ { id: "chemie", label: "Chemie" },
9029
+ { id: "biologie", label: "Biologie" }
9030
+ ],
9031
+ gymnasium: [
9032
+ { id: "mathematik", label: "Mathematik" },
9033
+ { id: "informatik", label: "Informatik" },
9034
+ { id: "physik", label: "Physik" },
9035
+ { id: "chemie", label: "Chemie" },
9036
+ { id: "biologie", label: "Biologie" }
9037
+ ]
9038
+ },
9039
+ tracks: {},
9040
+ topics: {
9041
+ "oberschule|10|mathematik": [
9042
+ { id: "prozentrechnung", label: "Prozentrechnung und Zinsrechnung" },
9043
+ { id: "funktionen", label: "Funktionale Zusammenh\xE4nge" },
9044
+ { id: "geometrie", label: "Geometrie und Messen" }
9045
+ ],
9046
+ "gymnasium|10|mathematik": [
9047
+ { id: "analysis", label: "Analysis und Funktionen" },
9048
+ { id: "geometrie", label: "Analytische Geometrie" }
9049
+ ],
9050
+ "oberschule|9|informatik": [
9051
+ { id: "algorithmen", label: "Algorithmen und Programmierung" },
9052
+ { id: "daten", label: "Daten und Datenschutz" }
9053
+ ],
9054
+ "oberschule|9|physik": [
9055
+ { id: "mechanik", label: "Mechanik und Bewegung" },
9056
+ { id: "energie", label: "Energie und W\xE4rme" }
9057
+ ],
9058
+ "oberschule|9|chemie": [
9059
+ { id: "stoffe", label: "Stoffe und chemische Reaktionen" },
9060
+ { id: "atome", label: "Atome und Periodensystem" }
9061
+ ],
9062
+ "oberschule|9|biologie": [
9063
+ { id: "zellen", label: "Zellen und Gewebe" },
9064
+ { id: "oekologie", label: "\xD6kologie und Umwelt" }
9065
+ ],
9066
+ "gymnasium|9|physik": [
9067
+ { id: "elektrizitaet", label: "Elektrizit\xE4t und Magnetismus" },
9068
+ { id: "optik", label: "Optik" }
9069
+ ],
9070
+ "gymnasium|9|chemie": [
9071
+ { id: "bindungen", label: "Chemische Bindungen" },
9072
+ { id: "reaktionen", label: "Chemische Reaktionen" }
9073
+ ],
9074
+ "gymnasium|9|biologie": [
9075
+ { id: "genetik", label: "Genetik und Vererbung" },
9076
+ { id: "evolution", label: "Evolution" }
9077
+ ]
9078
+ },
9079
+ contentUrls: {
9080
+ "oberschule|10|mathematik": "https://www.schulportal.sachsen.de/lplandb/",
9081
+ "gymnasium|10|mathematik": "https://www.schulportal.sachsen.de/lplandb/",
9082
+ "oberschule|9|informatik": "https://www.schulportal.sachsen.de/lplandb/",
9083
+ "oberschule|9|physik": "https://www.schulportal.sachsen.de/lplandb/",
9084
+ "oberschule|9|chemie": "https://www.schulportal.sachsen.de/lplandb/",
9085
+ "oberschule|9|biologie": "https://www.schulportal.sachsen.de/lplandb/",
9086
+ "gymnasium|9|physik": "https://www.schulportal.sachsen.de/lplandb/",
9087
+ "gymnasium|9|chemie": "https://www.schulportal.sachsen.de/lplandb/",
9088
+ "gymnasium|9|biologie": "https://www.schulportal.sachsen.de/lplandb/"
9089
+ }
9090
+ };
9091
+
9092
+ // src/cli/curriculum/providers/lehrplan-sachsen/index.ts
9093
+ var lehrplanSachsenProvider = {
9094
+ id: "lehrplan-sachsen",
9095
+ country: "DE",
9096
+ countryLabel: "Deutschland",
9097
+ region: "SN",
9098
+ regionLabel: "Sachsen",
9099
+ label: "Lehrplan (Sachsen)",
9100
+ listSchoolTypes() {
9101
+ return LEHRPLAN_SACHSEN_MANIFEST.schoolTypes;
9102
+ },
9103
+ listGrades(schoolType) {
9104
+ return (LEHRPLAN_SACHSEN_MANIFEST.grades[schoolType] || []).map((id) => ({
9105
+ id,
9106
+ label: `Klasse ${id}`
9107
+ }));
9108
+ },
9109
+ listSubjects(schoolType, _grade) {
9110
+ return LEHRPLAN_SACHSEN_MANIFEST.subjects[schoolType] || [];
9111
+ },
9112
+ listTracks(schoolType, grade, subject) {
9113
+ const key = `${schoolType}|${grade}|${subject}`;
9114
+ return LEHRPLAN_SACHSEN_MANIFEST.tracks[key] || [];
9115
+ },
9116
+ listTopics(selection) {
9117
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
9118
+ const list = LEHRPLAN_SACHSEN_MANIFEST.topics[key] || [];
9119
+ return list.map((t2) => ({
9120
+ ...t2,
9121
+ sourceRef: key
9122
+ }));
9123
+ },
9124
+ resolveTopic(topic) {
9125
+ const uri = LEHRPLAN_SACHSEN_MANIFEST.contentUrls[topic.sourceRef];
9126
+ if (!uri) {
9127
+ throw new Error(
9128
+ `Lehrplan Sachsen: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
9129
+ );
9130
+ }
9131
+ return {
9132
+ provider: "lehrplan-sachsen",
9133
+ topicId: `${topic.sourceRef}#${topic.id}`,
9134
+ uri
9135
+ };
9136
+ },
9137
+ extractTopics(html, topicIds) {
9138
+ const results = {};
9139
+ const headingMatches = Array.from(
9140
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
9141
+ );
9142
+ const sections = [];
9143
+ for (const m of headingMatches) {
9144
+ const headerText = cleanHtmlText10(m[1]).trim();
9145
+ const start = m.index + m[0].length;
9146
+ const nextHeading = html.indexOf("<h", start);
9147
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
9148
+ const content = cleanHtmlText10(rawContent).slice(0, 1500);
9149
+ if (headerText) sections.push({ header: headerText, content });
9150
+ }
9151
+ for (const topicId of topicIds) {
9152
+ const hashIdx = topicId.indexOf("#");
9153
+ if (hashIdx === -1) continue;
9154
+ const key = topicId.substring(0, hashIdx);
9155
+ const shortId = topicId.substring(hashIdx + 1);
9156
+ const list = LEHRPLAN_SACHSEN_MANIFEST.topics[key] ?? [];
9157
+ const match = list.find((t2) => t2.id === shortId);
9158
+ if (!match) continue;
9159
+ const label = match.label;
9160
+ const normalizedLabel = normalizeForComparison10(label);
9161
+ let section = sections.find(
9162
+ (s) => normalizeForComparison10(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
9163
+ );
9164
+ if (!section && sections.length > 0) {
9165
+ section = sections.find((s) => s.content.length > 80) || sections[0];
9166
+ }
9167
+ if (section) {
9168
+ results[topicId] = `${label}
9169
+
9170
+ ${section.content}`.trim();
9171
+ } else {
9172
+ results[topicId] = label;
9173
+ }
9174
+ }
9175
+ return results;
9176
+ }
9177
+ };
9178
+ function cleanHtmlText10(html) {
9179
+ let text = html.replace(
9180
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
9181
+ " "
9182
+ );
9183
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
9184
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
9185
+ text = text.replace(/<p[^>]*>/gi, "\n");
9186
+ text = text.replace(/<br\s*\/?>/gi, "\n");
9187
+ text = text.replace(/<[^>]+>/g, " ");
9188
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
9189
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9190
+ }
9191
+ function normalizeForComparison10(str) {
9192
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9193
+ }
9194
+
9195
+ // src/cli/curriculum/providers/lehrplan-thueringen/manifest.ts
9196
+ var LEHRPLAN_THUERINGEN_MANIFEST = {
9197
+ schoolYear: "2025/2026",
9198
+ capturedOn: "2026-07-02",
9199
+ sourceRevision: "Lehrpl\xE4ne Th\xFCringen",
9200
+ schoolTypes: [
9201
+ { id: "regelschule", label: "Regelschule" },
9202
+ { id: "gymnasium", label: "Gymnasium" }
9203
+ ],
9204
+ grades: {
9205
+ regelschule: ["7", "8", "9", "10"],
9206
+ gymnasium: ["7", "8", "9", "10"]
9207
+ },
9208
+ subjects: {
9209
+ regelschule: [
9210
+ { id: "mathematik", label: "Mathematik" },
9211
+ { id: "informatik", label: "Informatik" },
9212
+ { id: "physik", label: "Physik" },
9213
+ { id: "chemie", label: "Chemie" },
9214
+ { id: "biologie", label: "Biologie" }
9215
+ ],
9216
+ gymnasium: [
9217
+ { id: "mathematik", label: "Mathematik" },
9218
+ { id: "informatik", label: "Informatik" },
9219
+ { id: "physik", label: "Physik" },
9220
+ { id: "chemie", label: "Chemie" },
9221
+ { id: "biologie", label: "Biologie" }
9222
+ ]
9223
+ },
9224
+ tracks: {},
9225
+ topics: {
9226
+ "regelschule|10|mathematik": [{ id: "funktionen", label: "Funktionen" }],
9227
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
9228
+ "regelschule|9|physik": [{ id: "mechanik", label: "Mechanik" }],
9229
+ "regelschule|9|chemie": [{ id: "reaktionen", label: "Reaktionen" }],
9230
+ "regelschule|9|biologie": [{ id: "zellen", label: "Zellen" }],
9231
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
9232
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Bindungen" }],
9233
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
9234
+ },
9235
+ contentUrls: {
9236
+ "regelschule|10|mathematik": "https://www.schulportal-thueringen.de/web/guest/lehrplaene",
9237
+ "gymnasium|10|mathematik": "https://www.schulportal-thueringen.de/web/guest/lehrplaene",
9238
+ "regelschule|9|physik": "https://www.schulportal-thueringen.de/web/guest/lehrplaene",
9239
+ "regelschule|9|chemie": "https://www.schulportal-thueringen.de/web/guest/lehrplaene",
9240
+ "regelschule|9|biologie": "https://www.schulportal-thueringen.de/web/guest/lehrplaene",
9241
+ "gymnasium|9|physik": "https://www.schulportal-thueringen.de/web/guest/lehrplaene",
9242
+ "gymnasium|9|chemie": "https://www.schulportal-thueringen.de/web/guest/lehrplaene",
9243
+ "gymnasium|9|biologie": "https://www.schulportal-thueringen.de/web/guest/lehrplaene"
9244
+ }
9245
+ };
9246
+
9247
+ // src/cli/curriculum/providers/lehrplan-thueringen/index.ts
9248
+ var lehrplanThueringenProvider = {
9249
+ id: "lehrplan-thueringen",
9250
+ country: "DE",
9251
+ countryLabel: "Deutschland",
9252
+ region: "TH",
9253
+ regionLabel: "Th\xFCringen",
9254
+ label: "Lehrplan (Th\xFCringen)",
9255
+ listSchoolTypes() {
9256
+ return LEHRPLAN_THUERINGEN_MANIFEST.schoolTypes;
9257
+ },
9258
+ listGrades(schoolType) {
9259
+ return (LEHRPLAN_THUERINGEN_MANIFEST.grades[schoolType] || []).map((id) => ({
9260
+ id,
9261
+ label: `Klasse ${id}`
9262
+ }));
9263
+ },
9264
+ listSubjects(schoolType, _grade) {
9265
+ return LEHRPLAN_THUERINGEN_MANIFEST.subjects[schoolType] || [];
9266
+ },
9267
+ listTracks(schoolType, grade, subject) {
9268
+ const key = `${schoolType}|${grade}|${subject}`;
9269
+ return LEHRPLAN_THUERINGEN_MANIFEST.tracks[key] || [];
9270
+ },
9271
+ listTopics(selection) {
9272
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
9273
+ const list = LEHRPLAN_THUERINGEN_MANIFEST.topics[key] || [];
9274
+ return list.map((t2) => ({
9275
+ ...t2,
9276
+ sourceRef: key
9277
+ }));
9278
+ },
9279
+ resolveTopic(topic) {
9280
+ const uri = LEHRPLAN_THUERINGEN_MANIFEST.contentUrls[topic.sourceRef];
9281
+ if (!uri) {
9282
+ throw new Error(
9283
+ `Lehrplan Th\xFCringen: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
9284
+ );
9285
+ }
9286
+ return {
9287
+ provider: "lehrplan-thueringen",
9288
+ topicId: `${topic.sourceRef}#${topic.id}`,
9289
+ uri
9290
+ };
9291
+ },
9292
+ extractTopics(html, topicIds) {
9293
+ const results = {};
9294
+ const headingMatches = Array.from(
9295
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
9296
+ );
9297
+ const sections = [];
9298
+ for (const m of headingMatches) {
9299
+ const headerText = cleanHtmlText11(m[1]).trim();
9300
+ const start = m.index + m[0].length;
9301
+ const nextHeading = html.indexOf("<h", start);
9302
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
9303
+ const content = cleanHtmlText11(rawContent).slice(0, 1500);
9304
+ if (headerText) sections.push({ header: headerText, content });
9305
+ }
9306
+ for (const topicId of topicIds) {
9307
+ const hashIdx = topicId.indexOf("#");
9308
+ if (hashIdx === -1) continue;
9309
+ const key = topicId.substring(0, hashIdx);
9310
+ const shortId = topicId.substring(hashIdx + 1);
9311
+ const list = LEHRPLAN_THUERINGEN_MANIFEST.topics[key] ?? [];
9312
+ const match = list.find((t2) => t2.id === shortId);
9313
+ if (!match) continue;
9314
+ const label = match.label;
9315
+ const normalizedLabel = normalizeForComparison11(label);
9316
+ let section = sections.find(
9317
+ (s) => normalizeForComparison11(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
9318
+ );
9319
+ if (!section && sections.length > 0) {
9320
+ section = sections.find((s) => s.content.length > 80) || sections[0];
9321
+ }
9322
+ if (section) {
9323
+ results[topicId] = `${label}
9324
+
9325
+ ${section.content}`.trim();
9326
+ } else {
9327
+ results[topicId] = label;
9328
+ }
9329
+ }
9330
+ return results;
9331
+ }
9332
+ };
9333
+ function cleanHtmlText11(html) {
9334
+ let text = html.replace(
9335
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
9336
+ " "
9337
+ );
9338
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
9339
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
9340
+ text = text.replace(/<p[^>]*>/gi, "\n");
9341
+ text = text.replace(/<br\s*\/?>/gi, "\n");
9342
+ text = text.replace(/<[^>]+>/g, " ");
9343
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
9344
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9345
+ }
9346
+ function normalizeForComparison11(str) {
9347
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9348
+ }
9349
+
9350
+ // src/cli/curriculum/providers/lehrplanplus-bayern/manifest.ts
9351
+ var LEHRPLANPLUS_BAYERN_MANIFEST = {
9352
+ schoolYear: "2026/2027",
9353
+ capturedOn: "2026-07-02",
9354
+ sourceRevision: "LehrplanPLUS Realschule \u2013 Oktober 2023",
9355
+ schoolTypes: [
9356
+ { id: "grundschule", label: "Grundschule" },
9357
+ { id: "mittelschule", label: "Mittelschule" },
9358
+ { id: "foerderschule", label: "F\xF6rderschule" },
9359
+ { id: "realschule", label: "Realschule" },
9360
+ { id: "gymnasium", label: "Gymnasium" },
9361
+ { id: "wirtschaftsschule", label: "Wirtschaftsschule" },
9362
+ { id: "fos", label: "Fachoberschule" },
9363
+ { id: "bos", label: "Berufsoberschule" }
9364
+ ],
9365
+ grades: {
9366
+ realschule: ["5", "6", "7", "8", "9", "10"]
9367
+ },
9368
+ subjects: {
9369
+ realschule: [
9370
+ {
9371
+ id: "bwl-rechnungswesen",
9372
+ label: "Betriebswirtschaftslehre / Rechnungswesen"
9373
+ },
9374
+ { id: "biologie", label: "Biologie" },
9375
+ { id: "chemie", label: "Chemie" },
9376
+ { id: "deutsch", label: "Deutsch" },
9377
+ { id: "englisch", label: "Englisch" },
9378
+ { id: "ernaehrung_und_gesundheit", label: "Ern\xE4hrung und Gesundheit" },
9379
+ { id: "ethik", label: "Ethik" },
9380
+ {
9381
+ id: "evangelische-religionslehre",
9382
+ label: "Evangelische Religionslehre"
9383
+ },
9384
+ { id: "franzoesisch", label: "Franz\xF6sisch" },
9385
+ { id: "geographie", label: "Geographie" },
9386
+ { id: "geschichte", label: "Geschichte" },
9387
+ { id: "it", label: "Informationstechnologie" },
9388
+ { id: "iu", label: "Islamischer Unterricht" },
9389
+ { id: "ir", label: "Israelitische Religionslehre" },
9390
+ {
9391
+ id: "katholische-religionslehre",
9392
+ label: "Katholische Religionslehre"
9393
+ },
9394
+ { id: "kunst", label: "Kunst" },
9395
+ { id: "mathematik", label: "Mathematik" },
9396
+ { id: "musik", label: "Musik" },
9397
+ { id: "or", label: "Orthodoxe Religionslehre" },
9398
+ { id: "physik", label: "Physik" },
9399
+ { id: "pug", label: "Politik und Gesellschaft" },
9400
+ { id: "soziallehre", label: "Soziallehre" },
9401
+ { id: "sozialwesen", label: "Sozialwesen" },
9402
+ { id: "spanisch", label: "Spanisch" },
9403
+ { id: "sport", label: "Sport" },
9404
+ { id: "textiles-gestalten", label: "Textiles Gestalten" },
9405
+ { id: "werken", label: "Werken" },
9406
+ { id: "wirtschaft-und-recht", label: "Wirtschaft und Recht" }
9407
+ ]
9408
+ },
9409
+ tracks: {
9410
+ "realschule|9|mathematik": [
9411
+ { id: "wpfg1", label: "Mathematik 9 (I)" },
9412
+ { id: "wpfg2-3", label: "Mathematik 9 (II/III)" }
9413
+ ]
9414
+ },
9415
+ topics: {
9416
+ "realschule|9|mathematik|wpfg1": [
9417
+ { id: "lb1", label: "Reelle Zahlen", hours: 10 },
9418
+ { id: "lb2", label: "Zentrische Streckung", hours: 17 },
9419
+ { id: "lb3", label: "Rechtwinklige Dreiecke", hours: 20 },
9420
+ { id: "lb4", label: "Kreis", hours: 10 },
9421
+ { id: "lb5", label: "Raumgeometrie", hours: 20 },
9422
+ { id: "lb6", label: "Systeme linearer Gleichungen", hours: 12 },
9423
+ {
9424
+ id: "lb7",
9425
+ label: "Quadratische Funktionen und quadratische Gleichungen",
9426
+ hours: 42
9427
+ },
9428
+ { id: "lb8", label: "Daten und Zufall", hours: 9 }
9429
+ ],
9430
+ "realschule|9|mathematik|wpfg2-3": [
9431
+ { id: "lb1", label: "Reelle Zahlen", hours: 7 },
9432
+ { id: "lb2", label: "Zentrische Streckung", hours: 13 },
9433
+ { id: "lb3", label: "Rechtwinklige Dreiecke", hours: 20 },
9434
+ { id: "lb4", label: "Kreis", hours: 10 },
9435
+ { id: "lb5", label: "Lineare Funktionen", hours: 15 },
9436
+ { id: "lb6", label: "Systeme linearer Gleichungen", hours: 10 },
9437
+ { id: "lb7", label: "Daten und Zufall", hours: 9 }
9438
+ ],
9439
+ "realschule|9|deutsch": [
9440
+ { id: "lb1", label: "Sprechen und Zuh\xF6ren" },
9441
+ { id: "lb2", label: "Lesen \u2013 mit Texten und weiteren Medien umgehen" },
9442
+ { id: "lb3", label: "Schreiben" },
9443
+ {
9444
+ id: "lb4",
9445
+ label: "Sprachgebrauch und Sprache untersuchen und reflektieren"
9446
+ }
9447
+ ],
9448
+ "realschule|9|englisch": [
9449
+ { id: "lb1", label: "Kommunikative Kompetenzen" },
9450
+ { id: "lb2", label: "Interkulturelle Kompetenzen" },
9451
+ { id: "lb3", label: "Text- und Medienkompetenzen" },
9452
+ { id: "lb4", label: "Methodische Kompetenzen" },
9453
+ { id: "lb5", label: "Themengebiete" }
9454
+ ]
9455
+ },
9456
+ contentUrls: {
9457
+ "realschule|9|mathematik|wpfg1": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/9/fach/mathematik/inhalt/fachlehrplaene?w_schulart=realschule&wt_1=schulart&w_fach=mathematik&wt_2=fach&w_jgs=9&wt_3=jgs&w_auspraegung=wpfg1",
9458
+ "realschule|9|mathematik|wpfg2-3": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/9/fach/mathematik/inhalt/fachlehrplaene?w_schulart=realschule&wt_1=schulart&w_fach=mathematik&wt_2=fach&w_jgs=9&wt_3=jgs&w_auspraegung=wpfg2-3",
9459
+ "realschule|9|deutsch": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/9/fach/deutsch/inhalt/fachlehrplaene",
9460
+ "realschule|9|englisch": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/9/fach/englisch/inhalt/fachlehrplaene"
9461
+ }
9462
+ };
9463
+
9464
+ // src/cli/curriculum/providers/lehrplanplus-bayern/index.ts
9465
+ function levelKey(schoolType, grade, subject, track) {
9466
+ return track ? `${schoolType}|${grade}|${subject}|${track}` : `${schoolType}|${grade}|${subject}`;
9467
+ }
9468
+ var lehrplanplusBayernProvider = {
9469
+ id: "lehrplanplus-bayern",
9470
+ country: "DE",
9471
+ countryLabel: "Deutschland",
9472
+ region: "BY",
9473
+ regionLabel: "Bayern",
9474
+ label: "LehrplanPLUS (Bayern)",
9475
+ listSchoolTypes() {
9476
+ return LEHRPLANPLUS_BAYERN_MANIFEST.schoolTypes;
9477
+ },
9478
+ listGrades(schoolType) {
9479
+ return (LEHRPLANPLUS_BAYERN_MANIFEST.grades[schoolType] ?? []).map((grade) => ({
9480
+ id: grade,
9481
+ label: grade
9482
+ }));
9483
+ },
9484
+ listSubjects(schoolType, _grade) {
9485
+ return LEHRPLANPLUS_BAYERN_MANIFEST.subjects[schoolType] ?? [];
9486
+ },
9487
+ listTracks(schoolType, grade, subject) {
9488
+ return LEHRPLANPLUS_BAYERN_MANIFEST.tracks[levelKey(schoolType, grade, subject)] ?? [];
9489
+ },
9490
+ listTopics(selection) {
9491
+ const { schoolType, grade, subject, track } = selection;
9492
+ if (!schoolType || !grade || !subject) return [];
9493
+ const key = levelKey(schoolType, grade, subject, track);
9494
+ return (LEHRPLANPLUS_BAYERN_MANIFEST.topics[key] ?? []).map((topic) => ({
9495
+ ...topic,
9496
+ sourceRef: key
9497
+ }));
9498
+ },
9499
+ resolveTopic(topic) {
9500
+ const uri = LEHRPLANPLUS_BAYERN_MANIFEST.contentUrls[topic.sourceRef];
9501
+ if (!uri) {
9502
+ throw new Error(
9503
+ `LehrplanPLUS Bayern: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}). The manifest only covers the combinations curated as of ${LEHRPLANPLUS_BAYERN_MANIFEST.capturedOn}.`
9504
+ );
9505
+ }
9506
+ return {
9507
+ provider: "lehrplanplus-bayern",
9508
+ topicId: `${topic.sourceRef}#${topic.id}`,
9509
+ uri
9510
+ };
9511
+ },
9512
+ extractTopics(html, topicIds) {
9513
+ const chunks = html.split('<div id="thema_');
9514
+ const sections = [];
9515
+ for (let i = 1; i < chunks.length; i++) {
9516
+ const chunk = chunks[i];
9517
+ const quoteIdx = chunk.indexOf('"');
9518
+ if (quoteIdx === -1) continue;
9519
+ const id = chunk.slice(0, quoteIdx);
9520
+ const classStart = chunk.indexOf('class="');
9521
+ if (classStart === -1) continue;
9522
+ const classEnd = chunk.indexOf('"', classStart + 7);
9523
+ const classContent = chunk.slice(classStart + 7, classEnd);
9524
+ const lvlMatch = classContent.match(/headline_lvl(\d+)/);
9525
+ if (!lvlMatch) continue;
9526
+ const level = parseInt(lvlMatch[1], 10);
9527
+ const contentHtml = chunk.slice(classEnd + 1);
9528
+ const headerMatch = contentHtml.match(
9529
+ /<a[^>]*class="paragraph_toggle"[^>]*>([\s\S]*?)<\/a>/i
9530
+ );
9531
+ const headerText = headerMatch ? cleanHtmlText12(headerMatch[1]).trim() : "";
9532
+ sections.push({
9533
+ id,
9534
+ level,
9535
+ headerText,
9536
+ contentHtml
9537
+ });
9538
+ }
9539
+ const results = {};
9540
+ for (const topicId of topicIds) {
9541
+ let label = "";
9542
+ for (const key of Object.keys(LEHRPLANPLUS_BAYERN_MANIFEST.topics)) {
9543
+ const list = LEHRPLANPLUS_BAYERN_MANIFEST.topics[key];
9544
+ const match = list.find(
9545
+ (t2) => t2.id === topicId || `${key}#${t2.id}` === topicId
9546
+ );
9547
+ if (match) {
9548
+ label = match.label;
9549
+ break;
9550
+ }
9551
+ }
9552
+ if (!label) {
9553
+ continue;
9554
+ }
9555
+ const normalizedLabel = normalizeForComparison12(label);
9556
+ const lvl1Index = sections.findIndex((s) => {
9557
+ if (s.level !== 1) return false;
9558
+ const normalizedHeader = normalizeForComparison12(s.headerText);
9559
+ return normalizedHeader.includes(normalizedLabel);
9560
+ });
9561
+ if (lvl1Index === -1) {
9562
+ continue;
9563
+ }
9564
+ const collectedSections = [sections[lvl1Index]];
9565
+ for (let i = lvl1Index + 1; i < sections.length; i++) {
9566
+ if (sections[i].level === 1) {
9567
+ break;
9568
+ }
9569
+ collectedSections.push(sections[i]);
9570
+ }
9571
+ const fullHtml = collectedSections.map((s) => s.contentHtml).join("\n");
9572
+ results[topicId] = cleanHtmlText12(fullHtml);
9573
+ }
9574
+ return results;
9575
+ }
9576
+ };
9577
+ function cleanHtmlText12(html) {
9578
+ let text = html.replace(
9579
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
9580
+ " "
9581
+ );
9582
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
9583
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
9584
+ text = text.replace(/<p[^>]*>/gi, "\n");
9585
+ text = text.replace(/<br\s*\/?>/gi, "\n");
9586
+ text = text.replace(/<[^>]+>/g, " ");
9587
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&ndash;/g, "\u2013").replace(/&mdash;/g, "\u2014").replace(/&middot;/g, "\xB7");
9588
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9589
+ }
9590
+ function normalizeForComparison12(str) {
9591
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9592
+ }
9593
+
9594
+ // src/cli/curriculum/providers/rahmenlehrplan-berlin-brandenburg/manifest.ts
9595
+ var RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST = {
9596
+ schoolYear: "2025/2026",
9597
+ capturedOn: "2026-07-02",
9598
+ sourceRevision: "Gemeinsamer Rahmenlehrplan Berlin-Brandenburg",
9599
+ schoolTypes: [
9600
+ { id: "realschule", label: "Realschule" },
9601
+ { id: "gymnasium", label: "Gymnasium" }
9602
+ ],
9603
+ grades: {
9604
+ realschule: ["7", "8", "9", "10"],
9605
+ gymnasium: ["7", "8", "9", "10"]
9606
+ },
9607
+ subjects: {
9608
+ realschule: [
9609
+ { id: "mathematik", label: "Mathematik" },
9610
+ { id: "informatik", label: "Informatik" },
9611
+ { id: "physik", label: "Physik" },
9612
+ { id: "chemie", label: "Chemie" },
9613
+ { id: "biologie", label: "Biologie" }
9614
+ ],
9615
+ gymnasium: [
9616
+ { id: "mathematik", label: "Mathematik" },
9617
+ { id: "informatik", label: "Informatik" },
9618
+ { id: "physik", label: "Physik" },
9619
+ { id: "chemie", label: "Chemie" },
9620
+ { id: "biologie", label: "Biologie" }
9621
+ ]
9622
+ },
9623
+ tracks: {},
9624
+ topics: {
9625
+ "realschule|10|mathematik": [
9626
+ { id: "funktionen", label: "Funktionen" },
9627
+ { id: "geometrie", label: "Geometrie" },
9628
+ { id: "stochastik", label: "Stochastik" }
9629
+ ],
9630
+ "gymnasium|10|mathematik": [
9631
+ { id: "analysis", label: "Analysis" },
9632
+ { id: "vektoren", label: "Vektoren" }
9633
+ ],
9634
+ "realschule|9|informatik": [
9635
+ { id: "algorithmen", label: "Algorithmen" },
9636
+ { id: "daten", label: "Daten und Information" }
9637
+ ],
9638
+ "realschule|9|physik": [
9639
+ { id: "mechanik", label: "Mechanik" },
9640
+ { id: "elektrizitaet", label: "Elektrizit\xE4t" }
9641
+ ],
9642
+ "realschule|9|chemie": [
9643
+ { id: "reaktionen", label: "Chemische Reaktionen" },
9644
+ { id: "stoffe", label: "Stoffe" }
9645
+ ],
9646
+ "realschule|9|biologie": [
9647
+ { id: "zellen", label: "Zellen" },
9648
+ { id: "oekologie", label: "\xD6kologie" }
9649
+ ],
9650
+ "gymnasium|9|physik": [
9651
+ { id: "optik", label: "Optik" },
9652
+ { id: "felder", label: "Felder" }
9653
+ ],
9654
+ "gymnasium|9|chemie": [
9655
+ { id: "bindungen", label: "Bindungen" },
9656
+ { id: "saeuren", label: "S\xE4uren und Basen" }
9657
+ ],
9658
+ "gymnasium|9|biologie": [
9659
+ { id: "genetik", label: "Genetik" },
9660
+ { id: "evolution", label: "Evolution" }
9661
+ ]
9662
+ },
9663
+ contentUrls: {
9664
+ "realschule|10|mathematik": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/mathematik",
9665
+ "gymnasium|10|mathematik": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/mathematik",
9666
+ "realschule|9|informatik": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/informatik",
9667
+ "realschule|9|physik": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/physik",
9668
+ "realschule|9|chemie": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/chemie",
9669
+ "realschule|9|biologie": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/biologie",
9670
+ "gymnasium|9|physik": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/physik",
9671
+ "gymnasium|9|chemie": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/chemie",
9672
+ "gymnasium|9|biologie": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/biologie"
9673
+ }
9674
+ };
9675
+
9676
+ // src/cli/curriculum/providers/rahmenlehrplan-berlin-brandenburg/index.ts
9677
+ var rahmenlehrplanBerlinBrandenburgProvider = {
9678
+ id: "rahmenlehrplan-berlin-brandenburg",
9679
+ country: "DE",
9680
+ countryLabel: "Deutschland",
9681
+ region: "BE-BB",
9682
+ regionLabel: "Berlin / Brandenburg",
9683
+ label: "Rahmenlehrplan (Berlin-Brandenburg)",
9684
+ listSchoolTypes() {
9685
+ return RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST.schoolTypes;
9686
+ },
9687
+ listGrades(schoolType) {
9688
+ return (RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST.grades[schoolType] || []).map((id) => ({
9689
+ id,
9690
+ label: `Klasse ${id}`
9691
+ }));
9692
+ },
9693
+ listSubjects(schoolType, _grade) {
9694
+ return RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST.subjects[schoolType] || [];
9695
+ },
9696
+ listTracks(schoolType, grade, subject) {
9697
+ const key = `${schoolType}|${grade}|${subject}`;
9698
+ return RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST.tracks[key] || [];
9699
+ },
9700
+ listTopics(selection) {
9701
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
9702
+ const list = RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST.topics[key] || [];
9703
+ return list.map((t2) => ({
9704
+ ...t2,
9705
+ sourceRef: key
9706
+ }));
9707
+ },
9708
+ resolveTopic(topic) {
9709
+ const uri = RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST.contentUrls[topic.sourceRef];
9710
+ if (!uri) {
9711
+ throw new Error(
9712
+ `Rahmenlehrplan Berlin-Brandenburg: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
9713
+ );
9714
+ }
9715
+ return {
9716
+ provider: "rahmenlehrplan-berlin-brandenburg",
9717
+ topicId: `${topic.sourceRef}#${topic.id}`,
9718
+ uri
9719
+ };
9720
+ },
9721
+ extractTopics(html, topicIds) {
9722
+ const results = {};
9723
+ const headingMatches = Array.from(
9724
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
9725
+ );
9726
+ const sections = [];
9727
+ for (const m of headingMatches) {
9728
+ const headerText = cleanHtmlText13(m[1]).trim();
9729
+ const start = m.index + m[0].length;
9730
+ const nextHeading = html.indexOf("<h", start);
9731
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
9732
+ const content = cleanHtmlText13(rawContent).slice(0, 1500);
9733
+ if (headerText) sections.push({ header: headerText, content });
9734
+ }
9735
+ for (const topicId of topicIds) {
9736
+ const hashIdx = topicId.indexOf("#");
9737
+ if (hashIdx === -1) continue;
9738
+ const key = topicId.substring(0, hashIdx);
9739
+ const shortId = topicId.substring(hashIdx + 1);
9740
+ const list = RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST.topics[key] ?? [];
9741
+ const match = list.find((t2) => t2.id === shortId);
9742
+ if (!match) continue;
9743
+ const label = match.label;
9744
+ const normalizedLabel = normalizeForComparison13(label);
9745
+ let section = sections.find(
9746
+ (s) => normalizeForComparison13(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
9747
+ );
9748
+ if (!section && sections.length > 0) {
9749
+ section = sections.find((s) => s.content.length > 80) || sections[0];
9750
+ }
9751
+ if (section) {
9752
+ results[topicId] = `${label}
9753
+
9754
+ ${section.content}`.trim();
9755
+ } else {
9756
+ results[topicId] = label;
9757
+ }
9758
+ }
9759
+ return results;
9760
+ }
9761
+ };
9762
+ function cleanHtmlText13(html) {
9763
+ let text = html.replace(
9764
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
9765
+ " "
9766
+ );
9767
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
9768
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
9769
+ text = text.replace(/<p[^>]*>/gi, "\n");
9770
+ text = text.replace(/<br\s*\/?>/gi, "\n");
9771
+ text = text.replace(/<[^>]+>/g, " ");
9772
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
9773
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9774
+ }
9775
+ function normalizeForComparison13(str) {
9776
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9777
+ }
9778
+
9779
+ // src/cli/curriculum/providers/rahmenplan-mv/manifest.ts
9780
+ var RAHMENPLAN_MV_MANIFEST = {
9781
+ schoolYear: "2025/2026",
9782
+ capturedOn: "2026-07-02",
9783
+ sourceRevision: "Rahmenpl\xE4ne Mecklenburg-Vorpommern",
9784
+ schoolTypes: [
9785
+ { id: "regionale-schule", label: "Regionale Schule" },
9786
+ { id: "gymnasium", label: "Gymnasium" }
9787
+ ],
9788
+ grades: {
9789
+ "regionale-schule": ["7", "8", "9", "10"],
9790
+ gymnasium: ["7", "8", "9", "10"]
9791
+ },
9792
+ subjects: {
9793
+ "regionale-schule": [
9794
+ { id: "mathematik", label: "Mathematik" },
9795
+ { id: "informatik", label: "Informatik" },
9796
+ { id: "physik", label: "Physik" },
9797
+ { id: "chemie", label: "Chemie" },
9798
+ { id: "biologie", label: "Biologie" }
9799
+ ],
9800
+ gymnasium: [
9801
+ { id: "mathematik", label: "Mathematik" },
9802
+ { id: "informatik", label: "Informatik" },
9803
+ { id: "physik", label: "Physik" },
9804
+ { id: "chemie", label: "Chemie" },
9805
+ { id: "biologie", label: "Biologie" }
9806
+ ]
9807
+ },
9808
+ tracks: {},
9809
+ topics: {
9810
+ "regionale-schule|10|mathematik": [
9811
+ { id: "funktionen", label: "Funktionen" }
9812
+ ],
9813
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
9814
+ "regionale-schule|9|physik": [{ id: "mechanik", label: "Mechanik" }],
9815
+ "regionale-schule|9|chemie": [{ id: "reaktionen", label: "Reaktionen" }],
9816
+ "regionale-schule|9|biologie": [{ id: "zellen", label: "Zellen" }],
9817
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
9818
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Bindungen" }],
9819
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
9820
+ },
9821
+ contentUrls: {
9822
+ "regionale-schule|10|mathematik": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/",
9823
+ "gymnasium|10|mathematik": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/",
9824
+ "regionale-schule|9|physik": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/",
9825
+ "regionale-schule|9|chemie": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/",
9826
+ "regionale-schule|9|biologie": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/",
9827
+ "gymnasium|9|physik": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/",
9828
+ "gymnasium|9|chemie": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/",
9829
+ "gymnasium|9|biologie": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/"
9830
+ }
9831
+ };
9832
+
9833
+ // src/cli/curriculum/providers/rahmenplan-mv/index.ts
9834
+ var rahmenplanMvProvider = {
9835
+ id: "rahmenplan-mv",
9836
+ country: "DE",
9837
+ countryLabel: "Deutschland",
9838
+ region: "MV",
9839
+ regionLabel: "Mecklenburg-Vorpommern",
9840
+ label: "Rahmenplan (Mecklenburg-Vorpommern)",
9841
+ listSchoolTypes() {
9842
+ return RAHMENPLAN_MV_MANIFEST.schoolTypes;
9843
+ },
9844
+ listGrades(schoolType) {
9845
+ return (RAHMENPLAN_MV_MANIFEST.grades[schoolType] || []).map((id) => ({
9846
+ id,
9847
+ label: `Klasse ${id}`
9848
+ }));
9849
+ },
9850
+ listSubjects(schoolType, _grade) {
9851
+ return RAHMENPLAN_MV_MANIFEST.subjects[schoolType] || [];
9852
+ },
9853
+ listTracks(schoolType, grade, subject) {
9854
+ const key = `${schoolType}|${grade}|${subject}`;
9855
+ return RAHMENPLAN_MV_MANIFEST.tracks[key] || [];
9856
+ },
9857
+ listTopics(selection) {
9858
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
9859
+ const list = RAHMENPLAN_MV_MANIFEST.topics[key] || [];
9860
+ return list.map((t2) => ({
9861
+ ...t2,
9862
+ sourceRef: key
9863
+ }));
9864
+ },
9865
+ resolveTopic(topic) {
9866
+ const uri = RAHMENPLAN_MV_MANIFEST.contentUrls[topic.sourceRef];
9867
+ if (!uri) {
9868
+ throw new Error(
9869
+ `Rahmenplan MV: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
9870
+ );
9871
+ }
9872
+ return {
9873
+ provider: "rahmenplan-mv",
9874
+ topicId: `${topic.sourceRef}#${topic.id}`,
9875
+ uri
9876
+ };
9877
+ },
9878
+ extractTopics(html, topicIds) {
9879
+ const results = {};
9880
+ const headingMatches = Array.from(
9881
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
9882
+ );
9883
+ const sections = [];
9884
+ for (const m of headingMatches) {
9885
+ const headerText = cleanHtmlText14(m[1]).trim();
9886
+ const start = m.index + m[0].length;
9887
+ const nextHeading = html.indexOf("<h", start);
9888
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
9889
+ const content = cleanHtmlText14(rawContent).slice(0, 1500);
9890
+ if (headerText) sections.push({ header: headerText, content });
9891
+ }
9892
+ for (const topicId of topicIds) {
9893
+ const hashIdx = topicId.indexOf("#");
9894
+ if (hashIdx === -1) continue;
9895
+ const key = topicId.substring(0, hashIdx);
9896
+ const shortId = topicId.substring(hashIdx + 1);
9897
+ const list = RAHMENPLAN_MV_MANIFEST.topics[key] ?? [];
9898
+ const match = list.find((t2) => t2.id === shortId);
9899
+ if (!match) continue;
9900
+ const label = match.label;
9901
+ const normalizedLabel = normalizeForComparison14(label);
9902
+ let section = sections.find(
9903
+ (s) => normalizeForComparison14(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
9904
+ );
9905
+ if (!section && sections.length > 0) {
9906
+ section = sections.find((s) => s.content.length > 80) || sections[0];
9907
+ }
9908
+ if (section) {
9909
+ results[topicId] = `${label}
9910
+
9911
+ ${section.content}`.trim();
9912
+ } else {
9913
+ results[topicId] = label;
9914
+ }
9915
+ }
9916
+ return results;
9917
+ }
9918
+ };
9919
+ function cleanHtmlText14(html) {
9920
+ let text = html.replace(
9921
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
9922
+ " "
9923
+ );
9924
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
9925
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
9926
+ text = text.replace(/<p[^>]*>/gi, "\n");
9927
+ text = text.replace(/<br\s*\/?>/gi, "\n");
9928
+ text = text.replace(/<[^>]+>/g, " ");
9929
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
9930
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9931
+ }
9932
+ function normalizeForComparison14(str) {
9933
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9934
+ }
9935
+
9936
+ // src/cli/curriculum/providers/rahmenrichtlinien-st/manifest.ts
9937
+ var RAHMENRICHTLINIEN_ST_MANIFEST = {
9938
+ schoolYear: "2025/2026",
9939
+ capturedOn: "2026-07-02",
9940
+ sourceRevision: "Rahmenrichtlinien Sachsen-Anhalt",
9941
+ schoolTypes: [
9942
+ { id: "sekundarschule", label: "Sekundarschule" },
9943
+ { id: "gymnasium", label: "Gymnasium" }
9944
+ ],
9945
+ grades: {
9946
+ sekundarschule: ["7", "8", "9", "10"],
9947
+ gymnasium: ["7", "8", "9", "10"]
9948
+ },
9949
+ subjects: {
9950
+ sekundarschule: [
9951
+ { id: "mathematik", label: "Mathematik" },
9952
+ { id: "informatik", label: "Informatik" },
9953
+ { id: "physik", label: "Physik" },
9954
+ { id: "chemie", label: "Chemie" },
9955
+ { id: "biologie", label: "Biologie" }
9956
+ ],
9957
+ gymnasium: [
9958
+ { id: "mathematik", label: "Mathematik" },
9959
+ { id: "informatik", label: "Informatik" },
9960
+ { id: "physik", label: "Physik" },
9961
+ { id: "chemie", label: "Chemie" },
9962
+ { id: "biologie", label: "Biologie" }
9963
+ ]
9964
+ },
9965
+ tracks: {},
9966
+ topics: {
9967
+ "sekundarschule|10|mathematik": [{ id: "funktionen", label: "Funktionen" }],
9968
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
9969
+ "sekundarschule|9|physik": [{ id: "mechanik", label: "Mechanik" }],
9970
+ "sekundarschule|9|chemie": [{ id: "reaktionen", label: "Reaktionen" }],
9971
+ "sekundarschule|9|biologie": [{ id: "zellen", label: "Zellen" }],
9972
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
9973
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Bindungen" }],
9974
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
9975
+ },
9976
+ contentUrls: {
9977
+ "sekundarschule|10|mathematik": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien",
9978
+ "gymnasium|10|mathematik": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien",
9979
+ "sekundarschule|9|physik": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien",
9980
+ "sekundarschule|9|chemie": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien",
9981
+ "sekundarschule|9|biologie": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien",
9982
+ "gymnasium|9|physik": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien",
9983
+ "gymnasium|9|chemie": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien",
9984
+ "gymnasium|9|biologie": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien"
9985
+ }
9986
+ };
9987
+
9988
+ // src/cli/curriculum/providers/rahmenrichtlinien-st/index.ts
9989
+ var rahmenrichtlinienStProvider = {
9990
+ id: "rahmenrichtlinien-st",
9991
+ country: "DE",
9992
+ countryLabel: "Deutschland",
9993
+ region: "ST",
9994
+ regionLabel: "Sachsen-Anhalt",
9995
+ label: "Rahmenrichtlinien (Sachsen-Anhalt)",
9996
+ listSchoolTypes() {
9997
+ return RAHMENRICHTLINIEN_ST_MANIFEST.schoolTypes;
9998
+ },
9999
+ listGrades(schoolType) {
10000
+ return (RAHMENRICHTLINIEN_ST_MANIFEST.grades[schoolType] || []).map((id) => ({
10001
+ id,
10002
+ label: `Klasse ${id}`
10003
+ }));
10004
+ },
10005
+ listSubjects(schoolType, _grade) {
10006
+ return RAHMENRICHTLINIEN_ST_MANIFEST.subjects[schoolType] || [];
10007
+ },
10008
+ listTracks(schoolType, grade, subject) {
10009
+ const key = `${schoolType}|${grade}|${subject}`;
10010
+ return RAHMENRICHTLINIEN_ST_MANIFEST.tracks[key] || [];
10011
+ },
10012
+ listTopics(selection) {
10013
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
10014
+ const list = RAHMENRICHTLINIEN_ST_MANIFEST.topics[key] || [];
10015
+ return list.map((t2) => ({
10016
+ ...t2,
10017
+ sourceRef: key
10018
+ }));
10019
+ },
10020
+ resolveTopic(topic) {
10021
+ const uri = RAHMENRICHTLINIEN_ST_MANIFEST.contentUrls[topic.sourceRef];
10022
+ if (!uri) {
10023
+ throw new Error(
10024
+ `Rahmenrichtlinien ST: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
10025
+ );
10026
+ }
10027
+ return {
10028
+ provider: "rahmenrichtlinien-st",
10029
+ topicId: `${topic.sourceRef}#${topic.id}`,
10030
+ uri
10031
+ };
10032
+ },
10033
+ extractTopics(html, topicIds) {
10034
+ const results = {};
10035
+ const headingMatches = Array.from(
10036
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
10037
+ );
10038
+ const sections = [];
10039
+ for (const m of headingMatches) {
10040
+ const headerText = cleanHtmlText15(m[1]).trim();
10041
+ const start = m.index + m[0].length;
10042
+ const nextHeading = html.indexOf("<h", start);
10043
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
10044
+ const content = cleanHtmlText15(rawContent).slice(0, 1500);
10045
+ if (headerText) sections.push({ header: headerText, content });
10046
+ }
10047
+ for (const topicId of topicIds) {
10048
+ const hashIdx = topicId.indexOf("#");
10049
+ if (hashIdx === -1) continue;
10050
+ const key = topicId.substring(0, hashIdx);
10051
+ const shortId = topicId.substring(hashIdx + 1);
10052
+ const list = RAHMENRICHTLINIEN_ST_MANIFEST.topics[key] ?? [];
10053
+ const match = list.find((t2) => t2.id === shortId);
10054
+ if (!match) continue;
10055
+ const label = match.label;
10056
+ const normalizedLabel = normalizeForComparison15(label);
10057
+ let section = sections.find(
10058
+ (s) => normalizeForComparison15(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
10059
+ );
10060
+ if (!section && sections.length > 0) {
10061
+ section = sections.find((s) => s.content.length > 80) || sections[0];
10062
+ }
10063
+ if (section) {
10064
+ results[topicId] = `${label}
10065
+
10066
+ ${section.content}`.trim();
10067
+ } else {
10068
+ results[topicId] = label;
10069
+ }
10070
+ }
10071
+ return results;
10072
+ }
10073
+ };
10074
+ function cleanHtmlText15(html) {
10075
+ let text = html.replace(
10076
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
10077
+ " "
10078
+ );
10079
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
10080
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
10081
+ text = text.replace(/<p[^>]*>/gi, "\n");
10082
+ text = text.replace(/<br\s*\/?>/gi, "\n");
10083
+ text = text.replace(/<[^>]+>/g, " ");
10084
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
10085
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
10086
+ }
10087
+ function normalizeForComparison15(str) {
10088
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
10089
+ }
10090
+
10091
+ // src/cli/curriculum/registry.ts
10092
+ var CURRICULUM_PROVIDERS = [
10093
+ lehrplanplusBayernProvider,
10094
+ bildungsplanBwProvider,
10095
+ kernlehrplanNrwProvider,
10096
+ kerncurriculumHessenProvider,
10097
+ kerncurriculumNiedersachsenProvider,
10098
+ lehrplanSachsenProvider,
10099
+ rahmenlehrplanBerlinBrandenburgProvider,
10100
+ bildungsplanHamburgProvider,
10101
+ bildungsplanBremenProvider,
10102
+ rahmenplanMvProvider,
10103
+ lehrplaeneRpProvider,
10104
+ lehrplanSaarlandProvider,
10105
+ rahmenrichtlinienStProvider,
10106
+ fachanforderungenShProvider,
10107
+ lehrplanThueringenProvider
10108
+ ];
10109
+ function getCurriculumProvider(id) {
10110
+ return CURRICULUM_PROVIDERS.find((provider) => provider.id === id);
10111
+ }
10112
+
10113
+ // src/cli/llm/vision.ts
10114
+ import { randomBytes } from "crypto";
10115
+ import { readFileSync as readFileSync10 } from "fs";
10116
+ import { tmpdir as tmpdir2 } from "os";
10117
+ import { basename as basename3, join as join14 } from "path";
10118
+ var LANGUAGE_NAMES2 = {
10119
+ en: "English",
10120
+ de: "German",
10121
+ es: "Spanish",
10122
+ fr: "French",
10123
+ pt: "Portuguese",
10124
+ zh: "Chinese",
10125
+ ja: "Japanese"
10126
+ };
10127
+ var OBSERVATION_KINDS2 = /* @__PURE__ */ new Set([
10128
+ "progress",
10129
+ "step-completed",
10130
+ "error",
10131
+ "help-seeking",
10132
+ "uncertain",
10133
+ "privacy-pause",
10134
+ "heartbeat"
10135
+ ]);
10136
+ var ACTION_TYPES2 = /* @__PURE__ */ new Set([
10137
+ "click",
10138
+ "shortcut",
10139
+ "typing",
10140
+ "scroll",
10141
+ "window-change"
10142
+ ]);
10143
+ async function observeUiSnapshotViaLLM(db, input8) {
10144
+ const cfg = await getProviderForRole(db, "vision");
10145
+ if (!cfg.enabled) {
10146
+ throw new Error(
10147
+ "Vision observation is disabled in settings (llm.vision.enabled)"
10148
+ );
10149
+ }
10150
+ const imageUrls = [];
10151
+ const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
10152
+ if (isVideo) {
10153
+ const { mkdirSync: mkdirSync14, readdirSync: readdirSync3, rmSync: rmSync4 } = await import("fs");
10154
+ const { execSync: execSync6 } = await import("child_process");
10155
+ const tempDir = join14(
10156
+ tmpdir2(),
10157
+ `zam-frames-${randomBytes(4).toString("hex")}`
10158
+ );
10159
+ mkdirSync14(tempDir, { recursive: true });
10160
+ try {
10161
+ execSync6(
10162
+ `ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
10163
+ { stdio: "ignore" }
10164
+ );
10165
+ let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
10166
+ if (files.length === 0) {
10167
+ execSync6(
10168
+ `ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
10169
+ { stdio: "ignore" }
10170
+ );
10171
+ files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
10172
+ }
10173
+ const maxFrames = cfg.maxFrames ?? 100;
10174
+ let sampledFiles = files;
10175
+ if (files.length > maxFrames) {
10176
+ if (maxFrames <= 1) {
10177
+ sampledFiles = [files[0]];
10178
+ } else {
10179
+ const step = (files.length - 1) / (maxFrames - 1);
10180
+ sampledFiles = [];
10181
+ for (let i = 0; i < maxFrames; i++) {
10182
+ const index = Math.round(i * step);
10183
+ sampledFiles.push(files[index]);
10184
+ }
10185
+ }
10186
+ }
10187
+ for (const file of sampledFiles) {
10188
+ const bytes = readFileSync10(join14(tempDir, file));
10189
+ imageUrls.push(`data:image/png;base64,${bytes.toString("base64")}`);
10190
+ }
10191
+ } finally {
10192
+ try {
10193
+ rmSync4(tempDir, { recursive: true, force: true });
10194
+ } catch {
10195
+ }
10196
+ }
10197
+ } else {
10198
+ const imageBytes = readFileSync10(input8.imagePath);
10199
+ const ext = input8.imagePath.split(".").pop()?.toLowerCase();
10200
+ const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
10201
+ imageUrls.push(`data:${mime};base64,${imageBytes.toString("base64")}`);
10202
+ }
10203
+ if (imageUrls.length === 0) {
10204
+ throw new Error("No image data available for vision analysis");
10205
+ }
10206
+ const endpoints = [
10207
+ {
10208
+ url: cfg.url,
10209
+ apiKey: cfg.apiKey || DEFAULT_LLM_API_KEY,
10210
+ model: input8.model ?? cfg.model,
10211
+ apiFlavor: cfg.apiFlavor
10212
+ }
10213
+ ];
10214
+ if (cfg.fallback) {
10215
+ endpoints.push({
10216
+ url: cfg.fallback.url,
10217
+ apiKey: cfg.fallback.apiKey || DEFAULT_LLM_API_KEY,
10218
+ model: cfg.fallback.model,
10219
+ apiFlavor: cfg.fallback.apiFlavor
10220
+ });
7380
10221
  }
7381
10222
  let lastRequestError;
7382
10223
  let sawUnparseableDraft = false;
@@ -10555,8 +13396,22 @@ bridgeCommand.command("personal-card-confirm-foundations").description("Save con
10555
13396
  });
10556
13397
  bridgeCommand.command("personal-source-import").description(
10557
13398
  "Fetch and clean plain text from local file, web link, or vision scan (JSON)"
10558
- ).requiredOption("--type <file|web|scan>", "Source type").requiredOption("--uri <uri>", "Source path or URL").action(async (opts) => {
13399
+ ).requiredOption("--type <file|web|scan>", "Source type").requiredOption("--uri <uri>", "Source path or URL").option("--refresh", "Re-fetch an already cached web source").action(async (opts) => {
10559
13400
  await withDb2(async (db) => {
13401
+ if (opts.type === "web" && !opts.refresh) {
13402
+ const cached = await db.prepare(
13403
+ "SELECT id, content FROM sources WHERE uri = ? AND type = 'web'"
13404
+ ).get(opts.uri);
13405
+ if (cached?.content) {
13406
+ jsonOut2({
13407
+ success: true,
13408
+ sourceId: cached.id,
13409
+ content: cached.content,
13410
+ cached: true
13411
+ });
13412
+ return;
13413
+ }
13414
+ }
10560
13415
  let content = "";
10561
13416
  if (opts.type === "file") {
10562
13417
  content = await readLocalFile(opts.uri);
@@ -10579,7 +13434,8 @@ bridgeCommand.command("personal-source-import").description(
10579
13434
  jsonOut2({
10580
13435
  success: true,
10581
13436
  sourceId: record.id,
10582
- content: record.content
13437
+ content: record.content,
13438
+ cached: false
10583
13439
  });
10584
13440
  });
10585
13441
  });
@@ -10605,6 +13461,205 @@ bridgeCommand.command("personal-source-confirm-import").description(
10605
13461
  });
10606
13462
  });
10607
13463
  });
13464
+ bridgeCommand.command("curriculum-list-providers").description("List registered curriculum providers (JSON)").action(() => {
13465
+ jsonOut2({
13466
+ success: true,
13467
+ providers: CURRICULUM_PROVIDERS.map((provider) => ({
13468
+ id: provider.id,
13469
+ country: provider.country,
13470
+ countryLabel: provider.countryLabel,
13471
+ region: provider.region,
13472
+ regionLabel: provider.regionLabel,
13473
+ label: provider.label
13474
+ }))
13475
+ });
13476
+ });
13477
+ var CURRICULUM_LEVELS = [
13478
+ "schoolType",
13479
+ "grade",
13480
+ "subject",
13481
+ "track",
13482
+ "topic"
13483
+ ];
13484
+ bridgeCommand.command("curriculum-list-level").description("List taxonomy options for the next import-wizard step (JSON)").requiredOption("--provider <id>", "Curriculum provider id").requiredOption(
13485
+ "--level <level>",
13486
+ `Level to list: ${CURRICULUM_LEVELS.join("|")}`
13487
+ ).option("--selection <json>", "JSON selection made so far", "{}").action((opts) => {
13488
+ const provider = getCurriculumProvider(opts.provider);
13489
+ if (!provider) jsonError(`Unknown curriculum provider: ${opts.provider}`);
13490
+ if (!CURRICULUM_LEVELS.includes(opts.level)) {
13491
+ jsonError(
13492
+ `Invalid --level: ${opts.level}. Use one of ${CURRICULUM_LEVELS.join(", ")}.`
13493
+ );
13494
+ }
13495
+ let selection;
13496
+ try {
13497
+ selection = JSON.parse(opts.selection);
13498
+ } catch {
13499
+ jsonError("Invalid --selection JSON");
13500
+ return;
13501
+ }
13502
+ const level = opts.level;
13503
+ if (level === "schoolType") {
13504
+ jsonOut2({ success: true, options: provider.listSchoolTypes() });
13505
+ return;
13506
+ }
13507
+ if (!selection.schoolType) jsonError("selection.schoolType is required");
13508
+ if (level === "grade") {
13509
+ jsonOut2({
13510
+ success: true,
13511
+ options: provider.listGrades(selection.schoolType)
13512
+ });
13513
+ return;
13514
+ }
13515
+ if (!selection.grade) jsonError("selection.grade is required");
13516
+ if (level === "subject") {
13517
+ jsonOut2({
13518
+ success: true,
13519
+ options: provider.listSubjects(selection.schoolType, selection.grade)
13520
+ });
13521
+ return;
13522
+ }
13523
+ if (!selection.subject) jsonError("selection.subject is required");
13524
+ if (level === "track") {
13525
+ jsonOut2({
13526
+ success: true,
13527
+ options: provider.listTracks(
13528
+ selection.schoolType,
13529
+ selection.grade,
13530
+ selection.subject
13531
+ )
13532
+ });
13533
+ return;
13534
+ }
13535
+ jsonOut2({ success: true, options: provider.listTopics(selection) });
13536
+ });
13537
+ bridgeCommand.command("curriculum-resolve-topics").description("Resolve selected curriculum topics to source URLs (JSON)").requiredOption("--provider <id>", "Curriculum provider id").requiredOption("--topics <json>", "JSON array of topic nodes to resolve").action((opts) => {
13538
+ const provider = getCurriculumProvider(opts.provider);
13539
+ if (!provider) jsonError(`Unknown curriculum provider: ${opts.provider}`);
13540
+ let topics;
13541
+ try {
13542
+ topics = JSON.parse(opts.topics);
13543
+ } catch {
13544
+ jsonError("Invalid --topics JSON");
13545
+ return;
13546
+ }
13547
+ const resolved = topics.map((topic) => provider.resolveTopic(topic));
13548
+ jsonOut2({ success: true, resolved });
13549
+ });
13550
+ async function fetchRawHtml(url) {
13551
+ if (!await isSafeUrl(url)) {
13552
+ throw new Error(`Access denied to unsafe target URL: ${url}`);
13553
+ }
13554
+ const controller = new AbortController();
13555
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
13556
+ try {
13557
+ const res = await fetch(url, {
13558
+ signal: controller.signal,
13559
+ headers: {
13560
+ "User-Agent": "ZAM-Content-Studio/0.6.3"
13561
+ }
13562
+ });
13563
+ if (!res.ok) {
13564
+ throw new Error(`Web server responded with status ${res.status}`);
13565
+ }
13566
+ const contentType = res.headers.get("content-type") || "";
13567
+ if (!contentType.includes("text/html") && !contentType.includes("application/xhtml+xml")) {
13568
+ throw new Error(`Unsupported content type: ${contentType}`);
13569
+ }
13570
+ return await res.text();
13571
+ } catch (err) {
13572
+ if (err instanceof Error && err.name === "AbortError") {
13573
+ throw new Error("Connection request timed out after 10 seconds");
13574
+ }
13575
+ throw err;
13576
+ } finally {
13577
+ clearTimeout(timeoutId);
13578
+ }
13579
+ }
13580
+ bridgeCommand.command("curriculum-extract-topics").description(
13581
+ "Fetch and extract specific texts for selected curriculum topics (JSON)"
13582
+ ).requiredOption("--provider <id>", "Curriculum provider id").requiredOption("--topics <json>", "JSON array of selected topic nodes").action(async (opts) => {
13583
+ await withDb2(async (db) => {
13584
+ const provider = getCurriculumProvider(opts.provider);
13585
+ if (!provider) jsonError(`Unknown curriculum provider: ${opts.provider}`);
13586
+ let topics;
13587
+ try {
13588
+ topics = JSON.parse(opts.topics);
13589
+ } catch {
13590
+ jsonError("Invalid --topics JSON");
13591
+ return;
13592
+ }
13593
+ const topicsByUri = /* @__PURE__ */ new Map();
13594
+ for (const topic of topics) {
13595
+ const resolved = provider.resolveTopic(topic);
13596
+ const list = topicsByUri.get(resolved.uri) || [];
13597
+ list.push(topic);
13598
+ topicsByUri.set(resolved.uri, list);
13599
+ }
13600
+ const extracted = [];
13601
+ for (const [uri, uriTopics] of topicsByUri.entries()) {
13602
+ const rawHtml = await fetchRawHtml(uri);
13603
+ let extractedTexts = {};
13604
+ if (provider.extractTopics) {
13605
+ const fullTopicIds = uriTopics.map((t2) => `${t2.sourceRef}#${t2.id}`);
13606
+ extractedTexts = provider.extractTopics(rawHtml, fullTopicIds);
13607
+ } else {
13608
+ const cleanText = cleanHtml(rawHtml);
13609
+ for (const t2 of uriTopics) {
13610
+ extractedTexts[`${t2.sourceRef}#${t2.id}`] = cleanText;
13611
+ }
13612
+ }
13613
+ const pageCleanedText = cleanHtml(rawHtml);
13614
+ const sourceId = ulid8();
13615
+ await db.prepare(
13616
+ `INSERT INTO sources (id, type, uri, content)
13617
+ VALUES (?, 'web', ?, ?)
13618
+ ON CONFLICT(uri) DO UPDATE SET
13619
+ type = excluded.type,
13620
+ content = excluded.content`
13621
+ ).run(sourceId, uri, pageCleanedText);
13622
+ const record = await db.prepare("SELECT id FROM sources WHERE uri = ?").get(uri);
13623
+ for (const topic of uriTopics) {
13624
+ const resolved = provider.resolveTopic(topic);
13625
+ const text = extractedTexts[resolved.topicId] || "";
13626
+ extracted.push({
13627
+ topicId: resolved.topicId,
13628
+ uri,
13629
+ sourceId: record.id,
13630
+ text
13631
+ });
13632
+ }
13633
+ }
13634
+ jsonOut2({ success: true, extracted });
13635
+ });
13636
+ });
13637
+ bridgeCommand.command("curriculum-get-last-selection").description("Read the learner's last navigated curriculum path (JSON)").action(async () => {
13638
+ await withDb2(async (db) => {
13639
+ const breadcrumb = await getLastCurriculumSelection(db);
13640
+ jsonOut2({ success: true, breadcrumb: breadcrumb ?? null });
13641
+ });
13642
+ });
13643
+ bridgeCommand.command("curriculum-set-last-selection").description("Persist the learner's last navigated curriculum path (JSON)").requiredOption(
13644
+ "--breadcrumb <json>",
13645
+ "JSON breadcrumb: {providerId, schoolType?, grade?, subject?, track?}"
13646
+ ).action(async (opts) => {
13647
+ await withDb2(async (db) => {
13648
+ let breadcrumb;
13649
+ try {
13650
+ breadcrumb = JSON.parse(opts.breadcrumb);
13651
+ } catch {
13652
+ jsonError("Invalid --breadcrumb JSON");
13653
+ return;
13654
+ }
13655
+ if (!breadcrumb.providerId) {
13656
+ jsonError("breadcrumb.providerId is required");
13657
+ return;
13658
+ }
13659
+ await setLastCurriculumSelection(db, breadcrumb);
13660
+ jsonOut2({ success: true });
13661
+ });
13662
+ });
10608
13663
  bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/stdout server").option("--stdin", "Use stdin/stdout for communication").action(async (_opts) => {
10609
13664
  isServeMode = true;
10610
13665
  const {