gcf-aif-ext 0.5.0 → 0.5.2
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/README.md +11 -0
- package/extension.json +1 -1
- package/lib/design-check.js +24 -6
- package/lib/systems-doc.js +6 -2
- package/node_modules/cnlp-kit/CHANGELOG.md +207 -0
- package/node_modules/cnlp-kit/README.md +40 -3
- package/node_modules/cnlp-kit/bin/cnlp.js +71 -10
- package/node_modules/cnlp-kit/cnlp-format.md +36 -3
- package/node_modules/cnlp-kit/package.json +1 -1
- package/node_modules/cnlp-kit/profiles/docset.md +103 -103
- package/node_modules/cnlp-kit/skills/cnlp-docset/SKILL.md +6 -4
- package/node_modules/cnlp-kit/src/cnlp.js +71 -9
- package/node_modules/cnlp-kit/src/config.js +233 -15
- package/package.json +2 -2
- package/profiles/glossary.md +1 -1
- package/profiles/system.md +2 -2
- package/profiles/unit-constraint.md +3 -3
- package/profiles/unit-entity.md +3 -3
- package/profiles/unit-mechanic.md +6 -18
- package/profiles/unit-resource.md +6 -18
- package/profiles/unit-rule.md +3 -3
- package/skills/gcf-card/SKILL.md +3 -2
- package/templates/agents-block.md +1 -2
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: docset
|
|
3
|
-
description: The CNL-P profile of a docset index — the entry page of a library's agent documentation, and the closed list of what that library offers.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
format: cnlp-format.md
|
|
7
|
-
|
|
8
|
-
mood: imperative
|
|
9
|
-
|
|
10
|
-
headings:
|
|
11
|
-
- none: an index is one flat run of blocks, and the file name carries the title
|
|
12
|
-
|
|
13
|
-
frontmatter_fields:
|
|
14
|
-
- none: an index is read by path, so it carries no routing contract to restate
|
|
15
|
-
|
|
16
|
-
lexicon_exempt:
|
|
17
|
-
- none: an index has no frontmatter, so nothing is exempt
|
|
18
|
-
|
|
19
|
-
sections:
|
|
20
|
-
- key: identity
|
|
21
|
-
form: keyed-block
|
|
22
|
-
required: yes
|
|
23
|
-
record_keys: library, version, source
|
|
24
|
-
optional_record_keys: derived_from
|
|
25
|
-
note: the library covered and the revision it was read at;
|
|
26
|
-
|
|
27
|
-
- key: purpose
|
|
28
|
-
form: bullet-list
|
|
29
|
-
required: yes
|
|
30
|
-
note: what the library does, and the kind of task a reader reaches for it with
|
|
31
|
-
|
|
32
|
-
- key: read_protocol
|
|
33
|
-
form: numbered-list
|
|
34
|
-
required: yes
|
|
35
|
-
note:
|
|
36
|
-
|
|
37
|
-
- key: install
|
|
38
|
-
form: bullet-list
|
|
39
|
-
required: no
|
|
40
|
-
note: how a project takes the dependency; delete the block when the docset covers a dependency the project already carries
|
|
41
|
-
|
|
42
|
-
- key: canonical_use
|
|
43
|
-
form: keyed-block
|
|
44
|
-
required: yes
|
|
45
|
-
record_keys: shows
|
|
46
|
-
note: 1 complete working use of the library, carrying every load-bearing element; a reader holding only the index writes the common case from this block, and an index without one is a table of contents, which is enough for nothing
|
|
47
|
-
|
|
48
|
-
- key: walkthrough
|
|
49
|
-
form: record-list
|
|
50
|
-
required: yes
|
|
51
|
-
record_keys: part, page
|
|
52
|
-
note: each part of `canonical_use:` and the page owning its detail; a record and not a comment inside the fence, because a fence is opaque and a link inside one reaches no reader and no check
|
|
53
|
-
|
|
54
|
-
- key: concepts
|
|
55
|
-
form: record-list
|
|
56
|
-
required: yes
|
|
57
|
-
record_keys: page, does
|
|
58
|
-
note: the models a reader holds before any page reads correctly; a record and not a line, so every link in a docset is a `page:` value and a resolver guesses at none of them
|
|
59
|
-
|
|
60
|
-
- key: tasks
|
|
61
|
-
form: record-list
|
|
62
|
-
required: yes
|
|
63
|
-
record_keys: page, does
|
|
64
|
-
note: the goals a reader arrives with; a catalog keyed on capability routes a reader holding a symbol and leaves one holding a goal to guess
|
|
65
|
-
|
|
66
|
-
- key: catalog
|
|
67
|
-
form: record-list
|
|
68
|
-
required: yes
|
|
69
|
-
record_keys: capability, does, status
|
|
70
|
-
optional_record_keys: page, read, instead, fingerprint
|
|
71
|
-
record_values: status = covered, source, avoid
|
|
72
|
-
note: "every capability the library offers, in 1 record each; status is covered, source or avoid, and a symbol absent here is a symbol the library lacks; fingerprint changes when the source does, so a record without one cannot report drift"
|
|
73
|
-
|
|
74
|
-
- key: excluded
|
|
75
|
-
form: bullet-list
|
|
76
|
-
required: yes
|
|
77
|
-
note: a whole region the docset leaves out, with the reason; a region named in neither this block nor the catalog reads as an omission nobody decided
|
|
78
|
-
|
|
79
|
-
- key: companions
|
|
80
|
-
form: record-list
|
|
81
|
-
required: yes
|
|
82
|
-
record_keys: docset, when, owns
|
|
83
|
-
note: another docset covering what this library does not; `when:` is "always" once its index carries `overrides:`, since a reader loading it on demand misses the correction; "- none" when the library stands alone
|
|
84
|
-
|
|
85
|
-
- key: overrides
|
|
86
|
-
form: record-list
|
|
87
|
-
required: no
|
|
88
|
-
record_keys: page, by, reason
|
|
89
|
-
note: "a page of another docset that stops holding once this package is present, and the page here that replaces it; `page:` names the foreign page and carries its docset prefix, `by:` names the local one and carries none"
|
|
90
|
-
|
|
91
|
-
- key: version_notes
|
|
92
|
-
form: bullet-list
|
|
93
|
-
required: no
|
|
94
|
-
note: what changed in the covered version, for a reader arriving from an older one
|
|
95
|
-
|
|
96
|
-
custom_sections:
|
|
97
|
-
- none: a project fills this from the vocabulary its own docsets already use
|
|
98
|
-
|
|
99
|
-
enforcement:
|
|
100
|
-
- `npx cnlp check`, over the files a `cnlp.config.json` corpus entry maps to this profile
|
|
101
|
-
- any issue is an error: an index routes every other page, so a wrong index misroutes the whole docset
|
|
102
|
-
- `npx cnlp check` resolves every `page:`, `by:` and `docset:` value once the project declares `docsets:`
|
|
103
|
-
- `npx cnlp check` holds `status` to covered, source and avoid; what a status implies — a `read:` on source, an `instead:` on avoid — is caught by review
|
|
1
|
+
---
|
|
2
|
+
name: docset
|
|
3
|
+
description: The CNL-P profile of a docset index — the entry page of a library's agent documentation, and the closed list of what that library offers.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
format: cnlp-format.md
|
|
7
|
+
|
|
8
|
+
mood: imperative
|
|
9
|
+
|
|
10
|
+
headings:
|
|
11
|
+
- none: an index is one flat run of blocks, and the file name carries the title
|
|
12
|
+
|
|
13
|
+
frontmatter_fields:
|
|
14
|
+
- none: an index is read by path, so it carries no routing contract to restate
|
|
15
|
+
|
|
16
|
+
lexicon_exempt:
|
|
17
|
+
- none: an index has no frontmatter, so nothing is exempt
|
|
18
|
+
|
|
19
|
+
sections:
|
|
20
|
+
- key: identity
|
|
21
|
+
form: keyed-block
|
|
22
|
+
required: yes
|
|
23
|
+
record_keys: library, version, source
|
|
24
|
+
optional_record_keys: derived_from
|
|
25
|
+
note: the library covered and the revision it was read at; `version:` opens with the version alone, as a tool reads it up to the first comma; derived_from names the sources, because prose carries less weight than a signature
|
|
26
|
+
|
|
27
|
+
- key: purpose
|
|
28
|
+
form: bullet-list
|
|
29
|
+
required: yes
|
|
30
|
+
note: what the library does, and the kind of task a reader reaches for it with
|
|
31
|
+
|
|
32
|
+
- key: read_protocol
|
|
33
|
+
form: numbered-list
|
|
34
|
+
required: yes
|
|
35
|
+
note: the index is the cost of every question and a page the cost of 1: the common case is written from `canonical_use:`, a page opens for a contract the reader lacks, and a step that stops the reader is the defect
|
|
36
|
+
|
|
37
|
+
- key: install
|
|
38
|
+
form: bullet-list
|
|
39
|
+
required: no
|
|
40
|
+
note: how a project takes the dependency; delete the block when the docset covers a dependency the project already carries
|
|
41
|
+
|
|
42
|
+
- key: canonical_use
|
|
43
|
+
form: keyed-block
|
|
44
|
+
required: yes
|
|
45
|
+
record_keys: shows
|
|
46
|
+
note: 1 complete working use of the library, carrying every load-bearing element; a reader holding only the index writes the common case from this block, and an index without one is a table of contents, which is enough for nothing
|
|
47
|
+
|
|
48
|
+
- key: walkthrough
|
|
49
|
+
form: record-list
|
|
50
|
+
required: yes
|
|
51
|
+
record_keys: part, page
|
|
52
|
+
note: each part of `canonical_use:` and the page owning its detail; a record and not a comment inside the fence, because a fence is opaque and a link inside one reaches no reader and no check
|
|
53
|
+
|
|
54
|
+
- key: concepts
|
|
55
|
+
form: record-list
|
|
56
|
+
required: yes
|
|
57
|
+
record_keys: page, does
|
|
58
|
+
note: the models a reader holds before any page reads correctly; a record and not a line, so every link in a docset is a `page:` value and a resolver guesses at none of them
|
|
59
|
+
|
|
60
|
+
- key: tasks
|
|
61
|
+
form: record-list
|
|
62
|
+
required: yes
|
|
63
|
+
record_keys: page, does
|
|
64
|
+
note: the goals a reader arrives with; a catalog keyed on capability routes a reader holding a symbol and leaves one holding a goal to guess
|
|
65
|
+
|
|
66
|
+
- key: catalog
|
|
67
|
+
form: record-list
|
|
68
|
+
required: yes
|
|
69
|
+
record_keys: capability, does, status
|
|
70
|
+
optional_record_keys: page, read, instead, fingerprint
|
|
71
|
+
record_values: status = covered, source, avoid
|
|
72
|
+
note: "every capability the library offers, in 1 record each; status is covered, source or avoid, and a symbol absent here is a symbol the library lacks; fingerprint changes when the source does, so a record without one cannot report drift"
|
|
73
|
+
|
|
74
|
+
- key: excluded
|
|
75
|
+
form: bullet-list
|
|
76
|
+
required: yes
|
|
77
|
+
note: a whole region the docset leaves out, with the reason; a region named in neither this block nor the catalog reads as an omission nobody decided
|
|
78
|
+
|
|
79
|
+
- key: companions
|
|
80
|
+
form: record-list
|
|
81
|
+
required: yes
|
|
82
|
+
record_keys: docset, when, owns
|
|
83
|
+
note: another docset covering what this library does not; `when:` is "always" once its index carries `overrides:`, since a reader loading it on demand misses the correction; "- none" when the library stands alone
|
|
84
|
+
|
|
85
|
+
- key: overrides
|
|
86
|
+
form: record-list
|
|
87
|
+
required: no
|
|
88
|
+
record_keys: page, by, reason
|
|
89
|
+
note: "a page of another docset that stops holding once this package is present, and the page here that replaces it; `page:` names the foreign page and carries its docset prefix, `by:` names the local one and carries none"
|
|
90
|
+
|
|
91
|
+
- key: version_notes
|
|
92
|
+
form: bullet-list
|
|
93
|
+
required: no
|
|
94
|
+
note: what changed in the covered version, for a reader arriving from an older one
|
|
95
|
+
|
|
96
|
+
custom_sections:
|
|
97
|
+
- none: a project fills this from the vocabulary its own docsets already use
|
|
98
|
+
|
|
99
|
+
enforcement:
|
|
100
|
+
- `npx cnlp check`, over the files a `cnlp.config.json` corpus entry maps to this profile
|
|
101
|
+
- any issue is an error: an index routes every other page, so a wrong index misroutes the whole docset
|
|
102
|
+
- `npx cnlp check` resolves every `page:`, `by:` and `docset:` value once the project declares `docsets:`
|
|
103
|
+
- `npx cnlp check` holds `status` to covered, source and avoid; what a status implies — a `read:` on source, an `instead:` on avoid — is caught by review
|
|
@@ -51,7 +51,7 @@ workflow:
|
|
|
51
51
|
1. run `npx cnlp print format` and read it in full, §9 included: it states what a page reference is, and which of its rules a review holds rather than the checker
|
|
52
52
|
2. read the `docset`, `concept`, `api` and `task` profiles through `npx cnlp print <name>`: their `note:` keys carry the reason behind each block, and the rules and the values are read together
|
|
53
53
|
3. list every source the operator supplied and name the weight each one carries: the strongest weight in that list bounds what every page below claims, so report it before writing anything
|
|
54
|
-
4. run the clone or the install command each source needs, at a fixed revision, and record the library
|
|
54
|
+
4. run the clone or the install command each source needs, at a fixed revision, and record the library in `identity:`, and in `version:` the version alone, then a comma and the revision
|
|
55
55
|
5. fill `derived_from:` in `identity:` with those sources and their weights, so a reader tells a set written from declarations from a set written from prose
|
|
56
56
|
6. list every capability of the library, taking each one from the strongest source that states it: a source at weight 3 or below covers what its author chose to cover, so a capability it omits is a capability still to find
|
|
57
57
|
7. fill `catalog:` with 1 record per capability, every one at `status: source`, `read:` naming where to read it — the file, or the document and the anchor inside it
|
|
@@ -63,9 +63,11 @@ workflow:
|
|
|
63
63
|
13. write 1 task page per goal a reader arrives with, and put the multi-call composition there
|
|
64
64
|
14. update each catalog record to `covered` as its page lands, and fill `excluded:` with every whole region the set leaves out, each with its reason
|
|
65
65
|
15. fill `companions:` when another package extends this library, and `overrides:` when this package corrects a page of the set it extends
|
|
66
|
-
16.
|
|
67
|
-
17.
|
|
68
|
-
18.
|
|
66
|
+
16. name every set `companions:` or `overrides:` names that does not sit beside `<output_directory>`, and the directory holding it: the check resolves a crossing only against a set it can read
|
|
67
|
+
17. run `npx cnlp check <output_directory>`, with `--companion <set>=<dir>` for each set step 16 named, and fix what it reports: it resolves every reference in the set, and reports a page no other page reaches
|
|
68
|
+
18. read the finished set for the rules no check reaches: 1 fact stated on 1 page, 1 running example across the set, 1 page per routed name, and the keys a status implies
|
|
69
|
+
19. read each task page against the API pages it sends a reader to: a member the task steers away from carries that in its own `pitfalls:`, because a full API page otherwise outvotes a task page's preference
|
|
70
|
+
20. report the capabilities covered, the ones left at `source`, the weight the set rests on, and any defect the reading turned up in the library itself
|
|
69
71
|
|
|
70
72
|
invocation:
|
|
71
73
|
- Claude Code: `/cnlp-docset <sources> <output-directory>`, once this folder sits in `.claude/skills/`
|
|
@@ -29,21 +29,57 @@ export const HARD_LIMIT = 250; // §4: 150 is the target, 250 is compound whatev
|
|
|
29
29
|
|
|
30
30
|
const FORMS = new Set(['scalar', 'bullet-list', 'numbered-list', 'record-list', 'keyed-block']);
|
|
31
31
|
const KEY = /^([a-z][a-z0-9_]*):(.*)$/;
|
|
32
|
+
/** §3: a line that is 1 whole comment. `<!-- a --> - item` carries an item, and is read. */
|
|
33
|
+
const COMMENT = /^\s*<!--(?:(?!-->).)*-->\s*$/;
|
|
34
|
+
/** §3: the markers of a region another tool writes into the file. */
|
|
35
|
+
const REGION = /^\s*<!--\s*([\w.-]+):(start|end)\s*-->\s*$/;
|
|
32
36
|
|
|
33
37
|
/**
|
|
34
|
-
* Body split into lines, top-level `key:` sections and `##` headings
|
|
35
|
-
*
|
|
36
|
-
*
|
|
38
|
+
* Body split into lines, top-level `key:` sections and `##` headings. §3: a fence is blanked in
|
|
39
|
+
* both `lines` and `prose`, because its contents are data. An HTML comment and a marked region are
|
|
40
|
+
* blanked in `lines` only, which is the shape the profile reads: both stay in `prose`, which the
|
|
41
|
+
* lexicon reads, because an agent reads the raw file. Leading frontmatter is blanked when
|
|
42
|
+
* present. Line numbers are 1-based within the file.
|
|
37
43
|
*/
|
|
38
44
|
export function parseBlocks(raw) {
|
|
39
|
-
|
|
45
|
+
// Blanked, not removed: a reported line is a line of the file, frontmatter included.
|
|
46
|
+
const body = raw.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, (m) => '\n'.repeat(m.split('\n').length - 1));
|
|
47
|
+
const source = body.split(/\r?\n/);
|
|
48
|
+
// A comment that never closes hides nothing: it would otherwise exempt the rest of the file.
|
|
49
|
+
const lastClose = source.findLastIndex((l) => l.includes('-->'));
|
|
40
50
|
let fenced = false;
|
|
51
|
+
let comment = false;
|
|
41
52
|
const fences = []; // 1-based line numbers a fence covered: blanked, but not absent
|
|
42
|
-
const
|
|
53
|
+
const marks = [];
|
|
54
|
+
const hidden = new Map(); // 0-based line -> what is left of it once its comment is taken off
|
|
55
|
+
const prose = source.map((l, i) => {
|
|
56
|
+
if (comment) {
|
|
57
|
+
const end = l.indexOf('-->');
|
|
58
|
+
if (end >= 0) comment = false;
|
|
59
|
+
hidden.set(i, end < 0 ? '' : l.slice(end + 3));
|
|
60
|
+
return l;
|
|
61
|
+
}
|
|
43
62
|
if (/^\s*```/.test(l)) { fenced = !fenced; fences.push(i + 1); return ''; }
|
|
44
63
|
if (fenced) { fences.push(i + 1); return ''; }
|
|
64
|
+
const mark = l.match(REGION);
|
|
65
|
+
if (mark) marks.push({ name: mark[1], end: mark[2] === 'end', at: i });
|
|
66
|
+
if (COMMENT.test(l)) hidden.set(i, '');
|
|
67
|
+
else if (/^\s*<!--/.test(l) && !l.includes('-->') && i < lastClose) { comment = true; hidden.set(i, ''); }
|
|
45
68
|
return l;
|
|
46
69
|
});
|
|
70
|
+
// An end closes the latest start of its own name, so regions of 2 names never cancel each other.
|
|
71
|
+
// A start with no end marks nothing, and an end with no start is a comment like any other.
|
|
72
|
+
const regions = [];
|
|
73
|
+
const open = new Map();
|
|
74
|
+
for (const m of marks) {
|
|
75
|
+
if (!m.end) open.set(m.name, m.at);
|
|
76
|
+
else if (open.has(m.name)) {
|
|
77
|
+
regions.push({ name: m.name, line: open.get(m.name) + 1, end: m.at + 1 });
|
|
78
|
+
open.delete(m.name);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const inRegion = (i) => regions.some((r) => i + 1 > r.line && i + 1 < r.end);
|
|
82
|
+
const lines = prose.map((l, i) => (inRegion(i) ? '' : hidden.get(i) ?? l));
|
|
47
83
|
const sections = [];
|
|
48
84
|
const headings = [];
|
|
49
85
|
lines.forEach((l, i) => {
|
|
@@ -51,7 +87,8 @@ export function parseBlocks(raw) {
|
|
|
51
87
|
if (key) sections.push({ key: key[1], value: key[2].trim(), line: i + 1 });
|
|
52
88
|
if (/^##\s/.test(l)) headings.push({ text: l.trim(), line: i + 1 });
|
|
53
89
|
});
|
|
54
|
-
|
|
90
|
+
// A fence inside a region is the region's content, not content of the block above the region.
|
|
91
|
+
return { lines, prose, sections, headings, fences: new Set(fences.filter((n) => !inRegion(n - 1))), regions };
|
|
55
92
|
}
|
|
56
93
|
|
|
57
94
|
/** Lines of a section: everything up to the next top-level key or `##` heading. */
|
|
@@ -202,7 +239,7 @@ export async function loadProfile(name, opts) {
|
|
|
202
239
|
* about the document as a whole.
|
|
203
240
|
*/
|
|
204
241
|
export function bodyIssues(raw, profile) {
|
|
205
|
-
const { lines, sections, headings, fences } = parseBlocks(raw);
|
|
242
|
+
const { lines, prose, sections, headings, fences } = parseBlocks(raw);
|
|
206
243
|
const issues = [];
|
|
207
244
|
const at = (line, message) => issues.push({ line, message });
|
|
208
245
|
const byKey = new Map(profile.sections.map((s) => [s.key, s]));
|
|
@@ -271,7 +308,8 @@ export function bodyIssues(raw, profile) {
|
|
|
271
308
|
issues.push(...formIssues(key, spec, items));
|
|
272
309
|
}
|
|
273
310
|
|
|
274
|
-
|
|
311
|
+
// A region is exempt from the file's shape, not from the language: the lexicon reads it.
|
|
312
|
+
issues.push(...lexiconIssues(prose, profile.mood));
|
|
275
313
|
return issues.sort((a, b) => a.line - b.line);
|
|
276
314
|
}
|
|
277
315
|
|
|
@@ -502,7 +540,9 @@ export function lexiconIssues(lines, mood = '') {
|
|
|
502
540
|
// A converted document is where these 2 turn up: a mechanical split reads an ordinal as a
|
|
503
541
|
// sentence and leaves `- 4` behind, and cutting a line to the hard limit lands mid-span.
|
|
504
542
|
const item = l.match(/^\s*(?:[-*]|\d+\.)\s+(.*)$/);
|
|
505
|
-
|
|
543
|
+
// Any script's letter, not an ASCII one: the rule rejects an item made of digits and
|
|
544
|
+
// punctuation, and a body written in Cyrillic or CJK states its claims in letters too.
|
|
545
|
+
if (item && !/\p{L}/u.test(item[1])) issues.push({ line, message: 'the item carries no word — it states no claim' });
|
|
506
546
|
for (const [name, open, close] of UNCLOSED) {
|
|
507
547
|
const n = (l.match(open) || []).length;
|
|
508
548
|
if (close ? n !== (l.match(close) || []).length : n % 2) {
|
|
@@ -531,4 +571,26 @@ export function lexiconIssues(lines, mood = '') {
|
|
|
531
571
|
return issues;
|
|
532
572
|
}
|
|
533
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Whether the §5 lexicon reaches a document. Every word list in §5 — the deny list, the spelled
|
|
576
|
+
* thresholds, the open limits, the comparatives, the light verbs, the passive — is English, so a
|
|
577
|
+
* body written in another script matches none of them and would report clean against rules that
|
|
578
|
+
* never ran. The prose decides it, which is the line with its section key, its heading marker and
|
|
579
|
+
* its quoted spans taken off: a key is this format's own vocabulary and English in every document,
|
|
580
|
+
* and a Russian line naming `CombatFeature.cs` is still a Russian line.
|
|
581
|
+
* ponytail: this reads script, not language — a German or a Spanish body reads as English here, and
|
|
582
|
+
* only its reader can say otherwise.
|
|
583
|
+
*/
|
|
584
|
+
export function lexiconApplies(raw) {
|
|
585
|
+
const prose = raw
|
|
586
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
587
|
+
.split(/\r?\n/)
|
|
588
|
+
.filter((l) => !/^\s*#/.test(l))
|
|
589
|
+
.map((l) => unquoted(l).replace(/^\s*(?:[-*]\s+|\d+\.\s+)?[a-z][a-z0-9_]*:/, ' '))
|
|
590
|
+
.join('\n');
|
|
591
|
+
const letters = prose.match(/\p{L}/gu) ?? [];
|
|
592
|
+
const latin = letters.filter((c) => /[a-z]/i.test(c)).length;
|
|
593
|
+
return latin * 2 >= letters.length;
|
|
594
|
+
}
|
|
595
|
+
|
|
534
596
|
export { FORMS };
|