zam-core 0.15.5 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/app.js CHANGED
@@ -10745,7 +10745,7 @@ async function readWebLink(url) {
10745
10745
  redirect: "manual",
10746
10746
  signal: controller.signal,
10747
10747
  headers: {
10748
- "User-Agent": "ZAM-Content-Studio/0.15.5"
10748
+ "User-Agent": "ZAM-Content-Studio/0.16.0"
10749
10749
  }
10750
10750
  });
10751
10751
  if (res.status >= 300 && res.status < 400) {
@@ -12293,6 +12293,32 @@ function installCliShim(options = {}) {
12293
12293
  }
12294
12294
  }
12295
12295
 
12296
+ // src/cli/curriculum/concurrency.ts
12297
+ async function mapWithConcurrency(items, concurrency, mapper) {
12298
+ if (items.length === 0) return [];
12299
+ const workerCount = Math.max(
12300
+ 1,
12301
+ Math.min(items.length, Math.floor(concurrency) || 1)
12302
+ );
12303
+ const results = new Array(items.length);
12304
+ let nextIndex = 0;
12305
+ let failure;
12306
+ async function worker() {
12307
+ while (!failure) {
12308
+ const index = nextIndex++;
12309
+ if (index >= items.length) return;
12310
+ try {
12311
+ results[index] = await mapper(items[index], index);
12312
+ } catch (error) {
12313
+ failure ??= { error };
12314
+ }
12315
+ }
12316
+ }
12317
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
12318
+ if (failure) throw failure.error;
12319
+ return results;
12320
+ }
12321
+
12296
12322
  // src/cli/curriculum/breadcrumb.ts
12297
12323
  init_kernel();
12298
12324
  var LAST_SELECTION_KEY = "curriculum.lastSelection";
@@ -12309,6 +12335,50 @@ async function setLastCurriculumSelection(db, breadcrumb) {
12309
12335
  await setSetting(db, LAST_SELECTION_KEY, JSON.stringify(breadcrumb));
12310
12336
  }
12311
12337
 
12338
+ // src/cli/curriculum/content-filter.ts
12339
+ function trackHasTopics(provider, schoolType, grade, subject, track) {
12340
+ return provider.listTopics({ schoolType, grade, subject, track }).length > 0;
12341
+ }
12342
+ function subjectHasContent(provider, schoolType, grade, subject) {
12343
+ const tracks = provider.listTracks(schoolType, grade, subject);
12344
+ if (tracks.length === 0) {
12345
+ return provider.listTopics({ schoolType, grade, subject }).length > 0;
12346
+ }
12347
+ return tracks.some(
12348
+ (track) => trackHasTopics(provider, schoolType, grade, subject, track.id)
12349
+ );
12350
+ }
12351
+ function gradeHasContent(provider, schoolType, grade) {
12352
+ return provider.listSubjects(schoolType, grade).some(
12353
+ (subject) => subjectHasContent(provider, schoolType, grade, subject.id)
12354
+ );
12355
+ }
12356
+ function withImportableContentOnly(provider) {
12357
+ return {
12358
+ ...provider,
12359
+ listSchoolTypes() {
12360
+ return provider.listSchoolTypes().filter(
12361
+ (schoolType) => provider.listGrades(schoolType.id).some(
12362
+ (grade) => gradeHasContent(provider, schoolType.id, grade.id)
12363
+ )
12364
+ );
12365
+ },
12366
+ listGrades(schoolType) {
12367
+ return provider.listGrades(schoolType).filter((grade) => gradeHasContent(provider, schoolType, grade.id));
12368
+ },
12369
+ listSubjects(schoolType, grade) {
12370
+ return provider.listSubjects(schoolType, grade).filter(
12371
+ (subject) => subjectHasContent(provider, schoolType, grade, subject.id)
12372
+ );
12373
+ },
12374
+ listTracks(schoolType, grade, subject) {
12375
+ return provider.listTracks(schoolType, grade, subject).filter(
12376
+ (track) => trackHasTopics(provider, schoolType, grade, subject, track.id)
12377
+ );
12378
+ }
12379
+ };
12380
+ }
12381
+
12312
12382
  // src/cli/curriculum/providers/bildungsplan-bremen/manifest.ts
