dsh-thesis 1.0.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.
Files changed (188) hide show
  1. package/DESIGN.md +390 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +10 -0
  4. package/README.md +253 -0
  5. package/cordis.patch.yml +17 -0
  6. package/docs/host-api.md +640 -0
  7. package/lib/codewalk/commands.d.ts +23 -0
  8. package/lib/codewalk/commands.js +168 -0
  9. package/lib/codewalk/engine.d.ts +108 -0
  10. package/lib/codewalk/engine.js +449 -0
  11. package/lib/codewalk/errors.d.ts +31 -0
  12. package/lib/codewalk/errors.js +20 -0
  13. package/lib/codewalk/fs-types.d.ts +34 -0
  14. package/lib/codewalk/fs-types.js +10 -0
  15. package/lib/codewalk/gate.d.ts +30 -0
  16. package/lib/codewalk/gate.js +60 -0
  17. package/lib/codewalk/host-types.d.ts +198 -0
  18. package/lib/codewalk/host-types.js +11 -0
  19. package/lib/codewalk/prestep.d.ts +17 -0
  20. package/lib/codewalk/prestep.js +62 -0
  21. package/lib/codewalk/register.d.ts +23 -0
  22. package/lib/codewalk/register.js +31 -0
  23. package/lib/codewalk/skill.d.ts +15 -0
  24. package/lib/codewalk/skill.js +57 -0
  25. package/lib/codewalk/tools.d.ts +18 -0
  26. package/lib/codewalk/tools.js +314 -0
  27. package/lib/codewalk/types.d.ts +151 -0
  28. package/lib/codewalk/types.js +9 -0
  29. package/lib/commands.d.ts +33 -0
  30. package/lib/commands.js +60 -0
  31. package/lib/config.d.ts +125 -0
  32. package/lib/config.js +139 -0
  33. package/lib/dedup/commands.d.ts +21 -0
  34. package/lib/dedup/commands.js +93 -0
  35. package/lib/dedup/index.d.ts +75 -0
  36. package/lib/dedup/index.js +331 -0
  37. package/lib/dedup/normalize.d.ts +125 -0
  38. package/lib/dedup/normalize.js +329 -0
  39. package/lib/dedup/report.d.ts +114 -0
  40. package/lib/dedup/report.js +309 -0
  41. package/lib/dedup/rewrite.d.ts +71 -0
  42. package/lib/dedup/rewrite.js +228 -0
  43. package/lib/dedup/similarity.d.ts +169 -0
  44. package/lib/dedup/similarity.js +323 -0
  45. package/lib/gates/dangerous.d.ts +22 -0
  46. package/lib/gates/dangerous.js +47 -0
  47. package/lib/gates/detect.d.ts +54 -0
  48. package/lib/gates/detect.js +106 -0
  49. package/lib/gates/messages.d.ts +39 -0
  50. package/lib/gates/messages.js +74 -0
  51. package/lib/gates/prompt.d.ts +21 -0
  52. package/lib/gates/prompt.js +56 -0
  53. package/lib/gates/writing-intent.d.ts +49 -0
  54. package/lib/gates/writing-intent.js +68 -0
  55. package/lib/index.d.ts +36 -0
  56. package/lib/index.js +76 -0
  57. package/lib/ingest/code.d.ts +71 -0
  58. package/lib/ingest/code.js +356 -0
  59. package/lib/ingest/commands.d.ts +23 -0
  60. package/lib/ingest/commands.js +74 -0
  61. package/lib/ingest/docx.d.ts +29 -0
  62. package/lib/ingest/docx.js +92 -0
  63. package/lib/ingest/index.d.ts +179 -0
  64. package/lib/ingest/index.js +640 -0
  65. package/lib/ingest/ooxml.d.ts +58 -0
  66. package/lib/ingest/ooxml.js +393 -0
  67. package/lib/ingest/pdf.d.ts +86 -0
  68. package/lib/ingest/pdf.js +1016 -0
  69. package/lib/ingest/pptx.d.ts +32 -0
  70. package/lib/ingest/pptx.js +116 -0
  71. package/lib/ingest/text.d.ts +25 -0
  72. package/lib/ingest/text.js +180 -0
  73. package/lib/ingest/types.d.ts +48 -0
  74. package/lib/ingest/types.js +47 -0
  75. package/lib/ingest/xlsx.d.ts +72 -0
  76. package/lib/ingest/xlsx.js +411 -0
  77. package/lib/ingest/zip.d.ts +76 -0
  78. package/lib/ingest/zip.js +393 -0
  79. package/lib/intake/definitions.d.ts +65 -0
  80. package/lib/intake/definitions.js +42 -0
  81. package/lib/intake/gate-rules.d.ts +57 -0
  82. package/lib/intake/gate-rules.js +118 -0
  83. package/lib/intake/gate.d.ts +24 -0
  84. package/lib/intake/gate.js +86 -0
  85. package/lib/intake/index.d.ts +76 -0
  86. package/lib/intake/index.js +468 -0
  87. package/lib/intake/questions.d.ts +169 -0
  88. package/lib/intake/questions.js +720 -0
  89. package/lib/intake/spec.d.ts +66 -0
  90. package/lib/intake/spec.js +221 -0
  91. package/lib/intake/state.d.ts +192 -0
  92. package/lib/intake/state.js +420 -0
  93. package/lib/memory/recall.d.ts +69 -0
  94. package/lib/memory/recall.js +177 -0
  95. package/lib/memory/register.d.ts +37 -0
  96. package/lib/memory/register.js +52 -0
  97. package/lib/memory/service.d.ts +39 -0
  98. package/lib/memory/service.js +39 -0
  99. package/lib/memory/store.d.ts +45 -0
  100. package/lib/memory/store.js +123 -0
  101. package/lib/memory/tools.d.ts +19 -0
  102. package/lib/memory/tools.js +195 -0
  103. package/lib/memory/types.d.ts +55 -0
  104. package/lib/memory/types.js +6 -0
  105. package/lib/paper/commands.d.ts +13 -0
  106. package/lib/paper/commands.js +84 -0
  107. package/lib/paper/lib/aicheck.d.ts +38 -0
  108. package/lib/paper/lib/aicheck.js +241 -0
  109. package/lib/paper/lib/bibtex.d.ts +21 -0
  110. package/lib/paper/lib/bibtex.js +114 -0
  111. package/lib/paper/lib/check.d.ts +32 -0
  112. package/lib/paper/lib/check.js +215 -0
  113. package/lib/paper/lib/defense.d.ts +48 -0
  114. package/lib/paper/lib/defense.js +220 -0
  115. package/lib/paper/lib/docx.d.ts +35 -0
  116. package/lib/paper/lib/docx.js +213 -0
  117. package/lib/paper/lib/layout.d.ts +41 -0
  118. package/lib/paper/lib/layout.js +572 -0
  119. package/lib/paper/lib/ledger.d.ts +112 -0
  120. package/lib/paper/lib/ledger.js +349 -0
  121. package/lib/paper/lib/lit-api.d.ts +59 -0
  122. package/lib/paper/lib/lit-api.js +296 -0
  123. package/lib/paper/lib/lit-cache.d.ts +32 -0
  124. package/lib/paper/lib/lit-cache.js +91 -0
  125. package/lib/paper/lib/project.d.ts +29 -0
  126. package/lib/paper/lib/project.js +93 -0
  127. package/lib/paper/lib/review.d.ts +37 -0
  128. package/lib/paper/lib/review.js +194 -0
  129. package/lib/paper/lib/zip.d.ts +14 -0
  130. package/lib/paper/lib/zip.js +124 -0
  131. package/lib/paper/register.d.ts +20 -0
  132. package/lib/paper/register.js +183 -0
  133. package/lib/paper/tools/aicheck.d.ts +6 -0
  134. package/lib/paper/tools/aicheck.js +38 -0
  135. package/lib/paper/tools/build.d.ts +27 -0
  136. package/lib/paper/tools/build.js +166 -0
  137. package/lib/paper/tools/check.d.ts +7 -0
  138. package/lib/paper/tools/check.js +79 -0
  139. package/lib/paper/tools/decision.d.ts +13 -0
  140. package/lib/paper/tools/decision.js +53 -0
  141. package/lib/paper/tools/defense.d.ts +11 -0
  142. package/lib/paper/tools/defense.js +74 -0
  143. package/lib/paper/tools/init.d.ts +23 -0
  144. package/lib/paper/tools/init.js +99 -0
  145. package/lib/paper/tools/lit.d.ts +27 -0
  146. package/lib/paper/tools/lit.js +236 -0
  147. package/lib/paper/tools/progress.d.ts +18 -0
  148. package/lib/paper/tools/progress.js +77 -0
  149. package/lib/paper/tools/review.d.ts +11 -0
  150. package/lib/paper/tools/review.js +41 -0
  151. package/lib/ppt/actions.d.ts +97 -0
  152. package/lib/ppt/actions.js +263 -0
  153. package/lib/ppt/check.d.ts +64 -0
  154. package/lib/ppt/check.js +185 -0
  155. package/lib/ppt/commands.d.ts +28 -0
  156. package/lib/ppt/commands.js +71 -0
  157. package/lib/ppt/convert.d.ts +146 -0
  158. package/lib/ppt/convert.js +423 -0
  159. package/lib/ppt/index.d.ts +45 -0
  160. package/lib/ppt/index.js +57 -0
  161. package/lib/ppt/marp.d.ts +74 -0
  162. package/lib/ppt/marp.js +165 -0
  163. package/lib/ppt/outline.d.ts +206 -0
  164. package/lib/ppt/outline.js +695 -0
  165. package/lib/shared/define-tool.d.ts +77 -0
  166. package/lib/shared/define-tool.js +235 -0
  167. package/lib/shared/fs-errors.d.ts +50 -0
  168. package/lib/shared/fs-errors.js +90 -0
  169. package/lib/shared/index.d.ts +31 -0
  170. package/lib/shared/index.js +32 -0
  171. package/lib/shared/spawn.d.ts +38 -0
  172. package/lib/shared/spawn.js +50 -0
  173. package/lib/shared/text.d.ts +14 -0
  174. package/lib/shared/text.js +26 -0
  175. package/package.json +82 -0
  176. package/skills/code-walkthrough/SKILL.md +94 -0
  177. package/skills/code-walkthrough/references/examples.md +129 -0
  178. package/skills/paper-dedup/SKILL.md +58 -0
  179. package/skills/paper-intake/SKILL.md +64 -0
  180. package/skills/thesis-citation/SKILL.md +67 -0
  181. package/skills/thesis-defense/SKILL.md +87 -0
  182. package/skills/thesis-eng-design/SKILL.md +97 -0
  183. package/skills/thesis-inquiry/SKILL.md +55 -0
  184. package/skills/thesis-inquiry/references/intent-spec-template.md +39 -0
  185. package/skills/thesis-literature/SKILL.md +56 -0
  186. package/skills/thesis-opener/SKILL.md +77 -0
  187. package/skills/thesis-pipeline/SKILL.md +121 -0
  188. package/skills/thesis-writing/SKILL.md +86 -0
