Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ jobs:
- name: Check bilingual parity
run: node .harness/scripts/check-bilingual-parity.mjs

# `check-bilingual-parity` compara que el par EXISTA y que las cabeceras cuadren.
# Ninguna de las dos cosas ve el IDIOMA, asi que un fichero copiado, reetiquetado
# «English (this document)» y dejado en castellano lo pasa — que es exactamente el
# estado que describen GAP-011 y GAP-017, y por eso ambas filas seguian siendo
# ciertas con la paridad en verde. Medido el 2026-08-01: 9 de 151, incluidos
# DECISIONS.md y MASTER_INDEX.md.
#
# Va como TRINQUETE: los nueve estan declarados con su motivo en
# `untranslated-allowlist.json`, asi que hoy pasa; el decimo falla.
- name: English-labelled documents actually read as English
run: node .harness/scripts/check-translation-language.mjs

- name: Validate root cleanliness
run: node .harness/scripts/validate-root-cleanliness.mjs

Expand Down
119 changes: 119 additions & 0 deletions .harness/scripts/check-translation-language.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env node
/**
* Detect `*.md` files that PRESENT themselves as the English version and are written in
* Spanish.
*
* WHY. `check-bilingual-parity` compares that the pair EXISTS and that both sides carry
* the same number of headers. Neither test can see language, so a file copied to
* `X.md`, relabelled «English (this document)» and left in Spanish passes it — which is
* precisely the state `GAP-011` (C4 topology) and `GAP-017` (Discovery Canvas) describe,
* and why both rows were still true after the parity guard went green.
*
* Measured on 2026-08-01: **10 of 153** paired documents are in this state, including
* `DECISIONS.md` and `MASTER_INDEX.md`, which are canonical root documents. The rows
* named two of the ten; nothing measured the other eight.
*
* HOW, AND WHAT IT COSTS. Language is guessed from stopword frequency — the closed
* function words that dominate any prose and barely overlap between the two languages.
* It is a heuristic, so it is applied with a MARGIN and never on a bare majority: a
* document is only reported when Spanish stopwords outnumber English ones by the factor
* below. Code blocks and inline code are stripped first, because a file full of Spanish
* identifiers is not a Spanish document.
*
* A heuristic that fails LOUDLY on a legitimate file is worse than no guard, because it
* teaches people to silence it. Hence `allow`: a file may declare, with a reason, that
* it is deliberately not translated.
*/

import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { join, relative, extname } from 'node:path';
import process from 'node:process';

const ROOT = process.cwd();
const ALLOW_FILE = 'docs/audit/untranslated-allowlist.json';
const SKIP = new Set(['node_modules', '.git', 'dist', 'bin', 'obj']);

/**
* The margin. Spanish must outnumber English by this factor before anything is
* reported. 2.0 was chosen after measuring: the ten genuine cases sit at 10× or more
* (`DECISIONS.md` is 47 to 1), while the closest legitimate document sits near parity.
* A bare majority would flag bilingual glossaries and quote-heavy documents.
*/
const MARGIN = 2.0;

/** Minimum stopword evidence. Below this the sample is too small to judge. */
const MIN_SIGNAL = 12;

const ES = /\b(que|para|de|la|del|con|una|los|las|este|esta|como|desde|entre|donde|cuando|debe|puede|sin|por|más|según|cada|todo)\b/gi;
const EN = /\b(the|and|with|this|that|from|which|must|should|when|where|between|without|for|each|every|into|than)\b/gi;

function walk(dir, out = []) {
let entries;
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return out; }
for (const e of entries) {
if (e.name.startsWith('.') || SKIP.has(e.name)) continue;
const full = join(dir, e.name);
if (e.isDirectory()) walk(full, out);
else if (extname(e.name) === '.md') out.push(full);
}
return out;
}