12313
12383
  var BILDUNGSPLAN_BREMEN_MANIFEST = {
12314
12384
  schoolYear: "2025/2026",
@@ -78099,6 +78169,213 @@ var LEHRPLANPLUS_BAYERN_MANIFEST = {
78099
78169
  }
78100
78170
  };
78101
78171
 
78172
+ // src/cli/curriculum/providers/lehrplanplus-bayern/track-descriptions.ts
78173
+ var GYMNASIUM_ZWEIGE = {
78174
+ NTG: "Naturwissenschaftlich-technologisches Gymnasium",
78175
+ HG: "Humanistisches Gymnasium",
78176
+ SG: "Sprachliches Gymnasium",
78177
+ MuG: "Musisches Gymnasium",
78178
+ WWG: "Wirtschaftswissenschaftliches Gymnasium",
78179
+ SWG: "Sozialwissenschaftliches Gymnasium"
78180
+ };
78181
+ var FOS_BOS_RICHTUNGEN = {
78182
+ ABU: "Agrarwirtschaft, Bio- und Umwelttechnologie",
78183
+ G: "Gestaltung",
78184
+ GH: "Gesundheit",
78185
+ IW: "Internationale Wirtschaft",
78186
+ S: "Sozialwesen",
78187
+ T: "Technik",
78188
+ W: "Wirtschaft und Verwaltung"
78189
+ };
78190
+ function codesInLabel(label, table) {
78191
+ return Object.keys(table).filter(
78192
+ (code) => new RegExp(`\\b${code}\\b`).test(label)
78193
+ );
78194
+ }
78195
+ function fosBosCodesFromId(id) {
78196
+ const ignored = /* @__PURE__ */ new Set(["wahl", "pflicht"]);
78197
+ const tokens = id.toLowerCase().split(/[-_]/).filter((token) => token.length > 0 && !ignored.has(token));
78198
+ if (tokens.length === 0) return [];
78199
+ const codes = tokens.map((token) => token.toUpperCase());
78200
+ if (!codes.every((code) => code in FOS_BOS_RICHTUNGEN)) return [];
78201
+ return Object.keys(FOS_BOS_RICHTUNGEN).filter((code) => codes.includes(code));
78202
+ }
78203
+ function richtungenSentence(codes) {
78204
+ if (codes.length === 0) return void 0;
78205
+ const named = codes.map((code) => `${FOS_BOS_RICHTUNGEN[code]} (${code})`);
78206
+ if (named.length === 1) {
78207
+ return `F\xFCr die Ausbildungsrichtung ${named[0]}.`;
78208
+ }
78209
+ return `F\xFCr die Ausbildungsrichtungen: ${named.join(", ")}.`;
78210
+ }
78211
+ function collectRealschule(label, parts) {
78212
+ if (/\(I\)\s*$/.test(label)) {
78213
+ parts.push(
78214
+ "F\xFCr die Wahlpflichtf\xE4chergruppe I (mathematisch-naturwissenschaftlich-technischer Zweig, u. a. mit vertiefter Mathematik)."
78215
+ );
78216
+ } else if (/\(II\/III\)\s*$/.test(label)) {
78217
+ parts.push(
78218
+ "F\xFCr die Wahlpflichtf\xE4chergruppen II und III (wirtschaftlicher bzw. fremdsprachlicher, gestalterischer oder sozialer Zweig)."
78219
+ );
78220
+ }
78221
+ }
78222
+ function collectGymnasium(label, parts) {
78223
+ if (label.includes("erh\xF6htes Anforderungsniveau")) {
78224
+ parts.push(
78225
+ "Erh\xF6htes Anforderungsniveau: vertieftes Niveau der Oberstufe, z. B. f\xFCr dein Abiturpr\xFCfungsfach mit erh\xF6hten Anforderungen."
78226
+ );
78227
+ }
78228
+ if (label.includes("grundlegendes Anforderungsniveau")) {
78229
+ parts.push(
78230
+ "Grundlegendes Anforderungsniveau: das Standardniveau der Oberstufe."
78231
+ );
78232
+ }
78233
+ if (label.includes("Vertiefungskurs")) {
78234
+ parts.push(
78235
+ "Freiwilliger Vertiefungskurs zur Studienvorbereitung in der Oberstufe."
78236
+ );
78237
+ }
78238
+ if (/spät beginnende/i.test(label)) {
78239
+ parts.push(
78240
+ "\u201ESp\xE4t beginnend\u201C: f\xFCr alle, die das Fach erst in der Oberstufe neu begonnen haben."
78241
+ );
78242
+ } else if (label.includes("Fremdsprache") && /\d\./.test(label)) {
78243
+ parts.push(
78244
+ "Die Z\xE4hlung richtet sich danach, als wievielte Fremdsprache du die Sprache begonnen hast (Beginn ab Klasse 5 = 1. Fremdsprache)."
78245
+ );
78246
+ }
78247
+ const zweige = codesInLabel(label, GYMNASIUM_ZWEIGE);
78248
+ if (zweige.length === 1) {
78249
+ parts.push(
78250
+ `Gilt f\xFCr den Zweig ${GYMNASIUM_ZWEIGE[zweige[0]]} (${zweige[0]}).`
78251
+ );
78252
+ } else if (zweige.length > 1) {
78253
+ parts.push(
78254
+ `Gilt f\xFCr diese Zweige: ${zweige.map((code) => `${GYMNASIUM_ZWEIGE[code]} (${code})`).join(", ")}.`
78255
+ );
78256
+ }
78257
+ }
78258
+ function collectMittelschule(label, parts) {
78259
+ if (/\(R und M\)/.test(label)) {
78260
+ parts.push(
78261
+ "Gilt f\xFCr Regelklassen (R) und Mittlere-Reife-Klassen (M) gleicherma\xDFen."
78262
+ );
78263
+ } else if (/\bM\d+\b/.test(label)) {
78264
+ parts.push(
78265
+ "M-Klasse (Mittlere-Reife-Zug): erh\xF6htes Anforderungsniveau auf dem Weg zum mittleren Schulabschluss."
78266
+ );
78267
+ } else if (/\bR\d+\b/.test(label)) {
78268
+ parts.push("Regelklasse: der regul\xE4re Bildungsgang der Mittelschule.");
78269
+ }
78270
+ }
78271
+ function collectWirtschaftsschule(label, parts) {
78272
+ if (/(drei- und vier|vier- und drei)stufige/.test(label)) {
78273
+ parts.push(
78274
+ "Gilt f\xFCr die drei- und die vierstufige Wirtschaftsschule gleicherma\xDFen."
78275
+ );
78276
+ } else if (label.includes("vierstufig")) {
78277
+ parts.push("Vierstufige Wirtschaftsschule: Einstieg ab Jahrgangsstufe 7.");
78278
+ } else if (label.includes("dreistufig")) {
78279
+ parts.push("Dreistufige Wirtschaftsschule: Einstieg ab Jahrgangsstufe 8.");
78280
+ } else if (label.includes("zweistufig")) {
78281
+ parts.push("Zweistufige Wirtschaftsschule: Einstieg ab Jahrgangsstufe 10.");
78282
+ }
78283
+ }
78284
+ function collectFosBos(track, parts) {
78285
+ const { id, label } = track;
78286
+ if (label.includes("Vorklasse")) {
78287
+ parts.push(
78288
+ "Vorklasse: das Vollzeit-Vorbereitungsjahr vor Jahrgangsstufe 11 (FOS) bzw. 12 (BOS)."
78289
+ );
78290
+ }
78291
+ if (label.includes("Vorkurs")) {
78292
+ parts.push(
78293
+ "Vorkurs: vorbereitender Kurs (meist in Teilzeit) vor dem Einstieg in die FOS/BOS."
78294
+ );
78295
+ }
78296
+ if (/\bAHR\b/.test(label)) {
78297
+ parts.push(
78298
+ "AHR: Weg zur Allgemeinen Hochschulreife (Abitur), in der Regel mit zweiter Fremdsprache bis Jahrgangsstufe 13."
78299
+ );
78300
+ }
78301
+ if (label.includes("Grundkurs")) {
78302
+ parts.push("Grundkurs: Sprachkurs f\xFCr Einsteiger ohne Vorkenntnisse.");
78303
+ }
78304
+ if (label.includes("Aufbaukurs")) {
78305
+ parts.push(
78306
+ "Aufbaukurs: setzt Vorkenntnisse aus der vorherigen Schule voraus."
78307
+ );
78308
+ }
78309
+ if (label.includes("fortgef\xFChrt")) {
78310
+ parts.push(
78311
+ "Fortgef\xFChrt: Weiterf\xFChrung einer bereits erlernten Fremdsprache."
78312
+ );
78313
+ }
78314
+ if (label.includes("fachpraktisch")) {
78315
+ parts.push(
78316
+ "Teil der fachpraktischen Ausbildung (fpA) in Jahrgangsstufe 11."
78317
+ );
78318
+ }
78319
+ const codes = fosBosCodesFromId(id).length > 0 ? fosBosCodesFromId(id) : codesInLabel(label, FOS_BOS_RICHTUNGEN);
78320
+ const richtungen = richtungenSentence(codes);
78321
+ if (richtungen) {
78322
+ parts.push(richtungen);
78323
+ } else {
78324
+ const prefix = label.match(/^(.+?) – /)?.[1];
78325
+ const prefixRichtung = Object.values(FOS_BOS_RICHTUNGEN).find(
78326
+ (name) => prefix === name
78327
+ );
78328
+ if (prefixRichtung) {
78329
+ parts.push(`F\xFCr die Ausbildungsrichtung ${prefixRichtung}.`);
78330
+ }
78331
+ }
78332
+ }
78333
+ var WOCHENSTUNDEN = {
78334
+ einst\u00FCndig: "einer Wochenstunde",
78335
+ zweist\u00FCndig: "zwei Wochenstunden",
78336
+ dreist\u00FCndig: "drei Wochenstunden"
78337
+ };
78338
+ function collectShared(track, parts) {
78339
+ const { id, label } = track;
78340
+ if (label.includes("Basissport")) {
78341
+ parts.push("Basissport: der verbindliche Sportunterricht f\xFCr alle.");
78342
+ }
78343
+ if (label.includes("Differenzierter Sport")) {
78344
+ parts.push(
78345
+ "Differenzierter Sport: zus\xE4tzlicher Wahlsport in einzelnen Sportarten."
78346
+ );
78347
+ }
78348
+ const stunden = label.match(/(einstündig|zweistündig|dreistündig)/)?.[1];
78349
+ if (stunden) {
78350
+ parts.push(
78351
+ `Fassung mit ${WOCHENSTUNDEN[stunden]} \u2013 welche gilt, h\xE4ngt von der Stundentafel deiner Schule ab.`
78352
+ );
78353
+ }
78354
+ const gueltigAb = label.match(/gültig ab (?:SJ )?(\d{4}\/\d{2})/)?.[1];
78355
+ if (gueltigAb) {
78356
+ parts.push(`Neue Lehrplanfassung: gilt ab dem Schuljahr ${gueltigAb}.`);
78357
+ }
78358
+ const gueltigBis = id.match(/gueltig_bis_(\d{2})_(\d{2})/);
78359
+ if (gueltigBis) {
78360
+ parts.push(
78361
+ `Bisherige Lehrplanfassung: gilt noch bis einschlie\xDFlich Schuljahr 20${gueltigBis[1]}/${gueltigBis[2]}.`
78362
+ );
78363
+ }
78364
+ }
78365
+ function describeBayernTrack(schoolType, track) {
78366
+ const parts = [];
78367
+ if (schoolType === "realschule") collectRealschule(track.label, parts);
78368
+ else if (schoolType === "gymnasium") collectGymnasium(track.label, parts);
78369
+ else if (schoolType === "mittelschule")
78370
+ collectMittelschule(track.label, parts);
78371
+ else if (schoolType === "wirtschaftsschule")
78372
+ collectWirtschaftsschule(track.label, parts);
78373
+ else if (schoolType === "fos" || schoolType === "bos")
78374
+ collectFosBos(track, parts);
78375
+ collectShared(track, parts);
78376
+ return parts.length > 0 ? parts.join(" ") : void 0;
78377
+ }
78378
+
78102
78379
  // src/cli/curriculum/providers/lehrplanplus-bayern/index.ts
