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,296 @@
|
|
|
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
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { isAbortError } from "../../shared/fs-errors.js";
|
|
15
|
+
const DEFAULT_FETCH = (url, init) => fetch(url, init);
|
|
16
|
+
const MAX_RESULTS = 20;
|
|
17
|
+
const REQUEST_TIMEOUT_MS = 15000;
|
|
18
|
+
function timedFetch(fetchImpl, url, outerSignal) {
|
|
19
|
+
const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
|
|
20
|
+
const signal = outerSignal === undefined ? timeout : AbortSignal.any([outerSignal, timeout]);
|
|
21
|
+
return fetchImpl(url, { signal });
|
|
22
|
+
}
|
|
23
|
+
async function json(fetchImpl, url, outerSignal) {
|
|
24
|
+
const res = await timedFetch(fetchImpl, url, outerSignal);
|
|
25
|
+
if (!res.ok)
|
|
26
|
+
throw new Error(`HTTP ${res.status}`);
|
|
27
|
+
return await res.json();
|
|
28
|
+
}
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// 通用小工具
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
function decodeEntities(text) {
|
|
33
|
+
return text
|
|
34
|
+
.replaceAll('&', '&')
|
|
35
|
+
.replaceAll('<', '<')
|
|
36
|
+
.replaceAll('>', '>')
|
|
37
|
+
.replaceAll('"', '"')
|
|
38
|
+
.replaceAll(''', "'")
|
|
39
|
+
.replaceAll(''', "'")
|
|
40
|
+
.replace(/&#(\d+);/g, (_m, d) => String.fromCodePoint(Number(d)))
|
|
41
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_m, h) => String.fromCodePoint(Number.parseInt(h, 16)));
|
|
42
|
+
}
|
|
43
|
+
function stripTags(text) {
|
|
44
|
+
// 先解码实体(<em> → <em>),再剥标签,最后再解码一次(处理剥标签后残留的实体)。
|
|
45
|
+
const decoded = decodeEntities(text);
|
|
46
|
+
return decodeEntities(decoded.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim());
|
|
47
|
+
}
|
|
48
|
+
function classifyVenue(venue, hasArxivId) {
|
|
49
|
+
const v = venue.toLowerCase();
|
|
50
|
+
if (/journal|transactions|review|letters|magazine|quarterly|acta|annals/.test(v))
|
|
51
|
+
return 'article';
|
|
52
|
+
if (/conference|symposium|workshop|proceedings|meeting|congress/.test(v))
|
|
53
|
+
return 'inproceedings';
|
|
54
|
+
if (hasArxivId)
|
|
55
|
+
return 'misc';
|
|
56
|
+
return 'article';
|
|
57
|
+
}
|
|
58
|
+
function cleanAbstract(raw, max = 600) {
|
|
59
|
+
if (raw === undefined)
|
|
60
|
+
return undefined;
|
|
61
|
+
const text = stripTags(raw).trim();
|
|
62
|
+
if (text === '')
|
|
63
|
+
return undefined;
|
|
64
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
65
|
+
}
|
|
66
|
+
function normalizeTitle(raw) {
|
|
67
|
+
return stripTags(raw ?? '').replace(/\s+/g, ' ').trim();
|
|
68
|
+
}
|
|
69
|
+
async function searchSemanticScholar(fetchImpl, opts) {
|
|
70
|
+
const limit = Math.min(opts.limit ?? 10, MAX_RESULTS);
|
|
71
|
+
const params = new URLSearchParams({ query: opts.query, limit: String(limit), fields: 'title,authors,year,venue,abstract,url,citationCount,externalIds' });
|
|
72
|
+
if (opts.yearFrom !== undefined)
|
|
73
|
+
params.set('year', `${opts.yearFrom}-`);
|
|
74
|
+
const body = await json(fetchImpl, `https://api.semanticscholar.org/graph/v1/paper/search?${params}`, opts.signal);
|
|
75
|
+
return (body.data ?? []).flatMap(item => {
|
|
76
|
+
const hasArxiv = item.externalIds?.ArXiv !== undefined;
|
|
77
|
+
const title = normalizeTitle(item.title);
|
|
78
|
+
if (title === '')
|
|
79
|
+
return [];
|
|
80
|
+
const abstract = cleanAbstract(item.abstract ?? undefined);
|
|
81
|
+
return [{
|
|
82
|
+
id: makeId(item.externalIds?.DOI, item.url ?? (hasArxiv ? `https://arxiv.org/abs/${item.externalIds.ArXiv}` : undefined), title),
|
|
83
|
+
title,
|
|
84
|
+
authors: (item.authors ?? []).map(a => a.name).filter((n) => n !== undefined && n !== ''),
|
|
85
|
+
year: item.year ?? null,
|
|
86
|
+
venue: item.venue ?? '',
|
|
87
|
+
type: classifyVenue(item.venue ?? '', hasArxiv),
|
|
88
|
+
...(item.externalIds?.DOI !== undefined ? { doi: item.externalIds.DOI } : {}),
|
|
89
|
+
...(item.url !== undefined ? { url: item.url } : {}),
|
|
90
|
+
...(abstract !== undefined ? { abstract } : {}),
|
|
91
|
+
...(item.citationCount !== undefined ? { citationCount: item.citationCount } : {}),
|
|
92
|
+
source: 'semantic-scholar',
|
|
93
|
+
}];
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function dblpAuthors(raw) {
|
|
97
|
+
if (raw === undefined)
|
|
98
|
+
return [];
|
|
99
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
100
|
+
return list.map(a => {
|
|
101
|
+
if (typeof a === 'string')
|
|
102
|
+
return a;
|
|
103
|
+
if (a !== null && typeof a === 'object' && 'text' in a && typeof a.text === 'string') {
|
|
104
|
+
return a.text;
|
|
105
|
+
}
|
|
106
|
+
return '';
|
|
107
|
+
}).filter(n => n !== '' && !/^\d{4}(-\d{4}){0,3}$/.test(n.trim()));
|
|
108
|
+
}
|
|
109
|
+
async function searchDblp(fetchImpl, opts) {
|
|
110
|
+
const limit = Math.min(opts.limit ?? 10, MAX_RESULTS);
|
|
111
|
+
const params = new URLSearchParams({ q: opts.query, format: 'json', h: String(limit) });
|
|
112
|
+
const body = await json(fetchImpl, `https://dblp.org/search/publ/api?${params}`, opts.signal);
|
|
113
|
+
const hits = body.result?.hits?.hit;
|
|
114
|
+
const list = hits === undefined ? [] : Array.isArray(hits) ? hits : [hits];
|
|
115
|
+
return list.map(hit => {
|
|
116
|
+
const info = hit.info ?? {};
|
|
117
|
+
const title = normalizeTitle(info.title);
|
|
118
|
+
const year = typeof info.year === 'number' ? info.year : (info.year !== undefined ? Number.parseInt(String(info.year), 10) : Number.NaN);
|
|
119
|
+
const venue = info.venue ?? '';
|
|
120
|
+
// CoRR / arXiv 是预印本,即使 DBLP 按期刊条目收录也标为 misc。
|
|
121
|
+
const isPreprint = /coRR|arxiv/i.test(venue);
|
|
122
|
+
const typeMap = {
|
|
123
|
+
'Conference and Workshop Papers': 'inproceedings',
|
|
124
|
+
'Journal Articles': 'article',
|
|
125
|
+
'Books and Theses': 'book',
|
|
126
|
+
};
|
|
127
|
+
const type = isPreprint ? 'misc' : (typeMap[info.type ?? ''] ?? (info.doi !== undefined ? 'article' : 'inproceedings'));
|
|
128
|
+
const url = info.ee ?? info.url;
|
|
129
|
+
return {
|
|
130
|
+
id: makeId(info.doi, url, title),
|
|
131
|
+
title,
|
|
132
|
+
authors: dblpAuthors(info.authors?.author),
|
|
133
|
+
year: Number.isNaN(year) ? null : year,
|
|
134
|
+
venue,
|
|
135
|
+
type,
|
|
136
|
+
...(info.doi !== undefined ? { doi: info.doi } : {}),
|
|
137
|
+
...(url !== undefined ? { url } : {}),
|
|
138
|
+
source: 'dblp',
|
|
139
|
+
};
|
|
140
|
+
}).filter(r => r.title !== '');
|
|
141
|
+
}
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// 3. arXiv(Atom XML,最小解析器 + fixtures 测试)
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
function parseArxivAtom(xml) {
|
|
146
|
+
const entries = [];
|
|
147
|
+
const entryBlocks = xml.split('<entry').slice(1);
|
|
148
|
+
for (const block of entryBlocks) {
|
|
149
|
+
const grab = (tag) => {
|
|
150
|
+
const m = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`).exec(block);
|
|
151
|
+
return m?.[1] === undefined ? undefined : decodeEntities(stripTags(m[1]));
|
|
152
|
+
};
|
|
153
|
+
const title = grab('title');
|
|
154
|
+
if (title === undefined || title === '')
|
|
155
|
+
continue;
|
|
156
|
+
const idText = grab('id');
|
|
157
|
+
const arxivId = idText?.split('/abs/').pop()?.replace(/v\d+$/, '') ?? '';
|
|
158
|
+
const doiText = grab('doi');
|
|
159
|
+
const published = grab('published') ?? '';
|
|
160
|
+
const names = [...block.matchAll(/<name>([\s\S]*?)<\/name>/g)].map(m => decodeEntities(m[1]?.trim() ?? ''))
|
|
161
|
+
.filter(n => n !== '');
|
|
162
|
+
entries.push({
|
|
163
|
+
title,
|
|
164
|
+
summary: grab('summary') ?? '',
|
|
165
|
+
authors: names,
|
|
166
|
+
arxivId,
|
|
167
|
+
...(doiText !== undefined && doiText !== '' ? { doi: doiText } : {}),
|
|
168
|
+
published,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return entries;
|
|
172
|
+
}
|
|
173
|
+
async function searchArxiv(fetchImpl, opts) {
|
|
174
|
+
const limit = Math.min(opts.limit ?? 10, MAX_RESULTS);
|
|
175
|
+
const query = `all:${encodeURIComponent(opts.query)}`;
|
|
176
|
+
const url = `https://export.arxiv.org/api/query?search_query=${query}&start=0&max_results=${limit}`;
|
|
177
|
+
const res = await timedFetch(fetchImpl, url, opts.signal);
|
|
178
|
+
if (!res.ok)
|
|
179
|
+
throw new Error(`HTTP ${res.status}`);
|
|
180
|
+
const xml = await res.text();
|
|
181
|
+
return parseArxivAtom(xml).flatMap(entry => {
|
|
182
|
+
const yearRaw = Number.parseInt(entry.published.slice(0, 4), 10);
|
|
183
|
+
const year = Number.isNaN(yearRaw) ? null : yearRaw;
|
|
184
|
+
if (opts.yearFrom !== undefined && year !== null && year < opts.yearFrom)
|
|
185
|
+
return [];
|
|
186
|
+
const absUrl = `https://arxiv.org/abs/${entry.arxivId}`;
|
|
187
|
+
const abstract = cleanAbstract(entry.summary);
|
|
188
|
+
return [{
|
|
189
|
+
id: makeId(entry.doi, absUrl, entry.title),
|
|
190
|
+
title: entry.title,
|
|
191
|
+
authors: entry.authors,
|
|
192
|
+
year,
|
|
193
|
+
venue: 'arXiv preprint',
|
|
194
|
+
type: 'misc',
|
|
195
|
+
...(entry.doi !== undefined ? { doi: entry.doi } : {}),
|
|
196
|
+
url: absUrl,
|
|
197
|
+
...(abstract !== undefined ? { abstract } : {}),
|
|
198
|
+
source: 'arxiv',
|
|
199
|
+
}];
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
async function searchCrossref(fetchImpl, opts) {
|
|
203
|
+
const limit = Math.min(opts.limit ?? 10, MAX_RESULTS);
|
|
204
|
+
const params = new URLSearchParams({ query: opts.query, rows: String(limit), select: 'DOI,title,author,issued,container-title,type,abstract,URL' });
|
|
205
|
+
const body = await json(fetchImpl, `https://api.crossref.org/works?${params}`, opts.signal);
|
|
206
|
+
const items = body.message?.items ?? [];
|
|
207
|
+
const typeMap = {
|
|
208
|
+
'journal-article': 'article',
|
|
209
|
+
'proceedings-article': 'inproceedings',
|
|
210
|
+
book: 'book',
|
|
211
|
+
monograph: 'book',
|
|
212
|
+
dissertation: 'phdthesis',
|
|
213
|
+
'posted-content': 'misc',
|
|
214
|
+
};
|
|
215
|
+
return items.flatMap(item => {
|
|
216
|
+
const doi = item.DOI;
|
|
217
|
+
if (doi === undefined)
|
|
218
|
+
return [];
|
|
219
|
+
const title = normalizeTitle(item.title?.[0]);
|
|
220
|
+
const year = item.issued?.['date-parts']?.[0]?.[0] ?? null;
|
|
221
|
+
if (opts.yearFrom !== undefined && year !== null && year < opts.yearFrom)
|
|
222
|
+
return [];
|
|
223
|
+
return [{
|
|
224
|
+
id: makeId(doi, item.URL, title),
|
|
225
|
+
title,
|
|
226
|
+
authors: (item.author ?? []).map(a => a.name ?? [a.given, a.family].filter(Boolean).join(' ').trim()).filter(n => n !== ''),
|
|
227
|
+
year,
|
|
228
|
+
venue: item['container-title']?.[0] ?? '',
|
|
229
|
+
type: typeMap[item.type ?? ''] ?? 'article',
|
|
230
|
+
doi,
|
|
231
|
+
...(item.URL !== undefined ? { url: item.URL } : {}),
|
|
232
|
+
...cleanAbstract(item.abstract) !== undefined ? { abstract: cleanAbstract(item.abstract) } : {},
|
|
233
|
+
source: 'crossref',
|
|
234
|
+
}];
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// 组合检索(降级链)
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
export function makeId(doi, url, title) {
|
|
241
|
+
const basis = (doi ?? url ?? title).trim().toLowerCase();
|
|
242
|
+
return createHash('sha1').update(basis).digest('hex').slice(0, 16);
|
|
243
|
+
}
|
|
244
|
+
const SOURCE_ORDER = ['semantic-scholar', 'dblp', 'arxiv', 'crossref'];
|
|
245
|
+
const SEARCHERS = {
|
|
246
|
+
'semantic-scholar': searchSemanticScholar,
|
|
247
|
+
dblp: searchDblp,
|
|
248
|
+
arxiv: searchArxiv,
|
|
249
|
+
crossref: searchCrossref,
|
|
250
|
+
};
|
|
251
|
+
export const LIT_SOURCES = ['semantic-scholar', 'dblp', 'arxiv', 'crossref'];
|
|
252
|
+
/**
|
|
253
|
+
* 检索文献。source=auto 时按 Semantic Scholar → DBLP → arXiv → Crossref 顺序
|
|
254
|
+
* 降级:某源失败(网络/限流/超时)则尝试下一源;首个返回 ≥1 条结果的源胜出。
|
|
255
|
+
* 返回按年被引/年份排序后的去重列表(重复判定:id 相同)。
|
|
256
|
+
*
|
|
257
|
+
* **取消不是失败**:调用方取消时立刻把 `AbortError` 抛出去,不去试下一个源——
|
|
258
|
+
* 否则用户按了停止,工具会继续打四个学术 API(最多 60 秒),最后报一句
|
|
259
|
+
* 「所有检索源均未返回结果」,把取消误导成网络问题。
|
|
260
|
+
*/
|
|
261
|
+
export async function searchLiterature(fetchImpl, opts) {
|
|
262
|
+
const forced = opts.source !== undefined && opts.source !== 'auto' ? opts.source : undefined;
|
|
263
|
+
if (forced !== undefined && SEARCHERS[forced] === undefined) {
|
|
264
|
+
throw new Error(`未知的检索来源「${forced}」。可用:${LIT_SOURCES.join(' | ')},或省略 source 走降级链(auto)。`);
|
|
265
|
+
}
|
|
266
|
+
const order = forced !== undefined ? [forced] : SOURCE_ORDER;
|
|
267
|
+
const errors = [];
|
|
268
|
+
for (const source of order) {
|
|
269
|
+
try {
|
|
270
|
+
const results = await SEARCHERS[source](fetchImpl, opts);
|
|
271
|
+
if (results.length > 0) {
|
|
272
|
+
return dedupe(results)
|
|
273
|
+
.sort((a, b) => (b.citationCount ?? b.year ?? 0) - (a.citationCount ?? a.year ?? 0));
|
|
274
|
+
}
|
|
275
|
+
errors.push(`${source}: 无结果`);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (isAbortError(error))
|
|
279
|
+
throw error;
|
|
280
|
+
errors.push(`${source}: ${error instanceof Error ? error.message : String(error)}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
throw new Error(`所有检索源均未返回结果。${errors.join(';')}`);
|
|
284
|
+
}
|
|
285
|
+
function dedupe(records) {
|
|
286
|
+
const seen = new Set();
|
|
287
|
+
const out = [];
|
|
288
|
+
for (const record of records) {
|
|
289
|
+
if (seen.has(record.id))
|
|
290
|
+
continue;
|
|
291
|
+
seen.add(record.id);
|
|
292
|
+
out.push(record);
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
export { parseArxivAtom };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文献检索缓存与检索留痕(02-文献/.lit-cache.json 与 02-文献/检索记录.md)。
|
|
3
|
+
*
|
|
4
|
+
* 缓存是"零假文献"的机制保证:thesis_lit_save 只接受缓存中由真实检索
|
|
5
|
+
* 产生的记录 ID,不接受模型转述的元数据。
|
|
6
|
+
*/
|
|
7
|
+
import type { FileSystem } from '@deepseek-ai/dsh-fs';
|
|
8
|
+
import type { LitRecord } from './lit-api.ts';
|
|
9
|
+
export declare const LIT_CACHE_REL = "02-\u6587\u732E/.lit-cache.json";
|
|
10
|
+
export declare const LIT_RECORD_REL = "02-\u6587\u732E/\u68C0\u7D22\u8BB0\u5F55.md";
|
|
11
|
+
export declare const REFS_BIB_REL = "02-\u6587\u732E/refs.bib";
|
|
12
|
+
export declare const NOTES_DIR_REL = "02-\u6587\u732E/\u7B14\u8BB0";
|
|
13
|
+
export interface LitCache {
|
|
14
|
+
readonly version: 1;
|
|
15
|
+
readonly records: Record<string, LitRecord>;
|
|
16
|
+
}
|
|
17
|
+
export declare function emptyCache(): LitCache;
|
|
18
|
+
/**
|
|
19
|
+
* 读取检索缓存。
|
|
20
|
+
*
|
|
21
|
+
* **只有「缓存文件不存在」才返回空缓存**(第一次检索的正常情况)。
|
|
22
|
+
* 其余情况一律抛错:
|
|
23
|
+
* - 读失败(权限 / IO):抛错并说明「缓存存在但读不出来」,否则调用方会用空缓存
|
|
24
|
+
* 覆盖写回,把**全部历史检索记录**抹掉——那些记录是「零假文献」红线的审计证据;
|
|
25
|
+
* - JSON 损坏 / 版本不符:同样抛错,并提示备份后重建。
|
|
26
|
+
*/
|
|
27
|
+
export declare function loadCache(fs: FileSystem, root: string, signal?: AbortSignal): Promise<LitCache>;
|
|
28
|
+
export declare function saveCache(fs: FileSystem, root: string, cache: LitCache, signal?: AbortSignal): Promise<void>;
|
|
29
|
+
export declare function mergeRecords(cache: LitCache, records: readonly LitRecord[]): LitCache;
|
|
30
|
+
export declare function appendLitRecord(fs: FileSystem, root: string, row: string, signal?: AbortSignal): Promise<void>;
|
|
31
|
+
export declare function searchRow(date: string, source: string, query: string, hits: number, ids: readonly string[]): string;
|
|
32
|
+
export declare function saveRow(date: string, source: string, title: string, bibKey: string, doi?: string): string;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文献检索缓存与检索留痕(02-文献/.lit-cache.json 与 02-文献/检索记录.md)。
|
|
3
|
+
*
|
|
4
|
+
* 缓存是"零假文献"的机制保证:thesis_lit_save 只接受缓存中由真实检索
|
|
5
|
+
* 产生的记录 ID,不接受模型转述的元数据。
|
|
6
|
+
*/
|
|
7
|
+
import * as nodePath from 'node:path';
|
|
8
|
+
import { describeReadFailure, isMissingError } from "../../shared/fs-errors.js";
|
|
9
|
+
export const LIT_CACHE_REL = '02-文献/.lit-cache.json';
|
|
10
|
+
export const LIT_RECORD_REL = '02-文献/检索记录.md';
|
|
11
|
+
export const REFS_BIB_REL = '02-文献/refs.bib';
|
|
12
|
+
export const NOTES_DIR_REL = '02-文献/笔记';
|
|
13
|
+
export function emptyCache() {
|
|
14
|
+
return { version: 1, records: {} };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 读取检索缓存。
|
|
18
|
+
*
|
|
19
|
+
* **只有「缓存文件不存在」才返回空缓存**(第一次检索的正常情况)。
|
|
20
|
+
* 其余情况一律抛错:
|
|
21
|
+
* - 读失败(权限 / IO):抛错并说明「缓存存在但读不出来」,否则调用方会用空缓存
|
|
22
|
+
* 覆盖写回,把**全部历史检索记录**抹掉——那些记录是「零假文献」红线的审计证据;
|
|
23
|
+
* - JSON 损坏 / 版本不符:同样抛错,并提示备份后重建。
|
|
24
|
+
*/
|
|
25
|
+
export async function loadCache(fs, root, signal) {
|
|
26
|
+
const path = nodePath.join(root, LIT_CACHE_REL);
|
|
27
|
+
let raw;
|
|
28
|
+
try {
|
|
29
|
+
raw = await fs.readText(await fs.resolve(path, { signal }), signal);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (isMissingError(error))
|
|
33
|
+
return emptyCache();
|
|
34
|
+
throw new Error(describeReadFailure('文献检索缓存', path, error, signal));
|
|
35
|
+
}
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(raw);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new Error(`文献检索缓存不是合法 JSON:${path}(${error instanceof Error ? error.message : String(error)})。`
|
|
42
|
+
+ '为避免把历史检索记录改成空缓存,已停止本次操作。请备份该文件后删除它,再重新检索。');
|
|
43
|
+
}
|
|
44
|
+
if (parsed === null || typeof parsed !== 'object' || parsed.version !== 1
|
|
45
|
+
|| typeof parsed.records !== 'object' || parsed.records === null) {
|
|
46
|
+
throw new Error(`文献检索缓存的版本或结构不符:${path}(期望 version: 1 与 records 对象)。`
|
|
47
|
+
+ '为避免覆盖历史检索记录,已停止本次操作。请备份后删除该文件,再重新检索。');
|
|
48
|
+
}
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
export async function saveCache(fs, root, cache, signal) {
|
|
52
|
+
await fs.writeText(await fs.resolve(nodePath.join(root, LIT_CACHE_REL), { signal }), JSON.stringify(cache, null, 2) + '\n', undefined, signal);
|
|
53
|
+
}
|
|
54
|
+
export function mergeRecords(cache, records) {
|
|
55
|
+
const next = { ...cache.records };
|
|
56
|
+
for (const record of records)
|
|
57
|
+
next[record.id] = record;
|
|
58
|
+
return { version: 1, records: next };
|
|
59
|
+
}
|
|
60
|
+
const RECORD_HEADER = `# 文献检索记录
|
|
61
|
+
|
|
62
|
+
> 每次检索留痕:哪个库、什么关键词、命中多少、收录哪些。
|
|
63
|
+
> 这是"零假文献"的审计证据。
|
|
64
|
+
|
|
65
|
+
| 日期 | 动作 | 数据库 | 关键词/文献 | 命中 | 备注 |
|
|
66
|
+
|---|---|---|---|---|---|
|
|
67
|
+
`;
|
|
68
|
+
export async function appendLitRecord(fs, root, row, signal) {
|
|
69
|
+
const path = nodePath.join(root, LIT_RECORD_REL);
|
|
70
|
+
let raw;
|
|
71
|
+
try {
|
|
72
|
+
raw = await fs.readText(await fs.resolve(path, { signal }), signal);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
// 只有「检索记录还没建过」才用表头开始;读失败必须上抛,否则会抹掉审计证据。
|
|
76
|
+
if (!isMissingError(error))
|
|
77
|
+
throw new Error(describeReadFailure('文献检索记录', path, error, signal));
|
|
78
|
+
raw = RECORD_HEADER;
|
|
79
|
+
}
|
|
80
|
+
if (raw.trim() === '')
|
|
81
|
+
raw = RECORD_HEADER;
|
|
82
|
+
await fs.writeText(await fs.resolve(path, { signal }), `${raw.trimEnd()}\n${row}\n`, undefined, signal);
|
|
83
|
+
}
|
|
84
|
+
export function searchRow(date, source, query, hits, ids) {
|
|
85
|
+
const note = hits > 0 ? `缓存 id:${ids.join('、')}` : '无结果';
|
|
86
|
+
return `| ${date} | 检索 | ${source} | ${query.replaceAll('|', '\\|')} | ${hits} | ${note} |`;
|
|
87
|
+
}
|
|
88
|
+
export function saveRow(date, source, title, bibKey, doi) {
|
|
89
|
+
const note = `收录 → refs.bib [${bibKey}]${doi !== undefined ? `(DOI: ${doi})` : ''}`;
|
|
90
|
+
return `| ${date} | 收录 | ${source} | ${title.replaceAll('|', '\\|')} | 1 | ${note} |`;
|
|
91
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 论文工作区根目录定位:从当前会话 cwd 向上寻找 `00-管理/进度台账.md`,
|
|
3
|
+
* 或使用配置显式指定的工作区根。
|
|
4
|
+
*
|
|
5
|
+
* **为什么向上探测而不是记住路径**:装配发生在进程启动时,那时还没有会话,
|
|
6
|
+
* 也就没有 cwd。向上探测让「在论文仓库里任意子目录操作」都能正确定位根。
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-thesis/paper/lib/project
|
|
9
|
+
*/
|
|
10
|
+
import type { FileSystem } from '@deepseek-ai/dsh-fs';
|
|
11
|
+
export declare const LEDGER_REL = "00-\u7BA1\u7406/\u8FDB\u5EA6\u53F0\u8D26.md";
|
|
12
|
+
export declare const DECISION_REL = "00-\u7BA1\u7406/\u51B3\u5B9A\u65E5\u5FD7.md";
|
|
13
|
+
/** 装配时由 `src/index.ts` 调用;传 `undefined` 表示恢复「向上探测」。 */
|
|
14
|
+
export declare function setWorkspaceRoot(root: string | undefined): void;
|
|
15
|
+
/** 当前生效的显式工作区根(测试与诊断用)。 */
|
|
16
|
+
export declare function currentWorkspaceRoot(): string | undefined;
|
|
17
|
+
/** 工作区未找到时的统一错误文案(工具层直接抛)。 */
|
|
18
|
+
export declare function workspaceNotFoundError(cwd: string | undefined): Error;
|
|
19
|
+
/**
|
|
20
|
+
* 找到论文工作区根;确实找不到时返回 null。
|
|
21
|
+
*
|
|
22
|
+
* 三种情况严格区分:
|
|
23
|
+
* - **显式配置了 workspace**:只认它。台账不存在或读不出来 → 抛错(配置写错了要立刻知道,
|
|
24
|
+
* 绝不能悄悄退回到 cwd 探测,否则工具会写到别的目录去)。
|
|
25
|
+
* - **向上探测**:某层没有台账 → 继续往上;读失败(权限/IO)→ 抛错,
|
|
26
|
+
* 因为「读不出来」不等于「不存在」,静默跳过会让用户以为工作区不存在。
|
|
27
|
+
* - **走到头**:返回 null(调用方给可读错误)。
|
|
28
|
+
*/
|
|
29
|
+
export declare function findThesisRoot(fs: FileSystem, cwd: string | undefined, signal?: AbortSignal): Promise<string | null>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 论文工作区根目录定位:从当前会话 cwd 向上寻找 `00-管理/进度台账.md`,
|
|
3
|
+
* 或使用配置显式指定的工作区根。
|
|
4
|
+
*
|
|
5
|
+
* **为什么向上探测而不是记住路径**:装配发生在进程启动时,那时还没有会话,
|
|
6
|
+
* 也就没有 cwd。向上探测让「在论文仓库里任意子目录操作」都能正确定位根。
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-thesis/paper/lib/project
|
|
9
|
+
*/
|
|
10
|
+
import * as nodePath from 'node:path';
|
|
11
|
+
import { isAbortError, isMissingError } from "../../shared/fs-errors.js";
|
|
12
|
+
export const LEDGER_REL = '00-管理/进度台账.md';
|
|
13
|
+
export const DECISION_REL = '00-管理/决定日志.md';
|
|
14
|
+
/** 向上最多探测的层级数(防止在文件系统根上无意义循环)。 */
|
|
15
|
+
const MAX_UP = 8;
|
|
16
|
+
/**
|
|
17
|
+
* 配置显式指定的论文工作区根。
|
|
18
|
+
*
|
|
19
|
+
* 语义是**部署级**的(一个插件装载对应一个论文工作区),因此用模块级持有:
|
|
20
|
+
* 装配时由 {@link setWorkspaceRoot} 写入,工具执行时读取。这样做而不把
|
|
21
|
+
* workspace 参数穿透到十几个调用点,是因为宿主在进程启动时装配、之后才执行
|
|
22
|
+
* 工具,时序上不存在竞态;单用户单工作区是插件的目标场景。
|
|
23
|
+
*/
|
|
24
|
+
let configuredRoot;
|
|
25
|
+
/** 装配时由 `src/index.ts` 调用;传 `undefined` 表示恢复「向上探测」。 */
|
|
26
|
+
export function setWorkspaceRoot(root) {
|
|
27
|
+
configuredRoot = root === '' ? undefined : root;
|
|
28
|
+
}
|
|
29
|
+
/** 当前生效的显式工作区根(测试与诊断用)。 */
|
|
30
|
+
export function currentWorkspaceRoot() {
|
|
31
|
+
return configuredRoot;
|
|
32
|
+
}
|
|
33
|
+
/** 工作区未找到时的统一错误文案(工具层直接抛)。 */
|
|
34
|
+
export function workspaceNotFoundError(cwd) {
|
|
35
|
+
const where = cwd === undefined || cwd === '' ? '(本次调用没有会话工作目录)' : `(自 ${cwd} 向上查找 ${LEDGER_REL} 失败)`;
|
|
36
|
+
return new Error(`未找到论文工作区${where}。请先用 thesis_init 创建工作区后在论文目录内操作;`
|
|
37
|
+
+ '若工作区在别处,可在插件配置里用 `workspace` 指定它的绝对路径。');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 找到论文工作区根;确实找不到时返回 null。
|
|
41
|
+
*
|
|
42
|
+
* 三种情况严格区分:
|
|
43
|
+
* - **显式配置了 workspace**:只认它。台账不存在或读不出来 → 抛错(配置写错了要立刻知道,
|
|
44
|
+
* 绝不能悄悄退回到 cwd 探测,否则工具会写到别的目录去)。
|
|
45
|
+
* - **向上探测**:某层没有台账 → 继续往上;读失败(权限/IO)→ 抛错,
|
|
46
|
+
* 因为「读不出来」不等于「不存在」,静默跳过会让用户以为工作区不存在。
|
|
47
|
+
* - **走到头**:返回 null(调用方给可读错误)。
|
|
48
|
+
*/
|
|
49
|
+
export async function findThesisRoot(fs, cwd, signal) {
|
|
50
|
+
if (configuredRoot !== undefined) {
|
|
51
|
+
const root = nodePath.resolve(configuredRoot);
|
|
52
|
+
const ledger = nodePath.join(root, LEDGER_REL);
|
|
53
|
+
try {
|
|
54
|
+
await fs.readText(await fs.resolve(ledger, { signal }), signal);
|
|
55
|
+
return root;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (isAbortError(error))
|
|
59
|
+
throw error;
|
|
60
|
+
if (isMissingError(error)) {
|
|
61
|
+
throw new Error(`配置的 workspace 里没有 ${LEDGER_REL}:${root}。`
|
|
62
|
+
+ '该路径不是 dsh-thesis 论文工作区(或工作区尚未创建);请修正配置或先用 thesis_init 建库。');
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`配置的 workspace 里的 ${LEDGER_REL} 存在但读不出来:${ledger}`
|
|
65
|
+
+ `(${error instanceof Error ? error.message : String(error)})。请检查文件权限后重试。`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (cwd === undefined || cwd === '')
|
|
69
|
+
return null;
|
|
70
|
+
let dir = nodePath.resolve(cwd);
|
|
71
|
+
for (let i = 0; i <= MAX_UP; i += 1) {
|
|
72
|
+
try {
|
|
73
|
+
await fs.readText(await fs.resolve(nodePath.join(dir, LEDGER_REL), { signal }), signal);
|
|
74
|
+
return dir;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// 取消必须原样抛出:否则用户按了停止,探测还会继续往上走 8 层。
|
|
78
|
+
if (isAbortError(error))
|
|
79
|
+
throw error;
|
|
80
|
+
if (!isMissingError(error)) {
|
|
81
|
+
throw new Error(`读取 ${nodePath.join(dir, LEDGER_REL)} 失败`
|
|
82
|
+
+ `(${error instanceof Error ? error.message : String(error)})。`
|
|
83
|
+
+ '这不是「工作区不存在」,而是读取本身出错;为避免写错目录,已停止本次操作。');
|
|
84
|
+
}
|
|
85
|
+
// 该层确实没有台账,继续向上。
|
|
86
|
+
}
|
|
87
|
+
const parent = nodePath.dirname(dir);
|
|
88
|
+
if (parent === dir)
|
|
89
|
+
break;
|
|
90
|
+
dir = parent;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 章节确定性评审(thesis_review 的核心)。
|
|
3
|
+
*
|
|
4
|
+
* 六个可执行检查,全部基于真实文件内容,不做任何"感觉式"判断:
|
|
5
|
+
* 1. 字数:正文 CJK 字符数 vs 该章目标区间
|
|
6
|
+
* 2. 大纲:模板要求的全部小节标题是否齐全
|
|
7
|
+
* 3. 引用:文中引用编号是否都能对应 refs.bib 中的条目(顺序编码制)
|
|
8
|
+
* 4. 图表:图/表编号章号正确、序号从 1 连续
|
|
9
|
+
* 5. 未完成标记:待补/TODO/尖括号占位等残留
|
|
10
|
+
* 6. G2 清单:章节模板内嵌验收清单的勾选状态
|
|
11
|
+
*
|
|
12
|
+
* 语义质量(论证是否充分、逻辑是否连贯)由 AI 依据 thesis-writing 技能评审,
|
|
13
|
+
* 本工具只负责"可被机器证明"的部分——报告与清单一起构成 G2 验收依据。
|
|
14
|
+
*/
|
|
15
|
+
import type { ChapterMeta } from './layout.ts';
|
|
16
|
+
export interface ReviewFinding {
|
|
17
|
+
readonly check: string;
|
|
18
|
+
readonly ok: boolean;
|
|
19
|
+
readonly detail: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ReviewInput {
|
|
22
|
+
readonly chapterText: string;
|
|
23
|
+
readonly bibText: string;
|
|
24
|
+
readonly meta: ChapterMeta;
|
|
25
|
+
readonly chapterNo: number;
|
|
26
|
+
}
|
|
27
|
+
/** 图/表编号:第 n 章的编号必须是 n-k,k 从 1 连续,且不得出现其它章编号。 */
|
|
28
|
+
export declare function checkNumberedObjects(text: string, chapterNo: number, kind: '图' | '表'): ReviewFinding;
|
|
29
|
+
export interface ReviewReport {
|
|
30
|
+
readonly chapter: string;
|
|
31
|
+
readonly chapterNo: number;
|
|
32
|
+
readonly title: string;
|
|
33
|
+
readonly findings: readonly ReviewFinding[];
|
|
34
|
+
readonly allOk: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare function reviewChapter(input: ReviewInput): ReviewReport;
|
|
37
|
+
export declare function renderReviewReport(report: ReviewReport): string;
|