spec-up-t 1.2.6 → 1.2.8

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.
@@ -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(key => ({
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(key => ({
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 required field and generate result
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 evaluateRequiredField(field, projectSpecs, defaultSpecs) {
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: false,
136
- details: `Required "${field.key}" key is missing in specs.json`
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
- let configured = isFieldConfigured(projectValue, defaultValue);
238
+ const isConfigured = field.allowDefaultValue || isFieldConfigured(projectValue, defaultValue);
143
239
 
144
- // For fields that can keep their default values, mark as configured
145
- if (field.allowDefaultValue) {
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
- if (!configured) {
153
- if (field.mustChange) {
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
- } else {
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
- const hasField = projectSpecs.specs?.[0]?.hasOwnProperty(field.key);
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
- // Add a summary of missing required fields
232
- if (missingRequiredKeys.length > 0) {
233
- summaryResults.push({
234
- name: 'Required fields check',
235
- success: false,
236
- details: `Missing required fields: ${missingRequiredKeys.join(', ')}`
237
- });
238
- } else {
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
- // Check for unexpected fields
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 totalItems = fieldResults.length;
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}/${totalItems} items)`
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
- // Path to the project's specs.json
282
- const projectSpecsPath = path.join(projectRoot, 'specs.json');
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 { requiredFields, optionalFields } = categorizeFields(defaultSpecKeys);
312
-
313
- // Check each required field
314
- const missingRequiredKeys = [];
363
+ const { results: fieldResults, missingRequiredKeys } = processFieldValidation(
364
+ projectSpecs, defaultSpecs, defaultSpecKeys
365
+ );
315
366
 
316
- for (const field of requiredFields) {
317
- const result = evaluateRequiredField(field, projectSpecs, defaultSpecs);
318
- if (!result.success && result.details.includes('missing')) {
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
- results.push(...summaryResults);
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);