-
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathtemplate-structure.test.js
More file actions
168 lines (151 loc) · 5.59 KB
/
Copy pathtemplate-structure.test.js
File metadata and controls
168 lines (151 loc) · 5.59 KB
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
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
/**
* Drift-guard for `analysis/templates/*.md`.
*
* Every Stage-B template (except `README.md`, the translation-only scaffold,
* and anything under `_partials/`) MUST
* include:
* 1. The canonical ANALYSIS-TEMPLATE-FRONTMATTER:v1 block with all required
* keys (artifactId, methodology, catalogRow, depthFloorBreaking,
* mermaidType, partialsDir).
* 2. The canonical AI-INSTRUCTIONS:v1 block.
* 3. The SPDX header pair.
*
* If this test fails, run `npm run sync:templates` to regenerate.
*
* The script `scripts/templates/sync-template-frontmatter.js` is responsible
* for keeping the blocks in sync with the methodology library
* (artifact-catalog.md + reference-quality-thresholds.json).
*/
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const TEMPLATES_DIR = path.join(REPO_ROOT, 'analysis', 'templates');
const PARTIALS_DIR = path.join(TEMPLATES_DIR, '_partials');
const FRONTMATTER_TOKEN = 'ANALYSIS-TEMPLATE-FRONTMATTER:v1';
const AI_INSTRUCTIONS_TOKEN = 'AI-INSTRUCTIONS:v1';
const REQUIRED_FRONTMATTER_KEYS = [
'artifactId',
'methodology',
'catalogRow',
'depthFloorBreaking',
'mermaidType',
'partialsDir',
];
const EXCLUDED_BASENAMES = new Set([
'README.md',
'executive-brief-translation-template.md',
]);
function listTemplateFiles() {
return fs
.readdirSync(TEMPLATES_DIR)
.filter((name) => name.endsWith('.md'))
.filter((name) => !EXCLUDED_BASENAMES.has(name))
.sort();
}
function listPartialFiles() {
if (!fs.existsSync(PARTIALS_DIR)) return [];
return fs.readdirSync(PARTIALS_DIR).filter((name) => name.endsWith('.md'));
}
function extractFrontmatterBlock(content) {
const re = new RegExp(`<!--\\s*${FRONTMATTER_TOKEN}([\\s\\S]*?)-->`);
const match = content.match(re);
return match ? match[1] : null;
}
describe('analysis/templates structure (drift-guard)', () => {
const templates = listTemplateFiles();
it('finds at least 39 templates (catalog floor)', () => {
expect(templates.length).toBeGreaterThanOrEqual(39);
});
it('every template has the SPDX header pair', () => {
const missing = [];
// REUSE-IgnoreStart
const spdxCopyright = 'SPDX-FileCopyrightText:';
const spdxLicense = 'SPDX-License-Identifier:';
// REUSE-IgnoreEnd
for (const basename of templates) {
const content = fs.readFileSync(path.join(TEMPLATES_DIR, basename), 'utf8');
const reCopyright = new RegExp(`<!--\\s*${spdxCopyright}`, 'i');
const reLicense = new RegExp(`<!--\\s*${spdxLicense}`, 'i');
if (!reCopyright.test(content) || !reLicense.test(content)) {
missing.push(basename);
}
}
expect(missing, `Templates missing SPDX headers: ${missing.join(', ')}`).toEqual([]);
});
it(`every template has the canonical ${FRONTMATTER_TOKEN} block`, () => {
const missing = [];
for (const basename of templates) {
const content = fs.readFileSync(path.join(TEMPLATES_DIR, basename), 'utf8');
if (!content.includes(FRONTMATTER_TOKEN)) {
missing.push(basename);
}
}
expect(
missing,
`Templates missing front-matter (run \`npm run sync:templates\`): ${missing.join(', ')}`,
).toEqual([]);
});
it(`every template has the canonical ${AI_INSTRUCTIONS_TOKEN} block`, () => {
const missing = [];
for (const basename of templates) {
const content = fs.readFileSync(path.join(TEMPLATES_DIR, basename), 'utf8');
if (!content.includes(AI_INSTRUCTIONS_TOKEN)) {
missing.push(basename);
}
}
expect(
missing,
`Templates missing AI-instructions block (run \`npm run sync:templates\`): ${missing.join(', ')}`,
).toEqual([]);
});
it('every front-matter block contains the required keys', () => {
const offenders = [];
for (const basename of templates) {
const content = fs.readFileSync(path.join(TEMPLATES_DIR, basename), 'utf8');
const block = extractFrontmatterBlock(content);
if (!block) continue; // covered by the previous test
for (const key of REQUIRED_FRONTMATTER_KEYS) {
const re = new RegExp(`(^|\\n)\\s*${key}\\s*:`);
if (!re.test(block)) {
offenders.push(`${basename} (missing ${key})`);
}
}
}
expect(
offenders,
`Front-matter blocks missing keys: ${offenders.join(', ')}`,
).toEqual([]);
});
it('every front-matter artifactId equals the file basename', () => {
const offenders = [];
for (const basename of templates) {
const content = fs.readFileSync(path.join(TEMPLATES_DIR, basename), 'utf8');
const block = extractFrontmatterBlock(content);
if (!block) continue;
const idMatch = block.match(/(^|\n)\s*artifactId\s*:\s*([a-z0-9-]+)/);
const expected = basename.replace(/\.md$/, '');
if (!idMatch || idMatch[2] !== expected) {
offenders.push(`${basename} (got ${idMatch ? idMatch[2] : '<missing>'})`);
}
}
expect(offenders, `artifactId mismatch: ${offenders.join(', ')}`).toEqual([]);
});
it('the _partials directory exists and contains the canonical partials', () => {
const partials = listPartialFiles();
const expected = [
'README.md',
'ai-instructions.md',
'quality-checklist.md',
'citation-pattern.md',
'evidence-table.md',
'imf-callout.md',
];
for (const p of expected) {
expect(partials, `Missing partial: ${p}`).toContain(p);
}
});
});