dsh-plugin-lookatstudy 0.25.1 → 0.25.2

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.
Files changed (2) hide show
  1. package/lib/index.mjs +35 -9
  2. package/package.json +1 -1
package/lib/index.mjs CHANGED
@@ -6582,12 +6582,19 @@ function validateDesign(design, validFiles) {
6582
6582
  };
6583
6583
  }
6584
6584
  /**
6585
- * Heading similarity (word-level overlap) — the translation misalignment
6586
- * guard. Ported verbatim from upstream import-pipeline.ts (their audit F16,
6587
- * 2026-09-13): ordinal alignment assumes both heading lists share structure;
6588
- * a machine-translated file with a dropped header shifts every ordinal, and
6589
- * A's translation would silently land on B's node — overlap too low skips the
6590
- * segment (宁缺毋错). Empty titles cannot be checked and pass conservatively.
6585
+ * Heading similarity — the translation misalignment guard. Ported from
6586
+ * upstream import-pipeline.ts (their audit F16, 2026-09-13): ordinal
6587
+ * alignment assumes both heading lists share structure; a machine-translated
6588
+ * file with a dropped header shifts every ordinal, and A's translation would
6589
+ * silently land on B's node — overlap too low skips the segment (宁缺毋错).
6590
+ * Empty titles cannot be checked and pass conservatively.
6591
+ *
6592
+ * 0.25.2 (owner real-import catch): upstream's word overlap normalizes on
6593
+ * whitespace, and a CJK title is ONE token — paraphrased Chinese headings
6594
+ * (课程总览与环境搭建指南 vs 课程总览) could never overlap and the guard
6595
+ * skipped every non-verbatim segment (38/110 paired on the owner's import;
6596
+ * the 38 were exact matches). CJK strings now compare by character
6597
+ * containment first, then character-bigram overlap ≥ 0.3.
6591
6598
  */
6592
6599
  function headingsSimilar(a, b) {
6593
6600
  const norm = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim().split(/\s+/).filter(Boolean);
@@ -6595,6 +6602,24 @@ function headingsSimilar(a, b) {
6595
6602
  const B = norm(b);
6596
6603
  if (A.length === 0 || B.length === 0) return true;
6597
6604
  if (A.join(" ") === B.join(" ")) return true;
6605
+ const han = (s) => s.toLowerCase().replace(/[^\p{Script=Han}\p{N}]/gu, "");
6606
+ const ca = han(a);
6607
+ const cb = han(b);
6608
+ if (ca.length >= 2 || cb.length >= 2) {
6609
+ if (ca === "" || cb === "") return true;
6610
+ if (ca.includes(cb) || cb.includes(ca)) return true;
6611
+ const bigramsOf = (s) => {
6612
+ const out = /* @__PURE__ */ new Set();
6613
+ const chars = Array.from(s);
6614
+ for (let i = 0; i + 1 < chars.length; i++) out.add(chars[i] + chars[i + 1]);
6615
+ return out;
6616
+ };
6617
+ const ba = bigramsOf(ca);
6618
+ const bb = bigramsOf(cb);
6619
+ let hit = 0;
6620
+ for (const g of ba) if (bb.has(g)) hit++;
6621
+ return hit / Math.max(1, ba.size) >= .3;
6622
+ }
6598
6623
  const setB = new Set(B);
6599
6624
  return A.filter((w) => setB.has(w)).length / A.length >= .3;
6600
6625
  }
@@ -6675,7 +6700,8 @@ function buildCourseFromDesign(courseTitle, validated, contents, translations) {
6675
6700
  let tIndex = titleIndex;
6676
6701
  let keep = true;
6677
6702
  if (lesson.anchor === null || titleIndex === -1) tIndex = -1;
6678
- else if (titleIndex >= tHeadings.length || !headingsSimilar(lesson.title, tHeadings[titleIndex].title)) keep = false;
6703
+ else if (titleIndex >= tHeadings.length) keep = false;
6704
+ else if (!(tHeadings.length === headings.length && tHeadings.every((h, i) => h.level === headings[i].level)) && !headingsSimilar(lesson.title, tHeadings[titleIndex].title)) keep = false;
6679
6705
  if (keep) translation = replaceImagesByPosition(sliceLessonBody(pair.content, tHeadings, tIndex, isFirst), originalImagesOf(body));
6680
6706
  }
6681
6707
  return {
@@ -7739,7 +7765,7 @@ ${p.text}`));
7739
7765
  translations: {
7740
7766
  type: "integer",
7741
7767
  required: true,
7742
- description: "Lessons that landed with a paired translation (the interface-language mirror existed and its anchor matched) — their teach tab offers 原文/对照/译文."
7768
+ description: "Lessons that landed with a paired translation (the interface-language mirror existed and its anchor matched) — their teach tab offers 原文/译文."
7743
7769
  },
7744
7770
  created: {
7745
7771
  type: "boolean",
@@ -7754,7 +7780,7 @@ ${p.text}`));
7754
7780
  },
7755
7781
  render: (_args, value) => [{
7756
7782
  type: "text",
7757
- text: value.created === false ? `“${value.title}” (id ${value.courseId}) was ALREADY imported — the design did not create a new course. Continue with the existing course, or delete it first (study_delete_course) to rebuild from this design.` : `Imported designed course “${value.title}” (${value.sections} sections, ${value.lessons} lessons${value.droppedLessons > 0 ? `, ${value.droppedLessons} hallucinated lesson(s) dropped` : ""})${typeof value.translations === "number" && value.translations > 0 ? ` — ${value.translations} lesson(s) carry the learner's-language translation (原文/对照/译文 in the teach tab)` : ""}${typeof value.languageTarget === "string" ? ` — language course (${value.languageTarget}): keep reading passages, example sentences, and quiz language material in the target language` : ""}. First lesson: “${value.firstLessonTitle}” (id ${value.firstLessonId}). Present the course map to the learner.`
7783
+ text: value.created === false ? `“${value.title}” (id ${value.courseId}) was ALREADY imported — the design did not create a new course. Continue with the existing course, or delete it first (study_delete_course) to rebuild from this design.` : `Imported designed course “${value.title}” (${value.sections} sections, ${value.lessons} lessons${value.droppedLessons > 0 ? `, ${value.droppedLessons} hallucinated lesson(s) dropped` : ""})${typeof value.translations === "number" && value.translations > 0 ? ` — ${value.translations} lesson(s) carry the learner's-language translation (原文/译文 in the teach tab)` : ""}${typeof value.languageTarget === "string" ? ` — language course (${value.languageTarget}): keep reading passages, example sentences, and quiz language material in the target language` : ""}. First lesson: “${value.firstLessonTitle}” (id ${value.firstLessonId}). Present the course map to the learner.`
7758
7784
  }]
7759
7785
  },
7760
7786
  async execute(args, exec) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-lookatstudy",
3
- "version": "0.25.1",
3
+ "version": "0.25.2",
4
4
  "packageManager": "pnpm@11.7.0",
5
5
  "description": "Turn any markdown, local folder, or GitHub learning repo into a guided course inside DeepSeek Harness: gated skill-tree progression, BKT mastery tracking, SM-2 spaced repetition.",
6
6
  "type": "module",