diff --git a/.eamos-core/orchestrator/os/localization/labels.yaml b/.eamos-core/orchestrator/os/localization/labels.yaml new file mode 100644 index 0000000..04f1dbf --- /dev/null +++ b/.eamos-core/orchestrator/os/localization/labels.yaml @@ -0,0 +1,322 @@ +# Chrome labels (RFC-0001 §7, #61) — every rendered chrome string, per language, as DATA. +# The single source of truth replacing eight duplicated in-code tables: adding a language is a +# one-file edit here, never a code change. `en` is the declared fallback. A language is supported +# only when it defines EVERY key (half-localized is worse than honestly unsupported) — enforced by +# the labels-valid gate. Namespaces preserve intentional per-deliverable differences (the .docx +# uses title-case, the sheet says "Source / note"), so values must not be merged across them. +# +# core.verify — the suffix inside the ⟨value — …⟩ grounding marker (RFC-0001 §6) +# core.redacted — the word inside the ⟦…⟧ egress-redaction mask (RFC-0001 §11) +version: 1 + +en: + core: + verify: "to verify" + redacted: "redacted" + quiz_stem: "What is the value of" + quiz_disc: "What is the stance on" + md: + target: "target" + risk: "Risk" + ask: "Ask" + decision: "Decision" + review: "Verify before the room" + objective: "Objective" + fill: "source" + pro: "pro" + con: "con" + next_step: "Next step" + residual_risk: "Residual risk" + quiz_md: + title: "Interview quiz" + graded: "Graded (citation required)" + disc: "Discussion (un-scored)" + ans: "Answer" + src: "source" + verify: "to verify" + pptx: + target: "target" + risk: "Risk" + ask: "Ask" + pro: "pro" + con: "con" + review: "Verify before the room" + fill: "source" + next_step: "Next step" + residual_risk: "Residual risk" + docx: + target: "Target" + value: "Value" + kpi: "KPI" + risk: "Risk" + ask: "Ask" + pro: "pro" + con: "con" + review: "Verify before the room" + fill: "source" + preread: "Pre-read" + next_step: "Next step" + residual_risk: "Residual risk" + svg: + decisions: "Decisions required" + target: "target" + verify: "to verify" + xlsx: + kpi: "KPI" + value: "Value" + target: "Target" + source: "Source / note" + review: "Verify before the room" + facilitate: + agenda: "Agenda" + script: "Facilitation script" + min: "min" + obj: "Objective" + talk: "Talking points" + minutes: "Minutes" + decisions: "Decisions" + actions: "Action items" + owner: "owner" + due: "due" + notes: "Notes" + attendees: "Attendees" + joins_at: "joins at" + input_gathering: "Input gathering" + live_classification: "Live classification" + targeted_questions: "Targeted questions" + structured_synthesis: "Structured synthesis" + decision_framing: "Decision framing" + +it: + core: + verify: "da verificare" + redacted: "redatto" + quiz_stem: "Qual è il valore di" + quiz_disc: "Qual è la posizione su" + md: + target: "target" + risk: "Rischio" + ask: "Richiesta" + decision: "Decisione" + review: "Da verificare prima della sala" + objective: "Obiettivo" + fill: "fonte" + pro: "pro" + con: "contro" + next_step: "Prossimo passo" + residual_risk: "Rischio residuo" + quiz_md: + title: "Quiz d'intervista" + graded: "Valutate (citazione richiesta)" + disc: "Discussione (non valutate)" + ans: "Risposta" + src: "fonte" + verify: "da verificare" + pptx: + target: "target" + risk: "Rischio" + ask: "Richiesta" + pro: "pro" + con: "contro" + review: "Da verificare prima della sala" + fill: "fonte" + next_step: "Prossimo passo" + residual_risk: "Rischio residuo" + docx: + target: "Target" + value: "Valore" + kpi: "KPI" + risk: "Rischio" + ask: "Richiesta" + pro: "pro" + con: "contro" + review: "Da verificare prima della sala" + fill: "fonte" + preread: "Pre-read" + next_step: "Prossimo passo" + residual_risk: "Rischio residuo" + svg: + decisions: "Decisioni richieste" + target: "target" + verify: "valori da verificare" + xlsx: + kpi: "KPI" + value: "Valore" + target: "Target" + source: "Fonte / nota" + review: "Da verificare prima della sala" + facilitate: + agenda: "Agenda" + script: "Script di facilitazione" + min: "min" + obj: "Obiettivo" + talk: "Punti da toccare" + minutes: "Verbale" + decisions: "Decisioni" + actions: "Azioni" + owner: "owner" + due: "scadenza" + notes: "Note" + attendees: "Partecipanti" + joins_at: "entra da" + input_gathering: "Raccolta input" + live_classification: "Classificazione dal vivo" + targeted_questions: "Domande mirate" + structured_synthesis: "Sintesi strutturata" + decision_framing: "Inquadramento decisione" + +es: + core: + verify: "por verificar" + redacted: "redactado" + quiz_stem: "¿Cuál es el valor de" + quiz_disc: "¿Cuál es la posición sobre" + md: + target: "meta" + risk: "Riesgo" + ask: "Solicitud" + decision: "Decisión" + review: "Verificar antes de la reunión" + objective: "Objetivo" + fill: "fuente" + pro: "pro" + con: "contra" + next_step: "Próximo paso" + residual_risk: "Riesgo residual" + quiz_md: + title: "Cuestionario de entrevista" + graded: "Evaluadas (cita requerida)" + disc: "Discusión (sin puntuar)" + ans: "Respuesta" + src: "fuente" + verify: "por verificar" + pptx: + target: "meta" + risk: "Riesgo" + ask: "Solicitud" + pro: "pro" + con: "contra" + review: "Verificar antes de la reunión" + fill: "fuente" + next_step: "Próximo paso" + residual_risk: "Riesgo residual" + docx: + target: "Meta" + value: "Valor" + kpi: "KPI" + risk: "Riesgo" + ask: "Solicitud" + pro: "pro" + con: "contra" + review: "Verificar antes de la reunión" + fill: "fuente" + preread: "Prelectura" + next_step: "Próximo paso" + residual_risk: "Riesgo residual" + svg: + decisions: "Decisiones requeridas" + target: "meta" + verify: "valores por verificar" + xlsx: + kpi: "KPI" + value: "Valor" + target: "Meta" + source: "Fuente / nota" + review: "Verificar antes de la reunión" + facilitate: + agenda: "Agenda" + script: "Guion de facilitación" + min: "min" + obj: "Objetivo" + talk: "Puntos a tratar" + minutes: "Acta" + decisions: "Decisiones" + actions: "Acciones" + owner: "responsable" + due: "vencimiento" + notes: "Notas" + attendees: "Participantes" + joins_at: "se une desde" + input_gathering: "Recogida de datos" + live_classification: "Clasificación en vivo" + targeted_questions: "Preguntas dirigidas" + structured_synthesis: "Síntesis estructurada" + decision_framing: "Encuadre de la decisión" + +fr: + core: + verify: "à vérifier" + redacted: "caviardé" + quiz_stem: "Quelle est la valeur de" + quiz_disc: "Quelle est la position sur" + md: + target: "cible" + risk: "Risque" + ask: "Demande" + decision: "Décision" + review: "À vérifier avant la réunion" + objective: "Objectif" + fill: "source" + pro: "pour" + con: "contre" + next_step: "Prochaine étape" + residual_risk: "Risque résiduel" + quiz_md: + title: "Quiz d'entretien" + graded: "Notées (citation requise)" + disc: "Discussion (non notées)" + ans: "Réponse" + src: "source" + verify: "à vérifier" + pptx: + target: "cible" + risk: "Risque" + ask: "Demande" + pro: "pour" + con: "contre" + review: "À vérifier avant la réunion" + fill: "source" + next_step: "Prochaine étape" + residual_risk: "Risque résiduel" + docx: + target: "Cible" + value: "Valeur" + kpi: "KPI" + risk: "Risque" + ask: "Demande" + pro: "pour" + con: "contre" + review: "À vérifier avant la réunion" + fill: "source" + preread: "Pré-lecture" + next_step: "Prochaine étape" + residual_risk: "Risque résiduel" + svg: + decisions: "Décisions requises" + target: "cible" + verify: "valeurs à vérifier" + xlsx: + kpi: "KPI" + value: "Valeur" + target: "Cible" + source: "Source / note" + review: "À vérifier avant la réunion" + facilitate: + agenda: "Ordre du jour" + script: "Script de facilitation" + min: "min" + obj: "Objectif" + talk: "Points à aborder" + minutes: "Compte rendu" + decisions: "Décisions" + actions: "Actions" + owner: "responsable" + due: "échéance" + notes: "Notes" + attendees: "Participants" + joins_at: "rejoint à" + input_gathering: "Recueil des données" + live_classification: "Classification en direct" + targeted_questions: "Questions ciblées" + structured_synthesis: "Synthèse structurée" + decision_framing: "Cadrage de la décision" diff --git a/.eamos-core/tools/eamos_lint.py b/.eamos-core/tools/eamos_lint.py index 7d6735c..681e648 100644 --- a/.eamos-core/tools/eamos_lint.py +++ b/.eamos-core/tools/eamos_lint.py @@ -28,6 +28,8 @@ classification-valid — a problem-type classification (if present) uses taxonomy-allowed values. questions-valid — the adaptive question tree + routing stay consistent with the taxonomy. topology-valid — a topology diagram's edges/kinds/patterns are declared and in-vocabulary. + labels-valid — every language in os/localization/labels.yaml defines every chrome key + the en fallback defines (a language is supported when ALL chrome exists). """ import os @@ -36,6 +38,7 @@ TOOLS = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, TOOLS) import _cli # noqa: E402 (utf8_stdio, #54) +import labels # noqa: E402 (chrome labels as data, #61) import render # noqa: E402 (reuses the loader + build_deck_ir) import yamlmini # noqa: E402 @@ -275,6 +278,32 @@ def gate_topology_valid(manifest): return findings +def gate_labels_valid(): + """Chrome-label completeness (#61): every language in os/localization/labels.yaml defines every + key the `en` fallback defines, and nothing more — a language is supported when ALL chrome exists + (half-localized is worse than honestly unsupported), and a key unknown to the fallback is drift. + Structural + decidable, manifest-independent; vacuous if the file is absent.""" + findings = [] + data = labels.load() + ref = data.get(labels.FALLBACK) or {} + if not ref: + return findings + for lang, tables in sorted(data.items()): + if lang == labels.FALLBACK or not isinstance(tables, dict): + continue # skip the reference itself + scalars (version) + for ns, keys in sorted(ref.items()): + have = tables.get(ns) or {} + for k in sorted(keys): + if k not in have: + findings.append(("labels-valid", f"language '{lang}' is missing label '{ns}.{k}'")) + for ns, keys in sorted(tables.items()): + for k in sorted(keys or {}): + if k not in (ref.get(ns) or {}): + findings.append(("labels-valid", + f"label '{ns}.{k}' in '{lang}' is not defined by the '{labels.FALLBACK}' fallback")) + return findings + + def gate_registry_params(manifest): """Every deliverable's params are within the registry's declared enums (RFC-0002 §4).""" findings = [] @@ -365,6 +394,7 @@ def run_all(manifest, archetype, deck_ir, acc, ledger): findings += gate_classification_valid(manifest) findings += gate_questions_valid() findings += gate_topology_valid(manifest) + findings += gate_labels_valid() findings += gate_registry_params(manifest) findings += gate_rubric(manifest, deck_ir, {g for g, _ in findings}) findings += gate_confidentiality(manifest, {g for g, _ in findings}) diff --git a/.eamos-core/tools/emit_docx.py b/.eamos-core/tools/emit_docx.py index 9b8bdf5..6b4ae65 100644 --- a/.eamos-core/tools/emit_docx.py +++ b/.eamos-core/tools/emit_docx.py @@ -16,25 +16,15 @@ import os import sys -import _cli # utf8_stdio (#54) +import _cli # utf8_stdio (#54) +import labels # chrome labels as data (#61) AMBER = (0xB8, 0x6B, 0x00) MUTED = (0x70, 0x70, 0x70) -LABELS = { - "it": {"target": "Target", "value": "Valore", "kpi": "KPI", "risk": "Rischio", "ask": "Richiesta", - "pro": "pro", "con": "contro", "review": "Da verificare prima della sala", - "fill": "fonte", "preread": "Pre-read", - "next_step": "Prossimo passo", "residual_risk": "Rischio residuo"}, - "en": {"target": "Target", "value": "Value", "kpi": "KPI", "risk": "Risk", "ask": "Ask", - "pro": "pro", "con": "con", "review": "Verify before the room", - "fill": "source", "preread": "Pre-read", - "next_step": "Next step", "residual_risk": "Residual risk"}, -} - def _lab(lang, key): - return LABELS.get(lang, LABELS["en"]).get(key, LABELS["en"][key]) + return labels.lab(lang, "docx", key) # chrome labels as data (#61) def _run(paragraph, text, bold=False, color=None): diff --git a/.eamos-core/tools/emit_md.py b/.eamos-core/tools/emit_md.py index 7e25414..c3d7946 100644 --- a/.eamos-core/tools/emit_md.py +++ b/.eamos-core/tools/emit_md.py @@ -12,21 +12,13 @@ import os import sys -import _cli # utf8_stdio (#54) - -# Template chrome in the deck's output language (the section labels, not the content). -LABELS = { - "it": {"target": "target", "risk": "Rischio", "ask": "Richiesta", "decision": "Decisione", - "review": "Da verificare prima della sala", "objective": "Obiettivo", "fill": "fonte", - "pro": "pro", "con": "contro", "next_step": "Prossimo passo", "residual_risk": "Rischio residuo"}, - "en": {"target": "target", "risk": "Risk", "ask": "Ask", "decision": "Decision", - "review": "Verify before the room", "objective": "Objective", "fill": "source", - "pro": "pro", "con": "con", "next_step": "Next step", "residual_risk": "Residual risk"}, -} +import _cli # utf8_stdio (#54) +import labels # chrome labels as data (#61) def _lab(lang, key): - return LABELS.get(lang, LABELS["en"]).get(key, LABELS["en"][key]) + # Template chrome in the deck's output language (the section labels, not the content). + return labels.lab(lang, "md", key) def render_md(ir): @@ -87,17 +79,11 @@ def render_md(ir): return "\n".join(out).rstrip() + "\n" -_QUIZ_L = { - "it": {"title": "Quiz d'intervista", "graded": "Valutate (citazione richiesta)", - "disc": "Discussione (non valutate)", "ans": "Risposta", "src": "fonte", "verify": "da verificare"}, - "en": {"title": "Interview quiz", "graded": "Graded (citation required)", - "disc": "Discussion (un-scored)", "ans": "Answer", "src": "source", "verify": "to verify"}, -} def render_quiz_md(ir): lang = ir.get("output_lang", "en") - t = _QUIZ_L.get(lang, _QUIZ_L["en"]) + t = labels.table(lang, "quiz_md") out = [f"# {t['title']} — {ir.get('title', '')}", ""] graded = [q for q in ir.get("questions", []) if q.get("kind") == "graded"] disc = [q for q in ir.get("questions", []) if q.get("kind") == "discussion"] diff --git a/.eamos-core/tools/emit_pptx.py b/.eamos-core/tools/emit_pptx.py index 620a42e..bc87114 100644 --- a/.eamos-core/tools/emit_pptx.py +++ b/.eamos-core/tools/emit_pptx.py @@ -16,25 +16,17 @@ import os import sys -import _cli # utf8_stdio (#54) +import _cli # utf8_stdio (#54) +import labels # chrome labels as data (#61) NAVY = (0x1E, 0x27, 0x61) # title BODY = (0x2B, 0x2B, 0x2B) # body text AMBER = (0xB8, 0x6B, 0x00) # assumed / to-verify MUTED = (0x70, 0x70, 0x70) # captions -LABELS = { - "it": {"target": "target", "risk": "Rischio", "ask": "Richiesta", "pro": "pro", "con": "contro", - "review": "Da verificare prima della sala", "fill": "fonte", - "next_step": "Prossimo passo", "residual_risk": "Rischio residuo"}, - "en": {"target": "target", "risk": "Risk", "ask": "Ask", "pro": "pro", "con": "con", - "review": "Verify before the room", "fill": "source", - "next_step": "Next step", "residual_risk": "Residual risk"}, -} - def _lab(lang, key): - return LABELS.get(lang, LABELS["en"]).get(key, LABELS["en"][key]) + return labels.lab(lang, "pptx", key) # chrome labels as data (#61) def _line(block, lang): diff --git a/.eamos-core/tools/emit_svg.py b/.eamos-core/tools/emit_svg.py index 7c94fe6..227457e 100644 --- a/.eamos-core/tools/emit_svg.py +++ b/.eamos-core/tools/emit_svg.py @@ -18,7 +18,8 @@ import os import sys -import _cli # utf8_stdio (#54) +import _cli # utf8_stdio (#54) +import labels # chrome labels as data (#61) # Per visual_style palette (background, accent, body, amber, muted, card). Unknown styles fall back # to professional. Layout is shared; only the palette/theme changes (RFC-0002 §11-1). @@ -26,14 +27,10 @@ "professional": ("#FFFFFF", "#1E2761", "#2B2B2B", "#B86B00", "#707070", "#EEF1F7"), "scientific": ("#FFFFFF", "#0B3D5C", "#22303A", "#B8860B", "#5A6B73", "#EAF1F5"), } -LABELS = { - "it": {"decisions": "Decisioni richieste", "target": "target", "verify": "valori da verificare"}, - "en": {"decisions": "Decisions required", "target": "target", "verify": "to verify"}, -} def _lab(lang, key): - return LABELS.get(lang, LABELS["en"]).get(key, LABELS["en"][key]) + return labels.lab(lang, "svg", key) # chrome labels as data (#61) def esc(s): diff --git a/.eamos-core/tools/emit_xlsx.py b/.eamos-core/tools/emit_xlsx.py index 9434a80..34f0dfb 100644 --- a/.eamos-core/tools/emit_xlsx.py +++ b/.eamos-core/tools/emit_xlsx.py @@ -16,18 +16,11 @@ import os import sys -import _cli # utf8_stdio (#54) - -LABELS = { - "it": {"kpi": "KPI", "value": "Valore", "target": "Target", "source": "Fonte / nota", - "review": "Da verificare prima della sala"}, - "en": {"kpi": "KPI", "value": "Value", "target": "Target", "source": "Source / note", - "review": "Verify before the room"}, -} - +import _cli # utf8_stdio (#54) +import labels # chrome labels as data (#61) def _lab(lang, key): - return LABELS.get(lang, LABELS["en"]).get(key, LABELS["en"][key]) + return labels.lab(lang, "xlsx", key) # chrome labels as data (#61) def build_xlsx(ir, out_path): diff --git a/.eamos-core/tools/facilitate.py b/.eamos-core/tools/facilitate.py index 3bb17f5..bc004c3 100644 --- a/.eamos-core/tools/facilitate.py +++ b/.eamos-core/tools/facilitate.py @@ -25,30 +25,13 @@ TOOLS = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, TOOLS) import _cli # noqa: E402 (utf8_stdio, #54) +import labels # noqa: E402 (chrome labels as data, #61) import yamlmini # noqa: E402 import render # noqa: E402 import series # noqa: E402 -LABELS = { - "it": {"agenda": "Agenda", "script": "Script di facilitazione", "min": "min", "obj": "Obiettivo", - "talk": "Punti da toccare", "minutes": "Verbale", "decisions": "Decisioni", - "actions": "Azioni", "owner": "owner", "due": "scadenza", "notes": "Note", - "attendees": "Partecipanti", "joins_at": "entra da", - "input_gathering": "Raccolta input", "live_classification": "Classificazione dal vivo", - "targeted_questions": "Domande mirate", "structured_synthesis": "Sintesi strutturata", - "decision_framing": "Inquadramento decisione"}, - "en": {"agenda": "Agenda", "script": "Facilitation script", "min": "min", "obj": "Objective", - "talk": "Talking points", "minutes": "Minutes", "decisions": "Decisions", - "actions": "Action items", "owner": "owner", "due": "due", "notes": "Notes", - "attendees": "Attendees", "joins_at": "joins at", - "input_gathering": "Input gathering", "live_classification": "Live classification", - "targeted_questions": "Targeted questions", "structured_synthesis": "Structured synthesis", - "decision_framing": "Decision framing"}, -} - - def _lab(lang, k): - return LABELS.get(lang, LABELS["en"]).get(k, LABELS["en"][k]) + return labels.lab(lang, "facilitate", k) # chrome labels as data (#61) def _load(path, what="manifest"): diff --git a/.eamos-core/tools/labels.py b/.eamos-core/tools/labels.py new file mode 100644 index 0000000..ed794f3 --- /dev/null +++ b/.eamos-core/tools/labels.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Shared chrome-label loader (#61) — the three-tier language model's chrome as data. + +os/localization/labels.yaml is the single source of truth for every rendered chrome string: the +grounding-marker suffix, the redaction mask, the deck / sheet / SVG / quiz / facilitation labels. +It replaces eight duplicated in-code tables that had already drifted (RFC-0001 §7): adding a +language is a one-file data edit, and the labels-valid gate enforces that every language defines +every key — a language is supported when ALL chrome exists, half-localized is worse than honestly +unsupported. + +Namespaces preserve intentional per-deliverable differences (the .docx uses title-case "Target", +the sheet says "Source / note"): core, md, quiz_md, pptx, docx, svg, xlsx, facilitate. +""" + +import os + +TOOLS = os.path.dirname(os.path.abspath(__file__)) +PATH = os.path.join(os.path.dirname(TOOLS), "orchestrator", "os", "localization", "labels.yaml") +FALLBACK = "en" + +_cache = None + + +def load(): + """The full labels table (cached), or {} if the file is absent.""" + global _cache + if _cache is None: + if os.path.exists(PATH): + import yamlmini + with open(PATH, encoding="utf-8") as fh: + _cache = yamlmini.load_yaml(fh.read()) + else: + _cache = {} + return _cache + + +def table(lang, ns): + """The `ns` label table for `lang`; missing keys fall back per-key to English.""" + data = load() + base = (data.get(FALLBACK) or {}).get(ns) or {} + cur = (data.get(lang) or {}).get(ns) or {} + return {**base, **cur} + + +def lab(lang, ns, key): + """One chrome label; '' only if the key is unknown to the fallback too (labels-valid catches it).""" + return table(lang, ns).get(key, "") diff --git a/.eamos-core/tools/render.py b/.eamos-core/tools/render.py index 9061562..5e0f8bc 100644 --- a/.eamos-core/tools/render.py +++ b/.eamos-core/tools/render.py @@ -20,6 +20,7 @@ TOOLS = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, TOOLS) import _cli # noqa: E402 (utf8_stdio, #54) +import labels # noqa: E402 (chrome labels as data, #61) import yamlmini # noqa: E402 CORE = os.path.dirname(TOOLS) @@ -32,8 +33,6 @@ ROUTING = os.path.join(CORE, "orchestrator", "os", "intake", "routing.yaml") BIND_RE = re.compile(r"\{\{\s*([a-z][a-z0-9_.]*)\s*\}\}") -# The "verify before the room" label for an assumed value, by output language (RFC-0001 §6). -VERIFY_LABEL = {"it": "da verificare", "en": "to verify", "es": "por verificar", "fr": "à vérifier"} # The provenance enum. Anything else — a typo, a missing field, a future value — is treated as # assumed (fail closed, #53) and flagged for the grounding gate: only the explicit `sourced` # may render plain. @@ -45,7 +44,8 @@ def _new_acc(): def _mark(value, lang): - return f"⟨{value} — {VERIFY_LABEL.get(lang, VERIFY_LABEL['en'])}⟩" + # The "verify before the room" label for an assumed value (RFC-0001 §6) — chrome as data (#61). + return f"⟨{value} — {labels.lab(lang, 'core', 'verify')}⟩" def _cell_assumed(key, cell, acc): @@ -272,7 +272,7 @@ def apply_redaction(manifest, policy): the shared ledger — so EVERY projection is redacted identically (one ledger, RFC-0002 §7).""" tags = redact_tags_for(manifest, policy) lang = (manifest.get("context", {}) or {}).get("output_lang", "en") - mask = "⟦redatto⟧" if lang == "it" else "⟦redacted⟧" + mask = f"⟦{labels.lab(lang, 'core', 'redacted')}⟧" n = 0 for cell in (manifest.get("inputs", {}) or {}).values(): if isinstance(cell, dict) and any(cell.get(t) for t in tags): @@ -535,10 +535,6 @@ def build_graph_ir(manifest, archetype, altitude=None, function=None): "branches": branches, "review_appendix": review_appendix}, acc -_QUIZ_STEM = {"it": "Qual è il valore di", "en": "What is the value of"} -_QUIZ_DISC = {"it": "Qual è la posizione su", "en": "What is the stance on"} - - def build_quiz_ir(manifest, archetype, altitude=None, function=None): """Project an interview quiz (quiz-IR, RFC-0002 §3, §11-3): graded questions cite a ledger source (required); discussion questions (from decisions) are un-scored. SAME ledger.""" @@ -548,7 +544,7 @@ def build_quiz_ir(manifest, archetype, altitude=None, function=None): ledger = scorecard_ledger(manifest) content = manifest.get("content", {}) or {} lang = ctx.get("output_lang", "en") - stem, disc = _QUIZ_STEM.get(lang, _QUIZ_STEM["en"]), _QUIZ_DISC.get(lang, _QUIZ_DISC["en"]) + stem, disc = labels.lab(lang, "core", "quiz_stem"), labels.lab(lang, "core", "quiz_disc") questions = [] for key, cell in ledger.items(): diff --git a/.eamos-core/tools/tests/test_gates.py b/.eamos-core/tools/tests/test_gates.py index 44e28df..c05e613 100644 --- a/.eamos-core/tools/tests/test_gates.py +++ b/.eamos-core/tools/tests/test_gates.py @@ -175,6 +175,33 @@ def test_sox_makes_grounding_mandatory(self): self.assertIn("confidentiality", ids) # SOX flags the failed mandatory gate +class TestLabelsValid(unittest.TestCase): + def test_incomplete_language_fails_labels_valid(self): + import labels + orig = labels._cache + labels._cache = {"en": {"core": {"verify": "to verify"}}, + "de": {"core": {}}} # half-localized language (#61) + try: + findings = eamos_lint.gate_labels_valid() + self.assertIn(("labels-valid", "language 'de' is missing label 'core.verify'"), findings) + finally: + labels._cache = orig + + def test_key_unknown_to_fallback_fails_labels_valid(self): + import labels + orig = labels._cache + labels._cache = {"en": {"core": {"verify": "to verify"}}, + "it": {"core": {"verify": "da verificare", "vrfy": "typo"}}} # drift (#61) + try: + findings = eamos_lint.gate_labels_valid() + self.assertTrue(any("not defined by the 'en' fallback" in msg for _, msg in findings)) + finally: + labels._cache = orig + + def test_shipped_labels_are_complete(self): + self.assertEqual(eamos_lint.gate_labels_valid(), []) # es/fr gap is closed + + class TestUserErrors(unittest.TestCase): def test_empty_identity_does_not_crash_gates(self): arch = render.load_archetype("review") diff --git a/.eamos-core/tools/tests/test_render.py b/.eamos-core/tools/tests/test_render.py index eef546e..71040da 100644 --- a/.eamos-core/tools/tests/test_render.py +++ b/.eamos-core/tools/tests/test_render.py @@ -93,6 +93,29 @@ def test_invalid_provenance_fails_closed_across_projections(self): q = next(q for q in quiz["questions"] if q["kind"] == "graded" and "ARR" in q["q"]) self.assertTrue(q["assumed"]) + def test_es_deliverable_is_fully_localized(self): + # An output_lang the chrome doesn't fully cover used to render MIXED-language (#61): + # Spanish marker, English headings. Now every chrome string localizes together. + import emit_md + m, arch = load("qbr-c-level") + m["context"]["output_lang"] = "es" + deck, _ = render.build_deck_ir(m, arch) + md = emit_md.render_md(deck) + self.assertIn("por verificar", md) # grounding marker + self.assertIn("Riesgo", md) # chrome label + self.assertIn("Verificar antes de la reunión", md) # review appendix heading + self.assertNotIn("Verify before the room", md) # no English fallback leaks + + def test_fr_verify_marker_and_chrome(self): + import emit_md + m, arch = load("qbr-c-level") + m["context"]["output_lang"] = "fr" + deck, _ = render.build_deck_ir(m, arch) + md = emit_md.render_md(deck) + self.assertIn("à vérifier", md) + self.assertIn("Risque", md) + self.assertNotIn("Verify before the room", md) + def test_invalid_provenance_fails_closed_in_topology(self): m, arch = load("vendor-prework") m["inputs"]["sys.platform"]["provenance"] = "asumed" # typo must fail closed (#53)