euparliamentmonitor 0.8.36 → 0.8.37

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "euparliamentmonitor",
3
- "version": "0.8.36",
3
+ "version": "0.8.37",
4
4
  "type": "module",
5
5
  "description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
6
6
  "main": "scripts/index.js",
@@ -31,6 +31,85 @@ import { PROJECT_ROOT } from '../constants/config.js';
31
31
  // ─── Types ────────────────────────────────────────────────────────────────────
32
32
  /** Minimum line count below which an artifact is considered a stub */
33
33
  const DEFAULT_MIN_LINES = 30;
34
+ /**
35
+ * Load the Rule 22 per-artifact threshold catalogue for a given article type.
36
+ *
37
+ * Returns a `Map<relativePath, minLines>` containing every per-file floor
38
+ * defined under `thresholds.<articleType>` in
39
+ * `analysis/methodologies/reference-quality-thresholds.json`. When the
40
+ * catalogue file is missing, unreadable, malformed, or lacks an entry for the
41
+ * article type, an empty map is returned and the caller's flat
42
+ * `DEFAULT_MIN_LINES` floor applies to every artifact.
43
+ *
44
+ * This load is deliberately tolerant: a missing catalogue must not break
45
+ * existing article types whose depth is still enforced only by the flat floor.
46
+ *
47
+ * @param articleType - Article category slug (e.g. `breaking`).
48
+ * @param overrideFile - Optional absolute path to a thresholds JSON file,
49
+ * overriding the default `THRESHOLDS_FILE`. Intended for
50
+ * tests that need fixture thresholds without touching the
51
+ * repo-wide catalogue.
52
+ * @returns Map from `relativePath` → per-file `minLines` threshold.
53
+ */
54
+ function loadPerArtifactThresholds(articleType, overrideFile) {
55
+ // `overrideFile` may be absolute or relative. Relative paths are resolved
56
+ // against `PROJECT_ROOT` (matching how `--analysis-dir` is resolved) so that
57
+ // callers invoking the CLI from any working directory get consistent
58
+ // behaviour; otherwise the file is silently treated as missing and Rule 22
59
+ // floors fall back to the flat `--min-lines` value.
60
+ let file;
61
+ if (overrideFile === undefined) {
62
+ file = THRESHOLDS_FILE;
63
+ }
64
+ else if (path.isAbsolute(overrideFile)) {
65
+ file = overrideFile;
66
+ }
67
+ else {
68
+ file = path.join(PROJECT_ROOT, overrideFile);
69
+ }
70
+ if (!fs.existsSync(file))
71
+ return new Map();
72
+ let parsed;
73
+ try {
74
+ parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
75
+ }
76
+ catch {
77
+ return new Map();
78
+ }
79
+ const entry = parsed.thresholds?.[articleType];
80
+ if (!entry || typeof entry !== 'object')
81
+ return new Map();
82
+ const result = new Map();
83
+ for (const [rel, n] of Object.entries(entry)) {
84
+ if (typeof n === 'number' && Number.isFinite(n) && n > 0) {
85
+ result.set(rel, n);
86
+ }
87
+ }
88
+ return result;
89
+ }
90
+ /**
91
+ * Resolve the effective `minLines` floor for a specific artifact.
92
+ *
93
+ * When a Rule 22 per-artifact threshold is defined for the active
94
+ * `articleType`, the effective floor is `max(perArtifactFloor, flatFallback)`
95
+ * so `--min-lines` can raise (but never silently lower) a per-artifact floor.
96
+ * This keeps behaviour consistent between required-set artifacts and
97
+ * supplemental (manifest-listed) artifacts — both paths apply the same rule.
98
+ *
99
+ * @param relPath - Artifact path relative to the run directory.
100
+ * @param perArtifact - Per-artifact threshold map for the active article type.
101
+ * @param fallback - Flat floor supplied by the CLI (`--min-lines`, default
102
+ * `DEFAULT_MIN_LINES`). Used directly when no per-artifact
103
+ * entry exists; otherwise combined via `max` with the
104
+ * per-artifact entry.
105
+ * @returns Effective `minLines` threshold.
106
+ */
107
+ function effectiveMinLines(relPath, perArtifact, fallback) {
108
+ const configured = perArtifact.get(relPath);
109
+ if (configured === undefined)
110
+ return fallback;
111
+ return Math.max(configured, fallback);
112
+ }
34
113
  /** Placeholder markers that indicate an incomplete analysis artifact */
