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
|
@@ -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,
|
|
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
|
|
411
|
-
const
|
|
412
|
-
//
|
|
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
|
|
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,
|
|
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
|
|
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 -
|
|
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 -
|
|
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
|
|
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) {
|