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 | 6x 6x 6x 6x 13x 13x 10x 10x 10x 10x 10x 32x 32x 5x 5x 27x 3x 3x 10x 5x 10x 10x 3x 6x 4x 4x 2x 2x 1x 1x | /**
* @module scripts/validators/article/rules/mermaid-fences
* @description Mermaid fence integrity helpers — detect unclosed
* ```mermaid fences and count opening fences for coverage
* checks.
*
* Rule census: extracted from
* `scripts/validate-article.ts` lines 250–313
* (`UnclosedMermaidFence`, `findUnclosedMermaidFences`,
* `countMermaidOpenings`). Logic is byte-identical to the
* original.
*
* @author Hack23 AB
* @license Apache-2.0
*/
/**
* One unclosed `\`\`\`mermaid` fence found in the article body.
*
* `lineNumber` is 1-indexed and points at the opening `\`\`\`mermaid`
* line so the editor can jump straight to the regression.
*/
export interface UnclosedMermaidFence {
readonly lineNumber: number;
/** Line number where the fence implicitly ended (next opening fence
* or end-of-input). Useful for diagnostics. */
readonly impliedEndLineNumber: number;
}
/**
* Detect every `\`\`\`mermaid` opening fence whose matching closing
* `\`\`\`` is missing in the body. The aggregator concatenates raw
* artifact bodies and AI agents occasionally drop the closing fence —
* the renderer ({@link preprocessMermaidFences}) recovers gracefully
* by treating the next fence opening as an implicit close, but the
* source artifact still needs to be fixed so downstream tools (the
* `gh aw mcp inspect`-style audits, IDE preview, source review) stop
* silently mis-rendering.
*
* Pure string analysis — never mutates the input.
*
* @param body Aggregated `article.md` contents.
* @returns Empty array when every `\`\`\`mermaid` has a matching close.
*/
export function findUnclosedMermaidFences(body: string): readonly UnclosedMermaidFence[] {
const lines = body.split('\n');
const out: UnclosedMermaidFence[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i]!;
if (/^```mermaid[\t ]*$/.test(line)) {
const openLine = i + 1;
let j = i + 1;
let closed = false;
let stoppedAtNextOpening = false;
for (; j < lines.length; j += 1) {
const cur = lines[j]!;
if (/^```[\t ]*$/.test(cur)) {
closed = true;
break;
}
if (/^```/.test(cur)) {
stoppedAtNextOpening = true;
break;
}
}
if (!closed) {
out.push({ lineNumber: openLine, impliedEndLineNumber: j + 1 });
}
i = stoppedAtNextOpening ? j : j + 1;
continue;
}
i += 1;
}
return out;
}
/**
* Count `\`\`\`mermaid` opening fences in a body. Used by the
* mermaid-coverage cross-check between `article.md` and the source
* artifacts under the same `analysis/daily/$DATE/$SUBFOLDER` folder.
*/
export function countMermaidOpenings(body: string): number {
const matches = body.match(/^```mermaid[\t ]*$/gm);
return matches ? matches.length : 0;
}
import type { ArticleViolation } from '../types.js';
/** Unclosed-mermaid-fence rule (the cross-folder coverage check is in the orchestrator). */
export function checkUnclosedMermaidFences(rel: string, text: string): ArticleViolation[] {
const unclosed = findUnclosedMermaidFences(text);
if (unclosed.length === 0) return [];
const sample = unclosed.slice(0, 3).map((f) => `line ${f.lineNumber}`).join(', ');
return [
{
file: rel,
code: 'unclosed-mermaid-fence',
message: `${unclosed.length} unclosed \`\`\`mermaid fence(s) detected (${sample}${unclosed.length > 3 ? ', …' : ''}). Add a closing \`\`\` line. The renderer recovers gracefully but the source artifact must be fixed so the analysis-gate Check 5 and IDE preview render correctly.`,
},
];
}
|