@capsiynau/intelligence 0.4.0 → 0.5.1

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 CHANGED
@@ -6,6 +6,81 @@ All notable changes to `@capsiynau/intelligence` go here. Format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.1] - 2026-07-25
10
+
11
+ **Welsh review of the 0.5.0 seed lexicon. Four of the twelve entries were
12
+ wrong, two of them badly enough to emit ungrammatical Welsh.** Patch rather
13
+ than minor because no consumer resolves 0.5.x yet (`notes-app` and `postio`
14
+ are both on `^0.4.0`, and caret on a 0.x range permits patch only), so this
15
+ corrects a defect nobody has installed.
16
+
17
+ ### Fixed
18
+
19
+ - **`efo`/`gyda` and `isio`/`moyn` are now detect-only.** Both are genuine
20
+ regional pairs, but substituting them changes the grammar of the words
21
+ around them, and this module is mutation-blind and syntax-blind by design.
22
+ `gyda` takes the aspirate mutation and `efo` takes none, so the bare swap
23
+ gave `gyda cath` where the correct form is `gyda chath`. `moyn` takes a
24
+ linking `yn` and `isio` does not, so the bare swap gave `Dw i moyn`, which
25
+ is not grammatical. `checkDialect` still reports both; `applyDialect` now
26
+ skips them. Emitting broken Welsh is worse than leaving a term alone and
27
+ telling a human.
28
+ - **Removed `pres`/`arian` (money) and `dŵad`/`dod` (to come).** These are not
29
+ regional pairs. `arian` and `dod` are the universal standard forms, used in
30
+ the north as readily as the south; `pres` and `dŵad` are northern
31
+ colloquialisms. Filing them as regional meant telling a northern writer that
32
+ their standard Welsh was southern. Register belongs to the prompt fragment,
33
+ not this table. The reason is recorded in `EXCLUDED_TERMS` so the next person
34
+ does not re-add them.
35
+
36
+ ### Added
37
+
38
+ - `applySafe` on every `checkDialect` finding and every `listDialectTerms`
39
+ row, plus `applyNote` on detect-only entries. A caller can now tell the
40
+ difference between "this was corrected" and "this needs a human", rather
41
+ than assuming a clean `applyDialect` pass resolved everything.
42
+
43
+ ### Note
44
+
45
+ The lexicon has now had a machine-assisted review against house style and
46
+ known Welsh error patterns. That is **not** a native sign-off.
47
+ `DIALECT_TERMS_STATUS.reviewed` stays `false`.
48
+
49
+ ## [0.5.0] - 2026-07-25
50
+
51
+ Additive. No existing import path changes shape, so 0.4.x consumers can
52
+ upgrade without touching their code.
53
+
54
+ ### Added
55
+
56
+ - **`dialect` subpath.** The Welsh regional lexicon as data rather than as a
57
+ prompt. `applyDialect(text, region)` rewrites off-region word choices
58
+ deterministically and case-preservingly ("Rŵan" to "Nawr", "mas" to
59
+ "allan"); `checkDialect(text, region)` reports the same findings WITHOUT
60
+ changing the text, so a reviewer gets evidence rather than a silent
61
+ rewrite; `listDialectTerms(region)` exposes the table for a management
62
+ screen or a sign-off export. `region` accepts the same values as
63
+ `getDialectSystemPrompt`, with `broadcast` and `formal` both mapping onto
64
+ the neutral lexicon.
65
+
66
+ This does not replace `getDialectSystemPrompt`. Register ("avoid
67
+ colloquialisms") has no deterministic transform and stays a prompt
68
+ concern; word choice does have one and should not be left to a model that
69
+ applies it unevenly and unverifiably. The two compose.
70
+
71
+ Deliberate limits, all pinned by tests: mutated surface forms are not
72
+ matched (needs syntactic context, as in `welsh/mutations.js`), pronoun and
73
+ copula forms are excluded because "mae o'r ardal" would be corrupted, and
74
+ where both regional forms are marked with no neutral standard
75
+ (taid/tad-cu) a neutral target leaves the text alone rather than picking a
76
+ side. Word boundaries use lookarounds over a Welsh letter class, not `\b`,
77
+ which treats the diacritic in "rŵan" as a word boundary and matches inside
78
+ the word.
79
+
80
+ **The seed table is not natively reviewed.** `DIALECT_TERMS_STATUS`
81
+ reports `reviewed: false` in code, and consumers should treat output as a
82
+ draft until that changes.
83
+
9
84
  ## [0.4.0] - 2026-07-22
10
85
 
11
86
  First release to ship TypeScript declarations. Everything here is
package/package.json CHANGED
@@ -1,23 +1,24 @@
1
1
  {
2
2
  "name": "@capsiynau/intelligence",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Bilingual (Welsh + English) intelligence layer: text normalisation, mutation handling, digraph splitting, spellcheck primitives, and glossary/correction utilities. Pure functions, runs in Node or browser.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Aled Parry <aled@aledparry.com>",
8
- "homepage": "https://github.com/aledprysparry/Capsiynau_V2_ClaudeAI/tree/main/packages/intelligence",
8
+ "homepage": "https://github.com/aledprysparry/Capsiynau.com/tree/main/packages/intelligence",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/aledprysparry/Capsiynau_V2_ClaudeAI.git",
11
+ "url": "git+https://github.com/aledprysparry/Capsiynau.com.git",
12
12
  "directory": "packages/intelligence"
13
13
  },
14
14
  "bugs": {
15
- "url": "https://github.com/aledprysparry/Capsiynau_V2_ClaudeAI/issues"
15
+ "url": "https://github.com/aledprysparry/Capsiynau.com/issues"
16
16
  },
17
17
  "keywords": [
18
18
  "welsh",
19
19
  "cymraeg",
20
20
  "bilingual",
21
+ "dialect",
21
22
  "spellcheck",
22
23
  "mutation",
23
24
  "digraph",
@@ -56,6 +57,10 @@
56
57
  "./speakers": {
57
58
  "types": "./types/speakers/index.d.ts",
58
59
  "default": "./src/speakers/index.js"
60
+ },
61
+ "./dialect": {
62
+ "types": "./types/dialect/index.d.ts",
63
+ "default": "./src/dialect/index.js"
59
64
  }
60
65
  },
61
66
  "files": [
@@ -0,0 +1,299 @@
1
+ /**
2
+ * @capsiynau/intelligence/dialect
3
+ *
4
+ * The Welsh regional LEXICON as data, not as a prompt.
5
+ *
6
+ * `welsh/normaliser.js` already exposes `getDialectSystemPrompt()`, which
7
+ * returns a register instruction ("avoid colloquialisms", "maintain formal
8
+ * third-person constructions"). Register genuinely is a prompt concern -
9
+ * there is no deterministic transform for "sound less formal".
10
+ *
11
+ * Word choice is not. "rŵan" versus "nawr" is a dictionary lookup, and a
12
+ * dictionary applied in code is right every time, costs nothing, and can be
13
+ * shown to a reviewer. Asked of a model in a system prompt it is applied
14
+ * unevenly and cannot be verified afterwards. So this module owns the lexical
15
+ * half and the prompt fragment keeps the register half. They compose; neither
16
+ * replaces the other.
17
+ *
18
+ * What this module DOES:
19
+ * - `checkDialect(text, region)` - report off-region terms, mutating nothing
20
+ * - `applyDialect(text, region)` - deterministic case-preserving substitution
21
+ * - `listDialectTerms(region)` - expose the table for review UI and export
22
+ *
23
+ * What this module does NOT do:
24
+ * - Decide register, tone or formality. That is the prompt fragment's job.
25
+ * - Match MUTATED surface forms. "taid" after a possessive is "daid", and
26
+ * recognising that needs syntactic context, exactly as documented in
27
+ * `welsh/mutations.js`. Callers normalise first or accept the miss.
28
+ * - Touch pronouns or copula forms. See EXCLUDED_TERMS below; the false
29
+ * positives are worse than the misses.
30
+ * - Rewrite a term whose swap would change the grammar around it. Those
31
+ * entries carry `applySafe: false`: checkDialect REPORTS them, applyDialect
32
+ * SKIPS them. "efo" -> "gyda" is the clear case, since gyda takes the
33
+ * aspirate mutation and efo takes none, so the bare swap is ungrammatical.
34
+ * - Carry colloquial-versus-standard pairs. "arian" and "dod" are standard
35
+ * everywhere; "pres" and "dŵad" are northern colloquialisms, not southern
36
+ * opposites. That is register, and register belongs to the prompt fragment.
37
+ * - Infer a region from text. The caller declares it, per the house rule
38
+ * that source language and variety are never auto-detected.
39
+ *
40
+ * Pure JS, no external deps, no I/O. Runs in Node or the browser.
41
+ */
42
+
43
+ /**
44
+ * Provenance for the seed table, per the "evidence beats confidence" rule:
45
+ * state what this data IS rather than implying an authority it has not earned.
46
+ * These are widely attested north/south pairs, but they are a starting seed
47
+ * compiled alongside the existing dialect prompt fragments - NOT a reviewed
48
+ * lexicographic source. A native reviewer signs this off before any consumer
49
+ * treats `applyDialect` output as publishable without a human read.
50
+ */
51
+ export const DIALECT_TERMS_STATUS = {
52
+ reviewed: false,
53
+ reviewedBy: null,
54
+ seededOn: '2026-07-25',
55
+ note: 'Seed table. Needs native review before authoritative use.',
56
+ }
57
+
58
+ // Welsh letters, including the diacritics JS `\w` and `\b` do not cover. `\b`
59
+ // is unusable here: to `\b` the `ŵ` in `rŵan` is a NON-word character, so
60
+ // /\brŵan\b/ finds a boundary in the middle of the word and matches inside
61
+ // longer tokens. Lookarounds over this class are the fix.
62
+ const WELSH_LETTERS = "A-Za-zÂÊÎÔÛŴŶâêîôûŵŷÁÉÍÓÚáéíóúÀÈÌÒÙàèìòùÄËÏÖÜäëïöü'"
63
+
64
+ /**
65
+ * Seed lexicon. `neutral` is the form a broadcast or formal register would
66
+ * use; `null` means BOTH forms are regionally marked and there is no neutral
67
+ * standard, so a neutral target leaves the text alone rather than inventing
68
+ * one. Saying "no neutral form exists" is more useful than picking a side.
69
+ */
70
+ export const DIALECT_TERMS = [
71
+ { id: 'now', north: 'rŵan', south: 'nawr', neutral: 'nawr', gloss: 'now' },
72
+ { id: 'milk', north: 'llefrith', south: 'llaeth', neutral: 'llaeth', gloss: 'milk' },
73
+ { id: 'out', north: 'allan', south: 'mas', neutral: 'allan', gloss: 'out' },
74
+ { id: 'boy', north: 'hogyn', south: 'crwt', neutral: 'bachgen', gloss: 'boy' },
75
+ { id: 'girl', north: 'hogan', south: 'croten', neutral: 'merch', gloss: 'girl' },
76
+ { id: 'cuppa', north: 'panad', south: 'dishgled', neutral: 'paned', gloss: 'a cuppa' },
77
+
78
+ // Both forms regionally marked, no neutral standard.
79
+ { id: 'grandfather', north: 'taid', south: 'tad-cu', neutral: null, gloss: 'grandfather' },
80
+ { id: 'grandmother', north: 'nain', south: 'mam-gu', neutral: null, gloss: 'grandmother' },
81
+
82
+ // ── Detect-only: real regional pairs, but SUBSTITUTING them changes the
83
+ // grammar of the surrounding words, and this module is mutation-blind and
84
+ // syntax-blind by design. checkDialect reports them (always safe, and it is
85
+ // exactly what a reviewer wants to see); applyDialect refuses to rewrite
86
+ // them, because a swap that produces ungrammatical Welsh is worse than no
87
+ // swap at all.
88
+ {
89
+ id: 'with', north: 'efo', south: 'gyda', neutral: 'gyda', gloss: 'with',
90
+ applySafe: false,
91
+ applyNote:
92
+ '"gyda" triggers the aspirate mutation and "efo" triggers none, so a ' +
93
+ 'bare swap yields "gyda cath" where the correct form is "gyda chath". ' +
94
+ 'Fixing it needs the mutation machinery this module does not have.',
95
+ },
96
+ {
97
+ id: 'want', north: 'isio', south: 'moyn', neutral: 'eisiau', gloss: 'to want',
98
+ applySafe: false,
99
+ applyNote:
100
+ 'The three forms take different syntax: "Dw i isio" and "Dw i eisiau" ' +
101
+ 'take no linking "yn", but "moyn" does ("Dw i\'n moyn"). A bare swap ' +
102
+ 'yields "Dw i moyn", which is not grammatical.',
103
+ },
104
+ ]
105
+
106
+ /**
107
+ * Deliberately NOT in the table, with the reason. Kept in code because the
108
+ * next person to extend this will reach for exactly these, and the reason
109
+ * they are absent is not self-evident.
110
+ */
111
+ export const EXCLUDED_TERMS = [
112
+ {
113
+ id: 'colloquial-not-regional',
114
+ reason:
115
+ 'Pairs like "pres"/"arian" (money) and "dŵad"/"dod" (to come) look ' +
116
+ 'regional but are not: "arian" and "dod" are the universal standard ' +
117
+ 'forms, used in the north as readily as the south, and "pres"/"dŵad" ' +
118
+ 'are northern COLLOQUIAL forms. That is a register distinction, and ' +
119
+ 'register is the prompt fragment\'s job, not this table\'s. Filing them ' +
120
+ 'here would have told a northern writer their standard Welsh was ' +
121
+ 'southern. Removed 25.07.2026 after review.',
122
+ },
123
+ {
124
+ id: 'pronoun-o-e',
125
+ reason:
126
+ 'North "mae o" / south "mae e". The north form collides with the ' +
127
+ 'preposition "o" (from): substituting in "mae o\'r ardal" (is from the ' +
128
+ 'area) produces nonsense. Needs part-of-speech context.',
129
+ },
130
+ {
131
+ id: 'up-lan',
132
+ reason:
133
+ 'South "lan" (up) collides with "glan" soft-mutated to "lan" (shore), ' +
134
+ 'so "ar lan y môr" would be rewritten. Needs mutation awareness.',
135
+ },
136
+ {
137
+ id: 'watching-gwatsiad',
138
+ reason:
139
+ 'Appears in the north prompt fragment but is an anglicism, not a ' +
140
+ 'north/south pair. Codifying it as regional Welsh would be wrong.',
141
+ },
142
+ ]
143
+
144
+ // `broadcast` and `formal` both want the unmarked standard form, so they map
145
+ // onto the same lexical target. They still differ in REGISTER, which is the
146
+ // prompt fragment's concern, not this module's.
147
+ const REGION_ALIASES = {
148
+ north: 'north',
149
+ south: 'south',
150
+ neutral: 'neutral',
151
+ broadcast: 'neutral',
152
+ formal: 'neutral',
153
+ }
154
+
155
+ /** @param {string} region @returns {'north'|'south'|'neutral'} */
156
+ function resolveRegion(region) {
157
+ return REGION_ALIASES[region] || 'neutral'
158
+ }
159
+
160
+ function escapeRegExp(s) {
161
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
162
+ }
163
+
164
+ /**
165
+ * Whole-term matcher. Multi-word entries ("tad-cu", and any future entry with
166
+ * a space) tolerate variable internal whitespace.
167
+ */
168
+ function matcherFor(term) {
169
+ const body = escapeRegExp(term).replace(/\s+/g, '\\s+')
170
+ return new RegExp(`(?<![${WELSH_LETTERS}])(${body})(?![${WELSH_LETTERS}])`, 'gi')
171
+ }
172
+
173
+ /**
174
+ * Carry the observed capitalisation onto the replacement, so "Rŵan" becomes
175
+ * "Nawr" and "RŴAN" becomes "NAWR". Mixed case falls through to the table's
176
+ * own casing rather than guessing.
177
+ */
178
+ function matchCase(observed, replacement) {
179
+ if (observed === observed.toUpperCase() && observed !== observed.toLowerCase()) {
180
+ return replacement.toUpperCase()
181
+ }
182
+ if (observed[0] === observed[0].toUpperCase()) {
183
+ return replacement[0].toUpperCase() + replacement.slice(1)
184
+ }
185
+ return replacement
186
+ }
187
+
188
+ /**
189
+ * The entries that carry a usable target for `region`, each with the forms
190
+ * that should be replaced by it. Entries with no neutral form are skipped for
191
+ * a neutral target rather than forced onto one side.
192
+ */
193
+ function activeEntries(region) {
194
+ const resolved = resolveRegion(region)
195
+ const out = []
196
+ for (const entry of DIALECT_TERMS) {
197
+ const preferred = entry[resolved]
198
+ if (!preferred) continue
199
+ // Dedupe by lowercase form. Two regions often share a form (south and
200
+ // neutral are both "gyda"), and without this the same occurrence is
201
+ // reported twice by checkDialect.
202
+ const seen = new Set()
203
+ const alternatives = []
204
+ for (const r of ['north', 'south', 'neutral']) {
205
+ const form = r === resolved ? null : entry[r]
206
+ if (!form) continue
207
+ const key = form.toLowerCase()
208
+ if (key === preferred.toLowerCase() || seen.has(key)) continue
209
+ seen.add(key)
210
+ alternatives.push(form)
211
+ }
212
+ if (alternatives.length) {
213
+ out.push({ entry, preferred, alternatives, applySafe: entry.applySafe !== false })
214
+ }
215
+ }
216
+ return out
217
+ }
218
+
219
+ /**
220
+ * The lexicon as a reviewable list. This is what a "your preferred terms"
221
+ * management screen renders, and what a workspace exports for sign-off.
222
+ *
223
+ * @param {'north'|'south'|'neutral'|'broadcast'|'formal'} region
224
+ * @returns {Array<{id:string, gloss:string, preferred:string, alternatives:string[]}>}
225
+ */
226
+ export function listDialectTerms(region) {
227
+ return activeEntries(region).map(({ entry, preferred, alternatives, applySafe }) => ({
228
+ id: entry.id,
229
+ gloss: entry.gloss,
230
+ preferred,
231
+ alternatives,
232
+ applySafe,
233
+ // Present only on detect-only entries, so a UI can explain to the user
234
+ // why this one is flagged but never auto-corrected.
235
+ ...(entry.applyNote ? { applyNote: entry.applyNote } : {}),
236
+ }))
237
+ }
238
+
239
+ /**
240
+ * Report off-region terms WITHOUT changing the text. This is the validator
241
+ * half: it produces evidence a reviewer can act on, which is what makes the
242
+ * lexical layer auditable rather than merely applied.
243
+ *
244
+ * @param {string} text
245
+ * @param {'north'|'south'|'neutral'|'broadcast'|'formal'} region
246
+ * @returns {Array<{id:string, found:string, expected:string, index:number}>}
247
+ * Findings in document order. Empty array for empty input.
248
+ */
249
+ export function checkDialect(text, region) {
250
+ if (!text) return []
251
+ const findings = []
252
+ for (const { entry, preferred, alternatives, applySafe } of activeEntries(region)) {
253
+ for (const alternative of alternatives) {
254
+ const matcher = matcherFor(alternative)
255
+ let hit
256
+ while ((hit = matcher.exec(text)) !== null) {
257
+ findings.push({
258
+ id: entry.id,
259
+ found: hit[1],
260
+ expected: preferred,
261
+ index: hit.index,
262
+ // false means applyDialect will NOT fix this one: the caller must
263
+ // hand it to a human rather than assume a rewrite resolved it.
264
+ applySafe,
265
+ })
266
+ }
267
+ }
268
+ }
269
+ return findings.sort((a, b) => a.index - b.index)
270
+ }
271
+
272
+ /**
273
+ * Rewrite off-region terms to the target region's form. Deterministic and
274
+ * case-preserving; returns the input unchanged when there is nothing to do.
275
+ *
276
+ * Substitution is whole-term only and mutation-blind, so it is safe to run on
277
+ * mixed Welsh/English text: no entry in the table is an English word.
278
+ *
279
+ * Entries marked `applySafe: false` are SKIPPED here and reported by
280
+ * `checkDialect` instead. Those are pairs whose substitution would change the
281
+ * grammar of neighbouring words (mutation or linking "yn"), which this module
282
+ * has no machinery to get right. Emitting ungrammatical Welsh is worse than
283
+ * leaving the term alone and telling a human about it.
284
+ *
285
+ * @param {string} text
286
+ * @param {'north'|'south'|'neutral'|'broadcast'|'formal'} region
287
+ * @returns {string}
288
+ */
289
+ export function applyDialect(text, region) {
290
+ if (!text) return ''
291
+ let out = text
292
+ for (const { preferred, alternatives, applySafe } of activeEntries(region)) {
293
+ if (!applySafe) continue
294
+ for (const alternative of alternatives) {
295
+ out = out.replace(matcherFor(alternative), (m) => matchCase(m, preferred))
296
+ }
297
+ }
298
+ return out
299
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The lexicon as a reviewable list. This is what a "your preferred terms"
3
+ * management screen renders, and what a workspace exports for sign-off.
4
+ *
5
+ * @param {'north'|'south'|'neutral'|'broadcast'|'formal'} region
6
+ * @returns {Array<{id:string, gloss:string, preferred:string, alternatives:string[]}>}
7
+ */
8
+ export function listDialectTerms(region: "north" | "south" | "neutral" | "broadcast" | "formal"): Array<{
9
+ id: string;
10
+ gloss: string;
11
+ preferred: string;
12
+ alternatives: string[];
13
+ }>;
14
+ /**
15
+ * Report off-region terms WITHOUT changing the text. This is the validator
16
+ * half: it produces evidence a reviewer can act on, which is what makes the
17
+ * lexical layer auditable rather than merely applied.
18
+ *
19
+ * @param {string} text
20
+ * @param {'north'|'south'|'neutral'|'broadcast'|'formal'} region
21
+ * @returns {Array<{id:string, found:string, expected:string, index:number}>}
22
+ * Findings in document order. Empty array for empty input.
23
+ */
24
+ export function checkDialect(text: string, region: "north" | "south" | "neutral" | "broadcast" | "formal"): Array<{
25
+ id: string;
26
+ found: string;
27
+ expected: string;
28
+ index: number;
29
+ }>;
30
+ /**
31
+ * Rewrite off-region terms to the target region's form. Deterministic and
32
+ * case-preserving; returns the input unchanged when there is nothing to do.
33
+ *
34
+ * Substitution is whole-term only and mutation-blind, so it is safe to run on
35
+ * mixed Welsh/English text: no entry in the table is an English word.
36
+ *
37
+ * Entries marked `applySafe: false` are SKIPPED here and reported by
38
+ * `checkDialect` instead. Those are pairs whose substitution would change the
39
+ * grammar of neighbouring words (mutation or linking "yn"), which this module
40
+ * has no machinery to get right. Emitting ungrammatical Welsh is worse than
41
+ * leaving the term alone and telling a human about it.
42
+ *
43
+ * @param {string} text
44
+ * @param {'north'|'south'|'neutral'|'broadcast'|'formal'} region
45
+ * @returns {string}
46
+ */
47
+ export function applyDialect(text: string, region: "north" | "south" | "neutral" | "broadcast" | "formal"): string;
48
+ export namespace DIALECT_TERMS_STATUS {
49
+ let reviewed: boolean;
50
+ let reviewedBy: any;
51
+ let seededOn: string;
52
+ let note: string;
53
+ }
54
+ /**
55
+ * Seed lexicon. `neutral` is the form a broadcast or formal register would
56
+ * use; `null` means BOTH forms are regionally marked and there is no neutral
57
+ * standard, so a neutral target leaves the text alone rather than inventing
58
+ * one. Saying "no neutral form exists" is more useful than picking a side.
59
+ */
60
+ export const DIALECT_TERMS: ({
61
+ id: string;
62
+ north: string;
63
+ south: string;
64
+ neutral: string;
65
+ gloss: string;
66
+ applySafe?: undefined;
67
+ applyNote?: undefined;
68
+ } | {
69
+ id: string;
70
+ north: string;
71
+ south: string;
72
+ neutral: string;
73
+ gloss: string;
74
+ applySafe: boolean;
75
+ applyNote: string;
76
+ })[];
77
+ /**
78
+ * Deliberately NOT in the table, with the reason. Kept in code because the
79
+ * next person to extend this will reach for exactly these, and the reason
80
+ * they are absent is not self-evident.
81
+ */
82
+ export const EXCLUDED_TERMS: {
83
+ id: string;
84
+ reason: string;
85
+ }[];