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.
- package/DESIGN.md +390 -0
- package/LICENSE +21 -0
- package/NOTICE.md +10 -0
- package/README.md +253 -0
- package/cordis.patch.yml +17 -0
- package/docs/host-api.md +640 -0
- package/lib/codewalk/commands.d.ts +23 -0
- package/lib/codewalk/commands.js +168 -0
- package/lib/codewalk/engine.d.ts +108 -0
- package/lib/codewalk/engine.js +449 -0
- package/lib/codewalk/errors.d.ts +31 -0
- package/lib/codewalk/errors.js +20 -0
- package/lib/codewalk/fs-types.d.ts +34 -0
- package/lib/codewalk/fs-types.js +10 -0
- package/lib/codewalk/gate.d.ts +30 -0
- package/lib/codewalk/gate.js +60 -0
- package/lib/codewalk/host-types.d.ts +198 -0
- package/lib/codewalk/host-types.js +11 -0
- package/lib/codewalk/prestep.d.ts +17 -0
- package/lib/codewalk/prestep.js +62 -0
- package/lib/codewalk/register.d.ts +23 -0
- package/lib/codewalk/register.js +31 -0
- package/lib/codewalk/skill.d.ts +15 -0
- package/lib/codewalk/skill.js +57 -0
- package/lib/codewalk/tools.d.ts +18 -0
- package/lib/codewalk/tools.js +314 -0
- package/lib/codewalk/types.d.ts +151 -0
- package/lib/codewalk/types.js +9 -0
- package/lib/commands.d.ts +33 -0
- package/lib/commands.js +60 -0
- package/lib/config.d.ts +125 -0
- package/lib/config.js +139 -0
- package/lib/dedup/commands.d.ts +21 -0
- package/lib/dedup/commands.js +93 -0
- package/lib/dedup/index.d.ts +75 -0
- package/lib/dedup/index.js +331 -0
- package/lib/dedup/normalize.d.ts +125 -0
- package/lib/dedup/normalize.js +329 -0
- package/lib/dedup/report.d.ts +114 -0
- package/lib/dedup/report.js +309 -0
- package/lib/dedup/rewrite.d.ts +71 -0
- package/lib/dedup/rewrite.js +228 -0
- package/lib/dedup/similarity.d.ts +169 -0
- package/lib/dedup/similarity.js +323 -0
- package/lib/gates/dangerous.d.ts +22 -0
- package/lib/gates/dangerous.js +47 -0
- package/lib/gates/detect.d.ts +54 -0
- package/lib/gates/detect.js +106 -0
- package/lib/gates/messages.d.ts +39 -0
- package/lib/gates/messages.js +74 -0
- package/lib/gates/prompt.d.ts +21 -0
- package/lib/gates/prompt.js +56 -0
- package/lib/gates/writing-intent.d.ts +49 -0
- package/lib/gates/writing-intent.js +68 -0
- package/lib/index.d.ts +36 -0
- package/lib/index.js +76 -0
- package/lib/ingest/code.d.ts +71 -0
- package/lib/ingest/code.js +356 -0
- package/lib/ingest/commands.d.ts +23 -0
- package/lib/ingest/commands.js +74 -0
- package/lib/ingest/docx.d.ts +29 -0
- package/lib/ingest/docx.js +92 -0
- package/lib/ingest/index.d.ts +179 -0
- package/lib/ingest/index.js +640 -0
- package/lib/ingest/ooxml.d.ts +58 -0
- package/lib/ingest/ooxml.js +393 -0
- package/lib/ingest/pdf.d.ts +86 -0
- package/lib/ingest/pdf.js +1016 -0
- package/lib/ingest/pptx.d.ts +32 -0
- package/lib/ingest/pptx.js +116 -0
- package/lib/ingest/text.d.ts +25 -0
- package/lib/ingest/text.js +180 -0
- package/lib/ingest/types.d.ts +48 -0
- package/lib/ingest/types.js +47 -0
- package/lib/ingest/xlsx.d.ts +72 -0
- package/lib/ingest/xlsx.js +411 -0
- package/lib/ingest/zip.d.ts +76 -0
- package/lib/ingest/zip.js +393 -0
- package/lib/intake/definitions.d.ts +65 -0
- package/lib/intake/definitions.js +42 -0
- package/lib/intake/gate-rules.d.ts +57 -0
- package/lib/intake/gate-rules.js +118 -0
- package/lib/intake/gate.d.ts +24 -0
- package/lib/intake/gate.js +86 -0
- package/lib/intake/index.d.ts +76 -0
- package/lib/intake/index.js +468 -0
- package/lib/intake/questions.d.ts +169 -0
- package/lib/intake/questions.js +720 -0
- package/lib/intake/spec.d.ts +66 -0
- package/lib/intake/spec.js +221 -0
- package/lib/intake/state.d.ts +192 -0
- package/lib/intake/state.js +420 -0
- package/lib/memory/recall.d.ts +69 -0
- package/lib/memory/recall.js +177 -0
- package/lib/memory/register.d.ts +37 -0
- package/lib/memory/register.js +52 -0
- package/lib/memory/service.d.ts +39 -0
- package/lib/memory/service.js +39 -0
- package/lib/memory/store.d.ts +45 -0
- package/lib/memory/store.js +123 -0
- package/lib/memory/tools.d.ts +19 -0
- package/lib/memory/tools.js +195 -0
- package/lib/memory/types.d.ts +55 -0
- package/lib/memory/types.js +6 -0
- package/lib/paper/commands.d.ts +13 -0
- package/lib/paper/commands.js +84 -0
- package/lib/paper/lib/aicheck.d.ts +38 -0
- package/lib/paper/lib/aicheck.js +241 -0
- package/lib/paper/lib/bibtex.d.ts +21 -0
- package/lib/paper/lib/bibtex.js +114 -0
- package/lib/paper/lib/check.d.ts +32 -0
- package/lib/paper/lib/check.js +215 -0
- package/lib/paper/lib/defense.d.ts +48 -0
- package/lib/paper/lib/defense.js +220 -0
- package/lib/paper/lib/docx.d.ts +35 -0
- package/lib/paper/lib/docx.js +213 -0
- package/lib/paper/lib/layout.d.ts +41 -0
- package/lib/paper/lib/layout.js +572 -0
- package/lib/paper/lib/ledger.d.ts +112 -0
- package/lib/paper/lib/ledger.js +349 -0
- package/lib/paper/lib/lit-api.d.ts +59 -0
- package/lib/paper/lib/lit-api.js +296 -0
- package/lib/paper/lib/lit-cache.d.ts +32 -0
- package/lib/paper/lib/lit-cache.js +91 -0
- package/lib/paper/lib/project.d.ts +29 -0
- package/lib/paper/lib/project.js +93 -0
- package/lib/paper/lib/review.d.ts +37 -0
- package/lib/paper/lib/review.js +194 -0
- package/lib/paper/lib/zip.d.ts +14 -0
- package/lib/paper/lib/zip.js +124 -0
- package/lib/paper/register.d.ts +20 -0
- package/lib/paper/register.js +183 -0
- package/lib/paper/tools/aicheck.d.ts +6 -0
- package/lib/paper/tools/aicheck.js +38 -0
- package/lib/paper/tools/build.d.ts +27 -0
- package/lib/paper/tools/build.js +166 -0
- package/lib/paper/tools/check.d.ts +7 -0
- package/lib/paper/tools/check.js +79 -0
- package/lib/paper/tools/decision.d.ts +13 -0
- package/lib/paper/tools/decision.js +53 -0
- package/lib/paper/tools/defense.d.ts +11 -0
- package/lib/paper/tools/defense.js +74 -0
- package/lib/paper/tools/init.d.ts +23 -0
- package/lib/paper/tools/init.js +99 -0
- package/lib/paper/tools/lit.d.ts +27 -0
- package/lib/paper/tools/lit.js +236 -0
- package/lib/paper/tools/progress.d.ts +18 -0
- package/lib/paper/tools/progress.js +77 -0
- package/lib/paper/tools/review.d.ts +11 -0
- package/lib/paper/tools/review.js +41 -0
- package/lib/ppt/actions.d.ts +97 -0
- package/lib/ppt/actions.js +263 -0
- package/lib/ppt/check.d.ts +64 -0
- package/lib/ppt/check.js +185 -0
- package/lib/ppt/commands.d.ts +28 -0
- package/lib/ppt/commands.js +71 -0
- package/lib/ppt/convert.d.ts +146 -0
- package/lib/ppt/convert.js +423 -0
- package/lib/ppt/index.d.ts +45 -0
- package/lib/ppt/index.js +57 -0
- package/lib/ppt/marp.d.ts +74 -0
- package/lib/ppt/marp.js +165 -0
- package/lib/ppt/outline.d.ts +206 -0
- package/lib/ppt/outline.js +695 -0
- package/lib/shared/define-tool.d.ts +77 -0
- package/lib/shared/define-tool.js +235 -0
- package/lib/shared/fs-errors.d.ts +50 -0
- package/lib/shared/fs-errors.js +90 -0
- package/lib/shared/index.d.ts +31 -0
- package/lib/shared/index.js +32 -0
- package/lib/shared/spawn.d.ts +38 -0
- package/lib/shared/spawn.js +50 -0
- package/lib/shared/text.d.ts +14 -0
- package/lib/shared/text.js +26 -0
- package/package.json +82 -0
- package/skills/code-walkthrough/SKILL.md +94 -0
- package/skills/code-walkthrough/references/examples.md +129 -0
- package/skills/paper-dedup/SKILL.md +58 -0
- package/skills/paper-intake/SKILL.md +64 -0
- package/skills/thesis-citation/SKILL.md +67 -0
- package/skills/thesis-defense/SKILL.md +87 -0
- package/skills/thesis-eng-design/SKILL.md +97 -0
- package/skills/thesis-inquiry/SKILL.md +55 -0
- package/skills/thesis-inquiry/references/intent-spec-template.md +39 -0
- package/skills/thesis-literature/SKILL.md +56 -0
- package/skills/thesis-opener/SKILL.md +77 -0
- package/skills/thesis-pipeline/SKILL.md +121 -0
- package/skills/thesis-writing/SKILL.md +86 -0
|
@@ -0,0 +1,215 @@
|
|
|
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 { parseBibKeysOrdered } from "./bibtex.js";
|
|
10
|
+
import { chapterMetaFor, CHAPTER_META } from "./layout.js";
|
|
11
|
+
import { checkNumberedObjects } from "./review.js";
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// 1. 引用双向一致
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
function citedNumbersIn(text) {
|
|
16
|
+
const numbers = new Set();
|
|
17
|
+
for (const m of text.matchAll(/\[(\d+(?:[-,,]\d+)*)\]/g)) {
|
|
18
|
+
for (const part of m[1].split(/[,,]/)) {
|
|
19
|
+
const range = /^(\d+)-(\d+)$/.exec(part);
|
|
20
|
+
if (range !== null) {
|
|
21
|
+
for (let n = Number(range[1]); n <= Number(range[2]); n += 1)
|
|
22
|
+
numbers.add(n);
|
|
23
|
+
}
|
|
24
|
+
else if (/^\d+$/.test(part)) {
|
|
25
|
+
numbers.add(Number(part));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return [...numbers].sort((a, b) => a - b);
|
|
30
|
+
}
|
|
31
|
+
function checkCitationsGlobal(input) {
|
|
32
|
+
const lines = [];
|
|
33
|
+
const allCited = new Set();
|
|
34
|
+
const byChapter = new Map();
|
|
35
|
+
for (const chapter of input.chapters) {
|
|
36
|
+
const cited = citedNumbersIn(chapter.text);
|
|
37
|
+
byChapter.set(chapter.name, cited);
|
|
38
|
+
for (const n of cited)
|
|
39
|
+
allCited.add(n);
|
|
40
|
+
}
|
|
41
|
+
const bibKeys = parseBibKeysOrdered(input.bibText);
|
|
42
|
+
const bibCount = bibKeys.length;
|
|
43
|
+
if (bibCount === 0 && allCited.size > 0) {
|
|
44
|
+
lines.push(`✗ refs.bib 为空(0 条),但正文引用了 [${[...allCited].join('、')}]。请先经 thesis_lit_search/save 收录文献。`);
|
|
45
|
+
return { id: 'citations', title: '引用双向一致', ok: false, lines };
|
|
46
|
+
}
|
|
47
|
+
if (bibCount === 0) {
|
|
48
|
+
lines.push('⚠ 文献库为空。绪论与相关技术章写作前,请先用 thesis_lit_search 检索真实文献。');
|
|
49
|
+
return { id: 'citations', title: '引用双向一致', ok: false, lines };
|
|
50
|
+
}
|
|
51
|
+
const citedList = [...allCited].sort((a, b) => a - b);
|
|
52
|
+
const max = citedList[citedList.length - 1] ?? 0;
|
|
53
|
+
const outOfRange = citedList.filter(n => n > bibCount);
|
|
54
|
+
if (outOfRange.length > 0) {
|
|
55
|
+
lines.push(`✗ 引用编号超出文献库:${outOfRange.map(n => `[${n}]`).join('、')}(refs.bib 共 ${bibCount} 条)。`);
|
|
56
|
+
}
|
|
57
|
+
// 未被引用的条目(躺尸文献)
|
|
58
|
+
const uncited = Array.from({ length: bibCount }, (_v, i) => i + 1).filter(n => !allCited.has(n));
|
|
59
|
+
if (uncited.length > 0) {
|
|
60
|
+
const keys = uncited.map(n => bibKeys[n - 1] ?? `#${n}`).join('、');
|
|
61
|
+
lines.push(`⚠ 文献库中 ${uncited.length} 条从未被正文引用:${keys}。要么在正文补引,要么从 refs.bib 删除。`);
|
|
62
|
+
}
|
|
63
|
+
// 引用编号不连续(有 [3] 无 [2])
|
|
64
|
+
for (let n = 1; n <= max; n += 1) {
|
|
65
|
+
if (!allCited.has(n)) {
|
|
66
|
+
lines.push(`⚠ 引用编号不连续:缺 [${n}](顺序编码制下编号应与文献表顺序一致)。`);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// 章级概览
|
|
71
|
+
const chapterStats = input.chapters.map(c => `${c.name}: [${(byChapter.get(c.name) ?? []).join('、') || '无'}]`).join(';');
|
|
72
|
+
lines.push(`章级引用:${chapterStats}。`);
|
|
73
|
+
if (lines.length === 1) {
|
|
74
|
+
lines.unshift(`✓ 正文引用与 refs.bib(${bibCount} 条)双向一致:${citedList.length === 0 ? '正文暂无引用' : `引用编号 ${citedList.join('、')} 均有对应条目`}。`);
|
|
75
|
+
}
|
|
76
|
+
return { id: 'citations', title: '引用双向一致', ok: outOfRange.length === 0 && bibCount > 0, lines };
|
|
77
|
+
}
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// 2. 字数统计
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
function checkWordCountsGlobal(input) {
|
|
82
|
+
const lines = [];
|
|
83
|
+
let total = 0;
|
|
84
|
+
let problems = 0;
|
|
85
|
+
for (const chapter of input.chapters) {
|
|
86
|
+
const located = chapterMetaFor(chapter.name);
|
|
87
|
+
const count = (chapter.text.match(/[\u4e00-\u9fff]/g) ?? []).length;
|
|
88
|
+
total += count;
|
|
89
|
+
const rangeMatch = located === undefined ? null : /(\d+)\s*[-–]\s*(\d+)/.exec(located.meta.words);
|
|
90
|
+
if (rangeMatch === null) {
|
|
91
|
+
lines.push(`- ${chapter.name}:${count} 字(目标未知)`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const min = Number(rangeMatch[1]);
|
|
95
|
+
const max = Number(rangeMatch[2]);
|
|
96
|
+
const ok = count >= min && count <= max;
|
|
97
|
+
if (!ok)
|
|
98
|
+
problems += 1;
|
|
99
|
+
lines.push(`- ${chapter.name}:${count} 字 / 目标 ${min}-${max} ${ok ? '✓' : `✗(${count < min ? `差 ${min - count}` : `超 ${count - max}`})`}`);
|
|
100
|
+
}
|
|
101
|
+
lines.push(`- 全文合计:${total} 字(常见本科要求 1.5-2 万,以学校文件为准)`);
|
|
102
|
+
return { id: 'words', title: '字数统计', ok: problems === 0, lines };
|
|
103
|
+
}
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// 3. 图/表编号(全文逐章)
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
function checkFiguresGlobal(input) {
|
|
108
|
+
const lines = [];
|
|
109
|
+
let problems = 0;
|
|
110
|
+
for (const chapter of input.chapters) {
|
|
111
|
+
for (const kind of ['图', '表']) {
|
|
112
|
+
const finding = checkNumberedObjects(chapter.text, chapter.no, kind);
|
|
113
|
+
if (!finding.ok) {
|
|
114
|
+
problems += 1;
|
|
115
|
+
lines.push(`✗ ${chapter.name} ${finding.detail}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (problems === 0)
|
|
120
|
+
lines.push('✓ 各章图/表编号章号正确、序号连续(无图表的章不计)。');
|
|
121
|
+
return { id: 'figures', title: '图/表编号', ok: problems === 0, lines };
|
|
122
|
+
}
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// 4. 术语定义先于使用
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
const DEF_RE = /[\u4e00-\u9fff]{2,20}(([A-Z]{2,10}))/g;
|
|
127
|
+
function checkTerminology(input) {
|
|
128
|
+
const lines = [];
|
|
129
|
+
// 第一遍:记录每个缩写的首次定义章。
|
|
130
|
+
const definedAt = new Map();
|
|
131
|
+
input.chapters.forEach((chapter, idx) => {
|
|
132
|
+
for (const m of chapter.text.matchAll(DEF_RE)) {
|
|
133
|
+
const abbr = m[1];
|
|
134
|
+
if (!definedAt.has(abbr))
|
|
135
|
+
definedAt.set(abbr, idx);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
// 第二遍:在"去掉定义点"的文本里找使用,出现在定义章之前 → 违规。
|
|
139
|
+
const problems = new Map();
|
|
140
|
+
input.chapters.forEach((chapter, idx) => {
|
|
141
|
+
const scanText = chapter.text.replace(/([A-Z]{2,10})/g, '()');
|
|
142
|
+
for (const abbr of definedAt.keys()) {
|
|
143
|
+
const defIdx = definedAt.get(abbr);
|
|
144
|
+
if (idx >= defIdx)
|
|
145
|
+
continue;
|
|
146
|
+
const re = new RegExp(`(^|[^A-Za-z\u4e00-\u9fff])${abbr}([^A-Za-z\u4e00-\u9fff]|$)`, 'g');
|
|
147
|
+
if (re.test(scanText)) {
|
|
148
|
+
problems.set(abbr, defIdx);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
if (problems.size === 0) {
|
|
153
|
+
lines.push(definedAt.size === 0
|
|
154
|
+
? '✓ 未发现缩写术语定义(如使用缩写,请按"全称(缩写)"首次定义)。'
|
|
155
|
+
: `✓ 已定义的 ${definedAt.size} 个缩写(${[...definedAt.keys()].join('、')})均未在定义前使用。`);
|
|
156
|
+
return { id: 'terms', title: '术语定义先于使用', ok: true, lines };
|
|
157
|
+
}
|
|
158
|
+
for (const [abbr, defIdx] of problems) {
|
|
159
|
+
lines.push(`✗ ${abbr} 在第 ${defIdx + 1} 章才定义,但更早章节已使用。缩写首次出现时应写"全称(${abbr})"。`);
|
|
160
|
+
}
|
|
161
|
+
return { id: 'terms', title: '术语定义先于使用', ok: false, lines };
|
|
162
|
+
}
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// 5. 学校模板探测
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
function checkTemplate(templateFiles) {
|
|
167
|
+
const hasTemplate = templateFiles.some(f => /\.(docx?|dotx?)$/i.test(f));
|
|
168
|
+
if (hasTemplate) {
|
|
169
|
+
return {
|
|
170
|
+
id: 'template',
|
|
171
|
+
title: '学校模板',
|
|
172
|
+
ok: true,
|
|
173
|
+
lines: [`✓ 发现学校模板文件:${templateFiles.filter(f => /\.(docx?|dotx?)$/i.test(f)).join('、')}。thesis_build 将优先使用。`],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
id: 'template',
|
|
178
|
+
title: '学校模板',
|
|
179
|
+
ok: false,
|
|
180
|
+
lines: [
|
|
181
|
+
'⚠ 06-论文/assets/学校模板/ 中未发现模板文件(.doc/.docx)。',
|
|
182
|
+
' thesis_build 将使用内置过渡模板并显式标注"非学校模板"。',
|
|
183
|
+
' 提交前请从教务处/学院获取模板放入该目录后重建。',
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// 汇总
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
export function runGlobalCheck(input) {
|
|
191
|
+
const sections = [
|
|
192
|
+
checkCitationsGlobal(input),
|
|
193
|
+
checkWordCountsGlobal(input),
|
|
194
|
+
checkFiguresGlobal(input),
|
|
195
|
+
checkTerminology(input),
|
|
196
|
+
checkTemplate(input.templateFiles),
|
|
197
|
+
];
|
|
198
|
+
return { sections, allOk: sections.every(s => s.ok) };
|
|
199
|
+
}
|
|
200
|
+
export function renderCheckReport(report) {
|
|
201
|
+
const lines = [];
|
|
202
|
+
for (const section of report.sections) {
|
|
203
|
+
lines.push(`## ${section.title}`);
|
|
204
|
+
lines.push('');
|
|
205
|
+
for (const line of section.lines)
|
|
206
|
+
lines.push(line);
|
|
207
|
+
lines.push('');
|
|
208
|
+
}
|
|
209
|
+
const failed = report.sections.filter(s => !s.ok).length;
|
|
210
|
+
lines.push(report.allOk
|
|
211
|
+
? '结论:全部检查通过。剩余风险项(重复率、AI 检测)请按 08-合规/自查报告.md 流程自查。'
|
|
212
|
+
: `结论:${failed} 个检查项需处理。修复后重跑 thesis_check。`);
|
|
213
|
+
return lines.join('\n');
|
|
214
|
+
}
|
|
215
|
+
export { CHAPTER_META };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 答辩准备(thesis_defense 的核心)。
|
|
3
|
+
*
|
|
4
|
+
* 工具做"机器可提取"的部分:从论文工作区提取答辩素材(课题、章节概览、
|
|
5
|
+
* 技术选型、关键决定、测试数据、git 工作量),生成:
|
|
6
|
+
* - 07-答辩/答辩素材.md —— 素材摘要(答辩讲稿的事实底座)
|
|
7
|
+
* - 07-答辩/预答辩问题库.md —— 六类必问问题模板 + 证据锚点
|
|
8
|
+
*
|
|
9
|
+
* 问题的具体化与模拟问答由 AI(结合模型能力与素材)和用户共同完成,
|
|
10
|
+
* 流程见技能 thesis-defense。
|
|
11
|
+
*/
|
|
12
|
+
export interface ChapterDigest {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly no: number;
|
|
15
|
+
readonly title: string;
|
|
16
|
+
readonly cjk: number;
|
|
17
|
+
readonly headings: readonly string[];
|
|
18
|
+
readonly citations: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function extractHeadings(text: string): string[];
|
|
21
|
+
/** 提取技术选型表(03-设计/技术选型论证.md 的表格行)。 */
|
|
22
|
+
export declare function extractTechChoices(text: string): string[][];
|
|
23
|
+
/** 提取决定日志条目标题。 */
|
|
24
|
+
export declare function extractDecisionTitles(log: string): string[];
|
|
25
|
+
/** 统计测试计划用例数(TC-x 行)。 */
|
|
26
|
+
export declare function countTestCases(plan: string): number;
|
|
27
|
+
export interface GitStats {
|
|
28
|
+
readonly commits: number;
|
|
29
|
+
readonly firstCommit?: string;
|
|
30
|
+
readonly lastCommit?: string;
|
|
31
|
+
readonly ok: boolean;
|
|
32
|
+
readonly error?: string;
|
|
33
|
+
}
|
|
34
|
+
export type GitRunner = (root: string) => GitStats;
|
|
35
|
+
/** 默认实现:真实运行 git(子进程)。测试注入替身。 */
|
|
36
|
+
export declare const spawnGitStats: GitRunner;
|
|
37
|
+
export interface DefenseInput {
|
|
38
|
+
readonly title: string;
|
|
39
|
+
readonly chapters: readonly ChapterDigest[];
|
|
40
|
+
readonly techChoices: readonly (readonly string[])[];
|
|
41
|
+
readonly decisions: readonly string[];
|
|
42
|
+
readonly testCases: number;
|
|
43
|
+
readonly resultFiles: readonly string[];
|
|
44
|
+
readonly bibCount: number;
|
|
45
|
+
readonly git: GitStats;
|
|
46
|
+
}
|
|
47
|
+
export declare function renderDefenseMaterials(input: DefenseInput, now?: Date): string;
|
|
48
|
+
export declare function renderQuestionBank(title: string, now?: Date): string;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 答辩准备(thesis_defense 的核心)。
|
|
3
|
+
*
|
|
4
|
+
* 工具做"机器可提取"的部分:从论文工作区提取答辩素材(课题、章节概览、
|
|
5
|
+
* 技术选型、关键决定、测试数据、git 工作量),生成:
|
|
6
|
+
* - 07-答辩/答辩素材.md —— 素材摘要(答辩讲稿的事实底座)
|
|
7
|
+
* - 07-答辩/预答辩问题库.md —— 六类必问问题模板 + 证据锚点
|
|
8
|
+
*
|
|
9
|
+
* 问题的具体化与模拟问答由 AI(结合模型能力与素材)和用户共同完成,
|
|
10
|
+
* 流程见技能 thesis-defense。
|
|
11
|
+
*/
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
export function extractHeadings(text) {
|
|
14
|
+
return text.split('\n').map(l => l.trim()).filter(l => /^#{1,3}\s/.test(l)).map(l => l.replace(/^#{1,3}\s+/, ''));
|
|
15
|
+
}
|
|
16
|
+
function countCitations(text) {
|
|
17
|
+
const numbers = new Set();
|
|
18
|
+
for (const m of text.matchAll(/\[(\d+(?:[-,,]\d+)*)\]/g)) {
|
|
19
|
+
for (const part of m[1].split(/[,,]/)) {
|
|
20
|
+
const range = /^(\d+)-(\d+)$/.exec(part);
|
|
21
|
+
if (range !== null) {
|
|
22
|
+
for (let n = Number(range[1]); n <= Number(range[2]); n += 1)
|
|
23
|
+
numbers.add(n);
|
|
24
|
+
}
|
|
25
|
+
else if (/^\d+$/.test(part)) {
|
|
26
|
+
numbers.add(Number(part));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return numbers.size;
|
|
31
|
+
}
|
|
32
|
+
/** 提取技术选型表(03-设计/技术选型论证.md 的表格行)。 */
|
|
33
|
+
export function extractTechChoices(text) {
|
|
34
|
+
const rows = [];
|
|
35
|
+
for (const line of text.split('\n')) {
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
if (!trimmed.startsWith('|') || !trimmed.endsWith('|'))
|
|
38
|
+
continue;
|
|
39
|
+
const cells = trimmed.slice(1, -1).split('|').map(c => c.trim());
|
|
40
|
+
const isSeparator = cells.every(c => /^:?-{2,}:?$/.test(c));
|
|
41
|
+
const isHeader = cells.some(c => /选型项|选择|理由/.test(c));
|
|
42
|
+
if (isSeparator || isHeader || cells.every(c => c === ''))
|
|
43
|
+
continue;
|
|
44
|
+
rows.push(cells);
|
|
45
|
+
}
|
|
46
|
+
return rows;
|
|
47
|
+
}
|
|
48
|
+
/** 提取决定日志条目标题。 */
|
|
49
|
+
export function extractDecisionTitles(log) {
|
|
50
|
+
const titles = [];
|
|
51
|
+
for (const m of log.matchAll(/^## (\d{4}-\d{2}-\d{2}) · (.+)$/gm)) {
|
|
52
|
+
titles.push(`${m[1]} ${m[2]}`);
|
|
53
|
+
}
|
|
54
|
+
return titles;
|
|
55
|
+
}
|
|
56
|
+
/** 统计测试计划用例数(TC-x 行)。 */
|
|
57
|
+
export function countTestCases(plan) {
|
|
58
|
+
return (plan.match(/^\|\s*TC-\d+/gm) ?? []).length;
|
|
59
|
+
}
|
|
60
|
+
/** 默认实现:真实运行 git(子进程)。测试注入替身。 */
|
|
61
|
+
export const spawnGitStats = (root) => {
|
|
62
|
+
try {
|
|
63
|
+
const count = spawnSync('git', ['-C', root, 'rev-list', '--count', 'HEAD'], { encoding: 'utf8' });
|
|
64
|
+
if (count.status !== 0)
|
|
65
|
+
return { commits: 0, ok: false, error: (count.stderr ?? '').trim() };
|
|
66
|
+
const first = spawnSync('git', ['-C', root, 'log', '--reverse', '--format=%cs', 'HEAD'], { encoding: 'utf8' });
|
|
67
|
+
const last = spawnSync('git', ['-C', root, 'log', '-1', '--format=%cs', 'HEAD'], { encoding: 'utf8' });
|
|
68
|
+
const firstDate = (first.stdout ?? '').trim().split('\n')[0] ?? '';
|
|
69
|
+
return {
|
|
70
|
+
commits: Number.parseInt((count.stdout ?? '').trim(), 10) || 0,
|
|
71
|
+
...(firstDate !== '' ? { firstCommit: firstDate } : {}),
|
|
72
|
+
...((last.stdout ?? '').trim() !== '' ? { lastCommit: (last.stdout ?? '').trim() } : {}),
|
|
73
|
+
ok: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
return { commits: 0, ok: false, error: String(error) };
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
export function renderDefenseMaterials(input, now = new Date()) {
|
|
81
|
+
const lines = [];
|
|
82
|
+
lines.push('# 答辩素材');
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push(`> 由 thesis_defense 自动提取;时间:${now.toISOString()}`);
|
|
85
|
+
lines.push('> 这是答辩讲稿的事实底座——PPT 每页内容、问答答案都以这里的材料为准。');
|
|
86
|
+
lines.push('');
|
|
87
|
+
lines.push(`## 课题`);
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push(input.title);
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push('## 章节概览');
|
|
92
|
+
lines.push('');
|
|
93
|
+
for (const chapter of input.chapters) {
|
|
94
|
+
lines.push(`### 第 ${chapter.no} 章 ${chapter.title}(${chapter.cjk} 字,引用 ${chapter.citations} 处)`);
|
|
95
|
+
lines.push('');
|
|
96
|
+
for (const heading of chapter.headings)
|
|
97
|
+
lines.push(`- ${heading}`);
|
|
98
|
+
lines.push('');
|
|
99
|
+
}
|
|
100
|
+
lines.push('## 技术选型(答辩必问"为什么")');
|
|
101
|
+
lines.push('');
|
|
102
|
+
if (input.techChoices.length === 0) {
|
|
103
|
+
lines.push('(03-设计/技术选型论证.md 尚无选型记录)');
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
for (const row of input.techChoices)
|
|
107
|
+
lines.push(`- ${row[0] ?? '?'}:${row[1] ?? '?'}(理由:${row[2] ?? '未填'};备选:${row[3] ?? '未填'})`);
|
|
108
|
+
}
|
|
109
|
+
lines.push('');
|
|
110
|
+
lines.push('## 关键决定(决定日志)');
|
|
111
|
+
lines.push('');
|
|
112
|
+
if (input.decisions.length === 0)
|
|
113
|
+
lines.push('(暂无记录——答辩前把关键决策补录进决定日志)');
|
|
114
|
+
for (const title of input.decisions)
|
|
115
|
+
lines.push(`- ${title}`);
|
|
116
|
+
lines.push('');
|
|
117
|
+
lines.push('## 测试与数据');
|
|
118
|
+
lines.push('');
|
|
119
|
+
lines.push(`- 测试用例:${input.testCases} 个(05-实验测试/测试计划.md)`);
|
|
120
|
+
lines.push(`- 结果文件:${input.resultFiles.length === 0 ? '(尚无真实结果文件——红线:所有答辩数据必须真实可复现)' : input.resultFiles.join('、')}`);
|
|
121
|
+
lines.push('');
|
|
122
|
+
lines.push('## 工作量(git)');
|
|
123
|
+
lines.push('');
|
|
124
|
+
if (!input.git.ok) {
|
|
125
|
+
lines.push(`(git 统计不可用:${input.git.error ?? '未知原因'})`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
lines.push(`- 提交:${input.git.commits} 次`);
|
|
129
|
+
if (input.git.firstCommit !== undefined && input.git.lastCommit !== undefined) {
|
|
130
|
+
lines.push(`- 跨度:${input.git.firstCommit} ~ ${input.git.lastCommit}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
lines.push('');
|
|
134
|
+
lines.push('## 文献');
|
|
135
|
+
lines.push('');
|
|
136
|
+
lines.push(`refs.bib 收录 ${input.bibCount} 条(全部真实检索,审计记录见 02-文献/检索记录.md)`);
|
|
137
|
+
lines.push('');
|
|
138
|
+
return lines.join('\n');
|
|
139
|
+
}
|
|
140
|
+
const QUESTION_CATEGORIES = [
|
|
141
|
+
{
|
|
142
|
+
name: '一、实现细节(必问,老师会逐模块追问)',
|
|
143
|
+
anchor: '04-实现 代码 + 05-实验测试/结果 截图',
|
|
144
|
+
questions: [
|
|
145
|
+
'<核心模块>具体怎么实现的?核心流程从头讲一遍。',
|
|
146
|
+
'系统有哪些关键模块/数据表?数据在它们之间怎么流转?',
|
|
147
|
+
'开发中遇到的最难的问题是什么?怎么定位和解决的?',
|
|
148
|
+
'如果并发量/数据量扩大 10 倍,你的设计哪里会先出问题?',
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: '二、技术选型(答辩高频"为什么")',
|
|
153
|
+
anchor: '03-设计/技术选型论证.md + 决定日志',
|
|
154
|
+
questions: [
|
|
155
|
+
'为什么选 <技术 A> 而不是 <备选 B>?',
|
|
156
|
+
'<技术 A> 的缺点是什么?你在设计里怎么缓解的?',
|
|
157
|
+
'如果现在换成 <备选 B>,需要改动哪些部分?',
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: '三、需求与背景',
|
|
162
|
+
anchor: '03-设计/需求分析.md(FR/NFR 编号)',
|
|
163
|
+
questions: [
|
|
164
|
+
'这个系统的需求是怎么来的?谁在用、解决什么实际问题?',
|
|
165
|
+
'哪条功能需求最难实现?为什么?',
|
|
166
|
+
'你的系统与现有同类系统/方案相比,差异和亮点是什么?',
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: '四、工作量与过程(诚实原则)',
|
|
171
|
+
anchor: 'git 提交记录 + 决定日志 + 时间线',
|
|
172
|
+
questions: [
|
|
173
|
+
'哪些部分是你独立完成的?开发花了多久?',
|
|
174
|
+
'AI 辅助了哪些环节?你自己做的关键决策有哪些?',
|
|
175
|
+
'中期检查之后方案有没有调整?为什么调整?',
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: '五、数据可信',
|
|
180
|
+
anchor: '05-实验测试/测试计划.md + 结果/(须可复现)',
|
|
181
|
+
questions: [
|
|
182
|
+
'测试数据是怎么得到的?能现场演示/复现吗?',
|
|
183
|
+
'性能指标用什么工具测的?测试环境是什么?',
|
|
184
|
+
'有没有失败过的用例?后来怎么处理的?',
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: '六、不足与展望',
|
|
189
|
+
anchor: '07-总结与展望.md + 绪论 1.3 本文主要工作',
|
|
190
|
+
questions: [
|
|
191
|
+
'你觉得这个系统哪里做得不够好?',
|
|
192
|
+
'如果重新做一次,你会怎么改进?',
|
|
193
|
+
'后续如果要继续做,方向是什么?',
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
export function renderQuestionBank(title, now = new Date()) {
|
|
198
|
+
const lines = [];
|
|
199
|
+
lines.push('# 预答辩问题库');
|
|
200
|
+
lines.push('');
|
|
201
|
+
lines.push(`> 课题:${title};由 thesis_defense 生成骨架;时间:${now.toISOString()}`);
|
|
202
|
+
lines.push('> 使用方法:AI 依据《答辩素材》把 <> 占位替换为你的系统实际名称,生成 15-20 个具体问题;');
|
|
203
|
+
lines.push('> 你逐题回答并记录要点;答不上的回对应材料补学。演练流程见技能 thesis-defense。');
|
|
204
|
+
lines.push('');
|
|
205
|
+
for (const category of QUESTION_CATEGORIES) {
|
|
206
|
+
lines.push(`## ${category.name}`);
|
|
207
|
+
lines.push('');
|
|
208
|
+
lines.push(`证据锚点:${category.anchor}`);
|
|
209
|
+
lines.push('');
|
|
210
|
+
for (const question of category.questions) {
|
|
211
|
+
lines.push(`- [ ] ${question}`);
|
|
212
|
+
}
|
|
213
|
+
lines.push('');
|
|
214
|
+
}
|
|
215
|
+
lines.push('## 我的回答记录');
|
|
216
|
+
lines.push('');
|
|
217
|
+
lines.push('(每题回答后把要点记在这里;至少完整演练 2 轮,第 2 轮随机抽题)');
|
|
218
|
+
lines.push('');
|
|
219
|
+
return lines.join('\n');
|
|
220
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 零依赖 .docx 生成:Markdown-lite → WordprocessingML。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖论文写作实际用到的 Markdown 子集:
|
|
5
|
+
* - 标题 # / ## / ###(H1 每章另起一页)
|
|
6
|
+
* - 段落、粗体 **text**、行内代码 `code`、链接 [text](url)
|
|
7
|
+
* - 无序列表 - item、表格 | a | b |
|
|
8
|
+
* - G2 清单 - [ ] / - [x] 保持为列表项文本
|
|
9
|
+
*
|
|
10
|
+
* 中文排版:正文宋体 + Times New Roman、1.5 倍行距;标题黑体。
|
|
11
|
+
* 这是内置过渡引擎;机器装有 pandoc 时 thesis_build 优先走 pandoc。
|
|
12
|
+
*/
|
|
13
|
+
type Block = {
|
|
14
|
+
readonly kind: 'h1' | 'h2' | 'h3';
|
|
15
|
+
readonly text: string;
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: 'p';
|
|
18
|
+
readonly text: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: 'bullet';
|
|
21
|
+
readonly text: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: 'table';
|
|
24
|
+
readonly rows: readonly (readonly string[])[];
|
|
25
|
+
};
|
|
26
|
+
export declare function parseMarkdownBlocks(markdown: string): Block[];
|
|
27
|
+
export interface DocxOptions {
|
|
28
|
+
readonly title: string;
|
|
29
|
+
readonly markdown: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 生成完整 .docx 字节。title 渲染为封面页;markdown 为正文(章节合并内容)。
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildDocx(opts: DocxOptions): Uint8Array;
|
|
35
|
+
export {};
|