Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | 8x 1x 24x 3x 5x 4x 3x 5x 3x 3x 7x 2x 5x 5x 5x 5x 10x 2x 2x 8x 2x 2x 6x 1x 4x 3x 3x 3x 3x 7x 7x 7x 1x 6x 5x 1x 4x 4x 1x 3x 3x 1x | // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
/**
* @module Workflows/CompletenessGate/Validators
* @description Core validation functions for Stage C analysis completeness.
* These pure functions implement individual artifact checks extracted from
* the monolithic validate-analysis-completeness.js script.
*
* Each function is a single-responsibility checker that returns a boolean
* or enriches a result object — making them independently testable.
*/
import {
PLACEHOLDER_PATTERNS,
META_DOC_HINT_RE,
WEP_BAND_RE,
ADMIRALTY_RE,
BLUF_RE,
READER_BLOCK_RE,
SAT_LIST_RE,
MCP_TOOL_RE,
IMF_FIGURE_CLAIM_RE,
WB_ECONOMIC_INDICATOR_CODE_RE,
WB_ECONOMIC_CLAIM_RE,
DIAGRAM_DIRS,
DEFAULT_MIN_LINES,
} from './constants.js';
import type { ArtifactValidationResult } from '../types.js';
import type { ValidationRules } from './types.js';
import type { DataMode } from '../types.js';
import { DATA_MODE_REDUCTION } from '../types.js';
// ─── Content Checks ──────────────────────────────────────────────────────────
/**
* Check if content contains placeholder markers that indicate unfinished analysis.
*
* @param content - The full text content of an artifact
* @returns true if any placeholder pattern is found
*/
export function hasPlaceholders(content: string): boolean {
if (META_DOC_HINT_RE.test(content)) {
return false; // meta-doc contexts are exempt
}
return PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(content));
}
/**
* Check if content contains at least one mermaid fenced code block.
*
* @param content - The full text content of an artifact
* @returns true if a ```mermaid block is present
*/
export function hasMermaid(content: string): boolean {
return /```mermaid/i.test(content);
}
/**
* Check if content contains a WEP (Words of Estimative Probability) band marker.
*
* @param content - The full text content of an artifact
* @returns true if a WEP band marker is present
*/
export function hasWepBand(content: string): boolean {
return WEP_BAND_RE.test(content);
}
/**
* Check if content contains an Admiralty grading marker (A1–F6).
*
* @param content - The full text content of an artifact
* @returns true if an Admiralty grade is present
*/
export function hasAdmiraltyGrade(content: string): boolean {
return ADMIRALTY_RE.test(content);
}
/**
* Check if content contains an ICD 203 BLUF (Bottom Line Up Front) marker.
*
* @param content - The full text content of an artifact
* @returns true if a BLUF marker is present
*/
export function hasBluf(content: string): boolean {
return BLUF_RE.test(content);
}
/**
* Check if content contains a reader-perspective block heading.
*
* @param content - The full text content of an artifact
* @returns true if a reader block heading is present
*/
export function hasReaderBlock(content: string): boolean {
return READER_BLOCK_RE.test(content);
}
/**
* Count the number of SAT (Structured Analytic Technique) documentation bullets.
*
* @param content - The full text content of an artifact
* @returns the count of SAT bullet items found
*/
export function countSatBullets(content: string): number {
const matches = content.match(SAT_LIST_RE);
return matches ? matches.length : 0;
}
/**
* Check if content contains evidence of source diversity via MCP tool references
* or a structured evidence/source table with header, separator, and at least one
* data row (stricter check — a header-only table with no data rows is not sufficient).
*
* @param content - The full text content of an artifact
* @returns true if source diversity evidence is present
*/
export function hasSourceDiversityEvidence(content: string): boolean {
// Check for MCP tool references
if (MCP_TOOL_RE.test(content)) {
return true;
}
// Check for a structured evidence table: requires header row with Source/Evidence/Reference,
// a separator row (---|---), and at least one data row after the separator. This prevents
// header-only tables and plain prose markdown tables from being counted as source diversity.
const lines = content.split('\n');
let foundHeader = false;
let foundSeparator = false;
for (const line of lines) {
if (!foundHeader && /^\|[^|]*(?:Source|Evidence|Reference)[^|]*\|/i.test(line)) {
foundHeader = true;
continue;
}
if (foundHeader && !foundSeparator && /^\|[-: |]+\|/.test(line)) {
foundSeparator = true;
continue;
}
if (foundHeader && foundSeparator && /^\|[^|]+\|/.test(line)) {
// At least one non-empty data row after the separator
return true;
}
}
return false;
}
/**
* Check if an artifact contains an IMF numeric figure claim.
*
* @param content - The full text content of an artifact
* @returns true if an IMF figure claim pattern is found
*/
export function hasImfFigureClaim(content: string): boolean {
return IMF_FIGURE_CLAIM_RE.test(content);
}
/**
* Check if an economic-context artifact improperly uses World Bank economic codes.
*
* @param content - The full text content of an economic-context artifact
* @returns the matched WB indicator code, or null if clean
*/
export function findWbEconomicIndicator(content: string): string | null {
const match = content.match(WB_ECONOMIC_INDICATOR_CODE_RE);
return match ? (match[1] ?? null) : null;
}
/**
* Check if an economic-context artifact improperly makes World Bank economic claims.
*
* @param content - The full text content of an economic-context artifact
* @returns true if a WB economic prose claim is found
*/
export function hasWbEconomicClaim(content: string): boolean {
return WB_ECONOMIC_CLAIM_RE.test(content);
}
// ─── Threshold Computation ───────────────────────────────────────────────────
/**
* Compute the effective minimum line count for an artifact, accounting for
* per-artifact overrides and data-mode reduction factors.
*
* @param relativePath - The artifact's relative path
* @param rules - The loaded validation rules
* @param dataModeReduction - The effective reduction factor (0–1)
* @param explicitMinLines - CLI-provided floor (raises but never lowers)
* @returns the computed minimum line count
*/
export function computeEffectiveMinLines(
relativePath: string,
rules: ValidationRules,
dataModeReduction: number,
explicitMinLines?: number
): number {
const baseFloor = rules.minLines?.[relativePath] ?? rules.defaultMinLines ?? DEFAULT_MIN_LINES;
// Use Math.floor to match scripts/validate-analysis-completeness.js behavior.
// Clamp to at least 1 so every artifact has a meaningful floor even in minimal mode.
const reduced = Math.max(1, Math.floor(baseFloor * dataModeReduction));
// CLI --min-lines only raises, never lowers
if (explicitMinLines !== undefined && explicitMinLines > reduced) {
return explicitMinLines;
}
return reduced;
}
/**
* Resolve the effective data-mode reduction factor from a manifest's dataMode field.
*
* @param dataMode - The declared data mode (or undefined for full)
* @returns the reduction factor (0–1)
*/
export function resolveDataModeReduction(dataMode?: DataMode): number {
if (!dataMode) {
return 1.0;
}
return DATA_MODE_REDUCTION[dataMode] ?? 1.0;
}
// ─── Artifact Path Classification ────────────────────────────────────────────
/**
* Determine whether an artifact path implicitly requires a Mermaid diagram
* based on its directory location (intelligence/, classification/, etc).
*
* @param relativePath - The artifact's relative path
* @param rules - The loaded validation rules
* @returns true if the artifact requires a Mermaid diagram
*/
export function requiresMermaid(relativePath: string, rules: ValidationRules): boolean {
// Explicitly listed in mermaidRequired
if (rules.mermaidRequired?.includes(relativePath)) {
return true;
}
// Implicitly required by directory
const dir = relativePath.split('/')[0];
return dir !== undefined && DIAGRAM_DIRS.includes(dir);
}
// ─── Result Builders ─────────────────────────────────────────────────────────
/**
* Create an empty artifact validation result for a given path.
*
* @param relativePath - The artifact's relative path
* @returns a fresh ArtifactValidationResult with empty issues/warnings
*/
export function createEmptyResult(relativePath: string): ArtifactValidationResult {
return {
relativePath,
lines: 0,
issues: [],
warnings: [],
};
}
|