hoi4-agent-tools 3.7.0 → 3.8.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/CHANGELOG.md +12 -0
- package/README.md +59 -57
- package/dist/hoi4_agent_tools/core/configuration.js +2 -0
- package/dist/hoi4_agent_tools/core/job-store.js +19 -40
- package/dist/hoi4_agent_tools/core/job-worker-host.js +57 -8
- package/dist/hoi4_agent_tools/core/operation-tasks.js +2 -7
- package/dist/hoi4_agent_tools/core/scanner.js +3 -0
- package/dist/hoi4_agent_tools/core/shared-request-capacity.js +27 -13
- package/dist/hoi4_agent_tools/core/windows-sharing.js +42 -0
- package/dist/hoi4_agent_tools/core/workspace.js +114 -8
- package/dist/hoi4_agent_tools/event/artifact-validation.js +1 -1
- package/dist/hoi4_agent_tools/event/service.js +1 -1
- package/dist/hoi4_agent_tools/focus/service.js +2 -2
- package/dist/hoi4_agent_tools/gui/studio.js +16 -3
- package/dist/hoi4_agent_tools/map/model.js +81 -2
- package/dist/hoi4_agent_tools/map/service.js +1 -0
- package/dist/hoi4_agent_tools/map/validation.js +60 -4
- package/dist/hoi4_agent_tools/mcp/tools/reference.js +1 -1
- package/dist/hoi4_agent_tools/package-check/service.js +1 -1
- package/dist/hoi4_agent_tools/reference/ranking.js +309 -0
- package/dist/hoi4_agent_tools/reference/script-validation.js +3 -13
- package/dist/hoi4_agent_tools/reference/service.js +152 -116
- package/dist/hoi4_agent_tools/reference/source-locate.js +156 -0
- package/dist/hoi4_agent_tools/reference/source-lookup.js +37 -6
- package/dist/hoi4_agent_tools/reference/suggestions.js +38 -0
- package/dist/hoi4_agent_tools/schemas/reference.js +80 -2
- package/dist/hoi4_agent_tools/schemas/transaction.js +1 -1
- package/dist/hoi4_agent_tools/technology/service.js +1 -1
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/development.md +6 -0
- package/docs/examples.md +3 -3
- package/docs/map.md +10 -0
- package/docs/reference.md +5 -3
- package/docs/research/reference-benchmark.md +39 -0
- package/docs/setup.md +15 -0
- package/docs/tools.md +37 -37
- package/npm-shrinkwrap.json +18 -5
- package/package.json +2 -2
- package/schemas/configuration.schema.json +95 -88
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/reference-context-input.schema.json +1 -1
- package/schemas/reference-read-input.schema.json +1 -1
- package/schemas/reference-search-input.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/script-validate-input.schema.json +1 -1
- package/schemas/shared-scenario.schema.json +1 -1
- package/schemas/source-lookup-input.schema.json +113 -50
- package/server.json +2 -2
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Term statistics and ranking for local documentation sections.
|
|
3
|
+
*
|
|
4
|
+
* Sections are scored with a field-weighted BM25 model: headings weigh more than body text,
|
|
5
|
+
* rare terms more than common ones, and long sections are normalized so that a passing
|
|
6
|
+
* mention in a thousand-line table does not outrank a focused entry. Clausewitz identifiers
|
|
7
|
+
* are indexed whole and by their underscore-separated words, so `is_controlled_by` answers
|
|
8
|
+
* both the exact identifier and a question about what is "controlled by" a country.
|
|
9
|
+
*/
|
|
10
|
+
const stopWords = new Set([
|
|
11
|
+
'a',
|
|
12
|
+
'about',
|
|
13
|
+
'after',
|
|
14
|
+
'an',
|
|
15
|
+
'and',
|
|
16
|
+
'are',
|
|
17
|
+
'as',
|
|
18
|
+
'at',
|
|
19
|
+
'be',
|
|
20
|
+
'by',
|
|
21
|
+
'can',
|
|
22
|
+
'come',
|
|
23
|
+
'do',
|
|
24
|
+
'does',
|
|
25
|
+
'for',
|
|
26
|
+
'from',
|
|
27
|
+
'get',
|
|
28
|
+
'has',
|
|
29
|
+
'have',
|
|
30
|
+
'how',
|
|
31
|
+
'i',
|
|
32
|
+
'if',
|
|
33
|
+
'in',
|
|
34
|
+
'into',
|
|
35
|
+
'is',
|
|
36
|
+
'it',
|
|
37
|
+
'its',
|
|
38
|
+
'me',
|
|
39
|
+
'my',
|
|
40
|
+
'of',
|
|
41
|
+
'on',
|
|
42
|
+
'one',
|
|
43
|
+
'or',
|
|
44
|
+
'should',
|
|
45
|
+
'so',
|
|
46
|
+
'that',
|
|
47
|
+
'the',
|
|
48
|
+
'their',
|
|
49
|
+
'them',
|
|
50
|
+
'then',
|
|
51
|
+
'there',
|
|
52
|
+
'this',
|
|
53
|
+
'to',
|
|
54
|
+
'use',
|
|
55
|
+
'used',
|
|
56
|
+
'using',
|
|
57
|
+
'want',
|
|
58
|
+
'way',
|
|
59
|
+
'what',
|
|
60
|
+
'when',
|
|
61
|
+
'where',
|
|
62
|
+
'which',
|
|
63
|
+
'while',
|
|
64
|
+
'who',
|
|
65
|
+
'why',
|
|
66
|
+
'will',
|
|
67
|
+
'with',
|
|
68
|
+
'work',
|
|
69
|
+
'you',
|
|
70
|
+
]);
|
|
71
|
+
const vowels = /[aeiouy]/u;
|
|
72
|
+
const MAX_CACHED_STEMS = 100_000;
|
|
73
|
+
const stems = new Map();
|
|
74
|
+
const irregular = new Map([
|
|
75
|
+
['hidden', 'hide'],
|
|
76
|
+
['chosen', 'choose'],
|
|
77
|
+
['given', 'give'],
|
|
78
|
+
['written', 'write'],
|
|
79
|
+
['children', 'child'],
|
|
80
|
+
]);
|
|
81
|
+
/** Conservative suffix folding: `controls`, `controlled`, and `controlling` share `control`. */
|
|
82
|
+
export function stemTerm(input) {
|
|
83
|
+
const cached = stems.get(input);
|
|
84
|
+
if (cached !== undefined)
|
|
85
|
+
return cached;
|
|
86
|
+
const stem = foldTerm(input);
|
|
87
|
+
// Documentation vocabulary repeats heavily; a bounded memo avoids re-folding it.
|
|
88
|
+
if (stems.size >= MAX_CACHED_STEMS)
|
|
89
|
+
stems.clear();
|
|
90
|
+
stems.set(input, stem);
|
|
91
|
+
return stem;
|
|
92
|
+
}
|
|
93
|
+
function foldTerm(input) {
|
|
94
|
+
const word = irregular.get(input) ?? input;
|
|
95
|
+
if (word.length <= 3 || /\d/u.test(word))
|
|
96
|
+
return word;
|
|
97
|
+
let stem = word;
|
|
98
|
+
if (/i(?:es|ed)$/u.test(stem) && stem.length > 4)
|
|
99
|
+
stem = `${stem.slice(0, -3)}y`;
|
|
100
|
+
else if (stem.endsWith('sses'))
|
|
101
|
+
stem = stem.slice(0, -2);
|
|
102
|
+
else if (/(?:x|z|ch|sh)es$/u.test(stem))
|
|
103
|
+
stem = stem.slice(0, -2);
|
|
104
|
+
else if (stem.endsWith('s') && !/(?:ss|us|is)$/u.test(stem))
|
|
105
|
+
stem = stem.slice(0, -1);
|
|
106
|
+
else if (stem.endsWith('ing') && stem.length > 5 && vowels.test(stem.slice(0, -3)))
|
|
107
|
+
stem = stem.slice(0, -3);
|
|
108
|
+
else if (stem.endsWith('ed') && stem.length > 4 && vowels.test(stem.slice(0, -2)))
|
|
109
|
+
stem = stem.slice(0, -2);
|
|
110
|
+
// Fold a doubled final consonant (`stopp`, `controll`) but keep `ss` and `zz` words.
|
|
111
|
+
if (/([b-df-hj-np-rtv-y])\1$/u.test(stem) && stem.length > 4)
|
|
112
|
+
stem = stem.slice(0, -1);
|
|
113
|
+
// Derivational endings common in documentation prose: `capitulation`, `capitulates`,
|
|
114
|
+
// `capitulated`, and `capitulating` share `capitul`; `government` shares `govern`.
|
|
115
|
+
if (/(?:ated|ating)$/u.test(word) && stem.length > 6)
|
|
116
|
+
stem = stem.replace(/at$/u, '');
|
|
117
|
+
else if (stem.endsWith('ation') && stem.length > 8)
|
|
118
|
+
stem = stem.slice(0, -5);
|
|
119
|
+
else if (stem.endsWith('ate') && stem.length > 6)
|
|
120
|
+
stem = stem.slice(0, -3);
|
|
121
|
+
else if (stem.endsWith('ment') && stem.length > 7)
|
|
122
|
+
stem = stem.slice(0, -4);
|
|
123
|
+
if (stem.endsWith('e') && stem.length > 4)
|
|
124
|
+
stem = stem.slice(0, -1);
|
|
125
|
+
return stem;
|
|
126
|
+
}
|
|
127
|
+
const tokenPattern = /[a-z0-9]+(?:_[a-z0-9]+)*/gu;
|
|
128
|
+
/** Content terms of free text. Identifiers yield themselves and their stemmed words. */
|
|
129
|
+
export function textTerms(text) {
|
|
130
|
+
const output = [];
|
|
131
|
+
for (const token of text.toLowerCase().replace(/'s\b/gu, '').match(tokenPattern) ?? []) {
|
|
132
|
+
if (token.includes('_')) {
|
|
133
|
+
output.push(token);
|
|
134
|
+
for (const part of token.split('_'))
|
|
135
|
+
if (part.length > 0 && !stopWords.has(part))
|
|
136
|
+
output.push(stemTerm(part));
|
|
137
|
+
}
|
|
138
|
+
else if (!stopWords.has(token))
|
|
139
|
+
output.push(stemTerm(token));
|
|
140
|
+
}
|
|
141
|
+
return output;
|
|
142
|
+
}
|
|
143
|
+
function counts(terms) {
|
|
144
|
+
const result = new Map();
|
|
145
|
+
for (const term of terms)
|
|
146
|
+
result.set(term, (result.get(term) ?? 0) + 1);
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
const navigationLine = /^\s*(?:[*-]|\d+\.)\s+\[[^\]]*\]\((?:#|<)[^)]*\)\s*$/u;
|
|
150
|
+
export function sectionTerms(title, heading, lines) {
|
|
151
|
+
const bodyTerms = lines.flatMap((line) => textTerms(line));
|
|
152
|
+
const content = lines.filter((line) => line.trim().length > 0);
|
|
153
|
+
const links = content.filter((line) => navigationLine.test(line)).length;
|
|
154
|
+
return {
|
|
155
|
+
heading: counts(textTerms(heading)),
|
|
156
|
+
title: counts(textTerms(title)),
|
|
157
|
+
body: counts(bodyTerms),
|
|
158
|
+
bodyLength: bodyTerms.length,
|
|
159
|
+
headingWords: [
|
|
160
|
+
...new Set(textTerms(heading.replaceAll('_', ' ')).filter((term) => !term.includes('_'))),
|
|
161
|
+
],
|
|
162
|
+
navigation: heading.trim().toLowerCase() === 'table of contents' ||
|
|
163
|
+
(content.length >= 4 && links / content.length >= 0.6),
|
|
164
|
+
specialty: /console commands/iu.test(title)
|
|
165
|
+
? 'console'
|
|
166
|
+
: /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/u.test(heading.trim())
|
|
167
|
+
? 'define'
|
|
168
|
+
: undefined,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/** Script names abbreviate counts as `num_`, as in `num_divisions` or `num_of_factories`. */
|
|
172
|
+
const countWords = new Set(['count', 'number', 'amount', 'many', 'much']);
|
|
173
|
+
const headingWeight = 4;
|
|
174
|
+
const titleWeight = 0.5;
|
|
175
|
+
const bodyWeight = 1;
|
|
176
|
+
const saturation = 1.2;
|
|
177
|
+
const lengthNormalization = 0.75;
|
|
178
|
+
/** Corpus statistics for one immutable inventory of sections. */
|
|
179
|
+
export class RankingIndex {
|
|
180
|
+
entries;
|
|
181
|
+
#documentFrequency = new Map();
|
|
182
|
+
#averageBodyLength;
|
|
183
|
+
constructor(entries) {
|
|
184
|
+
this.entries = entries;
|
|
185
|
+
let total = 0;
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
total += entry.bodyLength;
|
|
188
|
+
const seen = new Set([...entry.heading.keys(), ...entry.title.keys(), ...entry.body.keys()]);
|
|
189
|
+
for (const term of seen)
|
|
190
|
+
this.#documentFrequency.set(term, (this.#documentFrequency.get(term) ?? 0) + 1);
|
|
191
|
+
}
|
|
192
|
+
this.#averageBodyLength = entries.length === 0 ? 1 : Math.max(1, total / entries.length);
|
|
193
|
+
}
|
|
194
|
+
idf(term) {
|
|
195
|
+
const frequency = this.#documentFrequency.get(term) ?? 0;
|
|
196
|
+
const count = this.entries.length;
|
|
197
|
+
return Math.log(1 + (count - frequency + 0.5) / (frequency + 0.5));
|
|
198
|
+
}
|
|
199
|
+
plan(query) {
|
|
200
|
+
const normalized = query.trim().toLowerCase();
|
|
201
|
+
const sequence = textTerms(normalized);
|
|
202
|
+
const weights = new Map(sequence.map((term) => [term, 1]));
|
|
203
|
+
const words = (normalized.replace(/'s\b/gu, '').match(tokenPattern) ?? []).filter((word) => !stopWords.has(word) && !word.includes('_'));
|
|
204
|
+
// Clausewitz names often join two English words: `wargoal`, `manpower`, `warscore`.
|
|
205
|
+
for (let index = 1; index < words.length; index += 1) {
|
|
206
|
+
const joined = stemTerm(`${words[index - 1]}${words[index]}`);
|
|
207
|
+
if (this.#documentFrequency.has(joined) && !weights.has(joined))
|
|
208
|
+
weights.set(joined, 1);
|
|
209
|
+
}
|
|
210
|
+
if (words.some((word) => countWords.has(word)) && !weights.has('num'))
|
|
211
|
+
weights.set('num', 0.6);
|
|
212
|
+
const terms = [...weights].map(([term, weight]) => ({ term, idf: this.idf(term), weight }));
|
|
213
|
+
const plain = sequence.filter((term) => !term.includes('_'));
|
|
214
|
+
const pairs = [...new Set(plain.slice(1).map((term, index) => `${plain[index]} ${term}`))].map((pair) => {
|
|
215
|
+
const [left, right] = pair.split(' ');
|
|
216
|
+
return { pair, idf: (this.idf(left) + this.idf(right)) / 2 };
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
terms,
|
|
220
|
+
pairs,
|
|
221
|
+
normalized,
|
|
222
|
+
identifiers: new Set(terms.map(({ term }) => term).filter((term) => term.includes('_'))),
|
|
223
|
+
wants: {
|
|
224
|
+
console: words.some((word) => word === 'console' || word.startsWith('command')),
|
|
225
|
+
define: words.some((word) => word.startsWith('define')) || /\b[A-Z]{2,}_[A-Z]/u.test(query),
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** Score one section; undefined when it matches too little of the query to cite. */
|
|
230
|
+
score(plan, entry, heading) {
|
|
231
|
+
if (plan.terms.length === 0)
|
|
232
|
+
return undefined;
|
|
233
|
+
const lengthFactor = 1 - lengthNormalization + (lengthNormalization * entry.bodyLength) / this.#averageBodyLength;
|
|
234
|
+
let score = 0;
|
|
235
|
+
let matched = 0;
|
|
236
|
+
for (const { term, idf, weight } of plan.terms) {
|
|
237
|
+
const weighted = headingWeight * (entry.heading.get(term) ?? 0) +
|
|
238
|
+
titleWeight * (entry.title.get(term) ?? 0) +
|
|
239
|
+
(bodyWeight * (entry.body.get(term) ?? 0)) / lengthFactor;
|
|
240
|
+
if (weighted === 0)
|
|
241
|
+
continue;
|
|
242
|
+
matched++;
|
|
243
|
+
score += (weight * idf * weighted * (saturation + 1)) / (weighted + saturation);
|
|
244
|
+
}
|
|
245
|
+
// Meaningful terms must agree; a single shared word rarely answers a longer question.
|
|
246
|
+
const words = plan.terms.filter(({ term, weight }) => !term.includes('_') && weight === 1).length;
|
|
247
|
+
const required = words >= 3 ? Math.ceil(words * 0.4) : 1;
|
|
248
|
+
if (matched === 0 || (matched < required && plan.identifiers.size === 0))
|
|
249
|
+
return undefined;
|
|
250
|
+
// A named identifier must itself appear; its words alone describe something else.
|
|
251
|
+
if (plan.identifiers.size > 0 &&
|
|
252
|
+
![...plan.identifiers].some((term) => entry.heading.has(term) || entry.title.has(term) || entry.body.has(term)))
|
|
253
|
+
return undefined;
|
|
254
|
+
const headingText = heading.trim().toLowerCase();
|
|
255
|
+
// An exact heading is the answer; authority, not incidental wording, orders duplicates.
|
|
256
|
+
if (headingText === plan.normalized)
|
|
257
|
+
return 1000;
|
|
258
|
+
else if (plan.identifiers.has(headingText))
|
|
259
|
+
score += 40;
|
|
260
|
+
// A short heading whose every word the question names is usually the topic itself.
|
|
261
|
+
if (entry.headingWords.length > 0) {
|
|
262
|
+
const queryTerms = new Set(plan.terms.map(({ term }) => term));
|
|
263
|
+
const covered = entry.headingWords.filter((word) => queryTerms.has(word));
|
|
264
|
+
const fraction = covered.length / entry.headingWords.length;
|
|
265
|
+
score += fraction * fraction * covered.reduce((sum, word) => sum + this.idf(word), 0) * 0.6;
|
|
266
|
+
}
|
|
267
|
+
if (entry.navigation)
|
|
268
|
+
score *= 0.25;
|
|
269
|
+
if (entry.specialty === 'console' && !plan.wants.console)
|
|
270
|
+
score *= 0.7;
|
|
271
|
+
if (entry.specialty === 'define' && !plan.wants.define)
|
|
272
|
+
score *= 0.6;
|
|
273
|
+
return score;
|
|
274
|
+
}
|
|
275
|
+
/** Extra credit for query words that appear next to each other in the section. */
|
|
276
|
+
phraseBonus(plan, lines) {
|
|
277
|
+
if (plan.pairs.length === 0)
|
|
278
|
+
return 0;
|
|
279
|
+
const wanted = new Map(plan.pairs.map(({ pair, idf }) => [pair, idf]));
|
|
280
|
+
const found = new Set();
|
|
281
|
+
for (const line of lines) {
|
|
282
|
+
const terms = textTerms(line).filter((term) => !term.includes('_'));
|
|
283
|
+
for (let index = 1; index < terms.length; index += 1) {
|
|
284
|
+
const pair = `${terms[index - 1]} ${terms[index]}`;
|
|
285
|
+
if (wanted.has(pair))
|
|
286
|
+
found.add(pair);
|
|
287
|
+
}
|
|
288
|
+
if (found.size === wanted.size)
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
return [...found].reduce((sum, pair) => sum + wanted.get(pair), 0) * 0.5;
|
|
292
|
+
}
|
|
293
|
+
/** The line offset carrying the most query information, for landing a bounded read. */
|
|
294
|
+
bestLine(plan, lines) {
|
|
295
|
+
let best = 0;
|
|
296
|
+
let bestScore = 0;
|
|
297
|
+
const weights = new Map(plan.terms.map(({ term, idf, weight }) => [term, idf * weight]));
|
|
298
|
+
for (const [index, line] of lines.entries()) {
|
|
299
|
+
let lineScore = 0;
|
|
300
|
+
for (const term of new Set(textTerms(line)))
|
|
301
|
+
lineScore += weights.get(term) ?? 0;
|
|
302
|
+
if (lineScore > bestScore) {
|
|
303
|
+
best = index;
|
|
304
|
+
bestScore = lineScore;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return best;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -2,6 +2,8 @@ import path from 'node:path';
|
|
|
2
2
|
import { hashCanonical } from '../core/canonical.js';
|
|
3
3
|
import { ServiceError } from '../core/result.js';
|
|
4
4
|
import { locationFor, parseClausewitz, } from '../core/source/index.js';
|
|
5
|
+
import { editDistance } from './suggestions.js';
|
|
6
|
+
import { publicReferenceSection } from './service.js';
|
|
5
7
|
const MAX_COMMANDS = 256;
|
|
6
8
|
const booleanCommands = new Set(['AND', 'OR', 'NOT', 'XOR', 'NAND', 'NOR']);
|
|
7
9
|
const containers = new Set([
|
|
@@ -21,18 +23,6 @@ const iteratorParameters = new Set([
|
|
|
21
23
|
'display_individual_scopes',
|
|
22
24
|
'count',
|
|
23
25
|
]);
|
|
24
|
-
function editDistance(a, b) {
|
|
25
|
-
let row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
26
|
-
for (let i = 0; i < a.length; i++) {
|
|
27
|
-
const next = [i + 1];
|
|
28
|
-
for (let j = 0; j < b.length; j++)
|
|
29
|
-
next.push(Math.min(next[j] + 1, row[j + 1] + 1, row[j] + Number(a[i] !== b[j])));
|
|
30
|
-
if (Math.min(...next) > 3)
|
|
31
|
-
return 4;
|
|
32
|
-
row = next;
|
|
33
|
-
}
|
|
34
|
-
return row[b.length];
|
|
35
|
-
}
|
|
36
26
|
/** Check only rules explicitly available from one selected documentation authority. */
|
|
37
27
|
export async function validateScript(references, workspace, input, signal) {
|
|
38
28
|
if (Buffer.byteLength(input.source, 'utf8') > 64_000)
|
|
@@ -59,7 +49,7 @@ export async function validateScript(references, workspace, input, signal) {
|
|
|
59
49
|
].sort();
|
|
60
50
|
const kind = match[1].startsWith('effect') ? 'effect' : 'trigger';
|
|
61
51
|
const previous = catalogs[kind].get(section.heading);
|
|
62
|
-
const
|
|
52
|
+
const reference = publicReferenceSection(section);
|
|
63
53
|
if (previous !== undefined) {
|
|
64
54
|
previous.conflict ||= previous.scopes.join(',') !== scopes.join(',');
|
|
65
55
|
}
|