spec-up-t-healthcheck 1.0.0
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/README.md +216 -0
- package/bin/cli.js +193 -0
- package/bin/demo-html.js +186 -0
- package/bin/simple-test.js +79 -0
- package/lib/checks/external-specs-urls.js +484 -0
- package/lib/checks/gitignore.js +350 -0
- package/lib/checks/package-json.js +518 -0
- package/lib/checks/spec-files.js +263 -0
- package/lib/checks/specsjson.js +361 -0
- package/lib/file-opener.js +127 -0
- package/lib/formatters.js +176 -0
- package/lib/health-check-orchestrator.js +413 -0
- package/lib/health-check-registry.js +396 -0
- package/lib/health-check-utils.js +234 -0
- package/lib/health-checker.js +145 -0
- package/lib/html-formatter.js +626 -0
- package/lib/index.js +123 -0
- package/lib/providers.js +184 -0
- package/lib/web.js +70 -0
- package/package.json +91 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Specification files health check module
|
|
3
|
+
*
|
|
4
|
+
* This module validates the presence and accessibility of specification files
|
|
5
|
+
* in repositories. It searches for markdown files in common specification
|
|
6
|
+
* directories and ensures that specification content is properly organized.
|
|
7
|
+
*
|
|
8
|
+
* @author spec-up-t-healthcheck
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createHealthCheckResult, createErrorResult } from '../health-check-utils.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The identifier for this health check, used in reports and registries.
|
|
15
|
+
* @type {string}
|
|
16
|
+
*/
|
|
17
|
+
export const CHECK_ID = 'spec-files';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Human-readable name for this health check.
|
|
21
|
+
* @type {string}
|
|
22
|
+
*/
|
|
23
|
+
export const CHECK_NAME = 'Specification Files Discovery';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Description of what this health check validates.
|
|
27
|
+
* @type {string}
|
|
28
|
+
*/
|
|
29
|
+
export const CHECK_DESCRIPTION = 'Discovers and validates specification files in the repository';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Common directory paths where specification files are typically located.
|
|
33
|
+
* These paths are searched in order of preference.
|
|
34
|
+
* @type {readonly string[]}
|
|
35
|
+
*/
|
|
36
|
+
const SPEC_DIRECTORIES = Object.freeze([
|
|
37
|
+
'spec/',
|
|
38
|
+
'specs/',
|
|
39
|
+
'docs/',
|
|
40
|
+
'documentation/',
|
|
41
|
+
'doc/'
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* File extensions that are considered specification files.
|
|
46
|
+
* @type {readonly string[]}
|
|
47
|
+
*/
|
|
48
|
+
const SPEC_EXTENSIONS = Object.freeze(['.md', '.markdown', '.rst', '.txt']);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Common specification file names that indicate primary specification content.
|
|
52
|
+
* These files are given higher priority in reporting.
|
|
53
|
+
* @type {readonly string[]}
|
|
54
|
+
*/
|
|
55
|
+
const PRIMARY_SPEC_NAMES = Object.freeze([
|
|
56
|
+
'spec.md',
|
|
57
|
+
'specification.md',
|
|
58
|
+
'README.md',
|
|
59
|
+
'index.md',
|
|
60
|
+
'main.md'
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Checks for the presence and accessibility of specification files in the repository.
|
|
65
|
+
*
|
|
66
|
+
* This health check searches for markdown and other documentation files in common
|
|
67
|
+
* specification directories as well as the repository root. It validates that
|
|
68
|
+
* specification content is available and properly organized.
|
|
69
|
+
*
|
|
70
|
+
* The check performs the following discovery:
|
|
71
|
+
* - Searches common spec directories (spec/, docs/, etc.)
|
|
72
|
+
* - Looks for markdown and text files in root directory
|
|
73
|
+
* - Identifies primary specification files
|
|
74
|
+
* - Reports on file organization and accessibility
|
|
75
|
+
*
|
|
76
|
+
* @param {import('../providers.js').Provider} provider - The provider instance for file operations
|
|
77
|
+
* @returns {Promise<import('../health-check-utils.js').HealthCheckResult>} The health check result with file discovery details
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```javascript
|
|
81
|
+
* const provider = createLocalProvider('/path/to/repo');
|
|
82
|
+
* const result = await checkSpecFiles(provider);
|
|
83
|
+
* console.log(result.details.specFiles); // Array of found specification files
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export async function checkSpecFiles(provider) {
|
|
87
|
+
try {
|
|
88
|
+
const discoveryResult = await discoverSpecificationFiles(provider);
|
|
89
|
+
|
|
90
|
+
const {
|
|
91
|
+
specFiles,
|
|
92
|
+
specDirectory,
|
|
93
|
+
primarySpecs,
|
|
94
|
+
rootSpecFiles,
|
|
95
|
+
searchedPaths,
|
|
96
|
+
totalFiles
|
|
97
|
+
} = discoveryResult;
|
|
98
|
+
|
|
99
|
+
// No specification files found
|
|
100
|
+
if (totalFiles === 0) {
|
|
101
|
+
return createHealthCheckResult(
|
|
102
|
+
CHECK_NAME,
|
|
103
|
+
'fail',
|
|
104
|
+
'No specification files found in repository',
|
|
105
|
+
{
|
|
106
|
+
searchedPaths,
|
|
107
|
+
searchedExtensions: SPEC_EXTENSIONS,
|
|
108
|
+
suggestions: [
|
|
109
|
+
'Create a spec/ or docs/ directory',
|
|
110
|
+
'Add a README.md file with specification content',
|
|
111
|
+
'Ensure specification files use supported extensions (.md, .markdown, .rst, .txt)'
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Determine health status - keep it simple like the original
|
|
118
|
+
let status = 'pass';
|
|
119
|
+
let message = `Found ${totalFiles} specification file${totalFiles === 1 ? '' : 's'}`;
|
|
120
|
+
|
|
121
|
+
const details = {
|
|
122
|
+
specFiles: specFiles.map(f => f.name),
|
|
123
|
+
specDirectory,
|
|
124
|
+
primarySpecs: primarySpecs.map(f => f.name),
|
|
125
|
+
rootSpecFiles: rootSpecFiles.map(f => f.name),
|
|
126
|
+
totalFiles,
|
|
127
|
+
hasOrganizedSpecs: !!specDirectory,
|
|
128
|
+
hasPrimarySpecs: primarySpecs.length > 0,
|
|
129
|
+
searchedPaths
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Only add organization info to message, don't change status
|
|
133
|
+
if (specDirectory) {
|
|
134
|
+
message += ` in organized ${specDirectory} directory`;
|
|
135
|
+
} else if (rootSpecFiles.length > 0) {
|
|
136
|
+
message += ' in repository root';
|
|
137
|
+
// Only warn if there are many unorganized files in root (more aggressive threshold)
|
|
138
|
+
if (rootSpecFiles.length > 5) {
|
|
139
|
+
details.organizationSuggestion = 'Consider moving specification files to a dedicated directory like spec/ or docs/';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Don't warn about missing primary specs - this is optional organizational advice only
|
|
144
|
+
if (primarySpecs.length === 0 && totalFiles > 1) {
|
|
145
|
+
details.primarySpecSuggestion = 'Consider adding a main specification file (spec.md, README.md, or index.md)';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return createHealthCheckResult(CHECK_NAME, status, message, details);
|
|
149
|
+
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return createErrorResult(CHECK_NAME, error, {
|
|
152
|
+
context: 'discovering specification files',
|
|
153
|
+
provider: provider.type
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Discovers all specification files in the repository.
|
|
160
|
+
*
|
|
161
|
+
* This function performs the actual file discovery logic, searching through
|
|
162
|
+
* common specification directories and the repository root for relevant files.
|
|
163
|
+
*
|
|
164
|
+
* @param {import('../providers.js').Provider} provider - The provider instance for file operations
|
|
165
|
+
* @returns {Promise<Object>} Discovery result with found files and metadata
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
async function discoverSpecificationFiles(provider) {
|
|
169
|
+
let specFiles = [];
|
|
170
|
+
let specDirectory = null;
|
|
171
|
+
let primarySpecs = [];
|
|
172
|
+
let rootSpecFiles = [];
|
|
173
|
+
const searchedPaths = [];
|
|
174
|
+
|
|
175
|
+
// Search spec directories first
|
|
176
|
+
for (const dir of SPEC_DIRECTORIES) {
|
|
177
|
+
searchedPaths.push(dir);
|
|
178
|
+
try {
|
|
179
|
+
const files = await provider.listFiles(dir);
|
|
180
|
+
if (files.length > 0) {
|
|
181
|
+
const relevantFiles = filterSpecificationFiles(files);
|
|
182
|
+
if (relevantFiles.length > 0) {
|
|
183
|
+
specDirectory = dir;
|
|
184
|
+
specFiles = relevantFiles;
|
|
185
|
+
primarySpecs = identifyPrimarySpecs(relevantFiles, dir);
|
|
186
|
+
break; // Use first directory with spec files
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (dirError) {
|
|
190
|
+
// Directory doesn't exist or can't be accessed, continue searching
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Also check root directory for specification files
|
|
195
|
+
try {
|
|
196
|
+
const rootFiles = await provider.listFiles('');
|
|
197
|
+
const rootRelevantFiles = filterSpecificationFiles(rootFiles);
|
|
198
|
+
rootSpecFiles = rootRelevantFiles;
|
|
199
|
+
|
|
200
|
+
// If no organized spec directory found, use root files as primary
|
|
201
|
+
if (!specDirectory && rootRelevantFiles.length > 0) {
|
|
202
|
+
specFiles = rootRelevantFiles;
|
|
203
|
+
primarySpecs = identifyPrimarySpecs(rootRelevantFiles, '');
|
|
204
|
+
}
|
|
205
|
+
} catch (rootError) {
|
|
206
|
+
// Can't access root directory
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const totalFiles = specFiles.length + (specDirectory ? 0 : rootSpecFiles.length);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
specFiles,
|
|
213
|
+
specDirectory,
|
|
214
|
+
primarySpecs,
|
|
215
|
+
rootSpecFiles,
|
|
216
|
+
searchedPaths,
|
|
217
|
+
totalFiles
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Filters a list of files to include only specification-relevant files.
|
|
223
|
+
*
|
|
224
|
+
* @param {Array} files - Array of file objects from provider.listFiles()
|
|
225
|
+
* @returns {Array} Filtered array of specification files
|
|
226
|
+
* @private
|
|
227
|
+
*/
|
|
228
|
+
function filterSpecificationFiles(files) {
|
|
229
|
+
return files.filter(file => {
|
|
230
|
+
if (!file.isFile) return false;
|
|
231
|
+
|
|
232
|
+
return SPEC_EXTENSIONS.some(ext =>
|
|
233
|
+
file.name.toLowerCase().endsWith(ext.toLowerCase())
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Identifies primary specification files from a list of files.
|
|
240
|
+
*
|
|
241
|
+
* Primary specification files are those with common names that typically
|
|
242
|
+
* contain the main specification content.
|
|
243
|
+
*
|
|
244
|
+
* @param {Array} files - Array of specification files
|
|
245
|
+
* @param {string} directory - The directory containing the files
|
|
246
|
+
* @returns {Array} Array of primary specification files
|
|
247
|
+
* @private
|
|
248
|
+
*/
|
|
249
|
+
function identifyPrimarySpecs(files, directory) {
|
|
250
|
+
const primaryFiles = [];
|
|
251
|
+
|
|
252
|
+
for (const file of files) {
|
|
253
|
+
const fileName = file.name.toLowerCase();
|
|
254
|
+
if (PRIMARY_SPEC_NAMES.some(name => fileName === name.toLowerCase())) {
|
|
255
|
+
primaryFiles.push(file);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return primaryFiles;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Export the health check function as default for easy registration
|
|
263
|
+
export default checkSpecFiles;
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Specs.json health check module
|
|
3
|
+
*
|
|
4
|
+
* This module validates the existence and structure of specs.json files in
|
|
5
|
+
* specification repositories. It ensures that essential spec metadata is
|
|
6
|
+
* present and properly formatted.
|
|
7
|
+
*
|
|
8
|
+
* @author spec-up-t-healthcheck
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createHealthCheckResult, createErrorResult } from '../health-check-utils.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The identifier for this health check, used in reports and registries.
|
|
15
|
+
* @type {string}
|
|
16
|
+
*/
|
|
17
|
+
export const CHECK_ID = 'specs-json';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Human-readable name for this health check.
|
|
21
|
+
* @type {string}
|
|
22
|
+
*/
|
|
23
|
+
export const CHECK_NAME = 'Specs.json Validation';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Description of what this health check validates.
|
|
27
|
+
* @type {string}
|
|
28
|
+
*/
|
|
29
|
+
export const CHECK_DESCRIPTION = 'Validates the existence and structure of specs.json file';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Required fields that must be present in a valid specs.json file.
|
|
33
|
+
* These fields are essential for proper spec functionality in Spec-Up-T.
|
|
34
|
+
* @type {readonly string[]}
|
|
35
|
+
*/
|
|
36
|
+
const REQUIRED_FIELDS = Object.freeze([
|
|
37
|
+
'title',
|
|
38
|
+
'description',
|
|
39
|
+
'author',
|
|
40
|
+
'spec_directory',
|
|
41
|
+
'spec_terms_directory',
|
|
42
|
+
'output_path',
|
|
43
|
+
'markdown_paths',
|
|
44
|
+
'logo',
|
|
45
|
+
'logo_link',
|
|
46
|
+
'source'
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Warning fields that should be present but missing values only trigger warnings.
|
|
51
|
+
* @type {readonly string[]}
|
|
52
|
+
*/
|
|
53
|
+
const WARNING_FIELDS = Object.freeze(['favicon']);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Optional fields that provide info if missing.
|
|
57
|
+
* @type {readonly string[]}
|
|
58
|
+
*/
|
|
59
|
+
const OPTIONAL_FIELDS = Object.freeze(['anchor_symbol', 'katex']);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validates the existence and structure of specs.json in a repository.
|
|
63
|
+
*
|
|
64
|
+
* This health check ensures that a valid specs.json file exists at the repository root
|
|
65
|
+
* and contains the required fields for proper Spec-Up-T functionality. It performs
|
|
66
|
+
* comprehensive validation of the specs.json structure including the specs array.
|
|
67
|
+
*
|
|
68
|
+
* The check performs the following validations:
|
|
69
|
+
* - File exists at repository root
|
|
70
|
+
* - File contains valid JSON
|
|
71
|
+
* - Root object contains exactly one 'specs' field
|
|
72
|
+
* - 'specs' is an array with exactly one object
|
|
73
|
+
* - Required fields are present and non-empty
|
|
74
|
+
* - Warning fields are checked (favicon)
|
|
75
|
+
* - Optional fields are noted if missing
|
|
76
|
+
* - Source object has required subfields
|
|
77
|
+
*
|
|
78
|
+
* @param {import('../providers.js').Provider} provider - The provider instance for file operations
|
|
79
|
+
* @returns {Promise<import('../health-check-utils.js').HealthCheckResult>} The health check result with validation details
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```javascript
|
|
83
|
+
* const provider = createLocalProvider('/path/to/repo');
|
|
84
|
+
* const result = await checkSpecsJson(provider);
|
|
85
|
+
* console.log(result.status); // 'pass', 'fail', or 'warn'
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export async function checkSpecsJson(provider) {
|
|
89
|
+
try {
|
|
90
|
+
// Check if specs.json exists
|
|
91
|
+
const exists = await provider.fileExists('specs.json');
|
|
92
|
+
if (!exists) {
|
|
93
|
+
return createHealthCheckResult(
|
|
94
|
+
CHECK_NAME,
|
|
95
|
+
'fail',
|
|
96
|
+
'specs.json not found in repository root',
|
|
97
|
+
{
|
|
98
|
+
suggestions: [
|
|
99
|
+
'Create a specs.json file in your repository root',
|
|
100
|
+
'Use the Spec-Up-T boilerplate as a template',
|
|
101
|
+
'Ensure the file is named exactly "specs.json"'
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Read and parse the specs.json file
|
|
108
|
+
const content = await provider.readFile('specs.json');
|
|
109
|
+
let specsData;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
specsData = JSON.parse(content);
|
|
113
|
+
} catch (parseError) {
|
|
114
|
+
return createHealthCheckResult(
|
|
115
|
+
CHECK_NAME,
|
|
116
|
+
'fail',
|
|
117
|
+
'specs.json contains invalid JSON',
|
|
118
|
+
{
|
|
119
|
+
parseError: parseError.message,
|
|
120
|
+
fileContent: content.substring(0, 500) + (content.length > 500 ? '...' : '')
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Validate basic structure
|
|
126
|
+
const structureValidation = validateBasicStructure(specsData);
|
|
127
|
+
if (!structureValidation.isValid) {
|
|
128
|
+
return createHealthCheckResult(
|
|
129
|
+
CHECK_NAME,
|
|
130
|
+
'fail',
|
|
131
|
+
structureValidation.message,
|
|
132
|
+
{ structureError: structureValidation.details }
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const spec = specsData.specs[0];
|
|
137
|
+
const validationResults = {
|
|
138
|
+
errors: [],
|
|
139
|
+
warnings: [],
|
|
140
|
+
info: [],
|
|
141
|
+
success: []
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Validate required fields
|
|
145
|
+
validateRequiredFields(spec, validationResults);
|
|
146
|
+
|
|
147
|
+
// Validate warning fields
|
|
148
|
+
validateWarningFields(spec, validationResults);
|
|
149
|
+
|
|
150
|
+
// Validate optional fields
|
|
151
|
+
validateOptionalFields(spec, validationResults);
|
|
152
|
+
|
|
153
|
+
// Validate field types and structure
|
|
154
|
+
validateFieldTypes(spec, validationResults);
|
|
155
|
+
|
|
156
|
+
// Determine overall status
|
|
157
|
+
let status = 'pass';
|
|
158
|
+
let message = 'specs.json is valid';
|
|
159
|
+
|
|
160
|
+
if (validationResults.errors.length > 0) {
|
|
161
|
+
status = 'fail';
|
|
162
|
+
message = `specs.json has ${validationResults.errors.length} error(s)`;
|
|
163
|
+
} else if (validationResults.warnings.length > 0) {
|
|
164
|
+
status = 'warn';
|
|
165
|
+
message = `specs.json is valid but has ${validationResults.warnings.length} warning(s)`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return createHealthCheckResult(
|
|
169
|
+
CHECK_NAME,
|
|
170
|
+
status,
|
|
171
|
+
message,
|
|
172
|
+
{
|
|
173
|
+
errors: validationResults.errors,
|
|
174
|
+
warnings: validationResults.warnings,
|
|
175
|
+
info: validationResults.info,
|
|
176
|
+
success: validationResults.success,
|
|
177
|
+
totalIssues: validationResults.errors.length + validationResults.warnings.length
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return createErrorResult(CHECK_NAME, error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Validates the basic structure of specs.json
|
|
188
|
+
* @param {any} data - Parsed JSON data
|
|
189
|
+
* @returns {{isValid: boolean, message?: string, details?: any}}
|
|
190
|
+
*/
|
|
191
|
+
function validateBasicStructure(data) {
|
|
192
|
+
// Check if data is an object
|
|
193
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
194
|
+
return {
|
|
195
|
+
isValid: false,
|
|
196
|
+
message: 'specs.json must contain a JSON object at root level',
|
|
197
|
+
details: { actualType: Array.isArray(data) ? 'array' : typeof data }
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check if it contains exactly one field called 'specs'
|
|
202
|
+
const keys = Object.keys(data);
|
|
203
|
+
if (keys.length !== 1) {
|
|
204
|
+
return {
|
|
205
|
+
isValid: false,
|
|
206
|
+
message: `Root object should contain exactly one field, found ${keys.length}: [${keys.join(', ')}]`,
|
|
207
|
+
details: { foundKeys: keys }
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (keys[0] !== 'specs') {
|
|
212
|
+
return {
|
|
213
|
+
isValid: false,
|
|
214
|
+
message: `Root object should contain field 'specs', found '${keys[0]}'`,
|
|
215
|
+
details: { foundKey: keys[0] }
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check if specs is an array
|
|
220
|
+
if (!Array.isArray(data.specs)) {
|
|
221
|
+
return {
|
|
222
|
+
isValid: false,
|
|
223
|
+
message: 'Field "specs" should be an array',
|
|
224
|
+
details: { actualType: typeof data.specs }
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check if array contains exactly one object
|
|
229
|
+
if (data.specs.length !== 1) {
|
|
230
|
+
return {
|
|
231
|
+
isValid: false,
|
|
232
|
+
message: `Field "specs" should contain exactly one object, found ${data.specs.length}`,
|
|
233
|
+
details: { arrayLength: data.specs.length }
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check if the single item is an object
|
|
238
|
+
if (typeof data.specs[0] !== 'object' || data.specs[0] === null || Array.isArray(data.specs[0])) {
|
|
239
|
+
return {
|
|
240
|
+
isValid: false,
|
|
241
|
+
message: 'The item in "specs" array should be an object',
|
|
242
|
+
details: { actualType: Array.isArray(data.specs[0]) ? 'array' : typeof data.specs[0] }
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { isValid: true };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Validates required fields
|
|
251
|
+
* @param {Object} spec - The spec object to validate
|
|
252
|
+
* @param {Object} results - Results accumulator
|
|
253
|
+
*/
|
|
254
|
+
function validateRequiredFields(spec, results) {
|
|
255
|
+
REQUIRED_FIELDS.forEach(field => {
|
|
256
|
+
if (!(field in spec)) {
|
|
257
|
+
results.errors.push(`Required field "${field}" is missing`);
|
|
258
|
+
} else if (spec[field] === null || spec[field] === undefined || spec[field] === '') {
|
|
259
|
+
results.errors.push(`Required field "${field}" is empty or null`);
|
|
260
|
+
} else if (Array.isArray(spec[field]) && spec[field].length === 0) {
|
|
261
|
+
results.errors.push(`Required field "${field}" is an empty array`);
|
|
262
|
+
} else {
|
|
263
|
+
results.success.push(`Required field "${field}" is present and valid`);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Validates warning fields
|
|
270
|
+
* @param {Object} spec - The spec object to validate
|
|
271
|
+
* @param {Object} results - Results accumulator
|
|
272
|
+
*/
|
|
273
|
+
function validateWarningFields(spec, results) {
|
|
274
|
+
WARNING_FIELDS.forEach(field => {
|
|
275
|
+
if (!(field in spec)) {
|
|
276
|
+
results.warnings.push(`Recommended field "${field}" is missing`);
|
|
277
|
+
} else if (spec[field] === null || spec[field] === undefined || spec[field] === '') {
|
|
278
|
+
results.warnings.push(`Recommended field "${field}" is empty or null`);
|
|
279
|
+
} else {
|
|
280
|
+
results.success.push(`Recommended field "${field}" is present and valid`);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Validates optional fields
|
|
287
|
+
* @param {Object} spec - The spec object to validate
|
|
288
|
+
* @param {Object} results - Results accumulator
|
|
289
|
+
*/
|
|
290
|
+
function validateOptionalFields(spec, results) {
|
|
291
|
+
OPTIONAL_FIELDS.forEach(field => {
|
|
292
|
+
if (!(field in spec)) {
|
|
293
|
+
results.info.push(`Optional field "${field}" is not set (this is acceptable)`);
|
|
294
|
+
} else {
|
|
295
|
+
results.success.push(`Optional field "${field}" is present`);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Validates specific field types and formats
|
|
302
|
+
* @param {Object} spec - The spec object to validate
|
|
303
|
+
* @param {Object} results - Results accumulator
|
|
304
|
+
*/
|
|
305
|
+
function validateFieldTypes(spec, results) {
|
|
306
|
+
// Validate markdown_paths is array of strings
|
|
307
|
+
if (spec.markdown_paths && Array.isArray(spec.markdown_paths)) {
|
|
308
|
+
if (spec.markdown_paths.every(item => typeof item === 'string')) {
|
|
309
|
+
results.success.push('Field "markdown_paths" contains valid string array');
|
|
310
|
+
} else {
|
|
311
|
+
results.errors.push('Field "markdown_paths" should contain only strings');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Validate source object structure
|
|
316
|
+
if (spec.source && typeof spec.source === 'object') {
|
|
317
|
+
const requiredSourceFields = ['host', 'account', 'repo', 'branch'];
|
|
318
|
+
let sourceValid = true;
|
|
319
|
+
|
|
320
|
+
requiredSourceFields.forEach(field => {
|
|
321
|
+
if (!(field in spec.source) || !spec.source[field]) {
|
|
322
|
+
results.errors.push(`Source field "${field}" is missing or empty`);
|
|
323
|
+
sourceValid = false;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
if (sourceValid) {
|
|
328
|
+
results.success.push('Source object structure is valid');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Validate external_specs if present
|
|
333
|
+
if (spec.external_specs) {
|
|
334
|
+
if (Array.isArray(spec.external_specs)) {
|
|
335
|
+
spec.external_specs.forEach((extSpec, index) => {
|
|
336
|
+
const requiredExtFields = ['external_spec', 'gh_page', 'url', 'terms_dir'];
|
|
337
|
+
requiredExtFields.forEach(field => {
|
|
338
|
+
if (!(field in extSpec) || !extSpec[field]) {
|
|
339
|
+
results.errors.push(`External spec ${index} missing "${field}"`);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
results.success.push(`External specs array contains ${spec.external_specs.length} entries`);
|
|
344
|
+
} else {
|
|
345
|
+
results.errors.push('Field "external_specs" should be an array');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Validate katex is boolean if present
|
|
350
|
+
if ('katex' in spec && typeof spec.katex !== 'boolean') {
|
|
351
|
+
results.errors.push('Field "katex" should be a boolean value');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Export as default for backward compatibility
|
|
356
|
+
export default {
|
|
357
|
+
CHECK_ID,
|
|
358
|
+
CHECK_NAME,
|
|
359
|
+
CHECK_DESCRIPTION,
|
|
360
|
+
checkSpecsJson
|
|
361
|
+
};
|