78103
78380
  function levelKey(schoolType, grade, subject, track) {
78104
78381
  return track ? `${schoolType}|${grade}|${subject}|${track}` : `${schoolType}|${grade}|${subject}`;
@@ -78212,7 +78489,12 @@ var lehrplanplusBayernProvider = {
78212
78489
  return LEHRPLANPLUS_BAYERN_MANIFEST.subjects[schoolType] ?? [];
78213
78490
  },
78214
78491
  listTracks(schoolType, grade, subject) {
78215
- return LEHRPLANPLUS_BAYERN_MANIFEST.tracks[levelKey(schoolType, grade, subject)] ?? [];
78492
+ return (LEHRPLANPLUS_BAYERN_MANIFEST.tracks[levelKey(schoolType, grade, subject)] ?? []).map(
78493
+ (track) => {
78494
+ const description = describeBayernTrack(schoolType, track);
78495
+ return description ? { ...track, description } : { ...track };
78496
+ }
78497
+ );
78216
78498
  },
78217
78499
  listTopics(selection) {
78218
78500
  const { schoolType, grade, subject, track } = selection;
@@ -78806,7 +79088,7 @@ var CURRICULUM_PROVIDERS = [
78806
79088
  rahmenrichtlinienStProvider,
78807
79089
  fachanforderungenShProvider,
78808
79090
  lehrplanThueringenProvider
78809
- ];
79091
+ ].map(withImportableContentOnly);
78810
79092
  function getCurriculumProvider(id) {
78811
79093
  return CURRICULUM_PROVIDERS.find((provider) => provider.id === id);
78812
79094
  }
@@ -83364,20 +83646,29 @@ bridgeCommand.command("curriculum-preview-topic").description(
83364
83646
  parentTopicId: resolved.topicId
83365
83647
  });
