champollion 0.3.3
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/LICENSE +133 -0
- package/README.md +387 -0
- package/bin/cli.js +278 -0
- package/index.js +135 -0
- package/lib/api-key.js +127 -0
- package/lib/autofix.js +432 -0
- package/lib/bridge/method_bridge.py +430 -0
- package/lib/card-source-resolution.mjs +284 -0
- package/lib/cards/cache.js +169 -0
- package/lib/cards/env.js +82 -0
- package/lib/cards/fetch-card-child.js +38 -0
- package/lib/cards/reader.js +435 -0
- package/lib/cards/refresh.js +111 -0
- package/lib/cards/remote.js +387 -0
- package/lib/cldf-export.mjs +540 -0
- package/lib/cldf-terms.mjs +62 -0
- package/lib/command-help.js +790 -0
- package/lib/commands/audit.js +49 -0
- package/lib/commands/card.js +454 -0
- package/lib/commands/doctor.js +559 -0
- package/lib/commands/fonts.js +489 -0
- package/lib/commands/help.js +91 -0
- package/lib/commands/init.js +1259 -0
- package/lib/commands/integrity.js +148 -0
- package/lib/commands/leaderboard.js +478 -0
- package/lib/commands/lint.js +30 -0
- package/lib/commands/models.js +177 -0
- package/lib/commands/plugin.js +103 -0
- package/lib/commands/provenance.js +45 -0
- package/lib/commands/recommend.js +75 -0
- package/lib/commands/register-corpus.js +678 -0
- package/lib/commands/repair-script.js +42 -0
- package/lib/commands/seal-corpus.js +355 -0
- package/lib/commands/seo.js +72 -0
- package/lib/commands/serve.js +147 -0
- package/lib/commands/status.js +265 -0
- package/lib/commands/submit.js +332 -0
- package/lib/commands/sync.js +89 -0
- package/lib/commands/tm.js +573 -0
- package/lib/commands/verify.js +39 -0
- package/lib/commands/watch.js +20 -0
- package/lib/commands/wrap.js +138 -0
- package/lib/commands/xliff.js +327 -0
- package/lib/commercial-eligibility.js +235 -0
- package/lib/concurrent.js +87 -0
- package/lib/config.js +523 -0
- package/lib/contamination-lane.js +76 -0
- package/lib/content-sync.js +731 -0
- package/lib/content.js +733 -0
- package/lib/corpus-registration.mjs +608 -0
- package/lib/cost-report.js +346 -0
- package/lib/diff.js +155 -0
- package/lib/docusaurus-sync.js +1256 -0
- package/lib/flatten.js +55 -0
- package/lib/format.js +954 -0
- package/lib/hash.js +159 -0
- package/lib/icu.js +473 -0
- package/lib/integrity.js +689 -0
- package/lib/license-gate.mjs +478 -0
- package/lib/license-identify.mjs +229 -0
- package/lib/lint.js +629 -0
- package/lib/method-manifest.js +60 -0
- package/lib/methods/anthropic.js +140 -0
- package/lib/methods/apertium.js +163 -0
- package/lib/methods/api.js +316 -0
- package/lib/methods/base.js +184 -0
- package/lib/methods/content-separator.js +45 -0
- package/lib/methods/deepl.js +426 -0
- package/lib/methods/direct-llm.js +586 -0
- package/lib/methods/external.js +332 -0
- package/lib/methods/fetch-with-retry.js +124 -0
- package/lib/methods/gemini.js +147 -0
- package/lib/methods/google-translate.js +402 -0
- package/lib/methods/http-utils.js +122 -0
- package/lib/methods/libretranslate.js +314 -0
- package/lib/methods/llm-coached.js +670 -0
- package/lib/methods/llm.js +592 -0
- package/lib/methods/local.js +76 -0
- package/lib/methods/microsoft-translator.js +331 -0
- package/lib/methods/openai.js +131 -0
- package/lib/methods/openrouter-client.js +327 -0
- package/lib/methods/openrouter-pricing.js +156 -0
- package/lib/methods/provider-env.js +115 -0
- package/lib/methods/provider-pricing.js +310 -0
- package/lib/methods/tilde.js +150 -0
- package/lib/methods/translated.js +229 -0
- package/lib/methods/translation-error.js +80 -0
- package/lib/models.js +258 -0
- package/lib/no-translate.js +233 -0
- package/lib/output.js +238 -0
- package/lib/pairs.js +547 -0
- package/lib/plugins.js +447 -0
- package/lib/provenance.js +323 -0
- package/lib/recommend.js +648 -0
- package/lib/registers.js +1185 -0
- package/lib/repair-script.js +266 -0
- package/lib/scripts.js +994 -0
- package/lib/seal.mjs +464 -0
- package/lib/sealed-qualifier.mjs +211 -0
- package/lib/security.js +59 -0
- package/lib/segment.js +369 -0
- package/lib/seo.js +275 -0
- package/lib/serve.js +854 -0
- package/lib/string-classify.js +85 -0
- package/lib/submit.mjs +344 -0
- package/lib/sync.js +969 -0
- package/lib/tags/bcp47.js +202 -0
- package/lib/tags/resolve.js +314 -0
- package/lib/terminology.js +111 -0
- package/lib/tm-seed.js +294 -0
- package/lib/tm.js +515 -0
- package/lib/translate-pair.js +197 -0
- package/lib/translate.js +203 -0
- package/lib/types.js +230 -0
- package/lib/validate.js +510 -0
- package/lib/verify.js +451 -0
- package/lib/watch.js +145 -0
- package/lib/xliff.js +184 -0
- package/package.json +93 -0
- package/shared/ATTRIBUTION.md +145 -0
- package/shared/CORPORA-CARDS.md +288 -0
- package/shared/DATA-SOVEREIGNTY.md +500 -0
- package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
- package/shared/card-lint-baseline.json +3189 -0
- package/shared/cards-fallback.json +1 -0
- package/shared/catalogue/card-config.json +6091 -0
- package/shared/catalogue/external-results.json +3888 -0
- package/shared/catalogue/gender-guidance.json +1038 -0
- package/shared/catalogue/method-coverage.json +1751 -0
- package/shared/catalogue/metric-coverage.json +170 -0
- package/shared/catalogue/metric-reliability.json +1 -0
- package/shared/catalogue/register-presets.json +3180 -0
- package/shared/catalogue/vitality-scales.json +55 -0
- package/shared/cldr-index.json +1115 -0
- package/shared/code-bridge.json +253 -0
- package/shared/corpora-cards-v1-reference.md +281 -0
- package/shared/curated-dictionary-flags.json +35 -0
- package/shared/curated-endonyms.json +35 -0
- package/shared/curated-fsts.json +51 -0
- package/shared/curated-orthography-conventions.json +26 -0
- package/shared/curated-sil-resources.json +374 -0
- package/shared/curated-tools.json +41 -0
- package/shared/docent/corpus.json +11333 -0
- package/shared/docent/faq.en.json +564 -0
- package/shared/docent/register-blocks.json +60 -0
- package/shared/docent/system-prompt.md +144 -0
- package/shared/domain-taxonomy.json +35 -0
- package/shared/explainers/glossary.json +2975 -0
- package/shared/explainers/tc-features.json +20112 -0
- package/shared/explainers/term-watchlist.json +147 -0
- package/shared/human-services.json +59 -0
- package/shared/license-corrections.json +261 -0
- package/shared/license-evidence.json +13452 -0
- package/shared/licenses.json +6781 -0
- package/shared/method-registry.json +236 -0
- package/shared/metric-registry.json +620 -0
- package/shared/model-aliases.json +7 -0
- package/shared/schemas/champollion-plugin.schema.json +206 -0
- package/shared/schemas/corpora-card.schema.json +957 -0
- package/shared/schemas/domain-taxonomy.schema.json +64 -0
- package/shared/schemas/external-results.schema.json +314 -0
- package/shared/schemas/human-services.schema.json +90 -0
- package/shared/schemas/language-card.schema.json +1308 -0
- package/shared/schemas/licenses.schema.json +155 -0
- package/shared/schemas/method-card.schema.json +412 -0
- package/shared/schemas/method-registry.schema.json +85 -0
- package/shared/schemas/metric-registry.schema.json +96 -0
- package/shared/schemas/metric-reliability.schema.json +178 -0
- package/shared/schemas/model-aliases.schema.json +27 -0
- package/shared/schemas/source-snapshot.schema.json +96 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String classification — shared heuristics for detecting non-translatable strings.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS: Both lint.js (shouldFlagString) and autofix.js (shouldFixText)
|
|
5
|
+
* need to distinguish user-facing text from code artifacts (identifiers, URLs,
|
|
6
|
+
* numbers, etc.). They previously maintained independent regex sets that were
|
|
7
|
+
* ~70% identical, creating a drift risk. This module provides the shared core
|
|
8
|
+
* that both call, plus lint-specific extensions.
|
|
9
|
+
*
|
|
10
|
+
* DESIGN: Conservative by default. When in doubt, return false (= "this looks
|
|
11
|
+
* like code, don't flag/fix it"). False negatives (missing a hardcoded string)
|
|
12
|
+
* are cheaper than false positives (wrapping a CSS class name in t()).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if a string is clearly non-translatable (code artifact, not user-facing text).
|
|
17
|
+
*
|
|
18
|
+
* Returns true if the string should be EXCLUDED from translation detection.
|
|
19
|
+
* This is the shared core used by both lint and autofix.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} text - The string to classify
|
|
22
|
+
* @param {number} minLength - Minimum length to consider (strings shorter than this are excluded)
|
|
23
|
+
* @returns {boolean} true if the string is non-translatable (should be skipped)
|
|
24
|
+
*/
|
|
25
|
+
function isNonTranslatableString(text, minLength = 2) {
|
|
26
|
+
const t = text.trim();
|
|
27
|
+
if (t.length < minLength) return true;
|
|
28
|
+
if (!t) return true;
|
|
29
|
+
|
|
30
|
+
// Pure punctuation/symbols
|
|
31
|
+
if (/^[\s\p{P}\p{S}]+$/u.test(t)) return true;
|
|
32
|
+
// Pure numbers (with currency symbols)
|
|
33
|
+
if (/^[\d.,\-+%$€£¥]+$/.test(t)) return true;
|
|
34
|
+
// camelCase identifier (starts with lowercase letter, $, or _)
|
|
35
|
+
if (/^[a-z_$][a-zA-Z0-9_$]*$/.test(t)) return true;
|
|
36
|
+
// SCREAMING_SNAKE_CASE constant
|
|
37
|
+
if (/^[A-Z][A-Z0-9_]+$/.test(t)) return true;
|
|
38
|
+
// Dot-notation path (e.g., "hero.title", "pages.about.description")
|
|
39
|
+
if (/^[\w]+(?:\.[\w]+)+$/.test(t)) return true;
|
|
40
|
+
// URLs
|
|
41
|
+
if (/^https?:\/\//.test(t)) return true;
|
|
42
|
+
// Template expressions (e.g., {{ .Title }})
|
|
43
|
+
if (/^\{\{.*\}\}$/.test(t)) return true;
|
|
44
|
+
// Hex colors (e.g., #ff0000, #333)
|
|
45
|
+
if (/^#[0-9a-fA-F]{3,8}$/.test(t)) return true;
|
|
46
|
+
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Additional lint-specific checks for non-translatable strings.
|
|
52
|
+
*
|
|
53
|
+
* These extend the shared core with patterns that are relevant for
|
|
54
|
+
* source code scanning (lint/flag) but not for autofix wrapping.
|
|
55
|
+
* Lint scans broader file types and sees more code artifacts.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} text - The string to classify
|
|
58
|
+
* @returns {boolean} true if the string is non-translatable (should be skipped)
|
|
59
|
+
*/
|
|
60
|
+
function isNonTranslatableLintExtended(text) {
|
|
61
|
+
const t = text.trim();
|
|
62
|
+
|
|
63
|
+
// File paths (e.g., ./components/Header, /images/logo.png)
|
|
64
|
+
if (/^\.?\/[\w\-./]/.test(t)) return true;
|
|
65
|
+
// Email addresses
|
|
66
|
+
if (/^\S+@\S+\.\S+$/.test(t)) return true;
|
|
67
|
+
// HTML entities (e.g., &, )
|
|
68
|
+
if (/^&\w+;$/.test(t)) return true;
|
|
69
|
+
// Single HTML tag names (e.g., div, span, img — short lowercase words)
|
|
70
|
+
if (/^[a-z][a-z0-9]*$/i.test(t) && t.length <= 5) return true;
|
|
71
|
+
|
|
72
|
+
// --- TypeScript type signatures ---
|
|
73
|
+
// Fragments like "Promise", "string[]", "React.FC", "void", "null"
|
|
74
|
+
if (/^(?:Promise|string|number|boolean|void|null|undefined|any|never|unknown|object|bigint|symbol)(?:\[\]|<.*>)?$/.test(t)) return true;
|
|
75
|
+
// Patterns like "}: Promise<void>" or "): string" (type annotation fragments)
|
|
76
|
+
if (/^[)}\]]+\s*[:,]?\s*\w/.test(t)) return true;
|
|
77
|
+
// Generic type syntax: "Record<string, any>", "Array<number>"
|
|
78
|
+
if (/^[A-Z]\w*<[^>]+>$/.test(t)) return true;
|
|
79
|
+
// Type union/intersection fragments: "string | null", "Foo & Bar"
|
|
80
|
+
if (/^\w+(?:\s*[|&]\s*\w+)+$/.test(t)) return true;
|
|
81
|
+
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { isNonTranslatableString, isNonTranslatableLintExtended };
|
package/lib/submit.mjs
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* submit — declarative model behind `champollion submit`.
|
|
3
|
+
*
|
|
4
|
+
* The CLI front door to Champollion's REVIEW-GATED community submission path.
|
|
5
|
+
* It gathers the fields a submission needs and hands the contributor a
|
|
6
|
+
* pre-filled GitHub issue URL (and, optionally, a local submission JSON). The
|
|
7
|
+
* GitHub issue IS the review queue: a maintainer reads it and — only after an
|
|
8
|
+
* IP / license / sovereignty review — adds the entry to the relevant
|
|
9
|
+
* source-of-truth. Nothing here writes to a registry, a card, or a database,
|
|
10
|
+
* and nothing is auto-approved. See docs/SUBMISSION_REVIEW.md.
|
|
11
|
+
*
|
|
12
|
+
* This module is the single source of truth for the submission TYPES and their
|
|
13
|
+
* fields. The field `id`s MUST stay in lockstep with the GitHub issue forms in
|
|
14
|
+
* .github/ISSUE_TEMPLATE/*.yml — the issue-form pre-fill query params are keyed
|
|
15
|
+
* by these ids. A parity test (cli/test/submit.test.js) checks that alignment.
|
|
16
|
+
*
|
|
17
|
+
* Pure + side-effect-free: every export is a plain function over plain data, so
|
|
18
|
+
* it is trivially unit-testable and reused by the interactive wizard, the
|
|
19
|
+
* non-interactive flag path, the URL builder, and the local-JSON writer alike.
|
|
20
|
+
*
|
|
21
|
+
* @see cli/lib/commands/submit.js
|
|
22
|
+
* @see .github/ISSUE_TEMPLATE/
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The compliance attestation every submission must carry. A single source of
|
|
27
|
+
* truth so the CLI, the issue forms, and the docs all say the SAME thing.
|
|
28
|
+
*/
|
|
29
|
+
export const ATTESTATION_TEXT =
|
|
30
|
+
'I confirm this is publicly listable, contains NO corpus content or personal data, ' +
|
|
31
|
+
"and respects the source's license and any community/sovereignty restrictions.";
|
|
32
|
+
|
|
33
|
+
/** Repo the issue forms live in. Overridable, but this is the real default. */
|
|
34
|
+
export const DEFAULT_REPO_BASE = 'https://github.com/gamedaysuits/Champollion';
|
|
35
|
+
|
|
36
|
+
// Field-kind cheat sheet:
|
|
37
|
+
// input — single-line free text
|
|
38
|
+
// text — multi-line free text (textarea)
|
|
39
|
+
// choice — one-of a fixed list (`choices`)
|
|
40
|
+
// bool — a required confirmation (attestation / consent); never sent in the
|
|
41
|
+
// pre-fill URL (GitHub can't pre-check a checkbox), only recorded
|
|
42
|
+
// locally and enforced by validation.
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The six submission types. Each maps to exactly one GitHub issue form and one
|
|
46
|
+
* downstream source-of-truth (recorded in `ssot` for the maintainer doc, not
|
|
47
|
+
* touched here). `primary: true` marks the field used to build the issue title.
|
|
48
|
+
*/
|
|
49
|
+
export const SUBMISSION_TYPES = [
|
|
50
|
+
{
|
|
51
|
+
key: 'dataset',
|
|
52
|
+
label: 'Benchmark / dataset',
|
|
53
|
+
template: '01-dataset.yml',
|
|
54
|
+
titlePrefix: '[Dataset] ',
|
|
55
|
+
labels: ['submission', 'submission:dataset', 'needs-review'],
|
|
56
|
+
ssot: 'arena/datasets/registry.json (+ a corpora-card)',
|
|
57
|
+
blurb: 'An evaluation corpus or benchmark — metadata + a fetch-from-source pointer only, never the corpus content.',
|
|
58
|
+
fields: [
|
|
59
|
+
{ id: 'dataset-name', label: 'Dataset name', kind: 'input', required: true, primary: true },
|
|
60
|
+
{ id: 'pairs', label: 'Language pair(s) (ISO 639-3, e.g. eng-crk; one per line)', kind: 'text', required: true },
|
|
61
|
+
{ id: 'license', label: 'License', kind: 'choice', required: true,
|
|
62
|
+
choices: ['CC-BY-4.0', 'CC-BY-SA-4.0', 'CC0-1.0', 'CC-BY-NC-4.0', 'Proprietary / all rights reserved', 'Other / unsure'] },
|
|
63
|
+
{ id: 'license-url', label: 'License URL', kind: 'input' },
|
|
64
|
+
{ id: 'source-url', label: 'Source / canonical URL', kind: 'input', required: true },
|
|
65
|
+
{ id: 'fetch-url', label: 'Fetch-from-source pointer (archive/repo URL)', kind: 'input' },
|
|
66
|
+
{ id: 'size', label: 'Size (number of sentence pairs)', kind: 'input' },
|
|
67
|
+
{ id: 'domain', label: 'Domain (news, conversational, educational, …)', kind: 'input' },
|
|
68
|
+
{ id: 'contamination', label: 'Contamination risk', kind: 'choice',
|
|
69
|
+
choices: ['NONE', 'LOW', 'MEDIUM', 'HIGH', 'Unsure'] },
|
|
70
|
+
{ id: 'held-out', label: 'Held-out / test split?', kind: 'choice',
|
|
71
|
+
choices: ['No — public / dev split', 'Yes — held-out test split', 'Unsure'] },
|
|
72
|
+
{ id: 'notes', label: 'Anything else', kind: 'text' },
|
|
73
|
+
{ id: 'attestation', label: ATTESTATION_TEXT, kind: 'bool', required: true },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: 'resource',
|
|
78
|
+
label: 'Resource (dictionary / archive / app / FST / tool)',
|
|
79
|
+
template: '02-resource.yml',
|
|
80
|
+
titlePrefix: '[Resource] ',
|
|
81
|
+
labels: ['submission', 'submission:resource', 'needs-review'],
|
|
82
|
+
ssot: 'a language card / resource entry',
|
|
83
|
+
blurb: 'A dictionary, archive, app, morphological analyzer (FST), or tool — listed as a pointer, never copied.',
|
|
84
|
+
fields: [
|
|
85
|
+
{ id: 'resource-name', label: 'Resource name', kind: 'input', required: true, primary: true },
|
|
86
|
+
{ id: 'resource-type', label: 'Resource type', kind: 'choice', required: true,
|
|
87
|
+
choices: ['Dictionary / lexicon', 'Archive / corpus collection', 'App / web service', 'FST / morphological analyzer', 'Tool / library', 'Other'] },
|
|
88
|
+
{ id: 'languages', label: 'Language(s) (ISO 639-3, one per line)', kind: 'text', required: true },
|
|
89
|
+
{ id: 'license', label: 'License', kind: 'choice', required: true,
|
|
90
|
+
choices: ['CC-BY-4.0', 'CC-BY-SA-4.0', 'CC0-1.0', 'CC-BY-NC-4.0', 'GPL / AGPL', 'Apache-2.0 / MIT / permissive', 'Proprietary / all rights reserved', 'Other / unsure'] },
|
|
91
|
+
{ id: 'license-url', label: 'License URL', kind: 'input' },
|
|
92
|
+
{ id: 'source-url', label: 'Source / homepage URL', kind: 'input', required: true },
|
|
93
|
+
{ id: 'access-level', label: 'Access level', kind: 'choice', required: true,
|
|
94
|
+
choices: ['open — freely accessible', 'restricted — login / request / paywall', 'consent-required — community / custodian permission needed'] },
|
|
95
|
+
{ id: 'description', label: 'Description', kind: 'text' },
|
|
96
|
+
{ id: 'notes', label: 'Anything else', kind: 'text' },
|
|
97
|
+
{ id: 'attestation', label: ATTESTATION_TEXT, kind: 'bool', required: true },
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
key: 'method',
|
|
102
|
+
label: 'Translation method',
|
|
103
|
+
template: '03-method.yml',
|
|
104
|
+
titlePrefix: '[Method] ',
|
|
105
|
+
labels: ['submission', 'submission:method', 'needs-review'],
|
|
106
|
+
ssot: 'shared/method-registry.json (+ a runtime adapter)',
|
|
107
|
+
blurb: 'A translation method / MT engine / LLM provider for the method registry.',
|
|
108
|
+
fields: [
|
|
109
|
+
{ id: 'method-name', label: 'Method / engine name (kebab-case id)', kind: 'input', required: true, primary: true },
|
|
110
|
+
{ id: 'kind', label: 'Kind', kind: 'choice', required: true,
|
|
111
|
+
choices: ['mt-api', 'llm-provider', 'local-model', 'plugin'] },
|
|
112
|
+
{ id: 'method-class', label: 'Method class', kind: 'choice', required: true,
|
|
113
|
+
choices: ['raw-llm', 'coached-llm', 'pipeline', 'custom-plugin', 'api', 'human'] },
|
|
114
|
+
{ id: 'paradigm', label: 'Paradigm', kind: 'choice', required: true,
|
|
115
|
+
choices: ['rule-based', 'statistical', 'neural-nmt', 'llm', 'hybrid', 'human', 'unknown'] },
|
|
116
|
+
{ id: 'languages', label: 'Languages / pairs supported (one per line; "any" is fine)', kind: 'text' },
|
|
117
|
+
{ id: 'license', label: 'License (the real one — Apache-2.0, AGPL-3.0, proprietary ToS, …)', kind: 'input', required: true },
|
|
118
|
+
{ id: 'homepage', label: 'Homepage / source URL', kind: 'input', required: true },
|
|
119
|
+
{ id: 'env', label: 'API-key environment variable(s) — names only, never a key', kind: 'input' },
|
|
120
|
+
{ id: 'commercial-ready', label: 'Commercial-ready?', kind: 'choice',
|
|
121
|
+
choices: ['yes', 'no', 'unknown'] },
|
|
122
|
+
{ id: 'description', label: 'Description', kind: 'text' },
|
|
123
|
+
{ id: 'attestation', label: ATTESTATION_TEXT, kind: 'bool', required: true },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
key: 'human-service',
|
|
128
|
+
label: 'Human translation service',
|
|
129
|
+
template: '04-human-service.yml',
|
|
130
|
+
titlePrefix: '[Human service] ',
|
|
131
|
+
labels: ['submission', 'submission:human-service', 'needs-review'],
|
|
132
|
+
ssot: 'shared/human-services.json',
|
|
133
|
+
blurb: 'An opt-in human translation provider keyed by language pair. Contact PII is supplied out-of-band, never in the issue.',
|
|
134
|
+
fields: [
|
|
135
|
+
{ id: 'display-name', label: 'Provider display name (consented public name)', kind: 'input', required: true, primary: true },
|
|
136
|
+
{ id: 'provider-type', label: 'Provider type', kind: 'choice', required: true,
|
|
137
|
+
choices: ['community_org', 'agency', 'individual'] },
|
|
138
|
+
{ id: 'pairs', label: 'Language pair(s) (ISO 639-3 source→target, one per line)', kind: 'text', required: true },
|
|
139
|
+
{ id: 'variety', label: 'Variety / script (e.g. SRO, Latn)', kind: 'input' },
|
|
140
|
+
{ id: 'dispatch-channel', label: 'Dispatch channel (the address itself is PII — omit it)', kind: 'choice',
|
|
141
|
+
choices: ['email', 'webhook', 'api'] },
|
|
142
|
+
{ id: 'turnaround-days', label: 'Typical turnaround (days)', kind: 'input' },
|
|
143
|
+
{ id: 'rate-per-word', label: 'Rate per source word', kind: 'input' },
|
|
144
|
+
{ id: 'domains', label: 'Domains served', kind: 'input' },
|
|
145
|
+
{ id: 'nc-terms', label: 'Non-commercial / community-set terms (if any)', kind: 'input' },
|
|
146
|
+
{ id: 'sovereignty-notes', label: 'Sovereignty / data-handling notes', kind: 'text' },
|
|
147
|
+
{ id: 'consent', label: 'The provider has explicitly consented to be listed publicly.', kind: 'bool', required: true },
|
|
148
|
+
{ id: 'attestation', label: ATTESTATION_TEXT, kind: 'bool', required: true },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
key: 'external-result',
|
|
153
|
+
label: 'External published result',
|
|
154
|
+
template: '05-external-result.yml',
|
|
155
|
+
titlePrefix: '[External result] ',
|
|
156
|
+
labels: ['submission', 'submission:external-result', 'needs-review'],
|
|
157
|
+
ssot: 'the external-results catalogue (shared/external-results.json — created on first acceptance)',
|
|
158
|
+
blurb: 'A published result from another system/paper — cited, never re-hosted or re-ranked as our own measurement.',
|
|
159
|
+
fields: [
|
|
160
|
+
{ id: 'system-name', label: 'System / method name', kind: 'input', required: true, primary: true },
|
|
161
|
+
{ id: 'pairs', label: 'Language pair(s) (ISO 639-3 source→target, one per line)', kind: 'text', required: true },
|
|
162
|
+
{ id: 'dataset', label: 'Dataset / benchmark', kind: 'input', required: true },
|
|
163
|
+
{ id: 'metric', label: 'Metric', kind: 'choice', required: true,
|
|
164
|
+
choices: ['chrF++', 'chrF', 'BLEU', 'COMET', 'TER', 'Other'] },
|
|
165
|
+
{ id: 'metric-variant', label: 'Metric variant / signature (e.g. a sacreBLEU signature)', kind: 'input' },
|
|
166
|
+
{ id: 'score', label: 'Score', kind: 'input', required: true },
|
|
167
|
+
{ id: 'citation', label: 'Citation / paper URL', kind: 'input', required: true },
|
|
168
|
+
{ id: 'notes', label: 'Anything else', kind: 'text' },
|
|
169
|
+
{ id: 'attestation', label: ATTESTATION_TEXT, kind: 'bool', required: true },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
key: 'card-correction',
|
|
174
|
+
label: 'Language-card correction',
|
|
175
|
+
template: '06-card-correction.yml',
|
|
176
|
+
titlePrefix: '[Card] ',
|
|
177
|
+
labels: ['submission', 'submission:card-correction', 'needs-review'],
|
|
178
|
+
ssot: 'the feeding data source / generator (cards are regenerated, never hand-edited)',
|
|
179
|
+
blurb: 'A language card shows something wrong, outdated, or missing — corrections are applied at the data source and cite their own source.',
|
|
180
|
+
fields: [
|
|
181
|
+
{ id: 'language', label: 'Language (name and code, e.g. Plains Cree (crk))', kind: 'input', required: true, primary: true },
|
|
182
|
+
{ id: 'card-field', label: 'Which part of the card (field or section)', kind: 'input', required: true },
|
|
183
|
+
{ id: 'current-value', label: 'What the card shows now', kind: 'text', required: true },
|
|
184
|
+
{ id: 'proposed-value', label: 'What it should show', kind: 'text', required: true },
|
|
185
|
+
{ id: 'source', label: 'Source for the correction (URL / reference / knowledge-holder with consent)', kind: 'text', required: true },
|
|
186
|
+
{ id: 'relationship', label: 'Your relationship to this language (optional)', kind: 'choice',
|
|
187
|
+
choices: ['Speaker / community member', 'Community organization / language authority', 'Researcher / linguist', 'Resource maintainer / publisher', 'Other / prefer not to say'] },
|
|
188
|
+
{ id: 'notes', label: 'Anything else (incl. removal/restriction requests)', kind: 'text' },
|
|
189
|
+
{ id: 'attestation', label: ATTESTATION_TEXT, kind: 'bool', required: true },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
/** Aliases that resolve to a canonical type key (forgiving CLI input). */
|
|
195
|
+
const TYPE_ALIASES = {
|
|
196
|
+
dataset: 'dataset', benchmark: 'dataset', corpus: 'dataset', data: 'dataset',
|
|
197
|
+
resource: 'resource', dictionary: 'resource', archive: 'resource', app: 'resource', fst: 'resource', tool: 'resource',
|
|
198
|
+
method: 'method', engine: 'method', model: 'method',
|
|
199
|
+
'human-service': 'human-service', human: 'human-service', 'human-translation': 'human-service', service: 'human-service', translator: 'human-service',
|
|
200
|
+
'external-result': 'external-result', external: 'external-result', result: 'external-result', published: 'external-result',
|
|
201
|
+
'card-correction': 'card-correction', card: 'card-correction', correction: 'card-correction', fix: 'card-correction',
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Resolve a free-form type token (key, alias, label, or 1-based list number)
|
|
206
|
+
* to its submission-type object. Returns null when nothing matches.
|
|
207
|
+
*
|
|
208
|
+
* @param {string|number} input
|
|
209
|
+
* @returns {object|null}
|
|
210
|
+
*/
|
|
211
|
+
export function resolveType(input) {
|
|
212
|
+
if (input == null || input === '') return null;
|
|
213
|
+
const raw = String(input).trim().toLowerCase();
|
|
214
|
+
// 1-based list number
|
|
215
|
+
if (/^\d+$/.test(raw)) {
|
|
216
|
+
const idx = Number(raw) - 1;
|
|
217
|
+
return SUBMISSION_TYPES[idx] || null;
|
|
218
|
+
}
|
|
219
|
+
const canon = TYPE_ALIASES[raw];
|
|
220
|
+
if (canon) return SUBMISSION_TYPES.find((t) => t.key === canon) || null;
|
|
221
|
+
// direct key or label match
|
|
222
|
+
return SUBMISSION_TYPES.find(
|
|
223
|
+
(t) => t.key === raw || t.label.toLowerCase() === raw,
|
|
224
|
+
) || null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Normalize a package.json `repository.url` (or any GitHub URL) to the
|
|
229
|
+
* `https://github.com/<owner>/<repo>` base used to build issue links. Falls
|
|
230
|
+
* back to DEFAULT_REPO_BASE for anything unrecognizable (never throws).
|
|
231
|
+
*
|
|
232
|
+
* @param {string} [repoUrl]
|
|
233
|
+
* @returns {string}
|
|
234
|
+
*/
|
|
235
|
+
export function repoBaseFromRepository(repoUrl) {
|
|
236
|
+
if (!repoUrl) return DEFAULT_REPO_BASE;
|
|
237
|
+
// Matches both https (git+https://github.com/o/r.git) and ssh
|
|
238
|
+
// (git@github.com:o/r.git) forms.
|
|
239
|
+
const m = String(repoUrl).match(/github\.com[/:]([^/]+)\/([^/#?]+?)(?:\.git)?(?:[/#?].*)?$/i);
|
|
240
|
+
if (!m) return DEFAULT_REPO_BASE;
|
|
241
|
+
return `https://github.com/${m[1]}/${m[2]}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Validate a gathered submission: every required field present, and the
|
|
246
|
+
* attestation (and, for human services, consent) affirmatively checked.
|
|
247
|
+
* Collects ALL problems and fails loud — never silently drops a field.
|
|
248
|
+
*
|
|
249
|
+
* @param {object} type one of SUBMISSION_TYPES
|
|
250
|
+
* @param {Record<string,*>} values
|
|
251
|
+
* @returns {{ok: boolean, errors: string[]}}
|
|
252
|
+
*/
|
|
253
|
+
export function validateSubmission(type, values = {}) {
|
|
254
|
+
const errors = [];
|
|
255
|
+
if (!type) return { ok: false, errors: ['unknown submission type — run `champollion submit --list`'] };
|
|
256
|
+
for (const f of type.fields) {
|
|
257
|
+
const v = values[f.id];
|
|
258
|
+
if (f.kind === 'bool') {
|
|
259
|
+
// Required confirmations must be an affirmative true.
|
|
260
|
+
if (f.required && v !== true) {
|
|
261
|
+
errors.push(`you must confirm: "${f.label}"`);
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (f.required) {
|
|
266
|
+
const empty = v == null || String(v).trim() === '';
|
|
267
|
+
if (empty) errors.push(`missing required field: ${f.id} (${f.label})`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { ok: errors.length === 0, errors };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The field whose value seeds the issue title (the `primary` one). */
|
|
274
|
+
function primaryField(type) {
|
|
275
|
+
return type.fields.find((f) => f.primary) || null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Build the pre-filled GitHub issue-form URL. The query is keyed by issue-form
|
|
280
|
+
* field `id` (so the ids here MUST match the .yml forms). Empty values and
|
|
281
|
+
* `bool` fields are omitted — GitHub cannot pre-check a checkbox, and the human
|
|
282
|
+
* must tick the attestation themselves in the browser.
|
|
283
|
+
*
|
|
284
|
+
* @param {object} args
|
|
285
|
+
* @param {object} args.type one of SUBMISSION_TYPES
|
|
286
|
+
* @param {Record<string,*>} args.values
|
|
287
|
+
* @param {string} [args.repoBase] defaults to DEFAULT_REPO_BASE
|
|
288
|
+
* @returns {string} an absolute https URL
|
|
289
|
+
*/
|
|
290
|
+
export function buildIssueUrl({ type, values = {}, repoBase = DEFAULT_REPO_BASE }) {
|
|
291
|
+
const params = new URLSearchParams();
|
|
292
|
+
params.set('template', type.template);
|
|
293
|
+
params.set('labels', type.labels.join(','));
|
|
294
|
+
|
|
295
|
+
const pf = primaryField(type);
|
|
296
|
+
const primaryVal = pf ? String(values[pf.id] ?? '').trim() : '';
|
|
297
|
+
params.set('title', `${type.titlePrefix}${primaryVal}`.trimEnd());
|
|
298
|
+
|
|
299
|
+
for (const f of type.fields) {
|
|
300
|
+
if (f.kind === 'bool') continue; // checkboxes can't be pre-checked via URL
|
|
301
|
+
const v = values[f.id];
|
|
302
|
+
if (v == null || String(v).trim() === '') continue;
|
|
303
|
+
params.set(f.id, String(v));
|
|
304
|
+
}
|
|
305
|
+
return `${repoBase}/issues/new?${params.toString()}`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Build a content-free local submission record (what `--out` writes). Carries
|
|
310
|
+
* the gathered metadata + the issue URL — never corpus content or PII; the
|
|
311
|
+
* contributor is responsible for honoring the attestation they confirmed.
|
|
312
|
+
*
|
|
313
|
+
* @param {object} args
|
|
314
|
+
* @param {object} args.type
|
|
315
|
+
* @param {Record<string,*>} args.values
|
|
316
|
+
* @param {string} [args.repoBase]
|
|
317
|
+
* @param {string} [args.generatedAt] ISO timestamp (caller supplies; this module stays clock-free)
|
|
318
|
+
* @returns {object}
|
|
319
|
+
*/
|
|
320
|
+
export function buildSubmissionRecord({ type, values = {}, repoBase = DEFAULT_REPO_BASE, generatedAt = null }) {
|
|
321
|
+
const fields = {};
|
|
322
|
+
for (const f of type.fields) {
|
|
323
|
+
if (f.kind === 'bool') continue;
|
|
324
|
+
const v = values[f.id];
|
|
325
|
+
if (v == null || String(v).trim() === '') continue;
|
|
326
|
+
fields[f.id] = String(v);
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
champollion_submission: 1,
|
|
330
|
+
type: type.key,
|
|
331
|
+
target_ssot: type.ssot,
|
|
332
|
+
fields,
|
|
333
|
+
attestation: {
|
|
334
|
+
text: ATTESTATION_TEXT,
|
|
335
|
+
confirmed: values.attestation === true,
|
|
336
|
+
...(type.fields.some((f) => f.id === 'consent')
|
|
337
|
+
? { provider_consent: values.consent === true }
|
|
338
|
+
: {}),
|
|
339
|
+
},
|
|
340
|
+
review: 'human-reviewed — not auto-approved',
|
|
341
|
+
issueUrl: buildIssueUrl({ type, values, repoBase }),
|
|
342
|
+
...(generatedAt ? { generatedAt } : {}),
|
|
343
|
+
};
|
|
344
|
+
}
|