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,194 @@
1
+ /**
2
+ * 章节确定性评审(thesis_review 的核心)。
3
+ *
4
+ * 六个可执行检查,全部基于真实文件内容,不做任何"感觉式"判断:
5
+ * 1. 字数:正文 CJK 字符数 vs 该章目标区间
6
+ * 2. 大纲:模板要求的全部小节标题是否齐全
7
+ * 3. 引用:文中引用编号是否都能对应 refs.bib 中的条目(顺序编码制)
8
+ * 4. 图表:图/表编号章号正确、序号从 1 连续
9
+ * 5. 未完成标记:待补/TODO/尖括号占位等残留
10
+ * 6. G2 清单:章节模板内嵌验收清单的勾选状态
11
+ *
12
+ * 语义质量(论证是否充分、逻辑是否连贯)由 AI 依据 thesis-writing 技能评审,
13
+ * 本工具只负责"可被机器证明"的部分——报告与清单一起构成 G2 验收依据。
14
+ */
15
+ import { parseBibKeys } from "./bibtex.js";
16
+ // ---------------------------------------------------------------------------
17
+ // 各检查项
18
+ // ---------------------------------------------------------------------------
19
+ function cjkCount(text) {
20
+ return (text.match(/[\u4e00-\u9fff]/g) ?? []).length;
21
+ }
22
+ function parseWordRange(meta) {
23
+ const m = /(\d+)\s*[-–]\s*(\d+)/.exec(meta.words);
24
+ if (m === null)
25
+ return undefined;
26
+ return { min: Number(m[1]), max: Number(m[2]) };
27
+ }
28
+ function checkWordCount(text, meta) {
29
+ const range = parseWordRange(meta);
30
+ if (range === undefined)
31
+ return { check: '字数', ok: true, detail: `目标区间未定义(${meta.words})` };
32
+ const count = cjkCount(text);
33
+ if (count < range.min) {
34
+ return { check: '字数', ok: false, detail: `${count} 字,低于目标 ${meta.words}(差 ${range.min - count} 字)` };
35
+ }
36
+ if (count > range.max) {
37
+ return { check: '字数', ok: false, detail: `${count} 字,超过目标 ${meta.words}(超 ${count - range.max} 字)` };
38
+ }
39
+ return { check: '字数', ok: true, detail: `${count} 字,符合目标 ${meta.words}` };
40
+ }
41
+ /** 从标题行提取小节编号(如 "## 1.1 研究背景" → "1.1")。 */
42
+ function headingNumbers(text) {
43
+ const numbers = new Set();
44
+ for (const line of text.split('\n')) {
45
+ const m = /^#{1,6}\s+(\d+(?:\.\d+)*)\s/.exec(line);
46
+ if (m?.[1] !== undefined)
47
+ numbers.add(m[1]);
48
+ }
49
+ return numbers;
50
+ }
51
+ function checkOutline(text, meta) {
52
+ const found = headingNumbers(text);
53
+ const missing = [];
54
+ for (const item of meta.outline) {
55
+ const m = /^(\d+(?:\.\d+)*)\s/.exec(item);
56
+ const number = m?.[1];
57
+ if (number !== undefined && !found.has(number))
58
+ missing.push(item);
59
+ }
60
+ if (missing.length === 0) {
61
+ return { check: '大纲', ok: true, detail: `${meta.outline.length} 个小节全部齐全` };
62
+ }
63
+ return { check: '大纲', ok: false, detail: `缺少小节:${missing.join('、')}` };
64
+ }
65
+ /** 解析正文引用标注 [1]、[2-4]、[1,3,5],返回所有引用编号。 */
66
+ function citedNumbers(text) {
67
+ const numbers = new Set();
68
+ for (const m of text.matchAll(/\[(\d+(?:[-,,]\d+)*)\]/g)) {
69
+ const token = m[1];
70
+ for (const part of token.split(/[,,]/)) {
71
+ const range = /^(\d+)-(\d+)$/.exec(part);
72
+ if (range !== null) {
73
+ const from = Number(range[1]);
74
+ const to = Number(range[2]);
75
+ for (let n = Math.min(from, to); n <= Math.max(from, to); n += 1)
76
+ numbers.add(n);
77
+ }
78
+ else if (/^\d+$/.test(part)) {
79
+ numbers.add(Number(part));
80
+ }
81
+ }
82
+ }
83
+ return [...numbers].sort((a, b) => a - b);
84
+ }
85
+ function checkCitations(text, bibText, chapterNo) {
86
+ const cited = citedNumbers(text);
87
+ const bibCount = parseBibKeys(bibText).size;
88
+ if (cited.length === 0) {
89
+ // 绪论(1)与相关技术(2)必须引用;其余章允许无引用。
90
+ if (chapterNo === 1 || chapterNo === 2) {
91
+ return { check: '引用', ok: false, detail: `正文无任何引用标注。绪论与相关技术章必须引用文献(当前 refs.bib 共 ${bibCount} 条)` };
92
+ }
93
+ return { check: '引用', ok: true, detail: `本章无引用(第 ${chapterNo} 章允许;绪论与相关技术章才强制要求)` };
94
+ }
95
+ const max = cited[cited.length - 1];
96
+ if (max > bibCount) {
97
+ return { check: '引用', ok: false, detail: `引用编号 [${max}] 超出文献库(refs.bib 共 ${bibCount} 条)` };
98
+ }
99
+ return { check: '引用', ok: true, detail: `引用编号 ${cited.join('、')} 共 ${cited.length} 个,均在 refs.bib(${bibCount} 条)范围内` };
100
+ }
101
+ /** 图/表编号:第 n 章的编号必须是 n-k,k 从 1 连续,且不得出现其它章编号。 */
102
+ export function checkNumberedObjects(text, chapterNo, kind) {
103
+ const pattern = kind === '图' ? /图\s*(\d+)[-–](\d+)/g : /表\s*(\d+)[-–](\d+)/g;
104
+ const numbers = new Set();
105
+ const wrongChapter = new Set();
106
+ for (const m of text.matchAll(pattern)) {
107
+ const chapter = Number(m[1]);
108
+ if (chapter === chapterNo) {
109
+ numbers.add(Number(m[2]));
110
+ }
111
+ else {
112
+ wrongChapter.add(`${kind} ${m[1]}-${m[2]}`);
113
+ }
114
+ }
115
+ const problems = [];
116
+ const list = [...numbers].sort((a, b) => a - b);
117
+ if (list.length === 0 && wrongChapter.size === 0) {
118
+ return { check: `${kind}编号`, ok: true, detail: `本章无${kind}(如后续添加,编号须为 ${kind} ${chapterNo}-1 起连续)` };
119
+ }
120
+ if (list.length > 0) {
121
+ const max = list[list.length - 1];
122
+ for (let n = 1; n <= max; n += 1) {
123
+ if (!numbers.has(n)) {
124
+ problems.push(`编号不连续:有 ${kind} ${chapterNo}-${max} 但缺 ${kind} ${chapterNo}-${n}`);
125
+ break;
126
+ }
127
+ }
128
+ }
129
+ if (wrongChapter.size > 0) {
130
+ problems.push(`出现其它章的编号:${[...wrongChapter].join('、')}(本章为第 ${chapterNo} 章)`);
131
+ }
132
+ if (problems.length > 0)
133
+ return { check: `${kind}编号`, ok: false, detail: problems.join(';') };
134
+ return { check: `${kind}编号`, ok: true, detail: `${list.map(n => `${kind} ${chapterNo}-${n}`).join('、')} 连续且章号正确` };
135
+ }
136
+ const PLACEHOLDER_RE = /(待补)|待补充|待写|TODO|TBD|FIXME|XXX|此处省略|<[^>]{2,}>|[待[^]]*]/g;
137
+ function checkPlaceholders(text) {
138
+ const hits = [];
139
+ for (const m of text.matchAll(PLACEHOLDER_RE))
140
+ hits.push(m[0]);
141
+ if (hits.length === 0)
142
+ return { check: '未完成标记', ok: true, detail: '无待补/TODO 等残留' };
143
+ const samples = [...new Set(hits)].slice(0, 5).join('、');
144
+ return { check: '未完成标记', ok: false, detail: `${hits.length} 处残留:${samples}${hits.length > 5 ? ' 等' : ''}。完稿前必须清理` };
145
+ }
146
+ function checkG2Checklist(text, meta) {
147
+ const section = /##\s*本章 G2 验收清单([\s\S]*?)(?=\n## |\n# |$)/.exec(text);
148
+ const body = section?.[1] ?? '';
149
+ const unchecked = (body.match(/^\s*- \[ \]/gm) ?? []).length;
150
+ const checked = (body.match(/^\s*- \[x\]/gm) ?? []).length;
151
+ const total = meta.checklist.length;
152
+ if (body.trim() === '') {
153
+ return { check: 'G2 清单', ok: false, detail: `章节缺少"本章 G2 验收清单"一节(模板要求 ${total} 项)` };
154
+ }
155
+ if (unchecked > 0) {
156
+ return { check: 'G2 清单', ok: false, detail: `${unchecked}/${total} 项未勾选(已勾选 ${checked} 项)。逐项完成后勾选,再进入用户人工验收` };
157
+ }
158
+ return { check: 'G2 清单', ok: true, detail: `${total} 项全部勾选` };
159
+ }
160
+ export function reviewChapter(input) {
161
+ const findings = [
162
+ checkWordCount(input.chapterText, input.meta),
163
+ checkOutline(input.chapterText, input.meta),
164
+ checkCitations(input.chapterText, input.bibText, input.chapterNo),
165
+ checkNumberedObjects(input.chapterText, input.chapterNo, '图'),
166
+ checkNumberedObjects(input.chapterText, input.chapterNo, '表'),
167
+ checkPlaceholders(input.chapterText),
168
+ checkG2Checklist(input.chapterText, input.meta),
169
+ ];
170
+ return {
171
+ chapter: input.meta.file,
172
+ chapterNo: input.chapterNo,
173
+ title: input.meta.title,
174
+ findings,
175
+ allOk: findings.every(f => f.ok),
176
+ };
177
+ }
178
+ export function renderReviewReport(report) {
179
+ const lines = [];
180
+ lines.push(`# 章节评审报告:${report.chapter}(第 ${report.chapterNo} 章 ${report.title})`);
181
+ lines.push('');
182
+ for (const f of report.findings) {
183
+ lines.push(`- ${f.ok ? '✓' : '✗'} ${f.check}:${f.detail}`);
184
+ }
185
+ const failed = report.findings.filter(f => !f.ok).length;
186
+ lines.push('');
187
+ if (failed === 0) {
188
+ lines.push('结论:全部确定性检查通过。接下来由 AI 依据 thesis-writing 技能做语义评审(论证/逻辑/去 AI 味),然后进入 G2 人工验收。');
189
+ }
190
+ else {
191
+ lines.push(`结论:${failed} 项检查未通过,先处理上述问题再复评(thesis_review 复跑)。处理完毕后进入 G2 人工验收:用户阅读→修改→签字,然后用 thesis_progress gate G2 pass。`);
192
+ }
193
+ return lines.join('\n');
194
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 零依赖 ZIP 写入器(仅 STORE 无压缩),用于产出 .docx。
3
+ *
4
+ * .docx 是 ZIP 容器;Word/WPS 对 STORE(method 0)条目完全兼容。
5
+ * 条目名使用 UTF-8(general purpose flag bit 11),CRC32 自实现。
6
+ */
7
+ export interface ZipEntry {
8
+ readonly name: string;
9
+ readonly data: Uint8Array;
10
+ }
11
+ export declare function crc32(data: Uint8Array): number;
12
+ export declare function buildZip(entries: readonly ZipEntry[]): Uint8Array;
13
+ /** 测试辅助:在 zip 字节里搜索给定条目名是否存在(字节级验证)。 */
14
+ export declare function zipContainsName(zip: Uint8Array, name: string): boolean;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * 零依赖 ZIP 写入器(仅 STORE 无压缩),用于产出 .docx。
3
+ *
4
+ * .docx 是 ZIP 容器;Word/WPS 对 STORE(method 0)条目完全兼容。
5
+ * 条目名使用 UTF-8(general purpose flag bit 11),CRC32 自实现。
6
+ */
7
+ // ---------------------------------------------------------------------------
8
+ // CRC32(IEEE 802.3,多项式 0xEDB88320,表驱动)
9
+ // ---------------------------------------------------------------------------
10
+ const CRC_TABLE = (() => {
11
+ const table = new Uint32Array(256);
12
+ for (let n = 0; n < 256; n += 1) {
13
+ let c = n;
14
+ for (let k = 0; k < 8; k += 1) {
15
+ c = (c & 1) !== 0 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
16
+ }
17
+ table[n] = c >>> 0;
18
+ }
19
+ return table;
20
+ })();
21
+ export function crc32(data) {
22
+ let c = 0xffffffff;
23
+ for (const byte of data) {
24
+ c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8);
25
+ }
26
+ return (c ^ 0xffffffff) >>> 0;
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // 组装
30
+ // ---------------------------------------------------------------------------
31
+ const encoder = new TextEncoder();
32
+ function u16le(value) {
33
+ const out = new Uint8Array(2);
34
+ out[0] = value & 0xff;
35
+ out[1] = (value >>> 8) & 0xff;
36
+ return out;
37
+ }
38
+ function u32le(value) {
39
+ const out = new Uint8Array(4);
40
+ out[0] = value & 0xff;
41
+ out[1] = (value >>> 8) & 0xff;
42
+ out[2] = (value >>> 16) & 0xff;
43
+ out[3] = (value >>> 24) & 0xff;
44
+ return out;
45
+ }
46
+ function concat(parts) {
47
+ const total = parts.reduce((sum, p) => sum + p.length, 0);
48
+ const out = new Uint8Array(total);
49
+ let offset = 0;
50
+ for (const part of parts) {
51
+ out.set(part, offset);
52
+ offset += part.length;
53
+ }
54
+ return out;
55
+ }
56
+ export function buildZip(entries) {
57
+ const localParts = [];
58
+ const centralParts = [];
59
+ let offset = 0;
60
+ for (const entry of entries) {
61
+ const nameBytes = encoder.encode(entry.name);
62
+ const crc = crc32(entry.data);
63
+ const size = entry.data.length;
64
+ // 本地文件头
65
+ const local = concat([
66
+ u32le(0x04034b50),
67
+ u16le(20), // version needed
68
+ u16le(0x0800), // flags: UTF-8 名称
69
+ u16le(0), // method: STORE
70
+ u16le(0), u16le(0), // 时间/日期(0 合法)
71
+ u32le(crc),
72
+ u32le(size),
73
+ u32le(size),
74
+ u16le(nameBytes.length),
75
+ u16le(0), // extra 长度
76
+ nameBytes,
77
+ entry.data,
78
+ ]);
79
+ localParts.push(local);
80
+ // 中央目录项
81
+ const central = concat([
82
+ u32le(0x02014b50),
83
+ u16le(20), u16le(20), // 版本
84
+ u16le(0x0800),
85
+ u16le(0),
86
+ u16le(0), u16le(0),
87
+ u32le(crc),
88
+ u32le(size),
89
+ u32le(size),
90
+ u16le(nameBytes.length),
91
+ u16le(0), u16le(0), u16le(0), // extra/comment/disk/attrs
92
+ u16le(0), // internal attrs
93
+ u32le(0), // external attrs
94
+ u32le(offset),
95
+ nameBytes,
96
+ ]);
97
+ centralParts.push(central);
98
+ offset += local.length;
99
+ }
100
+ const centralDir = concat(centralParts);
101
+ const centralSize = centralDir.length;
102
+ const count = entries.length;
103
+ const eocd = concat([
104
+ u32le(0x06054b50),
105
+ u16le(0), u16le(0), // 磁盘号
106
+ u16le(count), u16le(count),
107
+ u32le(centralSize),
108
+ u32le(offset),
109
+ u16le(0), // comment 长度
110
+ ]);
111
+ return concat([...localParts, centralDir, eocd]);
112
+ }
113
+ /** 测试辅助:在 zip 字节里搜索给定条目名是否存在(字节级验证)。 */
114
+ export function zipContainsName(zip, name) {
115
+ const needle = encoder.encode(name);
116
+ outer: for (let i = 0; i + needle.length <= zip.length; i += 1) {
117
+ for (let j = 0; j < needle.length; j += 1) {
118
+ if (zip[i + j] !== needle[j])
119
+ continue outer;
120
+ }
121
+ return true;
122
+ }
123
+ return false;
124
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 论文工作区的工具与命令装配:台账、关卡、文献、评审、检查、构建、答辩素材。
3
+ *
4
+ * 纯业务逻辑在 `paper/tools/*.ts` 与 `paper/lib/*.ts`(零 cordis 依赖、可独立
5
+ * 测试);本文件只做 `defineTool` / 命令注册的胶水。
6
+ *
7
+ * @module dsh-thesis/paper
8
+ */
9
+ import type { Context } from '@deepseek-ai/cordis';
10
+ /** 论文流水线装配所需的宿主侧输入。 */
11
+ export interface PaperOptions {
12
+ /** 随包发布的技能目录绝对路径(`thesis_init` 复制到论文仓库 `.dsh/skills/`)。 */
13
+ readonly skillsDir: string;
14
+ }
15
+ /**
16
+ * 注册 11 个论文工具 + 5 个斜杠命令。
17
+ *
18
+ * 命令名只允许 `[a-z][a-z0-9_-]*`,故统一 `thesis-` 前缀。
19
+ */
20
+ export declare function registerPaper(ctx: Context, options: PaperOptions): void;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * 论文工作区的工具与命令装配:台账、关卡、文献、评审、检查、构建、答辩素材。
3
+ *
4
+ * 纯业务逻辑在 `paper/tools/*.ts` 与 `paper/lib/*.ts`(零 cordis 依赖、可独立
5
+ * 测试);本文件只做 `defineTool` / 命令注册的胶水。
6
+ *
7
+ * @module dsh-thesis/paper
8
+ */
9
+ import { definePaperTool, sessionCwd, textOutput, writeBinary } from "../shared/index.js";
10
+ import { buildCommand, checkCommand, decideCommand, litCommand, statusCommand } from "./commands.js";
11
+ import { initSummary, runInit } from "./tools/init.js";
12
+ import { runProgress } from "./tools/progress.js";
13
+ import { runDecision } from "./tools/decision.js";
14
+ import { runLitNote, runLitSave, runLitSearch } from "./tools/lit.js";
15
+ import { runReview } from "./tools/review.js";
16
+ import { buildSummary, runBuild } from "./tools/build.js";
17
+ import { runCheck } from "./tools/check.js";
18
+ import { runAiSelfcheckTool } from "./tools/aicheck.js";
19
+ import { runDefensePrep } from "./tools/defense.js";
20
+ import { LIT_SOURCES } from "./lib/lit-api.js";
21
+ /**
22
+ * 注册 11 个论文工具 + 5 个斜杠命令。
23
+ *
24
+ * 命令名只允许 `[a-z][a-z0-9_-]*`,故统一 `thesis-` 前缀。
25
+ */
26
+ export function registerPaper(ctx, options) {
27
+ ctx.tools.register(definePaperTool({
28
+ name: 'thesis_init',
29
+ description: '在指定绝对路径创建论文工作区:目录结构、进度台账、决定日志、时间线、七章论文模板、文献库骨架,并把 dsh-thesis 技能包写入 .dsh/skills/(项目级技能,随 git 版本化)。可选 git 初始化并首次提交。目标目录须为空或不存在;绝不覆盖已有文件。',
30
+ parameters: {
31
+ root: { type: 'string', required: true, description: '工作区根目录的绝对路径(空目录或不存在)' },
32
+ title: { type: 'string', description: '论文/课题标题,缺省为"本科毕业设计"(之后可改)' },
33
+ git: { type: 'boolean', description: '是否 git 初始化并首次提交,默认 true' },
34
+ force: { type: 'boolean', description: '目标目录非空时仍尝试写入,默认 false(安全第一)' },
35
+ },
36
+ output: textOutput(),
37
+ async execute(rawArgs, exec) {
38
+ const outcome = await runInit(ctx.fs, rawArgs, options.skillsDir, exec.signal);
39
+ return initSummary(outcome);
40
+ },
41
+ }));
42
+ ctx.tools.register(definePaperTool({
43
+ name: 'thesis_progress',
44
+ description: '读写论文进度台账(00-管理/进度台账.md)并强制执行人工关卡闸门:' +
45
+ 'report 查看当前阶段/关卡/任务状态;update 推进任务状态(todo/doing/done,受阶段入口关卡约束);' +
46
+ 'gate 通过或重置人工关卡 G1(选题拍板)/G2(逐章验收,需 chapter)/G3(开题审阅)/G4(提交前自查),' +
47
+ '通过关卡前其关联任务必须先完成。',
48
+ parameters: {
49
+ action: { type: 'string', required: true, description: 'report | update | gate' },
50
+ task_id: { type: 'string', description: 'update 时必填:任务 ID(如 T1.1)' },
51
+ status: { type: 'string', description: 'update 时必填:todo | doing | done' },
52
+ note: { type: 'string', description: '可选备注(update 与 gate 均可用)' },
53
+ gate: { type: 'string', description: 'gate 时必填:G1 | G2 | G3 | G4' },
54
+ pass: { type: 'boolean', description: 'gate 时必填:true 通过,false 重置' },
55
+ chapter: { type: 'string', description: 'gate=G2 时必填:章节文件名,如 01-绪论' },
56
+ },
57
+ output: textOutput(),
58
+ async execute(rawArgs, exec) {
59
+ return await runProgress(ctx.fs, rawArgs, sessionCwd(exec), exec.signal);
60
+ },
61
+ }));
62
+ ctx.tools.register(definePaperTool({
63
+ name: 'thesis_decide',
64
+ description: '向决定日志(00-管理/决定日志.md)追加一条关键决定:内容、理由、备选方案。' +
65
+ '用于选题、技术选型、方案变更等所有「答辩时会被问到为什么」的决策,全程留痕。',
66
+ parameters: {
67
+ content: { type: 'string', required: true, description: '决定的实际内容' },
68
+ title: { type: 'string', description: '决定标题,缺省取 content 首行前 24 字' },
69
+ reason: { type: 'string', description: '做这个决定的理由(可选,强烈建议填写)' },
70
+ alternatives: { type: 'string', description: '考虑过的备选方案及弃用原因(可选,强烈建议填写)' },
71
+ },
72
+ output: textOutput(),
73
+ async execute(rawArgs, exec) {
74
+ return await runDecision(ctx.fs, rawArgs, sessionCwd(exec), exec.signal);
75
+ },
76
+ }));
77
+ ctx.tools.register(definePaperTool({
78
+ name: 'thesis_lit_search',
79
+ description: '从真实学术数据库检索文献(Semantic Scholar→DBLP→arXiv→Crossref 失败降级;source 可指定单一来源)。' +
80
+ '结果写入论文工作区的检索缓存与检索记录(审计留痕)。论文引用只能来自本工具的真实检索结果——这是零假文献红线。',
81
+ parameters: {
82
+ query: { type: 'string', required: true, description: '检索关键词(中英文均可,如 "attention is all you need")' },
83
+ year_from: { type: 'number', description: '年份下限,如 2020' },
84
+ limit: { type: 'number', description: '返回条数上限,默认 10,最大 20' },
85
+ source: { type: 'string', description: `指定单一来源:${LIT_SOURCES.join(' | ')};缺省 auto(降级链)` },
86
+ },
87
+ output: textOutput(),
88
+ async execute(rawArgs, exec) {
89
+ return await runLitSearch(ctx.fs, rawArgs, sessionCwd(exec), exec.signal, fetch);
90
+ },
91
+ }));
92
+ ctx.tools.register(definePaperTool({
93
+ name: 'thesis_lit_save',
94
+ description: '把 thesis_lit_search 检索到的文献(按缓存 id)收录进 02-文献/refs.bib,并写检索审计记录。' +
95
+ '只接受真实检索缓存中的 id;DOI 已在库中则自动跳过;bib key 冲突自动加后缀。',
96
+ parameters: {
97
+ ids: { type: 'array', items: { type: 'string' }, required: true, description: 'thesis_lit_search 返回的记录 id 列表' },
98
+ },
99
+ output: textOutput(),
100
+ async execute(rawArgs, exec) {
101
+ return await runLitSave(ctx.fs, rawArgs, sessionCwd(exec), exec.signal);
102
+ },
103
+ }));
104
+ ctx.tools.register(definePaperTool({
105
+ name: 'thesis_lit_note',
106
+ description: '为已检索/已收录的文献生成阅读笔记骨架(02-文献/笔记/<key>.md):元数据、原始摘要、' +
107
+ '"这篇解决什么问题/方法要点/实验结论/与我课题的关系/可用引用句"五节模板。已有笔记不覆盖(force 除外)。',
108
+ parameters: {
109
+ ref: { type: 'string', required: true, description: '缓存记录 id 或 refs.bib 条目 key' },
110
+ force: { type: 'boolean', description: '笔记已存在时强制覆盖,默认 false' },
111
+ },
112
+ output: textOutput(),
113
+ async execute(rawArgs, exec) {
114
+ return await runLitNote(ctx.fs, rawArgs, sessionCwd(exec), exec.signal);
115
+ },
116
+ }));
117
+ ctx.tools.register(definePaperTool({
118
+ name: 'thesis_review',
119
+ description: '对论文单章做确定性评审(六项可机器验证的检查):字数是否在目标区间、大纲小节是否齐全、' +
120
+ '引用编号是否都能对应 refs.bib 条目、图/表编号章号与连续性、待补/TODO 等未完成标记、' +
121
+ '章内 G2 验收清单勾选状态。语义质量(论证/逻辑)由 AI 依据 thesis-writing 技能另评;' +
122
+ '全部通过后进入 G2 人工验收(用户阅读→修改→签字,再 thesis_progress gate G2 pass)。',
123
+ parameters: {
124
+ chapter: { type: 'string', required: true, description: '章节文件名,如 01-绪论(可带 .md)' },
125
+ },
126
+ output: textOutput(),
127
+ async execute(rawArgs, exec) {
128
+ return await runReview(ctx.fs, rawArgs, sessionCwd(exec), exec.signal);
129
+ },
130
+ }));
131
+ ctx.tools.register(definePaperTool({
132
+ name: 'thesis_check',
133
+ description: '全文综合检查(五项):引用双向一致(正文引用 ↔ refs.bib、编号连续、躺尸文献)、每章字数 vs 目标区间与全文合计、' +
134
+ '图/表编号章号与连续性、术语缩写定义先于使用、学校模板文件探测。报告写入 08-合规/引用检查报告/ 与 08-合规/格式检查报告/。',
135
+ parameters: {},
136
+ output: textOutput(),
137
+ async execute(_rawArgs, exec) {
138
+ return await runCheck(ctx.fs, sessionCwd(exec), exec.signal);
139
+ },
140
+ }));
141
+ ctx.tools.register(definePaperTool({
142
+ name: 'thesis_build',
143
+ description: '把已撰写章节(06-论文/章节/*.md)构建为论文.docx(可附 pdf):优先 pandoc + 学校模板(06-论文/assets/学校模板/),' +
144
+ '无 pandoc 时用内置过渡引擎并显式标注。构建说明写入 06-论文/产出/构建说明.md。',
145
+ parameters: {
146
+ format: { type: 'string', description: 'docx(默认)| pdf | both' },
147
+ engine: { type: 'string', description: 'auto(默认,有 pandoc 用之)| pandoc | internal' },
148
+ },
149
+ output: textOutput(),
150
+ async execute(rawArgs, exec) {
151
+ const args = rawArgs;
152
+ const outcome = await runBuild(ctx.fs, args, sessionCwd(exec), exec.signal, writeBinary);
153
+ return buildSummary(outcome);
154
+ },
155
+ }));
156
+ ctx.tools.register(definePaperTool({
157
+ name: 'thesis_stylecheck',
158
+ description: '对已撰写章节做写作风格自查(六条确定性启发式):模板套话、空洞结论句、自我暴露(错误级)、翻译腔、' +
159
+ '句式单一(连续同开头段落)、无锚点长段落。命中项附行号与改写建议;报告写入 08-合规/AI味自查报告.md。'
160
+ + '这是自查不是检测结论,最终以学校检测结果为准。',
161
+ parameters: {},
162
+ output: textOutput(),
163
+ async execute(_rawArgs, exec) {
164
+ return await runAiSelfcheckTool(ctx.fs, sessionCwd(exec), exec.signal);
165
+ },
166
+ }));
167
+ ctx.tools.register(definePaperTool({
168
+ name: 'thesis_defense',
169
+ description: '答辩准备:从工作区提取答辩素材(课题/各章概览/技术选型/关键决定/测试数据/git 工作量/文献数)写入 07-答辩/答辩素材.md,' +
170
+ '并生成六类必问预答辩问题库骨架(实现细节/技术选型/需求背景/工作量/数据可信/不足展望,附证据锚点)写入 07-答辩/预答辩问题库.md。' +
171
+ '具体问题由 AI 依据素材生成,模拟问答由用户完成(技能 thesis-defense);「你的系统怎么实现的」这类问题用 defense_code_* 演练。',
172
+ parameters: {},
173
+ output: textOutput(),
174
+ async execute(_rawArgs, exec) {
175
+ return await runDefensePrep(ctx.fs, sessionCwd(exec), exec.signal);
176
+ },
177
+ }));
178
+ ctx.commands.register(statusCommand(ctx.fs));
179
+ ctx.commands.register(decideCommand(ctx.fs));
180
+ ctx.commands.register(litCommand(ctx.fs));
181
+ ctx.commands.register(checkCommand(ctx.fs));
182
+ ctx.commands.register(buildCommand(ctx.fs));
183
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * thesis_stylecheck:AI 味自查(六条启发式规则),
3
+ * 报告写入 08-合规/AI味自查报告.md(稳定文件名,每次重跑覆盖)。
4
+ */
5
+ import type { FileSystem } from '@deepseek-ai/dsh-fs';
6
+ export declare function runAiSelfcheckTool(fs: FileSystem, cwd: string | undefined, signal?: AbortSignal): Promise<string>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * thesis_stylecheck:AI 味自查(六条启发式规则),
3
+ * 报告写入 08-合规/AI味自查报告.md(稳定文件名,每次重跑覆盖)。
4
+ */
5
+ import * as nodePath from 'node:path';
6
+ import { renderAiSelfcheck, runAiSelfcheck } from "../lib/aicheck.js";
7
+ import { CHAPTER_META, isWrittenChapter } from "../lib/layout.js";
8
+ import { findThesisRoot } from "../lib/project.js";
9
+ export async function runAiSelfcheckTool(fs, cwd, signal) {
10
+ const root = await findThesisRoot(fs, cwd, signal);
11
+ if (root === null)
12
+ throw new Error('未找到论文工作区。请先在论文仓库目录内操作。');
13
+ const chapters = [];
14
+ const missing = [];
15
+ for (const [index, meta] of CHAPTER_META.entries()) {
16
+ let text;
17
+ try {
18
+ text = await fs.readText(await fs.resolve(nodePath.join(root, '06-论文/章节', `${meta.file}.md`), { signal }), signal);
19
+ }
20
+ catch {
21
+ text = undefined;
22
+ }
23
+ if (text === undefined || !isWrittenChapter(text, index + 1, meta)) {
24
+ missing.push(meta.file);
25
+ continue;
26
+ }
27
+ chapters.push({ name: meta.file, text });
28
+ }
29
+ const report = runAiSelfcheck(chapters);
30
+ const rendered = renderAiSelfcheck(report);
31
+ await fs.writeText(await fs.resolve(nodePath.join(root, '08-合规/AI味自查报告.md'), { signal }), rendered, undefined, signal);
32
+ return [
33
+ ...(missing.length > 0 ? [`⚠ 未撰写章节:${missing.join('、')}(不计入自查)。`, ''] : []),
34
+ rendered,
35
+ '',
36
+ '报告已写入:08-合规/AI味自查报告.md',
37
+ ].join('\n');
38
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * thesis_build:产出论文 docx(+ 可选 pdf)。
3
+ *
4
+ * 引擎选择:
5
+ * - pandoc 存在时优先(--reference-doc 使用学校模板);
6
+ * - 无 pandoc 时使用内置零依赖转换器(lib/docx.ts),并在构建说明中
7
+ * 显式标注"内置过渡引擎,非学校模板排版"。
8
+ *
9
+ * 二进制例外(文档化):ctx.fs 无二进制写入能力,docx/pdf 字节由本工具
10
+ * 通过 node:fs 直接写入论文仓库 06-论文/产出/ 目录。文本工作全部走 ctx.fs。
11
+ */
12
+ import type { FileSystem } from '@deepseek-ai/dsh-fs';
13
+ export interface BuildArgs {
14
+ format?: 'docx' | 'pdf' | 'both';
15
+ engine?: 'auto' | 'pandoc' | 'internal';
16
+ }
17
+ export type WriteBinary = (absPath: string, data: Uint8Array) => Promise<void>;
18
+ export interface BuildOutcome {
19
+ readonly docxPath: string;
20
+ readonly pdfPath?: string;
21
+ readonly engine: 'pandoc' | 'internal';
22
+ readonly template: string | undefined;
23
+ readonly chapters: readonly string[];
24
+ readonly missing: readonly string[];
25
+ }
26
+ export declare function runBuild(fs: FileSystem, args: BuildArgs, cwd: string | undefined, signal: AbortSignal | undefined, writeBinary: WriteBinary): Promise<BuildOutcome>;
27
+ export declare function buildSummary(outcome: BuildOutcome): string;