zam-core 0.6.2 → 0.7.0

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
@@ -656,6 +656,18 @@ CREATE TABLE IF NOT EXISTS user_config (
656
656
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
657
657
  );
658
658
 
659
+ -- Token embeddings: one vector per token for semantic search (ADR 2026-07-03).
660
+ -- Not a column on tokens: hot paths do SELECT * FROM tokens and a ~3KB blob
661
+ -- per row would ride along on all of them.
662
+ CREATE TABLE IF NOT EXISTS token_embeddings (
663
+ token_id TEXT PRIMARY KEY REFERENCES tokens(id) ON DELETE CASCADE,
664
+ embedding BLOB NOT NULL,
665
+ model TEXT NOT NULL,
666
+ dims INTEGER NOT NULL,
667
+ content_hash TEXT NOT NULL,
668
+ embedded_at TEXT NOT NULL DEFAULT (datetime('now'))
669
+ );
670
+
659
671
  -- Agent skills: task recipes the agent learns from user guidance
660
672
  CREATE TABLE IF NOT EXISTS agent_skills (
661
673
  id TEXT PRIMARY KEY,
@@ -1002,6 +1014,16 @@ async function runMigrations(db) {
1002
1014
  await db.exec(`ALTER TABLE tokens ADD COLUMN topic_id TEXT`);
1003
1015
  }
1004
1016
  }
1017
+ await db.exec(`
1018
+ CREATE TABLE IF NOT EXISTS token_embeddings (
1019
+ token_id TEXT PRIMARY KEY REFERENCES tokens(id) ON DELETE CASCADE,
1020
+ embedding BLOB NOT NULL,
1021
+ model TEXT NOT NULL,
1022
+ dims INTEGER NOT NULL,
1023
+ content_hash TEXT NOT NULL,
1024
+ embedded_at TEXT NOT NULL DEFAULT (datetime('now'))
1025
+ )
1026
+ `);
1005
1027
  }
1006
1028
 
1007
1029
  // src/kernel/db/snapshot.ts
@@ -2410,6 +2432,151 @@ async function deleteSetting(db, key) {
2410
2432
  return result.changes > 0;
2411
2433
  }
2412
2434
 
2435
+ // src/kernel/models/token-embedding.ts
2436
+ import { createHash as createHash2 } from "crypto";
2437
+ function embeddingContentForToken(t2) {
2438
+ return `${t2.concept}
2439
+ ${t2.question ?? ""}
2440
+ ${t2.domain}`;
2441
+ }
2442
+ function computeContentHash(text) {
2443
+ return createHash2("sha256").update(text, "utf8").digest("hex");
2444
+ }
2445
+ function encodeEmbedding(vec) {
2446
+ const f = Float32Array.from(vec);
2447
+ return new Uint8Array(f.buffer);
2448
+ }
2449
+ function decodeEmbedding(blob) {
2450
+ if (blob.byteLength % 4 !== 0) {
2451
+ throw new Error(
2452
+ "Invalid embedding blob size: must be a multiple of 4 bytes"
2453
+ );
2454
+ }
2455
+ if (blob.byteOffset % 4 === 0) {
2456
+ return new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);
2457
+ }
2458
+ const copy = Uint8Array.prototype.slice.call(blob);
2459
+ return new Float32Array(copy.buffer, 0, copy.byteLength / 4);
2460
+ }
2461
+ async function upsertTokenEmbedding(db, input8) {
2462
+ const encoded = encodeEmbedding(input8.embedding);
2463
+ const dims = input8.embedding.length;
2464
+ const embeddedAt = (/* @__PURE__ */ new Date()).toISOString();
2465
+ await db.prepare(`
2466
+ INSERT INTO token_embeddings (token_id, embedding, model, dims, content_hash, embedded_at)
2467
+ VALUES (?, ?, ?, ?, ?, ?)
2468
+ ON CONFLICT(token_id) DO UPDATE SET
2469
+ embedding = excluded.embedding,
2470
+ model = excluded.model,
2471
+ dims = excluded.dims,
2472
+ content_hash = excluded.content_hash,
2473
+ embedded_at = excluded.embedded_at
2474
+ `).run(
2475
+ input8.tokenId,
2476
+ encoded,
2477
+ input8.model,
2478
+ dims,
2479
+ input8.contentHash,
2480
+ embeddedAt
2481
+ );
2482
+ }
2483
+ async function listTokensNeedingEmbedding(db, model, opts) {
2484
+ const rows = await db.prepare(`
2485
+ SELECT t.*, e.model AS emb_model, e.dims AS emb_dims, e.content_hash AS emb_hash
2486
+ FROM tokens t
2487
+ LEFT JOIN token_embeddings e ON e.token_id = t.id
2488
+ WHERE t.deprecated_at IS NULL
2489
+ `).all();
2490
+ const needing = [];
2491
+ for (const row of rows) {
2492
+ let reason = null;
2493
+ let text = "";
2494
+ if (opts?.force) {
2495
+ reason = "content-changed";
2496
+ } else if (row.emb_model === null) {
2497
+ reason = "missing";
2498
+ } else if (row.emb_model !== model) {
2499
+ reason = "model-changed";
2500
+ } else if (opts?.dims !== void 0 && row.emb_dims !== opts.dims) {
2501
+ reason = "dimension-changed";
2502
+ } else {
2503
+ const computedText = embeddingContentForToken(row);
2504
+ const hash = computeContentHash(computedText);
2505
+ if (row.emb_hash !== hash) {
2506
+ reason = "content-changed";
2507
+ text = computedText;
2508
+ }
2509
+ }
2510
+ if (reason) {
2511
+ if (!text) {
2512
+ text = embeddingContentForToken(row);
2513
+ }
2514
+ needing.push({ token: row, text, reason });
2515
+ }
2516
+ }
2517
+ if (opts?.limit !== void 0) {
2518
+ return needing.slice(0, opts.limit);
2519
+ }
2520
+ return needing;
2521
+ }
2522
+ async function getEmbeddingCoverage(db, model, opts) {
2523
+ const rows = await db.prepare(`
2524
+ SELECT t.*, e.model AS emb_model, e.dims AS emb_dims, e.content_hash AS emb_hash
2525
+ FROM tokens t
2526
+ LEFT JOIN token_embeddings e ON e.token_id = t.id
2527
+ WHERE t.deprecated_at IS NULL
2528
+ `).all();
2529
+ let missing = 0;
2530
+ let stale = 0;
2531
+ for (const row of rows) {
2532
+ if (row.emb_model === null) {
2533
+ missing++;
2534
+ continue;
2535
+ }
2536
+ if (row.emb_model !== model) {
2537
+ stale++;
2538
+ continue;
2539
+ }
2540
+ if (opts?.dims !== void 0 && row.emb_dims !== opts.dims) {
2541
+ stale++;
2542
+ continue;
2543
+ }
2544
+ const hash = computeContentHash(embeddingContentForToken(row));
2545
+ if (row.emb_hash !== hash) {
2546
+ stale++;
2547
+ }
2548
+ }
2549
+ const tokens = rows.length;
2550
+ return {
2551
+ tokens,
2552
+ embedded: tokens - missing - stale,
2553
+ missing,
2554
+ stale
2555
+ };
2556
+ }
2557
+ async function listEmbeddedTokens(db, model) {
2558
+ const rows = await db.prepare(`
2559
+ SELECT t.*, e.embedding AS embedding, e.content_hash AS emb_hash
2560
+ FROM token_embeddings e
2561
+ JOIN tokens t ON t.id = e.token_id
2562
+ WHERE e.model = ? AND t.deprecated_at IS NULL
2563
+ `).all(model);
2564
+ return rows.flatMap((row) => {
2565
+ const { embedding, emb_hash, ...token } = row;
2566
+ if (emb_hash !== computeContentHash(embeddingContentForToken(token))) {
2567
+ return [];
2568
+ }
2569
+ try {
2570
+ return [{ token, embedding: decodeEmbedding(embedding) }];
2571
+ } catch (err) {
2572
+ console.warn(
2573
+ `Warning: Corrupted embedding for token ${token.slug} (${token.id}) ignored: ${err.message}`
2574
+ );
2575
+ return [];
2576
+ }
2577
+ });
2578
+ }
2579
+
2413
2580
  // src/kernel/observation/analyzer.ts
