euparliamentmonitor 0.9.20 → 0.9.21
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 +2 -2
- package/package.json +2 -2
- package/scripts/aggregator/editorial-brief-resolver.d.ts +38 -0
- package/scripts/aggregator/editorial-brief-resolver.js +32 -0
- package/scripts/aggregator/generator/render-one.js +35 -0
- package/scripts/aggregator/html/localize-body.d.ts +32 -0
- package/scripts/aggregator/html/localize-body.js +69 -0
- package/scripts/aggregator/html/shell.d.ts +10 -0
- package/scripts/aggregator/html/shell.js +11 -1
- package/scripts/aggregator/markdown-renderer.d.ts +23 -24
- package/scripts/aggregator/markdown-renderer.js +39 -25
- package/scripts/aggregator/metadata/resolve-helpers.js +9 -3
- package/scripts/aggregator/reader-guide/builder.js +3 -1
- package/scripts/aggregator/reader-guide/labels.d.ts +7 -0
- package/scripts/aggregator/reader-guide/labels.js +22 -0
- package/scripts/aggregator/reader-intelligence-guide.d.ts +1 -1
- package/scripts/aggregator/reader-intelligence-guide.js +1 -1
- package/scripts/aggregator/seo-entity-extractor.d.ts +45 -0
- package/scripts/aggregator/seo-entity-extractor.js +211 -0
- package/scripts/discover-untranslated-briefs.js +123 -4
- package/scripts/generators/news-indexes/per-language.js +21 -7
- package/scripts/generators/political-intelligence/html.js +39 -8
- package/scripts/generators/sitemap/html.js +25 -7
- package/scripts/mcp/ep/error-classifier.d.ts +2 -2
- package/scripts/mcp/ep/error-classifier.js +2 -2
- package/scripts/validate-brief-translations.js +119 -5
|
@@ -4,7 +4,7 @@ import { _parseResultPayload } from './parse.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Classify an error message into a diagnostic error category.
|
|
6
6
|
*
|
|
7
|
-
* Maps EP MCP Server v1.3.
|
|
7
|
+
* Maps EP MCP Server v1.3.10 structured error codes and generic HTTP/network
|
|
8
8
|
* errors into one of six broad categories used for logging and retry decisions:
|
|
9
9
|
*
|
|
10
10
|
* Returned categories (priority order):
|
|
@@ -55,7 +55,7 @@ export function classifyToolError(message) {
|
|
|
55
55
|
* covering the two shapes historically emitted by the EP MCP server.
|
|
56
56
|
*
|
|
57
57
|
* 1. **Uniform envelope** (all feeds as of
|
|
58
|
-
* `european-parliament-mcp-server@1.3.
|
|
58
|
+
* `european-parliament-mcp-server@1.3.10`) —
|
|
59
59
|
* `{status:"unavailable", items:[], generatedAt:"..."}` established by
|
|
60
60
|
* Hack23/European-Parliament-MCP-Server#301 and extended to
|
|
61
61
|
* `get_events_feed`/`get_procedures_feed` by
|
|
@@ -34,9 +34,25 @@
|
|
|
34
34
|
* is a machine-readable fixed token; dropping a diagram silently breaks
|
|
35
35
|
* downstream HTML rendering.
|
|
36
36
|
*
|
|
37
|
+
* **Skeleton-aware mode**: when a translation file declares itself as a
|
|
38
|
+
* Phase A skeleton via the `<!-- translation-skeleton: lang=<code> ... -->`
|
|
39
|
+
* marker (written by the `news-translate` workflow's 2-phase
|
|
40
|
+
* largeSource strategy), gates 3–7 are SKIPPED and a single
|
|
41
|
+
* `skeleton-incomplete` advisory is emitted instead. The advisory is
|
|
42
|
+
* classified as `severity: warning` and does NOT cause a non-zero exit
|
|
43
|
+
* unless `--strict-skeletons` is passed. This is intentional: emergency
|
|
44
|
+
* partial flushes (Step 4b of the translate workflow) write skeleton
|
|
45
|
+
* stubs for languages that did not reach Phase B before the wall-clock
|
|
46
|
+
* budget expired; those stubs would otherwise trigger 5+ cascading
|
|
47
|
+
* violations per file (length-floor, fixed-token-preservation,
|
|
48
|
+
* heading-parity, mermaid-parity) that drown out real defects in fully
|
|
49
|
+
* translated siblings. Real translations in the same brief continue to
|
|
50
|
+
* receive strict validation.
|
|
51
|
+
*
|
|
37
52
|
* Each translation that fails any gate produces a structured report entry.
|
|
38
53
|
* The process exits with code 1 if any failures are present (unless
|
|
39
|
-
* `--no-fail` is passed for advisory mode
|
|
54
|
+
* `--no-fail` is passed for advisory mode, or unless every remaining
|
|
55
|
+
* violation has `severity: warning`).
|
|
40
56
|
*
|
|
41
57
|
* This script is invoked by:
|
|
42
58
|
* - `npm run validate:translations` (CI + local)
|
|
@@ -48,6 +64,8 @@
|
|
|
48
64
|
* [--paths <glob>...] # validate specific translation files only
|
|
49
65
|
* [--report <path>] # write JSON report; default stdout
|
|
50
66
|
* [--no-fail] # exit 0 even when violations found
|
|
67
|
+
* [--strict-skeletons] # treat skeleton-incomplete advisories as
|
|
68
|
+
* # blocking violations (default: warning)
|
|
51
69
|
* [--quiet] # suppress per-file logging
|
|
52
70
|
*/
|
|
53
71
|
|
|
@@ -199,6 +217,45 @@ export function countMermaidBlocks(text) {
|
|
|
199
217
|
return countGlobal(text, MERMAID_OPENER);
|
|
200
218
|
}
|
|
201
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Marker that the `news-translate` workflow Phase A writes at the very top
|
|
222
|
+
* of every skeleton file (before the H1 line). When the validator sees this
|
|
223
|
+
* marker it knows the file is a deliberately incomplete Phase A skeleton
|
|
224
|
+
* from an emergency partial flush and emits a single `skeleton-incomplete`
|
|
225
|
+
* advisory instead of cascading length/token/heading/mermaid violations.
|
|
226
|
+
*
|
|
227
|
+
* The marker is intentionally a forgiving regex: any HTML comment starting
|
|
228
|
+
* with `<!-- translation-skeleton` within the first 10 lines counts. This
|
|
229
|
+
* accommodates both `<!-- translation-skeleton: lang=sv phase=A -->` and
|
|
230
|
+
* the legacy `<!-- translation-skeleton -->` formats.
|
|
231
|
+
*/
|
|
232
|
+
export const SKELETON_MARKER_RE = /<!--\s*translation-skeleton\b/;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Heuristic skeleton detector. A file is a skeleton if EITHER:
|
|
236
|
+
* - it contains the explicit `SKELETON_MARKER_RE` marker in its first
|
|
237
|
+
* 10 lines (preferred, set by Phase A), OR
|
|
238
|
+
* - it matches the fallback heuristic for older Phase A output that
|
|
239
|
+
* pre-dates the marker convention: ≥3 H2 headings AND the number of
|
|
240
|
+
* `<!-- pending -->` / `<PENDING>` / `PENDING` placeholders is at
|
|
241
|
+
* least equal to the H2 count (i.e. every H2 section appears to be
|
|
242
|
+
* an unfilled stub).
|
|
243
|
+
*
|
|
244
|
+
* @param {string} text
|
|
245
|
+
* @returns {boolean}
|
|
246
|
+
*/
|
|
247
|
+
export function isSkeletonStub(text) {
|
|
248
|
+
if (typeof text !== 'string' || text.length === 0) return false;
|
|
249
|
+
const head = text.split('\n', 10).join('\n');
|
|
250
|
+
if (SKELETON_MARKER_RE.test(head)) return true;
|
|
251
|
+
// Fallback heuristic: many H2s, almost every body line is a pending marker.
|
|
252
|
+
const h2Count = countHeadings(text, 2);
|
|
253
|
+
if (h2Count < 3) return false;
|
|
254
|
+
const pendingRe = /(<!--\s*pending\s*-->|<PENDING>|\bPENDING\b)/i;
|
|
255
|
+
const pendingHits = (text.match(new RegExp(pendingRe.source, 'gi')) || []).length;
|
|
256
|
+
return pendingHits >= h2Count;
|
|
257
|
+
}
|
|
258
|
+
|
|
202
259
|
/**
|
|
203
260
|
* Extract H2 section titles from markdown text. Mirrors the shape returned
|
|
204
261
|
* by `scripts/discover-untranslated-briefs.js#extractH2Titles` so the
|
|
@@ -324,6 +381,10 @@ export function aggregateByKey(items, key) {
|
|
|
324
381
|
* @property {string} lang
|
|
325
382
|
* @property {string} gate
|
|
326
383
|
* @property {string} message
|
|
384
|
+
* @property {'error'|'warning'} [severity] - When present, controls
|
|
385
|
+
* blocking semantics: `'warning'` entries (e.g. `skeleton-incomplete`)
|
|
386
|
+
* do not cause a non-zero exit unless `--strict-skeletons` is passed.
|
|
387
|
+
* Omitted entries default to blocking (`'error'` equivalent).
|
|
327
388
|
*/
|
|
328
389
|
|
|
329
390
|
/** Parse CLI argv. Exported for unit tests. */
|
|
@@ -334,6 +395,7 @@ export function parseArgs(argv) {
|
|
|
334
395
|
report: null,
|
|
335
396
|
fail: true,
|
|
336
397
|
quiet: false,
|
|
398
|
+
strictSkeletons: false,
|
|
337
399
|
};
|
|
338
400
|
for (let i = 0; i < argv.length; i += 1) {
|
|
339
401
|
const arg = argv[i];
|
|
@@ -355,6 +417,9 @@ export function parseArgs(argv) {
|
|
|
355
417
|
case '--no-fail':
|
|
356
418
|
opts.fail = false;
|
|
357
419
|
break;
|
|
420
|
+
case '--strict-skeletons':
|
|
421
|
+
opts.strictSkeletons = true;
|
|
422
|
+
break;
|
|
358
423
|
case '--quiet':
|
|
359
424
|
opts.quiet = true;
|
|
360
425
|
break;
|
|
@@ -362,7 +427,8 @@ export function parseArgs(argv) {
|
|
|
362
427
|
case '-h':
|
|
363
428
|
process.stdout.write(
|
|
364
429
|
'Usage: validate-brief-translations.js [--repo-root <path>] ' +
|
|
365
|
-
'[--paths <file>...] [--report <path>] [--no-fail]
|
|
430
|
+
'[--paths <file>...] [--report <path>] [--no-fail] ' +
|
|
431
|
+
'[--strict-skeletons] [--quiet]\n'
|
|
366
432
|
);
|
|
367
433
|
process.exit(0);
|
|
368
434
|
break;
|
|
@@ -444,6 +510,34 @@ export function validateTranslation(translationPath, repoRoot) {
|
|
|
444
510
|
return violations;
|
|
445
511
|
}
|
|
446
512
|
|
|
513
|
+
// Skeleton short-circuit: when a translation file declares itself as a
|
|
514
|
+
// Phase A skeleton (via the `<!-- translation-skeleton -->` marker the
|
|
515
|
+
// news-translate workflow writes during emergency partial flushes), skip
|
|
516
|
+
// gates 3–7 and emit a single non-blocking `skeleton-incomplete`
|
|
517
|
+
// advisory. The marker is a deliberate contract between the workflow
|
|
518
|
+
// and this validator: an emergency partial flush is a SUCCESSFUL
|
|
519
|
+
// outcome (some real translations were saved), and the unfilled
|
|
520
|
+
// skeleton stubs for languages that did not reach Phase B should not
|
|
521
|
+
// generate 5+ cascading violations that drown out real defects in
|
|
522
|
+
// the fully translated siblings. The next scheduled run will pick up
|
|
523
|
+
// the skeleton languages via the discovery queue's missing-language
|
|
524
|
+
// detection. See `.github/workflows/news-translate.md` §"🐘
|
|
525
|
+
// LARGE-SOURCE 2-PHASE STRATEGY" and §"4b. Wall-clock safety net".
|
|
526
|
+
const targetTextEarly = fs.readFileSync(translationPath, 'utf8');
|
|
527
|
+
if (isSkeletonStub(targetTextEarly)) {
|
|
528
|
+
violations.push({
|
|
529
|
+
translationPath: rel,
|
|
530
|
+
sourcePath: sourceRel,
|
|
531
|
+
lang,
|
|
532
|
+
gate: 'skeleton-incomplete',
|
|
533
|
+
severity: 'warning',
|
|
534
|
+
message:
|
|
535
|
+
`Phase A skeleton stub — translation for "${lang}" did not reach Phase B before the wall-clock budget expired. ` +
|
|
536
|
+
`Re-queue this language on the next scheduled run; the discovery script will detect it as a missing sibling.`,
|
|
537
|
+
});
|
|
538
|
+
return violations;
|
|
539
|
+
}
|
|
540
|
+
|
|
447
541
|
const sourceBytes = fs.statSync(sourcePath).size;
|
|
448
542
|
const targetBytes = fs.statSync(translationPath).size;
|
|
449
543
|
if (sourceBytes > 0 && targetBytes < sourceBytes * LENGTH_FLOOR_RATIO) {
|
|
@@ -458,7 +552,7 @@ export function validateTranslation(translationPath, repoRoot) {
|
|
|
458
552
|
});
|
|
459
553
|
}
|
|
460
554
|
|
|
461
|
-
const targetText =
|
|
555
|
+
const targetText = targetTextEarly;
|
|
462
556
|
let englishHits = 0;
|
|
463
557
|
for (const re of EN_PATTERNS) {
|
|
464
558
|
if (re.test(targetText)) englishHits += 1;
|
|
@@ -602,8 +696,9 @@ export function runValidation(translationPaths, repoRoot, { quiet = false } = {}
|
|
|
602
696
|
allViolations.push(...v);
|
|
603
697
|
if (!quiet) {
|
|
604
698
|
for (const entry of v) {
|
|
699
|
+
const icon = entry.severity === 'warning' ? '⚠️' : '❌';
|
|
605
700
|
process.stderr.write(
|
|
606
|
-
|
|
701
|
+
`${icon} ${entry.translationPath} [${entry.gate}] ${entry.message}\n`
|
|
607
702
|
);
|
|
608
703
|
}
|
|
609
704
|
}
|
|
@@ -614,6 +709,23 @@ export function runValidation(translationPaths, repoRoot, { quiet = false } = {}
|
|
|
614
709
|
return allViolations;
|
|
615
710
|
}
|
|
616
711
|
|
|
712
|
+
/**
|
|
713
|
+
* Count the entries in a violations list that should be treated as
|
|
714
|
+
* blocking (cause a non-zero exit). When `strictSkeletons` is false
|
|
715
|
+
* (the default), entries with `severity: 'warning'` — i.e. the
|
|
716
|
+
* skeleton-incomplete advisory emitted for Phase A stubs from
|
|
717
|
+
* emergency partial flushes — are not counted as blocking.
|
|
718
|
+
*/
|
|
719
|
+
export function countBlockingViolations(violations, { strictSkeletons = false } = {}) {
|
|
720
|
+
if (strictSkeletons) {
|
|
721
|
+
// Promote skeleton-incomplete advisories to blocking, but leave any
|
|
722
|
+
// other warning-level advisory types non-blocking so that future
|
|
723
|
+
// additions of new warning gates don't inadvertently become strict.
|
|
724
|
+
return violations.filter((v) => v.severity !== 'warning' || v.gate === 'skeleton-incomplete').length;
|
|
725
|
+
}
|
|
726
|
+
return violations.filter((v) => v.severity !== 'warning').length;
|
|
727
|
+
}
|
|
728
|
+
|
|
617
729
|
/** Main entry point. */
|
|
618
730
|
export function main(argv) {
|
|
619
731
|
const opts = parseArgs(argv);
|
|
@@ -622,12 +734,14 @@ export function main(argv) {
|
|
|
622
734
|
: findAllTranslations(opts.repoRoot);
|
|
623
735
|
|
|
624
736
|
const violations = runValidation(paths, opts.repoRoot, { quiet: opts.quiet });
|
|
737
|
+
const blocking = countBlockingViolations(violations, { strictSkeletons: opts.strictSkeletons });
|
|
625
738
|
|
|
626
739
|
const report = {
|
|
627
740
|
generatedAt: new Date().toISOString(),
|
|
628
741
|
totals: {
|
|
629
742
|
filesChecked: paths.length,
|
|
630
743
|
violations: violations.length,
|
|
744
|
+
blocking,
|
|
631
745
|
byGate: aggregateByKey(violations, 'gate'),
|
|
632
746
|
byLang: aggregateByKey(violations, 'lang'),
|
|
633
747
|
},
|
|
@@ -641,7 +755,7 @@ export function main(argv) {
|
|
|
641
755
|
process.stdout.write(json);
|
|
642
756
|
}
|
|
643
757
|
|
|
644
|
-
if (
|
|
758
|
+
if (blocking > 0 && opts.fail) {
|
|
645
759
|
process.exit(1);
|
|
646
760
|
}
|
|
647
761
|
return report;
|