@capsiynau/intelligence 0.4.0 → 0.5.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 +35 -0
- package/package.json +9 -4
- package/src/dialect/index.js +240 -0
- package/types/dialect/index.d.ts +69 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,41 @@ All notable changes to `@capsiynau/intelligence` go here. Format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.0] - 2026-07-25
|
|
10
|
+
|
|
11
|
+
Additive. No existing import path changes shape, so 0.4.x consumers can
|
|
12
|
+
upgrade without touching their code.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`dialect` subpath.** The Welsh regional lexicon as data rather than as a
|
|
17
|
+
prompt. `applyDialect(text, region)` rewrites off-region word choices
|
|
18
|
+
deterministically and case-preservingly ("Rŵan" to "Nawr", "mas" to
|
|
19
|
+
"allan"); `checkDialect(text, region)` reports the same findings WITHOUT
|
|
20
|
+
changing the text, so a reviewer gets evidence rather than a silent
|
|
21
|
+
rewrite; `listDialectTerms(region)` exposes the table for a management
|
|
22
|
+
screen or a sign-off export. `region` accepts the same values as
|
|
23
|
+
`getDialectSystemPrompt`, with `broadcast` and `formal` both mapping onto
|
|
24
|
+
the neutral lexicon.
|
|
25
|
+
|
|
26
|
+
This does not replace `getDialectSystemPrompt`. Register ("avoid
|
|
27
|
+
colloquialisms") has no deterministic transform and stays a prompt
|
|
28
|
+
concern; word choice does have one and should not be left to a model that
|
|
29
|
+
applies it unevenly and unverifiably. The two compose.
|
|
30
|
+
|
|
31
|
+
Deliberate limits, all pinned by tests: mutated surface forms are not
|
|
32
|
+
matched (needs syntactic context, as in `welsh/mutations.js`), pronoun and
|
|
33
|
+
copula forms are excluded because "mae o'r ardal" would be corrupted, and
|
|
34
|
+
where both regional forms are marked with no neutral standard
|
|
35
|
+
(taid/tad-cu) a neutral target leaves the text alone rather than picking a
|
|
36
|
+
side. Word boundaries use lookarounds over a Welsh letter class, not `\b`,
|
|
37
|
+
which treats the diacritic in "rŵan" as a word boundary and matches inside
|
|
38
|
+
the word.
|
|
39
|
+
|
|
40
|
+
**The seed table is not natively reviewed.** `DIALECT_TERMS_STATUS`
|
|
41
|
+
reports `reviewed: false` in code, and consumers should treat output as a
|
|
42
|
+
draft until that changes.
|
|
43
|
+
|
|
9
44
|
## [0.4.0] - 2026-07-22
|
|
10
45
|
|
|
11
46
|
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.
|
|
3
|
+
"version": "0.5.0",
|
|
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/
|
|
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/
|
|
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/
|
|
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,240 @@
|
|
|
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
|
+
* - Infer a region from text. The caller declares it, per the house rule
|
|
31
|
+
* that source language and variety are never auto-detected.
|
|
32
|
+
*
|
|
33
|
+
* Pure JS, no external deps, no I/O. Runs in Node or the browser.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Provenance for the seed table, per the "evidence beats confidence" rule:
|
|
38
|
+
* state what this data IS rather than implying an authority it has not earned.
|
|
39
|
+
* These are widely attested north/south pairs, but they are a starting seed
|
|
40
|
+
* compiled alongside the existing dialect prompt fragments - NOT a reviewed
|
|
41
|
+
* lexicographic source. A native reviewer signs this off before any consumer
|
|
42
|
+
* treats `applyDialect` output as publishable without a human read.
|
|
43
|
+
*/
|
|
44
|
+
export const DIALECT_TERMS_STATUS = {
|
|
45
|
+
reviewed: false,
|
|
46
|
+
reviewedBy: null,
|
|
47
|
+
seededOn: '2026-07-25',
|
|
48
|
+
note: 'Seed table. Needs native review before authoritative use.',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Welsh letters, including the diacritics JS `\w` and `\b` do not cover. `\b`
|
|
52
|
+
// is unusable here: to `\b` the `ŵ` in `rŵan` is a NON-word character, so
|
|
53
|
+
// /\brŵan\b/ finds a boundary in the middle of the word and matches inside
|
|
54
|
+
// longer tokens. Lookarounds over this class are the fix.
|
|
55
|
+
const WELSH_LETTERS = "A-Za-zÂÊÎÔÛŴŶâêîôûŵŷÁÉÍÓÚáéíóúÀÈÌÒÙàèìòùÄËÏÖÜäëïöü'"
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Seed lexicon. `neutral` is the form a broadcast or formal register would
|
|
59
|
+
* use; `null` means BOTH forms are regionally marked and there is no neutral
|
|
60
|
+
* standard, so a neutral target leaves the text alone rather than inventing
|
|
61
|
+
* one. Saying "no neutral form exists" is more useful than picking a side.
|
|
62
|
+
*/
|
|
63
|
+
export const DIALECT_TERMS = [
|
|
64
|
+
{ id: 'now', north: 'rŵan', south: 'nawr', neutral: 'nawr', gloss: 'now' },
|
|
65
|
+
{ id: 'with', north: 'efo', south: 'gyda', neutral: 'gyda', gloss: 'with' },
|
|
66
|
+
{ id: 'milk', north: 'llefrith', south: 'llaeth', neutral: 'llaeth', gloss: 'milk' },
|
|
67
|
+
{ id: 'want', north: 'isio', south: 'moyn', neutral: 'eisiau', gloss: 'to want' },
|
|
68
|
+
{ id: 'out', north: 'allan', south: 'mas', neutral: 'allan', gloss: 'out' },
|
|
69
|
+
{ id: 'money', north: 'pres', south: 'arian', neutral: 'arian', gloss: 'money' },
|
|
70
|
+
{ id: 'to-come', north: 'dŵad', south: 'dod', neutral: 'dod', gloss: 'to come' },
|
|
71
|
+
{ id: 'boy', north: 'hogyn', south: 'crwt', neutral: 'bachgen', gloss: 'boy' },
|
|
72
|
+
{ id: 'girl', north: 'hogan', south: 'croten', neutral: 'merch', gloss: 'girl' },
|
|
73
|
+
{ id: 'cuppa', north: 'panad', south: 'dishgled', neutral: 'paned', gloss: 'a cuppa' },
|
|
74
|
+
// Both forms regionally marked, no neutral standard.
|
|
75
|
+
{ id: 'grandfather', north: 'taid', south: 'tad-cu', neutral: null, gloss: 'grandfather' },
|
|
76
|
+
{ id: 'grandmother', north: 'nain', south: 'mam-gu', neutral: null, gloss: 'grandmother' },
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Deliberately NOT in the table, with the reason. Kept in code because the
|
|
81
|
+
* next person to extend this will reach for exactly these, and the reason
|
|
82
|
+
* they are absent is not self-evident.
|
|
83
|
+
*/
|
|
84
|
+
export const EXCLUDED_TERMS = [
|
|
85
|
+
{
|
|
86
|
+
id: 'pronoun-o-e',
|
|
87
|
+
reason:
|
|
88
|
+
'North "mae o" / south "mae e". The north form collides with the ' +
|
|
89
|
+
'preposition "o" (from): substituting in "mae o\'r ardal" (is from the ' +
|
|
90
|
+
'area) produces nonsense. Needs part-of-speech context.',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'up-lan',
|
|
94
|
+
reason:
|
|
95
|
+
'South "lan" (up) collides with "glan" soft-mutated to "lan" (shore), ' +
|
|
96
|
+
'so "ar lan y môr" would be rewritten. Needs mutation awareness.',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'watching-gwatsiad',
|
|
100
|
+
reason:
|
|
101
|
+
'Appears in the north prompt fragment but is an anglicism, not a ' +
|
|
102
|
+
'north/south pair. Codifying it as regional Welsh would be wrong.',
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
// `broadcast` and `formal` both want the unmarked standard form, so they map
|
|
107
|
+
// onto the same lexical target. They still differ in REGISTER, which is the
|
|
108
|
+
// prompt fragment's concern, not this module's.
|
|
109
|
+
const REGION_ALIASES = {
|
|
110
|
+
north: 'north',
|
|
111
|
+
south: 'south',
|
|
112
|
+
neutral: 'neutral',
|
|
113
|
+
broadcast: 'neutral',
|
|
114
|
+
formal: 'neutral',
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** @param {string} region @returns {'north'|'south'|'neutral'} */
|
|
118
|
+
function resolveRegion(region) {
|
|
119
|
+
return REGION_ALIASES[region] || 'neutral'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function escapeRegExp(s) {
|
|
123
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Whole-term matcher. Multi-word entries ("tad-cu", and any future entry with
|
|
128
|
+
* a space) tolerate variable internal whitespace.
|
|
129
|
+
*/
|
|
130
|
+
function matcherFor(term) {
|
|
131
|
+
const body = escapeRegExp(term).replace(/\s+/g, '\\s+')
|
|
132
|
+
return new RegExp(`(?<![${WELSH_LETTERS}])(${body})(?![${WELSH_LETTERS}])`, 'gi')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Carry the observed capitalisation onto the replacement, so "Rŵan" becomes
|
|
137
|
+
* "Nawr" and "RŴAN" becomes "NAWR". Mixed case falls through to the table's
|
|
138
|
+
* own casing rather than guessing.
|
|
139
|
+
*/
|
|
140
|
+
function matchCase(observed, replacement) {
|
|
141
|
+
if (observed === observed.toUpperCase() && observed !== observed.toLowerCase()) {
|
|
142
|
+
return replacement.toUpperCase()
|
|
143
|
+
}
|
|
144
|
+
if (observed[0] === observed[0].toUpperCase()) {
|
|
145
|
+
return replacement[0].toUpperCase() + replacement.slice(1)
|
|
146
|
+
}
|
|
147
|
+
return replacement
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The entries that carry a usable target for `region`, each with the forms
|
|
152
|
+
* that should be replaced by it. Entries with no neutral form are skipped for
|
|
153
|
+
* a neutral target rather than forced onto one side.
|
|
154
|
+
*/
|
|
155
|
+
function activeEntries(region) {
|
|
156
|
+
const resolved = resolveRegion(region)
|
|
157
|
+
const out = []
|
|
158
|
+
for (const entry of DIALECT_TERMS) {
|
|
159
|
+
const preferred = entry[resolved]
|
|
160
|
+
if (!preferred) continue
|
|
161
|
+
// Dedupe by lowercase form. Two regions often share a form (south and
|
|
162
|
+
// neutral are both "gyda"), and without this the same occurrence is
|
|
163
|
+
// reported twice by checkDialect.
|
|
164
|
+
const seen = new Set()
|
|
165
|
+
const alternatives = []
|
|
166
|
+
for (const r of ['north', 'south', 'neutral']) {
|
|
167
|
+
const form = r === resolved ? null : entry[r]
|
|
168
|
+
if (!form) continue
|
|
169
|
+
const key = form.toLowerCase()
|
|
170
|
+
if (key === preferred.toLowerCase() || seen.has(key)) continue
|
|
171
|
+
seen.add(key)
|
|
172
|
+
alternatives.push(form)
|
|
173
|
+
}
|
|
174
|
+
if (alternatives.length) out.push({ entry, preferred, alternatives })
|
|
175
|
+
}
|
|
176
|
+
return out
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The lexicon as a reviewable list. This is what a "your preferred terms"
|
|
181
|
+
* management screen renders, and what a workspace exports for sign-off.
|
|
182
|
+
*
|
|
183
|
+
* @param {'north'|'south'|'neutral'|'broadcast'|'formal'} region
|
|
184
|
+
* @returns {Array<{id:string, gloss:string, preferred:string, alternatives:string[]}>}
|
|
185
|
+
*/
|
|
186
|
+
export function listDialectTerms(region) {
|
|
187
|
+
return activeEntries(region).map(({ entry, preferred, alternatives }) => ({
|
|
188
|
+
id: entry.id,
|
|
189
|
+
gloss: entry.gloss,
|
|
190
|
+
preferred,
|
|
191
|
+
alternatives,
|
|
192
|
+
}))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Report off-region terms WITHOUT changing the text. This is the validator
|
|
197
|
+
* half: it produces evidence a reviewer can act on, which is what makes the
|
|
198
|
+
* lexical layer auditable rather than merely applied.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} text
|
|
201
|
+
* @param {'north'|'south'|'neutral'|'broadcast'|'formal'} region
|
|
202
|
+
* @returns {Array<{id:string, found:string, expected:string, index:number}>}
|
|
203
|
+
* Findings in document order. Empty array for empty input.
|
|
204
|
+
*/
|
|
205
|
+
export function checkDialect(text, region) {
|
|
206
|
+
if (!text) return []
|
|
207
|
+
const findings = []
|
|
208
|
+
for (const { entry, preferred, alternatives } of activeEntries(region)) {
|
|
209
|
+
for (const alternative of alternatives) {
|
|
210
|
+
const matcher = matcherFor(alternative)
|
|
211
|
+
let hit
|
|
212
|
+
while ((hit = matcher.exec(text)) !== null) {
|
|
213
|
+
findings.push({ id: entry.id, found: hit[1], expected: preferred, index: hit.index })
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return findings.sort((a, b) => a.index - b.index)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Rewrite off-region terms to the target region's form. Deterministic and
|
|
222
|
+
* case-preserving; returns the input unchanged when there is nothing to do.
|
|
223
|
+
*
|
|
224
|
+
* Substitution is whole-term only and mutation-blind, so it is safe to run on
|
|
225
|
+
* mixed Welsh/English text: no entry in the table is an English word.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} text
|
|
228
|
+
* @param {'north'|'south'|'neutral'|'broadcast'|'formal'} region
|
|
229
|
+
* @returns {string}
|
|
230
|
+
*/
|
|
231
|
+
export function applyDialect(text, region) {
|
|
232
|
+
if (!text) return ''
|
|
233
|
+
let out = text
|
|
234
|
+
for (const { preferred, alternatives } of activeEntries(region)) {
|
|
235
|
+
for (const alternative of alternatives) {
|
|
236
|
+
out = out.replace(matcherFor(alternative), (m) => matchCase(m, preferred))
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return out
|
|
240
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
* @param {string} text
|
|
38
|
+
* @param {'north'|'south'|'neutral'|'broadcast'|'formal'} region
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
export function applyDialect(text: string, region: "north" | "south" | "neutral" | "broadcast" | "formal"): string;
|
|
42
|
+
export namespace DIALECT_TERMS_STATUS {
|
|
43
|
+
let reviewed: boolean;
|
|
44
|
+
let reviewedBy: any;
|
|
45
|
+
let seededOn: string;
|
|
46
|
+
let note: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Seed lexicon. `neutral` is the form a broadcast or formal register would
|
|
50
|
+
* use; `null` means BOTH forms are regionally marked and there is no neutral
|
|
51
|
+
* standard, so a neutral target leaves the text alone rather than inventing
|
|
52
|
+
* one. Saying "no neutral form exists" is more useful than picking a side.
|
|
53
|
+
*/
|
|
54
|
+
export const DIALECT_TERMS: {
|
|
55
|
+
id: string;
|
|
56
|
+
north: string;
|
|
57
|
+
south: string;
|
|
58
|
+
neutral: string;
|
|
59
|
+
gloss: string;
|
|
60
|
+
}[];
|
|
61
|
+
/**
|
|
62
|
+
* Deliberately NOT in the table, with the reason. Kept in code because the
|
|
63
|
+
* next person to extend this will reach for exactly these, and the reason
|
|
64
|
+
* they are absent is not self-evident.
|
|
65
|
+
*/
|
|
66
|
+
export const EXCLUDED_TERMS: {
|
|
67
|
+
id: string;
|
|
68
|
+
reason: string;
|
|
69
|
+
}[];
|