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,356 @@
1
+ /**
2
+ * 代码结构摘要:把源码变成「论文与答辩用得上的结构信息」,而不是原文倒出。
3
+ *
4
+ * 为什么不做成"把源码当文本读":一个课程设计仓库动辄上万行,倒进上下文既挤爆预算、
5
+ * 又对写作没有帮助。真正有用的是**结构**——有哪些文件、各多少行、定义了哪些类与函数、
6
+ * 入口在哪、用了什么技术栈、依赖清单说了什么。这些正是:
7
+ * - 论文「系统实现」章的骨架;
8
+ * - `thesis_defense` 的「工作量」证据(文件数/代码行数);
9
+ * - 答辩必问「你的系统怎么实现的」的可检索索引;
10
+ * - `code-walkthrough` 做代码演练时的地图。
11
+ *
12
+ * 纯函数、无宿主依赖、零第三方库:只用正则做**结构化信号提取**(不求 AST 精度),
13
+ * 因此结果里只报"检出",不谎称"完整解析"。
14
+ *
15
+ * @module dsh-thesis/ingest/code
16
+ */
17
+ /** 扩展名 → 语言。 */
18
+ const LANGUAGE_BY_EXTENSION = {
19
+ '.ts': 'typescript', '.tsx': 'typescript', '.mts': 'typescript', '.cts': 'typescript',
20
+ '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
21
+ '.py': 'python', '.pyw': 'python',
22
+ '.java': 'java',
23
+ '.go': 'go',
24
+ '.rs': 'rust',
25
+ '.c': 'c', '.h': 'c',
26
+ '.cc': 'cpp', '.cpp': 'cpp', '.cxx': 'cpp', '.hpp': 'cpp', '.hh': 'cpp',
27
+ '.cs': 'csharp',
28
+ '.php': 'php',
29
+ '.rb': 'ruby',
30
+ '.kt': 'kotlin', '.kts': 'kotlin',
31
+ '.swift': 'swift',
32
+ '.sql': 'sql',
33
+ '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.ps1': 'shell', '.bat': 'shell',
34
+ '.vue': 'vue',
35
+ '.html': 'html', '.htm': 'html',
36
+ '.css': 'css', '.scss': 'css', '.less': 'css',
37
+ };
38
+ /** 依赖/构建清单文件(用于技术栈指纹)。 */
39
+ const MANIFEST_NAMES = [
40
+ 'package.json', 'pnpm-lock.yaml', 'yarn.lock', 'package-lock.json',
41
+ 'requirements.txt', 'pyproject.toml', 'pipfile', 'environment.yml',
42
+ 'go.mod', 'cargo.toml', 'pom.xml', 'build.gradle', 'build.gradle.kts',
43
+ 'composer.json', 'gemfile', 'dockerfile', 'docker-compose.yml', 'makefile',
44
+ ];
45
+ /** 语言名(用于摘要文本)。 */
46
+ export function languageLabel(language) {
47
+ return language;
48
+ }
49
+ /** 是否是受支持的源码文件;不是则返回 `undefined`。 */
50
+ export function codeLanguageOf(path) {
51
+ const lower = path.toLowerCase().replace(/\\/g, '/');
52
+ const base = lower.slice(lower.lastIndexOf('/') + 1);
53
+ const dot = base.lastIndexOf('.');
54
+ if (dot <= 0)
55
+ return undefined;
56
+ return LANGUAGE_BY_EXTENSION[base.slice(dot)];
57
+ }
58
+ /** 是否是依赖/构建清单。 */
59
+ export function isManifestFile(path) {
60
+ const lower = path.toLowerCase().replace(/\\/g, '/');
61
+ const base = lower.slice(lower.lastIndexOf('/') + 1);
62
+ return MANIFEST_NAMES.includes(base);
63
+ }
64
+ /** 每个文件最多索引多少条声明(防止超大生成文件把摘要撑爆)。 */
65
+ export const MAX_DECLARATIONS_PER_FILE = 40;
66
+ /** 单文件最多扫描多少行。 */
67
+ export const MAX_SCAN_LINES = 20_000;
68
+ /** 各语言的声明模式(顺序即优先级,命中即记录,同名字取首个)。 */
69
+ const PATTERNS = {
70
+ typescript: [
71
+ { kind: 'function', regex: /^\s*export\s+(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/ },
72
+ { kind: 'function', regex: /^\s*(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/ },
73
+ { kind: 'class', regex: /^\s*export\s+(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/ },
74
+ { kind: 'class', regex: /^\s*(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/ },
75
+ { kind: 'interface', regex: /^\s*export\s+interface\s+([A-Za-z_$][\w$]*)/ },
76
+ { kind: 'type', regex: /^\s*export\s+type\s+([A-Za-z_$][\w$]*)/ },
77
+ { kind: 'enum', regex: /^\s*export\s+enum\s+([A-Za-z_$][\w$]*)/ },
78
+ { kind: 'const', regex: /^\s*export\s+const\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:async\s*)?(?:\(|<)/ },
79
+ { kind: 'route', regex: /\.\s*(?:get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/ },
80
+ ],
81
+ javascript: [
82
+ { kind: 'function', regex: /^\s*export\s+(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/ },
83
+ { kind: 'function', regex: /^\s*(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/ },
84
+ { kind: 'class', regex: /^\s*(?:export\s+)?class\s+([A-Za-z_$][\w$]*)/ },
85
+ { kind: 'route', regex: /\.\s*(?:get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/ },
86
+ ],
87
+ python: [
88
+ { kind: 'class', regex: /^\s*class\s+([A-Za-z_]\w*)/ },
89
+ { kind: 'function', regex: /^\s*(?:async\s+)?def\s+([A-Za-z_]\w*)/ },
90
+ { kind: 'route', regex: /@\w+\.(?:route|get|post|put|delete)\s*\(\s*['"]([^'"]+)['"]/ },
91
+ ],
92
+ java: [
93
+ { kind: 'class', regex: /^\s*(?:public|protected|private)?\s*(?:final\s+|abstract\s+)?(?:class|interface|enum)\s+([A-Za-z_]\w*)/ },
94
+ { kind: 'method', regex: /^\s*(?:public|protected|private)\s+(?:static\s+|final\s+|synchronized\s+)*[\w<>\[\],.\s]+\s+([A-Za-z_]\w*)\s*\(/ },
95
+ ],
96
+ go: [
97
+ { kind: 'function', regex: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/ },
98
+ { kind: 'struct', regex: /^type\s+([A-Za-z_]\w*)\s+struct/ },
99
+ { kind: 'interface', regex: /^type\s+([A-Za-z_]\w*)\s+interface/ },
100
+ ],
101
+ rust: [
102
+ { kind: 'function', regex: /^\s*(?:pub\s+)?(?:async\s+)?fn\s+([A-Za-z_]\w*)/ },
103
+ { kind: 'struct', regex: /^\s*(?:pub\s+)?struct\s+([A-Za-z_]\w*)/ },
104
+ { kind: 'enum', regex: /^\s*(?:pub\s+)?enum\s+([A-Za-z_]\w*)/ },
105
+ { kind: 'trait', regex: /^\s*(?:pub\s+)?trait\s+([A-Za-z_]\w*)/ },
106
+ ],
107
+ c: [
108
+ { kind: 'struct', regex: /^\s*(?:typedef\s+)?struct\s+([A-Za-z_]\w*)/ },
109
+ { kind: 'function', regex: /^[A-Za-z_][\w\s*]*\s+([A-Za-z_]\w*)\s*\([^;]*\)\s*\{/ },
110
+ ],
111
+ cpp: [
112
+ { kind: 'class', regex: /^\s*class\s+([A-Za-z_]\w*)/ },
113
+ { kind: 'struct', regex: /^\s*(?:typedef\s+)?struct\s+([A-Za-z_]\w*)/ },
114
+ { kind: 'function', regex: /^[A-Za-z_][\w:<>*&\s]*\s+([A-Za-z_]\w*)\s*\([^;]*\)\s*\{/ },
115
+ ],
116
+ csharp: [
117
+ { kind: 'class', regex: /^\s*(?:public|internal|private|protected)?\s*(?:sealed\s+|abstract\s+|partial\s+)*(?:class|interface|enum|record)\s+([A-Za-z_]\w*)/ },
118
+ { kind: 'method', regex: /^\s*(?:public|private|protected|internal)\s+(?:static\s+|async\s+|virtual\s+|override\s+)*[\w<>\[\],.\s]+\s+([A-Za-z_]\w*)\s*\(/ },
119
+ ],
120
+ php: [
121
+ { kind: 'class', regex: /^\s*(?:final\s+|abstract\s+)?class\s+([A-Za-z_]\w*)/ },
122
+ { kind: 'function', regex: /^\s*(?:public|private|protected|static|\s)*function\s+([A-Za-z_]\w*)/ },
123
+ ],
124
+ ruby: [
125
+ { kind: 'class', regex: /^\s*class\s+([A-Za-z_]\w*)/ },
126
+ { kind: 'function', regex: /^\s*def\s+([A-Za-z_]\w*[?!=]?)/ },
127
+ ],
128
+ kotlin: [
129
+ { kind: 'class', regex: /^\s*(?:data\s+|sealed\s+|open\s+|abstract\s+)?class\s+([A-Za-z_]\w*)/ },
130
+ { kind: 'function', regex: /^\s*(?:private\s+|internal\s+|public\s+)?fun\s+([A-Za-z_]\w*)/ },
131
+ ],
132
+ swift: [
133
+ { kind: 'class', regex: /^\s*(?:final\s+|open\s+)?(?:class|struct|enum|protocol)\s+([A-Za-z_]\w*)/ },
134
+ { kind: 'function', regex: /^\s*(?:public\s+|private\s+|internal\s+)?func\s+([A-Za-z_]\w*)/ },
135
+ ],
136
+ sql: [
137
+ { kind: 'table', regex: /^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"[]?([A-Za-z_]\w*)/i },
138
+ { kind: 'view', regex: /^\s*CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+[`"[]?([A-Za-z_]\w*)/i },
139
+ ],
140
+ shell: [
141
+ { kind: 'function', regex: /^\s*(?:function\s+)?([A-Za-z_]\w*)\s*\(\s*\)\s*\{/ },
142
+ ],
143
+ vue: [
144
+ { kind: 'component', regex: /^\s*defineComponent\s*\(\s*\{?\s*name:\s*['"]([^'"]+)['"]/ },
145
+ { kind: 'function', regex: /^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/ },
146
+ { kind: 'const', regex: /^\s*const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:ref|reactive|computed|defineProps|defineEmits)\b/ },
147
+ ],
148
+ html: [
149
+ { kind: 'script', regex: /<script[^>]*src=["']([^"']+)["']/i },
150
+ { kind: 'link', regex: /<link[^>]*href=["']([^"']+)["']/i },
151
+ ],
152
+ css: [
153
+ { kind: 'selector', regex: /^([.#][A-Za-z_][\w-]*)\s*\{/ },
154
+ ],
155
+ };
156
+ /** 是否是注释行(语言无关的粗判;不求精确)。 */
157
+ function isCommentLine(line, language) {
158
+ const trimmed = line.trim();
159
+ if (trimmed === '')
160
+ return false;
161
+ if (language === 'python' || language === 'shell' || language === 'ruby')
162
+ return trimmed.startsWith('#');
163
+ if (language === 'sql')
164
+ return trimmed.startsWith('--');
165
+ if (language === 'html')
166
+ return trimmed.startsWith('<!--');
167
+ if (language === 'css')
168
+ return trimmed.startsWith('/*') || trimmed.startsWith('*');
169
+ return trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed.startsWith('*/');
170
+ }
171
+ /**
172
+ * 摘要一个源码文件。
173
+ *
174
+ * @param rel - 展示用相对路径
175
+ * @param text - 文件内容(已解码)
176
+ */
177
+ export function summarizeSource(rel, text) {
178
+ const language = codeLanguageOf(rel) ?? 'typescript';
179
+ const allLines = text.split('\n');
180
+ const truncated = allLines.length > MAX_SCAN_LINES;
181
+ const lines = truncated ? allLines.slice(0, MAX_SCAN_LINES) : allLines;
182
+ let blankLines = 0;
183
+ let commentLines = 0;
184
+ const declarations = [];
185
+ const seen = new Set();
186
+ const patterns = PATTERNS[language] ?? [];
187
+ for (let index = 0; index < lines.length; index += 1) {
188
+ const line = lines[index];
189
+ if (line.trim() === '') {
190
+ blankLines += 1;
191
+ continue;
192
+ }
193
+ if (isCommentLine(line, language)) {
194
+ commentLines += 1;
195
+ continue;
196
+ }
197
+ if (declarations.length >= MAX_DECLARATIONS_PER_FILE)
198
+ continue;
199
+ for (const pattern of patterns) {
200
+ const match = pattern.regex.exec(line);
201
+ const name = match?.[1];
202
+ if (name === undefined || name === '')
203
+ continue;
204
+ const key = `${pattern.kind}:${name}`;
205
+ if (seen.has(key))
206
+ break;
207
+ seen.add(key);
208
+ declarations.push({ kind: pattern.kind, name, line: index + 1 });
209
+ break;
210
+ }
211
+ }
212
+ return {
213
+ rel,
214
+ language,
215
+ lines: allLines.length,
216
+ codeLines: lines.length - blankLines - commentLines,
217
+ commentLines,
218
+ blankLines,
219
+ declarations,
220
+ truncated,
221
+ };
222
+ }
223
+ const STACK_SIGNALS = [
224
+ { name: 'Node.js / npm', regex: /"(?:dependencies|devDependencies)"\s*:/ },
225
+ { name: 'React', regex: /"react"\s*:/ },
226
+ { name: 'Vue', regex: /"vue"\s*:/ },
227
+ { name: 'Angular', regex: /"@angular\/core"\s*:/ },
228
+ { name: 'Express', regex: /"express"\s*:/ },
229
+ { name: 'Koa', regex: /"koa"\s*:/ },
230
+ { name: 'NestJS', regex: /"@nestjs\/core"\s*:/ },
231
+ { name: 'TypeScript', regex: /"typescript"\s*:/ },
232
+ { name: 'Vite', regex: /"vite"\s*:/ },
233
+ { name: 'Webpack', regex: /"webpack"\s*:/ },
234
+ { name: 'Next.js', regex: /"next"\s*:/ },
235
+ { name: 'FastAPI', regex: /fastapi/i },
236
+ { name: 'Flask', regex: /flask/i },
237
+ { name: 'Django', regex: /django/i },
238
+ { name: 'PyTorch', regex: /torch/i },
239
+ { name: 'TensorFlow', regex: /tensorflow/i },
240
+ { name: 'NumPy', regex: /numpy/i },
241
+ { name: 'Pandas', regex: /pandas/i },
242
+ { name: 'OpenCV', regex: /opencv/i },
243
+ { name: 'Spring Boot', regex: /spring-boot/i },
244
+ { name: 'MyBatis', regex: /mybatis/i },
245
+ { name: 'Maven', regex: /<groupId>/ },
246
+ { name: 'Gradle', regex: /plugins\s*\{|dependencies\s*\{/ },
247
+ { name: 'Go Modules', regex: /^module\s+\S+/m },
248
+ { name: 'Gin', regex: /gin-gonic\/gin/ },
249
+ { name: 'Cargo / Rust', regex: /^\[package\]/m },
250
+ { name: 'MySQL', regex: /mysql/i },
251
+ { name: 'PostgreSQL', regex: /postgres/i },
252
+ { name: 'SQLite', regex: /sqlite/i },
253
+ { name: 'Redis', regex: /redis/i },
254
+ { name: 'MongoDB', regex: /mongo/i },
255
+ { name: 'Docker', regex: /FROM\s+\S+|services:/i },
256
+ { name: 'Composer / PHP', regex: /"require"\s*:/ },
257
+ ];
258
+ /** 从清单文件内容推断技术栈。 */
259
+ export function detectTechStack(manifests) {
260
+ const entries = [];
261
+ const seen = new Set();
262
+ for (const [rel, content] of Object.entries(manifests)) {
263
+ for (const signal of STACK_SIGNALS) {
264
+ if (seen.has(signal.name))
265
+ continue;
266
+ if (signal.regex.test(content)) {
267
+ seen.add(signal.name);
268
+ entries.push({ name: signal.name, evidence: rel });
269
+ }
270
+ }
271
+ }
272
+ return entries;
273
+ }
274
+ /** 渲染成 Markdown(写进 `00-管理/材料/` 的摘要文件)。 */
275
+ export function renderProjectSummary(summary) {
276
+ const byLanguage = new Map();
277
+ for (const file of summary.files) {
278
+ const bucket = byLanguage.get(file.language) ?? { files: 0, lines: 0 };
279
+ bucket.files += 1;
280
+ bucket.lines += file.lines;
281
+ byLanguage.set(file.language, bucket);
282
+ }
283
+ const totalLines = summary.files.reduce((sum, file) => sum + file.lines, 0);
284
+ const totalCode = summary.files.reduce((sum, file) => sum + file.codeLines, 0);
285
+ const lines = [];
286
+ lines.push(`# 代码结构摘要:${summary.root}`);
287
+ lines.push('');
288
+ lines.push('> 由 `thesis_ingest` 生成。这里只记录**结构信号**(文件、行数、声明、入口、技术栈),');
289
+ lines.push('> 不含源码正文——论文「系统实现」章与答辩素材应从这些结构出发,而不是粘贴代码。');
290
+ lines.push('');
291
+ lines.push('## 规模');
292
+ lines.push('');
293
+ lines.push(`- 源码文件:${summary.files.length} 个`);
294
+ lines.push(`- 总行数:${totalLines} 行(其中代码行 ${totalCode} 行)`);
295
+ lines.push('');
296
+ if (byLanguage.size > 0) {
297
+ lines.push('| 语言 | 文件数 | 行数 |');
298
+ lines.push('|---|---|---|');
299
+ for (const [language, bucket] of [...byLanguage.entries()].sort((a, b) => b[1].lines - a[1].lines)) {
300
+ lines.push(`| ${language} | ${bucket.files} | ${bucket.lines} |`);
301
+ }
302
+ lines.push('');
303
+ }
304
+ const stack = detectTechStack(summary.manifests);
305
+ lines.push('## 技术栈(由依赖清单推断,须与你的实际选型核对)');
306
+ lines.push('');
307
+ if (stack.length === 0) {
308
+ lines.push('- 未在依赖清单里检出可识别技术栈;若确有依赖文件,请确认它在本目录内。');
309
+ }
310
+ else {
311
+ for (const entry of stack)
312
+ lines.push(`- ${entry.name} (证据:${entry.evidence})`);
313
+ }
314
+ lines.push('');
315
+ lines.push('## 文件与主要声明');
316
+ lines.push('');
317
+ if (summary.files.length === 0) {
318
+ lines.push('- (未摄取到源码文件:确认 path 指向代码目录,且没有被 SKIP_DIRS 跳过)');
319
+ lines.push('');
320
+ }
321
+ for (const file of [...summary.files].sort((a, b) => b.lines - a.lines)) {
322
+ const parts = [`${file.lines} 行`];
323
+ if (file.commentLines > 0)
324
+ parts.push(`注释 ${file.commentLines}`);
325
+ if (file.truncated)
326
+ parts.push('已截断摘要');
327
+ lines.push(`### ${file.rel} (${file.language},${parts.join(',')})`);
328
+ lines.push('');
329
+ if (file.declarations.length === 0) {
330
+ lines.push('- (未检出顶层声明)');
331
+ }
332
+ else {
333
+ for (const declaration of file.declarations) {
334
+ lines.push(`- \`${declaration.kind}\` **${declaration.name}** (第 ${declaration.line} 行)`);
335
+ }
336
+ }
337
+ lines.push('');
338
+ }
339
+ if (summary.skipped.length > 0) {
340
+ lines.push('## 未纳入摘要的文件');
341
+ lines.push('');
342
+ for (const rel of summary.skipped)
343
+ lines.push(`- ${rel}`);
344
+ lines.push('');
345
+ lines.push('(原因通常是文件过大或读取失败——需要时请单独查看。)');
346
+ lines.push('');
347
+ }
348
+ lines.push('## 怎么写进论文');
349
+ lines.push('');
350
+ lines.push('1. 「系统实现」章按**模块**组织:每个主要文件/声明的职责 + 关键实现取舍(而不是贴代码);');
351
+ lines.push('2. 「系统设计」章的模块划分应与这里的文件/声明结构一致,答辩时能一一指认;');
352
+ lines.push('3. 工作量证据用这里的文件数与代码行数,但**不要**把它们直接写进正文当论据堆砌;');
353
+ lines.push('4. 想让老师追问不倒你,用 `defense_code_status`/`defense_code_next` 建代码演练(配合 `code-walkthrough` 技能)。');
354
+ lines.push('');
355
+ return lines.join('\n');
356
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 斜杠命令:`/thesis-ingest`。
3
+ *
4
+ * 命令名遵循 DSH 的 `[a-z][a-z0-9_-]*` 约束(kebab-case),与 `/thesis-check` 等同一套写法。
5
+ * 命令只是 `thesis_ingest` 工具的薄封装:把命令行输入解析成 `IngestArgs` 后交给 `runIngest`,
6
+ * 因此行为与工具**完全一致**。
7
+ *
8
+ * @module dsh-thesis/ingest
9
+ */
10
+ import type { FileSystem } from '@deepseek-ai/dsh-fs';
11
+ import type { IngestOptions } from '../config.ts';
12
+ import { type IngestArgs } from './index.ts';
13
+ /**
14
+ * 解析 `/thesis-ingest` 的输入。
15
+ *
16
+ * 用法:`/thesis-ingest <绝对路径> [--glob docx,pdf] [--dry]`
17
+ * - `<绝对路径>`:文件或目录(目录递归;源码目录会产出代码结构摘要)
18
+ * - `--glob`:后缀过滤,逗号分隔
19
+ * - `--dry`:只读不落盘(不写材料清单与摘要文件)
20
+ */
21
+ export declare function parseIngestCommandInput(rawInput: string): IngestArgs;
22
+ /** 构造 `/thesis-ingest` 命令定义。 */
23
+ export declare function ingestCommand(fs: FileSystem, options: IngestOptions): unknown;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * 斜杠命令:`/thesis-ingest`。
3
+ *
4
+ * 命令名遵循 DSH 的 `[a-z][a-z0-9_-]*` 约束(kebab-case),与 `/thesis-check` 等同一套写法。
5
+ * 命令只是 `thesis_ingest` 工具的薄封装:把命令行输入解析成 `IngestArgs` 后交给 `runIngest`,
6
+ * 因此行为与工具**完全一致**。
7
+ *
8
+ * @module dsh-thesis/ingest
9
+ */
10
+ import { diskIO, renderOutcome, runIngest } from "./index.js";
11
+ async function safe(work) {
12
+ try {
13
+ return { kind: 'success', text: await work() };
14
+ }
15
+ catch (error) {
16
+ return { kind: 'error', text: error instanceof Error ? error.message : String(error) };
17
+ }
18
+ }
19
+ /**
20
+ * 解析 `/thesis-ingest` 的输入。
21
+ *
22
+ * 用法:`/thesis-ingest <绝对路径> [--glob docx,pdf] [--dry]`
23
+ * - `<绝对路径>`:文件或目录(目录递归;源码目录会产出代码结构摘要)
24
+ * - `--glob`:后缀过滤,逗号分隔
25
+ * - `--dry`:只读不落盘(不写材料清单与摘要文件)
26
+ */
27
+ export function parseIngestCommandInput(rawInput) {
28
+ const tokens = rawInput.trim().split(/\s+/).filter(token => token !== '');
29
+ let path;
30
+ let glob;
31
+ let write = true;
32
+ for (let index = 0; index < tokens.length; index += 1) {
33
+ const token = tokens[index];
34
+ const lowered = token.toLowerCase();
35
+ if (lowered === '--glob' || lowered === '-g') {
36
+ glob = tokens[++index];
37
+ if (glob === undefined)
38
+ throw new Error('--glob 后面要跟后缀列表,如 --glob docx,pdf');
39
+ continue;
40
+ }
41
+ if (lowered === '--dry' || lowered === '--no-write') {
42
+ write = false;
43
+ continue;
44
+ }
45
+ if (token.startsWith('-'))
46
+ throw new Error(`无法识别的选项「${token}」`);
47
+ if (path !== undefined)
48
+ throw new Error(`只能指定一个路径(多余的是「${token}」)`);
49
+ path = token;
50
+ }
51
+ if (path === undefined) {
52
+ throw new Error('用法:/thesis-ingest <绝对路径> [--glob docx,pdf] [--dry]');
53
+ }
54
+ return {
55
+ path,
56
+ ...(glob !== undefined ? { glob } : {}),
57
+ ...(write ? {} : { write: false }),
58
+ };
59
+ }
60
+ /** 构造 `/thesis-ingest` 命令定义。 */
61
+ export function ingestCommand(fs, options) {
62
+ return {
63
+ name: 'thesis-ingest',
64
+ description: '摄取材料:docx/xlsx/pptx/pdf/md/csv/bib + 源码目录 → 文本与代码结构摘要,写 00-管理/材料清单.md',
65
+ input: { hint: '<绝对路径> [--glob docx,pdf] [--dry]' },
66
+ async handler(invocation) {
67
+ return await safe(async () => {
68
+ const args = parseIngestCommandInput(invocation.rawInput);
69
+ const outcome = await runIngest(fs, diskIO, args, invocation.agent.session.header.cwd, options, invocation.signal);
70
+ return renderOutcome(outcome, options.maxChars);
71
+ });
72
+ },
73
+ };
74
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `.docx` 摄取:ZIP → `word/document.xml` → 段落文本。
3
+ *
4
+ * 说明:
5
+ * - 只读正文(`word/document.xml`),页眉/页脚/脚注/文本框暂不抽取
6
+ * (文本框内容在 `w:txbxContent` 里,与正文同文档,会被一起抽到);
7
+ * - 表格保留为制表符分隔的行(见 `ooxml.ts`);
8
+ * - 标题取 `docProps/core.xml` 的 `dc:title`(Word 的"标题"属性),没有就留空,
9
+ * 不拿正文第一行冒充标题;
10
+ * - 若该文件其实是 xlsx/pptx(改扩展名的常见误操作),给出明确提示而不是报解析错误。
11
+ *
12
+ * @module dsh-thesis/ingest
13
+ */
14
+ import { type ExtractOptions, type ExtractResult } from './types.ts';
15
+ /** docx 正文抽取的完整结果(供测试与上层复用)。 */
16
+ export interface DocxContent {
17
+ /** `dc:title`(没有则为 undefined)。 */
18
+ readonly title?: string;
19
+ /** 段落数组(表格行为 tab 分隔的单段)。 */
20
+ readonly paragraphs: readonly string[];
21
+ /** 段落用换行拼起来的正文。 */
22
+ readonly text: string;
23
+ }
24
+ /** 判定 OOXML 容器的真实类型(按内部条目,而不是扩展名)。 */
25
+ export declare function sniffOoxmlKind(bytes: Uint8Array): 'docx' | 'xlsx' | 'pptx' | undefined;
26
+ /** 纯抽取:ZIP 字节 → 标题 + 段落(不截断、不做结果包装)。 */
27
+ export declare function readDocx(bytes: Uint8Array): DocxContent;
28
+ /** 门面用入口:docx 字节 → 抽取结果(含截断/失败原因)。 */
29
+ export declare function extractDocx(bytes: Uint8Array, options: ExtractOptions): ExtractResult;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * `.docx` 摄取:ZIP → `word/document.xml` → 段落文本。
3
+ *
4
+ * 说明:
5
+ * - 只读正文(`word/document.xml`),页眉/页脚/脚注/文本框暂不抽取
6
+ * (文本框内容在 `w:txbxContent` 里,与正文同文档,会被一起抽到);
7
+ * - 表格保留为制表符分隔的行(见 `ooxml.ts`);
8
+ * - 标题取 `docProps/core.xml` 的 `dc:title`(Word 的"标题"属性),没有就留空,
9
+ * 不拿正文第一行冒充标题;
10
+ * - 若该文件其实是 xlsx/pptx(改扩展名的常见误操作),给出明确提示而不是报解析错误。
11
+ *
12
+ * @module dsh-thesis/ingest
13
+ */
14
+ import { extractElementText, extractDocxParagraphs, extractRawElement } from "./ooxml.js";
15
+ import { failureResult, successResult } from "./types.js";
16
+ import { ZipError, decodeBytes, openZip } from "./zip.js";
17
+ /** 判定 OOXML 容器的真实类型(按内部条目,而不是扩展名)。 */
18
+ export function sniffOoxmlKind(bytes) {
19
+ try {
20
+ const zip = openZip(bytes);
21
+ if (zip.has('word/document.xml'))
22
+ return 'docx';
23
+ if (zip.has('xl/workbook.xml'))
24
+ return 'xlsx';
25
+ if (zip.has('ppt/presentation.xml'))
26
+ return 'pptx';
27
+ return undefined;
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
33
+ /** 读取 `docProps/core.xml` 的 `dc:title`。 */
34
+ function readCoreTitle(bytes) {
35
+ let core;
36
+ try {
37
+ core = decodeBytes(openZip(bytes).require('docProps/core.xml'));
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ const raw = extractRawElement(core, ['dc:title', 'title']);
43
+ if (raw === undefined)
44
+ return undefined;
45
+ const cleaned = extractElementText(`<x>${raw}</x>`, 'x', ['x']).trim();
46
+ return cleaned === '' ? undefined : cleaned;
47
+ }
48
+ /** 纯抽取:ZIP 字节 → 标题 + 段落(不截断、不做结果包装)。 */
49
+ export function readDocx(bytes) {
50
+ const zip = openZip(bytes);
51
+ const documentBytes = zip.read('word/document.xml');
52
+ if (documentBytes === undefined) {
53
+ const actual = sniffOoxmlKind(bytes);
54
+ if (actual === 'xlsx') {
55
+ throw new ZipError('wrong-format', '这个文件内部是 Excel 工作簿(.xlsx),不是 Word 文档:请把扩展名改回 .xlsx,或先另存为 .docx。');
56
+ }
57
+ if (actual === 'pptx') {
58
+ throw new ZipError('wrong-format', '这个文件内部是 PowerPoint 演示文稿(.pptx),不是 Word 文档:请把扩展名改回 .pptx。');
59
+ }
60
+ throw new ZipError('missing-document', '这个 ZIP 里没有 word/document.xml:不是有效的 .docx(可能是 .odt/.zip 或已损坏)。请用 Word/WPS 另存为 .docx。');
61
+ }
62
+ const xml = decodeBytes(documentBytes);
63
+ const paragraphs = extractDocxParagraphs(xml);
64
+ const title = readCoreTitle(bytes);
65
+ return {
66
+ ...(title !== undefined ? { title } : {}),
67
+ paragraphs,
68
+ text: paragraphs.join('\n'),
69
+ };
70
+ }
71
+ /** 门面用入口:docx 字节 → 抽取结果(含截断/失败原因)。 */
72
+ export function extractDocx(bytes, options) {
73
+ let content;
74
+ try {
75
+ content = readDocx(bytes);
76
+ }
77
+ catch (error) {
78
+ const reason = error instanceof ZipError
79
+ ? error.message
80
+ : `读取 .docx 失败:${error instanceof Error ? error.message : String(error)}`;
81
+ return failureResult(reason);
82
+ }
83
+ if (content.text.trim() === '') {
84
+ return failureResult('这个 .docx 没有任何正文文本(可能是空文档,或正文全在文本框/图片里):请另存为含文本的文档再试。');
85
+ }
86
+ return successResult({
87
+ text: content.text,
88
+ maxChars: options.maxChars,
89
+ title: content.title,
90
+ note: `共 ${content.paragraphs.length} 段。`,
91
+ });
92
+ }