2414
2581
  function parseMonitorLog(jsonl) {
2415
2582
  const events = [];
@@ -4546,6 +4713,82 @@ function intersperseNew(reviews, newCards, interval) {
4546
4713
  return result;
4547
4714
  }
4548
4715
 
4716
+ // src/kernel/search/hybrid.ts
4717
+ function cosineSimilarity(a, b) {
4718
+ if (a.length !== b.length) return 0;
4719
+ let dot = 0;
4720
+ let normA = 0;
4721
+ let normB = 0;
4722
+ const len = a.length;
4723
+ for (let i = 0; i < len; i++) {
4724
+ dot += a[i] * b[i];
4725
+ normA += a[i] * a[i];
4726
+ normB += b[i] * b[i];
4727
+ }
4728
+ if (normA === 0 || normB === 0) return 0;
4729
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
4730
+ }
4731
+ async function searchTokensHybrid(db, query, opts) {
4732
+ const limit = opts?.limit ?? 20;
4733
+ const rrfK = opts?.rrfK ?? 60;
4734
+ const vectorTopK = opts?.vectorTopK ?? 10;
4735
+ const lexicalHits = await findTokens(db, query);
4736
+ let vectorHits = [];
4737
+ if (opts?.queryEmbedding && opts?.model) {
4738
+ const queryVec = Float32Array.from(opts.queryEmbedding);
4739
+ const embedded = await listEmbeddedTokens(db, opts.model);
4740
+ const candidates = [];
4741
+ for (const row of embedded) {
4742
+ if (row.embedding.length !== queryVec.length) {
4743
+ continue;
4744
+ }
4745
+ const similarity = cosineSimilarity(queryVec, row.embedding);
4746
+ if (similarity <= 0) {
4747
+ continue;
4748
+ }
4749
+ candidates.push({ token: row.token, similarity });
4750
+ }
4751
+ candidates.sort((a, b) => b.similarity - a.similarity);
4752
+ vectorHits = candidates.slice(0, vectorTopK);
4753
+ }
4754
+ const tokenMap = /* @__PURE__ */ new Map();
4755
+ const getOrCreateEntry = (token) => {
4756
+ let entry = tokenMap.get(token.id);
4757
+ if (!entry) {
4758
+ entry = {
4759
+ ...token,
4760
+ score: 0,
4761
+ lexicalRank: null,
4762
+ vectorRank: null,
4763
+ similarity: null
4764
+ };
4765
+ tokenMap.set(token.id, entry);
4766
+ }
4767
+ return entry;
4768
+ };
4769
+ for (let i = 0; i < lexicalHits.length; i++) {
4770
+ const hit = lexicalHits[i];
4771
+ const entry = getOrCreateEntry(hit);
4772
+ entry.lexicalRank = i + 1;
4773
+ entry.score += 1 / (rrfK + entry.lexicalRank);
4774
+ }
4775
+ for (let j = 0; j < vectorHits.length; j++) {
4776
+ const hit = vectorHits[j];
4777
+ const entry = getOrCreateEntry(hit.token);
4778
+ entry.vectorRank = j + 1;
4779
+ entry.similarity = hit.similarity;
4780
+ entry.score += 1 / (rrfK + entry.vectorRank);
4781
+ }
4782
+ const results = Array.from(tokenMap.values());
4783
+ results.sort((a, b) => {
4784
+ if (Math.abs(a.score - b.score) > 1e-9) {
4785
+ return b.score - a.score;
4786
+ }
4787
+ return a.slug.localeCompare(b.slug);
4788
+ });
4789
+ return results.slice(0, limit);
4790
+ }
4791
+
4549
4792
  // src/kernel/system/hooks.ts
4550
4793
  import {
4551
4794
  appendFileSync as appendFileSync3,
@@ -6092,6 +6335,21 @@ async function getLegacyRoleConfig(db, role, enabled) {
6092
6335
  maxFrames: Number.isNaN(parsed) ? 100 : parsed
6093
6336
  };
6094
6337
  }
6338
+ if (role === "embedding") {
6339
+ const url = await getSetting(db, "llm.embedding.url") || base.url;
6340
+ return {
6341
+ enabled,
6342
+ url,
6343
+ // Kept as a literal (not imported) to avoid a client.ts <-> embedder.ts
6344
+ // import cycle; must match DEFAULT_EMBEDDING_MODEL in embedder.ts.
6345
+ model: await getSetting(db, "llm.embedding.model") || "embeddinggemma",
6346
+ apiKey: await getSetting(db, "llm.embedding.api_key") || base.apiKey,
6347
+ apiFlavor: inferApiFlavor(url),
6348
+ locale: base.locale,
6349
+ source: "legacy",
6350
+ local: isLocalEndpoint(url)
6351
+ };
6352
+ }
6095
6353
  return {
6096
6354
  enabled,
6097
6355
  url: base.url,
@@ -7386,7 +7644,7 @@ async function readWebLink(url) {
7386
7644
  redirect: "manual",
7387
7645
  signal: controller.signal,
7388
7646
  headers: {
7389
- "User-Agent": "ZAM-Content-Studio/0.6.1"
7647
+ "User-Agent": "ZAM-Content-Studio/0.6.3"
7390
7648
  }
7391
7649
  });
7392
7650
  if (res.status >= 300 && res.status < 400) {
@@ -7464,23 +7722,184 @@ async function setLastCurriculumSelection(db, breadcrumb) {
7464
7722
  await setSetting(db, LAST_SELECTION_KEY, JSON.stringify(breadcrumb));
7465
7723
  }
7466
7724
 
7725
+ // src/cli/curriculum/providers/bildungsplan-bremen/manifest.ts
7726
+ var BILDUNGSPLAN_BREMEN_MANIFEST = {
7727
+ schoolYear: "2025/2026",
7728
+ capturedOn: "2026-07-02",
7729
+ sourceRevision: "Bildungspl\xE4ne Bremen",
7730
+ schoolTypes: [
7731
+ { id: "oberschule", label: "Oberschule" },
7732
+ { id: "gymnasium", label: "Gymnasium" }
7733
+ ],
7734
+ grades: {
7735
+ oberschule: ["7", "8", "9", "10"],
7736
+ gymnasium: ["7", "8", "9", "10"]
7737
+ },
7738
+ subjects: {
7739
+ oberschule: [
7740
+ { id: "mathematik", label: "Mathematik" },
7741
+ { id: "informatik", label: "Informatik" },
7742
+ { id: "physik", label: "Physik" },
7743
+ { id: "chemie", label: "Chemie" },
7744
+ { id: "biologie", label: "Biologie" }
7745
+ ],
7746
+ gymnasium: [
7747
+ { id: "mathematik", label: "Mathematik" },
7748
+ { id: "informatik", label: "Informatik" },
7749
+ { id: "physik", label: "Physik" },
7750
+ { id: "chemie", label: "Chemie" },
7751
+ { id: "biologie", label: "Biologie" }
7752
+ ]
7753
+ },
7754
+ tracks: {},
7755
+ topics: {
7756
+ "oberschule|10|mathematik": [
7757
+ { id: "funktionen", label: "Funktionen" },
7758
+ { id: "geometrie", label: "Geometrie" }
7759
+ ],
7760
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
7761
+ "oberschule|9|informatik": [{ id: "algorithmen", label: "Algorithmen" }],
7762
+ "oberschule|9|physik": [{ id: "mechanik", label: "Mechanik" }],
7763
+ "oberschule|9|chemie": [
7764
+ { id: "reaktionen", label: "Chemische Reaktionen" }
7765
+ ],
7766
+ "oberschule|9|biologie": [{ id: "zellen", label: "Zellen" }],
7767
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
7768
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Chemische Bindungen" }],
7769
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
7770
+ },
7771
+ contentUrls: {
7772
+ "oberschule|10|mathematik": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7773
+ "gymnasium|10|mathematik": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7774
+ "oberschule|9|informatik": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7775
+ "oberschule|9|physik": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7776
+ "oberschule|9|chemie": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7777
+ "oberschule|9|biologie": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7778
+ "gymnasium|9|physik": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7779
+ "gymnasium|9|chemie": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942",
7780
+ "gymnasium|9|biologie": "https://www.lis.bremen.de/schulqualitaet/bildungsplaene-21942"
7781
+ }
7782
+ };
7783
+
7784
+ // src/cli/curriculum/providers/bildungsplan-bremen/index.ts
7785
+ var bildungsplanBremenProvider = {
7786
+ id: "bildungsplan-bremen",
7787
+ country: "DE",
7788
+ countryLabel: "Deutschland",
7789
+ region: "HB",
7790
+ regionLabel: "Bremen",
7791
+ label: "Bildungsplan (Bremen)",
7792
+ listSchoolTypes() {
7793
+ return BILDUNGSPLAN_BREMEN_MANIFEST.schoolTypes;
7794
+ },
7795
+ listGrades(schoolType) {
7796
+ return (BILDUNGSPLAN_BREMEN_MANIFEST.grades[schoolType] || []).map((id) => ({
7797
+ id,
7798
+ label: `Klasse ${id}`
7799
+ }));
7800
+ },
7801
+ listSubjects(schoolType, _grade) {
7802
+ return BILDUNGSPLAN_BREMEN_MANIFEST.subjects[schoolType] || [];
7803
+ },
7804
+ listTracks(schoolType, grade, subject) {
7805
+ const key = `${schoolType}|${grade}|${subject}`;
7806
+ return BILDUNGSPLAN_BREMEN_MANIFEST.tracks[key] || [];
7807
+ },
7808
+ listTopics(selection) {
7809
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
7810
+ const list = BILDUNGSPLAN_BREMEN_MANIFEST.topics[key] || [];
7811
+ return list.map((t2) => ({
7812
+ ...t2,
7813
+ sourceRef: key
7814
+ }));
7815
+ },
7816
+ resolveTopic(topic) {
7817
+ const uri = BILDUNGSPLAN_BREMEN_MANIFEST.contentUrls[topic.sourceRef];
7818
+ if (!uri) {
7819
+ throw new Error(
7820
+ `Bildungsplan Bremen: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
7821
+ );
7822
+ }
7823
+ return {
7824
+ provider: "bildungsplan-bremen",
7825
+ topicId: `${topic.sourceRef}#${topic.id}`,
7826
+ uri
7827
+ };
7828
+ },
7829
+ extractTopics(html, topicIds) {
7830
+ const results = {};
7831
+ const headingMatches = Array.from(
7832
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
7833
+ );
7834
+ const sections = [];
7835
+ for (const m of headingMatches) {
7836
+ const headerText = cleanHtmlText(m[1]).trim();
7837
+ const start = m.index + m[0].length;
7838
+ const nextHeading = html.indexOf("<h", start);
7839
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
7840
+ const content = cleanHtmlText(rawContent).slice(0, 1500);
7841
+ if (headerText) sections.push({ header: headerText, content });
7842
+ }
7843
+ for (const topicId of topicIds) {
7844
+ const hashIdx = topicId.indexOf("#");
7845
+ if (hashIdx === -1) continue;
7846
+ const key = topicId.substring(0, hashIdx);
7847
+ const shortId = topicId.substring(hashIdx + 1);
7848
+ const list = BILDUNGSPLAN_BREMEN_MANIFEST.topics[key] ?? [];
7849
+ const match = list.find((t2) => t2.id === shortId);
7850
+ if (!match) continue;
7851
+ const label = match.label;
7852
+ const normalizedLabel = normalizeForComparison(label);
7853
+ let section = sections.find(
7854
+ (s) => normalizeForComparison(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
7855
+ );
7856
+ if (!section && sections.length > 0) {
7857
+ section = sections.find((s) => s.content.length > 80) || sections[0];
7858
+ }
7859
+ if (section) {
7860
+ results[topicId] = `${label}
7861
+
7862
+ ${section.content}`.trim();
7863
+ } else {
7864
+ results[topicId] = label;
7865
+ }
7866
+ }
7867
+ return results;
7868
+ }
7869
+ };
7870
+ function cleanHtmlText(html) {
7871
+ let text = html.replace(
7872
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
7873
+ " "
7874
+ );
7875
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
7876
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
7877
+ text = text.replace(/<p[^>]*>/gi, "\n");
7878
+ text = text.replace(/<br\s*\/?>/gi, "\n");
7879
+ text = text.replace(/<[^>]+>/g, " ");
7880
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
7881
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
7882
+ }
7883
+ function normalizeForComparison(str) {
7884
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
7885
+ }
7886
+
7467
7887
  // src/cli/curriculum/providers/bildungsplan-bw/manifest.ts
7468
7888
  var BILDUNGSPLAN_BW_MANIFEST = {
7469
7889
  schoolYear: "2026/2027",
7470
7890
  capturedOn: "2026-07-02",
7471
7891
  sourceRevision: "Bildungsplan Baden-W\xFCrttemberg Gymnasium 2016",
7472
- schoolTypes: [
7473
- { id: "gymnasium", label: "Gymnasium" },
7474
- { id: "realschule", label: "Realschule" }
7475
- ],
7892
+ schoolTypes: [{ id: "gymnasium", label: "Gymnasium" }],
7476
7893
  grades: {
7477
7894
  gymnasium: ["9", "10", "11", "12"]
7478
7895
  },
7479
7896
  subjects: {
7480
7897
  gymnasium: [
7481
7898
  { id: "mathematik", label: "Mathematik" },
7899
+ { id: "informatik", label: "Informatik" },
7482
7900
  { id: "physik", label: "Physik" },
7483
- { id: "englisch", label: "Englisch" }
7901
+ { id: "chemie", label: "Chemie" },
7902
+ { id: "biologie", label: "Biologie" }
7484
7903
  ]
7485
7904
  },
7486
7905
  tracks: {},
@@ -7491,10 +7910,44 @@ var BILDUNGSPLAN_BW_MANIFEST = {
7491
7910
  { id: "leitidee-raum", label: "Leitidee Raum und Form" },
7492
7911
  { id: "leitidee-funktion", label: "Leitidee Funktionaler Zusammenhang" },
7493
7912
  { id: "leitidee-daten", label: "Leitidee Daten und Zufall" }
7913
+ ],
7914
+ "gymnasium|10|informatik": [
7915
+ { id: "ik-daten-codierung", label: "Daten und Codierung" },
7916
+ { id: "ik-algorithmen", label: "Algorithmen" },
7917
+ { id: "ik-rechner-netze", label: "Rechner und Netze" },
7918
+ {
7919
+ id: "ik-informationsgesellschaft",
7920
+ label: "Informationsgesellschaft und Datensicherheit"
7921
+ }
7922
+ ],
7923
+ "gymnasium|10|physik": [
7924
+ { id: "optik-akustik", label: "Optik und Akustik" },
7925
+ { id: "mechanik", label: "Mechanik" },
7926
+ {
7927
+ id: "elektrizitaet-magnetismus",
7928
+ label: "Elektrizit\xE4t und Magnetismus"
7929
+ },
7930
+ { id: "energie-waerme", label: "Energie und W\xE4rmelehre" }
7931
+ ],
7932
+ "gymnasium|10|chemie": [
7933
+ { id: "stoffe-reaktionen", label: "Stoffe und chemische Reaktionen" },
7934
+ { id: "atome-molekuele", label: "Atome und Molek\xFCle" },
7935
+ { id: "saeuren-basen", label: "S\xE4uren, Basen und Salze" },
7936
+ { id: "organische-chemie", label: "Organische Chemie" }
7937
+ ],
7938
+ "gymnasium|10|biologie": [
7939
+ { id: "zellen", label: "Zellen und Gewebe" },
7940
+ { id: "genetik", label: "Genetik und Vererbung" },
7941
+ { id: "oekologie", label: "\xD6kologie und Umwelt" },
7942
+ { id: "evolution", label: "Evolution und Vielfalt" }
7494
7943
  ]
7495
7944
  },
7496
7945
  contentUrls: {
7497
- "gymnasium|10|mathematik": "http://www.bildungsplaene-bw.de/,Lde/LS/BP2016BW/ALLG/GYM/M/bp/klasse-10"
7946
+ "gymnasium|10|mathematik": "http://www.bildungsplaene-bw.de/,Lde/LS/BP2016BW/ALLG/GYM/M/bp/klasse-10",
7947
+ "gymnasium|10|informatik": "https://www.bildungsplaene-bw.de/,Lde/BP2016BW_ALLG_SEK1_INFWF",
7948
+ "gymnasium|10|physik": "https://www.bildungsplaene-bw.de/,Lde/BP2016BW_ALLG_GYM_PH",
7949
+ "gymnasium|10|chemie": "https://www.bildungsplaene-bw.de/,Lde/BP2016BW_ALLG_GYM_CH",
7950
+ "gymnasium|10|biologie": "https://www.bildungsplaene-bw.de/,Lde/BP2016BW_ALLG_GYM_BIO"
7498
7951
  }
7499
7952
  };
7500
7953
 
@@ -7555,7 +8008,7 @@ var bildungsplanBwProvider = {
7555
8008
  const headerMatch = contentHtml.match(
7556
8009
  /<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/i
7557
8010
  );
7558
- const headerText = headerMatch ? cleanHtmlText(headerMatch[1]).trim() : "";
8011
+ const headerText = headerMatch ? cleanHtmlText2(headerMatch[1]).trim() : "";
7559
8012
  sections.push({
7560
8013
  id,
7561
8014
  headerText,
@@ -7572,20 +8025,20 @@ var bildungsplanBwProvider = {
7572
8025
  const match = list.find((t2) => t2.id === shortId);
7573
8026
  if (!match) continue;
7574
8027
  const label = match.label;
7575
- const normalizedLabel = normalizeForComparison(label);
8028
+ const normalizedLabel = normalizeForComparison2(label);
7576
8029
  const sectionIndex = sections.findIndex((s) => {
7577
- const normalizedHeader = normalizeForComparison(s.headerText);
8030
+ const normalizedHeader = normalizeForComparison2(s.headerText);
7578
8031
  return normalizedHeader.includes(normalizedLabel) || s.id === shortId;
7579
8032
  });
7580
8033
  if (sectionIndex === -1) {
7581
8034
  continue;
7582
8035
  }
7583
- results[topicId] = cleanHtmlText(sections[sectionIndex].contentHtml);
8036
+ results[topicId] = cleanHtmlText2(sections[sectionIndex].contentHtml);
7584
8037
  }
7585
8038
  return results;
7586
8039
  }
7587
8040
  };
7588
- function cleanHtmlText(html) {
8041
+ function cleanHtmlText2(html) {
7589
8042
  let text = html.replace(
7590
8043
  /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
7591
8044
  " "
@@ -7598,238 +8051,2285 @@ function cleanHtmlText(html) {
7598
8051
  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");
7599
8052
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
7600
8053
  }
7601
- function normalizeForComparison(str) {
8054
+ function normalizeForComparison2(str) {
7602
8055
  return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
7603
8056
  }
7604
8057
 
7605
- // src/cli/curriculum/providers/lehrplanplus-bayern/manifest.ts
7606
- var LEHRPLANPLUS_BAYERN_MANIFEST = {
7607
- schoolYear: "2026/2027",
8058
+ // src/cli/curriculum/providers/bildungsplan-hamburg/manifest.ts
8059
+ var BILDUNGSPLAN_HAMBURG_MANIFEST = {
8060
+ schoolYear: "2025/2026",
7608
8061
  capturedOn: "2026-07-02",
7609
- sourceRevision: "LehrplanPLUS Realschule \u2013 Oktober 2023",
8062
+ sourceRevision: "Bildungspl\xE4ne Hamburg",
7610
8063
  schoolTypes: [
7611
- { id: "grundschule", label: "Grundschule" },
7612
- { id: "mittelschule", label: "Mittelschule" },
7613
- { id: "foerderschule", label: "F\xF6rderschule" },
7614
- { id: "realschule", label: "Realschule" },
7615
- { id: "gymnasium", label: "Gymnasium" },
7616
- { id: "wirtschaftsschule", label: "Wirtschaftsschule" },
7617
- { id: "fos", label: "Fachoberschule" },
7618
- { id: "bos", label: "Berufsoberschule" }
8064
+ { id: "stadtteilschule", label: "Stadtteilschule (Realschule)" },
8065
+ { id: "gymnasium", label: "Gymnasium" }
7619
8066
  ],
7620
8067
  grades: {
7621
- realschule: ["5", "6", "7", "8", "9", "10"]
8068
+ stadtteilschule: ["7", "8", "9", "10"],
8069
+ gymnasium: ["7", "8", "9", "10"]
7622
8070
  },
7623
8071
  subjects: {
7624
- realschule: [
7625
- {
7626
- id: "bwl-rechnungswesen",
7627
- label: "Betriebswirtschaftslehre / Rechnungswesen"
7628
- },
7629
- { id: "biologie", label: "Biologie" },
8072
+ stadtteilschule: [
8073
+ { id: "mathematik", label: "Mathematik" },
8074
+ { id: "informatik", label: "Informatik" },
8075
+ { id: "physik", label: "Physik" },
7630
8076
  { id: "chemie", label: "Chemie" },
7631
- { id: "deutsch", label: "Deutsch" },
7632
- { id: "englisch", label: "Englisch" },
7633
- { id: "ernaehrung_und_gesundheit", label: "Ern\xE4hrung und Gesundheit" },
7634
- { id: "ethik", label: "Ethik" },
7635
- {
7636
- id: "evangelische-religionslehre",
7637
- label: "Evangelische Religionslehre"
7638
- },
7639
- { id: "franzoesisch", label: "Franz\xF6sisch" },
7640
- { id: "geographie", label: "Geographie" },
7641
- { id: "geschichte", label: "Geschichte" },
7642
- { id: "it", label: "Informationstechnologie" },
7643
- { id: "iu", label: "Islamischer Unterricht" },
7644
- { id: "ir", label: "Israelitische Religionslehre" },
7645
- {
7646
- id: "katholische-religionslehre",
7647
- label: "Katholische Religionslehre"
7648
- },
7649
- { id: "kunst", label: "Kunst" },
8077
+ { id: "biologie", label: "Biologie" }
8078
+ ],
8079
+ gymnasium: [
7650
8080
  { id: "mathematik", label: "Mathematik" },
7651
- { id: "musik", label: "Musik" },
7652
- { id: "or", label: "Orthodoxe Religionslehre" },
8081
+ { id: "informatik", label: "Informatik" },
7653
8082
  { id: "physik", label: "Physik" },
7654
- { id: "pug", label: "Politik und Gesellschaft" },
7655
- { id: "soziallehre", label: "Soziallehre" },
7656
- { id: "sozialwesen", label: "Sozialwesen" },
7657
- { id: "spanisch", label: "Spanisch" },
7658
- { id: "sport", label: "Sport" },
7659
- { id: "textiles-gestalten", label: "Textiles Gestalten" },
7660
- { id: "werken", label: "Werken" },
7661
- { id: "wirtschaft-und-recht", label: "Wirtschaft und Recht" }
7662
- ]
7663
- },
7664
- tracks: {
7665
- "realschule|9|mathematik": [
7666
- { id: "wpfg1", label: "Mathematik 9 (I)" },
7667
- { id: "wpfg2-3", label: "Mathematik 9 (II/III)" }
8083
+ { id: "chemie", label: "Chemie" },
8084
+ { id: "biologie", label: "Biologie" }
7668
8085
  ]
7669
8086
  },
8087
+ tracks: {},
7670
8088
  topics: {
7671
- "realschule|9|mathematik|wpfg1": [
7672
- { id: "lb1", label: "Reelle Zahlen", hours: 10 },
7673
- { id: "lb2", label: "Zentrische Streckung", hours: 17 },
7674
- { id: "lb3", label: "Rechtwinklige Dreiecke", hours: 20 },
7675
- { id: "lb4", label: "Kreis", hours: 10 },
7676
- { id: "lb5", label: "Raumgeometrie", hours: 20 },
7677
- { id: "lb6", label: "Systeme linearer Gleichungen", hours: 12 },
7678
- {
7679
- id: "lb7",
7680
- label: "Quadratische Funktionen und quadratische Gleichungen",
7681
- hours: 42
7682
- },
7683
- { id: "lb8", label: "Daten und Zufall", hours: 9 }
8089
+ "stadtteilschule|10|mathematik": [
8090
+ { id: "funktionen", label: "Funktionen" },
8091
+ { id: "geometrie", label: "Geometrie" }
7684
8092
  ],
7685
- "realschule|9|mathematik|wpfg2-3": [
7686
- { id: "lb1", label: "Reelle Zahlen", hours: 7 },
7687
- { id: "lb2", label: "Zentrische Streckung", hours: 13 },
8093
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
8094
+ "stadtteilschule|9|physik": [{ id: "mechanik", label: "Mechanik" }],
8095
+ "stadtteilschule|9|chemie": [{ id: "reaktionen", label: "Reaktionen" }],
8096
+ "stadtteilschule|9|biologie": [{ id: "zellen", label: "Zellen" }],
8097
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
8098
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Bindungen" }],
8099
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
8100
+ },
8101
+ contentUrls: {
8102
+ "stadtteilschule|10|mathematik": "https://www.hamburg.de/bildungsplaene",
8103
+ "gymnasium|10|mathematik": "https://www.hamburg.de/bildungsplaene",
8104
+ "stadtteilschule|9|physik": "https://www.hamburg.de/bildungsplaene",
8105
+ "stadtteilschule|9|chemie": "https://www.hamburg.de/bildungsplaene",
8106
+ "stadtteilschule|9|biologie": "https://www.hamburg.de/bildungsplaene",
8107
+ "gymnasium|9|physik": "https://www.hamburg.de/bildungsplaene",
8108
+ "gymnasium|9|chemie": "https://www.hamburg.de/bildungsplaene",
8109
+ "gymnasium|9|biologie": "https://www.hamburg.de/bildungsplaene"
8110
+ }
8111
+ };
8112
+
8113
+ // src/cli/curriculum/providers/bildungsplan-hamburg/index.ts
8114
+ var bildungsplanHamburgProvider = {
8115
+ id: "bildungsplan-hamburg",
8116
+ country: "DE",
8117
+ countryLabel: "Deutschland",
8118
+ region: "HH",
8119
+ regionLabel: "Hamburg",
8120
+ label: "Bildungsplan (Hamburg)",
8121
+ listSchoolTypes() {
8122
+ return BILDUNGSPLAN_HAMBURG_MANIFEST.schoolTypes;
8123
+ },
8124
+ listGrades(schoolType) {
8125
+ return (BILDUNGSPLAN_HAMBURG_MANIFEST.grades[schoolType] || []).map((id) => ({
8126
+ id,
8127
+ label: `Klasse ${id}`
8128
+ }));
8129
+ },
8130
+ listSubjects(schoolType, _grade) {
8131
+ return BILDUNGSPLAN_HAMBURG_MANIFEST.subjects[schoolType] || [];
8132
+ },
8133
+ listTracks(schoolType, grade, subject) {
8134
+ const key = `${schoolType}|${grade}|${subject}`;
8135
+ return BILDUNGSPLAN_HAMBURG_MANIFEST.tracks[key] || [];
8136
+ },
8137
+ listTopics(selection) {
8138
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
8139
+ const list = BILDUNGSPLAN_HAMBURG_MANIFEST.topics[key] || [];
8140
+ return list.map((t2) => ({
8141
+ ...t2,
8142
+ sourceRef: key
8143
+ }));
8144
+ },
8145
+ resolveTopic(topic) {
8146
+ const uri = BILDUNGSPLAN_HAMBURG_MANIFEST.contentUrls[topic.sourceRef];
8147
+ if (!uri) {
8148
+ throw new Error(
8149
+ `Bildungsplan Hamburg: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
8150
+ );
8151
+ }
8152
+ return {
8153
+ provider: "bildungsplan-hamburg",
8154
+ topicId: `${topic.sourceRef}#${topic.id}`,
8155
+ uri
8156
+ };
8157
+ },
8158
+ extractTopics(html, topicIds) {
8159
+ const results = {};
8160
+ const headingMatches = Array.from(
8161
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
8162
+ );
8163
+ const sections = [];
8164
+ for (const m of headingMatches) {
8165
+ const headerText = cleanHtmlText3(m[1]).trim();
8166
+ const start = m.index + m[0].length;
8167
+ const nextHeading = html.indexOf("<h", start);
8168
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
8169
+ const content = cleanHtmlText3(rawContent).slice(0, 1500);
8170
+ if (headerText) sections.push({ header: headerText, content });
8171
+ }
8172
+ for (const topicId of topicIds) {
8173
+ const hashIdx = topicId.indexOf("#");
8174
+ if (hashIdx === -1) continue;
8175
+ const key = topicId.substring(0, hashIdx);
8176
+ const shortId = topicId.substring(hashIdx + 1);
8177
+ const list = BILDUNGSPLAN_HAMBURG_MANIFEST.topics[key] ?? [];
8178
+ const match = list.find((t2) => t2.id === shortId);
8179
+ if (!match) continue;
8180
+ const label = match.label;
8181
+ const normalizedLabel = normalizeForComparison3(label);
8182
+ let section = sections.find(
8183
+ (s) => normalizeForComparison3(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
8184
+ );
8185
+ if (!section && sections.length > 0) {
8186
+ section = sections.find((s) => s.content.length > 80) || sections[0];
8187
+ }
8188
+ if (section) {
8189
+ results[topicId] = `${label}
8190
+
8191
+ ${section.content}`.trim();
8192
+ } else {
8193
+ results[topicId] = label;
8194
+ }
8195
+ }
8196
+ return results;
8197
+ }
8198
+ };
8199
+ function cleanHtmlText3(html) {
8200
+ let text = html.replace(
8201
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
8202
+ " "
8203
+ );
8204
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
8205
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
8206
+ text = text.replace(/<p[^>]*>/gi, "\n");
8207
+ text = text.replace(/<br\s*\/?>/gi, "\n");
8208
+ text = text.replace(/<[^>]+>/g, " ");
8209
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
8210
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8211
+ }
8212
+ function normalizeForComparison3(str) {
8213
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8214
+ }
8215
+
8216
+ // src/cli/curriculum/providers/fachanforderungen-sh/manifest.ts
8217
+ var FACHANFORDERUNGEN_SH_MANIFEST = {
8218
+ schoolYear: "2025/2026",
8219
+ capturedOn: "2026-07-02",
8220
+ sourceRevision: "Fachanforderungen Schleswig-Holstein",
8221
+ schoolTypes: [
8222
+ { id: "gemeinschaftsschule", label: "Gemeinschaftsschule" },
8223
+ { id: "gymnasium", label: "Gymnasium" }
8224
+ ],
8225
+ grades: {
8226
+ gemeinschaftsschule: ["7", "8", "9", "10"],
8227
+ gymnasium: ["7", "8", "9", "10"]
8228
+ },
8229
+ subjects: {
8230
+ gemeinschaftsschule: [
8231
+ { id: "mathematik", label: "Mathematik" },
8232
+ { id: "informatik", label: "Informatik" },
8233
+ { id: "physik", label: "Physik" },
8234
+ { id: "chemie", label: "Chemie" },
8235
+ { id: "biologie", label: "Biologie" }
8236
+ ],
8237
+ gymnasium: [
8238
+ { id: "mathematik", label: "Mathematik" },
8239
+ { id: "informatik", label: "Informatik" },
8240
+ { id: "physik", label: "Physik" },
8241
+ { id: "chemie", label: "Chemie" },
8242
+ { id: "biologie", label: "Biologie" }
8243
+ ]
8244
+ },
8245
+ tracks: {},
8246
+ topics: {
8247
+ "gemeinschaftsschule|10|mathematik": [
8248
+ { id: "funktionen", label: "Funktionen" }
8249
+ ],
8250
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
8251
+ "gemeinschaftsschule|9|physik": [{ id: "mechanik", label: "Mechanik" }],
8252
+ "gemeinschaftsschule|9|chemie": [{ id: "reaktionen", label: "Reaktionen" }],
8253
+ "gemeinschaftsschule|9|biologie": [{ id: "zellen", label: "Zellen" }],
8254
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
8255
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Bindungen" }],
8256
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
8257
+ },
8258
+ contentUrls: {
8259
+ "gemeinschaftsschule|10|mathematik": "https://fachportal.lernnetz.de/sh/fachanforderungen.html",
8260
+ "gymnasium|10|mathematik": "https://fachportal.lernnetz.de/sh/fachanforderungen.html",
8261
+ "gemeinschaftsschule|9|physik": "https://fachportal.lernnetz.de/sh/fachanforderungen.html",
8262
+ "gemeinschaftsschule|9|chemie": "https://fachportal.lernnetz.de/sh/fachanforderungen.html",
8263
+ "gemeinschaftsschule|9|biologie": "https://fachportal.lernnetz.de/sh/fachanforderungen.html",
8264
+ "gymnasium|9|physik": "https://fachportal.lernnetz.de/sh/fachanforderungen.html",
8265
+ "gymnasium|9|chemie": "https://fachportal.lernnetz.de/sh/fachanforderungen.html",
8266
+ "gymnasium|9|biologie": "https://fachportal.lernnetz.de/sh/fachanforderungen.html"
8267
+ }
8268
+ };
8269
+
8270
+ // src/cli/curriculum/providers/fachanforderungen-sh/index.ts
8271
+ var fachanforderungenShProvider = {
8272
+ id: "fachanforderungen-sh",
8273
+ country: "DE",
8274
+ countryLabel: "Deutschland",
8275
+ region: "SH",
8276
+ regionLabel: "Schleswig-Holstein",
8277
+ label: "Fachanforderungen (Schleswig-Holstein)",
8278
+ listSchoolTypes() {
8279
+ return FACHANFORDERUNGEN_SH_MANIFEST.schoolTypes;
8280
+ },
8281
+ listGrades(schoolType) {
8282
+ return (FACHANFORDERUNGEN_SH_MANIFEST.grades[schoolType] || []).map((id) => ({
8283
+ id,
8284
+ label: `Klasse ${id}`
8285
+ }));
8286
+ },
8287
+ listSubjects(schoolType, _grade) {
8288
+ return FACHANFORDERUNGEN_SH_MANIFEST.subjects[schoolType] || [];
8289
+ },
8290
+ listTracks(schoolType, grade, subject) {
8291
+ const key = `${schoolType}|${grade}|${subject}`;
8292
+ return FACHANFORDERUNGEN_SH_MANIFEST.tracks[key] || [];
8293
+ },
8294
+ listTopics(selection) {
8295
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
8296
+ const list = FACHANFORDERUNGEN_SH_MANIFEST.topics[key] || [];
8297
+ return list.map((t2) => ({
8298
+ ...t2,
8299
+ sourceRef: key
8300
+ }));
8301
+ },
8302
+ resolveTopic(topic) {
8303
+ const uri = FACHANFORDERUNGEN_SH_MANIFEST.contentUrls[topic.sourceRef];
8304
+ if (!uri) {
8305
+ throw new Error(
8306
+ `Fachanforderungen SH: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
8307
+ );
8308
+ }
8309
+ return {
8310
+ provider: "fachanforderungen-sh",
8311
+ topicId: `${topic.sourceRef}#${topic.id}`,
8312
+ uri
8313
+ };
8314
+ },
8315
+ extractTopics(html, topicIds) {
8316
+ const results = {};
8317
+ const headingMatches = Array.from(
8318
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
8319
+ );
8320
+ const sections = [];
8321
+ for (const m of headingMatches) {
8322
+ const headerText = cleanHtmlText4(m[1]).trim();
8323
+ const start = m.index + m[0].length;
8324
+ const nextHeading = html.indexOf("<h", start);
8325
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
8326
+ const content = cleanHtmlText4(rawContent).slice(0, 1500);
8327
+ if (headerText) sections.push({ header: headerText, content });
8328
+ }
8329
+ for (const topicId of topicIds) {
8330
+ const hashIdx = topicId.indexOf("#");
8331
+ if (hashIdx === -1) continue;
8332
+ const key = topicId.substring(0, hashIdx);
8333
+ const shortId = topicId.substring(hashIdx + 1);
8334
+ const list = FACHANFORDERUNGEN_SH_MANIFEST.topics[key] ?? [];
8335
+ const match = list.find((t2) => t2.id === shortId);
8336
+ if (!match) continue;
8337
+ const label = match.label;
8338
+ const normalizedLabel = normalizeForComparison4(label);
8339
+ let section = sections.find(
8340
+ (s) => normalizeForComparison4(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
8341
+ );
8342
+ if (!section && sections.length > 0) {
8343
+ section = sections.find((s) => s.content.length > 80) || sections[0];
8344
+ }
8345
+ if (section) {
8346
+ results[topicId] = `${label}
8347
+
8348
+ ${section.content}`.trim();
8349
+ } else {
8350
+ results[topicId] = label;
8351
+ }
8352
+ }
8353
+ return results;
8354
+ }
8355
+ };
8356
+ function cleanHtmlText4(html) {
8357
+ let text = html.replace(
8358
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
8359
+ " "
8360
+ );
8361
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
8362
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
8363
+ text = text.replace(/<p[^>]*>/gi, "\n");
8364
+ text = text.replace(/<br\s*\/?>/gi, "\n");
8365
+ text = text.replace(/<[^>]+>/g, " ");
8366
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
8367
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8368
+ }
8369
+ function normalizeForComparison4(str) {
8370
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8371
+ }
8372
+
8373
+ // src/cli/curriculum/providers/kerncurriculum-hessen/manifest.ts
8374
+ var KERNCURRICULUM_HESSEN_MANIFEST = {
8375
+ schoolYear: "2025/2026",
8376
+ capturedOn: "2026-07-02",
8377
+ sourceRevision: "Kerncurricula Hessen (Sekundarstufe I)",
8378
+ schoolTypes: [
8379
+ { id: "realschule", label: "Realschule" },
8380
+ { id: "gymnasium", label: "Gymnasium" }
8381
+ ],
8382
+ grades: {
8383
+ realschule: ["7", "8", "9", "10"],
8384
+ gymnasium: ["7", "8", "9", "10"]
8385
+ },
8386
+ subjects: {
8387
+ realschule: [
8388
+ { id: "mathematik", label: "Mathematik" },
8389
+ { id: "informatik", label: "Informatik" },
8390
+ { id: "physik", label: "Physik" },
8391
+ { id: "chemie", label: "Chemie" },
8392
+ { id: "biologie", label: "Biologie" }
8393
+ ],
8394
+ gymnasium: [
8395
+ { id: "mathematik", label: "Mathematik" },
8396
+ { id: "informatik", label: "Informatik" },
8397
+ { id: "physik", label: "Physik" },
8398
+ { id: "chemie", label: "Chemie" },
8399
+ { id: "biologie", label: "Biologie" }
8400
+ ]
8401
+ },
8402
+ tracks: {},
8403
+ topics: {
8404
+ // Minimal for Hessen Realschule / Gym - based on Kerncurricula Inhaltsfelder
8405
+ "realschule|10|mathematik": [
8406
+ { id: "funktionen", label: "Funktionen" },
8407
+ { id: "geometrie", label: "Geometrie" },
8408
+ { id: "wahrscheinlichkeit", label: "Wahrscheinlichkeit und Statistik" }
8409
+ ],
8410
+ "gymnasium|10|mathematik": [
8411
+ { id: "analysis", label: "Analysis" },
8412
+ { id: "vektoren", label: "Vektoren und Geometrie" },
8413
+ { id: "stochastik", label: "Stochastik" }
8414
+ ],
8415
+ "realschule|9|informatik": [
8416
+ { id: "daten", label: "Daten und Information" },
8417
+ { id: "algorithmen", label: "Algorithmen und Programmierung" },
8418
+ { id: "netze", label: "Netze und Sicherheit" }
8419
+ ],
8420
+ "realschule|9|physik": [
8421
+ { id: "mechanik", label: "Mechanik" },
8422
+ { id: "energie", label: "Energie" },
8423
+ { id: "elektrizitaet", label: "Elektrizit\xE4t" }
8424
+ ],
8425
+ "realschule|9|chemie": [
8426
+ { id: "stoffe", label: "Stoffe und Reaktionen" },
8427
+ { id: "atome", label: "Atome und Periodensystem" },
8428
+ { id: "organisch", label: "Organische Verbindungen" }
8429
+ ],
8430
+ "realschule|9|biologie": [
8431
+ { id: "zelle", label: "Zelle und Stoffwechsel" },
8432
+ { id: "vererbung", label: "Vererbung" },
8433
+ { id: "oekologie", label: "\xD6kologie" }
8434
+ ],
8435
+ "gymnasium|9|physik": [
8436
+ { id: "optik", label: "Optik" },
8437
+ { id: "mechanik-dynamik", label: "Mechanik und Dynamik" },
8438
+ { id: "felder", label: "Elektrische und magnetische Felder" }
8439
+ ],
8440
+ "gymnasium|9|chemie": [
8441
+ { id: "bindung", label: "Chemische Bindung" },
8442
+ { id: "reaktionsgeschwindigkeit", label: "Reaktionsgeschwindigkeit" },
8443
+ { id: "saeure-base", label: "S\xE4ure-Base-Reaktionen" }
8444
+ ],
8445
+ "gymnasium|9|biologie": [
8446
+ { id: "genetik", label: "Genetik" },
8447
+ { id: "evolution", label: "Evolution" },
8448
+ { id: "neurobiologie", label: "Neurobiologie" }
8449
+ ]
8450
+ },
8451
+ contentUrls: {
8452
+ "realschule|10|mathematik": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8453
+ "gymnasium|10|mathematik": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8454
+ "realschule|9|informatik": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8455
+ "realschule|9|physik": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8456
+ "realschule|9|chemie": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8457
+ "realschule|9|biologie": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8458
+ "gymnasium|9|physik": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8459
+ "gymnasium|9|chemie": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula",
8460
+ "gymnasium|9|biologie": "https://kultusministerium.hessen.de/Unterricht/Kerncurricula-und-Lehrplaene/Kerncurricula/sekundarstufe-i-kerncurricula"
8461
+ }
8462
+ };
8463
+
8464
+ // src/cli/curriculum/providers/kerncurriculum-hessen/index.ts
8465
+ var kerncurriculumHessenProvider = {
8466
+ id: "kerncurriculum-hessen",
8467
+ country: "DE",
8468
+ countryLabel: "Deutschland",
8469
+ region: "HE",
8470
+ regionLabel: "Hessen",
8471
+ label: "Kerncurriculum (Hessen)",
8472
+ listSchoolTypes() {
8473
+ return KERNCURRICULUM_HESSEN_MANIFEST.schoolTypes;
8474
+ },
8475
+ listGrades(schoolType) {
8476
+ return (KERNCURRICULUM_HESSEN_MANIFEST.grades[schoolType] || []).map((id) => ({
8477
+ id,
8478
+ label: `Klasse ${id}`
8479
+ }));
8480
+ },
8481
+ listSubjects(schoolType, _grade) {
8482
+ return KERNCURRICULUM_HESSEN_MANIFEST.subjects[schoolType] || [];
8483
+ },
8484
+ listTracks(schoolType, grade, subject) {
8485
+ const key = `${schoolType}|${grade}|${subject}`;
8486
+ return KERNCURRICULUM_HESSEN_MANIFEST.tracks[key] || [];
8487
+ },
8488
+ listTopics(selection) {
8489
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
8490
+ const list = KERNCURRICULUM_HESSEN_MANIFEST.topics[key] || [];
8491
+ return list.map((t2) => ({
8492
+ ...t2,
8493
+ sourceRef: key
8494
+ }));
8495
+ },
8496
+ resolveTopic(topic) {
8497
+ const uri = KERNCURRICULUM_HESSEN_MANIFEST.contentUrls[topic.sourceRef];
8498
+ if (!uri) {
8499
+ throw new Error(
8500
+ `Kerncurriculum Hessen: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
8501
+ );
8502
+ }
8503
+ return {
8504
+ provider: "kerncurriculum-hessen",
8505
+ topicId: `${topic.sourceRef}#${topic.id}`,
8506
+ uri
8507
+ };
8508
+ },
8509
+ extractTopics(html, topicIds) {
8510
+ const results = {};
8511
+ const headingMatches = Array.from(
8512
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
8513
+ );
8514
+ const sections = [];
8515
+ for (const m of headingMatches) {
8516
+ const headerText = cleanHtmlText5(m[1]).trim();
8517
+ const start = m.index + m[0].length;
8518
+ const nextHeading = html.indexOf("<h", start);
8519
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
8520
+ const content = cleanHtmlText5(rawContent).slice(0, 1500);
8521
+ if (headerText) sections.push({ header: headerText, content });
8522
+ }
8523
+ for (const topicId of topicIds) {
8524
+ const hashIdx = topicId.indexOf("#");
8525
+ if (hashIdx === -1) continue;
8526
+ const key = topicId.substring(0, hashIdx);
8527
+ const shortId = topicId.substring(hashIdx + 1);
8528
+ const list = KERNCURRICULUM_HESSEN_MANIFEST.topics[key] ?? [];
8529
+ const match = list.find((t2) => t2.id === shortId);
8530
+ if (!match) continue;
8531
+ const label = match.label;
8532
+ const normalizedLabel = normalizeForComparison5(label);
8533
+ let section = sections.find(
8534
+ (s) => normalizeForComparison5(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
8535
+ );
8536
+ if (!section && sections.length > 0) {
8537
+ section = sections.find((s) => s.content.length > 80) || sections[0];
8538
+ }
8539
+ if (section) {
8540
+ results[topicId] = `${label}
8541
+
8542
+ ${section.content}`.trim();
8543
+ } else {
8544
+ results[topicId] = label;
8545
+ }
8546
+ }
8547
+ return results;
8548
+ }
8549
+ };
8550
+ function cleanHtmlText5(html) {
8551
+ let text = html.replace(
8552
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
8553
+ " "
8554
+ );
8555
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
8556
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
8557
+ text = text.replace(/<p[^>]*>/gi, "\n");
8558
+ text = text.replace(/<br\s*\/?>/gi, "\n");
8559
+ text = text.replace(/<[^>]+>/g, " ");
8560
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
8561
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8562
+ }
8563
+ function normalizeForComparison5(str) {
8564
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8565
+ }
8566
+
8567
+ // src/cli/curriculum/providers/kerncurriculum-niedersachsen/manifest.ts
8568
+ var KERNCURRICULUM_NIEDERSACHSEN_MANIFEST = {
8569
+ schoolYear: "2025/2026",
8570
+ capturedOn: "2026-07-02",
8571
+ sourceRevision: "Kerncurricula Niedersachsen (CuVo)",
8572
+ schoolTypes: [
8573
+ { id: "realschule", label: "Realschule" },
8574
+ { id: "gymnasium", label: "Gymnasium" }
8575
+ ],
8576
+ grades: {
8577
+ realschule: ["7", "8", "9", "10"],
8578
+ gymnasium: ["7", "8", "9", "10"]
8579
+ },
8580
+ subjects: {
8581
+ realschule: [
8582
+ { id: "mathematik", label: "Mathematik" },
8583
+ { id: "informatik", label: "Informatik" },
8584
+ { id: "physik", label: "Physik" },
8585
+ { id: "chemie", label: "Chemie" },
8586
+ { id: "biologie", label: "Biologie" }
8587
+ ],
8588
+ gymnasium: [
8589
+ { id: "mathematik", label: "Mathematik" },
8590
+ { id: "informatik", label: "Informatik" },
8591
+ { id: "physik", label: "Physik" },
8592
+ { id: "chemie", label: "Chemie" },
8593
+ { id: "biologie", label: "Biologie" }
8594
+ ]
8595
+ },
8596
+ tracks: {},
8597
+ topics: {
8598
+ "realschule|10|mathematik": [
8599
+ { id: "algebra-funktionen", label: "Algebra und Funktionen" },
8600
+ { id: "geometrie", label: "Geometrie" }
8601
+ ],
8602
+ "gymnasium|10|mathematik": [
8603
+ { id: "analysis", label: "Analysis" },
8604
+ { id: "lineare-algebra", label: "Lineare Algebra" }
8605
+ ],
8606
+ "realschule|9|physik": [
8607
+ { id: "mechanik", label: "Mechanik" },
8608
+ { id: "energie", label: "Energie und W\xE4rme" }
8609
+ ],
8610
+ "realschule|9|chemie": [
8611
+ { id: "reaktionen", label: "Chemische Reaktionen" },
8612
+ { id: "stoffkreislauf", label: "Stoffkreisl\xE4ufe" }
8613
+ ],
8614
+ "realschule|9|biologie": [
8615
+ { id: "zellen", label: "Zellen" },
8616
+ { id: "oekologie", label: "\xD6kologie" }
8617
+ ],
8618
+ "gymnasium|9|physik": [
8619
+ { id: "elektrizitaet", label: "Elektrizit\xE4t und Magnetismus" }
8620
+ ],
8621
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Chemische Bindungen" }],
8622
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
8623
+ },
8624
+ contentUrls: {
8625
+ "realschule|10|mathematik": "https://cuvo.nibis.de/cuvo.php",
8626
+ "gymnasium|10|mathematik": "https://cuvo.nibis.de/cuvo.php",
8627
+ "realschule|9|physik": "https://cuvo.nibis.de/cuvo.php",
8628
+ "realschule|9|chemie": "https://cuvo.nibis.de/cuvo.php",
8629
+ "realschule|9|biologie": "https://cuvo.nibis.de/cuvo.php",
8630
+ "gymnasium|9|physik": "https://cuvo.nibis.de/cuvo.php",
8631
+ "gymnasium|9|chemie": "https://cuvo.nibis.de/cuvo.php",
8632
+ "gymnasium|9|biologie": "https://cuvo.nibis.de/cuvo.php"
8633
+ }
8634
+ };
8635
+
8636
+ // src/cli/curriculum/providers/kerncurriculum-niedersachsen/index.ts
8637
+ var kerncurriculumNiedersachsenProvider = {
8638
+ id: "kerncurriculum-niedersachsen",
8639
+ country: "DE",
8640
+ countryLabel: "Deutschland",
8641
+ region: "NI",
8642
+ regionLabel: "Niedersachsen",
8643
+ label: "Kerncurriculum (Niedersachsen)",
8644
+ listSchoolTypes() {
8645
+ return KERNCURRICULUM_NIEDERSACHSEN_MANIFEST.schoolTypes;
8646
+ },
8647
+ listGrades(schoolType) {
8648
+ return (KERNCURRICULUM_NIEDERSACHSEN_MANIFEST.grades[schoolType] || []).map((id) => ({
8649
+ id,
8650
+ label: `Klasse ${id}`
8651
+ }));
8652
+ },
8653
+ listSubjects(schoolType, _grade) {
8654
+ return KERNCURRICULUM_NIEDERSACHSEN_MANIFEST.subjects[schoolType] || [];
8655
+ },
8656
+ listTracks(schoolType, grade, subject) {
8657
+ const key = `${schoolType}|${grade}|${subject}`;
8658
+ return KERNCURRICULUM_NIEDERSACHSEN_MANIFEST.tracks[key] || [];
8659
+ },
8660
+ listTopics(selection) {
8661
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
8662
+ const list = KERNCURRICULUM_NIEDERSACHSEN_MANIFEST.topics[key] || [];
8663
+ return list.map((t2) => ({
8664
+ ...t2,
8665
+ sourceRef: key
8666
+ }));
8667
+ },
8668
+ resolveTopic(topic) {
8669
+ const uri = KERNCURRICULUM_NIEDERSACHSEN_MANIFEST.contentUrls[topic.sourceRef];
8670
+ if (!uri) {
8671
+ throw new Error(
8672
+ `Kerncurriculum Niedersachsen: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
8673
+ );
8674
+ }
8675
+ return {
8676
+ provider: "kerncurriculum-niedersachsen",
8677
+ topicId: `${topic.sourceRef}#${topic.id}`,
8678
+ uri
8679
+ };
8680
+ },
8681
+ extractTopics(html, topicIds) {
8682
+ const results = {};
8683
+ const headingMatches = Array.from(
8684
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
8685
+ );
8686
+ const sections = [];
8687
+ for (const m of headingMatches) {
8688
+ const headerText = cleanHtmlText6(m[1]).trim();
8689
+ const start = m.index + m[0].length;
8690
+ const nextHeading = html.indexOf("<h", start);
8691
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
8692
+ const content = cleanHtmlText6(rawContent).slice(0, 1500);
8693
+ if (headerText) sections.push({ header: headerText, content });
8694
+ }
8695
+ for (const topicId of topicIds) {
8696
+ const hashIdx = topicId.indexOf("#");
8697
+ if (hashIdx === -1) continue;
8698
+ const key = topicId.substring(0, hashIdx);
8699
+ const shortId = topicId.substring(hashIdx + 1);
8700
+ const list = KERNCURRICULUM_NIEDERSACHSEN_MANIFEST.topics[key] ?? [];
8701
+ const match = list.find((t2) => t2.id === shortId);
8702
+ if (!match) continue;
8703
+ const label = match.label;
8704
+ const normalizedLabel = normalizeForComparison6(label);
8705
+ let section = sections.find(
8706
+ (s) => normalizeForComparison6(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
8707
+ );
8708
+ if (!section && sections.length > 0) {
8709
+ section = sections.find((s) => s.content.length > 80) || sections[0];
8710
+ }
8711
+ if (section) {
8712
+ results[topicId] = `${label}
8713
+
8714
+ ${section.content}`.trim();
8715
+ } else {
8716
+ results[topicId] = label;
8717
+ }
8718
+ }
8719
+ return results;
8720
+ }
8721
+ };
8722
+ function cleanHtmlText6(html) {
8723
+ let text = html.replace(
8724
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
8725
+ " "
8726
+ );
8727
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
8728
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
8729
+ text = text.replace(/<p[^>]*>/gi, "\n");
8730
+ text = text.replace(/<br\s*\/?>/gi, "\n");
8731
+ text = text.replace(/<[^>]+>/g, " ");
8732
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
8733
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8734
+ }
8735
+ function normalizeForComparison6(str) {
8736
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8737
+ }
8738
+
8739
+ // src/cli/curriculum/providers/kernlehrplan-nrw/manifest.ts
8740
+ var KERNLEHRPLAN_NRW_MANIFEST = {
8741
+ schoolYear: "2025/2026",
8742
+ capturedOn: "2026-07-02",
8743
+ sourceRevision: "Kernlehrpl\xE4ne Sekundarstufe I NRW (Lehrplannavigator)",
8744
+ schoolTypes: [
8745
+ { id: "realschule", label: "Realschule" },
8746
+ { id: "gymnasium", label: "Gymnasium" }
8747
+ ],
8748
+ grades: {
8749
+ realschule: ["7", "8", "9", "10"],
8750
+ gymnasium: ["7", "8", "9", "10"]
8751
+ },
8752
+ subjects: {
8753
+ realschule: [
8754
+ { id: "mathematik", label: "Mathematik" },
8755
+ { id: "informatik", label: "Informatik" },
8756
+ { id: "physik", label: "Physik" },
8757
+ { id: "chemie", label: "Chemie" },
8758
+ { id: "biologie", label: "Biologie" }
8759
+ ],
8760
+ gymnasium: [
8761
+ { id: "mathematik", label: "Mathematik" },
8762
+ { id: "informatik", label: "Informatik" },
8763
+ { id: "physik", label: "Physik" },
8764
+ { id: "chemie", label: "Chemie" },
8765
+ { id: "biologie", label: "Biologie" }
8766
+ ]
8767
+ },
8768
+ tracks: {},
8769
+ topics: {
8770
+ // Minimal starter set for Realschule Mathematik (example for one grade)
8771
+ "realschule|10|mathematik": [
8772
+ { id: "arithmetik-algebra", label: "Arithmetik und Algebra" },
8773
+ { id: "funktionen", label: "Funktionen" },
8774
+ { id: "geometrie", label: "Geometrie" },
8775
+ { id: "stochastik", label: "Stochastik" }
8776
+ ],
8777
+ // Minimal starter for Realschule Informatik (from UV examples in 5/6 + typical)
8778
+ "realschule|8|informatik": [
8779
+ { id: "daten-codierung", label: "Daten und Codierung" },
8780
+ { id: "algorithmen", label: "Algorithmen" },
8781
+ {
8782
+ id: "ki-datenbewusstsein",
8783
+ label: "K\xFCnstliche Intelligenz und Datenbewusstsein"
8784
+ }
8785
+ ],
8786
+ // Starter for Gymnasium Mathematik (higher track)
8787
+ "gymnasium|10|mathematik": [
8788
+ { id: "analysis", label: "Analysis / Funktionen" },
8789
+ {
8790
+ id: "lineare-algebra-geometrie",
8791
+ label: "Lineare Algebra und Geometrie"
8792
+ },
8793
+ { id: "stochastik", label: "Stochastik" }
8794
+ ],
8795
+ // Starter for Gymnasium WP Informatik
8796
+ "gymnasium|10|informatik": [
8797
+ { id: "logische-schaltungen", label: "Logische Schaltungen" },
8798
+ {
8799
+ id: "algorithmen-programmierung",
8800
+ label: "Algorithmen und Programmierung"
8801
+ },
8802
+ { id: "daten-netze-sicherheit", label: "Daten, Netze und Sicherheit" }
8803
+ ],
8804
+ // Naturwissenschaften for Realschule example
8805
+ "realschule|9|physik": [
8806
+ { id: "mechanik-kraefte", label: "Mechanik und Kr\xE4fte" },
8807
+ { id: "waerme-energie", label: "W\xE4rme und Energie" },
8808
+ { id: "elektrizitaet", label: "Elektrizit\xE4t" }
8809
+ ],
8810
+ "realschule|9|chemie": [
8811
+ { id: "stoffe-eigenschaften", label: "Stoffe und ihre Eigenschaften" },
8812
+ { id: "reaktionen", label: "Chemische Reaktionen" },
8813
+ { id: "atombau", label: "Atombau und Periodensystem" }
8814
+ ],
8815
+ "realschule|9|biologie": [
8816
+ { id: "zellen-lebewesen", label: "Zellen und Lebewesen" },
8817
+ { id: "vererbung", label: "Vererbung und Genetik" },
8818
+ { id: "oekosysteme", label: "\xD6kosysteme" }
8819
+ ],
8820
+ // Naturwissenschaften for Gymnasium
8821
+ "gymnasium|9|physik": [
8822
+ { id: "optik", label: "Optik" },
8823
+ { id: "elektromagnetismus", label: "Elektromagnetismus" },
8824
+ { id: "atomphysik", label: "Atom- und Kernphysik" }
8825
+ ],
8826
+ "gymnasium|9|chemie": [
8827
+ { id: "chemische-bindung", label: "Chemische Bindung" },
8828
+ { id: "saeuren-basen-salze", label: "S\xE4uren, Basen, Salze" },
8829
+ { id: "organische", label: "Organische Chemie" }
8830
+ ],
8831
+ "gymnasium|9|biologie": [
8832
+ { id: "genetik-evolution", label: "Genetik und Evolution" },
8833
+ { id: "physiologie", label: "Physiologie des Menschen" },
8834
+ { id: "oekologie-umwelt", label: "\xD6kologie und Umweltschutz" }
8835
+ ]
8836
+ },
8837
+ contentUrls: {
8838
+ "realschule|10|mathematik": "https://lehrplannavigator.nrw.de/sekundarstufe-i/realschule/mathematik-neu-ab-20222023",
8839
+ "realschule|8|informatik": "https://lehrplannavigator.nrw.de/sekundarstufe-i/realschule/informatik-realschule-neu-ab-20212022",
8840
+ "gymnasium|10|mathematik": "https://lehrplannavigator.nrw.de/sekundarstufe-i/kernlehrplaene-fuer-das-gymnasium-ab-20192020/mathematik",
8841
+ "gymnasium|10|informatik": "https://lehrplannavigator.nrw.de/sekundarstufe-i/kernlehrplaene-fuer-das-gymnasium-ab-20192020/wp-informatik-neu-ab-20232024",
8842
+ "realschule|9|physik": "https://lehrplannavigator.nrw.de/sekundarstufe-i/realschule/physik",
8843
+ "realschule|9|chemie": "https://lehrplannavigator.nrw.de/sekundarstufe-i/realschule/chemie",
8844
+ "realschule|9|biologie": "https://lehrplannavigator.nrw.de/sekundarstufe-i/realschule/biologie",
8845
+ "gymnasium|9|physik": "https://lehrplannavigator.nrw.de/sekundarstufe-i/kernlehrplaene-fuer-das-gymnasium-ab-20192020/physik",
8846
+ "gymnasium|9|chemie": "https://lehrplannavigator.nrw.de/sekundarstufe-i/kernlehrplaene-fuer-das-gymnasium-ab-20192020/chemie",
8847
+ "gymnasium|9|biologie": "https://lehrplannavigator.nrw.de/sekundarstufe-i/kernlehrplaene-fuer-das-gymnasium-ab-20192020/biologie"
8848
+ }
8849
+ };
8850
+
8851
+ // src/cli/curriculum/providers/kernlehrplan-nrw/index.ts
8852
+ var kernlehrplanNrwProvider = {
8853
+ id: "kernlehrplan-nrw",
8854
+ country: "DE",
8855
+ countryLabel: "Deutschland",
8856
+ region: "NW",
8857
+ regionLabel: "Nordrhein-Westfalen",
8858
+ label: "Kernlehrplan (Nordrhein-Westfalen)",
8859
+ listSchoolTypes() {
8860
+ return KERNLEHRPLAN_NRW_MANIFEST.schoolTypes;
8861
+ },
8862
+ listGrades(schoolType) {
8863
+ return (KERNLEHRPLAN_NRW_MANIFEST.grades[schoolType] || []).map((id) => ({
8864
+ id,
8865
+ label: `Klasse ${id}`
8866
+ }));
8867
+ },
8868
+ listSubjects(schoolType, _grade) {
8869
+ return KERNLEHRPLAN_NRW_MANIFEST.subjects[schoolType] || [];
8870
+ },
8871
+ listTracks(schoolType, grade, subject) {
8872
+ const key = `${schoolType}|${grade}|${subject}`;
8873
+ return KERNLEHRPLAN_NRW_MANIFEST.tracks[key] || [];
8874
+ },
8875
+ listTopics(selection) {
8876
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
8877
+ const list = KERNLEHRPLAN_NRW_MANIFEST.topics[key] || [];
8878
+ return list.map((t2) => ({
8879
+ ...t2,
8880
+ sourceRef: key
8881
+ }));
8882
+ },
8883
+ resolveTopic(topic) {
8884
+ const uri = KERNLEHRPLAN_NRW_MANIFEST.contentUrls[topic.sourceRef];
8885
+ if (!uri) {
8886
+ throw new Error(
8887
+ `Kernlehrplan NRW: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
8888
+ );
8889
+ }
8890
+ return {
8891
+ provider: "kernlehrplan-nrw",
8892
+ topicId: `${topic.sourceRef}#${topic.id}`,
8893
+ uri
8894
+ };
8895
+ },
8896
+ extractTopics(html, topicIds) {
8897
+ const results = {};
8898
+ const headingMatches = Array.from(
8899
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
8900
+ );
8901
+ const sections = [];
8902
+ for (const m of headingMatches) {
8903
+ const headerText = cleanHtmlText7(m[1]).trim();
8904
+ const start = m.index + m[0].length;
8905
+ const nextHeading = html.indexOf("<h", start);
8906
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 2e3);
8907
+ const content = cleanHtmlText7(rawContent).slice(0, 2e3);
8908
+ if (headerText) sections.push({ header: headerText, content });
8909
+ }
8910
+ for (const topicId of topicIds) {
8911
+ const hashIdx = topicId.indexOf("#");
8912
+ if (hashIdx === -1) continue;
8913
+ const key = topicId.substring(0, hashIdx);
8914
+ const shortId = topicId.substring(hashIdx + 1);
8915
+ const list = KERNLEHRPLAN_NRW_MANIFEST.topics[key] ?? [];
8916
+ const match = list.find((t2) => t2.id === shortId);
8917
+ if (!match) continue;
8918
+ const label = match.label;
8919
+ const normalizedLabel = normalizeForComparison7(label);
8920
+ let section = sections.find(
8921
+ (s) => normalizeForComparison7(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
8922
+ );
8923
+ if (!section && sections.length > 0) {
8924
+ section = sections.find((s) => s.content.length > 100) || sections[0];
8925
+ }
8926
+ if (section) {
8927
+ results[topicId] = `${label}
8928
+
8929
+ ${section.content}`.trim();
8930
+ } else {
8931
+ results[topicId] = label;
8932
+ }
8933
+ }
8934
+ return results;
8935
+ }
8936
+ };
8937
+ function cleanHtmlText7(html) {
8938
+ let text = html.replace(
8939
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
8940
+ " "
8941
+ );
8942
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
8943
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
8944
+ text = text.replace(/<p[^>]*>/gi, "\n");
8945
+ text = text.replace(/<br\s*\/?>/gi, "\n");
8946
+ text = text.replace(/<[^>]+>/g, " ");
8947
+ 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");
8948
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
8949
+ }
8950
+ function normalizeForComparison7(str) {
8951
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
8952
+ }
8953
+
8954
+ // src/cli/curriculum/providers/lehrplaene-rp/manifest.ts
8955
+ var LEHRPLAENE_RP_MANIFEST = {
8956
+ schoolYear: "2025/2026",
8957
+ capturedOn: "2026-07-02",
8958
+ sourceRevision: "Lehrpl\xE4ne Rheinland-Pfalz",
8959
+ schoolTypes: [
8960
+ { id: "realschule-plus", label: "Realschule plus" },
8961
+ { id: "gymnasium", label: "Gymnasium" }
8962
+ ],
8963
+ grades: {
8964
+ "realschule-plus": ["7", "8", "9", "10"],
8965
+ gymnasium: ["7", "8", "9", "10"]
8966
+ },
8967
+ subjects: {
8968
+ "realschule-plus": [
8969
+ { id: "mathematik", label: "Mathematik" },
8970
+ { id: "informatik", label: "Informatik" },
8971
+ { id: "physik", label: "Physik" },
8972
+ { id: "chemie", label: "Chemie" },
8973
+ { id: "biologie", label: "Biologie" }
8974
+ ],
8975
+ gymnasium: [
8976
+ { id: "mathematik", label: "Mathematik" },
8977
+ { id: "informatik", label: "Informatik" },
8978
+ { id: "physik", label: "Physik" },
8979
+ { id: "chemie", label: "Chemie" },
8980
+ { id: "biologie", label: "Biologie" }
8981
+ ]
8982
+ },
8983
+ tracks: {},
8984
+ topics: {
8985
+ "realschule-plus|10|mathematik": [
8986
+ { id: "funktionen", label: "Funktionen" }
8987
+ ],
8988
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
8989
+ "realschule-plus|9|physik": [{ id: "mechanik", label: "Mechanik" }],
8990
+ "realschule-plus|9|chemie": [{ id: "reaktionen", label: "Reaktionen" }],
8991
+ "realschule-plus|9|biologie": [{ id: "zellen", label: "Zellen" }],
8992
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
8993
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Bindungen" }],
8994
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
8995
+ },
8996
+ contentUrls: {
8997
+ "realschule-plus|10|mathematik": "https://lehrplaene.bildung-rp.de/",
8998
+ "gymnasium|10|mathematik": "https://lehrplaene.bildung-rp.de/",
8999
+ "realschule-plus|9|physik": "https://lehrplaene.bildung-rp.de/",
9000
+ "realschule-plus|9|chemie": "https://lehrplaene.bildung-rp.de/",
9001
+ "realschule-plus|9|biologie": "https://lehrplaene.bildung-rp.de/",
9002
+ "gymnasium|9|physik": "https://lehrplaene.bildung-rp.de/",
9003
+ "gymnasium|9|chemie": "https://lehrplaene.bildung-rp.de/",
9004
+ "gymnasium|9|biologie": "https://lehrplaene.bildung-rp.de/"
9005
+ }
9006
+ };
9007
+
9008
+ // src/cli/curriculum/providers/lehrplaene-rp/index.ts
9009
+ var lehrplaeneRpProvider = {
9010
+ id: "lehrplaene-rp",
9011
+ country: "DE",
9012
+ countryLabel: "Deutschland",
9013
+ region: "RP",
9014
+ regionLabel: "Rheinland-Pfalz",
9015
+ label: "Lehrpl\xE4ne (Rheinland-Pfalz)",
9016
+ listSchoolTypes() {
9017
+ return LEHRPLAENE_RP_MANIFEST.schoolTypes;
9018
+ },
9019
+ listGrades(schoolType) {
9020
+ return (LEHRPLAENE_RP_MANIFEST.grades[schoolType] || []).map((id) => ({
9021
+ id,
9022
+ label: `Klasse ${id}`
9023
+ }));
9024
+ },
9025
+ listSubjects(schoolType, _grade) {
9026
+ return LEHRPLAENE_RP_MANIFEST.subjects[schoolType] || [];
9027
+ },
9028
+ listTracks(schoolType, grade, subject) {
9029
+ const key = `${schoolType}|${grade}|${subject}`;
9030
+ return LEHRPLAENE_RP_MANIFEST.tracks[key] || [];
9031
+ },
9032
+ listTopics(selection) {
9033
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
9034
+ const list = LEHRPLAENE_RP_MANIFEST.topics[key] || [];
9035
+ return list.map((t2) => ({
9036
+ ...t2,
9037
+ sourceRef: key
9038
+ }));
9039
+ },
9040
+ resolveTopic(topic) {
9041
+ const uri = LEHRPLAENE_RP_MANIFEST.contentUrls[topic.sourceRef];
9042
+ if (!uri) {
9043
+ throw new Error(
9044
+ `Lehrpl\xE4ne RP: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
9045
+ );
9046
+ }
9047
+ return {
9048
+ provider: "lehrplaene-rp",
9049
+ topicId: `${topic.sourceRef}#${topic.id}`,
9050
+ uri
9051
+ };
9052
+ },
9053
+ extractTopics(html, topicIds) {
9054
+ const results = {};
9055
+ const headingMatches = Array.from(
9056
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
9057
+ );
9058
+ const sections = [];
9059
+ for (const m of headingMatches) {
9060
+ const headerText = cleanHtmlText8(m[1]).trim();
9061
+ const start = m.index + m[0].length;
9062
+ const nextHeading = html.indexOf("<h", start);
9063
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
9064
+ const content = cleanHtmlText8(rawContent).slice(0, 1500);
9065
+ if (headerText) sections.push({ header: headerText, content });
9066
+ }
9067
+ for (const topicId of topicIds) {
9068
+ const hashIdx = topicId.indexOf("#");
9069
+ if (hashIdx === -1) continue;
9070
+ const key = topicId.substring(0, hashIdx);
9071
+ const shortId = topicId.substring(hashIdx + 1);
9072
+ const list = LEHRPLAENE_RP_MANIFEST.topics[key] ?? [];
9073
+ const match = list.find((t2) => t2.id === shortId);
9074
+ if (!match) continue;
9075
+ const label = match.label;
9076
+ const normalizedLabel = normalizeForComparison8(label);
9077
+ let section = sections.find(
9078
+ (s) => normalizeForComparison8(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
9079
+ );
9080
+ if (!section && sections.length > 0) {
9081
+ section = sections.find((s) => s.content.length > 80) || sections[0];
9082
+ }
9083
+ if (section) {
9084
+ results[topicId] = `${label}
9085
+
9086
+ ${section.content}`.trim();
9087
+ } else {
9088
+ results[topicId] = label;
9089
+ }
9090
+ }
9091
+ return results;
9092
+ }
9093
+ };
9094
+ function cleanHtmlText8(html) {
9095
+ let text = html.replace(
9096
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
9097
+ " "
9098
+ );
9099
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
9100
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
9101
+ text = text.replace(/<p[^>]*>/gi, "\n");
9102
+ text = text.replace(/<br\s*\/?>/gi, "\n");
9103
+ text = text.replace(/<[^>]+>/g, " ");
9104
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
9105
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9106
+ }
9107
+ function normalizeForComparison8(str) {
9108
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9109
+ }
9110
+
9111
+ // src/cli/curriculum/providers/lehrplan-saarland/manifest.ts
9112
+ var LEHRPLAN_SAARLAND_MANIFEST = {
9113
+ schoolYear: "2025/2026",
9114
+ capturedOn: "2026-07-02",
9115
+ sourceRevision: "Lehrpl\xE4ne Saarland",
9116
+ schoolTypes: [
9117
+ { id: "gemeinschaftsschule", label: "Gemeinschaftsschule" },
9118
+ { id: "gymnasium", label: "Gymnasium" }
9119
+ ],
9120
+ grades: {
9121
+ gemeinschaftsschule: ["7", "8", "9", "10"],
9122
+ gymnasium: ["7", "8", "9", "10"]
9123
+ },
9124
+ subjects: {
9125
+ gemeinschaftsschule: [
9126
+ { id: "mathematik", label: "Mathematik" },
9127
+ { id: "informatik", label: "Informatik" },
9128
+ { id: "physik", label: "Physik" },
9129
+ { id: "chemie", label: "Chemie" },
9130
+ { id: "biologie", label: "Biologie" }
9131
+ ],
9132
+ gymnasium: [
9133
+ { id: "mathematik", label: "Mathematik" },
9134
+ { id: "informatik", label: "Informatik" },
9135
+ { id: "physik", label: "Physik" },
9136
+ { id: "chemie", label: "Chemie" },
9137
+ { id: "biologie", label: "Biologie" }
9138
+ ]
9139
+ },
9140
+ tracks: {},
9141
+ topics: {
9142
+ "gemeinschaftsschule|10|mathematik": [
9143
+ { id: "funktionen", label: "Funktionen" }
9144
+ ],
9145
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
9146
+ "gemeinschaftsschule|9|physik": [{ id: "mechanik", label: "Mechanik" }],
9147
+ "gemeinschaftsschule|9|chemie": [{ id: "reaktionen", label: "Reaktionen" }],
9148
+ "gemeinschaftsschule|9|biologie": [{ id: "zellen", label: "Zellen" }],
9149
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
9150
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Bindungen" }],
9151
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
9152
+ },
9153
+ contentUrls: {
9154
+ "gemeinschaftsschule|10|mathematik": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen",
9155
+ "gymnasium|10|mathematik": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen",
9156
+ "gemeinschaftsschule|9|physik": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen",
9157
+ "gemeinschaftsschule|9|chemie": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen",
9158
+ "gemeinschaftsschule|9|biologie": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen",
9159
+ "gymnasium|9|physik": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen",
9160
+ "gymnasium|9|chemie": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen",
9161
+ "gymnasium|9|biologie": "https://www.saarland.de/mbk/DE/portale/bildungsserver/bildungsthemen/lehrplaenehandreichungen"
9162
+ }
9163
+ };
9164
+
9165
+ // src/cli/curriculum/providers/lehrplan-saarland/index.ts
9166
+ var lehrplanSaarlandProvider = {
9167
+ id: "lehrplan-saarland",
9168
+ country: "DE",
9169
+ countryLabel: "Deutschland",
9170
+ region: "SL",
9171
+ regionLabel: "Saarland",
9172
+ label: "Lehrplan (Saarland)",
9173
+ listSchoolTypes() {
9174
+ return LEHRPLAN_SAARLAND_MANIFEST.schoolTypes;
9175
+ },
9176
+ listGrades(schoolType) {
9177
+ return (LEHRPLAN_SAARLAND_MANIFEST.grades[schoolType] || []).map((id) => ({
9178
+ id,
9179
+ label: `Klasse ${id}`
9180
+ }));
9181
+ },
9182
+ listSubjects(schoolType, _grade) {
9183
+ return LEHRPLAN_SAARLAND_MANIFEST.subjects[schoolType] || [];
9184
+ },
9185
+ listTracks(schoolType, grade, subject) {
9186
+ const key = `${schoolType}|${grade}|${subject}`;
9187
+ return LEHRPLAN_SAARLAND_MANIFEST.tracks[key] || [];
9188
+ },
9189
+ listTopics(selection) {
9190
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
9191
+ const list = LEHRPLAN_SAARLAND_MANIFEST.topics[key] || [];
9192
+ return list.map((t2) => ({
9193
+ ...t2,
9194
+ sourceRef: key
9195
+ }));
9196
+ },
9197
+ resolveTopic(topic) {
9198
+ const uri = LEHRPLAN_SAARLAND_MANIFEST.contentUrls[topic.sourceRef];
9199
+ if (!uri) {
9200
+ throw new Error(
9201
+ `Lehrplan Saarland: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
9202
+ );
9203
+ }
9204
+ return {
9205
+ provider: "lehrplan-saarland",
9206
+ topicId: `${topic.sourceRef}#${topic.id}`,
9207
+ uri
9208
+ };
9209
+ },
9210
+ extractTopics(html, topicIds) {
9211
+ const results = {};
9212
+ const headingMatches = Array.from(
9213
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
9214
+ );
9215
+ const sections = [];
9216
+ for (const m of headingMatches) {
9217
+ const headerText = cleanHtmlText9(m[1]).trim();
9218
+ const start = m.index + m[0].length;
9219
+ const nextHeading = html.indexOf("<h", start);
9220
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
9221
+ const content = cleanHtmlText9(rawContent).slice(0, 1500);
9222
+ if (headerText) sections.push({ header: headerText, content });
9223
+ }
9224
+ for (const topicId of topicIds) {
9225
+ const hashIdx = topicId.indexOf("#");
9226
+ if (hashIdx === -1) continue;
9227
+ const key = topicId.substring(0, hashIdx);
9228
+ const shortId = topicId.substring(hashIdx + 1);
9229
+ const list = LEHRPLAN_SAARLAND_MANIFEST.topics[key] ?? [];
9230
+ const match = list.find((t2) => t2.id === shortId);
9231
+ if (!match) continue;
9232
+ const label = match.label;
9233
+ const normalizedLabel = normalizeForComparison9(label);
9234
+ let section = sections.find(
9235
+ (s) => normalizeForComparison9(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
9236
+ );
9237
+ if (!section && sections.length > 0) {
9238
+ section = sections.find((s) => s.content.length > 80) || sections[0];
9239
+ }
9240
+ if (section) {
9241
+ results[topicId] = `${label}
9242
+
9243
+ ${section.content}`.trim();
9244
+ } else {
9245
+ results[topicId] = label;
9246
+ }
9247
+ }
9248
+ return results;
9249
+ }
9250
+ };
9251
+ function cleanHtmlText9(html) {
9252
+ let text = html.replace(
9253
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
9254
+ " "
9255
+ );
9256
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
9257
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
9258
+ text = text.replace(/<p[^>]*>/gi, "\n");
9259
+ text = text.replace(/<br\s*\/?>/gi, "\n");
9260
+ text = text.replace(/<[^>]+>/g, " ");
9261
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
9262
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9263
+ }
9264
+ function normalizeForComparison9(str) {
9265
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9266
+ }
9267
+
9268
+ // src/cli/curriculum/providers/lehrplan-sachsen/manifest.ts
9269
+ var LEHRPLAN_SACHSEN_MANIFEST = {
9270
+ schoolYear: "2025/2026",
9271
+ capturedOn: "2026-07-02",
9272
+ sourceRevision: "S\xE4chsische Lehrpl\xE4ne (Datenbank)",
9273
+ schoolTypes: [
9274
+ { id: "oberschule", label: "Oberschule (Realschule)" },
9275
+ { id: "gymnasium", label: "Gymnasium" }
9276
+ ],
9277
+ grades: {
9278
+ oberschule: ["7", "8", "9", "10"],
9279
+ gymnasium: ["7", "8", "9", "10"]
9280
+ },
9281
+ subjects: {
9282
+ oberschule: [
9283
+ { id: "mathematik", label: "Mathematik" },
9284
+ { id: "informatik", label: "Informatik" },
9285
+ { id: "physik", label: "Physik" },
9286
+ { id: "chemie", label: "Chemie" },
9287
+ { id: "biologie", label: "Biologie" }
9288
+ ],
9289
+ gymnasium: [
9290
+ { id: "mathematik", label: "Mathematik" },
9291
+ { id: "informatik", label: "Informatik" },
9292
+ { id: "physik", label: "Physik" },
9293
+ { id: "chemie", label: "Chemie" },
9294
+ { id: "biologie", label: "Biologie" }
9295
+ ]
9296
+ },
9297
+ tracks: {},
9298
+ topics: {
9299
+ "oberschule|10|mathematik": [
9300
+ { id: "prozentrechnung", label: "Prozentrechnung und Zinsrechnung" },
9301
+ { id: "funktionen", label: "Funktionale Zusammenh\xE4nge" },
9302
+ { id: "geometrie", label: "Geometrie und Messen" }
9303
+ ],
9304
+ "gymnasium|10|mathematik": [
9305
+ { id: "analysis", label: "Analysis und Funktionen" },
9306
+ { id: "geometrie", label: "Analytische Geometrie" }
9307
+ ],
9308
+ "oberschule|9|informatik": [
9309
+ { id: "algorithmen", label: "Algorithmen und Programmierung" },
9310
+ { id: "daten", label: "Daten und Datenschutz" }
9311
+ ],
9312
+ "oberschule|9|physik": [
9313
+ { id: "mechanik", label: "Mechanik und Bewegung" },
9314
+ { id: "energie", label: "Energie und W\xE4rme" }
9315
+ ],
9316
+ "oberschule|9|chemie": [
9317
+ { id: "stoffe", label: "Stoffe und chemische Reaktionen" },
9318
+ { id: "atome", label: "Atome und Periodensystem" }
9319
+ ],
9320
+ "oberschule|9|biologie": [
9321
+ { id: "zellen", label: "Zellen und Gewebe" },
9322
+ { id: "oekologie", label: "\xD6kologie und Umwelt" }
9323
+ ],
9324
+ "gymnasium|9|physik": [
9325
+ { id: "elektrizitaet", label: "Elektrizit\xE4t und Magnetismus" },
9326
+ { id: "optik", label: "Optik" }
9327
+ ],
9328
+ "gymnasium|9|chemie": [
9329
+ { id: "bindungen", label: "Chemische Bindungen" },
9330
+ { id: "reaktionen", label: "Chemische Reaktionen" }
9331
+ ],
9332
+ "gymnasium|9|biologie": [
9333
+ { id: "genetik", label: "Genetik und Vererbung" },
9334
+ { id: "evolution", label: "Evolution" }
9335
+ ]
9336
+ },
9337
+ contentUrls: {
9338
+ "oberschule|10|mathematik": "https://www.schulportal.sachsen.de/lplandb/",
9339
+ "gymnasium|10|mathematik": "https://www.schulportal.sachsen.de/lplandb/",
9340
+ "oberschule|9|informatik": "https://www.schulportal.sachsen.de/lplandb/",
9341
+ "oberschule|9|physik": "https://www.schulportal.sachsen.de/lplandb/",
9342
+ "oberschule|9|chemie": "https://www.schulportal.sachsen.de/lplandb/",
9343
+ "oberschule|9|biologie": "https://www.schulportal.sachsen.de/lplandb/",
9344
+ "gymnasium|9|physik": "https://www.schulportal.sachsen.de/lplandb/",
9345
+ "gymnasium|9|chemie": "https://www.schulportal.sachsen.de/lplandb/",
9346
+ "gymnasium|9|biologie": "https://www.schulportal.sachsen.de/lplandb/"
9347
+ }
9348
+ };
9349
+
9350
+ // src/cli/curriculum/providers/lehrplan-sachsen/index.ts
9351
+ var lehrplanSachsenProvider = {
9352
+ id: "lehrplan-sachsen",
9353
+ country: "DE",
9354
+ countryLabel: "Deutschland",
9355
+ region: "SN",
9356
+ regionLabel: "Sachsen",
9357
+ label: "Lehrplan (Sachsen)",
9358
+ listSchoolTypes() {
9359
+ return LEHRPLAN_SACHSEN_MANIFEST.schoolTypes;
9360
+ },
9361
+ listGrades(schoolType) {
9362
+ return (LEHRPLAN_SACHSEN_MANIFEST.grades[schoolType] || []).map((id) => ({
9363
+ id,
9364
+ label: `Klasse ${id}`
9365
+ }));
9366
+ },
9367
+ listSubjects(schoolType, _grade) {
9368
+ return LEHRPLAN_SACHSEN_MANIFEST.subjects[schoolType] || [];
9369
+ },
9370
+ listTracks(schoolType, grade, subject) {
9371
+ const key = `${schoolType}|${grade}|${subject}`;
9372
+ return LEHRPLAN_SACHSEN_MANIFEST.tracks[key] || [];
9373
+ },
9374
+ listTopics(selection) {
9375
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
9376
+ const list = LEHRPLAN_SACHSEN_MANIFEST.topics[key] || [];
9377
+ return list.map((t2) => ({
9378
+ ...t2,
9379
+ sourceRef: key
9380
+ }));
9381
+ },
9382
+ resolveTopic(topic) {
9383
+ const uri = LEHRPLAN_SACHSEN_MANIFEST.contentUrls[topic.sourceRef];
9384
+ if (!uri) {
9385
+ throw new Error(
9386
+ `Lehrplan Sachsen: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
9387
+ );
9388
+ }
9389
+ return {
9390
+ provider: "lehrplan-sachsen",
9391
+ topicId: `${topic.sourceRef}#${topic.id}`,
9392
+ uri
9393
+ };
9394
+ },
9395
+ extractTopics(html, topicIds) {
9396
+ const results = {};
9397
+ const headingMatches = Array.from(
9398
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
9399
+ );
9400
+ const sections = [];
9401
+ for (const m of headingMatches) {
9402
+ const headerText = cleanHtmlText10(m[1]).trim();
9403
+ const start = m.index + m[0].length;
9404
+ const nextHeading = html.indexOf("<h", start);
9405
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
9406
+ const content = cleanHtmlText10(rawContent).slice(0, 1500);
9407
+ if (headerText) sections.push({ header: headerText, content });
9408
+ }
9409
+ for (const topicId of topicIds) {
9410
+ const hashIdx = topicId.indexOf("#");
9411
+ if (hashIdx === -1) continue;
9412
+ const key = topicId.substring(0, hashIdx);
9413
+ const shortId = topicId.substring(hashIdx + 1);
9414
+ const list = LEHRPLAN_SACHSEN_MANIFEST.topics[key] ?? [];
9415
+ const match = list.find((t2) => t2.id === shortId);
9416
+ if (!match) continue;
9417
+ const label = match.label;
9418
+ const normalizedLabel = normalizeForComparison10(label);
9419
+ let section = sections.find(
9420
+ (s) => normalizeForComparison10(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
9421
+ );
9422
+ if (!section && sections.length > 0) {
9423
+ section = sections.find((s) => s.content.length > 80) || sections[0];
9424
+ }
9425
+ if (section) {
9426
+ results[topicId] = `${label}
9427
+
9428
+ ${section.content}`.trim();
9429
+ } else {
9430
+ results[topicId] = label;
9431
+ }
9432
+ }
9433
+ return results;
9434
+ }
9435
+ };
9436
+ function cleanHtmlText10(html) {
9437
+ let text = html.replace(
9438
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
9439
+ " "
9440
+ );
9441
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
9442
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
9443
+ text = text.replace(/<p[^>]*>/gi, "\n");
9444
+ text = text.replace(/<br\s*\/?>/gi, "\n");
9445
+ text = text.replace(/<[^>]+>/g, " ");
9446
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
9447
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9448
+ }
9449
+ function normalizeForComparison10(str) {
9450
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9451
+ }
9452
+
9453
+ // src/cli/curriculum/providers/lehrplan-thueringen/manifest.ts
9454
+ var LEHRPLAN_THUERINGEN_MANIFEST = {
9455
+ schoolYear: "2025/2026",
9456
+ capturedOn: "2026-07-02",
9457
+ sourceRevision: "Lehrpl\xE4ne Th\xFCringen",
9458
+ schoolTypes: [
9459
+ { id: "regelschule", label: "Regelschule" },
9460
+ { id: "gymnasium", label: "Gymnasium" }
9461
+ ],
9462
+ grades: {
9463
+ regelschule: ["7", "8", "9", "10"],
9464
+ gymnasium: ["7", "8", "9", "10"]
9465
+ },
9466
+ subjects: {
9467
+ regelschule: [
9468
+ { id: "mathematik", label: "Mathematik" },
9469
+ { id: "informatik", label: "Informatik" },
9470
+ { id: "physik", label: "Physik" },
9471
+ { id: "chemie", label: "Chemie" },
9472
+ { id: "biologie", label: "Biologie" }
9473
+ ],
9474
+ gymnasium: [
9475
+ { id: "mathematik", label: "Mathematik" },
9476
+ { id: "informatik", label: "Informatik" },
9477
+ { id: "physik", label: "Physik" },
9478
+ { id: "chemie", label: "Chemie" },
9479
+ { id: "biologie", label: "Biologie" }
9480
+ ]
9481
+ },
9482
+ tracks: {},
9483
+ topics: {
9484
+ "regelschule|10|mathematik": [{ id: "funktionen", label: "Funktionen" }],
9485
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
9486
+ "regelschule|9|physik": [{ id: "mechanik", label: "Mechanik" }],
9487
+ "regelschule|9|chemie": [{ id: "reaktionen", label: "Reaktionen" }],
9488
+ "regelschule|9|biologie": [{ id: "zellen", label: "Zellen" }],
9489
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
9490
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Bindungen" }],
9491
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
9492
+ },
9493
+ contentUrls: {
9494
+ "regelschule|10|mathematik": "https://www.schulportal-thueringen.de/web/guest/lehrplaene",
9495
+ "gymnasium|10|mathematik": "https://www.schulportal-thueringen.de/web/guest/lehrplaene",
9496
+ "regelschule|9|physik": "https://www.schulportal-thueringen.de/web/guest/lehrplaene",
9497
+ "regelschule|9|chemie": "https://www.schulportal-thueringen.de/web/guest/lehrplaene",
9498
+ "regelschule|9|biologie": "https://www.schulportal-thueringen.de/web/guest/lehrplaene",
9499
+ "gymnasium|9|physik": "https://www.schulportal-thueringen.de/web/guest/lehrplaene",
9500
+ "gymnasium|9|chemie": "https://www.schulportal-thueringen.de/web/guest/lehrplaene",
9501
+ "gymnasium|9|biologie": "https://www.schulportal-thueringen.de/web/guest/lehrplaene"
9502
+ }
9503
+ };
9504
+
9505
+ // src/cli/curriculum/providers/lehrplan-thueringen/index.ts
9506
+ var lehrplanThueringenProvider = {
9507
+ id: "lehrplan-thueringen",
9508
+ country: "DE",
9509
+ countryLabel: "Deutschland",
9510
+ region: "TH",
9511
+ regionLabel: "Th\xFCringen",
9512
+ label: "Lehrplan (Th\xFCringen)",
9513
+ listSchoolTypes() {
9514
+ return LEHRPLAN_THUERINGEN_MANIFEST.schoolTypes;
9515
+ },
9516
+ listGrades(schoolType) {
9517
+ return (LEHRPLAN_THUERINGEN_MANIFEST.grades[schoolType] || []).map((id) => ({
9518
+ id,
9519
+ label: `Klasse ${id}`
9520
+ }));
9521
+ },
9522
+ listSubjects(schoolType, _grade) {
9523
+ return LEHRPLAN_THUERINGEN_MANIFEST.subjects[schoolType] || [];
9524
+ },
9525
+ listTracks(schoolType, grade, subject) {
9526
+ const key = `${schoolType}|${grade}|${subject}`;
9527
+ return LEHRPLAN_THUERINGEN_MANIFEST.tracks[key] || [];
9528
+ },
9529
+ listTopics(selection) {
9530
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
9531
+ const list = LEHRPLAN_THUERINGEN_MANIFEST.topics[key] || [];
9532
+ return list.map((t2) => ({
9533
+ ...t2,
9534
+ sourceRef: key
9535
+ }));
9536
+ },
9537
+ resolveTopic(topic) {
9538
+ const uri = LEHRPLAN_THUERINGEN_MANIFEST.contentUrls[topic.sourceRef];
9539
+ if (!uri) {
9540
+ throw new Error(
9541
+ `Lehrplan Th\xFCringen: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
9542
+ );
9543
+ }
9544
+ return {
9545
+ provider: "lehrplan-thueringen",
9546
+ topicId: `${topic.sourceRef}#${topic.id}`,
9547
+ uri
9548
+ };
9549
+ },
9550
+ extractTopics(html, topicIds) {
9551
+ const results = {};
9552
+ const headingMatches = Array.from(
9553
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
9554
+ );
9555
+ const sections = [];
9556
+ for (const m of headingMatches) {
9557
+ const headerText = cleanHtmlText11(m[1]).trim();
9558
+ const start = m.index + m[0].length;
9559
+ const nextHeading = html.indexOf("<h", start);
9560
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
9561
+ const content = cleanHtmlText11(rawContent).slice(0, 1500);
9562
+ if (headerText) sections.push({ header: headerText, content });
9563
+ }
9564
+ for (const topicId of topicIds) {
9565
+ const hashIdx = topicId.indexOf("#");
9566
+ if (hashIdx === -1) continue;
9567
+ const key = topicId.substring(0, hashIdx);
9568
+ const shortId = topicId.substring(hashIdx + 1);
9569
+ const list = LEHRPLAN_THUERINGEN_MANIFEST.topics[key] ?? [];
9570
+ const match = list.find((t2) => t2.id === shortId);
9571
+ if (!match) continue;
9572
+ const label = match.label;
9573
+ const normalizedLabel = normalizeForComparison11(label);
9574
+ let section = sections.find(
9575
+ (s) => normalizeForComparison11(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
9576
+ );
9577
+ if (!section && sections.length > 0) {
9578
+ section = sections.find((s) => s.content.length > 80) || sections[0];
9579
+ }
9580
+ if (section) {
9581
+ results[topicId] = `${label}
9582
+
9583
+ ${section.content}`.trim();
9584
+ } else {
9585
+ results[topicId] = label;
9586
+ }
9587
+ }
9588
+ return results;
9589
+ }
9590
+ };
9591
+ function cleanHtmlText11(html) {
9592
+ let text = html.replace(
9593
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
9594
+ " "
9595
+ );
9596
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
9597
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
9598
+ text = text.replace(/<p[^>]*>/gi, "\n");
9599
+ text = text.replace(/<br\s*\/?>/gi, "\n");
9600
+ text = text.replace(/<[^>]+>/g, " ");
9601
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
9602
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9603
+ }
9604
+ function normalizeForComparison11(str) {
9605
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9606
+ }
9607
+
9608
+ // src/cli/curriculum/providers/lehrplanplus-bayern/manifest.ts
9609
+ var LEHRPLANPLUS_BAYERN_MANIFEST = {
9610
+ schoolYear: "2026/2027",
9611
+ capturedOn: "2026-07-02",
9612
+ sourceRevision: "LehrplanPLUS Realschule \u2013 Oktober 2023",
9613
+ schoolTypes: [
9614
+ { id: "grundschule", label: "Grundschule" },
9615
+ { id: "mittelschule", label: "Mittelschule" },
9616
+ { id: "foerderschule", label: "F\xF6rderschule" },
9617
+ { id: "realschule", label: "Realschule" },
9618
+ { id: "gymnasium", label: "Gymnasium" },
9619
+ { id: "wirtschaftsschule", label: "Wirtschaftsschule" },
9620
+ { id: "fos", label: "Fachoberschule" },
9621
+ { id: "bos", label: "Berufsoberschule" }
9622
+ ],
9623
+ grades: {
9624
+ realschule: ["5", "6", "7", "8", "9", "10"]
9625
+ },
9626
+ subjects: {
9627
+ realschule: [
9628
+ {
9629
+ id: "bwl-rechnungswesen",
9630
+ label: "Betriebswirtschaftslehre / Rechnungswesen"
9631
+ },
9632
+ { id: "biologie", label: "Biologie" },
9633
+ { id: "chemie", label: "Chemie" },
9634
+ { id: "deutsch", label: "Deutsch" },
9635
+ { id: "englisch", label: "Englisch" },
9636
+ { id: "ernaehrung_und_gesundheit", label: "Ern\xE4hrung und Gesundheit" },
9637
+ { id: "ethik", label: "Ethik" },
9638
+ {
9639
+ id: "evangelische-religionslehre",
9640
+ label: "Evangelische Religionslehre"
9641
+ },
9642
+ { id: "franzoesisch", label: "Franz\xF6sisch" },
9643
+ { id: "geographie", label: "Geographie" },
9644
+ { id: "geschichte", label: "Geschichte" },
9645
+ { id: "it", label: "Informationstechnologie" },
9646
+ { id: "iu", label: "Islamischer Unterricht" },
9647
+ { id: "ir", label: "Israelitische Religionslehre" },
9648
+ {
9649
+ id: "katholische-religionslehre",
9650
+ label: "Katholische Religionslehre"
9651
+ },
9652
+ { id: "kunst", label: "Kunst" },
9653
+ { id: "mathematik", label: "Mathematik" },
9654
+ { id: "musik", label: "Musik" },
9655
+ { id: "or", label: "Orthodoxe Religionslehre" },
9656
+ { id: "physik", label: "Physik" },
9657
+ { id: "pug", label: "Politik und Gesellschaft" },
9658
+ { id: "soziallehre", label: "Soziallehre" },
9659
+ { id: "sozialwesen", label: "Sozialwesen" },
9660
+ { id: "spanisch", label: "Spanisch" },
9661
+ { id: "sport", label: "Sport" },
9662
+ { id: "textiles-gestalten", label: "Textiles Gestalten" },
9663
+ { id: "werken", label: "Werken" },
9664
+ { id: "wirtschaft-und-recht", label: "Wirtschaft und Recht" }
9665
+ ]
9666
+ },
9667
+ tracks: {
9668
+ "realschule|9|mathematik": [
9669
+ { id: "wpfg1", label: "Mathematik 9 (I)" },
9670
+ { id: "wpfg2-3", label: "Mathematik 9 (II/III)" }
9671
+ ]
9672
+ },
9673
+ topics: {
9674
+ "realschule|9|mathematik|wpfg1": [
9675
+ { id: "lb1", label: "Reelle Zahlen", hours: 10 },
9676
+ { id: "lb2", label: "Zentrische Streckung", hours: 17 },
9677
+ { id: "lb3", label: "Rechtwinklige Dreiecke", hours: 20 },
9678
+ { id: "lb4", label: "Kreis", hours: 10 },
9679
+ { id: "lb5", label: "Raumgeometrie", hours: 20 },
9680
+ { id: "lb6", label: "Systeme linearer Gleichungen", hours: 12 },
9681
+ {
9682
+ id: "lb7",
9683
+ label: "Quadratische Funktionen und quadratische Gleichungen",
9684
+ hours: 42
9685
+ },
9686
+ { id: "lb8", label: "Daten und Zufall", hours: 9 }
9687
+ ],
9688
+ "realschule|9|mathematik|wpfg2-3": [
9689
+ { id: "lb1", label: "Reelle Zahlen", hours: 7 },
9690
+ { id: "lb2", label: "Zentrische Streckung", hours: 13 },
7688
9691
  { id: "lb3", label: "Rechtwinklige Dreiecke", hours: 20 },
7689
9692
  { id: "lb4", label: "Kreis", hours: 10 },
7690
9693
  { id: "lb5", label: "Lineare Funktionen", hours: 15 },
7691
9694
  { id: "lb6", label: "Systeme linearer Gleichungen", hours: 10 },
7692
9695
  { id: "lb7", label: "Daten und Zufall", hours: 9 }
7693
9696
  ],
7694
- "realschule|9|deutsch": [
7695
- { id: "lb1", label: "Sprechen und Zuh\xF6ren" },
7696
- { id: "lb2", label: "Lesen \u2013 mit Texten und weiteren Medien umgehen" },
7697
- { id: "lb3", label: "Schreiben" },
7698
- {
7699
- id: "lb4",
7700
- label: "Sprachgebrauch und Sprache untersuchen und reflektieren"
7701
- }
9697
+ "realschule|9|deutsch": [
9698
+ { id: "lb1", label: "Sprechen und Zuh\xF6ren" },
9699
+ { id: "lb2", label: "Lesen \u2013 mit Texten und weiteren Medien umgehen" },
9700
+ { id: "lb3", label: "Schreiben" },
9701
+ {
9702
+ id: "lb4",
9703
+ label: "Sprachgebrauch und Sprache untersuchen und reflektieren"
9704
+ }
9705
+ ],
9706
+ "realschule|9|englisch": [
9707
+ { id: "lb1", label: "Kommunikative Kompetenzen" },
9708
+ { id: "lb2", label: "Interkulturelle Kompetenzen" },
9709
+ { id: "lb3", label: "Text- und Medienkompetenzen" },
9710
+ { id: "lb4", label: "Methodische Kompetenzen" },
9711
+ { id: "lb5", label: "Themengebiete" }
9712
+ ]
9713
+ },
9714
+ contentUrls: {
9715
+ "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",
9716
+ "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",
9717
+ "realschule|9|deutsch": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/9/fach/deutsch/inhalt/fachlehrplaene",
9718
+ "realschule|9|englisch": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/9/fach/englisch/inhalt/fachlehrplaene"
9719
+ }
9720
+ };
9721
+
9722
+ // src/cli/curriculum/providers/lehrplanplus-bayern/index.ts
9723
+ function levelKey(schoolType, grade, subject, track) {
9724
+ return track ? `${schoolType}|${grade}|${subject}|${track}` : `${schoolType}|${grade}|${subject}`;
9725
+ }
9726
+ var lehrplanplusBayernProvider = {
9727
+ id: "lehrplanplus-bayern",
9728
+ country: "DE",
9729
+ countryLabel: "Deutschland",
9730
+ region: "BY",
9731
+ regionLabel: "Bayern",
9732
+ label: "LehrplanPLUS (Bayern)",
9733
+ listSchoolTypes() {
9734
+ return LEHRPLANPLUS_BAYERN_MANIFEST.schoolTypes;
9735
+ },
9736
+ listGrades(schoolType) {
9737
+ return (LEHRPLANPLUS_BAYERN_MANIFEST.grades[schoolType] ?? []).map((grade) => ({
9738
+ id: grade,
9739
+ label: grade
9740
+ }));
9741
+ },
9742
+ listSubjects(schoolType, _grade) {
9743
+ return LEHRPLANPLUS_BAYERN_MANIFEST.subjects[schoolType] ?? [];
9744
+ },
9745
+ listTracks(schoolType, grade, subject) {
9746
+ return LEHRPLANPLUS_BAYERN_MANIFEST.tracks[levelKey(schoolType, grade, subject)] ?? [];
9747
+ },
9748
+ listTopics(selection) {
9749
+ const { schoolType, grade, subject, track } = selection;
9750
+ if (!schoolType || !grade || !subject) return [];
9751
+ const key = levelKey(schoolType, grade, subject, track);
9752
+ return (LEHRPLANPLUS_BAYERN_MANIFEST.topics[key] ?? []).map((topic) => ({
9753
+ ...topic,
9754
+ sourceRef: key
9755
+ }));
9756
+ },
9757
+ resolveTopic(topic) {
9758
+ const uri = LEHRPLANPLUS_BAYERN_MANIFEST.contentUrls[topic.sourceRef];
9759
+ if (!uri) {
9760
+ throw new Error(
9761
+ `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}.`
9762
+ );
9763
+ }
9764
+ return {
9765
+ provider: "lehrplanplus-bayern",
9766
+ topicId: `${topic.sourceRef}#${topic.id}`,
9767
+ uri
9768
+ };
9769
+ },
9770
+ extractTopics(html, topicIds) {
9771
+ const chunks = html.split('<div id="thema_');
9772
+ const sections = [];
9773
+ for (let i = 1; i < chunks.length; i++) {
9774
+ const chunk = chunks[i];
9775
+ const quoteIdx = chunk.indexOf('"');
9776
+ if (quoteIdx === -1) continue;
9777
+ const id = chunk.slice(0, quoteIdx);
9778
+ const classStart = chunk.indexOf('class="');
9779
+ if (classStart === -1) continue;
9780
+ const classEnd = chunk.indexOf('"', classStart + 7);
9781
+ const classContent = chunk.slice(classStart + 7, classEnd);
9782
+ const lvlMatch = classContent.match(/headline_lvl(\d+)/);
9783
+ if (!lvlMatch) continue;
9784
+ const level = parseInt(lvlMatch[1], 10);
9785
+ const contentHtml = chunk.slice(classEnd + 1);
9786
+ const headerMatch = contentHtml.match(
9787
+ /<a[^>]*class="paragraph_toggle"[^>]*>([\s\S]*?)<\/a>/i
9788
+ );
9789
+ const headerText = headerMatch ? cleanHtmlText12(headerMatch[1]).trim() : "";
9790
+ sections.push({
9791
+ id,
9792
+ level,
9793
+ headerText,
9794
+ contentHtml
9795
+ });
9796
+ }
9797
+ const results = {};
9798
+ for (const topicId of topicIds) {
9799
+ let label = "";
9800
+ for (const key of Object.keys(LEHRPLANPLUS_BAYERN_MANIFEST.topics)) {
9801
+ const list = LEHRPLANPLUS_BAYERN_MANIFEST.topics[key];
9802
+ const match = list.find(
9803
+ (t2) => t2.id === topicId || `${key}#${t2.id}` === topicId
9804
+ );
9805
+ if (match) {
9806
+ label = match.label;
9807
+ break;
9808
+ }
9809
+ }
9810
+ if (!label) {
9811
+ continue;
9812
+ }
9813
+ const normalizedLabel = normalizeForComparison12(label);
9814
+ const lvl1Index = sections.findIndex((s) => {
9815
+ if (s.level !== 1) return false;
9816
+ const normalizedHeader = normalizeForComparison12(s.headerText);
9817
+ return normalizedHeader.includes(normalizedLabel);
9818
+ });
9819
+ if (lvl1Index === -1) {
9820
+ continue;
9821
+ }
9822
+ const collectedSections = [sections[lvl1Index]];
9823
+ for (let i = lvl1Index + 1; i < sections.length; i++) {
9824
+ if (sections[i].level === 1) {
9825
+ break;
9826
+ }
9827
+ collectedSections.push(sections[i]);
9828
+ }
9829
+ const fullHtml = collectedSections.map((s) => s.contentHtml).join("\n");
9830
+ results[topicId] = cleanHtmlText12(fullHtml);
9831
+ }
9832
+ return results;
9833
+ }
9834
+ };
9835
+ function cleanHtmlText12(html) {
9836
+ let text = html.replace(
9837
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
9838
+ " "
9839
+ );
9840
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
9841
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
9842
+ text = text.replace(/<p[^>]*>/gi, "\n");
9843
+ text = text.replace(/<br\s*\/?>/gi, "\n");
9844
+ text = text.replace(/<[^>]+>/g, " ");
9845
+ 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");
9846
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
9847
+ }
9848
+ function normalizeForComparison12(str) {
9849
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
9850
+ }
9851
+
9852
+ // src/cli/curriculum/providers/rahmenlehrplan-berlin-brandenburg/manifest.ts
9853
+ var RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST = {
9854
+ schoolYear: "2025/2026",
9855
+ capturedOn: "2026-07-02",
9856
+ sourceRevision: "Gemeinsamer Rahmenlehrplan Berlin-Brandenburg",
9857
+ schoolTypes: [
9858
+ { id: "realschule", label: "Realschule" },
9859
+ { id: "gymnasium", label: "Gymnasium" }
9860
+ ],
9861
+ grades: {
9862
+ realschule: ["7", "8", "9", "10"],
9863
+ gymnasium: ["7", "8", "9", "10"]
9864
+ },
9865
+ subjects: {
9866
+ realschule: [
9867
+ { id: "mathematik", label: "Mathematik" },
9868
+ { id: "informatik", label: "Informatik" },
9869
+ { id: "physik", label: "Physik" },
9870
+ { id: "chemie", label: "Chemie" },
9871
+ { id: "biologie", label: "Biologie" }
9872
+ ],
9873
+ gymnasium: [
9874
+ { id: "mathematik", label: "Mathematik" },
9875
+ { id: "informatik", label: "Informatik" },
9876
+ { id: "physik", label: "Physik" },
9877
+ { id: "chemie", label: "Chemie" },
9878
+ { id: "biologie", label: "Biologie" }
9879
+ ]
9880
+ },
9881
+ tracks: {},
9882
+ topics: {
9883
+ "realschule|10|mathematik": [
9884
+ { id: "funktionen", label: "Funktionen" },
9885
+ { id: "geometrie", label: "Geometrie" },
9886
+ { id: "stochastik", label: "Stochastik" }
9887
+ ],
9888
+ "gymnasium|10|mathematik": [
9889
+ { id: "analysis", label: "Analysis" },
9890
+ { id: "vektoren", label: "Vektoren" }
7702
9891
  ],
7703
- "realschule|9|englisch": [
7704
- { id: "lb1", label: "Kommunikative Kompetenzen" },
7705
- { id: "lb2", label: "Interkulturelle Kompetenzen" },
7706
- { id: "lb3", label: "Text- und Medienkompetenzen" },
7707
- { id: "lb4", label: "Methodische Kompetenzen" },
7708
- { id: "lb5", label: "Themengebiete" }
9892
+ "realschule|9|informatik": [
9893
+ { id: "algorithmen", label: "Algorithmen" },
9894
+ { id: "daten", label: "Daten und Information" }
9895
+ ],
9896
+ "realschule|9|physik": [
9897
+ { id: "mechanik", label: "Mechanik" },
9898
+ { id: "elektrizitaet", label: "Elektrizit\xE4t" }
9899
+ ],
9900
+ "realschule|9|chemie": [
9901
+ { id: "reaktionen", label: "Chemische Reaktionen" },
9902
+ { id: "stoffe", label: "Stoffe" }
9903
+ ],
9904
+ "realschule|9|biologie": [
9905
+ { id: "zellen", label: "Zellen" },
9906
+ { id: "oekologie", label: "\xD6kologie" }
9907
+ ],
9908
+ "gymnasium|9|physik": [
9909
+ { id: "optik", label: "Optik" },
9910
+ { id: "felder", label: "Felder" }
9911
+ ],
9912
+ "gymnasium|9|chemie": [
9913
+ { id: "bindungen", label: "Bindungen" },
9914
+ { id: "saeuren", label: "S\xE4uren und Basen" }
9915
+ ],
9916
+ "gymnasium|9|biologie": [
9917
+ { id: "genetik", label: "Genetik" },
9918
+ { id: "evolution", label: "Evolution" }
7709
9919
  ]
7710
9920
  },
7711
9921
  contentUrls: {
7712
- "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",
7713
- "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",
7714
- "realschule|9|deutsch": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/9/fach/deutsch/inhalt/fachlehrplaene",
7715
- "realschule|9|englisch": "https://www.lehrplanplus.bayern.de/schulart/realschule/jgs/9/fach/englisch/inhalt/fachlehrplaene"
9922
+ "realschule|10|mathematik": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/mathematik",
9923
+ "gymnasium|10|mathematik": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/mathematik",
9924
+ "realschule|9|informatik": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/informatik",
9925
+ "realschule|9|physik": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/physik",
9926
+ "realschule|9|chemie": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/chemie",
9927
+ "realschule|9|biologie": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/biologie",
9928
+ "gymnasium|9|physik": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/physik",
9929
+ "gymnasium|9|chemie": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/chemie",
9930
+ "gymnasium|9|biologie": "https://bildungsserver.berlin-brandenburg.de/rlp-online/c-faecher/biologie"
7716
9931
  }
7717
9932
  };
7718
9933
 
7719
- // src/cli/curriculum/providers/lehrplanplus-bayern/index.ts
7720
- function levelKey(schoolType, grade, subject, track) {
7721
- return track ? `${schoolType}|${grade}|${subject}|${track}` : `${schoolType}|${grade}|${subject}`;
9934
+ // src/cli/curriculum/providers/rahmenlehrplan-berlin-brandenburg/index.ts
9935
+ var rahmenlehrplanBerlinBrandenburgProvider = {
9936
+ id: "rahmenlehrplan-berlin-brandenburg",
9937
+ country: "DE",
9938
+ countryLabel: "Deutschland",
9939
+ region: "BE-BB",
9940
+ regionLabel: "Berlin / Brandenburg",
9941
+ label: "Rahmenlehrplan (Berlin-Brandenburg)",
9942
+ listSchoolTypes() {
9943
+ return RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST.schoolTypes;
9944
+ },
9945
+ listGrades(schoolType) {
9946
+ return (RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST.grades[schoolType] || []).map((id) => ({
9947
+ id,
9948
+ label: `Klasse ${id}`
9949
+ }));
9950
+ },
9951
+ listSubjects(schoolType, _grade) {
9952
+ return RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST.subjects[schoolType] || [];
9953
+ },
9954
+ listTracks(schoolType, grade, subject) {
9955
+ const key = `${schoolType}|${grade}|${subject}`;
9956
+ return RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST.tracks[key] || [];
9957
+ },
9958
+ listTopics(selection) {
9959
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
9960
+ const list = RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST.topics[key] || [];
9961
+ return list.map((t2) => ({
9962
+ ...t2,
9963
+ sourceRef: key
9964
+ }));
9965
+ },
9966
+ resolveTopic(topic) {
9967
+ const uri = RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST.contentUrls[topic.sourceRef];
9968
+ if (!uri) {
9969
+ throw new Error(
9970
+ `Rahmenlehrplan Berlin-Brandenburg: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
9971
+ );
9972
+ }
9973
+ return {
9974
+ provider: "rahmenlehrplan-berlin-brandenburg",
9975
+ topicId: `${topic.sourceRef}#${topic.id}`,
9976
+ uri
9977
+ };
9978
+ },
9979
+ extractTopics(html, topicIds) {
9980
+ const results = {};
9981
+ const headingMatches = Array.from(
9982
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
9983
+ );
9984
+ const sections = [];
9985
+ for (const m of headingMatches) {
9986
+ const headerText = cleanHtmlText13(m[1]).trim();
9987
+ const start = m.index + m[0].length;
9988
+ const nextHeading = html.indexOf("<h", start);
9989
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
9990
+ const content = cleanHtmlText13(rawContent).slice(0, 1500);
9991
+ if (headerText) sections.push({ header: headerText, content });
9992
+ }
9993
+ for (const topicId of topicIds) {
9994
+ const hashIdx = topicId.indexOf("#");
9995
+ if (hashIdx === -1) continue;
9996
+ const key = topicId.substring(0, hashIdx);
9997
+ const shortId = topicId.substring(hashIdx + 1);
9998
+ const list = RAHMENLEHRPLAN_BERLIN_BRANDENBURG_MANIFEST.topics[key] ?? [];
9999
+ const match = list.find((t2) => t2.id === shortId);
10000
+ if (!match) continue;
10001
+ const label = match.label;
10002
+ const normalizedLabel = normalizeForComparison13(label);
10003
+ let section = sections.find(
10004
+ (s) => normalizeForComparison13(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
10005
+ );
10006
+ if (!section && sections.length > 0) {
10007
+ section = sections.find((s) => s.content.length > 80) || sections[0];
10008
+ }
10009
+ if (section) {
10010
+ results[topicId] = `${label}
10011
+
10012
+ ${section.content}`.trim();
10013
+ } else {
10014
+ results[topicId] = label;
10015
+ }
10016
+ }
10017
+ return results;
10018
+ }
10019
+ };
10020
+ function cleanHtmlText13(html) {
10021
+ let text = html.replace(
10022
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
10023
+ " "
10024
+ );
10025
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
10026
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
10027
+ text = text.replace(/<p[^>]*>/gi, "\n");
10028
+ text = text.replace(/<br\s*\/?>/gi, "\n");
10029
+ text = text.replace(/<[^>]+>/g, " ");
10030
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
10031
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
7722
10032
  }
7723
- var lehrplanplusBayernProvider = {
7724
- id: "lehrplanplus-bayern",
10033
+ function normalizeForComparison13(str) {
10034
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
10035
+ }
10036
+
10037
+ // src/cli/curriculum/providers/rahmenplan-mv/manifest.ts
10038
+ var RAHMENPLAN_MV_MANIFEST = {
10039
+ schoolYear: "2025/2026",
10040
+ capturedOn: "2026-07-02",
10041
+ sourceRevision: "Rahmenpl\xE4ne Mecklenburg-Vorpommern",
10042
+ schoolTypes: [
10043
+ { id: "regionale-schule", label: "Regionale Schule" },
10044
+ { id: "gymnasium", label: "Gymnasium" }
10045
+ ],
10046
+ grades: {
10047
+ "regionale-schule": ["7", "8", "9", "10"],
10048
+ gymnasium: ["7", "8", "9", "10"]
10049
+ },
10050
+ subjects: {
10051
+ "regionale-schule": [
10052
+ { id: "mathematik", label: "Mathematik" },
10053
+ { id: "informatik", label: "Informatik" },
10054
+ { id: "physik", label: "Physik" },
10055
+ { id: "chemie", label: "Chemie" },
10056
+ { id: "biologie", label: "Biologie" }
10057
+ ],
10058
+ gymnasium: [
10059
+ { id: "mathematik", label: "Mathematik" },
10060
+ { id: "informatik", label: "Informatik" },
10061
+ { id: "physik", label: "Physik" },
10062
+ { id: "chemie", label: "Chemie" },
10063
+ { id: "biologie", label: "Biologie" }
10064
+ ]
10065
+ },
10066
+ tracks: {},
10067
+ topics: {
10068
+ "regionale-schule|10|mathematik": [
10069
+ { id: "funktionen", label: "Funktionen" }
10070
+ ],
10071
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
10072
+ "regionale-schule|9|physik": [{ id: "mechanik", label: "Mechanik" }],
10073
+ "regionale-schule|9|chemie": [{ id: "reaktionen", label: "Reaktionen" }],
10074
+ "regionale-schule|9|biologie": [{ id: "zellen", label: "Zellen" }],
10075
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
10076
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Bindungen" }],
10077
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
10078
+ },
10079
+ contentUrls: {
10080
+ "regionale-schule|10|mathematik": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/",
10081
+ "gymnasium|10|mathematik": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/",
10082
+ "regionale-schule|9|physik": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/",
10083
+ "regionale-schule|9|chemie": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/",
10084
+ "regionale-schule|9|biologie": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/",
10085
+ "gymnasium|9|physik": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/",
10086
+ "gymnasium|9|chemie": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/",
10087
+ "gymnasium|9|biologie": "https://www.bildung-mv.de/schueler/schule-und-unterricht/faecher-und-rahmenplaene/rahmenplaene-an-allgemeinbildenden-schulen/"
10088
+ }
10089
+ };
10090
+
10091
+ // src/cli/curriculum/providers/rahmenplan-mv/index.ts
10092
+ var rahmenplanMvProvider = {
10093
+ id: "rahmenplan-mv",
7725
10094
  country: "DE",
7726
10095
  countryLabel: "Deutschland",
7727
- region: "BY",
7728
- regionLabel: "Bayern",
7729
- label: "LehrplanPLUS (Bayern)",
10096
+ region: "MV",
10097
+ regionLabel: "Mecklenburg-Vorpommern",
10098
+ label: "Rahmenplan (Mecklenburg-Vorpommern)",
7730
10099
  listSchoolTypes() {
7731
- return LEHRPLANPLUS_BAYERN_MANIFEST.schoolTypes;
10100
+ return RAHMENPLAN_MV_MANIFEST.schoolTypes;
7732
10101
  },
7733
10102
  listGrades(schoolType) {
7734
- return (LEHRPLANPLUS_BAYERN_MANIFEST.grades[schoolType] ?? []).map((grade) => ({
7735
- id: grade,
7736
- label: grade
10103
+ return (RAHMENPLAN_MV_MANIFEST.grades[schoolType] || []).map((id) => ({
10104
+ id,
10105
+ label: `Klasse ${id}`
7737
10106
  }));
7738
10107
  },
7739
10108
  listSubjects(schoolType, _grade) {
7740
- return LEHRPLANPLUS_BAYERN_MANIFEST.subjects[schoolType] ?? [];
10109
+ return RAHMENPLAN_MV_MANIFEST.subjects[schoolType] || [];
7741
10110
  },
7742
10111
  listTracks(schoolType, grade, subject) {
7743
- return LEHRPLANPLUS_BAYERN_MANIFEST.tracks[levelKey(schoolType, grade, subject)] ?? [];
10112
+ const key = `${schoolType}|${grade}|${subject}`;
10113
+ return RAHMENPLAN_MV_MANIFEST.tracks[key] || [];
7744
10114
  },
7745
10115
  listTopics(selection) {
7746
- const { schoolType, grade, subject, track } = selection;
7747
- if (!schoolType || !grade || !subject) return [];
7748
- const key = levelKey(schoolType, grade, subject, track);
7749
- return (LEHRPLANPLUS_BAYERN_MANIFEST.topics[key] ?? []).map((topic) => ({
7750
- ...topic,
10116
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
10117
+ const list = RAHMENPLAN_MV_MANIFEST.topics[key] || [];
10118
+ return list.map((t2) => ({
10119
+ ...t2,
7751
10120
  sourceRef: key
7752
10121
  }));
7753
10122
  },
7754
10123
  resolveTopic(topic) {
7755
- const uri = LEHRPLANPLUS_BAYERN_MANIFEST.contentUrls[topic.sourceRef];
10124
+ const uri = RAHMENPLAN_MV_MANIFEST.contentUrls[topic.sourceRef];
7756
10125
  if (!uri) {
7757
10126
  throw new Error(
7758
- `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}.`
10127
+ `Rahmenplan MV: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
7759
10128
  );
7760
10129
  }
7761
10130
  return {
7762
- provider: "lehrplanplus-bayern",
10131
+ provider: "rahmenplan-mv",
7763
10132
  topicId: `${topic.sourceRef}#${topic.id}`,
7764
10133
  uri
7765
10134
  };
7766
10135
  },
7767
10136
  extractTopics(html, topicIds) {
7768
- const chunks = html.split('<div id="thema_');
10137
+ const results = {};
10138
+ const headingMatches = Array.from(
10139
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
10140
+ );
7769
10141
  const sections = [];
7770
- for (let i = 1; i < chunks.length; i++) {
7771
- const chunk = chunks[i];
7772
- const quoteIdx = chunk.indexOf('"');
7773
- if (quoteIdx === -1) continue;
7774
- const id = chunk.slice(0, quoteIdx);
7775
- const classStart = chunk.indexOf('class="');
7776
- if (classStart === -1) continue;
7777
- const classEnd = chunk.indexOf('"', classStart + 7);
7778
- const classContent = chunk.slice(classStart + 7, classEnd);
7779
- const lvlMatch = classContent.match(/headline_lvl(\d+)/);
7780
- if (!lvlMatch) continue;
7781
- const level = parseInt(lvlMatch[1], 10);
7782
- const contentHtml = chunk.slice(classEnd + 1);
7783
- const headerMatch = contentHtml.match(
7784
- /<a[^>]*class="paragraph_toggle"[^>]*>([\s\S]*?)<\/a>/i
10142
+ for (const m of headingMatches) {
10143
+ const headerText = cleanHtmlText14(m[1]).trim();
10144
+ const start = m.index + m[0].length;
10145
+ const nextHeading = html.indexOf("<h", start);
10146
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
10147
+ const content = cleanHtmlText14(rawContent).slice(0, 1500);
10148
+ if (headerText) sections.push({ header: headerText, content });
10149
+ }
10150
+ for (const topicId of topicIds) {
10151
+ const hashIdx = topicId.indexOf("#");
10152
+ if (hashIdx === -1) continue;
10153
+ const key = topicId.substring(0, hashIdx);
10154
+ const shortId = topicId.substring(hashIdx + 1);
10155
+ const list = RAHMENPLAN_MV_MANIFEST.topics[key] ?? [];
10156
+ const match = list.find((t2) => t2.id === shortId);
10157
+ if (!match) continue;
10158
+ const label = match.label;
10159
+ const normalizedLabel = normalizeForComparison14(label);
10160
+ let section = sections.find(
10161
+ (s) => normalizeForComparison14(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
10162
+ );
10163
+ if (!section && sections.length > 0) {
10164
+ section = sections.find((s) => s.content.length > 80) || sections[0];
10165
+ }
10166
+ if (section) {
10167
+ results[topicId] = `${label}
10168
+
10169
+ ${section.content}`.trim();
10170
+ } else {
10171
+ results[topicId] = label;
10172
+ }
10173
+ }
10174
+ return results;
10175
+ }
10176
+ };
10177
+ function cleanHtmlText14(html) {
10178
+ let text = html.replace(
10179
+ /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
10180
+ " "
10181
+ );
10182
+ text = text.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n\n$1\n\n");
10183
+ text = text.replace(/<li[^>]*>/gi, "\n- ");
10184
+ text = text.replace(/<p[^>]*>/gi, "\n");
10185
+ text = text.replace(/<br\s*\/?>/gi, "\n");
10186
+ text = text.replace(/<[^>]+>/g, " ");
10187
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
10188
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
10189
+ }
10190
+ function normalizeForComparison14(str) {
10191
+ return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
10192
+ }
10193
+
10194
+ // src/cli/curriculum/providers/rahmenrichtlinien-st/manifest.ts
10195
+ var RAHMENRICHTLINIEN_ST_MANIFEST = {
10196
+ schoolYear: "2025/2026",
10197
+ capturedOn: "2026-07-02",
10198
+ sourceRevision: "Rahmenrichtlinien Sachsen-Anhalt",
10199
+ schoolTypes: [
10200
+ { id: "sekundarschule", label: "Sekundarschule" },
10201
+ { id: "gymnasium", label: "Gymnasium" }
10202
+ ],
10203
+ grades: {
10204
+ sekundarschule: ["7", "8", "9", "10"],
10205
+ gymnasium: ["7", "8", "9", "10"]
10206
+ },
10207
+ subjects: {
10208
+ sekundarschule: [
10209
+ { id: "mathematik", label: "Mathematik" },
10210
+ { id: "informatik", label: "Informatik" },
10211
+ { id: "physik", label: "Physik" },
10212
+ { id: "chemie", label: "Chemie" },
10213
+ { id: "biologie", label: "Biologie" }
10214
+ ],
10215
+ gymnasium: [
10216
+ { id: "mathematik", label: "Mathematik" },
10217
+ { id: "informatik", label: "Informatik" },
10218
+ { id: "physik", label: "Physik" },
10219
+ { id: "chemie", label: "Chemie" },
10220
+ { id: "biologie", label: "Biologie" }
10221
+ ]
10222
+ },
10223
+ tracks: {},
10224
+ topics: {
10225
+ "sekundarschule|10|mathematik": [{ id: "funktionen", label: "Funktionen" }],
10226
+ "gymnasium|10|mathematik": [{ id: "analysis", label: "Analysis" }],
10227
+ "sekundarschule|9|physik": [{ id: "mechanik", label: "Mechanik" }],
10228
+ "sekundarschule|9|chemie": [{ id: "reaktionen", label: "Reaktionen" }],
10229
+ "sekundarschule|9|biologie": [{ id: "zellen", label: "Zellen" }],
10230
+ "gymnasium|9|physik": [{ id: "elektrizitaet", label: "Elektrizit\xE4t" }],
10231
+ "gymnasium|9|chemie": [{ id: "bindungen", label: "Bindungen" }],
10232
+ "gymnasium|9|biologie": [{ id: "genetik", label: "Genetik" }]
10233
+ },
10234
+ contentUrls: {
10235
+ "sekundarschule|10|mathematik": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien",
10236
+ "gymnasium|10|mathematik": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien",
10237
+ "sekundarschule|9|physik": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien",
10238
+ "sekundarschule|9|chemie": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien",
10239
+ "sekundarschule|9|biologie": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien",
10240
+ "gymnasium|9|physik": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien",
10241
+ "gymnasium|9|chemie": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien",
10242
+ "gymnasium|9|biologie": "https://lisa.sachsen-anhalt.de/schulqualitaet/lehrplaene-rahmenrichtlinien"
10243
+ }
10244
+ };
10245
+
10246
+ // src/cli/curriculum/providers/rahmenrichtlinien-st/index.ts
10247
+ var rahmenrichtlinienStProvider = {
10248
+ id: "rahmenrichtlinien-st",
10249
+ country: "DE",
10250
+ countryLabel: "Deutschland",
10251
+ region: "ST",
10252
+ regionLabel: "Sachsen-Anhalt",
10253
+ label: "Rahmenrichtlinien (Sachsen-Anhalt)",
10254
+ listSchoolTypes() {
10255
+ return RAHMENRICHTLINIEN_ST_MANIFEST.schoolTypes;
10256
+ },
10257
+ listGrades(schoolType) {
10258
+ return (RAHMENRICHTLINIEN_ST_MANIFEST.grades[schoolType] || []).map((id) => ({
10259
+ id,
10260
+ label: `Klasse ${id}`
10261
+ }));
10262
+ },
10263
+ listSubjects(schoolType, _grade) {
10264
+ return RAHMENRICHTLINIEN_ST_MANIFEST.subjects[schoolType] || [];
10265
+ },
10266
+ listTracks(schoolType, grade, subject) {
10267
+ const key = `${schoolType}|${grade}|${subject}`;
10268
+ return RAHMENRICHTLINIEN_ST_MANIFEST.tracks[key] || [];
10269
+ },
10270
+ listTopics(selection) {
10271
+ const key = selection.track ? `${selection.schoolType}|${selection.grade}|${selection.subject}|${selection.track}` : `${selection.schoolType}|${selection.grade}|${selection.subject}`;
10272
+ const list = RAHMENRICHTLINIEN_ST_MANIFEST.topics[key] || [];
10273
+ return list.map((t2) => ({
10274
+ ...t2,
10275
+ sourceRef: key
10276
+ }));
10277
+ },
10278
+ resolveTopic(topic) {
10279
+ const uri = RAHMENRICHTLINIEN_ST_MANIFEST.contentUrls[topic.sourceRef];
10280
+ if (!uri) {
10281
+ throw new Error(
10282
+ `Rahmenrichtlinien ST: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
7785
10283
  );
7786
- const headerText = headerMatch ? cleanHtmlText2(headerMatch[1]).trim() : "";
7787
- sections.push({
7788
- id,
7789
- level,
7790
- headerText,
7791
- contentHtml
7792
- });
7793
10284
  }
10285
+ return {
10286
+ provider: "rahmenrichtlinien-st",
10287
+ topicId: `${topic.sourceRef}#${topic.id}`,
10288
+ uri
10289
+ };
10290
+ },
10291
+ extractTopics(html, topicIds) {
7794
10292
  const results = {};
10293
+ const headingMatches = Array.from(
10294
+ html.matchAll(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi)
10295
+ );
10296
+ const sections = [];
10297
+ for (const m of headingMatches) {
10298
+ const headerText = cleanHtmlText15(m[1]).trim();
10299
+ const start = m.index + m[0].length;
10300
+ const nextHeading = html.indexOf("<h", start);
10301
+ const rawContent = nextHeading > -1 ? html.slice(start, nextHeading) : html.slice(start, start + 1500);
10302
+ const content = cleanHtmlText15(rawContent).slice(0, 1500);
10303
+ if (headerText) sections.push({ header: headerText, content });
10304
+ }
7795
10305
  for (const topicId of topicIds) {
7796
- let label = "";
7797
- for (const key of Object.keys(LEHRPLANPLUS_BAYERN_MANIFEST.topics)) {
7798
- const list = LEHRPLANPLUS_BAYERN_MANIFEST.topics[key];
7799
- const match = list.find(
7800
- (t2) => t2.id === topicId || `${key}#${t2.id}` === topicId
7801
- );
7802
- if (match) {
7803
- label = match.label;
7804
- break;
7805
- }
7806
- }
7807
- if (!label) {
7808
- continue;
7809
- }
7810
- const normalizedLabel = normalizeForComparison2(label);
7811
- const lvl1Index = sections.findIndex((s) => {
7812
- if (s.level !== 1) return false;
7813
- const normalizedHeader = normalizeForComparison2(s.headerText);
7814
- return normalizedHeader.includes(normalizedLabel);
7815
- });
7816
- if (lvl1Index === -1) {
7817
- continue;
10306
+ const hashIdx = topicId.indexOf("#");
10307
+ if (hashIdx === -1) continue;
10308
+ const key = topicId.substring(0, hashIdx);
10309
+ const shortId = topicId.substring(hashIdx + 1);
10310
+ const list = RAHMENRICHTLINIEN_ST_MANIFEST.topics[key] ?? [];
10311
+ const match = list.find((t2) => t2.id === shortId);
10312
+ if (!match) continue;
10313
+ const label = match.label;
10314
+ const normalizedLabel = normalizeForComparison15(label);
10315
+ let section = sections.find(
10316
+ (s) => normalizeForComparison15(s.header).includes(normalizedLabel) || s.header.toLowerCase().includes(shortId.toLowerCase())
10317
+ );
10318
+ if (!section && sections.length > 0) {
10319
+ section = sections.find((s) => s.content.length > 80) || sections[0];
7818
10320
  }
7819
- const collectedSections = [sections[lvl1Index]];
7820
- for (let i = lvl1Index + 1; i < sections.length; i++) {
7821
- if (sections[i].level === 1) {
7822
- break;
7823
- }
7824
- collectedSections.push(sections[i]);
10321
+ if (section) {
10322
+ results[topicId] = `${label}
10323
+
10324
+ ${section.content}`.trim();
10325
+ } else {
10326
+ results[topicId] = label;
7825
10327
  }
7826
- const fullHtml = collectedSections.map((s) => s.contentHtml).join("\n");
7827
- results[topicId] = cleanHtmlText2(fullHtml);
7828
10328
  }
7829
10329
  return results;
7830
10330
  }
7831
10331
  };
7832
- function cleanHtmlText2(html) {
10332
+ function cleanHtmlText15(html) {
7833
10333
  let text = html.replace(
7834
10334
  /<(head|script|style|svg)[^>]*>[\s\S]*?<\/\1>/gi,
7835
10335
  " "
@@ -7839,22 +10339,248 @@ function cleanHtmlText2(html) {
7839
10339
  text = text.replace(/<p[^>]*>/gi, "\n");
7840
10340
  text = text.replace(/<br\s*\/?>/gi, "\n");
7841
10341
  text = text.replace(/<[^>]+>/g, " ");
7842
- 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");
10342
+ text = text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
7843
10343
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
7844
10344
  }
7845
- function normalizeForComparison2(str) {
10345
+ function normalizeForComparison15(str) {
7846
10346
  return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
7847
10347
  }
7848
10348
 
7849
10349
  // src/cli/curriculum/registry.ts
7850
10350
  var CURRICULUM_PROVIDERS = [
7851
10351
  lehrplanplusBayernProvider,
7852
- bildungsplanBwProvider
10352
+ bildungsplanBwProvider,
10353
+ kernlehrplanNrwProvider,
10354
+ kerncurriculumHessenProvider,
10355
+ kerncurriculumNiedersachsenProvider,
10356
+ lehrplanSachsenProvider,
10357
+ rahmenlehrplanBerlinBrandenburgProvider,
10358
+ bildungsplanHamburgProvider,
10359
+ bildungsplanBremenProvider,
10360
+ rahmenplanMvProvider,
10361
+ lehrplaeneRpProvider,
10362
+ lehrplanSaarlandProvider,
10363
+ rahmenrichtlinienStProvider,
10364
+ fachanforderungenShProvider,
10365
+ lehrplanThueringenProvider
7853
10366
  ];
7854
10367
  function getCurriculumProvider(id) {
7855
10368
  return CURRICULUM_PROVIDERS.find((provider) => provider.id === id);
7856
10369
  }
7857
10370
 
10371
+ // src/cli/llm/embedder.ts
10372
+ var CANONICAL_EMBEDDING_MODEL_ID = "embeddinggemma-300m";
10373
+ var EMBEDDINGGEMMA_ALIASES = /* @__PURE__ */ new Set([
10374
+ "embeddinggemma",
10375
+ "embeddinggemma:300m",
10376
+ "embeddinggemma-300m",
10377
+ "embed-gemma",
10378
+ "embed-gemma:300m",
10379
+ "google/embeddinggemma-300m"
10380
+ ]);
10381
+ function canonicalEmbeddingModelId(model) {
10382
+ const lowered = model.trim().toLowerCase();
10383
+ if (EMBEDDINGGEMMA_ALIASES.has(lowered)) {
10384
+ return CANONICAL_EMBEDDING_MODEL_ID;
10385
+ }
10386
+ return lowered;
10387
+ }
10388
+ function embeddingTextForToken(t2, model) {
10389
+ const isGemma = EMBEDDINGGEMMA_ALIASES.has(model.trim().toLowerCase());
10390
+ const content = embeddingContentForToken(t2);
10391
+ return isGemma ? `title: none | text: ${content}` : content;
10392
+ }
10393
+ function embeddingTextForQuery(text, model) {
10394
+ const isGemma = EMBEDDINGGEMMA_ALIASES.has(model.trim().toLowerCase());
10395
+ return isGemma ? `task: search result | query: ${text}` : text;
10396
+ }
10397
+ async function resolveUsableEmbeddingEndpoint(db) {
10398
+ const cfg = await getProviderForRole(db, "embedding");
10399
+ if (!cfg.enabled) return null;
10400
+ for (const endpoint of [cfg, ...cfg.fallback ? [cfg.fallback] : []]) {
10401
+ if (endpoint.apiFlavor !== "chat-completions") continue;
10402
+ const online = await isLlmOnline(endpoint.url);
10403
+ if (!online) continue;
10404
+ const availableModels = await getAvailableModels(
10405
+ endpoint.url,
10406
+ endpoint.apiKey
10407
+ );
10408
+ const modelAvailable = availableModels.length === 0 || availableModels.some(
10409
+ (candidate) => candidate.toLowerCase() === endpoint.model.toLowerCase()
10410
+ );
10411
+ if (modelAvailable) return endpoint;
10412
+ }
10413
+ return null;
10414
+ }
10415
+ async function embedTexts(endpoint, texts) {
10416
+ const controller = new AbortController();
10417
+ const timeoutId = setTimeout(() => controller.abort(), 6e4);
10418
+ let res;
10419
+ try {
10420
+ res = await fetch(`${endpoint.url}/embeddings`, {
10421
+ method: "POST",
10422
+ headers: {
10423
+ "Content-Type": "application/json",
10424
+ Authorization: `Bearer ${endpoint.apiKey || DEFAULT_LLM_API_KEY}`
10425
+ },
10426
+ body: JSON.stringify({ model: endpoint.model, input: texts }),
10427
+ signal: controller.signal
10428
+ });
10429
+ } finally {
10430
+ clearTimeout(timeoutId);
10431
+ }
10432
+ if (!res.ok) {
10433
+ const body = await res.text().catch(() => "");
10434
+ throw new Error(
10435
+ `Embedding request failed: ${res.statusText} (${res.status}) - ${body}`
10436
+ );
10437
+ }
10438
+ const data = await res.json();
10439
+ const items = data.data ?? [];
10440
+ if (items.length !== texts.length) {
10441
+ throw new Error(
10442
+ `Embedding response returned ${items.length} vectors for ${texts.length} inputs`
10443
+ );
10444
+ }
10445
+ const ordered = new Array(texts.length);
10446
+ for (let i = 0; i < items.length; i++) {
10447
+ const item = items[i];
10448
+ const index = typeof item.index === "number" ? item.index : i;
10449
+ if (!Number.isInteger(index)) {
10450
+ throw new Error("Embedding response item is missing a valid index");
10451
+ }
10452
+ if (index < 0 || index >= texts.length) {
10453
+ throw new Error(`Embedding response index out of range: ${index}`);
10454
+ }
10455
+ if (!Array.isArray(item.embedding) || item.embedding.length === 0 || !item.embedding.every((n) => typeof n === "number" && Number.isFinite(n))) {
10456
+ throw new Error(
10457
+ `Embedding response at index ${index} is not a non-empty array of finite numbers`
10458
+ );
10459
+ }
10460
+ ordered[index] = item.embedding;
10461
+ }
10462
+ const firstLength = ordered[0]?.length;
10463
+ if (ordered.some((vec) => vec === void 0 || vec.length !== firstLength)) {
10464
+ throw new Error("Embedding response vectors have inconsistent lengths");
10465
+ }
10466
+ return ordered;
10467
+ }
10468
+ var EMBED_BATCH_SIZE = 16;
10469
+ async function ensureTokenEmbeddings(db, opts) {
10470
+ const endpoint = await resolveUsableEmbeddingEndpoint(db);
10471
+ if (!endpoint) {
10472
+ const reason = await describeUnavailableReason(db);
10473
+ return { status: "unavailable", embedded: 0, remaining: 0, reason };
10474
+ }
10475
+ const model = canonicalEmbeddingModelId(endpoint.model);
10476
+ const pending = await listTokensNeedingEmbedding(db, model, {
10477
+ limit: opts?.limit ?? 64,
10478
+ force: opts?.force,
10479
+ dims: opts?.dims
10480
+ });
10481
+ let embedded = 0;
10482
+ let failure;
10483
+ try {
10484
+ for (let i = 0; i < pending.length; i += EMBED_BATCH_SIZE) {
10485
+ const batch = pending.slice(i, i + EMBED_BATCH_SIZE);
10486
+ const formattedTexts = batch.map(
10487
+ (item) => embeddingTextForToken(item.token, endpoint.model)
10488
+ );
10489
+ const vectors = await embedTexts(
10490
+ { url: endpoint.url, model: endpoint.model, apiKey: endpoint.apiKey },
10491
+ formattedTexts
10492
+ );
10493
+ for (const [index, item] of batch.entries()) {
10494
+ await upsertTokenEmbedding(db, {
10495
+ tokenId: item.token.id,
10496
+ embedding: vectors[index],
10497
+ model,
10498
+ contentHash: computeContentHash(item.text)
10499
+ });
10500
+ embedded++;
10501
+ }
10502
+ }
10503
+ } catch (err) {
10504
+ failure = err instanceof Error ? err.message : String(err);
10505
+ }
10506
+ const coverage = await getEmbeddingCoverage(db, model, { dims: opts?.dims });
10507
+ const remaining = coverage.missing + coverage.stale;
10508
+ if (failure !== void 0) {
10509
+ return { status: "unavailable", embedded, remaining, reason: failure };
10510
+ }
10511
+ return { status: "ok", embedded, remaining };
10512
+ }
10513
+ async function describeUnavailableReason(db) {
10514
+ const cfg = await getProviderForRole(db, "embedding");
10515
+ if (!cfg.enabled) {
10516
+ return "embedding role is disabled in settings (llm.enabled)";
10517
+ }
10518
+ const endpoints = [cfg, ...cfg.fallback ? [cfg.fallback] : []];
10519
+ const online = await Promise.all(
10520
+ endpoints.map((endpoint) => isLlmOnline(endpoint.url))
10521
+ );
10522
+ if (!online.some(Boolean)) {
10523
+ return `embedding endpoints offline (${endpoints.map((endpoint) => endpoint.url).join(", ")})`;
10524
+ }
10525
+ return `no configured embedding model is available (${endpoints.map((endpoint) => endpoint.model).join(", ")})`;
10526
+ }
10527
+ async function embedQuery(db, text) {
10528
+ const endpoint = await resolveUsableEmbeddingEndpoint(db);
10529
+ if (!endpoint) return null;
10530
+ try {
10531
+ const [vector] = await embedTexts(
10532
+ { url: endpoint.url, model: endpoint.model, apiKey: endpoint.apiKey },
10533
+ [embeddingTextForQuery(text, endpoint.model)]
10534
+ );
10535
+ return { vector, model: canonicalEmbeddingModelId(endpoint.model) };
10536
+ } catch {
10537
+ return null;
10538
+ }
10539
+ }
10540
+ async function findPossibleDuplicates(db, candidate, embed = embedQuery) {
10541
+ const queryText = embeddingContentForToken({
10542
+ concept: candidate.concept,
10543
+ question: candidate.question ?? null,
10544
+ domain: candidate.domain ?? ""
10545
+ });
10546
+ const q2 = await embed(db, queryText);
10547
+ if (!q2) {
10548
+ return [];
10549
+ }
10550
+ const coverage = await getEmbeddingCoverage(db, q2.model, {
10551
+ dims: q2.vector.length
10552
+ });
10553
+ const totalNeeding = coverage.missing + coverage.stale;
10554
+ if (totalNeeding > 0) {
10555
+ await ensureTokenEmbeddings(db, { limit: 100, dims: q2.vector.length });
10556
+ if (totalNeeding > 100) {
10557
+ console.warn(
10558
+ `Warning: Duplicate check is incomplete. ${totalNeeding - 100} tokens are still missing embeddings. Run 'zam token reembed' to complete indexing.`
10559
+ );
10560
+ }
10561
+ }
10562
+ const thresholdStr = await getSetting(db, "search.dedup_threshold");
10563
+ const parsed = thresholdStr ? Number.parseFloat(thresholdStr) : Number.NaN;
10564
+ const threshold = Number.isFinite(parsed) && parsed > 0 && parsed <= 1 ? parsed : 0.85;
10565
+ const hits = await searchTokensHybrid(db, queryText, {
10566
+ queryEmbedding: q2.vector,
10567
+ model: q2.model,
10568
+ limit: 1e3,
10569
+ vectorTopK: 1e3
10570
+ });
10571
+ const results = [];
10572
+ for (const hit of hits) {
10573
+ if (hit.similarity !== null && hit.similarity >= threshold) {
10574
+ results.push({
10575
+ slug: hit.slug,
10576
+ concept: hit.concept,
10577
+ similarity: hit.similarity
10578
+ });
10579
+ }
10580
+ }
10581
+ return results;
10582
+ }
10583
+
7858
10584
  // src/cli/llm/vision.ts
7859
10585
  import { randomBytes } from "crypto";
7860
10586
  import { readFileSync as readFileSync10 } from "fs";
@@ -8294,7 +11020,7 @@ var VALID_API_FLAVORS = [
8294
11020
  "chat-completions",
8295
11021
  "anthropic-messages"
8296
11022
  ];
8297
- var VALID_ROLES = ["vision", "recall", "text"];
11023
+ var VALID_ROLES = ["vision", "recall", "text", "embedding"];
8298
11024
  function upsertProviderRecord(providers, name, patch) {
8299
11025
  const merged = { ...providers[name] ?? {} };
8300
11026
  if (patch.url !== void 0) merged.url = patch.url;
@@ -8927,14 +11653,23 @@ async function backupDatabaseTo(db, targetDir) {
8927
11653
 
8928
11654
  // src/cli/commands/bridge.ts
8929
11655
  var isServeMode = false;
11656
+ var serveStdinPayload;
8930
11657
  function jsonOut2(data) {
8931
11658
  console.log(JSON.stringify(data, null, 2));
8932
11659
  }
8933
11660
  function jsonError(message) {
11661
+ let msg = message;
11662
+ if (message.startsWith('{"error":')) {
11663
+ try {
11664
+ const parsed = JSON.parse(message);
11665
+ msg = parsed.error;
11666
+ } catch {
11667
+ }
11668
+ }
8934
11669
  if (isServeMode) {
8935
- throw new Error(JSON.stringify({ error: message }));
11670
+ throw new Error(JSON.stringify({ error: msg }));
8936
11671
  }
8937
- console.log(JSON.stringify({ error: message }, null, 2));
11672
+ console.log(JSON.stringify({ error: msg }, null, 2));
8938
11673
  process.exit(1);
8939
11674
  }
8940
11675
  function parseNonNegativeIntegerOption(name, value) {
@@ -9257,7 +11992,10 @@ bridgeCommand.command("agent-open").description("Launch an agent harness in the
9257
11992
  });
9258
11993
  });
9259
11994
  });
9260
- bridgeCommand.command("get-review").description("Get next review card with prompt (JSON)").option("--user <id>", "User ID (default: whoami)").option("--no-resolve", "Skip resolving the token's source_link into context").action(async (opts) => {
11995
+ bridgeCommand.command("get-review").description("Get next review card with prompt (JSON)").option("--user <id>", "User ID (default: whoami)").option("--no-resolve", "Skip resolving the token's source_link into context").option(
11996
+ "--no-dynamic-question",
11997
+ "Use the stored question without generating a fresh LLM question"
11998
+ ).action(async (opts) => {
9261
11999
  await withDb2(async (db) => {
9262
12000
  const userId = await resolveUser(opts, db, { json: true });
9263
12001
  const queue = await buildReviewQueue(db, {
@@ -9281,7 +12019,7 @@ bridgeCommand.command("get-review").description("Get next review card with promp
9281
12019
  let resolvedQuestion = item.question;
9282
12020
  let questionSource = "original";
9283
12021
  let questionModel;
9284
- if (isLlmEnabled) {
12022
+ if (isLlmEnabled && opts.dynamicQuestion !== false) {
9285
12023
  try {
9286
12024
  const healed = await ensureHighQualityQuestion(db, {
9287
12025
  id: item.tokenId,
@@ -9528,11 +12266,16 @@ bridgeCommand.command("analyze-monitor").description("Analyze monitor log with t
9528
12266
  });
9529
12267
  return;
9530
12268
  }
9531
- const chunks = [];
9532
- for await (const chunk of process.stdin) {
9533
- chunks.push(chunk);
12269
+ let raw;
12270
+ if (isServeMode) {
12271
+ raw = serveStdinPayload ?? "";
12272
+ } else {
12273
+ const chunks = [];
12274
+ for await (const chunk of process.stdin) {
12275
+ chunks.push(chunk);
12276
+ }
12277
+ raw = Buffer.concat(chunks).toString("utf-8").trim();
9534
12278
  }
9535
- const raw = Buffer.concat(chunks).toString("utf-8").trim();
9536
12279
  if (!raw) {
9537
12280
  jsonError("No input received on stdin. Pipe JSON with token patterns.");
9538
12281
  }
@@ -9557,13 +12300,17 @@ bridgeCommand.command("analyze-monitor").description("Analyze monitor log with t
9557
12300
  }
9558
12301
  });
9559
12302
  bridgeCommand.command("add-token").description("Create a token + card from JSON stdin").option("--user <id>", "User ID (default: whoami)").action(async (opts) => {
9560
- let db;
9561
- try {
9562
- const chunks = [];
9563
- for await (const chunk of process.stdin) {
9564
- chunks.push(chunk);
12303
+ await withDb2(async (db) => {
12304
+ let raw;
12305
+ if (isServeMode) {
12306
+ raw = serveStdinPayload ?? "";
12307
+ } else {
12308
+ const chunks = [];
12309
+ for await (const chunk of process.stdin) {
12310
+ chunks.push(chunk);
12311
+ }
12312
+ raw = Buffer.concat(chunks).toString("utf-8").trim();
9565
12313
  }
9566
- const raw = Buffer.concat(chunks).toString("utf-8").trim();
9567
12314
  if (!raw) {
9568
12315
  jsonError("No input received on stdin. Pipe JSON with token data.");
9569
12316
  }
@@ -9576,8 +12323,12 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
9576
12323
  if (!data?.slug || !data?.concept) {
9577
12324
  jsonError("JSON must include 'slug' and 'concept' fields");
9578
12325
  }
9579
- db = await openDatabase();
9580
12326
  const userId = await resolveUser(opts, db, { json: true });
12327
+ const possibleDuplicates = await findPossibleDuplicates(db, {
12328
+ concept: data?.concept,
12329
+ question: data?.question ?? null,
12330
+ domain: data?.domain
12331
+ });
9581
12332
  const token = await createToken(db, {
9582
12333
  slug: data?.slug,
9583
12334
  concept: data?.concept,
@@ -9589,6 +12340,10 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
9589
12340
  question: data?.question ?? null
9590
12341
  });
9591
12342
  const card = await ensureCard(db, token.id, userId);
12343
+ try {
12344
+ await ensureTokenEmbeddings(db, { limit: 8 });
12345
+ } catch {
12346
+ }
9592
12347
  jsonOut2({
9593
12348
  success: true,
9594
12349
  token,
@@ -9599,15 +12354,79 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
9599
12354
  state: card.state,
9600
12355
  dueAt: card.due_at,
9601
12356
  blocked: card.blocked
12357
+ },
12358
+ possible_duplicates: possibleDuplicates
12359
+ });
12360
+ });
12361
+ });
12362
+ bridgeCommand.command("relevant-tokens").description("Find tokens relevant to a given context").option("--user <id>", "User ID (default: whoami)").action(async (opts) => {
12363
+ await withDb2(async (db) => {
12364
+ let raw;
12365
+ if (isServeMode) {
12366
+ raw = serveStdinPayload ?? "";
12367
+ } else {
12368
+ const chunks = [];
12369
+ for await (const chunk of process.stdin) {
12370
+ chunks.push(chunk);
9602
12371
  }
12372
+ raw = Buffer.concat(chunks).toString("utf-8").trim();
12373
+ }
12374
+ if (!raw) {
12375
+ jsonError("No input received on stdin. Pipe JSON with context.");
12376
+ }
12377
+ let data;
12378
+ try {
12379
+ data = JSON.parse(raw);
12380
+ } catch {
12381
+ jsonError("Invalid JSON input");
12382
+ }
12383
+ if (!data?.context || data.context.trim() === "") {
12384
+ jsonError("JSON must include a non-empty 'context' field");
12385
+ }
12386
+ const userId = await resolveUser(opts, db, { json: true });
12387
+ const truncatedContext = data.context.slice(0, 2e3);
12388
+ const q2 = await embedQuery(db, truncatedContext);
12389
+ try {
12390
+ await ensureTokenEmbeddings(db, {
12391
+ limit: 32,
12392
+ dims: q2?.vector.length
12393
+ });
12394
+ } catch {
12395
+ }
12396
+ let limit = data.limit ?? 10;
12397
+ if (typeof limit !== "number" || limit <= 0 || !Number.isInteger(limit)) {
12398
+ limit = 10;
12399
+ }
12400
+ if (limit > 100) {
12401
+ limit = 100;
12402
+ }
12403
+ const results = await searchTokensHybrid(db, truncatedContext, {
12404
+ queryEmbedding: q2?.vector,
12405
+ model: q2?.model,
12406
+ limit
9603
12407
  });
9604
- await db.close();
9605
- } catch (err) {
9606
- await db?.close();
9607
- if (err.message) {
9608
- jsonError(err.message);
12408
+ const tokens = [];
12409
+ for (const t2 of results) {
12410
+ const card = await getCard(db, t2.id, userId);
12411
+ tokens.push({
12412
+ slug: t2.slug,
12413
+ concept: t2.concept,
12414
+ domain: t2.domain,
12415
+ bloom_level: t2.bloom_level,
12416
+ score: t2.score,
12417
+ similarity: t2.similarity,
12418
+ card: card ? {
12419
+ state: card.state,
12420
+ due_at: card.due_at,
12421
+ blocked: card.blocked
12422
+ } : null
12423
+ });
9609
12424
  }
9610
- }
12425
+ jsonOut2({
12426
+ semantic: q2 !== null,
12427
+ tokens
12428
+ });
12429
+ });
9611
12430
  });
9612
12431
  bridgeCommand.command("discover-skills").description(
9613
12432
  "Analyze monitor logs across sessions to discover recurring patterns"
@@ -10417,12 +13236,13 @@ bridgeCommand.command("check-llm").description("Check if LLM is enabled and onli
10417
13236
  });
10418
13237
  bridgeCommand.command("provider-status").description("Show secret-safe provider status for LLM roles (JSON)").action(async () => {
10419
13238
  await withDb2(async (db) => {
10420
- const [recall, vision, text] = await Promise.all([
13239
+ const [recall, vision, text, embedding] = await Promise.all([
10421
13240
  getProviderRoleStatus(db, "recall"),
10422
13241
  getProviderRoleStatus(db, "vision"),
10423
- getProviderRoleStatus(db, "text")
13242
+ getProviderRoleStatus(db, "text"),
13243
+ getProviderRoleStatus(db, "embedding")
10424
13244
  ]);
10425
- jsonOut2({ roles: { recall, vision, text } });
13245
+ jsonOut2({ roles: { recall, vision, text, embedding } });
10426
13246
  });
10427
13247
  });
10428
13248
  bridgeCommand.command("provider-config-list").description("List provider records and role bindings (JSON)").option("--scope <scope>", "machine (default) or shared", "machine").action(async (opts) => {
@@ -10728,6 +13548,46 @@ bridgeCommand.command("get-settings").description("Get active ZAM settings (JSON
10728
13548
  });
10729
13549
  });
10730
13550
  });
13551
+ async function readDatabaseUserSummaries(db) {
13552
+ return await db.prepare(
13553
+ `SELECT user_id AS id, COUNT(*) AS cardCount
13554
+ FROM cards
13555
+ GROUP BY user_id
13556
+ ORDER BY user_id`
13557
+ ).all();
13558
+ }
13559
+ bridgeCommand.command("database-status").description("Show the active database target and learning profiles (JSON)").action(async () => {
13560
+ const target = getDatabaseTargetInfo();
13561
+ await withDb2(async (db) => {
13562
+ const userId = await getSetting(db, "user.id") ?? null;
13563
+ const users = await readDatabaseUserSummaries(db);
13564
+ jsonOut2({
13565
+ success: true,
13566
+ connected: true,
13567
+ target,
13568
+ userId,
13569
+ cardCount: users.find((user) => user.id === userId)?.cardCount ?? 0,
13570
+ users
13571
+ });
13572
+ });
13573
+ });
13574
+ bridgeCommand.command("database-select-user").description("Select an existing learning profile for this database (JSON)").requiredOption("--user <id>", "Existing user ID").action(async (opts) => {
13575
+ await withDb2(async (db) => {
13576
+ const userId = String(opts.user ?? "").trim();
13577
+ const users = await readDatabaseUserSummaries(db);
13578
+ const selected = users.find((user) => user.id === userId);
13579
+ if (!selected) {
13580
+ jsonError(`Learning profile not found: ${userId}`);
13581
+ return;
13582
+ }
13583
+ await setSetting(db, "user.id", userId);
13584
+ jsonOut2({
13585
+ success: true,
13586
+ userId,
13587
+ cardCount: selected.cardCount
13588
+ });
13589
+ });
13590
+ });
10731
13591
  bridgeCommand.command("list-tokens").description(
10732
13592
  "List tokens (optionally enriched with user card state for viz) (JSON)"
10733
13593
  ).option(
@@ -11302,7 +14162,7 @@ async function fetchRawHtml(url) {
11302
14162
  const res = await fetch(url, {
11303
14163
  signal: controller.signal,
11304
14164
  headers: {
11305
- "User-Agent": "ZAM-Content-Studio/0.6.1"
14165
+ "User-Agent": "ZAM-Content-Studio/0.6.3"
11306
14166
  }
11307
14167
  });
11308
14168
  if (!res.ok) {
@@ -11452,6 +14312,13 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
11452
14312
  requestId = req.id ?? null;
11453
14313
  const cmd = req.cmd;
11454
14314
  const args = req.args ?? [];
14315
+ if (typeof req.stdin === "string") {
14316
+ serveStdinPayload = req.stdin;
14317
+ } else if (typeof req.stdin === "object" && req.stdin !== null) {
14318
+ serveStdinPayload = JSON.stringify(req.stdin);
14319
+ } else {
14320
+ serveStdinPayload = void 0;
14321
+ }
11455
14322
  if (!cmd) {
11456
14323
  return JSON.stringify({
11457
14324
  id: requestId,
@@ -11506,6 +14373,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
11506
14373
  id: requestId,
11507
14374
  error: `Invalid JSON request: ${err.message}`
11508
14375
  });
14376
+ } finally {
14377
+ serveStdinPayload = void 0;
11509
14378
  }
11510
14379
  };
11511
14380
  const readline = await import("readline");
@@ -14511,7 +17380,7 @@ import { Command as Command20 } from "commander";
14511
17380
  var tokenCommand = new Command20("token").description(
14512
17381
  "Manage knowledge tokens"
14513
17382
  );
14514
- tokenCommand.command("register").description("Register a new knowledge token").requiredOption("--slug <slug>", "Unique token slug").requiredOption("--concept <concept>", "Concept description").option("--domain <domain>", "Knowledge domain", "").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option("--source-link <link>", "Source file path or reference URL", "").option("--question <question>", "Specific question prompt for recall", "").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
17383
+ tokenCommand.command("register").description("Register a new knowledge token").requiredOption("--slug <slug>", "Unique token slug").requiredOption("--concept <concept>", "Concept description").option("--domain <domain>", "Knowledge domain", "").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option("--source-link <link>", "Source file path or reference URL", "").option("--question <question>", "Specific question prompt for recall", "").option("--user <id>", "Owner of the personal card (default: whoami)").option("--no-card", "Register the token only; do not create a personal card").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
14515
17384
  await withDb(async (db) => {
14516
17385
  let question = opts.question || null;
14517
17386
  if (!question) {
@@ -14521,6 +17390,11 @@ tokenCommand.command("register").description("Register a new knowledge token").r
14521
17390
  opts.domain
14522
17391
  );
14523
17392
  }
17393
+ const possibleDuplicates = await findPossibleDuplicates(db, {
17394
+ concept: opts.concept,
17395
+ question,
17396
+ domain: opts.domain
17397
+ });
14524
17398
  const token = await createToken(db, {
14525
17399
  slug: opts.slug,
14526
17400
  concept: opts.concept,
@@ -14529,9 +17403,30 @@ tokenCommand.command("register").description("Register a new knowledge token").r
14529
17403
  source_link: opts.sourceLink || null,
14530
17404
  question
14531
17405
  });
17406
+ let cardUserId = null;
17407
+ if (opts.card !== false) {
17408
+ cardUserId = opts.user ?? await getSetting(db, "user.id");
17409
+ if (cardUserId) {
17410
+ await ensureCard(db, token.id, cardUserId);
17411
+ }
17412
+ }
17413
+ try {
17414
+ await ensureTokenEmbeddings(db, { limit: 8 });
17415
+ } catch {
17416
+ }
14532
17417
  if (opts.quiet) return;
14533
17418
  if (opts.json) {
14534
- console.log(JSON.stringify(token, null, 2));
17419
+ console.log(
17420
+ JSON.stringify(
17421
+ {
17422
+ ...token,
17423
+ card: cardUserId ? { userId: cardUserId } : null,
17424
+ possible_duplicates: possibleDuplicates
17425
+ },
17426
+ null,
17427
+ 2
17428
+ )
17429
+ );
14535
17430
  } else {
14536
17431
  console.log(`Registered token: ${token.slug} (${token.id})`);
14537
17432
  console.log(` Concept: ${token.concept}`);
@@ -14541,12 +17436,41 @@ tokenCommand.command("register").description("Register a new knowledge token").r
14541
17436
  if (token.source_link) {
14542
17437
  console.log(` Source: ${token.source_link}`);
14543
17438
  }
17439
+ if (cardUserId) {
17440
+ console.log(` Card: created for ${cardUserId}`);
17441
+ } else if (opts.card === false) {
17442
+ console.log(` Card: skipped (--no-card)`);
17443
+ } else {
17444
+ console.log(` Card: skipped (no default user set)`);
17445
+ }
17446
+ if (possibleDuplicates.length > 0) {
17447
+ console.log(`
17448
+ WARNING: Possible duplicate tokens found:`);
17449
+ for (const dup of possibleDuplicates) {
17450
+ console.log(
17451
+ ` - ${dup.slug} (similarity: ${dup.similarity.toFixed(2)})`
17452
+ );
17453
+ }
17454
+ }
14544
17455
  }
14545
17456
  });
14546
17457
  });
14547
17458
  tokenCommand.command("find").description("Fuzzy search for tokens").requiredOption("--query <query>", "Search query").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
14548
17459
  await withDb(async (db) => {
14549
- const results = await findTokens(db, opts.query);
17460
+ const q2 = await embedQuery(db, opts.query);
17461
+ const embRes = await ensureTokenEmbeddings(db, {
17462
+ limit: 32,
17463
+ dims: q2?.vector.length
17464
+ });
17465
+ if (embRes.status === "unavailable" && !opts.json && !opts.quiet) {
17466
+ console.error(
17467
+ `Note: semantic search unavailable (${embRes.reason}) \u2014 lexical matches only.`
17468
+ );
17469
+ }
17470
+ const results = await searchTokensHybrid(db, opts.query, {
17471
+ queryEmbedding: q2?.vector,
17472
+ model: q2?.model
17473
+ });
14550
17474
  if (opts.quiet) return;
14551
17475
  if (opts.json) {
14552
17476
  console.log(JSON.stringify(results, null, 2));
@@ -14559,12 +17483,14 @@ tokenCommand.command("find").description("Fuzzy search for tokens").requiredOpti
14559
17483
  console.log(`Found ${results.length} token(s):
14560
17484
  `);
14561
17485
  console.log(
14562
- "Score Slug Concept Domain Bloom"
17486
+ "Score Sim Slug Concept Domain Bloom"
14563
17487
  );
14564
- console.log("\u2500".repeat(90));
17488
+ console.log("\u2500".repeat(95));
14565
17489
  for (const t2 of results) {
17490
+ const scoreStr = t2.score.toFixed(3).padEnd(6);
17491
+ const simStr = (t2.similarity?.toFixed(2) ?? "-").padEnd(4);
14566
17492
  console.log(
14567
- `${String(t2.score).padEnd(6)} ${t2.slug.padEnd(21)} ${t2.concept.slice(0, 31).padEnd(31)} ${(t2.domain || "-").padEnd(11)} ${t2.bloom_level}`
17493
+ `${scoreStr} ${simStr} ${t2.slug.padEnd(21)} ${t2.concept.slice(0, 31).padEnd(31)} ${(t2.domain || "-").padEnd(11)} ${t2.bloom_level}`
14568
17494
  );
14569
17495
  }
14570
17496
  });
@@ -14792,6 +17718,56 @@ tokenCommand.command("status").description("Show full status of a token for a us
14792
17718
  }
14793
17719
  });
14794
17720
  });
17721
+ tokenCommand.command("reembed").description("Backfill or refresh semantic-search embeddings for tokens").option("--all", "Force re-embed every token, including already-fresh ones").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
17722
+ await withDb(async (db) => {
17723
+ const probe = await embedQuery(db, "ZAM embedding dimension probe");
17724
+ const model = probe?.model ?? null;
17725
+ const dims = probe?.vector.length;
17726
+ const before = model ? await getEmbeddingCoverage(db, model, { dims }) : { tokens: 0, embedded: 0, missing: 0, stale: 0 };
17727
+ let embedded = 0;
17728
+ let reason;
17729
+ let force = Boolean(opts.all);
17730
+ while (true) {
17731
+ const result = await ensureTokenEmbeddings(
17732
+ db,
17733
+ force ? { force: true, limit: Number.MAX_SAFE_INTEGER, dims } : { dims }
17734
+ );
17735
+ force = false;
17736
+ embedded += result.embedded;
17737
+ if (result.status === "unavailable") {
17738
+ reason = result.reason;
17739
+ break;
17740
+ }
17741
+ if (result.remaining === 0 || result.embedded === 0) break;
17742
+ }
17743
+ if (reason !== void 0) {
17744
+ if (opts.quiet) {
17745
+ process.exit(1);
17746
+ }
17747
+ if (opts.json) {
17748
+ jsonOut({ error: reason, embedded });
17749
+ } else {
17750
+ console.error(`Error: ${reason}`);
17751
+ if (embedded > 0) {
17752
+ console.error(
17753
+ `(${embedded} token(s) were embedded before the failure)`
17754
+ );
17755
+ }
17756
+ }
17757
+ process.exit(1);
17758
+ }
17759
+ const after = await getEmbeddingCoverage(db, model, { dims });
17760
+ if (opts.quiet) return;
17761
+ const staleBefore = before.missing + before.stale;
17762
+ if (opts.json) {
17763
+ jsonOut({ embedded, model, before, after });
17764
+ return;
17765
+ }
17766
+ console.log(
17767
+ `Embedded ${embedded} tokens (${after.tokens} total, ${staleBefore} stale before) with ${model}`
17768
+ );
17769
+ });
17770
+ });
14795
17771
 
14796
17772
  // src/cli/commands/ui.ts
14797
17773
  import { spawn as spawn3, spawnSync } from "child_process";