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,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 降重的核心度量:字符 n-gram 指纹(shingle)、倒排索引候选剪枝、
|
|
3
|
+
* 包含率/杰卡德/最长公共子串(含起止偏移)。全部为纯函数、无随机。
|
|
4
|
+
*
|
|
5
|
+
* 口径说明(必须如实写进报告):
|
|
6
|
+
* - 指纹:归一化后按**字符**滑动窗口取 n 元组(`n = options.shingle`,
|
|
7
|
+
* 中文 4 字、拉丁按 4 字符近似词),相同的 shingle 用稳定哈希比较。
|
|
8
|
+
* - `containment`:本段 shingle 出现在对方中的比例 —— 回答"这段有多少是抄来的";
|
|
9
|
+
* `jaccard`:双方 shingle 的交集/并集 —— 对长文档更保守。
|
|
10
|
+
* - 命中定位:最长公共子串(归一化字符坐标),并回溯到原文偏移。
|
|
11
|
+
* - 本模块**不是**知网/维普类系统:它只在给定语料内做字符串重合度估算,
|
|
12
|
+
* 不能给出学校检测系统的重复率,也不接入任何收费库。
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-thesis/dedup
|
|
15
|
+
*/
|
|
16
|
+
import type { NormalizedText, Segment } from './normalize.ts';
|
|
17
|
+
/** 带指纹的归一化文本(`chars` 是归一化字符数,索引用 `starts` 定位)。 */
|
|
18
|
+
export interface FingerprintedText {
|
|
19
|
+
readonly text: string;
|
|
20
|
+
readonly chars: number;
|
|
21
|
+
/** shingle 哈希集合。 */
|
|
22
|
+
readonly shingles: ReadonlySet<number>;
|
|
23
|
+
/** 第 i 个 shingle 在归一化字符坐标中的起点。 */
|
|
24
|
+
readonly starts: readonly number[];
|
|
25
|
+
}
|
|
26
|
+
export interface ScoreOptions {
|
|
27
|
+
/** shingle 窗口(字符数)。 */
|
|
28
|
+
readonly shingle: number;
|
|
29
|
+
/** 命中片段的最短长度(归一化字符数),缺省 `2 × shingle`。 */
|
|
30
|
+
readonly minRun?: number;
|
|
31
|
+
/** 最多返回多少条命中片段(按长度降序),缺省 5。 */
|
|
32
|
+
readonly maxSpans?: number;
|
|
33
|
+
}
|
|
34
|
+
/** 一条命中的连续片段(坐标是归一化字符坐标,`aStart`/`bStart` 对应各自文本)。 */
|
|
35
|
+
export interface MatchSpan {
|
|
36
|
+
readonly length: number;
|
|
37
|
+
readonly aStart: number;
|
|
38
|
+
readonly aEnd: number;
|
|
39
|
+
readonly bStart: number;
|
|
40
|
+
readonly bEnd: number;
|
|
41
|
+
}
|
|
42
|
+
export interface CompareResult {
|
|
43
|
+
/** 综合得分:取 `containment`(本次实现保证 `score === containment`)。 */
|
|
44
|
+
readonly score: number;
|
|
45
|
+
readonly containment: number;
|
|
46
|
+
readonly jaccard: number;
|
|
47
|
+
/** 最长公共子串长度(归一化字符数)。 */
|
|
48
|
+
readonly longestRun: number;
|
|
49
|
+
readonly spans: readonly MatchSpan[];
|
|
50
|
+
}
|
|
51
|
+
/** 语料侧指纹计数(正文字符数口径,与段落长度无关)。 */
|
|
52
|
+
export interface CorpusDoc {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly label: string;
|
|
55
|
+
readonly shingle: FingerprintedText;
|
|
56
|
+
/** 全局语料文档序号(用于倒排索引)。 */
|
|
57
|
+
readonly index: number;
|
|
58
|
+
}
|
|
59
|
+
export interface CorpusIndex {
|
|
60
|
+
readonly docs: readonly CorpusDoc[];
|
|
61
|
+
/** shingle 哈希 → 语料文档序号列表。 */
|
|
62
|
+
readonly inverted: ReadonlyMap<number, readonly number[]>;
|
|
63
|
+
}
|
|
64
|
+
export interface CorpusHit {
|
|
65
|
+
readonly id: string;
|
|
66
|
+
readonly label: string;
|
|
67
|
+
readonly score: number;
|
|
68
|
+
readonly containment: number;
|
|
69
|
+
readonly jaccard: number;
|
|
70
|
+
readonly longestRun: number;
|
|
71
|
+
readonly spans: readonly CorpusSpan[];
|
|
72
|
+
}
|
|
73
|
+
/** 命中片段 + 回溯到原文的片段文本与行号。 */
|
|
74
|
+
export interface CorpusSpan {
|
|
75
|
+
readonly length: number;
|
|
76
|
+
/** 本段原文中的片段(自 `segment.text` 切出)。 */
|
|
77
|
+
readonly text: string;
|
|
78
|
+
/** 片段在 `segment.text` 中的起止偏移(`[start, end)`)。 */
|
|
79
|
+
readonly start: number;
|
|
80
|
+
readonly end: number;
|
|
81
|
+
readonly lineStart: number;
|
|
82
|
+
readonly lineEnd: number;
|
|
83
|
+
}
|
|
84
|
+
export interface RiskItem {
|
|
85
|
+
readonly segment: Segment;
|
|
86
|
+
readonly best: CorpusHit;
|
|
87
|
+
/** 该段的全部候选命中(按得分降序,含 best)。 */
|
|
88
|
+
readonly hits: readonly CorpusHit[];
|
|
89
|
+
}
|
|
90
|
+
export interface ScanOptions extends ScoreOptions {
|
|
91
|
+
/** 判定高风险的包含率阈值。 */
|
|
92
|
+
readonly threshold: number;
|
|
93
|
+
}
|
|
94
|
+
export interface ScanResult {
|
|
95
|
+
readonly items: readonly RiskItem[];
|
|
96
|
+
/** 参与比对的正文段落数。 */
|
|
97
|
+
readonly paragraphCount: number;
|
|
98
|
+
/** 参与比对段落的归一化字符总数(加权重复率的分母)。 */
|
|
99
|
+
readonly totalChars: number;
|
|
100
|
+
readonly corpusCount: number;
|
|
101
|
+
/** 有任一非零命中的段落数。 */
|
|
102
|
+
readonly matchedCount: number;
|
|
103
|
+
readonly high: number;
|
|
104
|
+
readonly medium: number;
|
|
105
|
+
readonly low: number;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 稳定 32 位哈希(FNV-1a 变体)。自行实现而不依赖 node:crypto,
|
|
109
|
+
* 保证"同一输入 → 同一哈希",跨语言/跨版本可复现。
|
|
110
|
+
*/
|
|
111
|
+
export declare function hashShingle(text: string): number;
|
|
112
|
+
/**
|
|
113
|
+
* 对**已归一化**的文本取字符 n-gram 指纹。
|
|
114
|
+
* 文本短于窗口时退化为「整串一个 shingle」,使 `containment` 仍有定义。
|
|
115
|
+
*/
|
|
116
|
+
export declare function fingerprintText(doc: NormalizedText, shingle: number): FingerprintedText;
|
|
117
|
+
/** 段落 → 指纹。 */
|
|
118
|
+
export declare function fingerprintSegment(segment: Segment, shingle: number): FingerprintedText;
|
|
119
|
+
/** 原始文本 → 指纹(内部先归一化,便于直接比较任意字符串)。 */
|
|
120
|
+
export declare function fingerprintOf(text: string, shingle: number): FingerprintedText;
|
|
121
|
+
/** 两个集合的交集大小(迭代较小者)。 */
|
|
122
|
+
export declare function intersectionSize(a: ReadonlySet<number>, b: ReadonlySet<number>): number;
|
|
123
|
+
/** 用语料建立倒排索引:shingle → 语料文档序号列表。 */
|
|
124
|
+
export declare function buildIndex(docs: readonly CorpusDoc[]): CorpusIndex;
|
|
125
|
+
/**
|
|
126
|
+
* 找出全部足够长的公共片段(按长度降序,去掉被更长片段覆盖的短片段)。
|
|
127
|
+
* 同长度片段按 (aStart, bStart) 稳定排序,保证可复现。
|
|
128
|
+
*/
|
|
129
|
+
export declare function matchSpans(a: FingerprintedText, b: FingerprintedText, options: {
|
|
130
|
+
shingle: number;
|
|
131
|
+
minRun?: number;
|
|
132
|
+
maxSpans?: number;
|
|
133
|
+
}): MatchSpan[];
|
|
134
|
+
/** 比较两份指纹:包含率、杰卡德、最长公共子串与命中片段。 */
|
|
135
|
+
export declare function compareFingerprints(a: FingerprintedText, b: FingerprintedText, options: {
|
|
136
|
+
shingle: number;
|
|
137
|
+
minRun?: number;
|
|
138
|
+
maxSpans?: number;
|
|
139
|
+
}): CompareResult;
|
|
140
|
+
/**
|
|
141
|
+
* 比较两段文本。返回 `{ score, containment, jaccard, longestRun, spans }`;
|
|
142
|
+
* `spans` 的坐标是**归一化字符坐标**,调用方可配合 `offsetsFor` 回到原文。
|
|
143
|
+
*/
|
|
144
|
+
export declare function compareText(a: string, b: string, options: ScoreOptions): CompareResult;
|
|
145
|
+
/** 可选的两两朴素比对(结果必须与倒排索引一致,供等价性测试与排查使用)。 */
|
|
146
|
+
export type CandidateMode = 'index' | 'all';
|
|
147
|
+
export interface ScanInput {
|
|
148
|
+
readonly segments: readonly Segment[];
|
|
149
|
+
readonly corpus: readonly {
|
|
150
|
+
readonly id: string;
|
|
151
|
+
readonly label: string;
|
|
152
|
+
readonly text: string;
|
|
153
|
+
}[];
|
|
154
|
+
readonly options: ScanOptions;
|
|
155
|
+
/** 候选策略:index(默认,倒排剪枝)| all(与全语料两两比对)。 */
|
|
156
|
+
readonly candidateMode?: CandidateMode;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 扫描正文段落:每段给出最佳命中(来源 id、得分、命中片段偏移与文本)。
|
|
160
|
+
*
|
|
161
|
+
* 候选剪枝:先用倒排索引筛出与本段有任何共同 shingle 的语料文档;
|
|
162
|
+
* 没有任何共同 shingle 的文档,其 `containment` 必然为 0(本段没有被它覆盖),
|
|
163
|
+
* 因此不会被选为最佳命中 —— 这正是剪枝不改变结果的原因。
|
|
164
|
+
*
|
|
165
|
+
* **自比屏蔽**:语料文档 id 与本段 `file` 相同的一律跳过。默认语料含各章正文
|
|
166
|
+
* (正文互查),若不跳过,段落与它所在的那一章必然 100% 重合——那是"自己跟
|
|
167
|
+
* 自己比",不是重复。判断按文件路径做,两种候选策略下行为一致。
|
|
168
|
+
*/
|
|
169
|
+
export declare function scan(input: ScanInput): ScanResult;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 降重的核心度量:字符 n-gram 指纹(shingle)、倒排索引候选剪枝、
|
|
3
|
+
* 包含率/杰卡德/最长公共子串(含起止偏移)。全部为纯函数、无随机。
|
|
4
|
+
*
|
|
5
|
+
* 口径说明(必须如实写进报告):
|
|
6
|
+
* - 指纹:归一化后按**字符**滑动窗口取 n 元组(`n = options.shingle`,
|
|
7
|
+
* 中文 4 字、拉丁按 4 字符近似词),相同的 shingle 用稳定哈希比较。
|
|
8
|
+
* - `containment`:本段 shingle 出现在对方中的比例 —— 回答"这段有多少是抄来的";
|
|
9
|
+
* `jaccard`:双方 shingle 的交集/并集 —— 对长文档更保守。
|
|
10
|
+
* - 命中定位:最长公共子串(归一化字符坐标),并回溯到原文偏移。
|
|
11
|
+
* - 本模块**不是**知网/维普类系统:它只在给定语料内做字符串重合度估算,
|
|
12
|
+
* 不能给出学校检测系统的重复率,也不接入任何收费库。
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-thesis/dedup
|
|
15
|
+
*/
|
|
16
|
+
import { normalizeText, offsetsFor, sliceRaw } from "./normalize.js";
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// 指纹
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* 稳定 32 位哈希(FNV-1a 变体)。自行实现而不依赖 node:crypto,
|
|
22
|
+
* 保证"同一输入 → 同一哈希",跨语言/跨版本可复现。
|
|
23
|
+
*/
|
|
24
|
+
export function hashShingle(text) {
|
|
25
|
+
let hash = 0x811C9DC5;
|
|
26
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
27
|
+
hash ^= text.charCodeAt(i);
|
|
28
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
29
|
+
}
|
|
30
|
+
return hash >>> 0;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 对**已归一化**的文本取字符 n-gram 指纹。
|
|
34
|
+
* 文本短于窗口时退化为「整串一个 shingle」,使 `containment` 仍有定义。
|
|
35
|
+
*/
|
|
36
|
+
export function fingerprintText(doc, shingle) {
|
|
37
|
+
const n = Math.max(1, Math.floor(shingle));
|
|
38
|
+
const chars = doc.chars.map(c => c.ch);
|
|
39
|
+
const shingles = new Set();
|
|
40
|
+
const starts = [];
|
|
41
|
+
if (chars.length === 0) {
|
|
42
|
+
return { text: doc.text, chars: 0, shingles, starts };
|
|
43
|
+
}
|
|
44
|
+
if (chars.length <= n) {
|
|
45
|
+
shingles.add(hashShingle(chars.join('')));
|
|
46
|
+
starts.push(0);
|
|
47
|
+
return { text: doc.text, chars: chars.length, shingles, starts };
|
|
48
|
+
}
|
|
49
|
+
for (let i = 0; i + n <= chars.length; i += 1) {
|
|
50
|
+
const shingleText = chars.slice(i, i + n).join('');
|
|
51
|
+
const hash = hashShingle(shingleText);
|
|
52
|
+
if (!shingles.has(hash)) {
|
|
53
|
+
shingles.add(hash);
|
|
54
|
+
starts.push(i);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { text: doc.text, chars: chars.length, shingles, starts };
|
|
58
|
+
}
|
|
59
|
+
/** 段落 → 指纹。 */
|
|
60
|
+
export function fingerprintSegment(segment, shingle) {
|
|
61
|
+
return fingerprintText(segment.doc, shingle);
|
|
62
|
+
}
|
|
63
|
+
/** 原始文本 → 指纹(内部先归一化,便于直接比较任意字符串)。 */
|
|
64
|
+
export function fingerprintOf(text, shingle) {
|
|
65
|
+
return fingerprintText(normalizeText(text), shingle);
|
|
66
|
+
}
|
|
67
|
+
/** 两个集合的交集大小(迭代较小者)。 */
|
|
68
|
+
export function intersectionSize(a, b) {
|
|
69
|
+
const [small, large] = a.size <= b.size ? [a, b] : [b, a];
|
|
70
|
+
let count = 0;
|
|
71
|
+
for (const value of small) {
|
|
72
|
+
if (large.has(value))
|
|
73
|
+
count += 1;
|
|
74
|
+
}
|
|
75
|
+
return count;
|
|
76
|
+
}
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// 倒排索引
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
/** 用语料建立倒排索引:shingle → 语料文档序号列表。 */
|
|
81
|
+
export function buildIndex(docs) {
|
|
82
|
+
const inverted = new Map();
|
|
83
|
+
for (const doc of docs) {
|
|
84
|
+
for (const shingle of doc.shingle.shingles) {
|
|
85
|
+
const list = inverted.get(shingle);
|
|
86
|
+
if (list === undefined) {
|
|
87
|
+
inverted.set(shingle, [doc.index]);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
list.push(doc.index);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { docs, inverted };
|
|
95
|
+
}
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// 度量
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
/**
|
|
100
|
+
* 从共同 shingle 出发向两侧扩展,求最长的连续公共子串(片段起点是区间起点)。
|
|
101
|
+
* 复杂度 O(片段长度),用于把「有重合」细化成「抄了哪一段」。
|
|
102
|
+
*/
|
|
103
|
+
function expand(a, b, i, j) {
|
|
104
|
+
let aStart = i;
|
|
105
|
+
let bStart = j;
|
|
106
|
+
while (aStart > 0 && bStart > 0 && a[aStart - 1] === b[bStart - 1]) {
|
|
107
|
+
aStart -= 1;
|
|
108
|
+
bStart -= 1;
|
|
109
|
+
}
|
|
110
|
+
let length = 0;
|
|
111
|
+
let x = aStart;
|
|
112
|
+
let y = bStart;
|
|
113
|
+
while (x < a.length && y < b.length && a[x] === b[y]) {
|
|
114
|
+
x += 1;
|
|
115
|
+
y += 1;
|
|
116
|
+
length += 1;
|
|
117
|
+
}
|
|
118
|
+
return { length, aStart, bStart };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 找出全部足够长的公共片段(按长度降序,去掉被更长片段覆盖的短片段)。
|
|
122
|
+
* 同长度片段按 (aStart, bStart) 稳定排序,保证可复现。
|
|
123
|
+
*/
|
|
124
|
+
export function matchSpans(a, b, options) {
|
|
125
|
+
const minRun = Math.max(1, Math.floor(options.minRun ?? Math.max(2, options.shingle * 2)));
|
|
126
|
+
const maxSpans = Math.max(1, Math.floor(options.maxSpans ?? 5));
|
|
127
|
+
const aChars = a.text;
|
|
128
|
+
const bChars = b.text;
|
|
129
|
+
if (aChars.length === 0 || bChars.length === 0)
|
|
130
|
+
return [];
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
const spans = [];
|
|
133
|
+
const narrow = a.shingles.size <= b.shingles.size ? a : b;
|
|
134
|
+
for (const start of narrow.starts) {
|
|
135
|
+
const probe = narrow.text.slice(start, start + Math.max(1, options.shingle));
|
|
136
|
+
const other = narrow === a ? b : a;
|
|
137
|
+
let from = 0;
|
|
138
|
+
for (;;) {
|
|
139
|
+
const at = other.text.indexOf(probe, from);
|
|
140
|
+
if (at < 0)
|
|
141
|
+
break;
|
|
142
|
+
const aIdx = narrow === a ? start : at;
|
|
143
|
+
const bIdx = narrow === a ? at : start;
|
|
144
|
+
const grown = expand(aChars, bChars, aIdx, bIdx);
|
|
145
|
+
from = at + 1;
|
|
146
|
+
if (grown.length < minRun)
|
|
147
|
+
continue;
|
|
148
|
+
const key = `${grown.aStart}:${grown.bStart}:${grown.length}`;
|
|
149
|
+
if (seen.has(key))
|
|
150
|
+
continue;
|
|
151
|
+
seen.add(key);
|
|
152
|
+
spans.push({
|
|
153
|
+
length: grown.length,
|
|
154
|
+
aStart: grown.aStart,
|
|
155
|
+
aEnd: grown.aStart + grown.length,
|
|
156
|
+
bStart: grown.bStart,
|
|
157
|
+
bEnd: grown.bStart + grown.length,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
spans.sort((x, y) => y.length - x.length || x.aStart - y.aStart || x.bStart - y.bStart);
|
|
162
|
+
const kept = [];
|
|
163
|
+
for (const span of spans) {
|
|
164
|
+
if (kept.length >= maxSpans)
|
|
165
|
+
break;
|
|
166
|
+
const covered = kept.some(k => k.aStart <= span.aStart && k.bStart <= span.bStart
|
|
167
|
+
&& k.aStart + k.length >= span.aStart + span.length
|
|
168
|
+
&& k.bStart + k.length >= span.bStart + span.length);
|
|
169
|
+
if (!covered)
|
|
170
|
+
kept.push(span);
|
|
171
|
+
}
|
|
172
|
+
return kept;
|
|
173
|
+
}
|
|
174
|
+
/** 比较两份指纹:包含率、杰卡德、最长公共子串与命中片段。 */
|
|
175
|
+
export function compareFingerprints(a, b, options) {
|
|
176
|
+
const intersection = intersectionSize(a.shingles, b.shingles);
|
|
177
|
+
const containment = a.shingles.size === 0 ? 0 : intersection / a.shingles.size;
|
|
178
|
+
const union = a.shingles.size + b.shingles.size - intersection;
|
|
179
|
+
const jaccard = union === 0 ? 0 : intersection / union;
|
|
180
|
+
const spans = matchSpans(a, b, options);
|
|
181
|
+
const longestRun = spans.length > 0 ? spans[0].length : 0;
|
|
182
|
+
return { score: containment, containment, jaccard, longestRun, spans };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* 比较两段文本。返回 `{ score, containment, jaccard, longestRun, spans }`;
|
|
186
|
+
* `spans` 的坐标是**归一化字符坐标**,调用方可配合 `offsetsFor` 回到原文。
|
|
187
|
+
*/
|
|
188
|
+
export function compareText(a, b, options) {
|
|
189
|
+
const fa = fingerprintOf(a, options.shingle);
|
|
190
|
+
const fb = fingerprintOf(b, options.shingle);
|
|
191
|
+
const scoreOptions = { shingle: options.shingle };
|
|
192
|
+
if (options.minRun !== undefined)
|
|
193
|
+
scoreOptions.minRun = options.minRun;
|
|
194
|
+
if (options.maxSpans !== undefined)
|
|
195
|
+
scoreOptions.maxSpans = options.maxSpans;
|
|
196
|
+
return compareFingerprints(fa, fb, scoreOptions);
|
|
197
|
+
}
|
|
198
|
+
function documentFor(text, shingle) {
|
|
199
|
+
return fingerprintOf(text, shingle);
|
|
200
|
+
}
|
|
201
|
+
function spansFor(segment, result) {
|
|
202
|
+
const spans = [];
|
|
203
|
+
for (const span of result.spans) {
|
|
204
|
+
const mapped = offsetsFor(segment.doc, span.aStart, span.aEnd);
|
|
205
|
+
if (mapped === null)
|
|
206
|
+
continue;
|
|
207
|
+
const text = sliceRaw(segment.text, mapped.start, mapped.end);
|
|
208
|
+
spans.push({
|
|
209
|
+
length: span.length,
|
|
210
|
+
text,
|
|
211
|
+
start: mapped.start,
|
|
212
|
+
end: mapped.end,
|
|
213
|
+
lineStart: lineAtSafe(segment, mapped.start),
|
|
214
|
+
lineEnd: lineAtSafe(segment, mapped.end),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return spans;
|
|
218
|
+
}
|
|
219
|
+
function lineAtSafe(segment, offset) {
|
|
220
|
+
const clamped = Math.max(0, Math.min(segment.text.length, offset));
|
|
221
|
+
let line = segment.lineStart;
|
|
222
|
+
for (let i = 0; i < clamped; i += 1) {
|
|
223
|
+
if (segment.text[i] === '\n')
|
|
224
|
+
line += 1;
|
|
225
|
+
}
|
|
226
|
+
return line;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* 扫描正文段落:每段给出最佳命中(来源 id、得分、命中片段偏移与文本)。
|
|
230
|
+
*
|
|
231
|
+
* 候选剪枝:先用倒排索引筛出与本段有任何共同 shingle 的语料文档;
|
|
232
|
+
* 没有任何共同 shingle 的文档,其 `containment` 必然为 0(本段没有被它覆盖),
|
|
233
|
+
* 因此不会被选为最佳命中 —— 这正是剪枝不改变结果的原因。
|
|
234
|
+
*
|
|
235
|
+
* **自比屏蔽**:语料文档 id 与本段 `file` 相同的一律跳过。默认语料含各章正文
|
|
236
|
+
* (正文互查),若不跳过,段落与它所在的那一章必然 100% 重合——那是"自己跟
|
|
237
|
+
* 自己比",不是重复。判断按文件路径做,两种候选策略下行为一致。
|
|
238
|
+
*/
|
|
239
|
+
export function scan(input) {
|
|
240
|
+
const options = input.options;
|
|
241
|
+
const corpusDocs = input.corpus.map((entry, index) => ({
|
|
242
|
+
id: entry.id,
|
|
243
|
+
label: entry.label,
|
|
244
|
+
shingle: documentFor(entry.text, options.shingle),
|
|
245
|
+
index,
|
|
246
|
+
}));
|
|
247
|
+
const corpusIndex = buildIndex(corpusDocs);
|
|
248
|
+
const items = [];
|
|
249
|
+
let high = 0;
|
|
250
|
+
let medium = 0;
|
|
251
|
+
let low = 0;
|
|
252
|
+
let matched = 0;
|
|
253
|
+
let totalChars = 0;
|
|
254
|
+
for (const segment of input.segments) {
|
|
255
|
+
totalChars += segment.doc.chars.length;
|
|
256
|
+
const target = fingerprintSegment(segment, options.shingle);
|
|
257
|
+
const candidates = candidateIds(target, corpusIndex, input.candidateMode ?? 'index');
|
|
258
|
+
const selfId = segment.file.replace(/\\/g, '/');
|
|
259
|
+
const hits = [];
|
|
260
|
+
for (const candidate of candidates) {
|
|
261
|
+
const doc = corpusDocs[candidate];
|
|
262
|
+
if (doc.id.replace(/\\/g, '/') === selfId)
|
|
263
|
+
continue;
|
|
264
|
+
const result = compareFingerprints(target, doc.shingle, {
|
|
265
|
+
shingle: options.shingle,
|
|
266
|
+
...(options.minRun !== undefined ? { minRun: options.minRun } : {}),
|
|
267
|
+
...(options.maxSpans !== undefined ? { maxSpans: options.maxSpans } : {}),
|
|
268
|
+
});
|
|
269
|
+
if (result.score <= 0)
|
|
270
|
+
continue;
|
|
271
|
+
hits.push({
|
|
272
|
+
id: doc.id,
|
|
273
|
+
label: doc.label,
|
|
274
|
+
score: result.score,
|
|
275
|
+
containment: result.containment,
|
|
276
|
+
jaccard: result.jaccard,
|
|
277
|
+
longestRun: result.longestRun,
|
|
278
|
+
spans: spansFor(segment, result),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
hits.sort((x, y) => y.score - x.score || y.longestRun - x.longestRun || (x.id < y.id ? -1 : x.id > y.id ? 1 : 0));
|
|
282
|
+
const best = hits[0];
|
|
283
|
+
if (best === undefined) {
|
|
284
|
+
low += 1;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
matched += 1;
|
|
288
|
+
if (best.score >= options.threshold)
|
|
289
|
+
high += 1;
|
|
290
|
+
else if (best.score >= options.threshold * 0.6)
|
|
291
|
+
medium += 1;
|
|
292
|
+
else
|
|
293
|
+
low += 1;
|
|
294
|
+
items.push({ segment, best, hits });
|
|
295
|
+
}
|
|
296
|
+
items.sort((x, y) => y.best.score - x.best.score
|
|
297
|
+
|| y.best.longestRun - x.best.longestRun
|
|
298
|
+
|| (x.segment.file < y.segment.file ? -1 : x.segment.file > y.segment.file ? 1 : 0)
|
|
299
|
+
|| x.segment.lineStart - y.segment.lineStart);
|
|
300
|
+
return {
|
|
301
|
+
items,
|
|
302
|
+
paragraphCount: input.segments.length,
|
|
303
|
+
totalChars,
|
|
304
|
+
corpusCount: input.corpus.length,
|
|
305
|
+
matchedCount: matched,
|
|
306
|
+
high,
|
|
307
|
+
medium,
|
|
308
|
+
low,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function candidateIds(target, index, mode) {
|
|
312
|
+
if (mode === 'all')
|
|
313
|
+
return index.docs.map(doc => doc.index);
|
|
314
|
+
const ids = new Set();
|
|
315
|
+
for (const shingle of target.shingles) {
|
|
316
|
+
const list = index.inverted.get(shingle);
|
|
317
|
+
if (list === undefined)
|
|
318
|
+
continue;
|
|
319
|
+
for (const id of list)
|
|
320
|
+
ids.add(id);
|
|
321
|
+
}
|
|
322
|
+
return [...ids].sort((a, b) => a - b);
|
|
323
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 不可逆操作闸门:`tools/pre-execute` 监听。
|
|
3
|
+
*
|
|
4
|
+
* 当模型即将执行命中高危规则的工具调用(默认只查 bash:递归删除、强制推送、
|
|
5
|
+
* 数据库破坏语句、管道执行远程脚本等)时,以 ask 决策拦截——审批界面会把
|
|
6
|
+
* 两个问题(最坏结果能否接受?能否回滚?)呈现给用户。
|
|
7
|
+
*
|
|
8
|
+
* 这是唯一不可由模型代答、也不可被提示词绕过的闸门:不可逆操作必须由人当场
|
|
9
|
+
* 拍板。用户可以批准(想清楚了)或拒绝(拿到更安全的替代方案)。
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-thesis/gates/dangerous
|
|
12
|
+
*/
|
|
13
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
14
|
+
import { type GateOptions } from './writing-intent.ts';
|
|
15
|
+
/**
|
|
16
|
+
* 安装高危操作闸门监听。
|
|
17
|
+
*
|
|
18
|
+
* 未命中的调用原样委托 `next()`;命中时不再调用 next(ask 决策直接返回,
|
|
19
|
+
* 由审批服务决定放行与否)。直接调用 `ctx.tools.execute()` 的无模型调用
|
|
20
|
+
* (exec.agent 缺失)不拦截。
|
|
21
|
+
*/
|
|
22
|
+
export declare function installDangerousGate(ctx: Context, config?: GateOptions): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 不可逆操作闸门:`tools/pre-execute` 监听。
|
|
3
|
+
*
|
|
4
|
+
* 当模型即将执行命中高危规则的工具调用(默认只查 bash:递归删除、强制推送、
|
|
5
|
+
* 数据库破坏语句、管道执行远程脚本等)时,以 ask 决策拦截——审批界面会把
|
|
6
|
+
* 两个问题(最坏结果能否接受?能否回滚?)呈现给用户。
|
|
7
|
+
*
|
|
8
|
+
* 这是唯一不可由模型代答、也不可被提示词绕过的闸门:不可逆操作必须由人当场
|
|
9
|
+
* 拍板。用户可以批准(想清楚了)或拒绝(拿到更安全的替代方案)。
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-thesis/gates/dangerous
|
|
12
|
+
*/
|
|
13
|
+
import { excerpt } from "../shared/text.js";
|
|
14
|
+
import { compileDestructiveRules, findDestructiveHit } from "./detect.js";
|
|
15
|
+
import { destructiveAskReason } from "./messages.js";
|
|
16
|
+
import { GATE_DEFAULTS } from "./writing-intent.js";
|
|
17
|
+
/** 决策理由中引用的命令摘录长度上限。 */
|
|
18
|
+
const COMMAND_EXCERPT_MAX = 200;
|
|
19
|
+
/**
|
|
20
|
+
* 安装高危操作闸门监听。
|
|
21
|
+
*
|
|
22
|
+
* 未命中的调用原样委托 `next()`;命中时不再调用 next(ask 决策直接返回,
|
|
23
|
+
* 由审批服务决定放行与否)。直接调用 `ctx.tools.execute()` 的无模型调用
|
|
24
|
+
* (exec.agent 缺失)不拦截。
|
|
25
|
+
*/
|
|
26
|
+
export function installDangerousGate(ctx, config = {}) {
|
|
27
|
+
if (config.destructiveGate === false)
|
|
28
|
+
return;
|
|
29
|
+
const toolNames = config.destructiveToolNames ?? GATE_DEFAULTS.destructiveToolNames;
|
|
30
|
+
const rules = compileDestructiveRules(config.destructiveRules ?? GATE_DEFAULTS.destructiveRules, pattern => {
|
|
31
|
+
ctx.logger.warn(`dsh-thesis: 无效的 destructiveRules 正则已跳过: ${pattern}`);
|
|
32
|
+
});
|
|
33
|
+
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
34
|
+
if (exec.agent === undefined)
|
|
35
|
+
return next();
|
|
36
|
+
if (!toolNames.includes(exec.name))
|
|
37
|
+
return next();
|
|
38
|
+
const args = exec.arguments;
|
|
39
|
+
const command = args !== undefined && typeof args.command === 'string' ? args.command : undefined;
|
|
40
|
+
if (command === undefined)
|
|
41
|
+
return next();
|
|
42
|
+
const hit = findDestructiveHit(command, rules);
|
|
43
|
+
if (hit === undefined)
|
|
44
|
+
return next();
|
|
45
|
+
return { kind: 'ask', reason: destructiveAskReason(hit.label, excerpt(command, COMMAND_EXCERPT_MAX)) };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 闸门的判定逻辑:正则预编译 + 命中判断。零 cordis 依赖,可独立单测。
|
|
3
|
+
*
|
|
4
|
+
* 三条设计原则(都来自真实返工,不是审美):
|
|
5
|
+
* - **便宜**:只做启发式判断。误报的代价是模型多问一句,漏报的代价是整章按错误要求重写。
|
|
6
|
+
* - **面向人**:闸门只负责「发现问题」,问题由模型转述、由用户回答。
|
|
7
|
+
* - **有终止条件**:用户一句「直接写」即可绕开写作意图闸门;但**不可逆操作闸门不可绕过**——
|
|
8
|
+
* 删除、强制推送这类操作必须由人当场拍板。
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-thesis/gates/detect
|
|
11
|
+
*/
|
|
12
|
+
import type { DestructiveRule } from './writing-intent.ts';
|
|
13
|
+
/** 一条命中的高危命令规则:标签 + 命中的正则源。 */
|
|
14
|
+
export interface DestructiveHit {
|
|
15
|
+
label: string;
|
|
16
|
+
pattern: string;
|
|
17
|
+
}
|
|
18
|
+
/** 一条已编译的高危规则:预编译后的正则随标签一并保留,避免每次调用重新编译。 */
|
|
19
|
+
export interface CompiledDestructiveRule {
|
|
20
|
+
label: string;
|
|
21
|
+
pattern: string;
|
|
22
|
+
regex: RegExp;
|
|
23
|
+
}
|
|
24
|
+
/** 编译正则列表;无效模式跳过并上报,绝不把异常抛进 agent 循环。 */
|
|
25
|
+
export declare function compilePatterns(patterns: string[], onInvalid: (pattern: string) => void): RegExp[];
|
|
26
|
+
/**
|
|
27
|
+
* 编译「已含验收标准/约束」的标记:按正则源编译。
|
|
28
|
+
*
|
|
29
|
+
* `criteriaMarkers` 里既有字面标记(如「验收」),也有条件句式正则(如「如果…就」),
|
|
30
|
+
* 因此不做字面转义——字面标记本身不含正则元字符,直接当正则源编译即保持子串匹配语义。
|
|
31
|
+
*/
|
|
32
|
+
export declare function compileCriteriaMarkers(markers: string[], onInvalid: (marker: string) => void): RegExp[];
|
|
33
|
+
/**
|
|
34
|
+
* 编译「授权绕过」标记:字面短语 + 否定防护。
|
|
35
|
+
*
|
|
36
|
+
* 中文标记加负向后顾,使「不要直接写」「别直接写」不再命中「直接写」;
|
|
37
|
+
* 纯拉丁标记用词边界包裹,避免「just write」命中「just write it later」这类子串误判。
|
|
38
|
+
*/
|
|
39
|
+
export declare function compileBypassMarkers(markers: string[], onInvalid: (marker: string) => void): RegExp[];
|
|
40
|
+
/** 预编译高危命令规则:一次编译,后续检测直接复用。 */
|
|
41
|
+
export declare function compileDestructiveRules(rules: DestructiveRule[], onInvalid: (pattern: string) => void): CompiledDestructiveRule[];
|
|
42
|
+
/**
|
|
43
|
+
* 判断一段用户文本是否「有写作意图、但没带上验收标准」。
|
|
44
|
+
*
|
|
45
|
+
* @returns true = 应当触发写作意图闸门。
|
|
46
|
+
*/
|
|
47
|
+
export declare function isUnguardedWritingIntent(text: string, patterns: RegExp[], criteriaMarkers: RegExp[], bypassMarkers: RegExp[]): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* 在命令字符串中查找第一条命中的高危规则。
|
|
50
|
+
*
|
|
51
|
+
* @param rules 已由 {@link compileDestructiveRules} 预编译的规则。
|
|
52
|
+
* @returns 命中结果;无命中返回 undefined。
|
|
53
|
+
*/
|
|
54
|
+
export declare function findDestructiveHit(command: string, rules: CompiledDestructiveRule[]): DestructiveHit | undefined;
|