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,84 @@
1
+ /**
2
+ * 斜杠命令:/thesis-status 与 /thesis-decide。
3
+ *
4
+ * 注:DSH 命令名只允许 [a-z][a-z0-9_-]*,因此使用 kebab-case
5
+ * (DESIGN.md 中的 /thesis:status 在此修正为 /thesis-status)。
6
+ * 其余命令(/thesis-lit、/thesis-check、/thesis-build)随 M2/M4 工具一并注册。
7
+ */
8
+ import { writeFile } from 'node:fs/promises';
9
+ import { runDecision } from "./tools/decision.js";
10
+ import { runLitSearch } from "./tools/lit.js";
11
+ import { runProgress } from "./tools/progress.js";
12
+ import { buildSummary, runBuild } from "./tools/build.js";
13
+ import { runCheck } from "./tools/check.js";
14
+ function cwdOf(invocation) {
15
+ return invocation.agent.session.header.cwd;
16
+ }
17
+ async function safe(work) {
18
+ try {
19
+ return { kind: 'success', text: await work() };
20
+ }
21
+ catch (error) {
22
+ return { kind: 'error', text: error instanceof Error ? error.message : String(error) };
23
+ }
24
+ }
25
+ export function statusCommand(fs) {
26
+ return {
27
+ name: 'thesis-status',
28
+ description: '查看毕业设计进度台账:当前阶段、人工关卡、任务状态',
29
+ async handler(invocation) {
30
+ return await safe(async () => runProgress(fs, { action: 'report' }, cwdOf(invocation), invocation.signal));
31
+ },
32
+ };
33
+ }
34
+ export function decideCommand(fs) {
35
+ return {
36
+ name: 'thesis-decide',
37
+ description: '记录一条关键决定(内容/理由/备选),写入决定日志',
38
+ input: { hint: '决定内容' },
39
+ async handler(invocation) {
40
+ const content = invocation.rawInput.trim();
41
+ if (content === '') {
42
+ return { kind: 'error', text: '用法:/thesis-decide <决定内容>。内容不能为空。' };
43
+ }
44
+ return await safe(async () => runDecision(fs, { content }, cwdOf(invocation), invocation.signal));
45
+ },
46
+ };
47
+ }
48
+ export function litCommand(fs) {
49
+ return {
50
+ name: 'thesis-lit',
51
+ description: '从真实学术数据库检索文献(结果写入检索缓存与审计记录)',
52
+ input: { hint: '检索关键词' },
53
+ async handler(invocation) {
54
+ const query = invocation.rawInput.trim();
55
+ if (query === '') {
56
+ return { kind: 'error', text: '用法:/thesis-lit <关键词>。例如 /thesis-lit 目标检测 小目标' };
57
+ }
58
+ return await safe(async () => runLitSearch(fs, { query, limit: 5 }, cwdOf(invocation), invocation.signal, fetch));
59
+ },
60
+ };
61
+ }
62
+ export function checkCommand(fs) {
63
+ return {
64
+ name: 'thesis-check',
65
+ description: '全文综合检查:引用/字数/图表/术语/模板,报告写入 08-合规/',
66
+ async handler(invocation) {
67
+ return await safe(async () => runCheck(fs, cwdOf(invocation), invocation.signal));
68
+ },
69
+ };
70
+ }
71
+ export function buildCommand(fs) {
72
+ return {
73
+ name: 'thesis-build',
74
+ description: '构建论文.docx(优先 pandoc + 学校模板,否则内置过渡引擎)',
75
+ async handler(invocation) {
76
+ return await safe(async () => {
77
+ const outcome = await runBuild(fs, {}, cwdOf(invocation), invocation.signal, async (absPath, data) => {
78
+ await writeFile(absPath, data);
79
+ });
80
+ return buildSummary(outcome);
81
+ });
82
+ },
83
+ };
84
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * AI 味自查(thesis_stylecheck 的核心):确定性启发式扫描。
3
+ *
4
+ * 六条规则,全部基于文本特征,可复现:
5
+ * 1. 模板套话("综上所述/众所周知/随着…的发展"等清单)
6
+ * 2. 空洞结论句("具有一定的意义"类)
7
+ * 3. 自我暴露(正文出现 AI/大语言模型等表述 —— 错误级)
8
+ * 4. 翻译腔("被用于/进行了…的/一个…的过程"等)
9
+ * 5. 句式单一(连续 3 段以上以同一开头)
10
+ * 6. 无锚点段落(长段落无数字、无引用、无图表、无具体对象)
11
+ *
12
+ * 本工具是"自查",不是"证明":命中项给出改写方向,最终是否修改由用户
13
+ * 在 G2/G4 关卡决定。改写建议与 thesis-writing 技能 §4 的去 AI 味规则一致。
14
+ */
15
+ export interface AiFindingItem {
16
+ readonly excerpt: string;
17
+ readonly line: number;
18
+ }
19
+ export interface AiFinding {
20
+ readonly rule: string;
21
+ readonly level: 'error' | 'warn';
22
+ readonly advice: string;
23
+ readonly items: readonly AiFindingItem[];
24
+ }
25
+ export interface AiChapterReport {
26
+ readonly chapter: string;
27
+ readonly findings: readonly AiFinding[];
28
+ }
29
+ export interface AiSelfcheckReport {
30
+ readonly chapters: readonly AiChapterReport[];
31
+ readonly totalFindings: number;
32
+ readonly errorCount: number;
33
+ }
34
+ export declare function runAiSelfcheck(chapters: readonly {
35
+ name: string;
36
+ text: string;
37
+ }[]): AiSelfcheckReport;
38
+ export declare function renderAiSelfcheck(report: AiSelfcheckReport, now?: Date): string;
@@ -0,0 +1,241 @@
1
+ /**
2
+ * AI 味自查(thesis_stylecheck 的核心):确定性启发式扫描。
3
+ *
4
+ * 六条规则,全部基于文本特征,可复现:
5
+ * 1. 模板套话("综上所述/众所周知/随着…的发展"等清单)
6
+ * 2. 空洞结论句("具有一定的意义"类)
7
+ * 3. 自我暴露(正文出现 AI/大语言模型等表述 —— 错误级)
8
+ * 4. 翻译腔("被用于/进行了…的/一个…的过程"等)
9
+ * 5. 句式单一(连续 3 段以上以同一开头)
10
+ * 6. 无锚点段落(长段落无数字、无引用、无图表、无具体对象)
11
+ *
12
+ * 本工具是"自查",不是"证明":命中项给出改写方向,最终是否修改由用户
13
+ * 在 G2/G4 关卡决定。改写建议与 thesis-writing 技能 §4 的去 AI 味规则一致。
14
+ */
15
+ const CLICHES = [
16
+ '综上所述', '总而言之', '众所周知', '值得注意的是', '不难发现', '显而易见',
17
+ '在当今', '近年来', '意义重大', '迫在眉睫', '不可或缺', '日益重要', '突飞猛进', '日新月异',
18
+ ];
19
+ const RULES = [
20
+ {
21
+ name: '模板套话',
22
+ level: 'warn',
23
+ advice: '删除或改写为具体内容:套话不携带信息,是 AI 检测的重点信号。',
24
+ test: (text) => {
25
+ const hits = new Set();
26
+ for (const phrase of CLICHES) {
27
+ if (text.includes(phrase))
28
+ hits.add(`"${phrase}"`);
29
+ }
30
+ for (const m of text.matchAll(/随着[\u4e00-\u9fff]{2,20}的(发展|进步|普及|应用)/g))
31
+ hits.add(`"${m[0]}"`);
32
+ return hits.size > 0 ? [...hits] : null;
33
+ },
34
+ },
35
+ {
36
+ name: '空洞结论句',
37
+ level: 'warn',
38
+ advice: '"具有一定的意义"没有信息量。改为具体结论:解决了什么问题、带来了什么可度量的效果。',
39
+ test: (text) => {
40
+ const hits = new Set();
41
+ for (const m of text.matchAll(/(具有|有着)(一定的|重要的|较大的|深远的)?(意义|价值|作用)/g))
42
+ hits.add(m[0]);
43
+ return hits.size > 0 ? [...hits] : null;
44
+ },
45
+ },
46
+ {
47
+ name: '自我暴露',
48
+ level: 'error',
49
+ advice: '论文正文不得出现 AI 工具表述。删除该句;若确需说明工具使用,放在致谢或按学校规定位置,并先与导师沟通。',
50
+ test: (text) => {
51
+ const hits = new Set();
52
+ for (const m of text.matchAll(/作为\s*AI|人工智能助手|大语言模型|大型语言模型|语言模型生成|ChatGPT|由\s*AI\s*(生成|撰写|完成)/gi))
53
+ hits.add(m[0]);
54
+ return hits.size > 0 ? [...hits] : null;
55
+ },
56
+ },
57
+ {
58
+ name: '翻译腔',
59
+ level: 'warn',
60
+ advice: '改为自然中文表达:"被用于"→"用于/用来";"进行了…的研究"→"研究了…";"一个…的过程"→直接说动作。',
61
+ test: (text) => {
62
+ const hits = new Set();
63
+ const patterns = [
64
+ /被用于/g,
65
+ /对[\u4e00-\u9fff]{2,20}进行了/g,
66
+ /进行了[\u4e00-\u9fff]{2,20}的/g,
67
+ /一个[\u4e00-\u9fff]{2,10}的过程/g,
68
+ /基于[\u4e00-\u9fff]{2,20}的基础(之上)?/g,
69
+ /进行(深入)?的(研究|分析|探讨)/g,
70
+ ];
71
+ for (const pattern of patterns) {
72
+ for (const m of text.matchAll(pattern))
73
+ hits.add(m[0]);
74
+ }
75
+ return hits.size > 0 ? [...hits] : null;
76
+ },
77
+ },
78
+ {
79
+ name: '句式单一',
80
+ level: 'warn',
81
+ advice: '连续多段以同一短语开头会暴露模式化生成。变换段落开头方式:从结论、对象、转折或数据切入。',
82
+ paragraphs: (paragraphs) => {
83
+ const items = [];
84
+ let runStart = 0;
85
+ let runKey = '';
86
+ let runCount = 0;
87
+ const flush = () => {
88
+ if (runCount >= 3) {
89
+ items.push({
90
+ excerpt: `连续 ${runCount} 段以"${runKey.slice(0, 12)}"开头`,
91
+ line: paragraphs[runStart].line,
92
+ });
93
+ }
94
+ };
95
+ for (const para of paragraphs) {
96
+ const key = para.text.slice(0, 6);
97
+ if (key === runKey) {
98
+ runCount += 1;
99
+ }
100
+ else {
101
+ flush();
102
+ runKey = key;
103
+ runStart = paragraphs.indexOf(para);
104
+ runCount = 1;
105
+ }
106
+ }
107
+ flush();
108
+ return items;
109
+ },
110
+ },
111
+ ];
112
+ // ---------------------------------------------------------------------------
113
+ // 段落切分与扫描
114
+ // ---------------------------------------------------------------------------
115
+ function bodyParagraphs(chapterText) {
116
+ const paragraphs = [];
117
+ const lines = chapterText.split('\n');
118
+ let start = -1;
119
+ const flush = (end) => {
120
+ if (start >= 0) {
121
+ const block = lines.slice(start, end).map(l => l.trim()).filter(l => l !== '' && !l.startsWith('|') && !l.startsWith('- ')).join('');
122
+ if (block.length > 20)
123
+ paragraphs.push({ text: block, line: start + 1 });
124
+ }
125
+ };
126
+ for (let i = 0; i < lines.length; i += 1) {
127
+ const line = lines[i];
128
+ if (line.trim() === '') {
129
+ flush(i);
130
+ start = -1;
131
+ continue;
132
+ }
133
+ if (/^#{1,6}\s/.test(line)) {
134
+ flush(i);
135
+ start = -1;
136
+ continue;
137
+ }
138
+ if (start < 0)
139
+ start = i;
140
+ }
141
+ flush(lines.length);
142
+ return paragraphs;
143
+ }
144
+ function lineOf(text, needle) {
145
+ const idx = text.indexOf(needle);
146
+ if (idx < 0)
147
+ return 0;
148
+ return text.slice(0, idx).split('\n').length;
149
+ }
150
+ function scanChapter(chapterText) {
151
+ const findings = [];
152
+ const paragraphs = bodyParagraphs(chapterText);
153
+ const paragraphText = paragraphs.map(p => p.text).join('\n');
154
+ for (const rule of RULES) {
155
+ let items = [];
156
+ if (rule.test !== undefined) {
157
+ const hits = rule.test(paragraphText);
158
+ if (hits !== null) {
159
+ items = hits.slice(0, 8).map(hit => ({ excerpt: hit, line: lineOf(chapterText, hit) }));
160
+ }
161
+ }
162
+ if (rule.paragraphs !== undefined) {
163
+ items = [...items, ...rule.paragraphs(paragraphs)];
164
+ }
165
+ if (items.length > 0) {
166
+ findings.push({ rule: rule.name, level: rule.level, advice: rule.advice, items });
167
+ }
168
+ }
169
+ // 无锚点段落(独立规则,需要逐段判断)
170
+ const anchorless = paragraphs.filter(para => {
171
+ const cjk = (para.text.match(/[\u4e00-\u9fff]/g) ?? []).length;
172
+ if (cjk < 100)
173
+ return false;
174
+ const hasNumber = /\d/.test(para.text);
175
+ const hasCitation = /\[\d/.test(para.text);
176
+ const hasFigure = /[图表]\s*\d+-\d+/.test(para.text);
177
+ const hasConcrete = /系统|模块|数据|实验|测试|本文|本系统|方法|算法|模型|接口|数据库|页面|功能/.test(para.text);
178
+ return !hasNumber && !hasCitation && !hasFigure && !hasConcrete;
179
+ });
180
+ if (anchorless.length > 0) {
181
+ findings.push({
182
+ rule: '无锚点段落',
183
+ level: 'warn',
184
+ advice: '长段落没有任何具体锚点(数字/引用/图表/具体对象),是"泛泛而谈"的信号。落到本课题的具体模块、数据或现象上,或拆分改写。',
185
+ items: anchorless.slice(0, 5).map(para => ({ excerpt: para.text.slice(0, 40), line: para.line })),
186
+ });
187
+ }
188
+ return findings;
189
+ }
190
+ export function runAiSelfcheck(chapters) {
191
+ const chapterReports = chapters.map(chapter => ({ chapter: chapter.name, findings: scanChapter(chapter.text) }));
192
+ let total = 0;
193
+ let errors = 0;
194
+ for (const report of chapterReports) {
195
+ for (const finding of report.findings) {
196
+ total += finding.items.length;
197
+ if (finding.level === 'error')
198
+ errors += finding.items.length;
199
+ }
200
+ }
201
+ return { chapters: chapterReports, totalFindings: total, errorCount: errors };
202
+ }
203
+ export function renderAiSelfcheck(report, now = new Date()) {
204
+ const lines = [];
205
+ lines.push('# AI 味自查报告');
206
+ lines.push('');
207
+ lines.push(`> 工具:thesis_stylecheck;生成时间:${now.toISOString()}`);
208
+ lines.push('> 本报告是启发式自查,不是检测结论;命中项请结合 thesis-writing 技能 §4 逐条改写。');
209
+ lines.push('');
210
+ lines.push('## 总览');
211
+ lines.push('');
212
+ lines.push(`- 命中:${report.totalFindings} 处;其中错误级 ${report.errorCount} 处`);
213
+ lines.push(`- 错误级(自我暴露)必须处理;警告级逐条判断后改写`);
214
+ lines.push('');
215
+ const withFindings = report.chapters.filter(c => c.findings.length > 0);
216
+ if (withFindings.length === 0) {
217
+ lines.push('## 各章明细');
218
+ lines.push('');
219
+ lines.push('全部章节未命中任何规则。仍需注意:启发式检查无法覆盖全部 AI 特征,最终以学校检测结果为准。');
220
+ }
221
+ for (const chapter of withFindings) {
222
+ lines.push(`## ${chapter.chapter}`);
223
+ lines.push('');
224
+ for (const finding of chapter.findings) {
225
+ lines.push(`### ${finding.level === 'error' ? '✗' : '⚠'} ${finding.rule}(${finding.items.length} 处)`);
226
+ lines.push('');
227
+ for (const item of finding.items) {
228
+ lines.push(`- 第 ${item.line} 行附近:${item.excerpt}`);
229
+ }
230
+ lines.push('');
231
+ lines.push(`改写建议:${finding.advice}`);
232
+ lines.push('');
233
+ }
234
+ }
235
+ lines.push('## 自查结论(用户填写)');
236
+ lines.push('');
237
+ lines.push('- [ ] 我已逐条处理上述命中项,并对全文做过一轮人工通读与修改');
238
+ lines.push('- [ ] 确认人:________ 日期:________');
239
+ lines.push('');
240
+ return lines.join('\n');
241
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * BibTeX 构造与解析(文献库 02-文献/refs.bib)。
3
+ *
4
+ * 只从 LitRecord(真实检索记录)构造条目;不做任何"凭空补全"。
5
+ * 条目使用 UTF-8(pandoc/biblatex/biber 现代工具链默认支持)。
6
+ */
7
+ import type { LitRecord } from './lit-api.ts';
8
+ export interface BibEntry {
9
+ readonly key: string;
10
+ readonly entry: string;
11
+ }
12
+ export declare function bibKeyFor(record: LitRecord): string;
13
+ export declare function buildBibtex(record: LitRecord, keyOverride?: string): BibEntry;
14
+ /** 提取 refs.bib 中已用的 key 集合。 */
15
+ export declare function parseBibKeys(bib: string): Set<string>;
16
+ /** 按出现顺序提取 key(GB/T 7714 顺序编码制下,条目顺序即引用编号)。 */
17
+ export declare function parseBibKeysOrdered(bib: string): string[];
18
+ /** 提取 refs.bib 中已收录的 DOI(小写),用于去重。 */
19
+ export declare function parseBibDois(bib: string): Set<string>;
20
+ /** 给 key 加字母后缀直到不与现有 key 冲突。 */
21
+ export declare function dedupeKey(key: string, existing: ReadonlySet<string>): string;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * BibTeX 构造与解析(文献库 02-文献/refs.bib)。
3
+ *
4
+ * 只从 LitRecord(真实检索记录)构造条目;不做任何"凭空补全"。
5
+ * 条目使用 UTF-8(pandoc/biblatex/biber 现代工具链默认支持)。
6
+ */
7
+ function sanitizeKeyPart(text) {
8
+ // 保留拉丁字母/数字与中日韩汉字(UTF-8 bib key 由现代工具链支持)。
9
+ return text
10
+ .toLowerCase()
11
+ .normalize('NFKD')
12
+ .replace(/[\u0300-\u036f]/g, '')
13
+ .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '');
14
+ }
15
+ /** 作者姓氏:英文取最后一个词;中文(无空格)整体保留。 */
16
+ function authorKeyPart(author) {
17
+ const trimmed = author.trim();
18
+ const parts = trimmed.split(/\s+/);
19
+ const last = parts[parts.length - 1] ?? trimmed;
20
+ const s = sanitizeKeyPart(last);
21
+ return s === '' ? sanitizeKeyPart(trimmed) : s;
22
+ }
23
+ function titleKeyPart(title) {
24
+ // 中文标题:取全部汉字的前 4 个(稳定、确定性;冲突由 dedupeKey 处理)。
25
+ const cjk = (title.match(/[\u4e00-\u9fff]+/g) ?? []).join('');
26
+ if (cjk !== '')
27
+ return cjk.slice(0, 4);
28
+ const words = title.split(/\s+/).filter(w => !/^(a|an|the|on|of|for|and)$/i.test(w));
29
+ for (const word of words) {
30
+ const s = sanitizeKeyPart(word);
31
+ if (s !== '')
32
+ return s;
33
+ }
34
+ return 'paper';
35
+ }
36
+ export function bibKeyFor(record) {
37
+ const author = record.authors[0] !== undefined ? authorKeyPart(record.authors[0]) : 'anon';
38
+ const year = record.year ?? '';
39
+ const title = titleKeyPart(record.title);
40
+ return `${author}${year}${title}`.slice(0, 40);
41
+ }
42
+ function authorsField(authors) {
43
+ return authors.map(a => a.trim()).filter(a => a !== '').join(' and ');
44
+ }
45
+ function optionalFields(record, extra) {
46
+ const fields = [];
47
+ for (const [name, value] of extra) {
48
+ if (value !== undefined && value !== '')
49
+ fields.push(` ${name} = {${value}}`);
50
+ }
51
+ if (record.doi !== undefined && record.doi !== '')
52
+ fields.push(` doi = {${record.doi}}`);
53
+ if (record.url !== undefined && record.url !== '')
54
+ fields.push(` url = {${record.url}}`);
55
+ return fields;
56
+ }
57
+ export function buildBibtex(record, keyOverride) {
58
+ const key = keyOverride ?? bibKeyFor(record);
59
+ const author = authorsField(record.authors);
60
+ const year = record.year ?? '';
61
+ let entry;
62
+ switch (record.type) {
63
+ case 'article':
64
+ entry = `@article{${key},\n author = {${author}},\n title = {${record.title}},\n journal = {${record.venue || 'unknown'}},\n year = {${year}},\n${optionalFields(record, []).join(',\n')}\n}`;
65
+ break;
66
+ case 'inproceedings':
67
+ entry = `@inproceedings{${key},\n author = {${author}},\n title = {${record.title}},\n booktitle = {${record.venue || 'unknown'}},\n year = {${year}},\n${optionalFields(record, []).join(',\n')}\n}`;
68
+ break;
69
+ case 'book':
70
+ entry = `@book{${key},\n author = {${author}},\n title = {${record.title}},\n publisher = {${record.venue || 'unknown'}},\n year = {${year}},\n${optionalFields(record, []).join(',\n')}\n}`;
71
+ break;
72
+ case 'phdthesis':
73
+ entry = `@phdthesis{${key},\n author = {${author}},\n title = {${record.title}},\n school = {${record.venue || 'unknown'}},\n year = {${year}},\n${optionalFields(record, []).join(',\n')}\n}`;
74
+ break;
75
+ case 'misc':
76
+ entry = `@misc{${key},\n author = {${author}},\n title = {${record.title}},\n howpublished = {${record.venue || 'Preprint'}},\n year = {${year}},\n${optionalFields(record, []).join(',\n')}\n}`;
77
+ break;
78
+ }
79
+ return { key, entry };
80
+ }
81
+ /** 提取 refs.bib 中已用的 key 集合。 */
82
+ export function parseBibKeys(bib) {
83
+ return new Set(parseBibKeysOrdered(bib));
84
+ }
85
+ /** 按出现顺序提取 key(GB/T 7714 顺序编码制下,条目顺序即引用编号)。 */
86
+ export function parseBibKeysOrdered(bib) {
87
+ const keys = [];
88
+ for (const m of bib.matchAll(/^@\w+\s*\{\s*([^,\s]+)/gm)) {
89
+ if (m[1] !== undefined)
90
+ keys.push(m[1]);
91
+ }
92
+ return keys;
93
+ }
94
+ /** 提取 refs.bib 中已收录的 DOI(小写),用于去重。 */
95
+ export function parseBibDois(bib) {
96
+ const dois = new Set();
97
+ for (const m of bib.matchAll(/doi\s*=\s*[{"]([^}"\s]+)[}"]/gi)) {
98
+ if (m[1] !== undefined)
99
+ dois.add(m[1].toLowerCase());
100
+ }
101
+ return dois;
102
+ }
103
+ /** 给 key 加字母后缀直到不与现有 key 冲突。 */
104
+ export function dedupeKey(key, existing) {
105
+ if (!existing.has(key))
106
+ return key;
107
+ const suffix = 'abcdefghijklmnopqrstuvwxyz';
108
+ for (const letter of suffix) {
109
+ const candidate = `${key}${letter}`;
110
+ if (!existing.has(candidate))
111
+ return candidate;
112
+ }
113
+ return `${key}x${Math.random().toString(36).slice(2, 5)}`;
114
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 全文综合检查(thesis_check 的核心),五项确定性检查:
3
+ * 1. 引用双向一致:文中引用 ↔ refs.bib(顺序编码制下编号=条目顺序)
4
+ * 2. 字数统计:每章 CJK 字数 vs 目标区间 + 全文合计
5
+ * 3. 图/表编号:每章章号与连续性(复用 review 的检查器)
6
+ * 4. 术语定义先于使用:缩写(ABBR)第一次出现必须在定义"全称(ABBR)"之后
7
+ * 5. 学校模板探测:assets/学校模板 有无模板文件
8
+ */
9
+ import { CHAPTER_META } from './layout.ts';
10
+ export interface CheckChapter {
11
+ readonly name: string;
12
+ readonly no: number;
13
+ readonly text: string;
14
+ }
15
+ export interface CheckInput {
16
+ readonly chapters: readonly CheckChapter[];
17
+ readonly bibText: string;
18
+ readonly templateFiles: readonly string[];
19
+ }
20
+ export interface CheckSection {
21
+ readonly id: string;
22
+ readonly title: string;
23
+ readonly ok: boolean;
24
+ readonly lines: readonly string[];
25
+ }
26
+ export interface CheckReport {
27
+ readonly sections: readonly CheckSection[];
28
+ readonly allOk: boolean;
29
+ }
30
+ export declare function runGlobalCheck(input: CheckInput): CheckReport;
31
+ export declare function renderCheckReport(report: CheckReport): string;
32
+ export { CHAPTER_META };