35
114
  const PLACEHOLDER_MARKERS = [
36
115
  '[AI_ANALYSIS_REQUIRED]',
@@ -39,6 +118,12 @@ const PLACEHOLDER_MARKERS = [
39
118
  '[TBD]',
40
119
  'TODO:',
41
120
  ];
121
+ /**
122
+ * Location of the Rule 22 per-artifact depth-floor catalogue.
123
+ * When present, per-artifact thresholds defined here override the flat
124
+ * `DEFAULT_MIN_LINES` floor for matching `articleType × relativePath` tuples.
125
+ */
126
+ const THRESHOLDS_FILE = path.join(PROJECT_ROOT, 'analysis', 'methodologies', 'reference-quality-thresholds.json');
42
127
  /**
43
128
  * The seven reference-quality intelligence artifacts per
44
129
  * `analysis/methodologies/ai-driven-analysis-guide.md` §Reference-Quality Depth
@@ -101,6 +186,10 @@ function applyArg(arg, opts) {
101
186
  opts.minLines = parsed;
102
187
  return true;
103
188
  }
189
+ if (arg.startsWith('--thresholds-file=')) {
190
+ opts.thresholdsFile = arg.slice('--thresholds-file='.length);
191
+ return true;
192
+ }
104
193
  if (arg === '--json') {
105
194
  opts.json = true;
106
195
  return true;
@@ -156,6 +245,11 @@ Options:
156
245
  --article-type=<slug> Article category slug (breaking, week-in-review, …).
157
246
  When omitted, inferred from manifest.json.
158
247
  --min-lines=<n> Minimum line count per artifact (default 30).
248
+ Used as fallback when no Rule 22 per-artifact
249
+ threshold is defined for this article type × path.
250
+ --thresholds-file=<path> Override the Rule 22 thresholds catalogue (default:
251
+ analysis/methodologies/reference-quality-thresholds.json).
252
+ Primarily for tests.
159
253
  --json Emit a JSON report on stdout instead of text.
160
254
  --warn-only Exit 0 on validation failure (report only). Use for
161
255
  local exploration; workflows MUST NOT pass this flag.
@@ -239,15 +333,18 @@ function loadManifest(runDir) {
239
333
  * @param runDir - Absolute path to the analysis run directory.
240
334
  * @param relPath - Path relative to `runDir` of the artifact to inspect.
241
335
  * @param listedInManifest - Whether the artifact appears under `manifest.files.*`.
336
+ * @param minLines - Effective `minLines` floor (Rule 22 per-artifact threshold
337
+ * when defined for this path, or the flat fallback otherwise).
242
338
  * @returns Presence, line count, placeholder findings, and manifest-listing flag.
243
339
  */
244
- function inspectArtifact(runDir, relPath, listedInManifest) {
340
+ function inspectArtifact(runDir, relPath, listedInManifest, minLines) {
245
341
  const abs = path.join(runDir, relPath);
246
342
  if (!fs.existsSync(abs)) {
247
343
  return {
248
344
  relativePath: relPath,
249
345
  present: false,
250
346
  lineCount: 0,
347
+ minLines,
251
348
  placeholdersFound: [],
252
349
  listedInManifest,
253
350
  };
@@ -264,6 +361,7 @@ function inspectArtifact(runDir, relPath, listedInManifest) {
264
361
  relativePath: relPath,
265
362
  present: true,
266
363
  lineCount,
364
+ minLines,
267
365
  placeholdersFound: placeholders,
268
366
  listedInManifest,
269
367
  };
@@ -355,17 +453,19 @@ function computeRequired(articleType) {
355
453
  /**
356
454
  * Count how many artifact checks failed, combined with any manifest errors.
357
455
  *
456
+ * Each check carries its own `minLines` threshold (Rule 22 per-artifact floor
457
+ * or the flat fallback), so no single `minLines` argument is needed here.
458
+ *
358
459
  * @param checks - Per-artifact inspection results.
359
- * @param minLines - Minimum required line count.
360
460
  * @param manifestErrorCount - Number of manifest-level errors.
361
461
  * @returns Total error count used for the pass/fail decision.
362
462
  */
363
- function countErrors(checks, minLines, manifestErrorCount) {
463
+ function countErrors(checks, manifestErrorCount) {
364
464
  let errorCount = manifestErrorCount;
365
465
  for (const c of checks) {
366
466
  if (!c.present)
367
467
  errorCount++;
368
- else if (c.lineCount < minLines)
468
+ else if (c.lineCount < c.minLines)
369
469
  errorCount++;
370
470
  else if (c.placeholdersFound.length > 0)
371
471
  errorCount++;
@@ -407,14 +507,33 @@ function validate(options) {
407
507
  const articleType = options.articleType ?? manifest.raw.articleType ?? 'unknown';
408
508
  const required = computeRequired(articleType);
409
509
  const listedSet = new Set(manifest.allListedPaths);
410
- const checks = required.map((rel) => inspectArtifact(absRunDir, rel, listedSet.has(rel)));
411
- const onDiskIntel = walkIntelligenceDir(absRunDir);
412
- // O(1)-per-path lookup: convert `required` into a Set for the orphan filter.
510
+ const perArtifactThresholds = loadPerArtifactThresholds(articleType, options.thresholdsFile);
511
+ const checks = required.map((rel) => inspectArtifact(absRunDir, rel, listedSet.has(rel), effectiveMinLines(rel, perArtifactThresholds, options.minLines)));
512
+ // Rule 22 supplemental enforcement: any manifest-listed file that has a
513
+ // per-artifact threshold entry but is NOT in the mandatory `required` set
514
+ // (e.g. `risk-scoring/*`, `documents/*`, `classification/*`) also has its
515
+ // depth floor enforced. This keeps `reference-quality-thresholds.json` and
516
+ // `.github/prompts/SHARED_PROMPT_PATTERNS.md §Per-Artifact Budgets` truthful
517
+ // about which files are machine-enforced.
413
518
  const requiredSet = new Set(required);
414
- const orphaned = onDiskIntel.filter((rel) => !listedSet.has(rel) && !requiredSet.has(rel));
519
+ const supplementalChecks = [];
520
+ for (const rel of perArtifactThresholds.keys()) {
521
+ if (requiredSet.has(rel))
522
+ continue;
523
+ if (!listedSet.has(rel))
524
+ continue;
525
+ supplementalChecks.push(inspectArtifact(absRunDir, rel, true, effectiveMinLines(rel, perArtifactThresholds, options.minLines)));
526
+ }
527
+ supplementalChecks.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
528
+ checks.push(...supplementalChecks);
529
+ const onDiskIntel = walkIntelligenceDir(absRunDir);
530
+ // O(1)-per-path lookup: build a lookup set that includes both required and
531
+ // supplemental-threshold artifacts so the orphan filter doesn't flag them.
532
+ const inspectedSet = new Set(checks.map((c) => c.relativePath));
533
+ const orphaned = onDiskIntel.filter((rel) => !listedSet.has(rel) && !inspectedSet.has(rel));
415
534
  // Orphaned files are warnings, not errors (per Rule 6 "contamination risk"
416
535
  // they're a signal but not a blocker — a second workflow may legitimately add files)
417
- const errorCount = countErrors(checks, options.minLines, manifest.errors.length);
536
+ const errorCount = countErrors(checks, manifest.errors.length);
418
537
  return {
419
538
  analysisDir: absRunDir,
420
539
  articleType,
@@ -431,16 +550,17 @@ function validate(options) {
431
550
  /**
432
551
  * Build a list of issue labels for a single artifact check.
433
552
  *
553
+ * Uses the per-check `minLines` (Rule 22 per-artifact floor or flat fallback).
554
+ *
434
555
  * @param c - The artifact check result.
435
- * @param minLines - Minimum required line count.
436
556
  * @returns Array of short issue labels; empty if the artifact passes.
437
557
  */
438
- function artifactIssues(c, minLines) {
558
+ function artifactIssues(c) {
439
559
  if (!c.present)
440
560
  return ['MISSING'];
441
561
  const parts = [];
442
- if (c.lineCount < minLines)
443
- parts.push(`SHORT (${c.lineCount} < ${minLines} lines)`);
562
+ if (c.lineCount < c.minLines)
563
+ parts.push(`SHORT (${c.lineCount} < ${c.minLines} lines)`);
444
564
  if (c.placeholdersFound.length > 0) {
445
565
  parts.push(`PLACEHOLDERS (${c.placeholdersFound.join(', ')})`);
446
566
  }
@@ -452,16 +572,16 @@ function artifactIssues(c, minLines) {
452
572
  * Print the header block of a text-mode report.
453
573
  *
454
574
  * @param result - Validation result.
455
- * @param minLines - Minimum required line count.
575
+ * @param minLines - Flat fallback line floor (displayed as the default).
456
576
  */
457
577
  function printHeader(result, minLines) {
458
578
  console.log('━'.repeat(72));
459
- console.log('🔍 Analysis Completeness Validator (Rule 19 pre-flight gate)');
579
+ console.log('🔍 Analysis Completeness Validator (Rule 19 + Rule 22 pre-flight gate)');
460
580
  console.log('━'.repeat(72));
461
581
  console.log(`📁 Run dir : ${path.relative(PROJECT_ROOT, result.analysisDir)}`);
462
582
  console.log(`🏷️ Article type : ${result.articleType}`);
463
583
  console.log(`📋 Required count : ${result.required.length}`);
464
- console.log(`🧾 Min lines/file : ${minLines}`);
584
+ console.log(`🧾 Min lines/file : ${minLines} (default) — per-artifact floors from Rule 22 thresholds`);
465
585
  console.log('');
466
586
  }
467
587
  /**
@@ -477,7 +597,7 @@ function printFooter(result) {
477
597
  else {
478
598
  console.log(`❌ Pre-flight gate FAILED — ${result.errorCount} error(s). ` +
479
599
  'Article generation MUST NOT proceed.');
480
- console.log(' See analysis/methodologies/ai-driven-analysis-guide.md §Rule 19 and');
600
+ console.log(' See analysis/methodologies/ai-driven-analysis-guide.md §Rule 19 / Rule 22 and');
481
601
  console.log(' .github/prompts/SHARED_PROMPT_PATTERNS.md §Article Generation Pre-Flight.');
482
602
  }
483
603
  console.log('━'.repeat(72));
@@ -486,7 +606,7 @@ function printFooter(result) {
486
606
  * Render the full text-mode report to stdout.
487
607
  *
488
608
  * @param result - Validation result.
489
- * @param minLines - Minimum required line count threshold.
609
+ * @param minLines - Flat fallback line floor (per-artifact floors live on each check).
490
610
  */
491
611
  function renderTextReport(result, minLines) {
492
612
  printHeader(result, minLines);
@@ -498,9 +618,9 @@ function renderTextReport(result, minLines) {
498
618
  }
499
619
  console.log('📊 Artifact checks:');
500
620
  for (const c of result.checks) {
501
- const issues = artifactIssues(c, minLines);
621
+ const issues = artifactIssues(c);
502
622
  const status = issues.length === 0 ? '✅ ok' : `❌ ${issues.join('; ')}`;
503
- const lineInfo = c.present ? ` (${c.lineCount} lines)` : '';
623
+ const lineInfo = c.present ? ` (${c.lineCount}/${c.minLines} lines)` : '';
504
624
  console.log(` ${status.padEnd(60)} ${c.relativePath}${lineInfo}`);
505
625
  }
506
626
  if (result.orphanedOnDisk.length > 0) {