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,349 @@
1
+ /**
2
+ * 进度台账、时间线、决定日志的数据模型与读写逻辑。
3
+ *
4
+ * 设计要点(见 DESIGN.md §5):
5
+ * - 台账的机器真相是一个 JSON 状态块,藏在 Markdown 的 `<!-- thesis:state -->`
6
+ * HTML 注释里;表格等人类可读部分由 renderLedger 每次重绘,两者永不失步。
7
+ * - 关卡闸门(G1/G2/G3/G4)在工具层强制执行,不是"靠 AI 自觉":
8
+ * 未通过某阶段的 gateIn,该阶段任务不允许进入 doing/done。
9
+ *
10
+ * 本模块只依赖 Node 内置能力,可在任何环境独立测试。
11
+ */
12
+ // ---------------------------------------------------------------------------
13
+ // 阶段与关卡注册表(与 skills/thesis-pipeline/SKILL.md 保持一致)
14
+ // ---------------------------------------------------------------------------
15
+ export const STAGES = [
16
+ { id: 1, name: '选题', gateOut: 'G1' },
17
+ { id: 2, name: '开题', gateIn: 'G1', gateOut: 'G3' },
18
+ { id: 3, name: '文献调研' },
19
+ { id: 4, name: '系统设计' },
20
+ { id: 5, name: '系统实现' },
21
+ { id: 6, name: '系统测试' },
22
+ { id: 7, name: '论文撰写' },
23
+ { id: 8, name: '定稿与合规', gateIn: 'G2' },
24
+ { id: 9, name: '答辩', gateIn: 'G4' },
25
+ ];
26
+ export const GATE_NAMES = {
27
+ G1: '选题拍板',
28
+ G2: '逐章验收',
29
+ G3: '开题报告审阅',
30
+ G4: '提交前自查',
31
+ };
32
+ /** G2 覆盖的论文七章(与 layout.ts 的章节模板一致)。 */
33
+ export const CHAPTERS = [
34
+ '01-绪论',
35
+ '02-相关技术',
36
+ '03-需求分析',
37
+ '04-系统设计',
38
+ '05-系统实现',
39
+ '06-系统测试',
40
+ '07-总结与展望',
41
+ ];
42
+ export function defaultTasks() {
43
+ return [
44
+ { id: 'T1.1', title: '选题工作坊:明确课题方向', stage: 1, status: 'todo' },
45
+ { id: 'T1.2', title: '选题确认书(G1)', stage: 1, status: 'todo', gate: 'G1' },
46
+ { id: 'T2.1', title: '文献初检索(支撑选题与开题)', stage: 2, status: 'todo' },
47
+ { id: 'T2.2', title: '开题报告起草', stage: 2, status: 'todo' },
48
+ { id: 'T2.3', title: '开题报告审阅(G3)', stage: 2, status: 'todo', gate: 'G3' },
49
+ { id: 'T3.1', title: '建立文献库 refs.bib', stage: 3, status: 'todo' },
50
+ { id: 'T3.2', title: '逐篇文献笔记', stage: 3, status: 'todo' },
51
+ { id: 'T3.3', title: '文献综述素材整理', stage: 3, status: 'todo' },
52
+ { id: 'T4.1', title: '需求分析', stage: 4, status: 'todo' },
53
+ { id: 'T4.2', title: '系统设计(架构/模块/接口)', stage: 4, status: 'todo' },
54
+ { id: 'T4.3', title: '技术选型论证', stage: 4, status: 'todo' },
55
+ { id: 'T5.1', title: '开发环境搭建', stage: 5, status: 'todo' },
56
+ { id: 'T5.2', title: '核心功能实现(可运行优先)', stage: 5, status: 'todo' },
57
+ { id: 'T5.3', title: '中期检查材料', stage: 5, status: 'todo' },
58
+ { id: 'T6.1', title: '测试计划', stage: 6, status: 'todo' },
59
+ { id: 'T6.2', title: '功能测试执行(真实结果留证)', stage: 6, status: 'todo' },
60
+ { id: 'T6.3', title: '测试结果记录', stage: 6, status: 'todo' },
61
+ { id: 'T7.1', title: '论文大纲', stage: 7, status: 'todo' },
62
+ { id: 'T7.2', title: '逐章撰写与验收(G2×7)', stage: 7, status: 'todo', gate: 'G2' },
63
+ { id: 'T7.3', title: '全文统稿', stage: 7, status: 'todo' },
64
+ { id: 'T8.1', title: '格式检查', stage: 8, status: 'todo' },
65
+ { id: 'T8.2', title: '引用检查', stage: 8, status: 'todo' },
66
+ { id: 'T8.3', title: '自查报告(G4)', stage: 8, status: 'todo', gate: 'G4' },
67
+ { id: 'T9.1', title: '答辩 PPT', stage: 9, status: 'todo' },
68
+ { id: 'T9.2', title: '预答辩演练', stage: 9, status: 'todo' },
69
+ ];
70
+ }
71
+ export function defaultState(now = new Date()) {
72
+ const chapters = {};
73
+ for (const c of CHAPTERS)
74
+ chapters[c] = 'pending';
75
+ return {
76
+ currentStage: 1,
77
+ updatedAt: now.toISOString(),
78
+ gates: {
79
+ G1: { status: 'pending' },
80
+ G2: { status: 'pending', chapters },
81
+ G3: { status: 'pending' },
82
+ G4: { status: 'pending' },
83
+ },
84
+ tasks: defaultTasks(),
85
+ };
86
+ }
87
+ export function stageMeta(id) {
88
+ return STAGES.find(s => s.id === id);
89
+ }
90
+ export function gatePassed(state, gate) {
91
+ const g = state.gates[gate];
92
+ if (!g)
93
+ return false;
94
+ if (gate !== 'G2')
95
+ return g.status === 'passed';
96
+ // G2 需七章全部通过。
97
+ return g.status === 'passed' || (g.chapters !== undefined && CHAPTERS.every(c => g.chapters[c] === 'passed'));
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // 状态块提取与序列化
101
+ // ---------------------------------------------------------------------------
102
+ const STATE_RE = /<!-- thesis:state\n([\s\S]*?)\n-->\s*/;
103
+ /** 从台账 Markdown 提取 JSON 状态块;缺失时返回 found:false 与默认状态。 */
104
+ export function parseLedger(md) {
105
+ const m = STATE_RE.exec(md);
106
+ if (!m || m[1] === undefined)
107
+ return { state: defaultState(), found: false };
108
+ try {
109
+ return { state: JSON.parse(m[1]), found: true };
110
+ }
111
+ catch {
112
+ return { state: defaultState(), found: false };
113
+ }
114
+ }
115
+ export function stateToJson(state) {
116
+ return JSON.stringify(state, null, 2);
117
+ }
118
+ /**
119
+ * 把任务推进到 toStatus 是否被允许。
120
+ * 规则:任务所在阶段有 gateIn 时,该关卡必须已通过;关联 gate 的任务
121
+ * 必须先完成(done)才能把该关卡标记通过(由 applyGate 检查)。
122
+ */
123
+ export function canUpdateTask(state, task, toStatus) {
124
+ if (toStatus === 'todo')
125
+ return { ok: true, reason: '' };
126
+ const meta = stageMeta(task.stage);
127
+ if (meta?.gateIn !== undefined && !gatePassed(state, meta.gateIn)) {
128
+ return {
129
+ ok: false,
130
+ reason: `阶段 ${task.stage}(${meta.name})的入口关卡 ${meta.gateIn}(${GATE_NAMES[meta.gateIn] ?? meta.gateIn})尚未通过,禁止推进任务 ${task.id}。`,
131
+ };
132
+ }
133
+ return { ok: true, reason: '' };
134
+ }
135
+ /**
136
+ * 重算 currentStage:第一个"仍有待办任务且入口关卡已通过(或没有入口关卡)"的阶段。
137
+ * 阶段出口关卡未通过时不会越过该阶段进入下一阶段。
138
+ */
139
+ export function recomputeStage(state) {
140
+ let candidate = state.currentStage;
141
+ for (const meta of STAGES) {
142
+ if (meta.gateIn !== undefined && !gatePassed(state, meta.gateIn))
143
+ continue;
144
+ const hasWork = state.tasks.some(t => t.stage === meta.id && t.status !== 'done');
145
+ if (hasWork) {
146
+ candidate = meta.id;
147
+ break;
148
+ }
149
+ }
150
+ state.currentStage = candidate;
151
+ }
152
+ export function applyTaskUpdate(state, taskId, toStatus, note, now = new Date()) {
153
+ const task = state.tasks.find(t => t.id === taskId);
154
+ if (!task)
155
+ return { ok: false, reason: `任务 ${taskId} 不存在。可用任务见进度台账。` };
156
+ const check = canUpdateTask(state, task, toStatus);
157
+ if (!check.ok)
158
+ return check;
159
+ task.status = toStatus;
160
+ if (note !== undefined)
161
+ task.note = note;
162
+ recomputeStage(state);
163
+ state.updatedAt = now.toISOString();
164
+ return { ok: true, reason: '', task };
165
+ }
166
+ export function applyGate(state, gate, pass, opts = {}) {
167
+ const g = state.gates[gate];
168
+ if (!g)
169
+ return { ok: false, reason: `关卡 ${gate} 不存在。有效关卡:${Object.keys(GATE_NAMES).join('、')}。` };
170
+ const now = opts.now ?? new Date();
171
+ if (gate === 'G2') {
172
+ const chapter = opts.chapter;
173
+ if (chapter === undefined || !CHAPTERS.includes(chapter)) {
174
+ return { ok: false, reason: `关卡 G2 需要指定 chapter,有效值:${CHAPTERS.join('、')}。` };
175
+ }
176
+ if (pass) {
177
+ g.chapters = { ...(g.chapters ?? {}), [chapter]: 'passed' };
178
+ if (CHAPTERS.every(c => (g.chapters ?? {})[c] === 'passed'))
179
+ g.status = 'passed';
180
+ }
181
+ else {
182
+ g.chapters = { ...(g.chapters ?? {}), [chapter]: 'pending' };
183
+ g.status = 'pending';
184
+ }
185
+ g.date = now.toISOString();
186
+ if (opts.note !== undefined)
187
+ g.note = opts.note;
188
+ }
189
+ else {
190
+ // 非 G2 关卡:通过前必须确认其关联任务已完成。
191
+ if (pass) {
192
+ const owners = state.tasks.filter(t => t.gate === gate);
193
+ const notDone = owners.filter(t => t.status !== 'done');
194
+ if (notDone.length > 0) {
195
+ return {
196
+ ok: false,
197
+ reason: `关卡 ${gate} 关联任务 ${notDone.map(t => t.id).join('、')} 尚未完成(done),请先完成对应产出再通过关卡。`,
198
+ };
199
+ }
200
+ }
201
+ g.status = pass ? 'passed' : 'pending';
202
+ g.date = pass ? now.toISOString() : undefined;
203
+ if (opts.note !== undefined)
204
+ g.note = opts.note;
205
+ }
206
+ state.updatedAt = now.toISOString();
207
+ recomputeStage(state);
208
+ return { ok: true, reason: '' };
209
+ }
210
+ // ---------------------------------------------------------------------------
211
+ // 渲染:台账 Markdown 与状态报告
212
+ // ---------------------------------------------------------------------------
213
+ const STATUS_MARK = { todo: '待办', doing: '进行中', done: '完成' };
214
+ export function renderLedger(state) {
215
+ const lines = [];
216
+ lines.push('# 进度台账');
217
+ lines.push('');
218
+ lines.push('> 本文件由 `thesis_progress` 工具维护:`<!-- thesis:state -->` 内是机器真相(JSON),');
219
+ lines.push('> 下方表格由工具重绘。请勿手工修改 JSON 块;如确需手工编辑,请保持 JSON 合法。');
220
+ lines.push('');
221
+ lines.push('<!-- thesis:state');
222
+ lines.push(stateToJson(state));
223
+ lines.push('-->');
224
+ lines.push('');
225
+ const meta = stageMeta(state.currentStage);
226
+ lines.push('## 当前状态');
227
+ lines.push('');
228
+ lines.push(`- 阶段:${meta ? `${meta.id} · ${meta.name}` : `未知(${state.currentStage})`}`);
229
+ if (state.title)
230
+ lines.push(`- 课题:${state.title}`);
231
+ lines.push(`- 最近更新:${state.updatedAt}`);
232
+ lines.push('');
233
+ lines.push('## 人工关卡');
234
+ lines.push('');
235
+ lines.push('| 关卡 | 名称 | 状态 | 说明 |');
236
+ lines.push('|---|---|---|---|');
237
+ for (const [id, name] of Object.entries(GATE_NAMES)) {
238
+ const g = state.gates[id];
239
+ const passed = gatePassed(state, id);
240
+ const detail = id === 'G2' && g?.chapters !== undefined
241
+ ? `已过 ${CHAPTERS.filter(c => g.chapters[c] === 'passed').length}/${CHAPTERS.length} 章`
242
+ : (g?.note ?? '');
243
+ lines.push(`| ${id} | ${name} | ${passed ? '✅ 已通过' : '⬜ 未通过'} | ${detail} |`);
244
+ }
245
+ lines.push('');
246
+ for (const s of STAGES) {
247
+ const tasks = state.tasks.filter(t => t.stage === s.id);
248
+ if (tasks.length === 0)
249
+ continue;
250
+ lines.push(`## 阶段 ${s.id} · ${s.name}`);
251
+ lines.push('');
252
+ lines.push('| ID | 任务 | 状态 | 备注 |');
253
+ lines.push('|---|---|---|---|');
254
+ for (const t of tasks) {
255
+ lines.push(`| ${t.id} | ${t.title}${t.gate ? `(${t.gate})` : ''} | ${STATUS_MARK[t.status]} | ${t.note ?? ''} |`);
256
+ }
257
+ lines.push('');
258
+ }
259
+ return lines.join('\n');
260
+ }
261
+ export function renderReport(state) {
262
+ const meta = stageMeta(state.currentStage);
263
+ const parts = [];
264
+ parts.push(`当前阶段:${meta ? `${meta.id} · ${meta.name}` : `未知(${state.currentStage})`}${state.title ? `;课题:${state.title}` : ''}`);
265
+ const gates = Object.entries(GATE_NAMES).map(([id, name]) => {
266
+ const passed = gatePassed(state, id);
267
+ const g = state.gates[id];
268
+ const detail = id === 'G2' && g?.chapters !== undefined
269
+ ? `(${CHAPTERS.filter(c => g.chapters[c] === 'passed').length}/${CHAPTERS.length} 章)`
270
+ : '';
271
+ return `${id} ${name}: ${passed ? '已通过' : '未通过'}${detail}`;
272
+ });
273
+ parts.push('关卡:' + gates.join(';'));
274
+ const stageTasks = state.tasks.filter(t => t.stage === state.currentStage);
275
+ parts.push(`本阶段任务:${stageTasks.map(t => `${t.id} ${t.title} [${STATUS_MARK[t.status]}]`).join(';') || '(无)'}`);
276
+ const next = state.tasks.find(t => t.status === 'todo');
277
+ parts.push(next ? `下一个待办:${next.id} ${next.title}` : '全部任务已完成,进入答辩前复核。');
278
+ return parts.join('\n');
279
+ }
280
+ // ---------------------------------------------------------------------------
281
+ // 时间线
282
+ // ---------------------------------------------------------------------------
283
+ const TIMELINE_RE = /<!-- thesis:timeline\n([\s\S]*?)\n-->\s*/;
284
+ export function defaultTimeline() {
285
+ return {
286
+ milestones: [
287
+ { id: 'M1', name: '选题确定', status: 'pending', dependsOn: [] },
288
+ { id: 'M2', name: '开题报告提交', status: 'pending', dependsOn: ['M1'] },
289
+ { id: 'M3', name: '系统设计完成', status: 'pending', dependsOn: ['M2'] },
290
+ { id: 'M4', name: '核心功能实现', status: 'pending', dependsOn: ['M3'] },
291
+ { id: 'M5', name: '论文初稿', status: 'pending', dependsOn: ['M4'] },
292
+ { id: 'M6', name: '定稿与自查', status: 'pending', dependsOn: ['M5'] },
293
+ { id: 'M7', name: '答辩', status: 'pending', dependsOn: ['M6'] },
294
+ ],
295
+ };
296
+ }
297
+ export function parseTimeline(md) {
298
+ const m = TIMELINE_RE.exec(md);
299
+ if (!m || m[1] === undefined)
300
+ return { state: defaultTimeline(), found: false };
301
+ try {
302
+ return { state: JSON.parse(m[1]), found: true };
303
+ }
304
+ catch {
305
+ return { state: defaultTimeline(), found: false };
306
+ }
307
+ }
308
+ export function renderTimeline(t) {
309
+ const lines = [];
310
+ lines.push('# 时间线');
311
+ lines.push('');
312
+ lines.push('> 里程碑与截止日期。`due` 使用 ISO 日期(YYYY-MM-DD)。');
313
+ lines.push('> 由 thesis-pipeline 技能指导维护;`<!-- thesis:timeline -->` 内是机器真相。');
314
+ lines.push('');
315
+ lines.push('<!-- thesis:timeline');
316
+ lines.push(JSON.stringify(t, null, 2));
317
+ lines.push('-->');
318
+ lines.push('');
319
+ lines.push('| 里程碑 | 截止 | 状态 | 依赖 |');
320
+ lines.push('|---|---|---|---|');
321
+ for (const m of t.milestones) {
322
+ lines.push(`| ${m.id} ${m.name} | ${m.due ?? '—'} | ${m.status === 'done' ? '✅' : '⬜'} | ${m.dependsOn.join('、') || '—'} |`);
323
+ }
324
+ lines.push('');
325
+ return lines.join('\n');
326
+ }
327
+ // ---------------------------------------------------------------------------
328
+ // 决定日志
329
+ // ---------------------------------------------------------------------------
330
+ export function renderDecisionEntry(entry) {
331
+ const lines = [];
332
+ lines.push(`## ${entry.date} · ${entry.title}`);
333
+ lines.push('');
334
+ lines.push(`- 决定:${entry.content}`);
335
+ if (entry.reason)
336
+ lines.push(`- 理由:${entry.reason}`);
337
+ if (entry.alternatives)
338
+ lines.push(`- 备选:${entry.alternatives}`);
339
+ lines.push('');
340
+ return lines.join('\n');
341
+ }
342
+ export function appendDecision(md, entry) {
343
+ const base = md.trimEnd();
344
+ return base.length > 0 ? `${base}\n\n${renderDecisionEntry(entry)}` : `# 决定日志\n\n> 每个关键决定的留痕:内容、理由、备选。答辩时它是"我为什么这么写"的证据。\n\n${renderDecisionEntry(entry)}`;
345
+ }
346
+ export function countDecisions(md) {
347
+ const matches = md.match(/^## \d{4}-\d{2}-\d{2} · /gm);
348
+ return matches === null ? 0 : matches.length;
349
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * 真实学术文献检索:四个免费来源 + 失败降级链。
3
+ *
4
+ * 来源(均无需 API key):
5
+ * 1. Semantic Scholar Graph API https://api.semanticscholar.org/graph/v1/paper/search
6
+ * 2. DBLP https://dblp.org/search/publ/api
7
+ * 3. arXiv https://export.arxiv.org/api/query(Atom XML)
8
+ * 4. Crossref https://api.crossref.org/works
9
+ *
10
+ * 红线(DESIGN 最高标准之一):论文引用只能来自这里的真实检索结果。
11
+ * 本模块不产出任何"凭记忆生成"的文献记录。
12
+ */
13
+ export type LitType = 'article' | 'inproceedings' | 'misc' | 'book' | 'phdthesis';
14
+ export interface LitRecord {
15
+ /** 缓存 ID:sha1(doi ?? url ?? title),save/note 用它引用(防止 AI 转述元数据出错)。 */
16
+ readonly id: string;
17
+ readonly title: string;
18
+ readonly authors: readonly string[];
19
+ readonly year: number | null;
20
+ readonly venue: string;
21
+ readonly type: LitType;
22
+ readonly doi?: string;
23
+ readonly url?: string;
24
+ readonly abstract?: string;
25
+ readonly citationCount?: number;
26
+ readonly source: string;
27
+ }
28
+ export type LitSource = 'auto' | 'semantic-scholar' | 'dblp' | 'arxiv' | 'crossref';
29
+ export interface LitSearchOptions {
30
+ readonly query: string;
31
+ readonly yearFrom?: number;
32
+ readonly limit?: number;
33
+ readonly source?: LitSource;
34
+ readonly signal?: AbortSignal;
35
+ }
36
+ export type FetchLike = (url: string, init?: {
37
+ signal?: AbortSignal;
38
+ }) => Promise<Response>;
39
+ declare function parseArxivAtom(xml: string): Array<{
40
+ title: string;
41
+ summary: string;
42
+ authors: string[];
43
+ arxivId: string;
44
+ doi?: string;
45
+ published: string;
46
+ }>;
47
+ export declare function makeId(doi: string | undefined, url: string | undefined, title: string): string;
48
+ export declare const LIT_SOURCES: readonly string[];
49
+ /**
50
+ * 检索文献。source=auto 时按 Semantic Scholar → DBLP → arXiv → Crossref 顺序
51
+ * 降级:某源失败(网络/限流/超时)则尝试下一源;首个返回 ≥1 条结果的源胜出。
52
+ * 返回按年被引/年份排序后的去重列表(重复判定:id 相同)。
53
+ *
54
+ * **取消不是失败**:调用方取消时立刻把 `AbortError` 抛出去,不去试下一个源——
55
+ * 否则用户按了停止,工具会继续打四个学术 API(最多 60 秒),最后报一句
56
+ * 「所有检索源均未返回结果」,把取消误导成网络问题。
57
+ */
58
+ export declare function searchLiterature(fetchImpl: FetchLike, opts: LitSearchOptions): Promise<LitRecord[]>;
59
+ export { parseArxivAtom };