83366
83648
  }
83649
+ const generationChunks = chunks.filter((chunk) => chunk.text.trim());
83650
+ const textProvider = await getProviderForRole(db, "text");
83651
+ const generatedChunks = await mapWithConcurrency(
83652
+ generationChunks,
83653
+ textProvider.local ? 1 : 3,
83654
+ async (chunk) => ({
83655
+ chunk,
83656
+ cards: await importCurriculumViaLLM(
83657
+ db,
83658
+ chunk.text,
83659
+ opts.domain,
83660
+ item.uri,
83661
+ { knowledgeContext: firstContext }
83662
+ )
83663
+ })
83664
+ );
83367
83665
  let proposalIndex = 0;
83368
- for (const chunk of chunks) {
83369
- if (!chunk.text.trim()) continue;
83370
- const cards = await importCurriculumViaLLM(
83371
- db,
83372
- chunk.text,
83373
- opts.domain,
83374
- item.uri,
83375
- { knowledgeContext: firstContext }
83376
- );
83666
+ for (const { chunk, cards } of generatedChunks) {
83377
83667
  const scopedTopicId = chunk.subTopicId ? `${resolved.topicId}@${chunk.subTopicId}` : resolved.topicId;
83378
83668
  for (const card of cards) {
83669
+ const targetDomain = (typeof opts.domain === "string" ? opts.domain.trim() : "") || card.domain;
83379
83670
  const slug = proposalBaseSlug(
83380
- card.domain,
83671
+ targetDomain.split("/")[0] || targetDomain,
83381
83672
  card.question,
83382
83673
  card.concept
83383
83674
  );
@@ -83387,7 +83678,7 @@ bridgeCommand.command("curriculum-preview-topic").description(
83387
83678
  const proposal = {
83388
83679
  question: card.question,
83389
83680
  concept: card.concept,
83390
- domain: card.domain,
83681
+ domain: targetDomain,
83391
83682
  bloom_level: card.bloom_level,
83392
83683
  symbiosis_mode: card.symbiosis_mode || "none",
83393
83684
  excerpt: card.context || "",
@@ -83405,7 +83696,7 @@ bridgeCommand.command("curriculum-preview-topic").description(
83405
83696
  symbiosis_mode: proposal.symbiosis_mode,
83406
83697
  excerpt: proposal.excerpt,
83407
83698
  isExisting: false,
83408
- selected: false,
83699
+ selected: true,
83409
83700
  subTopicId: chunk.subTopicId,
83410
83701
  parentTopicId: resolved.topicId,
83411
83702
  proposal