spec-up-t 1.1.53 → 1.1.55
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/assets/js/create-alphabet-index.js +8 -1
- package/branches.md +6 -2
- package/index.js +9 -1
- package/package.json +2 -1
- package/src/health-check/external-specs-checker.js +207 -0
- package/src/health-check/output-gitignore-checker.js +261 -0
- package/src/health-check/specs-configuration-checker.js +274 -0
- package/src/health-check/term-references-checker.js +190 -0
- package/src/health-check/terms-intro-checker.js +81 -0
- package/src/health-check/tref-term-checker.js +462 -0
- package/src/health-check.js +445 -0
- package/src/install-from-boilerplate/config-scripts-keys.js +2 -0
- package/src/install-from-boilerplate/menu.sh +6 -3
- package/src/markdown-it-extensions.js +105 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { shouldProcessFile } = require('../utils/file-filter');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extracts the spec name and term from a tref tag at the beginning of a markdown file
|
|
7
|
+
* @param {string} firstLine - The first line of a markdown file
|
|
8
|
+
* @returns {Object|null} - Object containing repo and term, or null if not found
|
|
9
|
+
*/
|
|
10
|
+
function extractTrefInfo(firstLine) {
|
|
11
|
+
if (!firstLine.includes('[[tref:')) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Extract content between [[tref: and ]]
|
|
17
|
+
const trefMatch = firstLine.match(/\[\[tref:([^\]]+)\]\]/);
|
|
18
|
+
if (!trefMatch || !trefMatch[1]) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const trefContent = trefMatch[1].trim();
|
|
23
|
+
|
|
24
|
+
// Split by the first comma
|
|
25
|
+
const parts = trefContent.split(',');
|
|
26
|
+
if (parts.length < 2) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Extract repo and term
|
|
31
|
+
const repo = parts[0].trim();
|
|
32
|
+
const term = parts.slice(1).join(',').trim();
|
|
33
|
+
|
|
34
|
+
return { repo, term };
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Error extracting tref info:', error);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Find all JSON cache files for repositories
|
|
43
|
+
* @param {string} cacheDir - Directory containing the cached files
|
|
44
|
+
* @returns {Array} - List of all JSON cache files
|
|
45
|
+
*/
|
|
46
|
+
function findAllCacheFiles(cacheDir) {
|
|
47
|
+
if (!fs.existsSync(cacheDir)) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const files = fs.readdirSync(cacheDir)
|
|
53
|
+
.filter(file => file.endsWith('.json'))
|
|
54
|
+
.map(file => path.join(cacheDir, file));
|
|
55
|
+
|
|
56
|
+
return files;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(`Error finding cache files:`, error);
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Checks if a term exists in a repository JSON cache file
|
|
65
|
+
* @param {string} filePath - Path to the cached repository file
|
|
66
|
+
* @param {string} term - Term to search for
|
|
67
|
+
* @returns {boolean} - Whether the term exists in the file
|
|
68
|
+
*/
|
|
69
|
+
function termExistsInRepo(filePath, term) {
|
|
70
|
+
if (!fs.existsSync(filePath)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const cacheData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
76
|
+
|
|
77
|
+
// Check if the file has a terms array
|
|
78
|
+
if (!cacheData || !cacheData.terms || !Array.isArray(cacheData.terms)) {
|
|
79
|
+
console.log(`Warning: Cache file ${filePath} has no terms array`);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Case-insensitive search for the term
|
|
84
|
+
const termLower = term.toLowerCase();
|
|
85
|
+
|
|
86
|
+
// Check each term in the terms array
|
|
87
|
+
for (const termObj of cacheData.terms) {
|
|
88
|
+
// First check the 'term' property if it exists
|
|
89
|
+
if (termObj.term && termObj.term.toLowerCase() === termLower) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If there's a definition property, check if the term appears in it
|
|
94
|
+
if (termObj.definition) {
|
|
95
|
+
// Look for patterns like [[def: term]] or similar
|
|
96
|
+
const defMatch = termObj.definition.match(/\[\[def:\s*([^\],]+)/i);
|
|
97
|
+
if (defMatch && defMatch[1] && defMatch[1].trim().toLowerCase() === termLower) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return false;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error(`Error checking if term exists in file ${filePath}:`, error);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Find which repo a cache file belongs to
|
|
112
|
+
* @param {string} filePath - Path to the cache file
|
|
113
|
+
* @param {Array} externalSpecs - List of external specs
|
|
114
|
+
* @returns {string|null} - The external_spec identifier or null
|
|
115
|
+
*/
|
|
116
|
+
function findRepoForCacheFile(filePath, externalSpecs) {
|
|
117
|
+
try {
|
|
118
|
+
const cacheData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
119
|
+
|
|
120
|
+
if (!cacheData || !cacheData.repository) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Extract owner/repo from the repository field
|
|
125
|
+
const repoPath = cacheData.repository;
|
|
126
|
+
|
|
127
|
+
// Find matching external spec
|
|
128
|
+
for (const spec of externalSpecs) {
|
|
129
|
+
if (!spec.url) continue;
|
|
130
|
+
|
|
131
|
+
// Extract owner/repo from the URL
|
|
132
|
+
const match = spec.url.match(/github\.com\/([^\/]+\/[^\/]+)/i);
|
|
133
|
+
if (match && match[1] && repoPath.includes(match[1])) {
|
|
134
|
+
return spec.external_spec;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return null;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Checks for specified term in all available external repos
|
|
146
|
+
* @param {string} cacheDir - Directory containing the cached repository files
|
|
147
|
+
* @param {Array} externalSpecs - List of external specs from specs.json
|
|
148
|
+
* @param {string} currentRepo - The repository being checked (to exclude it)
|
|
149
|
+
* @param {string} term - Term to search for
|
|
150
|
+
* @returns {Array} - List of repositories where the term was found
|
|
151
|
+
*/
|
|
152
|
+
function findTermInOtherRepos(cacheDir, externalSpecs, currentRepo, term) {
|
|
153
|
+
const reposWithTerm = [];
|
|
154
|
+
|
|
155
|
+
// Get all cache files
|
|
156
|
+
const cacheFiles = findAllCacheFiles(cacheDir);
|
|
157
|
+
|
|
158
|
+
for (const cacheFile of cacheFiles) {
|
|
159
|
+
// Check which repo this cache file belongs to
|
|
160
|
+
const repo = findRepoForCacheFile(cacheFile, externalSpecs);
|
|
161
|
+
|
|
162
|
+
// Skip if we couldn't determine which repo this is or if it's the current repo
|
|
163
|
+
if (!repo || repo === currentRepo) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check if the term exists in this repo
|
|
168
|
+
if (termExistsInRepo(cacheFile, term)) {
|
|
169
|
+
reposWithTerm.push(repo);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return reposWithTerm;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Find cache file for a specific external repo
|
|
178
|
+
* @param {string} cacheDir - Cache directory
|
|
179
|
+
* @param {Object} specConfig - External spec configuration
|
|
180
|
+
* @returns {string|null} - Path to the cache file or null
|
|
181
|
+
*/
|
|
182
|
+
function findCacheFileForRepo(cacheDir, specConfig) {
|
|
183
|
+
if (!fs.existsSync(cacheDir) || !specConfig || !specConfig.url) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// Extract owner and repo from URL
|
|
189
|
+
const match = specConfig.url.match(/github\.com\/([^\/]+)\/([^\/]+)/i);
|
|
190
|
+
if (!match || !match[1] || !match[2]) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const owner = match[1];
|
|
195
|
+
const repo = match[2];
|
|
196
|
+
|
|
197
|
+
// Find all JSON files in the cache directory
|
|
198
|
+
const files = fs.readdirSync(cacheDir)
|
|
199
|
+
.filter(file => file.endsWith('.json'))
|
|
200
|
+
.map(file => ({
|
|
201
|
+
path: path.join(cacheDir, file),
|
|
202
|
+
name: file
|
|
203
|
+
}))
|
|
204
|
+
// Sort by timestamp descending (assuming timestamp is at the beginning of filename)
|
|
205
|
+
.sort((a, b) => {
|
|
206
|
+
const timestampA = parseInt(a.name.split('-')[0] || '0', 10);
|
|
207
|
+
const timestampB = parseInt(b.name.split('-')[0] || '0', 10);
|
|
208
|
+
return timestampB - timestampA;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Find the most recent cache file for this repo
|
|
212
|
+
for (const file of files) {
|
|
213
|
+
if (file.name.includes(owner) && file.name.includes(repo)) {
|
|
214
|
+
return file.path;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return null;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error(`Error finding cache file for repo:`, error);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check if terms referenced via tref tags exist in the corresponding external repos
|
|
227
|
+
* @param {string} projectRoot - Root directory of the project
|
|
228
|
+
* @returns {Promise<Array>} - Array of check results
|
|
229
|
+
*/
|
|
230
|
+
async function checkTrefTerms(projectRoot) {
|
|
231
|
+
const results = [];
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
// Path to the project's specs.json
|
|
235
|
+
const specsPath = path.join(projectRoot, 'specs.json');
|
|
236
|
+
|
|
237
|
+
// Check if specs.json exists
|
|
238
|
+
if (!fs.existsSync(specsPath)) {
|
|
239
|
+
return [{
|
|
240
|
+
name: 'Find specs.json file',
|
|
241
|
+
success: false,
|
|
242
|
+
details: 'specs.json file not found in project root'
|
|
243
|
+
}];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
results.push({
|
|
247
|
+
name: 'Find specs.json file',
|
|
248
|
+
success: true,
|
|
249
|
+
details: 'specs.json file found'
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Read specs.json to get the spec directory
|
|
253
|
+
const specsContent = fs.readFileSync(specsPath, 'utf8');
|
|
254
|
+
const specs = JSON.parse(specsContent);
|
|
255
|
+
|
|
256
|
+
// Get the external specs
|
|
257
|
+
if (!specs.specs || !Array.isArray(specs.specs)) {
|
|
258
|
+
results.push({
|
|
259
|
+
name: 'Find specs configuration',
|
|
260
|
+
success: false,
|
|
261
|
+
details: 'specs array not found in specs.json'
|
|
262
|
+
});
|
|
263
|
+
return results;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Collect all external specs and spec directories
|
|
267
|
+
const externalSpecs = [];
|
|
268
|
+
const specDirs = [];
|
|
269
|
+
|
|
270
|
+
specs.specs.forEach(spec => {
|
|
271
|
+
if (spec.external_specs && Array.isArray(spec.external_specs)) {
|
|
272
|
+
externalSpecs.push(...spec.external_specs);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (spec.spec_directory && spec.spec_terms_directory) {
|
|
276
|
+
const termsDir = path.join(
|
|
277
|
+
projectRoot,
|
|
278
|
+
spec.spec_directory,
|
|
279
|
+
spec.spec_terms_directory
|
|
280
|
+
);
|
|
281
|
+
specDirs.push(termsDir);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (externalSpecs.length === 0) {
|
|
286
|
+
results.push({
|
|
287
|
+
name: 'Find external specs',
|
|
288
|
+
success: false,
|
|
289
|
+
details: 'No external specs found in specs.json'
|
|
290
|
+
});
|
|
291
|
+
return results;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
results.push({
|
|
295
|
+
name: 'Find external specs',
|
|
296
|
+
success: true,
|
|
297
|
+
details: `Found ${externalSpecs.length} external specs`
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
if (specDirs.length === 0) {
|
|
301
|
+
results.push({
|
|
302
|
+
name: 'Find spec terms directories',
|
|
303
|
+
success: false,
|
|
304
|
+
details: 'No spec terms directories found'
|
|
305
|
+
});
|
|
306
|
+
return results;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
results.push({
|
|
310
|
+
name: 'Find spec terms directories',
|
|
311
|
+
success: true,
|
|
312
|
+
details: `Found ${specDirs.length} spec terms directories`
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Path to the GitHub cache directory
|
|
316
|
+
const githubCacheDir = path.join(projectRoot, 'output', 'github-cache');
|
|
317
|
+
|
|
318
|
+
if (!fs.existsSync(githubCacheDir)) {
|
|
319
|
+
results.push({
|
|
320
|
+
name: 'Find GitHub cache directory',
|
|
321
|
+
success: false,
|
|
322
|
+
details: 'GitHub cache directory not found at output/github-cache. Terms in external repos cannot be verified.'
|
|
323
|
+
});
|
|
324
|
+
return results;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
results.push({
|
|
328
|
+
name: 'Find GitHub cache directory',
|
|
329
|
+
success: true,
|
|
330
|
+
details: 'GitHub cache directory found'
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Find and check all markdown files in all spec directories
|
|
334
|
+
let totalFiles = 0;
|
|
335
|
+
let filesWithTref = 0;
|
|
336
|
+
let validTerms = 0;
|
|
337
|
+
let invalidTerms = 0;
|
|
338
|
+
let termsFoundInOtherRepos = 0;
|
|
339
|
+
|
|
340
|
+
for (const specDir of specDirs) {
|
|
341
|
+
if (!fs.existsSync(specDir)) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Get all markdown files
|
|
346
|
+
const allFiles = fs.readdirSync(specDir);
|
|
347
|
+
const markdownFiles = allFiles.filter(file => shouldProcessFile(file));
|
|
348
|
+
|
|
349
|
+
totalFiles += markdownFiles.length;
|
|
350
|
+
|
|
351
|
+
for (const file of markdownFiles) {
|
|
352
|
+
const filePath = path.join(specDir, file);
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
356
|
+
const lines = content.split('\n');
|
|
357
|
+
const firstLine = lines[0];
|
|
358
|
+
|
|
359
|
+
if (!firstLine.includes('[[tref:')) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
filesWithTref++;
|
|
364
|
+
|
|
365
|
+
// Extract repo and term information
|
|
366
|
+
const trefInfo = extractTrefInfo(firstLine);
|
|
367
|
+
if (!trefInfo) {
|
|
368
|
+
results.push({
|
|
369
|
+
name: `Parse tref in ${file}`,
|
|
370
|
+
success: false,
|
|
371
|
+
details: `Could not parse tref information from first line: "${firstLine}"`
|
|
372
|
+
});
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const { repo, term } = trefInfo;
|
|
377
|
+
|
|
378
|
+
// Check if the referenced repo exists in external_specs
|
|
379
|
+
const specConfig = externalSpecs.find(spec => spec.external_spec === repo);
|
|
380
|
+
if (!specConfig) {
|
|
381
|
+
results.push({
|
|
382
|
+
name: `Check repo reference in ${file}`,
|
|
383
|
+
success: false,
|
|
384
|
+
details: `Referenced repo "${repo}" is not defined in external_specs`
|
|
385
|
+
});
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Find the cache file for this repo
|
|
390
|
+
const cacheFile = findCacheFileForRepo(githubCacheDir, specConfig);
|
|
391
|
+
|
|
392
|
+
if (!cacheFile) {
|
|
393
|
+
results.push({
|
|
394
|
+
name: `Find cache for repo "${repo}" referenced in ${file}`,
|
|
395
|
+
success: false,
|
|
396
|
+
details: `No cache file found for repo "${repo}". Unable to verify if term "${term}" exists.`
|
|
397
|
+
});
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Check if the term exists in the repo
|
|
402
|
+
const termExists = termExistsInRepo(cacheFile, term);
|
|
403
|
+
|
|
404
|
+
if (termExists) {
|
|
405
|
+
validTerms++;
|
|
406
|
+
results.push({
|
|
407
|
+
name: `Term "${term}" in repo "${repo}" (${file})`,
|
|
408
|
+
success: true,
|
|
409
|
+
details: `Term "${term}" found in repo "${repo}"`
|
|
410
|
+
});
|
|
411
|
+
} else {
|
|
412
|
+
// Check if the term exists in other repos
|
|
413
|
+
const otherRepos = findTermInOtherRepos(githubCacheDir, externalSpecs, repo, term);
|
|
414
|
+
|
|
415
|
+
if (otherRepos.length > 0) {
|
|
416
|
+
// Change: Show as warning (partial success) instead of failure
|
|
417
|
+
termsFoundInOtherRepos++;
|
|
418
|
+
results.push({
|
|
419
|
+
name: `Term "${term}" in repo "${repo}" (${file})`,
|
|
420
|
+
success: 'partial', // Use 'partial' to indicate a warning
|
|
421
|
+
details: `Warning: Term <code>${term}</code> NOT found in repo ${repo} but found in these repos: <code>${otherRepos.join(', ')}</code>. Consider updating the reference.`
|
|
422
|
+
});
|
|
423
|
+
} else {
|
|
424
|
+
invalidTerms++;
|
|
425
|
+
results.push({
|
|
426
|
+
name: `Term "${term}" in repo "${repo}" (${file})`,
|
|
427
|
+
success: false,
|
|
428
|
+
details: `Term <code>${term}</code> NOT found in repo <code>${repo}</code> and not found in any other external repos`
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} catch (error) {
|
|
433
|
+
results.push({
|
|
434
|
+
name: `Process file ${file}`,
|
|
435
|
+
success: false,
|
|
436
|
+
details: `Error processing file: ${error.message}`
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Add summary results - modified to include terms found in other repos
|
|
443
|
+
results.push({
|
|
444
|
+
name: 'Term reference validation summary',
|
|
445
|
+
success: invalidTerms === 0,
|
|
446
|
+
details: `Processed ${totalFiles} files, found ${filesWithTref} with tref tags. ${validTerms} terms found correctly, ${termsFoundInOtherRepos} terms found in alternative repos (warnings), ${invalidTerms} terms missing.`
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
return results;
|
|
450
|
+
} catch (error) {
|
|
451
|
+
console.error('Error checking tref terms:', error);
|
|
452
|
+
return [{
|
|
453
|
+
name: 'Term reference validation check',
|
|
454
|
+
success: false,
|
|
455
|
+
details: `Error: ${error.message}`
|
|
456
|
+
}];
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
module.exports = {
|
|
461
|
+
checkTrefTerms
|
|
462
|
+
};
|