/** Strip code, so identifiers and commands do not vote on the prose's language. */
function prose(text) {
return text
.replace(/```[\s\S]*?```/g, ' ')
.replace(/`[^`\n]*`/g, ' ')
.replace(/^\s{4,}\S.*$/gm, ' ')
.replace(/https?:\/\/\S+/g, ' ');
}

function main() {
const allow = existsSync(join(ROOT, ALLOW_FILE))
? JSON.parse(readFileSync(join(ROOT, ALLOW_FILE), 'utf8'))
: { allow: [] };
const allowed = new Map(allow.allow.map((a) => [a.file, a.reason]));

const suspects = [];
let paired = 0;

for (const file of walk(ROOT)) {
const rel = relative(ROOT, file);
if (rel.endsWith('.es.md') || rel.startsWith('docs/audit')) continue;
if (!existsSync(file.replace(/\.md$/, '.es.md'))) continue;
paired += 1;

const body = prose(readFileSync(file, 'utf8')).slice(0, 8000);
const es = (body.match(ES) ?? []).length;
const en = (body.match(EN) ?? []).length;
if (es < MIN_SIGNAL) continue;
if (es < en * MARGIN) continue;

if (allowed.has(rel)) continue;
suspects.push({ rel, es, en });
}

console.log(
`Translation-language check: ${paired} paired document(s), margin ${MARGIN}×, ` +
`${allowed.size} allowlisted.`,
);

if (suspects.length === 0) {
console.log('✅ Every English-labelled document reads as English.');
return;
}

console.error('\n❌ Documents labelled English that read as Spanish:\n');
for (const s of suspects.sort((a, b) => b.es - b.en - (a.es - a.en))) {
console.error(` ✖ ${s.rel} (es=${s.es} en=${s.en})`);
}
console.error(
`\nEach is a file whose pair EXISTS and whose headers MATCH, so ` +
`check-bilingual-parity\npasses it. Translate it, or declare it in ${ALLOW_FILE} ` +
`with a reason —\nsilencing this guard without one puts the repository back where ` +
`it was.\n`,
);
process.exit(1);
}

main();
4 changes: 2 additions & 2 deletions docs/audit/tracker-gaps-opportunities-tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ This document is the only operational gap register in this repository. The maste

| # | Status | ID | Type | Category | Component | Module | Story(ies) | Description | Resolution / Next Step | Criticality | Complexity |
|---:|---|---|---|---|---|---|---|---|---|:---:|:---:|
| 1 | 🟡 OPEN | [GAP-011](#detail-gap-011) | Docs | Documentation gap | Docs | Docs | N/A | C4 Topology No English version | Pending owner/action definition in this register. | 🟡 MEDIUM | 🟡 MEDIUM |
| 1 | 🟡 OPEN | [GAP-011](#detail-gap-011) | Docs | Documentation gap | Docs | Docs | N/A | C4 Topology No English version | CONFIRMED and MEASURED 2026-08-01. The row is right, and the file EXISTS — that is the trap. `X.md` is present, labelled «English (this document)», and written in Spanish, so `check-bilingual-parity` passes it: that guard compares file presence and header counts, neither of which sees language. `check-translation-language` does, and found **9 documents in this state of 143 paired**, including `DECISIONS.md` (128 Spanish markers to 1 English) and `MASTER_INDEX.md`. This row names one of the nine; all nine are declared with a reason in `untranslated-allowlist.json`, so the debt is counted rather than invisible and the tenth fails CI. | 🟡 MEDIUM | 🟡 MEDIUM |
| 2 | 🟡 OPEN | [GAP-013](#detail-gap-013) | Docs | Documentation gap | Docs | Docs | N/A | 14 Technical Design Docs Lack ES Version | Pending owner/action definition in this register. | 🟡 MEDIUM | 🟡 MEDIUM |
| 3 | 🟡 OPEN | [GAP-016](#detail-gap-016) | Docs | Documentation gap | Docs | Docs | N/A | Roadmap Has No Calendar Dates | Pending owner/action definition in this register. | 🟡 MEDIUM | 🟡 MEDIUM |
| 4 | 🟡 OPEN | [GAP-017](#detail-gap-017) | Docs | Documentation gap | Docs | Docs | N/A | Discovery Canvas Has No ES Version | Pending owner/action definition in this register. | 🟡 MEDIUM | 🟡 MEDIUM |
| 4 | 🟡 OPEN | [GAP-017](#detail-gap-017) | Docs | Documentation gap | Docs | Docs | N/A | Discovery Canvas Has No ES Version | CONFIRMED and MEASURED 2026-08-01. The row is right, and the file EXISTS — that is the trap. `X.md` is present, labelled «English (this document)», and written in Spanish, so `check-bilingual-parity` passes it: that guard compares file presence and header counts, neither of which sees language. `check-translation-language` does, and found **9 documents in this state of 143 paired**, including `DECISIONS.md` (128 Spanish markers to 1 English) and `MASTER_INDEX.md`. This row names one of the nine; all nine are declared with a reason in `untranslated-allowlist.json`, so the debt is counted rather than invisible and the tenth fails CI. | 🟡 MEDIUM | 🟡 MEDIUM |
| 5 | 🟡 OPEN | [GAP-023](#detail-gap-023) | Docs | Documentation gap | Docs | Docs | N/A | Re-Do Flow Not Fully Designed | Pending owner/action definition in this register. | 🟡 MEDIUM | 🟡 MEDIUM |
| 6 | 🟡 OPEN | [GAP-025](#detail-gap-025) | Docs | Documentation gap | Docs | Docs | N/A | Portal DDD incomplete — NARROWED 2026-08-01: the strategic-map half is DONE (`reference/specs/architecture/bounded-context-map.md` calls itself «the single, authoritative strategic map» of the 9 contexts, with integration patterns and cross-context events). What remains is the four support contexts. | Only the remaining half is open. Withdrawn from `falsifiable-claims.json`: «four support contexts» needs judgement about WHICH four, and a probe that pretended to check it would refute the row on any document mentioning a context. | 🟡 MEDIUM | 🟡 MEDIUM |
| 7 | 🟡 OPEN | [COH-012](#detail-coh-012) | GAP | Missing capability / corrective gap | Backend | Discovery | US-DIS-006 | Gherkin covers only 2 of 4 CRUD operations (Create, Update). Delete and Read entirely missing. Zero edge cases. | Pending owner/action definition in this register. | 🟢 | 🟢 |
Expand Down
45 changes: 45 additions & 0 deletions docs/audit/untranslated-allowlist.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"$comment": "Documents labelled English that are written in Spanish. See .harness/scripts/check-translation-language.mjs.",
"policy": {
"why": "Nine were found on 2026-08-01 by a guard written that day; `check-bilingual-parity` cannot see language, so all nine had been passing. Declaring them makes the debt COUNTED instead of invisible, and makes the tenth fail immediately.",
"rule": "An entry is a promise to translate, not a pardon. Every reason must name the row that tracks it or state a concrete blocker. `not a priority` is not a reason."
},
"allow": [
{
"file": "DECISIONS.md",
"reason": "Root ADR index. Tracked by GAP-013 (bilingual debt). The largest of the nine at 128 Spanish stopwords to 1 English — translating it means translating every ADR summary it carries, so it is the last one to move, not the first."
},
{
"file": "MASTER_INDEX.md",
"reason": "Root navigation index. Tracked by GAP-013. Its entries are titles of other documents, so it can only be translated after the documents it points at."
},
{
"file": "docs/adrs/T-044-single-tenant-isolation-model.md",
"reason": "ADR awaiting PO ratification (its own Status says so). Translating an unratified decision would produce two versions to keep in step while its content may still change."
},
{
"file": "reference/specs/discovery/DISCOVERY_CANVAS.md",
"reason": "This IS `GAP-017` — the row is correct and stays open. Listed so the guard measures it instead of asserting it."
},
{
"file": "reference/specs/architecture/c4-macro-topology-phase1.md",
"reason": "This IS `GAP-011` — the row is correct and stays open. Listed so the guard measures it instead of asserting it."
},
{
"file": "reference/specs/architecture/scale-out-strategy.md",
"reason": "Tracked by GAP-013."
},
{
"file": "docs/diagrams/evolith-tracker-master-flow.md",
"reason": "Tracked by GAP-013. Mermaid diagram labels dominate the prose, so a translation must decide whether node labels move too — a decision nobody has taken."
},
{
"file": "docs/diagrams/evolith-tracker-geo-diagram.md",
"reason": "Tracked by GAP-013. Same Mermaid-label question as the master flow."
},
{
"file": "docs/diagrams/evolith-tracker-crosscutting-diagram.md",
"reason": "Tracked by GAP-013. Same Mermaid-label question as the master flow."
}
]
}
Loading