@@ -0,0 +1,329 @@
1
+ /**
2
+ * 降重模块的文本归一化与切分(确定性,无第三方依赖)。
3
+ *
4
+ * 两件事:
5
+ * 1. **归一化**:全角/半角统一(NFKC 的常用子集)、去 Markdown 标记与空白标点、
6
+ * 中文按字切分、拉丁按词切分(统一小写)。归一化后每个字符都带一条
7
+ * 「原文偏移」记录,使任何命中都能回到原始行号与原文片段。
8
+ * 2. **段落切分**:按空行与 Markdown 结构切段,标题行/代码块/公式行/
9
+ * 参考文献条目/表格行/引用块不算正文重复;短于 `minChars` 的段落跳过。
10
+ *
11
+ * 归一化与偏移必须一一对应:{@link NormalizedText.chars} 的第 i 个字符来自
12
+ * {@link NormalizedText.tokens} 中某个 token 的第 j 个字符,而该 token 记录了
13
+ * 它在**原文**中的起止偏移(`[start, end)`)。
14
+ *
15
+ * @module dsh-thesis/dedup
16
+ */
17
+ // ---------------------------------------------------------------------------
18
+ // 全角 / 半角
19
+ // ---------------------------------------------------------------------------
20
+ /** 常见中文标点 → ASCII 等价物;归一化后会被当作可忽略标点。 */
21
+ const PUNCT_MAP = {
22
+ ',': ',', '。': '.', '、': ',', ';': ';', ':': ':', '?': '?', '!': '!',
23
+ '(': '(', ')': ')', '【': '[', '】': ']', '《': '<', '》': '>',
24
+ '“': '"', '”': '"', '‘': "'", '’': "'", '—': '-', '–': '-', '…': '.',
25
+ '~': '~', '·': '.', ' ': ' ',
26
+ };
27
+ /**
28
+ * 全角 → 半角、常见中文标点 → ASCII、统一大小写与换行符。
29
+ * 除 BOM(丢弃)与 CRLF(合并为一个 `\n`)外逐字符等长替换,
30
+ * 因此行号与原文字符偏移都保持可对应。
31
+ */
32
+ export function toHalfWidth(input) {
33
+ let out = '';
34
+ let pendingCr = false;
35
+ for (const ch of input) {
36
+ if (ch === '\uFEFF')
37
+ continue;
38
+ if (ch === '\r') {
39
+ out += '\n';
40
+ pendingCr = true;
41
+ continue;
42
+ }
43
+ if (ch === '\n') {
44
+ // CRLF 只算一次换行(保证行号与原始文件一致)。
45
+ if (!pendingCr)
46
+ out += '\n';
47
+ pendingCr = false;
48
+ continue;
49
+ }
50
+ pendingCr = false;
51
+ if (ch === '\t') {
52
+ out += ch;
53
+ continue;
54
+ }
55
+ const mapped = PUNCT_MAP[ch];
56
+ if (mapped !== undefined) {
57
+ out += mapped;
58
+ continue;
59
+ }
60
+ const code = ch.codePointAt(0) ?? 0;
61
+ if (code >= 0xFF01 && code <= 0xFF5E) {
62
+ out += String.fromCharCode(code - 0xFEE0);
63
+ continue;
64
+ }
65
+ out += ch;
66
+ }
67
+ return out;
68
+ }
69
+ // ---------------------------------------------------------------------------
70
+ // 行级屏蔽(结构行不算正文)
71
+ // ---------------------------------------------------------------------------
72
+ /** 标题行:`# 标题`。 */
73
+ export function isHeadingLine(line) {
74
+ return /^\s{0,3}#{1,6}\s/.test(line);
75
+ }
76
+ /** 围栏代码块:``` 或 ~~~(≥3 个)。 */
77
+ export function isCodeFence(line) {
78
+ return /^\s{0,3}(`{3,}|~{3,})/.test(line);
79
+ }
80
+ /** 缩进代码块(4 空格 / 1 个制表符开头)。 */
81
+ export function isIndentedCode(line) {
82
+ return /^( {4,}|\t)\S/.test(line);
83
+ }
84
+ /** 独立公式行:`$$...$$`、`\begin{...}` 环境,或整行基本由数学符号构成。 */
85
+ export function isFormulaLine(line) {
86
+ const t = line.trim();
87
+ if (t === '')
88
+ return false;
89
+ if (/^\$\$/.test(t) || /^\\[a-zA-Z]*\{?(equation|align|array)/.test(t))
90
+ return true;
91
+ const compact = t.replace(/\s/g, '');
92
+ if (!/[$\\=]/.test(t) || compact.length === 0)
93
+ return false;
94
+ const math = compact.replace(/[^$\\=+\-*/^_{}()[\]]/g, '').length;
95
+ return math >= compact.length * 0.5;
96
+ }
97
+ /** 图片行、水平分割线、HTML 注释行。 */
98
+ export function isStructuralLine(line) {
99
+ const t = line.trim();
100
+ return /^!\[/.test(t) || /^(-{3,}|\*{3,}|_{3,})$/.test(t) || /^<!--/.test(t) || /-->$/.test(t);
101
+ }
102
+ /** 表格行。 */
103
+ export function isTableLine(line) {
104
+ return /^\s{0,3}\|/.test(line);
105
+ }
106
+ /** 引用块行(以 `>` 开头的说明性文字)。 */
107
+ export function isQuoteLine(line) {
108
+ return /^\s{0,3}>/.test(line);
109
+ }
110
+ /** 列表项行(提纲/清单,不是连续论述)。 */
111
+ export function isListItemLine(line) {
112
+ return /^\s{0,3}([-*+]|\d+[.)])\s/.test(line);
113
+ }
114
+ /** 参考文献标题(`## 参考文献` / `# References` 等)。 */
115
+ export function isReferenceHeading(line) {
116
+ const t = line.trim().replace(/^#{1,6}\s*/, '');
117
+ return /^(参考文献|引用文献|references?|bibliography)$/i.test(t);
118
+ }
119
+ /** 参考文献条目行:`[1] 作者. 题名. 期刊, 年.` */
120
+ export function isReferenceEntry(line) {
121
+ return /^\s{0,3}\[\d{1,3}\]\s*\S/.test(line);
122
+ }
123
+ /** 模板脚手架提示行(`(写作要求:...)`、`(待补)` 等)。 */
124
+ export function isPlaceholderLine(line) {
125
+ const t = line.trim();
126
+ return /^[((]\s*(写作要求|待补|待写|待演练|待填写)/.test(t) || /^(TODO|TBD|FIXME)\b/.test(t);
127
+ }
128
+ /** 该行是否属于「不构成正文重复」的结构行(不含空行判断)。 */
129
+ export function isSkippedLine(line) {
130
+ return isHeadingLine(line)
131
+ || isCodeFence(line)
132
+ || isIndentedCode(line)
133
+ || isFormulaLine(line)
134
+ || isStructuralLine(line)
135
+ || isTableLine(line)
136
+ || isQuoteLine(line)
137
+ || isListItemLine(line)
138
+ || isReferenceHeading(line)
139
+ || isReferenceEntry(line)
140
+ || isPlaceholderLine(line);
141
+ }
142
+ // ---------------------------------------------------------------------------
143
+ // 归一化 + 偏移映射
144
+ // ---------------------------------------------------------------------------
145
+ function isCjk(ch) {
146
+ const code = ch.codePointAt(0) ?? 0;
147
+ return (code >= 0x4E00 && code <= 0x9FFF)
148
+ || (code >= 0x3400 && code <= 0x4DBF)
149
+ || (code >= 0xF900 && code <= 0xFAFF);
150
+ }
151
+ function isLatinStart(ch) {
152
+ return /[A-Za-z\u00C0-\u024F]/.test(ch);
153
+ }
154
+ function isLatinBody(ch) {
155
+ return /[A-Za-z0-9_\u00C0-\u024F]/.test(ch);
156
+ }
157
+ /** 拉丁词内部保留数字与下划线:`http_server`、`apiv2` 仍是可区分的标识符。 */
158
+ function isKeptLatin(ch) {
159
+ return /[A-Za-z0-9_\u00C0-\u024F]/.test(ch);
160
+ }
161
+ /** 数字(含小数):数字是降重的"硬要素",必须参与比对(改了数字就是改数据)。 */
162
+ function isDigitStart(ch) {
163
+ return ch >= '0' && ch <= '9';
164
+ }
165
+ function isDigitBody(ch) {
166
+ return (ch >= '0' && ch <= '9') || ch === '.';
167
+ }
168
+ /**
169
+ * 归一化一段文本,并记录每个归一化字符的原文偏移。
170
+ *
171
+ * 保留中文、拉丁词(含数字与下划线)与独立数字;标点、空白、Markdown 标记
172
+ * 全部丢弃 —— 字符级过滤不会破坏偏移映射(字符是逐个追加的)。
173
+ */
174
+ export function normalizeText(text) {
175
+ const half = toHalfWidth(text);
176
+ const tokens = [];
177
+ const chars = [];
178
+ let normalized = '';
179
+ const pushChar = (ch, offset) => {
180
+ tokens.push({ text: ch, start: offset, end: offset + 1 });
181
+ chars.push({ ch, offset, tokenStart: true, breakBefore: true });
182
+ normalized += ch;
183
+ };
184
+ const pushWord = (from, to, word) => {
185
+ if (word === '')
186
+ return;
187
+ tokens.push({ text: word, start: from, end: to });
188
+ let first = true;
189
+ for (let k = 0; k < to - from; k += 1) {
190
+ const c = word[k];
191
+ if (c === undefined)
192
+ continue;
193
+ chars.push({ ch: c, offset: from + k, tokenStart: first, breakBefore: first });
194
+ first = false;
195
+ }
196
+ normalized += word;
197
+ };
198
+ let i = 0;
199
+ while (i < half.length) {
200
+ const ch = half[i];
201
+ if (isCjk(ch)) {
202
+ pushChar(ch, i);
203
+ i += 1;
204
+ continue;
205
+ }
206
+ if (isLatinStart(ch)) {
207
+ let j = i + 1;
208
+ while (j < half.length && isLatinBody(half[j]))
209
+ j += 1;
210
+ pushWord(i, j, half.slice(i, j).toLowerCase());
211
+ i = j;
212
+ continue;
213
+ }
214
+ if (isDigitStart(ch)) {
215
+ let j = i + 1;
216
+ while (j < half.length && isDigitBody(half[j]))
217
+ j += 1;
218
+ pushWord(i, j, half.slice(i, j));
219
+ i = j;
220
+ continue;
221
+ }
222
+ i += 1;
223
+ }
224
+ return { text: normalized, tokens, chars };
225
+ }
226
+ /**
227
+ * 把归一化字符区间 `[from, to)` 映射回**原文**的字符区间。
228
+ * 返回闭开区间 `{ start, end }`;空区间返回 `null`。
229
+ */
230
+ export function offsetsFor(doc, from, to) {
231
+ const lo = Math.max(0, from);
232
+ const hi = Math.min(doc.chars.length, to);
233
+ if (hi <= lo)
234
+ return null;
235
+ const first = doc.chars[lo];
236
+ const last = doc.chars[hi - 1];
237
+ return { start: first.offset, end: last.offset + 1 };
238
+ }
239
+ /** 原文区间 `[start, end)` 的文本(越界自动截断)。 */
240
+ export function sliceRaw(raw, start, end) {
241
+ const lo = Math.max(0, Math.min(raw.length, start));
242
+ const hi = Math.max(lo, Math.min(raw.length, end));
243
+ return raw.slice(lo, hi);
244
+ }
245
+ /** 原始文本中某个字符偏移对应的 1 基行号。 */
246
+ export function lineAt(raw, offset) {
247
+ const clamped = Math.max(0, Math.min(raw.length, offset));
248
+ let line = 1;
249
+ for (let i = 0; i < clamped; i += 1) {
250
+ if (raw[i] === '\n')
251
+ line += 1;
252
+ }
253
+ return line;
254
+ }
255
+ // ---------------------------------------------------------------------------
256
+ // 段落切分
257
+ // ---------------------------------------------------------------------------
258
+ /**
259
+ * 按空行与 Markdown 结构切段。
260
+ *
261
+ * 规则(全部确定性):
262
+ * - 围栏代码块(``` 之间)、标题行、公式行、表格行、引用块、列表项行、
263
+ * 参考文献条目/文献区、模板占位行,一律**不参与**比对;
264
+ * - 连续的非跳过行合并为一个段落,段落边界在空行或跳过行处断开;
265
+ * - 归一化后不足 `minChars` 个字符的段落跳过(返回 `skipped` 便于解释原因)。
266
+ */
267
+ export function segmentsFromText(text, options) {
268
+ // 归一化输入的形态:全角→半角(逐字符等长,BOM 除外)与 CRLF→LF,
269
+ // 因此行号、段内字符偏移都与原文一致。
270
+ const halfText = toHalfWidth(text);
271
+ const halfLines = halfText.split('\n');
272
+ const segments = [];
273
+ const chapter = options.chapter ?? options.file;
274
+ let skippedShort = 0;
275
+ let inCode = false;
276
+ let inReferences = false;
277
+ let bufferStart = -1;
278
+ let bufferEnd = -1;
279
+ const flush = () => {
280
+ if (bufferStart < 0)
281
+ return;
282
+ const start = bufferStart;
283
+ const end = bufferEnd;
284
+ bufferStart = -1;
285
+ bufferEnd = -1;
286
+ const raw = halfLines.slice(start - 1, end).join('\n');
287
+ const doc = normalizeText(raw);
288
+ if (doc.chars.length < options.minChars) {
289
+ skippedShort += 1;
290
+ return;
291
+ }
292
+ segments.push({
293
+ file: options.file,
294
+ chapter,
295
+ lineStart: start,
296
+ lineEnd: end,
297
+ raw,
298
+ text: raw,
299
+ doc,
300
+ });
301
+ };
302
+ for (let index = 0; index < halfLines.length; index += 1) {
303
+ const line = halfLines[index];
304
+ const lineNo = index + 1;
305
+ if (isCodeFence(line)) {
306
+ flush();
307
+ inCode = !inCode;
308
+ continue;
309
+ }
310
+ if (inCode)
311
+ continue;
312
+ if (isReferenceHeading(line)) {
313
+ flush();
314
+ inReferences = true;
315
+ continue;
316
+ }
317
+ if (inReferences)
318
+ continue;
319
+ if (line.trim() === '' || isSkippedLine(line)) {
320
+ flush();
321
+ continue;
322
+ }
323
+ if (bufferStart < 0)
324
+ bufferStart = lineNo;
325
+ bufferEnd = lineNo;
326
+ }
327
+ flush();
328
+ return { segments, skippedShort };
329
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * 降重报告的渲染:分级、行号、命中来源、原文片段、处方、全文加权重复率估算,
3
+ * 以及可回填的「学校检测结果」区块。
4
+ *
5
+ * 报告口径(必须与文字一起出现,不得省去):
6
+ * - 本地相似度是**字符串重合度估算**,算法为 shingle 指纹 + 包含率,
7
+ * 只在用户给定的参考语料内比较,**不等于**知网/维普等系统的重复率;
8
+ * - 本插件不接入任何收费查重系统(无账号、也不应接入);学校结果是权威口径,
9
+ * 用户自行送检后可把重复率回填进本报告,用于对齐两套口径的差距。
10
+ *
11
+ * @module dsh-thesis/dedup
12
+ */
13
+ import type { ScanResult } from './similarity.ts';
14
+ import { type RewritePlan } from './rewrite.ts';
15
+ /** 风险分级:high(≥ threshold)| medium(≥ 0.6×threshold)| low(其余)。 */
16
+ export type RiskLevel = 'high' | 'medium' | 'low';
17
+ export interface ReportRiskItem {
18
+ readonly level: RiskLevel;
19
+ readonly chapter: string;
20
+ readonly file: string;
21
+ readonly lineStart: number;
22
+ readonly lineEnd: number;
23
+ readonly score: number;
24
+ readonly source: string;
25
+ /** 原文片段(截断到 120 字)或命中片段(原文坐标)。 */
26
+ readonly excerpt: string;
27
+ /** 命中片段(原文坐标,含行号)。 */
28
+ readonly matches: readonly {
29
+ readonly lineStart: number;
30
+ readonly lineEnd: number;
31
+ readonly text: string;
32
+ }[];
33
+ /** 结构化改写处方。 */
34
+ readonly plan: RewritePlan;
35
+ /** 该段原始文本(未截断,供结构化消费方使用)。 */
36
+ readonly text: string;
37
+ }
38
+ export interface ReportSummary {
39
+ readonly paragraphCount: number;
40
+ readonly matchedCount: number;
41
+ readonly high: number;
42
+ readonly medium: number;
43
+ readonly low: number;
44
+ /** 唯一命中来源数。 */
45
+ readonly sourceCount: number;
46
+ /** 全文加权重复率估计(见 {@link ReportOptions.algorithm})。 */
47
+ readonly weightedRate: number;
48
+ }
49
+ export interface DetectedResult {
50
+ /** 用户自行送检得到的重复率(0..1,或 0..100 的百分数,>1 视为百分数)。 */
51
+ readonly rate: number;
52
+ /** 检测系统名称(如"知网 PMLC"),可缺省。 */
53
+ readonly system?: string;
54
+ /** 送检/出报告日期(任意字符串,原样保留)。 */
55
+ readonly date?: string;
56
+ }
57
+ export interface ReportOptions {
58
+ /** 判定高风险的包含率阈值。 */
59
+ readonly threshold: number;
60
+ readonly shingle: number;
61
+ readonly minChars: number;
62
+ /** 用户自行送检的结果(可选,回填进报告)。 */
63
+ readonly detected?: DetectedResult;
64
+ /** 查询时间(默认 `new Date()`;测试注入固定时间以保证可复现)。 */
65
+ readonly now?: Date;
66
+ /** 本次扫描的参考语料说明(文件/目录,展示用)。 */
67
+ readonly corpusLabels?: readonly string[];
68
+ }
69
+ export interface DedupReport {
70
+ readonly summary: ReportSummary;
71
+ readonly risks: readonly ReportRiskItem[];
72
+ readonly algorithm: string;
73
+ readonly generatedAt: string;
74
+ readonly detected?: DetectedResult;
75
+ readonly boundaries: readonly string[];
76
+ }
77
+ /** 估算口径说明(单一来源,Markdown 与结构化对象共用)。 */
78
+ export declare const ALGORITHM_NOTE: string;
79
+ /** 边界说明(写入报告,避免误用)。 */
80
+ export declare const BOUNDARY_NOTES: readonly string[];
81
+ /**
82
+ * 全文加权重复率估计(口径明确、可复现):
83
+ * `Σ(命中段落归一化字符数 × 该段最佳命中得分) / 全文参与比对的字符总数`。
84
+ * 分母包含**未命中段落**(按 0 计入),因此该值是保守估计(不会低报)。
85
+ */
86
+ export declare function weightedRateOf(scanResult: ScanResult): number;
87
+ /** 结构化报告对象即「报告的结构化形态」。 */
88
+ export declare function buildReport(scanResult: ScanResult, options: ReportOptions): DedupReport;
89
+ export declare function levelLabel(level: RiskLevel): string;
90
+ /** 渲染 Markdown 报告。 */
91
+ export declare function renderReport(report: DedupReport, options: ReportOptions): string;
92
+ export interface BaselineSnapshot {
93
+ readonly paragraphCount: number;
94
+ readonly matchedCount: number;
95
+ readonly high: number;
96
+ readonly medium: number;
97
+ readonly weightedRate: number;
98
+ }
99
+ /** 从 baseline 报告(Markdown)解析出汇总指标;解析失败返回 null。 */
100
+ export declare function parseBaseline(markdown: string): BaselineSnapshot | null;
101
+ export interface VerifyDelta {
102
+ readonly baseline: BaselineSnapshot;
103
+ readonly current: BaselineSnapshot;
104
+ /** 加权重复率降幅(百分点,正数表示下降)。 */
105
+ readonly rateDrop: number;
106
+ readonly rateDropRatio: number;
107
+ readonly highDrop: number;
108
+ readonly mediumDrop: number;
109
+ readonly verdict: string;
110
+ }
111
+ /** 计算复测降幅(正数 = 下降)。 */
112
+ export declare function verifyDelta(baseline: BaselineSnapshot, current: BaselineSnapshot): VerifyDelta;
113
+ /** 渲染复测报告(降重报告-复测.md)。 */
114
+ export declare function renderVerifyReport(delta: VerifyDelta, options: ReportOptions, riskSource: DedupReport): string;