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/.agent/skills/zam/SKILL.md +7 -3
- package/.agents/skills/zam/SKILL.md +7 -3
- package/.claude/skills/zam/SKILL.md +7 -3
- package/dist/cli/index.js +3188 -212
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +122 -2
- package/dist/index.js +268 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/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: "
|
|
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 ?
|
|
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 =
|
|
8028
|
+
const normalizedLabel = normalizeForComparison2(label);
|
|
7576
8029
|
const sectionIndex = sections.findIndex((s) => {
|
|
7577
|
-
const normalizedHeader =
|
|
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] =
|
|
8036
|
+
results[topicId] = cleanHtmlText2(sections[sectionIndex].contentHtml);
|
|
7584
8037
|
}
|
|
7585
8038
|
return results;
|
|
7586
8039
|
}
|
|
7587
8040
|
};
|
|
7588
|
-
function
|
|
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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/–/g, "\u2013").replace(/—/g, "\u2014").replace(/·/g, "\xB7");
|
|
7599
8052
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
7600
8053
|
}
|
|
7601
|
-
function
|
|
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/
|
|
7606
|
-
var
|
|
7607
|
-
schoolYear: "2026
|
|
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: "
|
|
8062
|
+
sourceRevision: "Bildungspl\xE4ne Hamburg",
|
|
7610
8063
|
schoolTypes: [
|
|
7611
|
-
{ id: "
|
|
7612
|
-
{ id: "
|
|
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
|
-
|
|
8068
|
+
stadtteilschule: ["7", "8", "9", "10"],
|
|
8069
|
+
gymnasium: ["7", "8", "9", "10"]
|
|
7622
8070
|
},
|
|
7623
8071
|
subjects: {
|
|
7624
|
-
|
|
7625
|
-
{
|
|
7626
|
-
|
|
7627
|
-
|
|
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: "
|
|
7632
|
-
|
|
7633
|
-
|
|
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: "
|
|
7652
|
-
{ id: "or", label: "Orthodoxe Religionslehre" },
|
|
8081
|
+
{ id: "informatik", label: "Informatik" },
|
|
7653
8082
|
{ id: "physik", label: "Physik" },
|
|
7654
|
-
{ id: "
|
|
7655
|
-
{ id: "
|
|
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
|
-
"
|
|
7672
|
-
{ id: "
|
|
7673
|
-
{ id: "
|
|
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
|
-
"
|
|
7686
|
-
|
|
7687
|
-
|
|
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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/–/g, "\u2013").replace(/—/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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/–/g, "\u2013").replace(/—/g, "\u2014").replace(/·/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|
|
|
7704
|
-
{ id: "
|
|
7705
|
-
{ id: "
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
{ id: "
|
|
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|
|
|
7713
|
-
"
|
|
7714
|
-
"realschule|9|
|
|
7715
|
-
"realschule|9|
|
|
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/
|
|
7720
|
-
|
|
7721
|
-
|
|
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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'");
|
|
10031
|
+
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
7722
10032
|
}
|
|
7723
|
-
|
|
7724
|
-
|
|
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: "
|
|
7728
|
-
regionLabel: "
|
|
7729
|
-
label: "
|
|
10096
|
+
region: "MV",
|
|
10097
|
+
regionLabel: "Mecklenburg-Vorpommern",
|
|
10098
|
+
label: "Rahmenplan (Mecklenburg-Vorpommern)",
|
|
7730
10099
|
listSchoolTypes() {
|
|
7731
|
-
return
|
|
10100
|
+
return RAHMENPLAN_MV_MANIFEST.schoolTypes;
|
|
7732
10101
|
},
|
|
7733
10102
|
listGrades(schoolType) {
|
|
7734
|
-
return (
|
|
7735
|
-
id
|
|
7736
|
-
label:
|
|
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
|
|
10109
|
+
return RAHMENPLAN_MV_MANIFEST.subjects[schoolType] || [];
|
|
7741
10110
|
},
|
|
7742
10111
|
listTracks(schoolType, grade, subject) {
|
|
7743
|
-
|
|
10112
|
+
const key = `${schoolType}|${grade}|${subject}`;
|
|
10113
|
+
return RAHMENPLAN_MV_MANIFEST.tracks[key] || [];
|
|
7744
10114
|
},
|
|
7745
10115
|
listTopics(selection) {
|
|
7746
|
-
const {
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
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 =
|
|
10124
|
+
const uri = RAHMENPLAN_MV_MANIFEST.contentUrls[topic.sourceRef];
|
|
7756
10125
|
if (!uri) {
|
|
7757
10126
|
throw new Error(
|
|
7758
|
-
`
|
|
10127
|
+
`Rahmenplan MV: no resolvable source URL for topic "${topic.id}" (${topic.sourceRef}).`
|
|
7759
10128
|
);
|
|
7760
10129
|
}
|
|
7761
10130
|
return {
|
|
7762
|
-
provider: "
|
|
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
|
|
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 (
|
|
7771
|
-
const
|
|
7772
|
-
const
|
|
7773
|
-
|
|
7774
|
-
const
|
|
7775
|
-
const
|
|
7776
|
-
if (
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
const
|
|
7780
|
-
if (
|
|
7781
|
-
const
|
|
7782
|
-
const
|
|
7783
|
-
const
|
|
7784
|
-
|
|
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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/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
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
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
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
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
|
|
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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'")
|
|
10342
|
+
text = text.replace(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'");
|
|
7843
10343
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
7844
10344
|
}
|
|
7845
|
-
function
|
|
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:
|
|
11670
|
+
throw new Error(JSON.stringify({ error: msg }));
|
|
8936
11671
|
}
|
|
8937
|
-
console.log(JSON.stringify({ error:
|
|
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").
|
|
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
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
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
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
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
|
-
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
|
|
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.
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
`${
|
|
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";
|