champollion 0.3.3 → 0.3.4
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 -11
- package/lib/cards/reader.js +17 -3
- package/lib/command-help.js +3 -3
- package/lib/commands/register-corpus.js +21 -9
- package/lib/commands/tm.js +1 -1
- package/lib/corpus-registration.mjs +3 -3
- package/lib/scripts.js +187 -105
- package/lib/seal.mjs +2 -2
- package/lib/sealed-qualifier.mjs +1 -1
- package/package.json +3 -2
- package/shared/DATA-SOVEREIGNTY.md +19 -20
- package/shared/LANGUAGE-CARD-FIELDS.md +1 -1
- package/shared/cards-fallback.json +1 -1
- package/shared/catalogue/card-config.json +1 -1
- package/shared/docent/corpus.json +1156 -750
- package/shared/docent/faq.en.json +14 -16
- package/shared/docent/system-prompt.md +17 -19
- package/shared/explainers/tc-features.json +15 -15
- package/shared/human-services.json +1 -1
- package/shared/schemas/corpora-card.schema.json +12 -8
- package/shared/schemas/human-services.schema.json +2 -2
- package/shared/schemas/language-card.schema.json +1 -1
- package/shared/schemas/method-card.schema.json +1 -1
package/README.md
CHANGED
|
@@ -16,9 +16,9 @@ npx champollion sync
|
|
|
16
16
|
|
|
17
17
|
Champollion auto-detects your locale files, their format, and the target languages. It translates missing keys, skips what's already done, and writes the results. That's it.
|
|
18
18
|
|
|
19
|
-
> **Part of Champollion** — infrastructure for trustworthy machine
|
|
20
|
-
> across every language, source-available and free for noncommercial
|
|
21
|
-
> evaluation harness and shared registries are open source). This CLI is the
|
|
19
|
+
> **Part of Champollion** — infrastructure for trustworthy machine-translation
|
|
20
|
+
> evaluation across every language, source-available and free for noncommercial
|
|
21
|
+
> use (the evaluation harness and shared registries are open source). This CLI is the
|
|
22
22
|
> deployment end of a larger
|
|
23
23
|
> project that builds the test sets and the map showing who can translate what,
|
|
24
24
|
> how good each method is on each kind of text, and where the gaps still are. It
|
|
@@ -26,8 +26,8 @@ Champollion auto-detects your locale files, their format, and the target languag
|
|
|
26
26
|
> every method welcome) and sovereign benchmarks — secret test sets that
|
|
27
27
|
> communities create, own, and control, and that we never see. The infrastructure
|
|
28
28
|
> is source-available and singly stewarded; the test sets and the methods for a
|
|
29
|
-
> community's language belong to that community.
|
|
30
|
-
>
|
|
29
|
+
> community's language belong to that community. Designed to work with communities,
|
|
30
|
+
> never hosting their corpora — the design puts the keys in their hands. Every method is welcome, human and
|
|
31
31
|
> machine. Explore the network at
|
|
32
32
|
> [champollion.dev/docs/network](https://champollion.dev/docs/network/).
|
|
33
33
|
|
|
@@ -37,7 +37,7 @@ You could write a quick script that loops through your English keys and calls Go
|
|
|
37
37
|
|
|
38
38
|
- **No change detection.** When you update an English string, the translation stays stale forever. Champollion tracks every source value with SHA-256 hashes and re-translates only what changed.
|
|
39
39
|
- **No batching.** One API call per key means 200 keys = 200 round trips. Champollion batches intelligently (configurable, default 80 keys/batch for LLM, 128 for Google).
|
|
40
|
-
- **No quality gate.** Machine translation hallucinates, echoes the source back, or outputs in the wrong script. Champollion
|
|
40
|
+
- **No quality gate.** Machine translation hallucinates, echoes the source back, or outputs in the wrong script. Champollion checks every translation before writing it — empty output, source echoes, repetition loops, length inflation, deleted content and wrong script are caught and rejected. The gate catches broken output, not wrong meaning.
|
|
41
41
|
- **No format awareness.** Hardcoded to JSON? Champollion handles JSON, TOML, YAML, and Hugo Markdown (frontmatter + body) with auto-detection.
|
|
42
42
|
- **No safety.** Champollion guards against prototype pollution, path traversal via crafted locale codes, and code block corruption during Markdown translation.
|
|
43
43
|
|
|
@@ -118,17 +118,17 @@ You handle the i18n framework (next-intl, i18next, Hugo). Champollion handles th
|
|
|
118
118
|
- **Multi-format** — JSON, TOML, YAML, Hugo Markdown (front matter + body), and XLIFF 1.2
|
|
119
119
|
- **Incremental** — Only translates what changed (SHA-256 hash tracking)
|
|
120
120
|
- **Cached** — Translation Memory stores previous results; re-running sync costs nothing for unchanged keys
|
|
121
|
-
- **Quality-gated** —
|
|
121
|
+
- **Quality-gated** — Checks every translation for broken output: repetition loops, wrong-script output, source echoes, length inflation and deleted content (it does not check meaning)
|
|
122
122
|
- **Content-aware** — LLM methods shield code blocks, shortcodes, links, and interpolation variables during Markdown translation
|
|
123
123
|
- **Pipeline tools** — `lint`, `audit`, `integrity`, `seo` for CI gates
|
|
124
124
|
- **XLIFF interop** — Export translations for professional review in CAT tools (memoQ, SDL Trados, Phrase), import them back
|
|
125
|
-
- **Minimal dependencies** — two runtime dependencies (
|
|
125
|
+
- **Minimal dependencies** — two runtime dependencies: `@translated/lara` (the Lara provider SDK) and `cree-sro-syllabics` (ALTLab/NRC's Plains Cree SRO ⇄ syllabics converter). Requires Node 20+
|
|
126
126
|
|
|
127
127
|
## Beyond Google Translate
|
|
128
128
|
|
|
129
129
|
The quick start gets you running with an LLM or Google Translate. But Google Translate supports ~130 languages. There are over 7,000.
|
|
130
130
|
|
|
131
|
-
**Champollion's core idea: the translation method is configurable per language pair.** Use Google Translate for French, an LLM with
|
|
131
|
+
**Champollion's core idea: the translation method is configurable per language pair.** Use Google Translate for French, an LLM coached with your own grammar notes and dictionary for Plains Cree, and an API you host for a language no service covers — all in the same project, all with the same CLI.
|
|
132
132
|
|
|
133
133
|
```json
|
|
134
134
|
{
|
|
@@ -136,7 +136,7 @@ The quick start gets you running with an LLM or Google Translate. But Google Tra
|
|
|
136
136
|
"pairs": {
|
|
137
137
|
"en:fr": { "method": "google-translate" },
|
|
138
138
|
"en:ja": { "method": "llm" },
|
|
139
|
-
"en:crk": { "
|
|
139
|
+
"en:crk": { "method": "llm-coached" }
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
```
|
|
@@ -188,7 +188,7 @@ champollion sync --method deepl
|
|
|
188
188
|
"pairs": {
|
|
189
189
|
"en:fr": { "method": "deepl" },
|
|
190
190
|
"en:ja": { "method": "openai", "model": "gpt-4o" },
|
|
191
|
-
"en:crk": { "
|
|
191
|
+
"en:crk": { "method": "llm-coached" }
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
```
|
package/lib/cards/reader.js
CHANGED
|
@@ -386,10 +386,24 @@ export function normalizeCard(card) {
|
|
|
386
386
|
if (scales) {
|
|
387
387
|
const claims = attributions(out.endangerment);
|
|
388
388
|
for (const source of scales.authorityOrder) {
|
|
389
|
-
|
|
389
|
+
// One source can hold several assessments (ELCat keeps one per
|
|
390
|
+
// record, each with its own certainty). The projector stores them
|
|
391
|
+
// alphabetically, which says nothing about confidence — reading the
|
|
392
|
+
// first one showed Plains Cree as "endangered" from a 0.2-certainty,
|
|
393
|
+
// BC-only record over ELCat's 0.8-certainty "threatened". Take the
|
|
394
|
+
// assessment the source is most certain of; ties keep stored order.
|
|
395
|
+
let hit = null;
|
|
396
|
+
let tier = null;
|
|
397
|
+
let best = -Infinity;
|
|
398
|
+
for (const c of claims) {
|
|
399
|
+
if (!String(c.source ?? '').startsWith(source)) continue;
|
|
400
|
+
const t = scales.scales[source]?.map?.[String(c.value).trim()];
|
|
401
|
+
if (!t) continue;
|
|
402
|
+
const m = /Certainty:\s*([0-9.]+)/.exec(String(c.note ?? ''));
|
|
403
|
+
const certainty = m ? Number(m[1]) : -1;
|
|
404
|
+
if (certainty > best) { best = certainty; hit = c; tier = t; }
|
|
405
|
+
}
|
|
390
406
|
if (!hit) continue;
|
|
391
|
-
const tier = scales.scales[source]?.map?.[String(hit.value).trim()];
|
|
392
|
-
if (!tier) continue;
|
|
393
407
|
out.vitality = {
|
|
394
408
|
unescoStatus: tier,
|
|
395
409
|
assessedBy: hit.source,
|
package/lib/command-help.js
CHANGED
|
@@ -195,9 +195,9 @@ const COMMAND_HELP = {
|
|
|
195
195
|
' your text is never uploaded or hosted; you keep custody.',
|
|
196
196
|
' public — publish a metadata card + fetch-from-source pointer; text never',
|
|
197
197
|
' hosted by us, and gated to redistribution-cleared licenses.',
|
|
198
|
-
' sealed — encrypt the corpus ON YOUR DEVICE
|
|
199
|
-
'
|
|
200
|
-
'
|
|
198
|
+
' sealed — encrypt the corpus ON YOUR DEVICE to the custodian group’s',
|
|
199
|
+
' key; the ciphertext stays with you and we receive only a',
|
|
200
|
+
' content-free card. Paired with a public qualifier a method',
|
|
201
201
|
' must clear before any sealed run can be proposed.',
|
|
202
202
|
'',
|
|
203
203
|
'Champollion never reads, uploads, or hosts your corpus plaintext in ANY tier.',
|
|
@@ -218,7 +218,7 @@ async function runInteractive(rl, args) {
|
|
|
218
218
|
builder = await ask(rl, 'Builder adapter id (rebuilds from source, e.g. tatoeba-challenge)', builder || '');
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
// Sealed tier — encrypt on this device
|
|
221
|
+
// Sealed tier — encrypt on this device; the ciphertext stays local, we get a content-free card.
|
|
222
222
|
let custodianGroupId = args['custodian-group'] || null;
|
|
223
223
|
let thresholdPubkey = args['threshold-pubkey'] || null;
|
|
224
224
|
let sealInput = args['seal-input'] || null;
|
|
@@ -228,8 +228,8 @@ async function runInteractive(rl, args) {
|
|
|
228
228
|
const keyScheme = args['key-scheme'] || null;
|
|
229
229
|
if (tier.key === 'sealed') {
|
|
230
230
|
console.log('');
|
|
231
|
-
console.log(' Sealed sets are encrypted ON THIS DEVICE
|
|
232
|
-
console.log(' We receive
|
|
231
|
+
console.log(' Sealed sets are encrypted ON THIS DEVICE, and the ciphertext stays');
|
|
232
|
+
console.log(' where you write it. We receive only a content-free card.');
|
|
233
233
|
console.log('');
|
|
234
234
|
sealInput = await ask(rl, 'Path to the corpus file to seal (your plaintext, stays local)', sealInput || '');
|
|
235
235
|
thresholdPubkey = await ask(rl, 'Custodian group threshold public key (path or PEM/base64)', thresholdPubkey || '');
|
|
@@ -307,7 +307,7 @@ function fromFlags(args) {
|
|
|
307
307
|
* the custodian group's threshold PUBLIC key, write a ciphertext-ONLY artifact,
|
|
308
308
|
* and return the content-free `sealed` card block. The plaintext never leaves
|
|
309
309
|
* this machine in readable form; we hold only ciphertext + metadata, and we
|
|
310
|
-
* cannot decrypt it (no single party can — see docs/governance
|
|
310
|
+
* cannot decrypt it (no single party can — see the community-custodian multisig plan in docs/governance).
|
|
311
311
|
*
|
|
312
312
|
* @returns {{cardBlock:object, artifactPath:string}|{error:string}}
|
|
313
313
|
*/
|
|
@@ -582,14 +582,14 @@ async function run(args, cwd) {
|
|
|
582
582
|
console.log(' the public queue/leaderboard (custodian-controlled, WMT-style held-out).');
|
|
583
583
|
console.log('');
|
|
584
584
|
console.log(' Next: rebuild the registry, then evaluate privately:');
|
|
585
|
-
|
|
585
|
+
for (const line of REGISTRY_REBUILD_HINT) console.log(line);
|
|
586
586
|
console.log(` mt-eval run --corpus ${id} --private # publish scores without exposing text`);
|
|
587
587
|
} else {
|
|
588
588
|
console.log(' A metadata card + fetch-from-source pointer is registered — your corpus');
|
|
589
589
|
console.log(' TEXT is never hosted by Champollion; it is fetched from source on demand.');
|
|
590
590
|
console.log('');
|
|
591
591
|
console.log(' Next: rebuild the registry so the pair enters the public queue:');
|
|
592
|
-
|
|
592
|
+
for (const line of REGISTRY_REBUILD_HINT) console.log(line);
|
|
593
593
|
}
|
|
594
594
|
console.log('');
|
|
595
595
|
return 0;
|
|
@@ -616,9 +616,9 @@ function showHelp() {
|
|
|
616
616
|
text never uploaded or hosted; you keep custody.
|
|
617
617
|
public Publish a metadata card + fetch-from-source pointer; text never
|
|
618
618
|
hosted by us. Requires a redistribution-cleared license (gated).
|
|
619
|
-
sealed Encrypt the corpus ON YOUR DEVICE
|
|
620
|
-
|
|
621
|
-
|
|
619
|
+
sealed Encrypt the corpus ON YOUR DEVICE to the custodian group's
|
|
620
|
+
key; the ciphertext stays with you and we receive only a
|
|
621
|
+
content-free card. Catalogued + quarantined; paired with a
|
|
622
622
|
public qualifier a method must clear before any sealed run.
|
|
623
623
|
|
|
624
624
|
LICENSES (--license)
|
|
@@ -675,4 +675,16 @@ function showHelp() {
|
|
|
675
675
|
`);
|
|
676
676
|
}
|
|
677
677
|
|
|
678
|
+
/**
|
|
679
|
+
* The registry-rebuild step a registrant runs after a card lands. The registry
|
|
680
|
+
* (arena/datasets/registry*.json + the wheel's bundled copy) is BUILT from the
|
|
681
|
+
* cards by the Python builder — there is no npm script for it, and a printed
|
|
682
|
+
* next-step that does not exist is a broken door. Tested against the builder's
|
|
683
|
+
* real path in test/corpus-registration.test.js.
|
|
684
|
+
*/
|
|
685
|
+
export const REGISTRY_REBUILD_HINT = Object.freeze([
|
|
686
|
+
' python3 arena/scripts/build_registry.py --diff # from the monorepo root: preview the registry change',
|
|
687
|
+
' python3 arena/scripts/build_registry.py # rebuild arena/datasets/registry*.json (+ the bundled wheel copy)',
|
|
688
|
+
]);
|
|
689
|
+
|
|
678
690
|
export { run };
|
package/lib/commands/tm.js
CHANGED
|
@@ -438,7 +438,7 @@ async function runPrune(args, cwd) {
|
|
|
438
438
|
}
|
|
439
439
|
try {
|
|
440
440
|
// Case-sensitive, no flags: banned-term patterns are usually
|
|
441
|
-
// case-shaped ("
|
|
441
|
+
// case-shaped ("NASA" vs a word that merely contains "nasa").
|
|
442
442
|
// Encode any case-insensitivity in the pattern itself ([Cc]…).
|
|
443
443
|
matching = new RegExp(matchingRaw);
|
|
444
444
|
} catch (err) {
|
|
@@ -191,7 +191,7 @@ export const EXPOSURE_TIERS = [
|
|
|
191
191
|
uploadsCiphertext: true, // an encrypted, content-free artifact MAY be produced
|
|
192
192
|
tracked: true,
|
|
193
193
|
explanation:
|
|
194
|
-
'Encrypts your corpus ON YOUR DEVICE
|
|
194
|
+
'Encrypts your corpus ON YOUR DEVICE to the custodian group’s key. The ciphertext is written where you choose and stays with you; Champollion receives only a content-free card and CANNOT decrypt anything. Catalogued but quarantined; pair it with a public qualifier that methods must clear before any sealed run can be proposed.',
|
|
195
195
|
},
|
|
196
196
|
];
|
|
197
197
|
|
|
@@ -448,7 +448,7 @@ export function buildCorpusCard(o) {
|
|
|
448
448
|
(isPublic
|
|
449
449
|
? 'Author-asserted at registration. Public fetch-from-source corpus; review upstream exposure before relying on this rating.'
|
|
450
450
|
: isSealed
|
|
451
|
-
? 'Sealed at registration — corpus encrypted client-side
|
|
451
|
+
? 'Sealed at registration — corpus encrypted client-side to the custodian group’s key; the ciphertext stays with the holder and only a content-free card reaches this project, so the plaintext is not present in any training set via this project.'
|
|
452
452
|
: 'Private/unpublished at registration — text never uploaded or hosted, so not present in any training set via this project.');
|
|
453
453
|
|
|
454
454
|
// source block — a fetch-from-source pointer ONLY for the public tier.
|
|
@@ -530,7 +530,7 @@ export function buildCorpusCard(o) {
|
|
|
530
530
|
if (isSealed) {
|
|
531
531
|
card.quarantine = true;
|
|
532
532
|
card.quarantineReason =
|
|
533
|
-
'Sealed / community-controlled secret test set — corpus encrypted client-side under the custodian group’s threshold key (exposureTier=sealed). Only
|
|
533
|
+
'Sealed / community-controlled secret test set — corpus encrypted client-side under the custodian group’s threshold key (exposureTier=sealed). Only this content-free card reaches our side; the ciphertext stays with the holder, and no single party can decrypt it. Catalogued but never publicly runnable; a sealed run requires M-of-N custodian approval (see the community-custodian multisig plan).';
|
|
534
534
|
if (o.sealed) card.sealed = o.sealed;
|
|
535
535
|
}
|
|
536
536
|
|
package/lib/scripts.js
CHANGED
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
* - Japanese: Romaji → Hiragana/Katakana
|
|
13
13
|
* - Hindi: Romanized → Devanagari
|
|
14
14
|
*
|
|
15
|
-
* All converters here are DETERMINISTIC — no LLM needed
|
|
16
|
-
*
|
|
17
|
-
* convert to display
|
|
15
|
+
* All converters here are DETERMINISTIC — no LLM needed: lookup tables, or a
|
|
16
|
+
* community-maintained converter where one exists (Plains Cree). They run as a
|
|
17
|
+
* post-translation hook: translate in working script, then convert to display
|
|
18
|
+
* script — and only over the spans of a string that are language, never over
|
|
19
|
+
* placeholders, ICU syntax, tags or URLs (see splitConvertible).
|
|
18
20
|
*
|
|
19
21
|
* ADDING A NEW CONVERTER:
|
|
20
22
|
* 1. Add the conversion map below
|
|
@@ -23,115 +25,53 @@
|
|
|
23
25
|
* 4. Add the `scripts` field to the language's register entry in registers.js
|
|
24
26
|
*/
|
|
25
27
|
|
|
28
|
+
import creeSroSyllabics from 'cree-sro-syllabics';
|
|
29
|
+
|
|
26
30
|
// -----------------------------------------------------------------
|
|
27
31
|
// Plains Cree: SRO → Syllabics
|
|
28
32
|
// -----------------------------------------------------------------
|
|
29
33
|
|
|
30
34
|
/**
|
|
31
|
-
* SRO
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
+
* Plains Cree SRO ⇄ syllabics is delegated to `cree-sro-syllabics` (MIT;
|
|
36
|
+
* Eddie Antonio Santos, NRC — the converter ALTLab's itwêwina uses). The
|
|
37
|
+
* hand-written table this replaced mapped 62 of the 64 consonant+w+vowel
|
|
38
|
+
* syllables to the wrong glyph and had no bare w-series at all, so
|
|
39
|
+
* "nêhiyawêwin" came out as ᓀᐦᐃᔭᐤᐁᐤᐃᐣ instead of ᓀᐦᐃᔭᐍᐏᐣ, and no test
|
|
40
|
+
* checked a single real spelling. Cree spelling is the community converter's
|
|
41
|
+
* call; this module only decides which spans of a UI string it may touch
|
|
42
|
+
* (see splitConvertible / convertScript).
|
|
35
43
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* ORDER MATTERS: Longer sequences must be matched before shorter ones
|
|
41
|
-
* (e.g., "twê" before "tw" before "t").
|
|
44
|
+
* Library conventions are kept as they are: a hyphen between preverb and
|
|
45
|
+
* verb becomes U+202F NARROW NO-BREAK SPACE, and a full stop after a syllabic
|
|
46
|
+
* becomes ᙮ (U+166E CANADIAN SYLLABICS FULL STOP).
|
|
42
47
|
*/
|
|
43
|
-
const
|
|
44
|
-
// Long vowels with w-glide (must come before short vowel w-glide)
|
|
45
|
-
['pwê', 'ᐻ'], ['pwî', 'ᐽ'], ['pwô', 'ᐿ'], ['pwâ', 'ᑁ'],
|
|
46
|
-
['twê', 'ᑗ'], ['twî', 'ᑙ'], ['twô', 'ᑛ'], ['twâ', 'ᑝ'],
|
|
47
|
-
['kwê', 'ᑵ'], ['kwî', 'ᑷ'], ['kwô', 'ᑹ'], ['kwâ', 'ᑻ'],
|
|
48
|
-
['cwê', 'ᒑ'], ['cwî', 'ᒓ'], ['cwô', 'ᒕ'], ['cwâ', 'ᒗ'],
|
|
49
|
-
['mwê', 'ᒫ'], ['mwî', 'ᒭ'], ['mwô', 'ᒯ'], ['mwâ', 'ᒱ'],
|
|
50
|
-
['nwê', 'ᓇ'], ['nwî', 'ᓉ'], ['nwô', 'ᓋ'], ['nwâ', 'ᓍ'],
|
|
51
|
-
['swê', 'ᓭ'], ['swî', 'ᓯ'], ['swô', 'ᓱ'], ['swâ', 'ᓳ'],
|
|
52
|
-
['ywê', 'ᔋ'], ['ywî', 'ᔍ'], ['ywô', 'ᔏ'], ['ywâ', 'ᔑ'],
|
|
53
|
-
|
|
54
|
-
// Short vowels with w-glide
|
|
55
|
-
['pwe', 'ᐺ'], ['pwi', 'ᐼ'], ['pwo', 'ᐾ'], ['pwa', 'ᑀ'],
|
|
56
|
-
['twe', 'ᑖ'], ['twi', 'ᑘ'], ['two', 'ᑚ'], ['twa', 'ᑜ'],
|
|
57
|
-
['kwe', 'ᑴ'], ['kwi', 'ᑶ'], ['kwo', 'ᑸ'], ['kwa', 'ᑺ'],
|
|
58
|
-
['cwe', 'ᒐ'], ['cwi', 'ᒒ'], ['cwo', 'ᒔ'], ['cwa', 'ᒖ'],
|
|
59
|
-
['mwe', 'ᒪ'], ['mwi', 'ᒬ'], ['mwo', 'ᒮ'], ['mwa', 'ᒰ'],
|
|
60
|
-
['nwe', 'ᓈ'], ['nwi', 'ᓊ'], ['nwo', 'ᓌ'], ['nwa', 'ᓎ'],
|
|
61
|
-
['swe', 'ᓬ'], ['swi', 'ᓮ'], ['swo', 'ᓰ'], ['swa', 'ᓲ'],
|
|
62
|
-
['ywe', 'ᔊ'], ['ywi', 'ᔌ'], ['ywo', 'ᔎ'], ['ywa', 'ᔐ'],
|
|
63
|
-
|
|
64
|
-
// Long vowels (macron forms — these must come before short vowels)
|
|
65
|
-
['pê', 'ᐯ'], ['pî', 'ᐲ'], ['pô', 'ᐴ'], ['pâ', 'ᐹ'],
|
|
66
|
-
['tê', 'ᑌ'], ['tî', 'ᑏ'], ['tô', 'ᑑ'], ['tâ', 'ᑖ'],
|
|
67
|
-
['kê', 'ᑫ'], ['kî', 'ᑮ'], ['kô', 'ᑰ'], ['kâ', 'ᑳ'],
|
|
68
|
-
['cê', 'ᒉ'], ['cî', 'ᒌ'], ['cô', 'ᒎ'], ['câ', 'ᒑ'],
|
|
69
|
-
['mê', 'ᒣ'], ['mî', 'ᒦ'], ['mô', 'ᒨ'], ['mâ', 'ᒫ'],
|
|
70
|
-
['nê', 'ᓀ'], ['nî', 'ᓃ'], ['nô', 'ᓅ'], ['nâ', 'ᓈ'],
|
|
71
|
-
['sê', 'ᓭ'], ['sî', 'ᓰ'], ['sô', 'ᓲ'], ['sâ', 'ᓵ'],
|
|
72
|
-
['yê', 'ᔦ'], ['yî', 'ᔩ'], ['yô', 'ᔫ'], ['yâ', 'ᔮ'],
|
|
73
|
-
|
|
74
|
-
// Short vowels (consonant+vowel)
|
|
75
|
-
['pe', 'ᐯ'], ['pi', 'ᐱ'], ['po', 'ᐳ'], ['pa', 'ᐸ'],
|
|
76
|
-
['te', 'ᑌ'], ['ti', 'ᑎ'], ['to', 'ᑐ'], ['ta', 'ᑕ'],
|
|
77
|
-
['ke', 'ᑫ'], ['ki', 'ᑭ'], ['ko', 'ᑯ'], ['ka', 'ᑲ'],
|
|
78
|
-
['ce', 'ᒉ'], ['ci', 'ᒋ'], ['co', 'ᒍ'], ['ca', 'ᒐ'],
|
|
79
|
-
['me', 'ᒣ'], ['mi', 'ᒥ'], ['mo', 'ᒧ'], ['ma', 'ᒪ'],
|
|
80
|
-
['ne', 'ᓀ'], ['ni', 'ᓂ'], ['no', 'ᓄ'], ['na', 'ᓇ'],
|
|
81
|
-
['se', 'ᓭ'], ['si', 'ᓯ'], ['so', 'ᓱ'], ['sa', 'ᓴ'],
|
|
82
|
-
['ye', 'ᔦ'], ['yi', 'ᔨ'], ['yo', 'ᔪ'], ['ya', 'ᔭ'],
|
|
83
|
-
|
|
84
|
-
// Standalone vowels (long first)
|
|
85
|
-
['ê', 'ᐁ'], ['î', 'ᐄ'], ['ô', 'ᐆ'], ['â', 'ᐋ'],
|
|
86
|
-
['e', 'ᐁ'], ['i', 'ᐃ'], ['o', 'ᐅ'], ['a', 'ᐊ'],
|
|
87
|
-
|
|
88
|
-
// Digraphs (must come before single-char finals)
|
|
89
|
-
['th', 'ᖧ'],
|
|
90
|
-
|
|
91
|
-
// Finals (standalone consonants — no following vowel)
|
|
92
|
-
['p', 'ᑊ'], ['t', 'ᐟ'], ['k', 'ᐠ'], ['c', 'ᐨ'],
|
|
93
|
-
['m', 'ᒼ'], ['n', 'ᐣ'], ['s', 'ᐢ'], ['y', 'ᐩ'],
|
|
94
|
-
|
|
95
|
-
// Special characters
|
|
96
|
-
['h', 'ᐦ'], ['w', 'ᐤ'], ['l', 'ᓬ'], ['r', 'ᕒ'],
|
|
97
|
-
];
|
|
48
|
+
const { sro2syllabics, syllabics2sro } = creeSroSyllabics;
|
|
98
49
|
|
|
99
50
|
/**
|
|
100
|
-
* Convert SRO text to Cree Syllabics.
|
|
101
|
-
*
|
|
102
|
-
* This is a greedy left-to-right scan: at each position, try the longest
|
|
103
|
-
* possible match first. Characters that don't match any pattern (spaces,
|
|
104
|
-
* punctuation, numbers) pass through unchanged.
|
|
51
|
+
* Convert SRO text to Cree Syllabics. Letters the library does not recognise
|
|
52
|
+
* as SRO pass through unchanged; creeLeftoverLetters() reports them.
|
|
105
53
|
*
|
|
106
54
|
* @param {string} sro - SRO text to convert
|
|
107
55
|
* @returns {string} Syllabics text
|
|
108
56
|
*/
|
|
109
57
|
function sroToSyllabics(sro) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
let i = 0;
|
|
58
|
+
return sro2syllabics(sro);
|
|
59
|
+
}
|
|
113
60
|
|
|
114
|
-
|
|
115
|
-
|
|
61
|
+
/** The Unified Canadian Aboriginal Syllabics blocks (base and extended). */
|
|
62
|
+
const SYLLABICS_CHAR = /[\u1400-\u167F\u18B0-\u18FF]/u;
|
|
116
63
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// No match — pass character through (space, punctuation, etc.)
|
|
128
|
-
if (!matched) {
|
|
129
|
-
result += input[i];
|
|
130
|
-
i++;
|
|
131
|
-
}
|
|
64
|
+
/**
|
|
65
|
+
* Letters the Cree converter left unconverted, derived from its behaviour:
|
|
66
|
+
* convert, then report every letter in the output that is not syllabics.
|
|
67
|
+
* Coverage therefore cannot drift from what the library actually does.
|
|
68
|
+
*/
|
|
69
|
+
function creeLeftoverLetters(text) {
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const ch of sroToSyllabics(text)) {
|
|
72
|
+
if (/\p{L}/u.test(ch) && !SYLLABICS_CHAR.test(ch) && !out.includes(ch)) out.push(ch);
|
|
132
73
|
}
|
|
133
|
-
|
|
134
|
-
return result;
|
|
74
|
+
return out;
|
|
135
75
|
}
|
|
136
76
|
|
|
137
77
|
// -----------------------------------------------------------------
|
|
@@ -456,9 +396,13 @@ const SCRIPT_CONVERTERS = {
|
|
|
456
396
|
fromScript: 'Latn',
|
|
457
397
|
toScript: 'Cans',
|
|
458
398
|
type: 'deterministic',
|
|
459
|
-
|
|
399
|
+
// No table of our own: conversion, coverage and reversal all come from
|
|
400
|
+
// the cree-sro-syllabics library (see the Plains Cree section above).
|
|
401
|
+
map: null,
|
|
460
402
|
puaRange: null,
|
|
461
403
|
converter: sroToSyllabics,
|
|
404
|
+
leftoverLetters: creeLeftoverLetters,
|
|
405
|
+
reverse: syllabics2sro,
|
|
462
406
|
},
|
|
463
407
|
sr: {
|
|
464
408
|
from: 'Latin',
|
|
@@ -815,6 +759,9 @@ function unmappedLetters(text, converterKey) {
|
|
|
815
759
|
const conv = SCRIPT_CONVERTERS[converterKey];
|
|
816
760
|
if (!conv || typeof text !== 'string' || text === '') return [];
|
|
817
761
|
|
|
762
|
+
// Library-backed converters report their own leftovers.
|
|
763
|
+
if (conv.leftoverLetters) return conv.leftoverLetters(text);
|
|
764
|
+
|
|
818
765
|
// Kryptonian is arithmetic over A–Z with no table; every letter outside
|
|
819
766
|
// the basic Latin alphabet is unmapped.
|
|
820
767
|
if (!conv.map) {
|
|
@@ -830,9 +777,7 @@ function unmappedLetters(text, converterKey) {
|
|
|
830
777
|
const sequences = mappedSequences(converterKey);
|
|
831
778
|
// Converters that lowercase (or uppercase) their input before matching must
|
|
832
779
|
// be probed in that same normalised form, or every capital reads as unmapped.
|
|
833
|
-
const probe = conv.converter === latinToTengwar
|
|
834
|
-
? text.toLowerCase()
|
|
835
|
-
: text;
|
|
780
|
+
const probe = conv.converter === latinToTengwar ? text.toLowerCase() : text;
|
|
836
781
|
|
|
837
782
|
const out = [];
|
|
838
783
|
let i = 0;
|
|
@@ -871,6 +816,11 @@ function reverseScript(text, converterKey) {
|
|
|
871
816
|
|
|
872
817
|
const caseLossy = conv.converter !== romanizationToPiqad;
|
|
873
818
|
|
|
819
|
+
// Library-backed converters bring their own inverse (Cree: syllabics2sro).
|
|
820
|
+
if (conv.reverse) {
|
|
821
|
+
return { reversed: conv.reverse(text), caseLossy, unreversed: [] };
|
|
822
|
+
}
|
|
823
|
+
|
|
874
824
|
if (!conv.map) {
|
|
875
825
|
// Kryptonian: U+E100–E119 → A–Z.
|
|
876
826
|
const [lo, hi] = conv.puaRange;
|
|
@@ -916,13 +866,136 @@ function isPrivateUse(codePoint) {
|
|
|
916
866
|
|| (codePoint >= 0x100000 && codePoint <= 0x10FFFD);
|
|
917
867
|
}
|
|
918
868
|
|
|
869
|
+
// Tokens inside ordinary message text that are code, not language: double-
|
|
870
|
+
// brace and ${…} placeholders, printf tokens, HTML/XML tags, URLs, e-mail.
|
|
871
|
+
const FLAT_PROTECTED = new RegExp([
|
|
872
|
+
String.raw`\{\{[^{}]*\}\}`,
|
|
873
|
+
String.raw`\$\{[^{}]*\}`,
|
|
874
|
+
String.raw`%(?:\d+\$)?[-+ #0]*\d*(?:\.\d+)?[sdifuxXoeEgGc@]`,
|
|
875
|
+
String.raw`%\([A-Za-z_][A-Za-z0-9_]*\)[sd]`,
|
|
876
|
+
String.raw`<\/?[A-Za-z][^<>]*>`,
|
|
877
|
+
String.raw`(?:https?:\/\/|www\.)[^\s<>{}"']+`,
|
|
878
|
+
String.raw`[\w.+-]+@[\w-]+\.[\w.-]+`,
|
|
879
|
+
].join('|'), 'g');
|
|
880
|
+
|
|
881
|
+
const ICU_BRANCHING = new Set(['plural', 'select', 'selectordinal']);
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Split a UI string into the spans a script converter may touch and the spans
|
|
885
|
+
* it must leave byte-for-byte.
|
|
886
|
+
*
|
|
887
|
+
* WHY: converters used to run over the whole value, so "tânisi, {name}!"
|
|
888
|
+
* became "ᑖᓂᓯ, {ᓇᒣ}!" and Serbian "{name}" became "{наме}" — a broken
|
|
889
|
+
* placeholder that crashes the app at render time and that nothing caught.
|
|
890
|
+
*
|
|
891
|
+
* Protected: ICU argument names, types, styles and plural/select selectors
|
|
892
|
+
* (the TEXT of each branch stays convertible, recursively), `#` in plural
|
|
893
|
+
* branches, and the FLAT_PROTECTED tokens. Concatenating the segments'
|
|
894
|
+
* `text` always reproduces the input exactly.
|
|
895
|
+
*
|
|
896
|
+
* @param {string} text
|
|
897
|
+
* @returns {{ text: string, convert: boolean }[]}
|
|
898
|
+
*/
|
|
899
|
+
function splitConvertible(text) {
|
|
900
|
+
const segments = [];
|
|
901
|
+
const push = (s, convert) => {
|
|
902
|
+
if (!s) return;
|
|
903
|
+
const last = segments[segments.length - 1];
|
|
904
|
+
if (last && last.convert === convert) last.text += s;
|
|
905
|
+
else segments.push({ text: s, convert });
|
|
906
|
+
};
|
|
907
|
+
const pushText = (s) => {
|
|
908
|
+
let at = 0;
|
|
909
|
+
for (const m of s.matchAll(FLAT_PROTECTED)) {
|
|
910
|
+
push(s.slice(at, m.index), true);
|
|
911
|
+
push(m[0], false);
|
|
912
|
+
at = m.index + m[0].length;
|
|
913
|
+
}
|
|
914
|
+
push(s.slice(at), true);
|
|
915
|
+
};
|
|
916
|
+
// Index of the brace that closes the one at `i`, or -1.
|
|
917
|
+
const matchingBrace = (i) => {
|
|
918
|
+
let depth = 0;
|
|
919
|
+
for (let k = i; k < text.length; k++) {
|
|
920
|
+
if (text[k] === '{') depth++;
|
|
921
|
+
else if (text[k] === '}' && --depth === 0) return k;
|
|
922
|
+
}
|
|
923
|
+
return -1;
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
// Message text from `i` until the end or (inBranch) the branch's closing
|
|
927
|
+
// brace; returns the index of that brace, or text.length.
|
|
928
|
+
function message(i, inBranch) {
|
|
929
|
+
let chunk = '';
|
|
930
|
+
while (i < text.length) {
|
|
931
|
+
const ch = text[i];
|
|
932
|
+
if (ch === '}' && inBranch) break;
|
|
933
|
+
if (ch === '{') {
|
|
934
|
+
pushText(chunk); chunk = '';
|
|
935
|
+
i = argument(i);
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (ch === '#' && inBranch) {
|
|
939
|
+
pushText(chunk); chunk = '';
|
|
940
|
+
push('#', false); i++;
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
chunk += ch; i++;
|
|
944
|
+
}
|
|
945
|
+
pushText(chunk);
|
|
946
|
+
return i;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// An ICU argument starting at the brace at `i`; returns the index after it.
|
|
950
|
+
function argument(i) {
|
|
951
|
+
const close = matchingBrace(i);
|
|
952
|
+
if (close === -1) { push(text[i], false); return i + 1; } // stray brace
|
|
953
|
+
// {{mustache}} and ${…} (the "$" was already pushed as text) stay whole.
|
|
954
|
+
if (text[i + 1] === '{' || text[i - 1] === '$') {
|
|
955
|
+
push(text.slice(i, close + 1), false);
|
|
956
|
+
return close + 1;
|
|
957
|
+
}
|
|
958
|
+
// Greedy name: a lazy one lets the optional ", plural," group match nothing.
|
|
959
|
+
const head = /^\{[^,{}]*(?:,\s*([A-Za-z]+)\s*,?)?/.exec(text.slice(i, close + 1));
|
|
960
|
+
const type = head?.[1];
|
|
961
|
+
if (!type || !ICU_BRANCHING.has(type)) {
|
|
962
|
+
push(text.slice(i, close + 1), false); // {name}, {n, number}, {d, date, short}
|
|
963
|
+
return close + 1;
|
|
964
|
+
}
|
|
965
|
+
// plural / select: protect the header and every selector, convert branches.
|
|
966
|
+
let k = i + head[0].length;
|
|
967
|
+
push(text.slice(i, k), false);
|
|
968
|
+
while (k < close) {
|
|
969
|
+
const sel = /^\s*[^\s{}]*\s*/.exec(text.slice(k, close))[0]; // "one ", "=0", "offset:1 "
|
|
970
|
+
push(sel, false); k += sel.length;
|
|
971
|
+
if (text[k] === '{') {
|
|
972
|
+
push('{', false);
|
|
973
|
+
k = message(k + 1, true);
|
|
974
|
+
push('}', false);
|
|
975
|
+
k += 1;
|
|
976
|
+
} else if (sel === '') {
|
|
977
|
+
break; // malformed; the remainder is protected below
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
push(text.slice(k, close + 1), false);
|
|
981
|
+
return close + 1;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
message(0, false);
|
|
985
|
+
return segments;
|
|
986
|
+
}
|
|
987
|
+
|
|
919
988
|
/**
|
|
920
989
|
* Convert text using the registered converter for a locale.
|
|
921
990
|
*
|
|
991
|
+
* Only the spans splitConvertible() marks as language are converted;
|
|
992
|
+
* placeholders, ICU syntax, tags and URLs come back byte-for-byte.
|
|
993
|
+
*
|
|
922
994
|
* `unmapped` lists letters the converter could not translate and passed
|
|
923
|
-
* through as-is
|
|
924
|
-
*
|
|
925
|
-
*
|
|
995
|
+
* through as-is, counted over the converted spans only. A non-empty
|
|
996
|
+
* `unmapped` means the input was not valid text in the converter's working
|
|
997
|
+
* script, and the output is a mix of both scripts — callers must treat it as
|
|
998
|
+
* a failure rather than writing it out.
|
|
926
999
|
*
|
|
927
1000
|
* @param {string} text - Text in the source script
|
|
928
1001
|
* @param {string} localeCode - Locale code (e.g., "crk", "sr")
|
|
@@ -933,11 +1006,19 @@ function convertScript(text, localeCode) {
|
|
|
933
1006
|
if (!converter) {
|
|
934
1007
|
return { converted: text, converterUsed: null, unmapped: [] };
|
|
935
1008
|
}
|
|
1009
|
+
const converterUsed = `${converter.from} → ${converter.to}`;
|
|
1010
|
+
if (typeof text !== 'string' || text === '') {
|
|
1011
|
+
return { converted: converter.converter(text ?? ''), converterUsed, unmapped: [] };
|
|
1012
|
+
}
|
|
936
1013
|
|
|
1014
|
+
const segments = splitConvertible(text);
|
|
937
1015
|
return {
|
|
938
|
-
converted: converter.converter(text),
|
|
939
|
-
converterUsed
|
|
940
|
-
unmapped: unmappedLetters(
|
|
1016
|
+
converted: segments.map((s) => (s.convert ? converter.converter(s.text) : s.text)).join(''),
|
|
1017
|
+
converterUsed,
|
|
1018
|
+
unmapped: unmappedLetters(
|
|
1019
|
+
segments.filter((s) => s.convert).map((s) => s.text).join(' '),
|
|
1020
|
+
localeCode,
|
|
1021
|
+
),
|
|
941
1022
|
};
|
|
942
1023
|
}
|
|
943
1024
|
|
|
@@ -990,5 +1071,6 @@ export {
|
|
|
990
1071
|
unmappedLetters,
|
|
991
1072
|
reverseScript,
|
|
992
1073
|
isPrivateUse,
|
|
1074
|
+
splitConvertible,
|
|
993
1075
|
SCRIPT_CONVERTERS,
|
|
994
1076
|
};
|
package/lib/seal.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* seal.mjs — CLIENT-SIDE encryption for the SEALED exposure tier.
|
|
3
3
|
*
|
|
4
4
|
* This is the cryptographic front door for the zero-knowledge sovereign-eval
|
|
5
|
-
* PoC (docs/governance
|
|
5
|
+
* PoC (community-custodian multisig plan, docs/governance, mechanism M1). A community that
|
|
6
6
|
* chooses to make a held-out set centrally evaluable-on-demand encrypts it
|
|
7
7
|
* **on their own machine, before a single byte leaves**, under a *threshold
|
|
8
8
|
* public key* the platform cannot decrypt alone. Champollion ever only receives
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
* format and the card block do NOT change under either model; that is the
|
|
48
48
|
* whole point of the seam. Historical note: earlier docs named FROST here —
|
|
49
49
|
* FROST is threshold *signing*, the wrong primitive for this decryption path;
|
|
50
|
-
* see docs/governance
|
|
50
|
+
* see the community-custodian multisig plan (docs/governance) for the corrected framing.
|
|
51
51
|
*
|
|
52
52
|
* @module seal
|
|
53
53
|
*/
|
package/lib/sealed-qualifier.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A sealed (community-controlled secret) test set is precious: every evaluation
|
|
5
5
|
* against it leaks a little signal, and custodian approval is a real,
|
|
6
|
-
* low-frequency act (M-of-N, docs/governance
|
|
6
|
+
* low-frequency act (M-of-N, community-custodian multisig plan M2, docs/governance). So a
|
|
7
7
|
* method must EARN the right to even *propose* a sealed run by first clearing a
|
|
8
8
|
* disjoint, fully public twin — the **qualifier**.
|
|
9
9
|
*
|