@capsiynau/intelligence 0.3.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.
@@ -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
+ }[];
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Parse a CSV-ish string. Accepts (with or without header row):
3
+ * term,definition
4
+ * term,definition,category
5
+ * term,category,definition
6
+ * Header row (detected by token match) maps columns by name and
7
+ * tolerates aliases (name, meaning, tag). RFC-4180-lite: quoted
8
+ * fields + escaped quotes on a single line. Multi-line quoted values
9
+ * are out of scope - report and skip.
10
+ */
11
+ export function parseImportedCsv(raw: any): {
12
+ rows: {
13
+ source_term: string;
14
+ preferred_term: string;
15
+ definition: string;
16
+ category: any;
17
+ }[];
18
+ errors: {
19
+ line: number;
20
+ reason: string;
21
+ }[];
22
+ };
23
+ /**
24
+ * Parse plain text. One term per line. Separator tried in order: tab,
25
+ * `:` followed by space, ` - ` (hyphen with spaces). Three columns
26
+ * (term, definition, optional category) match our copy-as-text output
27
+ * so users can round-trip cleanly.
28
+ */
29
+ export function parseImportedText(raw: any): {
30
+ rows: {
31
+ source_term: string;
32
+ preferred_term: string;
33
+ definition: string;
34
+ category: string;
35
+ }[];
36
+ errors: {
37
+ line: number;
38
+ reason: string;
39
+ }[];
40
+ };
41
+ /**
42
+ * Split incoming rows against the existing set. Matches case-insensitive
43
+ * on trimmed source_term. Returns {additions, conflicts} where conflicts
44
+ * is an array of {existing, incoming} pairs the UI surfaces for
45
+ * resolution. Dedup within the import itself happens against additions
46
+ * (the second occurrence of a duplicated import term becomes a conflict
47
+ * with the first occurrence).
48
+ */
49
+ export function splitImportAgainstExisting(existing: any, incoming: any): {
50
+ additions: any[];
51
+ conflicts: {
52
+ existing: any;
53
+ incoming: any;
54
+ }[];
55
+ };
@@ -0,0 +1,6 @@
1
+ export { WEB_SEARCH_TOOL } from "./tools.js";
2
+ export { normaliseUrl, isHttpUrl } from "./url.js";
3
+ export { extractText, parseTermsJson, normaliseTermRow, flagDiacriticStrip } from "./parse.js";
4
+ export { RESEARCH_SYSTEM, buildGenerationSystemPrompt, buildExpandSystemPrompt } from "./prompts.js";
5
+ export { parseImportedCsv, parseImportedText, splitImportAgainstExisting } from "./import.js";
6
+ export { suggestGlossaryName, languagePairFor, expandRowsForDb, buildGlossaryRow, suggestVersionedName } from "./persist.js";
@@ -0,0 +1,63 @@
1
+ export function extractText(message: any): any;
2
+ /**
3
+ * Defensive JSON parse for the generation phase. The model is told to
4
+ * reply with raw JSON only, but markdown fences and conversational
5
+ * preamble do leak through. Strip what we can, then fall back to the
6
+ * first [...] match. Return null on total failure - never throw.
7
+ */
8
+ export function parseTermsJson(raw: any): any[];
9
+ /**
10
+ * Heuristic guard: any Welsh text longer than 25 chars with ZERO
11
+ * diacritics is suspicious - the model probably stripped them.
12
+ *
13
+ * Not a perfect check (some short Welsh text genuinely has none) but
14
+ * catches the common "model normalised everything to ASCII" failure
15
+ * mode. Substitution detection (ŵ → w, ŷ → y in specific positions)
16
+ * requires a dictionary and is deferred to a future Tier-3 spellcheck
17
+ * integration; not in scope for the wizard.
18
+ */
19
+ export function flagDiacriticStrip(text: any): boolean;
20
+ /**
21
+ * Normalise one row from the model's JSON output.
22
+ *
23
+ * In 'en' mode: tolerate naming wobbles ({term} vs {source_term}; etc).
24
+ * In 'cy' mode: same shape, but route Welsh text through normaliseWelsh().
25
+ * In 'bilingual' mode: expect {term_en, term_cy, definition_en,
26
+ * definition_cy, category}; route the *_cy fields through normaliseWelsh().
27
+ *
28
+ * Returns null for entries missing the required fields so the caller
29
+ * can `.map(normaliseTermRow).filter(Boolean)`. Adds a transient
30
+ * `_diacritic_warning` flag the handler can aggregate; it does NOT
31
+ * persist to the DB.
32
+ */
33
+ export function normaliseTermRow(t: any, opts?: {}): {
34
+ source_term: any;
35
+ preferred_term: any;
36
+ definition: any;
37
+ term_en: any;
38
+ term_cy: any;
39
+ definition_en: any;
40
+ definition_cy: any;
41
+ category: string;
42
+ _diacritic_warning: boolean;
43
+ } | {
44
+ source_term: any;
45
+ preferred_term: any;
46
+ definition: any;
47
+ category: string;
48
+ _diacritic_warning: boolean;
49
+ term_en?: undefined;
50
+ term_cy?: undefined;
51
+ definition_en?: undefined;
52
+ definition_cy?: undefined;
53
+ } | {
54
+ source_term: string;
55
+ preferred_term: string;
56
+ definition: string;
57
+ category: string;
58
+ term_en?: undefined;
59
+ term_cy?: undefined;
60
+ definition_en?: undefined;
61
+ definition_cy?: undefined;
62
+ _diacritic_warning?: undefined;
63
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Pure helpers for converting Glossary Builder wizard rows into the
3
+ * shapes the `glossaries` + `terms` Supabase tables expect.
4
+ *
5
+ * No Supabase calls here - those live in the view layer
6
+ * (src/components/glossaryBuilder/savePersist.js) so this module
7
+ * stays unit-testable and side-effect-free, mirroring how
8
+ * @capsiynau/intelligence/welsh/normaliser splits pure logic from
9
+ * the API handler.
10
+ */
11
+ /**
12
+ * Suggest a glossary name based on the source URL. Strips
13
+ * protocol + leading `www.` so the result reads naturally
14
+ * ("Site terms - example.com").
15
+ */
16
+ export function suggestGlossaryName(url: any): string;
17
+ /**
18
+ * Map wizard languageMode to glossaries.language_pair. Single-language
19
+ * modes get 'en-GB' or 'cy-GB'; bilingual is 'en-GB,cy-GB' (the
20
+ * existing convention in the schema for multi-language glossaries).
21
+ */
22
+ export function languagePairFor(languageMode: any): "cy-GB" | "en-GB,cy-GB" | "en-GB";
23
+ /**
24
+ * Expand wizard rows into terms-table inserts.
25
+ *
26
+ * Single-language modes produce one row per wizard row.
27
+ * Bilingual produces TWO rows per pair (EN + CY), linked by sharing
28
+ * glossary_id and category, with source_language differing. This uses
29
+ * the schema as designed and keeps the live-relay engine able to
30
+ * query by language without a JOIN.
31
+ *
32
+ * Drops rows missing term or definition silently (the wizard
33
+ * shouldn't let them through, but defence in depth at persistence
34
+ * time means a partial bilingual row contributes only its complete
35
+ * leg rather than aborting the whole save).
36
+ */
37
+ export function expandRowsForDb(wizardRows: any, opts?: {}): any[];
38
+ /**
39
+ * Build the glossaries-table insert payload for a wizard save.
40
+ * Defaults to visibility='shared' (org-wide, not just creator) - the
41
+ * wizard is collaborative tooling, not personal scratch space.
42
+ */
43
+ export function buildGlossaryRow({ name, url, languageMode, organisationId, createdBy, visibility, }: {
44
+ name: any;
45
+ url: any;
46
+ languageMode: any;
47
+ organisationId: any;
48
+ createdBy: any;
49
+ visibility?: string;
50
+ }): {
51
+ name: any;
52
+ description: string;
53
+ organisation_id: any;
54
+ created_by: any;
55
+ visibility: string;
56
+ language_pair: string;
57
+ source_url: any;
58
+ is_default: boolean;
59
+ is_system: boolean;
60
+ };
61
+ /**
62
+ * Suggest a versioned name when "Create new version" is chosen on
63
+ * conflict. Bumps an existing (vN) suffix or appends (v2) to a name
64
+ * that lacks one.
65
+ */
66
+ export function suggestVersionedName(existingName: any): string;
@@ -0,0 +1,10 @@
1
+ export function buildGenerationSystemPrompt({ audience, topics, languageMode, dialect, }?: {
2
+ languageMode?: string;
3
+ dialect?: string;
4
+ }): string;
5
+ export function buildExpandSystemPrompt({ audience, topics, languageMode, dialect, existingTerms, }?: {
6
+ languageMode?: string;
7
+ dialect?: string;
8
+ existingTerms?: any[];
9
+ }): string;
10
+ export const RESEARCH_SYSTEM: "You are helping a SaaS user build a starter glossary for their organisation. You have a web_search tool - use it to look up the URL the user provides and identify the industry, the products / services they offer, and the likely audience. Reply conversationally in 2-3 sentences confirming what you found, then ask who the glossary is for (e.g. \"clients\", \"internal team\", \"general public\", \"broadcasters\"). Keep the tone warm and direct. Do NOT produce a glossary yet - that comes in a follow-up step.";
@@ -0,0 +1,4 @@
1
+ export namespace WEB_SEARCH_TOOL {
2
+ let type: string;
3
+ let name: string;
4
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * URL helpers for the Glossary Builder engine.
3
+ *
4
+ * Anthropic's web_search expects a full URL; a bare 'example.com'
5
+ * returns "no relevant pages" and the whole flow degrades silently.
6
+ * normaliseUrl prepends https:// when the caller forgot a scheme.
7
+ */
8
+ export function normaliseUrl(raw: any): string;
9
+ export function isHttpUrl(url: any): boolean;
@@ -0,0 +1,2 @@
1
+ export * from "./corrections/index.js";
2
+ export * from "./welsh/index.js";
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Total milliseconds the utterances cover WITHIN the transcript span
3
+ * `[spanStart, spanEnd]`, counted as a UNION so overlapping utterances don't
4
+ * double-count. Each utterance is first clipped to the span (speech entirely
5
+ * outside the transcript window contributes 0), then the clipped intervals are
6
+ * merged and their lengths summed. This is what makes coverage measure the
7
+ * fraction of the transcript actually overlapped by the speaker timeline,
8
+ * rather than total diarisation duration (which can sit outside the window
9
+ * when diarisation ran over the full audio but the transcript is a clip).
10
+ *
11
+ * @param {Array<{start?:number,end?:number}>} utts
12
+ * @param {number} spanStart inclusive lower bound (ms)
13
+ * @param {number} spanEnd inclusive upper bound (ms)
14
+ * @returns {number} unioned, clipped duration in ms (>= 0)
15
+ */
16
+ export function clampedUnionMs(utts: Array<{
17
+ start?: number;
18
+ end?: number;
19
+ }>, spanStart: number, spanEnd: number): number;
20
+ /**
21
+ * Judge whether a diarisation timeline is reliable enough to label segments.
22
+ *
23
+ * @param {Array<{start:number,end:number,speaker:string}>} utterances
24
+ * diarisation timeline in milliseconds (AssemblyAI `utterances` shape).
25
+ * @param {Array<{start_ms:number,end_ms:number}>} segments
26
+ * transcript segments in milliseconds (used to measure coverage).
27
+ * @param {Partial<typeof DIARISATION_GATE_DEFAULTS>} [opts] threshold overrides.
28
+ * @returns {{ reliable: boolean, speakerCount: number, coverage: number,
29
+ * durationMs: number, reason: string }}
30
+ */
31
+ export function assessDiarisation(utterances: Array<{
32
+ start: number;
33
+ end: number;
34
+ speaker: string;
35
+ }>, segments: Array<{
36
+ start_ms: number;
37
+ end_ms: number;
38
+ }>, opts?: Partial<typeof DIARISATION_GATE_DEFAULTS>): {
39
+ reliable: boolean;
40
+ speakerCount: number;
41
+ coverage: number;
42
+ durationMs: number;
43
+ reason: string;
44
+ };
45
+ /**
46
+ * Map acoustic diarisation speakers ("Speaker 1/2/3") to REAL names from a
47
+ * labelled reference timeline (platform transcript / roster), by greatest
48
+ * total time-overlap. Upgrades anonymous acoustic labels to real names when a
49
+ * labelled source exists (speaker-ID ladder rung A over C). Pure.
50
+ *
51
+ * Only confident matches are returned: the winning name must cover
52
+ * >= minOverlapFraction of the acoustic speaker's speaking time AND beat the
53
+ * runner-up by >= dominanceRatio. Ambiguous / unmatched acoustic speakers are
54
+ * listed in `unmapped`, so the caller keeps their anonymous label.
55
+ *
56
+ * Note: two acoustic speakers can map to the same real name (acoustic over-
57
+ * split one person). That is acceptable - downstream can merge. 1:1 dedup is a
58
+ * future refinement.
59
+ *
60
+ * @param {Array<{start:number,end:number,speaker:string}>} acoustic utterances (ms)
61
+ * @param {Array<{start:number,end:number,name:string}>} labelled real-name turns (ms)
62
+ * @param {Partial<typeof SPEAKER_MAP_DEFAULTS>} [opts]
63
+ * @returns {{ map: Record<string,string>, unmapped: string[] }}
64
+ */
65
+ export function mapSpeakersToNames(acoustic: Array<{
66
+ start: number;
67
+ end: number;
68
+ speaker: string;
69
+ }>, labelled: Array<{
70
+ start: number;
71
+ end: number;
72
+ name: string;
73
+ }>, opts?: Partial<typeof SPEAKER_MAP_DEFAULTS>): {
74
+ map: Record<string, string>;
75
+ unmapped: string[];
76
+ };
77
+ export namespace DIARISATION_GATE_DEFAULTS {
78
+ let soleSpeakerSuspectAboveMs: number;
79
+ let minCoverage: number;
80
+ let trustAtSpeakers: number;
81
+ }
82
+ export namespace SPEAKER_MAP_DEFAULTS {
83
+ let minOverlapFraction: number;
84
+ let dominanceRatio: number;
85
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Check a single word against the supplied dictionary + accept-list.
3
+ *
4
+ * @param {string} word - The word to check (no punctuation, single token).
5
+ * @param {object} opts
6
+ * @param {object} opts.nspell - An nspell instance (server side, built
7
+ * from dictionary-cy or dictionary-en-gb).
8
+ * @param {Set<string>} [opts.accept] - Lowercased proper nouns + glossary
9
+ * + word_boost_approved terms that
10
+ * must never be flagged.
11
+ * @param {boolean} [opts.welsh] - When true, additionally try mutation
12
+ * reversal before declaring the word
13
+ * misspelled. Pass false for English.
14
+ * @returns {boolean} true if the word is considered correctly spelled.
15
+ */
16
+ export function checkWord(word: string, { nspell, accept, welsh }: {
17
+ nspell: object;
18
+ accept?: Set<string>;
19
+ welsh?: boolean;
20
+ }): boolean;
21
+ /**
22
+ * Tokenise a string into checkable words + their positions, so a UI
23
+ * can underline only the wrong tokens without re-tokenising itself.
24
+ *
25
+ * Position is the offset in the original string. Tokens are word-like
26
+ * runs of letters/marks/apostrophes/hyphens; punctuation is skipped.
27
+ *
28
+ * @param {string} text
29
+ * @returns {{ word: string, start: number, end: number }[]}
30
+ */
31
+ export function tokenise(text: string): {
32
+ word: string;
33
+ start: number;
34
+ end: number;
35
+ }[];
36
+ /**
37
+ * Check an entire string and return the misspelled tokens (if any).
38
+ * Convenience wrapper around tokenise + checkWord.
39
+ *
40
+ * @param {string} text
41
+ * @param {object} opts - same shape as checkWord opts
42
+ * @param {object} opts.nspell - An nspell instance.
43
+ * @param {Set<string>} [opts.accept] - Terms that must never be flagged.
44
+ * @param {boolean} [opts.welsh] - Try mutation reversal before flagging.
45
+ * @returns {{ word: string, start: number, end: number }[]}
46
+ */
47
+ export function checkSentence(text: string, opts: {
48
+ nspell: object;
49
+ accept?: Set<string>;
50
+ welsh?: boolean;
51
+ }): {
52
+ word: string;
53
+ start: number;
54
+ end: number;
55
+ }[];
56
+ /**
57
+ * Generate up to N suggestions for a misspelled word. Wraps
58
+ * nspell.suggest() but biases toward `accept` (proper nouns are
59
+ * surfaced first when they're close in edit distance).
60
+ *
61
+ * @param {string} word
62
+ * @param {object} opts
63
+ * @param {object} opts.nspell
64
+ * @param {Set<string>} [opts.accept]
65
+ * @param {boolean} [opts.welsh] - Bias suggestions for Welsh mutation forms.
66
+ * @param {number} [opts.max] - default 6
67
+ * @returns {string[]}
68
+ */
69
+ export function suggest(word: string, { nspell, accept, welsh, max }: {
70
+ nspell: object;
71
+ accept?: Set<string>;
72
+ welsh?: boolean;
73
+ max?: number;
74
+ }): string[];
@@ -0,0 +1 @@
1
+ export { checkWord, checkSentence, tokenise, suggest } from "./core.js";
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Split a Welsh word into its constituent letters, treating digraphs
3
+ * as single units. Case-preserving.
4
+ *
5
+ * @example
6
+ * splitIntoLetters('llwybr') // ['ll', 'w', 'y', 'b', 'r']
7
+ * splitIntoLetters('CHWARAE') // ['CH', 'W', 'A', 'R', 'A', 'E']
8
+ * splitIntoLetters('rhag-') // ['rh', 'a', 'g', '-']
9
+ *
10
+ * @param {string} word
11
+ * @returns {string[]}
12
+ */
13
+ export function splitIntoLetters(word: string): string[];
14
+ /**
15
+ * Welsh-aware character count: a digraph counts as ONE letter.
16
+ *
17
+ * @example
18
+ * welshLength('llwybr') // 5 (ll-w-y-b-r), NOT 6
19
+ * welshLength('chwarae') // 6 (ch-w-a-r-a-e)
20
+ * welshLength('Eisteddfod') // 9 (e-i-s-t-e-dd-f-o-d), NOT 10
21
+ *
22
+ * @param {string} word
23
+ * @returns {number}
24
+ */
25
+ export function welshLength(word: string): number;
26
+ /**
27
+ * Reverse a Welsh word preserving digraphs (used by anagram /
28
+ * caption-style transforms that mirror text).
29
+ *
30
+ * @example
31
+ * welshReverse('llwybr') // 'rbywll' (NOT 'rbywll' broken to 'rbyw ll')
32
+ *
33
+ * @param {string} word
34
+ * @returns {string}
35
+ */
36
+ export function welshReverse(word: string): string;
37
+ export function welshCompare(a: any, b: any): number;
38
+ /**
39
+ * Check if a string position lies on a digraph boundary. Useful for
40
+ * cursor / selection logic: when the cursor sits after `l` in `llwybr`,
41
+ * a single backspace should remove BOTH letters of `ll`, not just one.
42
+ *
43
+ * Returns `true` if the character at `index` is the second half of a
44
+ * digraph (i.e. the digraph started at `index - 1`).
45
+ *
46
+ * @example
47
+ * isDigraphContinuation('llwybr', 1) // true ('l' completing 'll')
48
+ * isDigraphContinuation('llwybr', 2) // false ('w')
49
+ * isDigraphContinuation('cath', 1) // false ('a')
50
+ *
51
+ * @param {string} word
52
+ * @param {number} index
53
+ * @returns {boolean}
54
+ */
55
+ export function isDigraphContinuation(word: string, index: number): boolean;
56
+ /**
57
+ * @capsiynau/intelligence/welsh/digraphs
58
+ *
59
+ * Welsh digraph handling. In the Welsh alphabet, these eight digraphs
60
+ * are SINGLE letters and must be treated as one unit for:
61
+ * - character iteration
62
+ * - cursor movement
63
+ * - word length
64
+ * - alphabetical sort
65
+ * - reversal (for anagrams etc.)
66
+ *
67
+ * The eight digraphs (canonical order in the Welsh alphabet):
68
+ * ch dd ff ng ll ph rh th
69
+ *
70
+ * Note: `ng` is the most context-sensitive. In some words it represents
71
+ * a single sound (canu's "ng" is /ŋ/) but in compound words it can be
72
+ * `n` + `g` separately (e.g. "Bangor" is BAN+GOR phonetically but
73
+ * conventionally still written without splitting). For purely
74
+ * orthographic operations (length, indexing, anagrams), we treat every
75
+ * occurrence as a single digraph. Callers needing phonetic accuracy
76
+ * must layer their own lexicon lookup on top.
77
+ *
78
+ * Pure JS, no external deps.
79
+ */
80
+ /**
81
+ * The eight Welsh digraphs in their canonical alphabetical order.
82
+ * @type {readonly string[]}
83
+ */
84
+ export const WELSH_DIGRAPHS: readonly string[];
85
+ /**
86
+ * Map of digraph -> alphabetical position (0-indexed within the digraph set).
87
+ * For full Welsh-alphabet collation, layer this on top of the single-letter
88
+ * position map in `WELSH_ALPHABET_ORDER` below.
89
+ */
90
+ export const DIGRAPH_INDEX: Readonly<{
91
+ [k: string]: number;
92
+ }>;
93
+ /**
94
+ * The canonical Welsh alphabet in collation order. Note the digraphs
95
+ * follow their leading letter: `c` then `ch`, `d` then `dd`, etc.
96
+ * Letters not in Welsh (`k`, `q`, `v`, `x`, `z`) are omitted; loan words
97
+ * containing them are conventionally sorted as if they were English.
98
+ *
99
+ * Source: Geiriadur Prifysgol Cymru collation rules.
100
+ * @type {readonly string[]}
101
+ */
102
+ export const WELSH_ALPHABET_ORDER: readonly string[];
@@ -0,0 +1,3 @@
1
+ export * from "./normaliser.js";
2
+ export * from "./digraphs.js";
3
+ export * from "./mutations.js";
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Apply a mutation to a Welsh word. Returns the mutated form.
3
+ * Preserves the case of the first character.
4
+ *
5
+ * Soft mutation of `g` is DELETION — the word loses its initial `g`.
6
+ * e.g. 'gardd' -> soft -> 'ardd' (the garden, after 'yr' for feminine).
7
+ *
8
+ * If the word's initial consonant is not subject to the given mutation,
9
+ * returns the word unchanged.
10
+ *
11
+ * @param {string} word
12
+ * @param {'soft'|'nasal'|'aspirate'} kind
13
+ * @returns {string}
14
+ *
15
+ * @example
16
+ * applyMutation('cath', 'soft') // 'gath'
17
+ * applyMutation('Tad', 'soft') // 'Dad' (case preserved)
18
+ * applyMutation('ci', 'nasal') // 'nghi'
19
+ * applyMutation('Pen', 'aspirate') // 'Phen'
20
+ * applyMutation('gardd', 'soft') // 'ardd' (g-deletion)
21
+ * applyMutation('llaw', 'soft') // 'law'
22
+ * applyMutation('siop', 'soft') // 'siop' (s not subject)
23
+ */
24
+ export function applyMutation(word: string, kind: "soft" | "nasal" | "aspirate"): string;
25
+ /**
26
+ * Reverse a mutation: given an observed (possibly mutated) word,
27
+ * return all candidate root forms. The caller can intersect this
28
+ * with a dictionary lookup to pick the real root.
29
+ *
30
+ * If `kind` is provided, only candidates from that mutation are
31
+ * returned. If omitted, candidates from all three mutations.
32
+ *
33
+ * The observed word itself is ALWAYS included as the first candidate
34
+ * (it might not actually be mutated).
35
+ *
36
+ * @param {string} word
37
+ * @param {'soft'|'nasal'|'aspirate'} [kind]
38
+ * @returns {{ root: string, kind: string | null }[]}
39
+ *
40
+ * @example
41
+ * possibleRoots('gath') // [
42
+ * // { root: 'gath', kind: null }, // as-is
43
+ * // { root: 'cath', kind: 'soft' }, // c -> g
44
+ * // ]
45
+ * possibleRoots('phen', 'aspirate') // [
46
+ * // { root: 'phen', kind: null },
47
+ * // { root: 'pen', kind: 'aspirate' },
48
+ * // ]
49
+ */
50
+ export function possibleRoots(word: string, kind?: "soft" | "nasal" | "aspirate"): {
51
+ root: string;
52
+ kind: string | null;
53
+ }[];
54
+ /**
55
+ * Best-effort classification: does this word LOOK mutated?
56
+ *
57
+ * Returns the mutation kind if the word starts with a sequence
58
+ * characteristic of one specific mutation (e.g. 'ph' / 'th' / 'ch' are
59
+ * aspirate-only; 'mh' / 'nh' / 'ngh' are nasal-only). Returns null if
60
+ * the word either isn't mutated or its surface form is ambiguous.
61
+ *
62
+ * NOT a reliable mutation detector on its own. Use with caller context.
63
+ *
64
+ * @param {string} word
65
+ * @returns {'soft'|'nasal'|'aspirate'|null}
66
+ */
67
+ export function classifyMutation(word: string): "soft" | "nasal" | "aspirate" | null;
68
+ /**
69
+ * The three mutation kinds as a const enum-ish object.
70
+ * @type {Readonly<{soft:'soft', nasal:'nasal', aspirate:'aspirate'}>}
71
+ */
72
+ export const MUTATION: Readonly<{
73
+ soft: "soft";
74
+ nasal: "nasal";
75
+ aspirate: "aspirate";
76
+ }>;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Normalise raw Welsh/English transcript text.
3
+ * Repairs broken contractions, fixes encoding, removes zero-width chars.
4
+ * Safe to apply to English too — repairs are Welsh-specific patterns unlikely
5
+ * to match English text.
6
+ */
7
+ export function normaliseWelsh(text: any): any;
8
+ /**
9
+ * Get a dialect-specific system prompt fragment for AI calls.
10
+ * @param {'broadcast'|'north'|'south'|'formal'} dialect
11
+ * @returns {string}
12
+ */
13
+ export function getDialectSystemPrompt(dialect: "broadcast" | "north" | "south" | "formal"): string;