spec-up-t 1.2.7 → 1.2.9
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/.github/copilot-instructions.md +4 -0
- package/assets/compiled/body.js +2 -1
- package/assets/compiled/head.css +1 -0
- package/assets/compiled/refs.json +1 -1
- package/assets/css/highlight-heading-plus-sibling-nodes.css +6 -0
- package/assets/css/index.css +9 -0
- package/assets/js/collapse-definitions.js +0 -6
- package/assets/js/highlight-heading-plus-sibling-nodes.js +259 -0
- package/config/asset-map.json +2 -0
- package/gulpfile.js +8 -2
- package/index.js +24 -16
- package/package.json +1 -1
- package/src/collect-external-references.js +10 -23
- package/src/escape-handler.js +67 -0
- package/src/escape-mechanism.js +57 -0
- package/src/health-check/specs-configuration-checker.js +178 -144
- package/src/health-check.js +199 -10
- package/src/markdown-it-extensions.js +8 -0
- package/src/collectExternalReferences/fetchTermsFromIndex.1.js +0 -340
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Spec-Up-T specs.json Configuration Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates specs.json configuration files by comparing project configurations
|
|
5
|
+
* against default templates to ensure proper setup and catch common issues.
|
|
6
|
+
*
|
|
7
|
+
* **Validation Flow:**
|
|
8
|
+
* 1. File existence check (project specs.json + default template)
|
|
9
|
+
* 2. Field categorization (required vs optional fields)
|
|
10
|
+
* 3. Required field validation (presence + configuration status)
|
|
11
|
+
* 4. Optional field validation (configuration warnings)
|
|
12
|
+
* 5. Unexpected field detection (typo prevention)
|
|
13
|
+
* 6. Summary report generation
|
|
14
|
+
*
|
|
15
|
+
* **Field Categories:**
|
|
16
|
+
* - **Required fields**: Must be present (e.g., title, author, source)
|
|
17
|
+
* - **Optional fields**: Can be omitted (e.g., logo, external_specs)
|
|
18
|
+
* - **Must-change fields**: Cannot use default values (title, author, etc.)
|
|
19
|
+
* - **Allow-default fields**: Can keep default values (spec_directory, etc.)
|
|
20
|
+
* - **Deprecated fields**: Legacy fields ignored during validation
|
|
21
|
+
*
|
|
22
|
+
* **Output:**
|
|
23
|
+
* Returns structured validation results with pass/fail/warning status,
|
|
24
|
+
* detailed messages, and actionable feedback for configuration improvements.
|
|
25
|
+
*
|
|
26
|
+
* @author Spec-Up-T Team
|
|
27
|
+
* @since 2025-06-06
|
|
28
|
+
*/
|
|
29
|
+
|
|
1
30
|
const fs = require('fs');
|
|
2
31
|
const path = require('path');
|
|
3
32
|
|
|
@@ -51,9 +80,18 @@ const knownOptionalFields = [
|
|
|
51
80
|
'external_specs',
|
|
52
81
|
'logo_link',
|
|
53
82
|
'favicon',
|
|
54
|
-
'katex'
|
|
83
|
+
'katex',
|
|
84
|
+
'spec_directory',
|
|
85
|
+
'spec_terms_directory',
|
|
86
|
+
'output_path',
|
|
87
|
+
'markdown_paths'
|
|
55
88
|
];
|
|
56
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Deprecated fields that should not be flagged as unexpected
|
|
92
|
+
*/
|
|
93
|
+
const deprecatedFields = [];
|
|
94
|
+
|
|
57
95
|
/**
|
|
58
96
|
* Check if the files needed for configuration check exist
|
|
59
97
|
* @param {string} projectSpecsPath - Path to project specs.json
|
|
@@ -80,32 +118,87 @@ function checkFilesExist(projectSpecsPath, defaultSpecsPath) {
|
|
|
80
118
|
return null;
|
|
81
119
|
}
|
|
82
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Get all valid field names (required + optional + deprecated). Creates a comprehensive list of all field names that should not be flagged as "unexpected"
|
|
123
|
+
* @param {Array} defaultSpecKeys - Keys from default specs
|
|
124
|
+
* @returns {Array} - All valid field names
|
|
125
|
+
*/
|
|
126
|
+
function getAllValidFields(defaultSpecKeys) {
|
|
127
|
+
// Get all field names from descriptions (these are the canonical field names)
|
|
128
|
+
const canonicalFields = Object.keys(fieldDescriptions);
|
|
129
|
+
|
|
130
|
+
// Combine with known optional fields and deprecated fields
|
|
131
|
+
const allValidFields = [
|
|
132
|
+
...canonicalFields,
|
|
133
|
+
...knownOptionalFields,
|
|
134
|
+
...deprecatedFields,
|
|
135
|
+
...defaultSpecKeys
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
// Remove duplicates
|
|
139
|
+
return [...new Set(allValidFields)];
|
|
140
|
+
}
|
|
141
|
+
|
|
83
142
|
/**
|
|
84
143
|
* Categorize fields into required and optional
|
|
85
144
|
* @param {Array} defaultSpecKeys - Keys from default specs
|
|
86
145
|
* @returns {Object} - Object containing required and optional fields
|
|
87
146
|
*/
|
|
88
147
|
function categorizeFields(defaultSpecKeys) {
|
|
148
|
+
const createFieldObject = key => ({
|
|
149
|
+
key,
|
|
150
|
+
description: fieldDescriptions[key] || `${key.replace(/_/g, ' ')} field`,
|
|
151
|
+
allowDefaultValue: allowDefaultValueFields.includes(key),
|
|
152
|
+
mustChange: mustChangeFields.includes(key)
|
|
153
|
+
});
|
|
154
|
+
|
|
89
155
|
const requiredFields = defaultSpecKeys
|
|
90
156
|
.filter(key => !knownOptionalFields.includes(key))
|
|
91
|
-
.map(
|
|
92
|
-
key,
|
|
93
|
-
description: fieldDescriptions[key] || `${key.replace(/_/g, ' ')} field`,
|
|
94
|
-
allowDefaultValue: allowDefaultValueFields.includes(key),
|
|
95
|
-
mustChange: mustChangeFields.includes(key)
|
|
96
|
-
}));
|
|
157
|
+
.map(createFieldObject);
|
|
97
158
|
|
|
98
159
|
const optionalFields = defaultSpecKeys
|
|
99
160
|
.filter(key => knownOptionalFields.includes(key))
|
|
100
|
-
.map(
|
|
101
|
-
key,
|
|
102
|
-
description: fieldDescriptions[key] || `${key.replace(/_/g, ' ')} field`,
|
|
103
|
-
allowDefaultValue: allowDefaultValueFields.includes(key)
|
|
104
|
-
}));
|
|
161
|
+
.map(createFieldObject);
|
|
105
162
|
|
|
106
163
|
return { requiredFields, optionalFields };
|
|
107
164
|
}
|
|
108
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Process field validation results. Orchestrates the validation of all fields in the specs.json
|
|
168
|
+
* @param {Object} projectSpecs - Project specs object
|
|
169
|
+
* @param {Object} defaultSpecs - Default specs object
|
|
170
|
+
* @param {Array} defaultSpecKeys - Keys from default specs
|
|
171
|
+
* @returns {Object} - Object with results and missingRequiredKeys
|
|
172
|
+
*/
|
|
173
|
+
function processFieldValidation(projectSpecs, defaultSpecs, defaultSpecKeys) {
|
|
174
|
+
const { requiredFields, optionalFields } = categorizeFields(defaultSpecKeys);
|
|
175
|
+
|
|
176
|
+
const requiredResults = requiredFields.map(field => evaluateRequiredField(field, projectSpecs, defaultSpecs));
|
|
177
|
+
const optionalResults = optionalFields.map(field => evaluateOptionalField(field, projectSpecs, defaultSpecs));
|
|
178
|
+
|
|
179
|
+
const missingRequiredKeys = requiredResults
|
|
180
|
+
.filter(result => !result.success && result.details.includes('missing'))
|
|
181
|
+
.map((_, index) => requiredFields[index].key);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
results: [...requiredResults, ...optionalResults],
|
|
185
|
+
missingRequiredKeys
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check for unexpected fields in project specs
|
|
191
|
+
* @param {Object} projectSpecs - Project specs object
|
|
192
|
+
* @param {Array} defaultSpecKeys - Keys from default specs
|
|
193
|
+
* @returns {Array} - Array of unexpected field names
|
|
194
|
+
*/
|
|
195
|
+
function findUnexpectedFields(projectSpecs, defaultSpecKeys) {
|
|
196
|
+
const projectKeys = Object.keys(projectSpecs.specs?.[0] || {});
|
|
197
|
+
const allValidFields = getAllValidFields(defaultSpecKeys);
|
|
198
|
+
|
|
199
|
+
return projectKeys.filter(key => !allValidFields.includes(key));
|
|
200
|
+
}
|
|
201
|
+
|
|
109
202
|
/**
|
|
110
203
|
* Check if a field value has been configured
|
|
111
204
|
* @param {any} projectValue - Value from project specs
|
|
@@ -120,61 +213,58 @@ function isFieldConfigured(projectValue, defaultValue) {
|
|
|
120
213
|
}
|
|
121
214
|
|
|
122
215
|
/**
|
|
123
|
-
* Evaluate a
|
|
216
|
+
* Evaluate a field and generate result (unified for required/optional)
|
|
124
217
|
* @param {Object} field - Field definition
|
|
125
218
|
* @param {Object} projectSpecs - Project specs object
|
|
126
219
|
* @param {Object} defaultSpecs - Default specs object
|
|
220
|
+
* @param {boolean} isRequired - Whether field is required
|
|
127
221
|
* @returns {Object} - Check result
|
|
128
222
|
*/
|
|
129
|
-
function
|
|
223
|
+
function evaluateField(field, projectSpecs, defaultSpecs, isRequired) {
|
|
130
224
|
const hasField = projectSpecs.specs?.[0]?.hasOwnProperty(field.key);
|
|
131
225
|
|
|
132
226
|
if (!hasField) {
|
|
133
227
|
return {
|
|
134
228
|
name: `${field.description} configuration`,
|
|
135
|
-
success:
|
|
136
|
-
details:
|
|
229
|
+
success: !isRequired,
|
|
230
|
+
details: isRequired
|
|
231
|
+
? `Required "${field.key}" key is missing in specs.json`
|
|
232
|
+
: `Optional "${field.key}" key is not present (this is not required)`
|
|
137
233
|
};
|
|
138
234
|
}
|
|
139
235
|
|
|
140
236
|
const projectValue = projectSpecs.specs[0][field.key];
|
|
141
237
|
const defaultValue = defaultSpecs.specs?.[0]?.[field.key];
|
|
142
|
-
|
|
238
|
+
const isConfigured = field.allowDefaultValue || isFieldConfigured(projectValue, defaultValue);
|
|
143
239
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
configured = true;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
let status;
|
|
150
|
-
let success = true;
|
|
240
|
+
// Show warning when fields haven't been configured from their default values
|
|
241
|
+
const status = isConfigured ? undefined : 'warning';
|
|
151
242
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
status = undefined; // No status means it shows as failure
|
|
155
|
-
success = false;
|
|
156
|
-
} else {
|
|
157
|
-
status = 'warning';
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
let details = '';
|
|
162
|
-
if (configured) {
|
|
163
|
-
details = (projectValue === defaultValue && field.allowDefaultValue)
|
|
243
|
+
const details = isConfigured
|
|
244
|
+
? (projectValue === defaultValue && field.allowDefaultValue)
|
|
164
245
|
? `Default value for ${field.description} is acceptable`
|
|
165
|
-
: `${field.description} has been changed from default
|
|
166
|
-
|
|
167
|
-
details = `${field.description} is still set to default value${['title', 'author'].includes(field.key) ? `: \"${defaultValue}\"` : ''}`;
|
|
168
|
-
}
|
|
246
|
+
: `${field.description} has been changed from default`
|
|
247
|
+
: `${field.description} is still set to default value${mustChangeFields.includes(field.key) ? `: \"${defaultValue}\"` : ''}`;
|
|
169
248
|
|
|
170
249
|
return {
|
|
171
250
|
name: `${field.description} configuration`,
|
|
172
251
|
status,
|
|
173
|
-
success,
|
|
252
|
+
success: true,
|
|
174
253
|
details
|
|
175
254
|
};
|
|
176
255
|
}
|
|
177
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Evaluate a required field and generate result
|
|
259
|
+
* @param {Object} field - Field definition
|
|
260
|
+
* @param {Object} projectSpecs - Project specs object
|
|
261
|
+
* @param {Object} defaultSpecs - Default specs object
|
|
262
|
+
* @returns {Object} - Check result
|
|
263
|
+
*/
|
|
264
|
+
function evaluateRequiredField(field, projectSpecs, defaultSpecs) {
|
|
265
|
+
return evaluateField(field, projectSpecs, defaultSpecs, true);
|
|
266
|
+
}
|
|
267
|
+
|
|
178
268
|
/**
|
|
179
269
|
* Evaluate an optional field and generate result
|
|
180
270
|
* @param {Object} field - Field definition
|
|
@@ -183,39 +273,7 @@ function evaluateRequiredField(field, projectSpecs, defaultSpecs) {
|
|
|
183
273
|
* @returns {Object} - Check result
|
|
184
274
|
*/
|
|
185
275
|
function evaluateOptionalField(field, projectSpecs, defaultSpecs) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (!hasField) {
|
|
189
|
-
return {
|
|
190
|
-
name: `${field.description} configuration`,
|
|
191
|
-
success: true,
|
|
192
|
-
details: `Optional "${field.key}" key is not present (this is not required)`
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const projectValue = projectSpecs.specs[0][field.key];
|
|
197
|
-
const defaultValue = defaultSpecs.specs?.[0]?.[field.key];
|
|
198
|
-
let configured = isFieldConfigured(projectValue, defaultValue);
|
|
199
|
-
|
|
200
|
-
if (field.allowDefaultValue) {
|
|
201
|
-
configured = true;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
let details = '';
|
|
205
|
-
if (configured) {
|
|
206
|
-
details = (projectValue === defaultValue && field.allowDefaultValue)
|
|
207
|
-
? `Default value for ${field.description} is acceptable`
|
|
208
|
-
: `${field.description} has been changed from default`;
|
|
209
|
-
} else {
|
|
210
|
-
details = `${field.description} is still set to default value`;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
name: `${field.description} configuration`,
|
|
215
|
-
status: configured ? undefined : 'warning',
|
|
216
|
-
success: true, // Always true for optional fields
|
|
217
|
-
details
|
|
218
|
-
};
|
|
276
|
+
return evaluateField(field, projectSpecs, defaultSpecs, false);
|
|
219
277
|
}
|
|
220
278
|
|
|
221
279
|
/**
|
|
@@ -228,22 +286,16 @@ function evaluateOptionalField(field, projectSpecs, defaultSpecs) {
|
|
|
228
286
|
function generateSummaryResults(results, missingRequiredKeys, unexpectedKeys) {
|
|
229
287
|
const summaryResults = [];
|
|
230
288
|
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
summaryResults.push({
|
|
240
|
-
name: 'Required fields check',
|
|
241
|
-
success: true,
|
|
242
|
-
details: 'All required fields are present'
|
|
243
|
-
});
|
|
244
|
-
}
|
|
289
|
+
// Required fields summary
|
|
290
|
+
summaryResults.push({
|
|
291
|
+
name: 'Required fields check',
|
|
292
|
+
success: missingRequiredKeys.length === 0,
|
|
293
|
+
details: missingRequiredKeys.length > 0
|
|
294
|
+
? `Missing required fields: ${missingRequiredKeys.join(', ')}`
|
|
295
|
+
: 'All required fields are present'
|
|
296
|
+
});
|
|
245
297
|
|
|
246
|
-
//
|
|
298
|
+
// Unexpected fields check
|
|
247
299
|
if (unexpectedKeys.length > 0) {
|
|
248
300
|
summaryResults.push({
|
|
249
301
|
name: 'Unexpected fields check',
|
|
@@ -253,24 +305,44 @@ function generateSummaryResults(results, missingRequiredKeys, unexpectedKeys) {
|
|
|
253
305
|
}
|
|
254
306
|
|
|
255
307
|
// Overall configuration status
|
|
256
|
-
const fieldResults = results.filter(r =>
|
|
257
|
-
r.name.includes('configuration') &&
|
|
258
|
-
!r.name.includes('Overall')
|
|
308
|
+
const fieldResults = results.filter(r =>
|
|
309
|
+
r.name.includes('configuration') && !r.name.includes('Overall')
|
|
259
310
|
);
|
|
260
|
-
|
|
311
|
+
|
|
261
312
|
const configuredItemsCount = fieldResults.filter(r => r.success).length;
|
|
262
|
-
const
|
|
263
|
-
const configurationPercentage = Math.round((configuredItemsCount / totalItems) * 100);
|
|
313
|
+
const configurationPercentage = Math.round((configuredItemsCount / fieldResults.length) * 100);
|
|
264
314
|
|
|
265
315
|
summaryResults.push({
|
|
266
316
|
name: 'Overall configuration status',
|
|
267
317
|
success: configurationPercentage > 50 && missingRequiredKeys.length === 0,
|
|
268
|
-
details: `${configurationPercentage}% of specs.json has been configured (${configuredItemsCount}/${
|
|
318
|
+
details: `${configurationPercentage}% of specs.json has been configured (${configuredItemsCount}/${fieldResults.length} items)`
|
|
269
319
|
});
|
|
270
320
|
|
|
271
321
|
return summaryResults;
|
|
272
322
|
}
|
|
273
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Load and parse configuration files
|
|
326
|
+
* @param {string} projectRoot - Root directory of the project
|
|
327
|
+
* @returns {Object} - Object containing parsed specs and file paths
|
|
328
|
+
*/
|
|
329
|
+
function loadConfigurationFiles(projectRoot) {
|
|
330
|
+
const projectSpecsPath = path.join(projectRoot, 'specs.json');
|
|
331
|
+
const defaultSpecsPath = path.join(
|
|
332
|
+
__dirname, '..', 'install-from-boilerplate', 'boilerplate', 'specs.json'
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const fileCheckResults = checkFilesExist(projectSpecsPath, defaultSpecsPath);
|
|
336
|
+
if (fileCheckResults) {
|
|
337
|
+
return { error: fileCheckResults };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const projectSpecs = JSON.parse(fs.readFileSync(projectSpecsPath, 'utf8'));
|
|
341
|
+
const defaultSpecs = JSON.parse(fs.readFileSync(defaultSpecsPath, 'utf8'));
|
|
342
|
+
|
|
343
|
+
return { projectSpecs, defaultSpecs };
|
|
344
|
+
}
|
|
345
|
+
|
|
274
346
|
/**
|
|
275
347
|
* Check if specs.json has been configured from default
|
|
276
348
|
* @param {string} projectRoot - Root directory of the project
|
|
@@ -278,27 +350,8 @@ function generateSummaryResults(results, missingRequiredKeys, unexpectedKeys) {
|
|
|
278
350
|
*/
|
|
279
351
|
async function checkSpecsJsonConfiguration(projectRoot) {
|
|
280
352
|
try {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// Path to the default boilerplate specs.json
|
|
285
|
-
const defaultSpecsPath = path.join(
|
|
286
|
-
__dirname,
|
|
287
|
-
'..',
|
|
288
|
-
'install-from-boilerplate',
|
|
289
|
-
'boilerplate',
|
|
290
|
-
'specs.json'
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
// Check if required files exist
|
|
294
|
-
const fileCheckResults = checkFilesExist(projectSpecsPath, defaultSpecsPath);
|
|
295
|
-
if (fileCheckResults) {
|
|
296
|
-
return fileCheckResults;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Read both files
|
|
300
|
-
const projectSpecs = JSON.parse(fs.readFileSync(projectSpecsPath, 'utf8'));
|
|
301
|
-
const defaultSpecs = JSON.parse(fs.readFileSync(defaultSpecsPath, 'utf8'));
|
|
353
|
+
const { error, projectSpecs, defaultSpecs } = loadConfigurationFiles(projectRoot);
|
|
354
|
+
if (error) return error;
|
|
302
355
|
|
|
303
356
|
const results = [{
|
|
304
357
|
name: 'specs.json exists',
|
|
@@ -306,36 +359,17 @@ async function checkSpecsJsonConfiguration(projectRoot) {
|
|
|
306
359
|
details: 'Project specs.json file found'
|
|
307
360
|
}];
|
|
308
361
|
|
|
309
|
-
// Define required and optional fields based on the default specs.json
|
|
310
362
|
const defaultSpecKeys = Object.keys(defaultSpecs.specs?.[0] || {});
|
|
311
|
-
const {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const missingRequiredKeys = [];
|
|
363
|
+
const { results: fieldResults, missingRequiredKeys } = processFieldValidation(
|
|
364
|
+
projectSpecs, defaultSpecs, defaultSpecKeys
|
|
365
|
+
);
|
|
315
366
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
missingRequiredKeys.push(field.key);
|
|
320
|
-
}
|
|
321
|
-
results.push(result);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Check optional fields
|
|
325
|
-
for (const field of optionalFields) {
|
|
326
|
-
results.push(evaluateOptionalField(field, projectSpecs, defaultSpecs));
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Check for unexpected fields
|
|
330
|
-
const allStandardKeys = [...requiredFields, ...optionalFields].map(f => f.key);
|
|
331
|
-
const unexpectedKeys = Object.keys(projectSpecs.specs?.[0] || {})
|
|
332
|
-
.filter(key => !allStandardKeys.includes(key));
|
|
333
|
-
|
|
334
|
-
// Add summary results
|
|
367
|
+
results.push(...fieldResults);
|
|
368
|
+
|
|
369
|
+
const unexpectedKeys = findUnexpectedFields(projectSpecs, defaultSpecKeys);
|
|
335
370
|
const summaryResults = generateSummaryResults(results, missingRequiredKeys, unexpectedKeys);
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return results;
|
|
371
|
+
|
|
372
|
+
return [...results, ...summaryResults];
|
|
339
373
|
|
|
340
374
|
} catch (error) {
|
|
341
375
|
console.error('Error checking specs.json configuration:', error);
|