chaffjs 0.8.0 → 0.10.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/dist/address-chain.d.ts +9 -0
- package/dist/address-chain.d.ts.map +1 -0
- package/dist/address-chain.js +105 -0
- package/dist/address-chain.js.map +1 -0
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +1 -1
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-text.d.ts.map +1 -1
- package/dist/cli-text.js +4 -0
- package/dist/cli-text.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +19 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +2 -1
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/feedback.d.ts +35 -0
- package/dist/commands/feedback.d.ts.map +1 -0
- package/dist/commands/feedback.js +157 -0
- package/dist/commands/feedback.js.map +1 -0
- package/dist/commands/test.d.ts.map +1 -1
- package/dist/commands/test.js +3 -2
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/tree.d.ts.map +1 -1
- package/dist/commands/tree.js +3 -1
- package/dist/commands/tree.js.map +1 -1
- package/dist/config/by-path.d.ts +5 -1
- package/dist/config/by-path.d.ts.map +1 -1
- package/dist/config/by-path.js +1 -4
- package/dist/config/by-path.js.map +1 -1
- package/dist/config/load.d.ts +2 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +3 -1
- package/dist/config/load.js.map +1 -1
- package/dist/detectors/char-shape.d.ts.map +1 -1
- package/dist/detectors/char-shape.js +3 -2
- package/dist/detectors/char-shape.js.map +1 -1
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +4 -1
- package/dist/detectors/index.js.map +1 -1
- package/dist/detectors/lexicon.d.ts.map +1 -1
- package/dist/detectors/lexicon.js +8 -1
- package/dist/detectors/lexicon.js.map +1 -1
- package/dist/detectors/structure-tree.d.ts +5 -0
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +50 -1
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/detectors/team.d.ts.map +1 -1
- package/dist/detectors/team.js +7 -2
- package/dist/detectors/team.js.map +1 -1
- package/dist/detectors/word-list.d.ts +3 -0
- package/dist/detectors/word-list.d.ts.map +1 -0
- package/dist/detectors/word-list.js +3 -0
- package/dist/detectors/word-list.js.map +1 -0
- package/dist/document.d.ts +2 -2
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +4 -2
- package/dist/document.js.map +1 -1
- package/dist/feedback/draft.d.ts +28 -0
- package/dist/feedback/draft.d.ts.map +1 -0
- package/dist/feedback/draft.js +66 -0
- package/dist/feedback/draft.js.map +1 -0
- package/dist/feedback/excerpt.d.ts +10 -0
- package/dist/feedback/excerpt.d.ts.map +1 -0
- package/dist/feedback/excerpt.js +20 -0
- package/dist/feedback/excerpt.js.map +1 -0
- package/dist/legal-address.d.ts +2 -0
- package/dist/legal-address.d.ts.map +1 -0
- package/dist/legal-address.js +16 -0
- package/dist/legal-address.js.map +1 -0
- package/dist/orthography.d.ts.map +1 -1
- package/dist/orthography.js +15 -0
- package/dist/orthography.js.map +1 -1
- package/dist/plugin.d.ts +98 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/profile/for-file.d.ts +7 -0
- package/dist/profile/for-file.d.ts.map +1 -0
- package/dist/profile/for-file.js +6 -0
- package/dist/profile/for-file.js.map +1 -0
- package/dist/profile/load.d.ts +4 -0
- package/dist/profile/load.d.ts.map +1 -0
- package/dist/profile/load.js +25 -0
- package/dist/profile/load.js.map +1 -0
- package/dist/profile/parse.d.ts +19 -0
- package/dist/profile/parse.d.ts.map +1 -0
- package/dist/profile/parse.js +95 -0
- package/dist/profile/parse.js.map +1 -0
- package/dist/profile/select.d.ts +22 -0
- package/dist/profile/select.d.ts.map +1 -0
- package/dist/profile/select.js +21 -0
- package/dist/profile/select.js.map +1 -0
- package/dist/reasons.d.ts +1 -0
- package/dist/reasons.d.ts.map +1 -1
- package/dist/reasons.js +2 -0
- package/dist/reasons.js.map +1 -1
- package/dist/render/compact.d.ts.map +1 -1
- package/dist/render/compact.js +3 -1
- package/dist/render/compact.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +40 -7
- package/dist/run.js.map +1 -1
- package/dist/structure/build.d.ts +2 -1
- package/dist/structure/build.d.ts.map +1 -1
- package/dist/structure/build.js +105 -11
- package/dist/structure/build.js.map +1 -1
- package/dist/structure/date-order.d.ts +12 -0
- package/dist/structure/date-order.d.ts.map +1 -0
- package/dist/structure/date-order.js +30 -0
- package/dist/structure/date-order.js.map +1 -0
- package/dist/structure/issues.d.ts.map +1 -1
- package/dist/structure/issues.js +9 -2
- package/dist/structure/issues.js.map +1 -1
- package/dist/structure/of.d.ts +2 -1
- package/dist/structure/of.d.ts.map +1 -1
- package/dist/structure/of.js +7 -1
- package/dist/structure/of.js.map +1 -1
- package/dist/structure/relative-find.d.ts +8 -0
- package/dist/structure/relative-find.d.ts.map +1 -0
- package/dist/structure/relative-find.js +190 -0
- package/dist/structure/relative-find.js.map +1 -0
- package/dist/structure/relative-resolve.d.ts +3 -0
- package/dist/structure/relative-resolve.d.ts.map +1 -0
- package/dist/structure/relative-resolve.js +175 -0
- package/dist/structure/relative-resolve.js.map +1 -0
- package/dist/structure/runs.d.ts +20 -0
- package/dist/structure/runs.d.ts.map +1 -0
- package/dist/structure/runs.js +63 -0
- package/dist/structure/runs.js.map +1 -0
- package/dist/structure/total.d.ts +14 -0
- package/dist/structure/total.d.ts.map +1 -0
- package/dist/structure/total.js +119 -0
- package/dist/structure/total.js.map +1 -0
- package/dist/structure/unnumbered.d.ts +15 -0
- package/dist/structure/unnumbered.d.ts.map +1 -0
- package/dist/structure/unnumbered.js +28 -0
- package/dist/structure/unnumbered.js.map +1 -0
- package/dist/structure/unread.d.ts +8 -0
- package/dist/structure/unread.d.ts.map +1 -0
- package/dist/structure/unread.js +34 -0
- package/dist/structure/unread.js.map +1 -0
- package/dist/structure/weekday.d.ts +7 -0
- package/dist/structure/weekday.d.ts.map +1 -0
- package/dist/structure/weekday.js +21 -0
- package/dist/structure/weekday.js.map +1 -0
- package/package.json +4 -3
- package/profiles/statute.yaml +66 -0
- package/rules/date-order.yaml +26 -0
- package/rules/date-weekday-mismatch.yaml +26 -0
- package/rules/internal-jargon.yaml +1 -1
- package/rules/total-mismatch.yaml +26 -0
- package/skills/chaff/SKILL.md +24 -4
- package/src/address-chain.ts +113 -0
- package/src/cli-args.ts +1 -1
- package/src/cli-text.ts +4 -0
- package/src/cli.ts +20 -1
- package/src/commands/eval.ts +2 -1
- package/src/commands/feedback.ts +210 -0
- package/src/commands/test.ts +3 -2
- package/src/commands/tree.ts +3 -1
- package/src/config/by-path.ts +11 -3
- package/src/config/load.ts +5 -1
- package/src/detectors/char-shape.ts +5 -4
- package/src/detectors/index.ts +4 -1
- package/src/detectors/lexicon.ts +11 -1
- package/src/detectors/structure-tree.ts +67 -1
- package/src/detectors/team.ts +7 -2
- package/src/detectors/word-list.ts +2 -0
- package/src/document.ts +11 -3
- package/src/feedback/draft.ts +102 -0
- package/src/feedback/excerpt.ts +23 -0
- package/src/orthography.ts +15 -0
- package/src/plugin.ts +95 -0
- package/src/profile/for-file.ts +11 -0
- package/src/profile/load.ts +26 -0
- package/src/profile/parse.ts +119 -0
- package/src/profile/select.ts +35 -0
- package/src/reasons.ts +5 -0
- package/src/render/compact.ts +4 -1
- package/src/run.ts +42 -7
- package/src/structure/build.ts +135 -15
- package/src/structure/date-order.ts +44 -0
- package/src/structure/issues.ts +10 -2
- package/src/structure/of.ts +15 -3
- package/src/structure/relative-find.ts +237 -0
- package/src/structure/relative-resolve.ts +209 -0
- package/src/structure/runs.ts +71 -0
- package/src/structure/total.ts +157 -0
- package/src/structure/unnumbered.ts +33 -0
- package/src/structure/unread.ts +39 -0
- package/src/structure/weekday.ts +25 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Lines around a line of a document, 1-based and inclusive. Only these lines go into a report. */
|
|
2
|
+
export type Excerpt = { readonly from: number; readonly to: number; readonly lines: readonly string[] };
|
|
3
|
+
|
|
4
|
+
export const EXCERPT_RADIUS = 2;
|
|
5
|
+
|
|
6
|
+
/** Pure: the excerpts around each line, overlapping ones merged, in document order. Lines outside the document are dropped. */
|
|
7
|
+
export const excerptsAround = (source: string, lines: readonly number[], radius: number = EXCERPT_RADIUS): Excerpt[] => {
|
|
8
|
+
const all = source.split("\n");
|
|
9
|
+
const ranges = [...new Set(lines)]
|
|
10
|
+
.filter((line) => line >= 1 && line <= all.length)
|
|
11
|
+
.sort((left, right) => left - right)
|
|
12
|
+
.map((line) => ({ from: Math.max(1, line - radius), to: Math.min(all.length, line + radius) }));
|
|
13
|
+
const merged = ranges.reduce<{ from: number; to: number }[]>((acc, range) => {
|
|
14
|
+
const last = acc.at(-1);
|
|
15
|
+
if (last !== undefined && range.from <= last.to + 1) {
|
|
16
|
+
last.to = Math.max(last.to, range.to);
|
|
17
|
+
return acc;
|
|
18
|
+
}
|
|
19
|
+
acc.push({ ...range });
|
|
20
|
+
return acc;
|
|
21
|
+
}, []);
|
|
22
|
+
return merged.map(({ from, to }) => ({ from, to, lines: all.slice(from - 1, to) }));
|
|
23
|
+
};
|
package/src/orthography.ts
CHANGED
|
@@ -80,8 +80,23 @@ const leftRunBeside = (chars: readonly string[], last: number, japanese: string
|
|
|
80
80
|
return kind === "letter" && DIGIT.test(runStart(chars, last) ?? "") ? "after-digit" : kind;
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* 「第3条」「第4条第2項」の数字は番地の書き方で、空けるか詰めるかの好みではない。数えると、条番号の多い文書では
|
|
85
|
+
* 「2 か所」のような普通の書き方のほうが少数に見えてしまう。「第」のすぐ後ろの数字の並びは、両側とも数えない。
|
|
86
|
+
*/
|
|
87
|
+
const ORDINAL_PREFIX = "第";
|
|
88
|
+
|
|
89
|
+
const isOrdinalRun = (chars: readonly string[], index: number): boolean => {
|
|
90
|
+
let first = index;
|
|
91
|
+
while (first > 0 && /[\d.]/u.test(chars[first - 1] ?? "")) first -= 1;
|
|
92
|
+
const before = chars[first - 1] === " " ? first - 2 : first - 1;
|
|
93
|
+
return chars[before] === ORDINAL_PREFIX;
|
|
94
|
+
};
|
|
95
|
+
|
|
83
96
|
const boundaryAt = (chars: readonly string[], index: number): Omit<Boundary, "offset"> | undefined => {
|
|
84
97
|
const [left, right, after] = [chars[index], chars[index + 1], chars[index + 2]];
|
|
98
|
+
const numberAfterPrefix = left === ORDINAL_PREFIX && DIGIT.test((right === " " ? after : right) ?? "");
|
|
99
|
+
if (numberAfterPrefix || (DIGIT.test(left ?? "") && isOrdinalRun(chars, index))) return undefined;
|
|
85
100
|
const touching = latinBeside(left, right, "before-digit") ?? leftRunBeside(chars, index, right);
|
|
86
101
|
if (touching !== undefined) return { kind: touching, spaced: false };
|
|
87
102
|
if (right !== " ") return undefined;
|
package/src/plugin.ts
CHANGED
|
@@ -93,6 +93,11 @@ export type NumberedLine = {
|
|
|
93
93
|
readonly label: string;
|
|
94
94
|
/** 番号に続く見出し。「第3条(支払)」の「支払」。本文しか無ければ空。 */
|
|
95
95
|
readonly heading: string;
|
|
96
|
+
/**
|
|
97
|
+
* 番号に付いた語("section"、"article")。英米の文書は Section と Article を別のものとして使うので、
|
|
98
|
+
* 参照の attrs.numbering と比べて、この文書に無い種類の番号への参照(Section で組んだ法令の中の "Article 6")を見分ける。
|
|
99
|
+
*/
|
|
100
|
+
readonly numbering?: string | undefined;
|
|
96
101
|
/** 番号の後ろの文字列全部。定義や参照はここから探す。 */
|
|
97
102
|
readonly rest: string;
|
|
98
103
|
/**
|
|
@@ -124,6 +129,16 @@ export type StructurePatterns = {
|
|
|
124
129
|
readonly obligations: (text: string) => readonly Mention[];
|
|
125
130
|
/** 数量。attrs.value に数、attrs.unit に単位。 */
|
|
126
131
|
readonly quantities: (text: string) => readonly Mention[];
|
|
132
|
+
/**
|
|
133
|
+
* 「In this section—」のように、この後の定義はいま開いている条の中でだけ通じると宣言する行なら true。
|
|
134
|
+
* その条の定義に scope: "local" が付く。定義の並びが次の行から始まるので、定義の行だけを見ても分からない。
|
|
135
|
+
*/
|
|
136
|
+
readonly opensDefinitionScope?: (text: string) => boolean;
|
|
137
|
+
/**
|
|
138
|
+
* 範囲が条より広いまとまり(「In this Part—」)なら、そのまとまりの深さ(NumberedLine.depth)。
|
|
139
|
+
* そのまとまりの中の定義どうしだけを比べる。無ければ、opensDefinitionScope のとおり条の中。
|
|
140
|
+
*/
|
|
141
|
+
readonly definitionScopeDepth?: (text: string) => number | undefined;
|
|
127
142
|
/** 日付。attrs.value に "2024-04-01"・"2024-04"・"04-01"・"2024" のどれか。無い言語は日付を読まない。 */
|
|
128
143
|
readonly dates?: (text: string) => readonly Mention[];
|
|
129
144
|
/**
|
|
@@ -131,6 +146,8 @@ export type StructurePatterns = {
|
|
|
131
146
|
* 数字と点だけの通し番号は core が言語を問わず読むので、それを数量と見分けられるのは言語パッケージだけ。
|
|
132
147
|
*/
|
|
133
148
|
readonly countedAfter?: (number: string, rest: string) => boolean;
|
|
149
|
+
/** 数の書き方(「二十二」「3」)を数にする。相対の参照の「前二項」「前条第二項」が使う。 */
|
|
150
|
+
readonly number?: (text: string) => number | undefined;
|
|
134
151
|
};
|
|
135
152
|
|
|
136
153
|
export type StructureKind = "doc" | "section" | "chapter" | "article" | "item" | "definition" | "reference" | "obligation" | "quantity" | "date";
|
|
@@ -150,6 +167,8 @@ export type StructureNode = {
|
|
|
150
167
|
readonly level?: number;
|
|
151
168
|
/** 範囲をまとめた行の最後の位置(NumberedLine.ordinalTo)。S 式には出さない。 */
|
|
152
169
|
readonly ordinalTo?: number;
|
|
170
|
+
/** 番号に付いた語(NumberedLine.numbering)。S 式には出さない。 */
|
|
171
|
+
readonly numbering?: string;
|
|
153
172
|
};
|
|
154
173
|
|
|
155
174
|
// ───────── 文書モデルと rule ─────────
|
|
@@ -175,6 +194,78 @@ export type BulletList = { readonly span: Span; readonly items: readonly number[
|
|
|
175
194
|
* detector に渡る唯一の入り口。core が I/O を済ませてから呼ぶ。
|
|
176
195
|
* detector は純関数で、fs / network / clock に触れない。spec §6。
|
|
177
196
|
*/
|
|
197
|
+
/**
|
|
198
|
+
* 文書の種類の知識(法令の番地の書き方など)。コードは種類を知らず、profiles/*.yaml に書いたものを読む。
|
|
199
|
+
* 言語ごとに一つに絞ったもの。正規表現は文字列のまま持ち、使う側が組み立てる。
|
|
200
|
+
*/
|
|
201
|
+
export type DocumentProfile = {
|
|
202
|
+
readonly id: string;
|
|
203
|
+
/** 番地の書き方。漢字の連なりにも、数量にも数えない。 */
|
|
204
|
+
readonly addresses: readonly string[];
|
|
205
|
+
/** 番地と番地のあいだ、番地の後ろに付く語。 */
|
|
206
|
+
readonly connectives: readonly string[];
|
|
207
|
+
/** 番地の並びのすぐ後ろに来てよい文字(正規表現)。無ければどこで終わってもよい。 */
|
|
208
|
+
readonly addressEnd?: string | undefined;
|
|
209
|
+
/**
|
|
210
|
+
* 条の前の行に置く見出しの形(正規表現。最初の括弧の中が見出しの言葉)。法令の「(解雇の予告)」。
|
|
211
|
+
* すぐ次の行の条が見出しを持たなければ、その条の見出しになる。
|
|
212
|
+
*/
|
|
213
|
+
readonly caption?: string | undefined;
|
|
214
|
+
/** 前条・同項のように、書いた場所から番地が決まる参照の読み方。無ければ読まない。 */
|
|
215
|
+
readonly relative?: RelativeVocabulary | undefined;
|
|
216
|
+
/**
|
|
217
|
+
* 番号を書かない単位(古い法令の第 2 項以降)。indent で始まる行を、inside の種類の単位の中の、depth の深さの次の単位にする。
|
|
218
|
+
* inside の単位の行に、番号の後ろの本文が続くときだけ。
|
|
219
|
+
*/
|
|
220
|
+
readonly unnumbered?: { readonly indent: string; readonly inside: string; readonly depth: number } | undefined;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 相対の参照の語彙。語の意味(一つ前・一つ後・直前に引いたもの・今いるところ)はコードが知り、どの語がそれに当たるかは種類が決める。
|
|
225
|
+
* units は単位の語から木の深さ(NumberedLine.depth)への対応。
|
|
226
|
+
*/
|
|
227
|
+
export type RelativeVocabulary = {
|
|
228
|
+
readonly before: readonly string[];
|
|
229
|
+
readonly after: readonly string[];
|
|
230
|
+
readonly same: readonly string[];
|
|
231
|
+
readonly current: readonly string[];
|
|
232
|
+
/** 「前各項」の「各」。一つ前だけでなく、前の全部。 */
|
|
233
|
+
readonly every: readonly string[];
|
|
234
|
+
/** 「前二項」の「二」のような、いくつ分かの数の書き方(正規表現)。数は言語パッケージの number が読む。 */
|
|
235
|
+
readonly count: string;
|
|
236
|
+
readonly units: Readonly<Record<string, number>>;
|
|
237
|
+
/** 「前条第二項」の「第」。後ろに続く番地の書き出し。 */
|
|
238
|
+
readonly suffixPrefix: string;
|
|
239
|
+
/**
|
|
240
|
+
* 最初のものに番号を振らない深さ(法令の項)。あれば「第二条第一号」は 2.1.1、「第四条第一項」は第四条そのものでもある。
|
|
241
|
+
*/
|
|
242
|
+
readonly implicitFirst: number | undefined;
|
|
243
|
+
/** すぐ前に来てはいけない文字(正規表現)。「事前条件」の「前条」を参照にしない。 */
|
|
244
|
+
readonly notAfter: string | undefined;
|
|
245
|
+
/**
|
|
246
|
+
* 読み替え(「『前条』とあるのは『…』」)の括弧。中の前条・同項は読み替える先の文の言葉で、ここからは決められないので読まない。
|
|
247
|
+
* 閉じの直後に after、開きの直前に before のどれかがあるものだけ。中の番地を名指しした参照は、document の名前の文書を指す。
|
|
248
|
+
*/
|
|
249
|
+
readonly substitution:
|
|
250
|
+
| {
|
|
251
|
+
readonly open: string;
|
|
252
|
+
readonly close: string;
|
|
253
|
+
readonly after: readonly string[];
|
|
254
|
+
readonly before: readonly string[];
|
|
255
|
+
readonly document: string | undefined;
|
|
256
|
+
}
|
|
257
|
+
| undefined;
|
|
258
|
+
/** 条を書かずにこの単位で始まる番地(法令の「第一項」「第二号」)は、書いた場所を含むまとまりの中を指す。 */
|
|
259
|
+
readonly inside: readonly string[];
|
|
260
|
+
/**
|
|
261
|
+
* 並びをつなぐもの(正規表現。及び・若しくは・から・、・枝番号の「の二」)。すぐ前の参照とのあいだがこれとつなぎの語(connectives)と
|
|
262
|
+
* 閉じた括弧だけなら、条を書かない番地はその参照の続き。「第三十三条第七項若しくは第九項」の「第九項」は第三十三条の第九項。
|
|
263
|
+
*/
|
|
264
|
+
readonly joiners: readonly string[];
|
|
265
|
+
/** 括弧書き(「(…)」)の開きと閉じ。並びの途中に挟まっても、並びを切らない。 */
|
|
266
|
+
readonly aside: { readonly open: string; readonly close: string } | undefined;
|
|
267
|
+
};
|
|
268
|
+
|
|
178
269
|
export type ProseDocument = {
|
|
179
270
|
readonly path: string;
|
|
180
271
|
readonly source: string;
|
|
@@ -196,6 +287,10 @@ export type ProseDocument = {
|
|
|
196
287
|
readonly requiredSections: readonly string[];
|
|
197
288
|
/** 番地の付いた木(§27)。adapter が structure を持たない言語では無い。 */
|
|
198
289
|
readonly structure: StructureNode | undefined;
|
|
290
|
+
/** 文書の種類(法令など)。選ばれなければ無い。 */
|
|
291
|
+
readonly profile?: DocumentProfile | undefined;
|
|
292
|
+
/** 本文でないもの(コード・HTML・強調の印)を同じ長さの空白で覆った source。位置は source と同じ。 */
|
|
293
|
+
readonly prose?: string | undefined;
|
|
199
294
|
};
|
|
200
295
|
|
|
201
296
|
export type Severity = "error" | "warning" | "info";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { applyByPath } from "../config/by-path.ts";
|
|
2
|
+
import type { Config } from "../config/load.ts";
|
|
3
|
+
import type { DocumentProfile } from "../plugin.ts";
|
|
4
|
+
import { loadProfiles } from "./load.ts";
|
|
5
|
+
import { chooseProfile } from "./select.ts";
|
|
6
|
+
|
|
7
|
+
type ProfileSettings = Pick<Config, "profile" | "byPath" | "baseDir">;
|
|
8
|
+
|
|
9
|
+
/** 1 ファイルの文書の種類。by_path、chaff.yaml、内容の順に選ぶ。 */
|
|
10
|
+
export const profileFor = (config: ProfileSettings, path: string, source: string, language: string): DocumentProfile | undefined =>
|
|
11
|
+
chooseProfile(loadProfiles(), { byPath: applyByPath(config.byPath, config.baseDir, path).profile, config: config.profile, source, language })?.profile;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { parse } from "yaml";
|
|
5
|
+
import { parseProfile, type ProfileDefinition } from "./parse.ts";
|
|
6
|
+
|
|
7
|
+
const PROFILES_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "profiles");
|
|
8
|
+
|
|
9
|
+
const loaded: { value: readonly ProfileDefinition[] | undefined } = { value: undefined };
|
|
10
|
+
|
|
11
|
+
/** 同梱の profiles/*.yaml。一度だけ読む。 */
|
|
12
|
+
export const loadProfiles = (): readonly ProfileDefinition[] => {
|
|
13
|
+
loaded.value ??= readdirSync(PROFILES_DIR)
|
|
14
|
+
.filter((file) => file.endsWith(".yaml"))
|
|
15
|
+
.sort((left, right) => left.localeCompare(right))
|
|
16
|
+
.flatMap((file) => {
|
|
17
|
+
const path = join(PROFILES_DIR, file);
|
|
18
|
+
try {
|
|
19
|
+
const definition = parseProfile(parse(readFileSync(path, "utf8")));
|
|
20
|
+
return definition === undefined ? [] : [definition];
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw new Error(`cannot read the profile ${path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return loaded.value;
|
|
26
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { DocumentProfile, RelativeVocabulary } from "../plugin.ts";
|
|
2
|
+
|
|
3
|
+
/** profiles/*.yaml を一つ読んだもの。言語ごとの中身と、内容から選ぶための形を持つ。 */
|
|
4
|
+
export type ProfileDefinition = {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly name: Readonly<Record<string, string>>;
|
|
7
|
+
readonly detect: Readonly<Record<string, ProfileDetect>>;
|
|
8
|
+
readonly languages: Readonly<Record<string, DocumentProfile>>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** この形の行が minLines 行以上あれば、その種類の文書として読む。 */
|
|
12
|
+
type ProfileDetect = { readonly line: string; readonly minLines: number };
|
|
13
|
+
|
|
14
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
|
|
16
|
+
const strings = (value: unknown): string[] => (Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string" && entry !== "") : []);
|
|
17
|
+
|
|
18
|
+
const texts = (value: unknown): Record<string, string> =>
|
|
19
|
+
isRecord(value) ? Object.fromEntries(Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === "string")) : {};
|
|
20
|
+
|
|
21
|
+
/** 空でない文字列だけ。空の正規表現は何にでも当たる。 */
|
|
22
|
+
const text = (value: unknown): string | undefined => (typeof value === "string" && value !== "" ? value : undefined);
|
|
23
|
+
|
|
24
|
+
const detectOf = (value: unknown): ProfileDetect | undefined => {
|
|
25
|
+
if (!isRecord(value) || typeof value["line"] !== "string") return undefined;
|
|
26
|
+
const minLines = value["min_lines"];
|
|
27
|
+
return { line: value["line"], minLines: typeof minLines === "number" && minLines > 0 ? minLines : 1 };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const unitsOf = (value: unknown): Record<string, number> =>
|
|
31
|
+
isRecord(value) ? Object.fromEntries(Object.entries(value).filter((entry): entry is [string, number] => entry[0] !== "" && Number.isInteger(entry[1]))) : {};
|
|
32
|
+
|
|
33
|
+
const asideOf = (value: unknown): RelativeVocabulary["aside"] => {
|
|
34
|
+
if (!isRecord(value)) return undefined;
|
|
35
|
+
const [open, close] = [text(value["open"]), text(value["close"])];
|
|
36
|
+
return open === undefined || close === undefined ? undefined : { open, close };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** 括弧の開き・閉じと、目印が一つも無ければ読まない。 */
|
|
40
|
+
const substitutionOf = (value: unknown): RelativeVocabulary["substitution"] => {
|
|
41
|
+
if (!isRecord(value)) return undefined;
|
|
42
|
+
const [open, close] = [text(value["open"]), text(value["close"])];
|
|
43
|
+
const [after, before] = [strings(value["after"]), strings(value["before"])];
|
|
44
|
+
return open === undefined || close === undefined || after.length + before.length === 0
|
|
45
|
+
? undefined
|
|
46
|
+
: { open, close, after, before, document: text(value["document"]) };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** 字下げと、どの種類の中か、どの深さかのどれかが無ければ読まない。 */
|
|
50
|
+
const unnumberedOf = (value: unknown): DocumentProfile["unnumbered"] => {
|
|
51
|
+
if (!isRecord(value)) return undefined;
|
|
52
|
+
const [indent, inside, depth] = [text(value["indent"]), text(value["inside"]), value["depth"]];
|
|
53
|
+
return indent === undefined || inside === undefined || !Number.isInteger(depth) || typeof depth !== "number" ? undefined : { indent, inside, depth };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** 単位が一つも無ければ読まない。何を指すのか決められない。 */
|
|
57
|
+
const relativeOf = (value: unknown): RelativeVocabulary | undefined => {
|
|
58
|
+
if (!isRecord(value)) return undefined;
|
|
59
|
+
const units = unitsOf(value["units"]);
|
|
60
|
+
if (Object.keys(units).length === 0) return undefined;
|
|
61
|
+
return {
|
|
62
|
+
before: strings(value["before"]),
|
|
63
|
+
after: strings(value["after"]),
|
|
64
|
+
same: strings(value["same"]),
|
|
65
|
+
current: strings(value["current"]),
|
|
66
|
+
every: strings(value["every"]),
|
|
67
|
+
count: text(value["count"]) ?? "(?!)",
|
|
68
|
+
units,
|
|
69
|
+
suffixPrefix: text(value["suffix_prefix"]) ?? "",
|
|
70
|
+
implicitFirst: typeof value["implicit_first"] === "string" ? units[value["implicit_first"]] : undefined,
|
|
71
|
+
notAfter: text(value["not_after"]),
|
|
72
|
+
substitution: substitutionOf(value["substitution"]),
|
|
73
|
+
inside: strings(value["inside"]).filter((unit) => units[unit] !== undefined),
|
|
74
|
+
joiners: strings(value["joiners"]),
|
|
75
|
+
aside: asideOf(value["aside"]),
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const LANGUAGE_KEYS = new Set(["id", "name", "why", "detect"]);
|
|
80
|
+
|
|
81
|
+
const languagesOf = (id: string, raw: Record<string, unknown>): Record<string, DocumentProfile> =>
|
|
82
|
+
Object.fromEntries(
|
|
83
|
+
Object.entries(raw)
|
|
84
|
+
.filter(([key, value]) => !LANGUAGE_KEYS.has(key) && isRecord(value))
|
|
85
|
+
.map(([language, value]) => {
|
|
86
|
+
const section = isRecord(value) ? value : {};
|
|
87
|
+
const addressEnd = text(section["address_end"]);
|
|
88
|
+
const caption = text(section["caption"]);
|
|
89
|
+
const relative = relativeOf(section["relative"]);
|
|
90
|
+
const unnumbered = unnumberedOf(section["unnumbered"]);
|
|
91
|
+
return [
|
|
92
|
+
language,
|
|
93
|
+
{ id, addresses: strings(section["addresses"]), connectives: strings(section["connectives"]), addressEnd, caption, relative, unnumbered },
|
|
94
|
+
];
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
/** id の無いものは読まない。どの設定から選ばれたのか言えなくなるため。 */
|
|
99
|
+
export const parseProfile = (raw: unknown): ProfileDefinition | undefined => {
|
|
100
|
+
if (!isRecord(raw) || typeof raw["id"] !== "string" || raw["id"] === "") return undefined;
|
|
101
|
+
const id = raw["id"];
|
|
102
|
+
const detect = isRecord(raw["detect"])
|
|
103
|
+
? Object.fromEntries(
|
|
104
|
+
Object.entries(raw["detect"]).flatMap(([language, value]) => {
|
|
105
|
+
const found = detectOf(value);
|
|
106
|
+
return found === undefined ? [] : [[language, found] as const];
|
|
107
|
+
}),
|
|
108
|
+
)
|
|
109
|
+
: {};
|
|
110
|
+
return { id, name: texts(raw["name"]), detect, languages: languagesOf(id, raw) };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** 行頭から数える。source 全体に m フラグで当てれば、行ごとに分けずに数えられる。 */
|
|
114
|
+
export const detected = (definition: ProfileDefinition, source: string, language: string): boolean => {
|
|
115
|
+
const detect = definition.detect[language];
|
|
116
|
+
if (detect === undefined) return false;
|
|
117
|
+
const count = [...source.matchAll(new RegExp(detect.line, "gmu"))].length;
|
|
118
|
+
return count >= detect.minLines;
|
|
119
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DocumentProfile } from "../plugin.ts";
|
|
2
|
+
import { detected, type ProfileDefinition } from "./parse.ts";
|
|
3
|
+
|
|
4
|
+
/** 設定で書いた「使わない」。内容から選ぶのも止める。 */
|
|
5
|
+
export const NO_PROFILE = "none";
|
|
6
|
+
|
|
7
|
+
type ProfileSource = "by-path" | "config" | "content";
|
|
8
|
+
|
|
9
|
+
export type ChosenProfile = { readonly profile: DocumentProfile; readonly from: ProfileSource };
|
|
10
|
+
|
|
11
|
+
export type ProfileRequest = {
|
|
12
|
+
readonly byPath: string | undefined;
|
|
13
|
+
readonly config: string | undefined;
|
|
14
|
+
readonly source: string;
|
|
15
|
+
readonly language: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const named = (definitions: readonly ProfileDefinition[], id: string, language: string): DocumentProfile | undefined =>
|
|
19
|
+
definitions.find((definition) => definition.id === id)?.languages[language];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 強いものから: by_path、chaff.yaml の profile、内容。どれかが none と書けばそこで止め、内容からも選ばない。
|
|
23
|
+
* 名前で選んだ種類にその言語の中身が無ければ選ばない。黙って別の種類にしない。
|
|
24
|
+
*/
|
|
25
|
+
export const chooseProfile = (definitions: readonly ProfileDefinition[], request: ProfileRequest): ChosenProfile | undefined => {
|
|
26
|
+
const written = request.byPath ?? request.config;
|
|
27
|
+
if (written === NO_PROFILE) return undefined;
|
|
28
|
+
if (written !== undefined) {
|
|
29
|
+
const profile = named(definitions, written, request.language);
|
|
30
|
+
return profile === undefined ? undefined : { profile, from: request.byPath === undefined ? "config" : "by-path" };
|
|
31
|
+
}
|
|
32
|
+
const found = definitions.find((definition) => detected(definition, request.source, request.language));
|
|
33
|
+
const profile = found?.languages[request.language];
|
|
34
|
+
return profile === undefined ? undefined : { profile, from: "content" };
|
|
35
|
+
};
|
package/src/reasons.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type Reasons = {
|
|
|
9
9
|
readonly experimental: string;
|
|
10
10
|
readonly turnedOff: string;
|
|
11
11
|
readonly noStructure: (language: string) => string;
|
|
12
|
+
readonly unreadStructure: (clauses: number, units: number) => string;
|
|
12
13
|
readonly noDetector: (name: string) => string;
|
|
13
14
|
readonly noLexicon: (language: string, list: string) => string;
|
|
14
15
|
};
|
|
@@ -27,6 +28,8 @@ export const REASONS: Texts<Reasons> = {
|
|
|
27
28
|
experimental: "まだ試験中のため",
|
|
28
29
|
turnedOff: "設定で止めているため",
|
|
29
30
|
noStructure: (language) => `${language} のパッケージは文書の構造を読めないため`,
|
|
31
|
+
unreadStructure: (clauses, units) =>
|
|
32
|
+
`条項の番号が本文に ${String(clauses)} 個あるのに、番号として読めたのは ${String(units)} 個のため(深い字下げや、行が本文につながった文書)`,
|
|
30
33
|
noDetector: (name) => `検出器 ${name} がないため`,
|
|
31
34
|
noLexicon: (language, list) => `${language} の語彙表 ${list} が無いため`,
|
|
32
35
|
},
|
|
@@ -38,6 +41,8 @@ export const REASONS: Texts<Reasons> = {
|
|
|
38
41
|
experimental: "still experimental",
|
|
39
42
|
turnedOff: "turned off in the settings",
|
|
40
43
|
noStructure: (language) => `the ${language} package cannot read a document's structure`,
|
|
44
|
+
unreadStructure: (clauses, units) =>
|
|
45
|
+
`the text has ${String(clauses)} clause numbers but only ${String(units)} were read as numbered lines (deep indents, or lines run into the text)`,
|
|
41
46
|
noDetector: (name) => `no detector named ${name}`,
|
|
42
47
|
noLexicon: (language, list) => `the ${language} package has no word list ${list}`,
|
|
43
48
|
},
|
package/src/render/compact.ts
CHANGED
|
@@ -2,6 +2,9 @@ import type { RuleDefinition } from "../plugin.ts";
|
|
|
2
2
|
import type { RunResult } from "../run.ts";
|
|
3
3
|
import { messageOf } from "./text.ts";
|
|
4
4
|
|
|
5
|
+
/** 「1 finding」「2 findings」。 */
|
|
6
|
+
const counted = (count: number, noun: string): string => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
|
|
7
|
+
|
|
5
8
|
/** エンジニア向け。1 件 2 行。 */
|
|
6
9
|
export const renderCompact = (header: string, result: RunResult, rules: readonly RuleDefinition[], language: string): string => {
|
|
7
10
|
const byId = new Map(rules.map((rule) => [rule.id, rule]));
|
|
@@ -11,7 +14,7 @@ export const renderCompact = (header: string, result: RunResult, rules: readonly
|
|
|
11
14
|
const at = `${finding.line}:${finding.column}`.padEnd(8);
|
|
12
15
|
return [` ${at}${finding.severity.padEnd(8)}${messageOf(rule, finding, language)}`, ` ${" ".repeat(8)}${finding.rule}`];
|
|
13
16
|
});
|
|
14
|
-
const tail = [
|
|
17
|
+
const tail = [counted(result.findings.length, "finding"), result.skipped.length > 0 ? `${counted(result.skipped.length, "rule")} not run` : undefined].filter(
|
|
15
18
|
(part) => part !== undefined,
|
|
16
19
|
);
|
|
17
20
|
return ["", header, "", ...lines, "", tail.join(", "), ""].join("\n");
|
package/src/run.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { resolve } from "./levels.ts";
|
|
|
3
3
|
import type { AdapterNeeds, Finding, Level, ProseDocument, RuleDefinition } from "./plugin.ts";
|
|
4
4
|
import { lineStarts, placeOf } from "./position.ts";
|
|
5
5
|
import { REASONS, type Reasons } from "./reasons.ts";
|
|
6
|
+
import { joinWords } from "./detectors/word-list.ts";
|
|
7
|
+
import { unreadStructure, type Unread } from "./structure/unread.ts";
|
|
8
|
+
import { isMarkdownPath } from "./structure/markdown-path.ts";
|
|
6
9
|
import { uiLanguageOf } from "./ui.ts";
|
|
7
10
|
|
|
8
11
|
export type Skipped = { readonly rule: string; readonly why: string };
|
|
@@ -26,6 +29,39 @@ const levelFor = (rule: RuleDefinition, settings: Settings, experimental: boolea
|
|
|
26
29
|
|
|
27
30
|
const reasonsFor = (doc: ProseDocument): Reasons => REASONS[uiLanguageOf(doc.language)];
|
|
28
31
|
|
|
32
|
+
/**
|
|
33
|
+
* 構造の rule が動けない理由。木を作れない言語か、番号の行を読めなかった文書。
|
|
34
|
+
* 読めなかった文書で「参照先が無い」が 0 件なのは、確かめた結果ではないので、読めなかったと言う。
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* 木を読む rule の要求。structure は番号の並びを読む(読めなかった文書では動かない)。dates と quantities は木の日付と数量だけを
|
|
38
|
+
* 読むので、番号を読めなかった文書でも動く。どちらも capability ではなく、adapter が木を作れるかで決まる。
|
|
39
|
+
*/
|
|
40
|
+
const TREE_NEEDS: ReadonlySet<string> = new Set(["structure", "dates", "quantities"]);
|
|
41
|
+
const VALUE_NEEDS: readonly string[] = ["dates", "quantities"];
|
|
42
|
+
|
|
43
|
+
const treeNeed = (rule: RuleDefinition, doc: ProseDocument): string | undefined => {
|
|
44
|
+
if (rule.requires.includes("structure")) return treeProblem(doc);
|
|
45
|
+
return rule.requires.some((need) => VALUE_NEEDS.includes(need)) && doc.structure === undefined ? reasonsFor(doc).noStructure(doc.language) : undefined;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const treeProblem = (doc: ProseDocument): string | undefined => {
|
|
49
|
+
if (doc.structure === undefined) return reasonsFor(doc).noStructure(doc.language);
|
|
50
|
+
const unread = unreadOf(doc);
|
|
51
|
+
return unread === undefined ? undefined : reasonsFor(doc).unreadStructure(unread.clauses, unread.units);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** 木が読んだのと同じ本文。Markdown はコードを覆ったもの、.txt はそのまま(.txt の字下げはコードではない)。 */
|
|
55
|
+
const textTheTreeRead = (doc: ProseDocument): string => (isMarkdownPath(doc.path) ? (doc.prose ?? doc.source) : doc.source);
|
|
56
|
+
|
|
57
|
+
const unreadByDocument = new WeakMap<ProseDocument, Unread | undefined>();
|
|
58
|
+
|
|
59
|
+
/** 文書ごとに一度だけ数える。構造の rule が三つあっても、本文を三度なめない。 */
|
|
60
|
+
const unreadOf = (doc: ProseDocument): Unread | undefined => {
|
|
61
|
+
if (!unreadByDocument.has(doc)) unreadByDocument.set(doc, doc.structure === undefined ? undefined : unreadStructure(textTheTreeRead(doc), doc.structure));
|
|
62
|
+
return unreadByDocument.get(doc);
|
|
63
|
+
};
|
|
64
|
+
|
|
29
65
|
/** 知らない要求は満たされていないものとして扱う。黙って無視すると、要求なしで動いてしまう。 */
|
|
30
66
|
const has = (capabilities: ProseDocument["capabilities"], need: string): boolean => {
|
|
31
67
|
if (need === "pos") return capabilities.pos;
|
|
@@ -45,7 +81,7 @@ const hasTokens = (doc: ProseDocument): boolean => doc.sentences.length === 0 ||
|
|
|
45
81
|
/** 要求を満たさない rule は動かせない。満たさないまま動かすと「指摘 0 件」が保証に見える。 */
|
|
46
82
|
const unmet = (rule: RuleDefinition, doc: ProseDocument): string | undefined => {
|
|
47
83
|
if (rule.languages !== undefined && !rule.languages.includes(doc.language)) return reasonsFor(doc).otherLanguage(doc.language);
|
|
48
|
-
const missing = rule.requires.filter((need) => need
|
|
84
|
+
const missing = rule.requires.filter((need) => !TREE_NEEDS.has(need)).find((need) => !has(doc.capabilities, need));
|
|
49
85
|
if (missing !== undefined) return reasonsFor(doc).noCapability(missing);
|
|
50
86
|
return undefined;
|
|
51
87
|
};
|
|
@@ -84,7 +120,7 @@ const place = (starts: readonly number[], finding: Finding): Finding => {
|
|
|
84
120
|
* 元の指摘は消さない。`padded-intro` のように単独でも正しい指摘が混ざっており、
|
|
85
121
|
* まとめるために消すと本物の指摘が見えなくなる。
|
|
86
122
|
*/
|
|
87
|
-
const compositeOf = (rule: RuleDefinition, found: readonly Finding[], limit: number, starts: readonly number[]): Finding[] => {
|
|
123
|
+
const compositeOf = (rule: RuleDefinition, found: readonly Finding[], limit: number, starts: readonly number[], language: string): Finding[] => {
|
|
88
124
|
const fired = rule.from.filter((id) => found.some((finding) => finding.rule === id));
|
|
89
125
|
if (fired.length < limit) return [];
|
|
90
126
|
const first = found.find((finding) => fired.includes(finding.rule));
|
|
@@ -95,7 +131,7 @@ const compositeOf = (rule: RuleDefinition, found: readonly Finding[], limit: num
|
|
|
95
131
|
line: 0,
|
|
96
132
|
column: 0,
|
|
97
133
|
quote: first?.quote ?? "",
|
|
98
|
-
values: { word: fired
|
|
134
|
+
values: { word: joinWords(fired, language), count: fired.length, limit, offset: first === undefined ? 0 : Number(first.values["offset"] ?? 0) },
|
|
99
135
|
}),
|
|
100
136
|
];
|
|
101
137
|
};
|
|
@@ -135,9 +171,8 @@ export const runRules = (
|
|
|
135
171
|
if (noTags !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: noTags }] };
|
|
136
172
|
// 木は capability ではなく、adapter が structure を持つかで決まる。持たない言語で動かすと「参照先が無い」が 0 件に見える。
|
|
137
173
|
// 段階を見た後で聞く。doc.structure は触れたときに木を作るので、止めている rule のために作らない。
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
174
|
+
const noTree = treeNeed(rule, doc);
|
|
175
|
+
if (noTree !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: noTree }] };
|
|
141
176
|
// 複合シグナルは二段目で扱う。一段目では「検出器が無い」と言わせない。
|
|
142
177
|
if (rule.from.length > 0) return acc;
|
|
143
178
|
const detector = DETECTORS[rule.how_to_find];
|
|
@@ -159,7 +194,7 @@ export const runRules = (
|
|
|
159
194
|
);
|
|
160
195
|
const composites = applicable
|
|
161
196
|
.filter((rule) => rule.from.length > 0 && levelFor(rule, settings, experimental) !== "off")
|
|
162
|
-
.flatMap((rule) => compositeOf(rule, outcome.findings, limitFor(rule, levelFor(rule, settings, experimental), genre, limits), starts));
|
|
197
|
+
.flatMap((rule) => compositeOf(rule, outcome.findings, limitFor(rule, levelFor(rule, settings, experimental), genre, limits), starts, doc.language));
|
|
163
198
|
const all = [...outcome.findings, ...composites];
|
|
164
199
|
return { ...outcome, findings: [...all].sort((left, right) => left.line - right.line), forcedExperimental: forced };
|
|
165
200
|
};
|