Skip to content
This repository was archived by the owner on Oct 10, 2025. It is now read-only.

Commit fec9710

Browse files
committed
feat: add missing fields collection
1 parent ac5faa1 commit fec9710

File tree

2 files changed

+82
-24
lines changed

2 files changed

+82
-24
lines changed

linters/typescript/src/context_linter.ts

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,21 @@ import matter from 'gray-matter';
66
import { ContextdocsLinter } from './contextdocs_linter';
77
import { ContextignoreLinter } from './contextignore_linter';
88
import { getContextFiles, lintFileIfExists, fileExists, printHeader } from './utils/file_utils';
9-
import { ContextValidator } from './utils/validator';
9+
import { ContextValidator, ValidationResult } from './utils/validator';
1010

1111
export class ContextLinter {
1212
private md: MarkdownIt;
1313
private contextdocsLinter: ContextdocsLinter;
1414
private contextignoreLinter: ContextignoreLinter;
1515
private contextValidator: ContextValidator;
16+
private missingFieldsSummary: Map<string, string[]>;
1617

1718
constructor() {
1819
this.md = new MarkdownIt();
1920
this.contextdocsLinter = new ContextdocsLinter();
2021
this.contextignoreLinter = new ContextignoreLinter();
2122
this.contextValidator = new ContextValidator();
23+
this.missingFieldsSummary = new Map();
2224
}
2325

2426
public async lintDirectory(directoryPath: string, packageVersion: string): Promise<boolean> {
@@ -29,6 +31,15 @@ export class ContextLinter {
2931
isValid = await this.handleContextFilesRecursively(directoryPath) && isValid;
3032

3133
console.log('\nLinting completed.');
34+
35+
if (this.missingFieldsSummary.size > 0) {
36+
console.log('\nMissing Fields Summary:');
37+
for (const [file, missingFields] of this.missingFieldsSummary.entries()) {
38+
console.log(` ${file}:`);
39+
console.log(` ${missingFields.join(', ')}`);
40+
}
41+
}
42+
3243
return isValid;
3344
}
3445

@@ -69,16 +80,18 @@ export class ContextLinter {
6980
private async lintContextFile(filePath: string): Promise<boolean> {
7081
console.log(`\nLinting file: ${filePath}`);
7182
return await lintFileIfExists(filePath, async (fileContent) => {
72-
let isValid = false;
83+
let result: ValidationResult;
7384
if (filePath.endsWith('.context.md')) {
74-
isValid = await this.lintMarkdownFile(fileContent);
85+
result = await this.lintMarkdownFile(fileContent, filePath);
7586
} else if (filePath.endsWith('.context.yaml')) {
76-
isValid = await this.lintYamlFile(fileContent);
87+
result = await this.lintYamlFile(fileContent, filePath);
7788
} else if (filePath.endsWith('.context.json')) {
78-
isValid = await this.lintJsonFile(fileContent);
89+
result = await this.lintJsonFile(fileContent, filePath);
90+
} else {
91+
result = { isValid: false, coveragePercentage: 0, missingFields: [] };
7992
}
80-
this.printValidationResult(isValid, filePath);
81-
return isValid;
93+
this.printValidationResult(result.isValid, filePath);
94+
return result.isValid;
8295
}) || false;
8396
}
8497

@@ -91,18 +104,27 @@ export class ContextLinter {
91104
}
92105
}
93106

94-
private async lintMarkdownFile(content: string): Promise<boolean> {
107+
private async lintMarkdownFile(content: string, filePath: string): Promise<ValidationResult> {
95108
console.log(' - Validating Markdown structure');
96109
console.log(' - Checking YAML frontmatter');
97110

98111
try {
99112
const { data: frontmatterData, content: markdownContent } = matter(content);
100-
const frontmatterValid = await this.contextValidator.validateContextData(frontmatterData, 'markdown');
113+
const validationResult = this.contextValidator.validateContextData(frontmatterData, 'markdown');
101114
const markdownValid = this.validateMarkdownContent(markdownContent.trim());
102-
return frontmatterValid && markdownValid;
115+
116+
if (validationResult.missingFields.length > 0) {
117+
this.missingFieldsSummary.set(path.basename(filePath), validationResult.missingFields);
118+
}
119+
120+
return {
121+
isValid: validationResult.isValid && markdownValid,
122+
coveragePercentage: validationResult.coveragePercentage,
123+
missingFields: validationResult.missingFields
124+
};
103125
} catch (error) {
104126
console.error(` Error parsing Markdown file: ${error}`);
105-
return false;
127+
return { isValid: false, coveragePercentage: 0, missingFields: [] };
106128
}
107129
}
108130

@@ -138,35 +160,47 @@ export class ContextLinter {
138160
return isValid;
139161
}
140162

141-
private async lintYamlFile(content: string): Promise<boolean> {
163+
private async lintYamlFile(content: string, filePath: string): Promise<ValidationResult> {
142164
console.log(' - Validating YAML structure');
143165

144166
try {
145167
const yamlData = this.parseYaml(content);
146-
return await this.contextValidator.validateContextData(yamlData, 'yaml');
168+
const validationResult = this.contextValidator.validateContextData(yamlData, 'yaml');
169+
170+
if (validationResult.missingFields.length > 0) {
171+
this.missingFieldsSummary.set(path.basename(filePath), validationResult.missingFields);
172+
}
173+
174+
return validationResult;
147175
} catch (error) {
148176
if (error instanceof yaml.YAMLException) {
149177
console.error(` Error parsing YAML file: ${this.formatYamlError(error)}`);
150178
} else {
151179
console.error(` Error parsing YAML file: ${error}`);
152180
}
153-
return false;
181+
return { isValid: false, coveragePercentage: 0, missingFields: [] };
154182
}
155183
}
156184

157-
private async lintJsonFile(content: string): Promise<boolean> {
185+
private async lintJsonFile(content: string, filePath: string): Promise<ValidationResult> {
158186
console.log(' - Validating JSON structure');
159187

160188
try {
161189
const jsonData = JSON.parse(content) as Record<string, unknown>;
162-
return await this.contextValidator.validateContextData(jsonData, 'json');
190+
const validationResult = this.contextValidator.validateContextData(jsonData, 'json');
191+
192+
if (validationResult.missingFields.length > 0) {
193+
this.missingFieldsSummary.set(path.basename(filePath), validationResult.missingFields);
194+
}
195+
196+
return validationResult;
163197
} catch (error) {
164198
if (error instanceof SyntaxError) {
165199
console.error(` Error parsing JSON file: ${this.formatJsonError(error, content)}`);
166200
} else {
167201
console.error(` Error parsing JSON file: ${error}`);
168202
}
169-
return false;
203+
return { isValid: false, coveragePercentage: 0, missingFields: [] };
170204
}
171205
}
172206

linters/typescript/src/utils/validator.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import { kebabToCamelCase } from './string_utils';
22
import { allowedTopLevelFields, sectionChecks, listTypes, stringTypes, directoryTypes } from './context_structure';
33

4+
export interface ValidationResult {
5+
isValid: boolean;
6+
coveragePercentage: number;
7+
missingFields: string[];
8+
}
9+
410
export class ContextValidator {
511
private kebabToCamelCache: Map<string, string>;
612

713
constructor() {
814
this.kebabToCamelCache = new Map();
915
}
1016

11-
public validateContextData(data: Record<string, unknown>, format: 'markdown' | 'yaml' | 'json'): boolean {
17+
public validateContextData(data: Record<string, unknown>, format: 'markdown' | 'yaml' | 'json'): ValidationResult {
1218
const isJson = format === 'json';
1319
const coveredFields = new Set<string>();
1420
let isValid = true;
21+
const allMissingFields: string[] = [];
1522

1623
for (const [field, value] of Object.entries(data)) {
1724
const normalizedField = isJson ? kebabToCamelCase(field, this.kebabToCamelCache) : field;
@@ -25,16 +32,22 @@ export class ContextValidator {
2532
}
2633
}
2734

28-
const coveragePercentage = (coveredFields.size / allowedTopLevelFields.size) * 100;
35+
const { coveragePercentage, missingFields } = this.calculateCoverage(coveredFields, allowedTopLevelFields);
36+
allMissingFields.push(...missingFields);
2937
console.log(` Context coverage: ${coveragePercentage.toFixed(2)}% (${coveredFields.size}/${allowedTopLevelFields.size} fields)`);
38+
if (missingFields.length > 0) {
39+
console.log(` Missing top-level fields: ${missingFields.join(', ')}`);
40+
}
3041

3142
for (const section of Object.keys(sectionChecks)) {
3243
if (section in data) {
33-
isValid = this.validateSectionFields(section, data[section] as Record<string, unknown>, isJson) && isValid;
44+
const sectionResult = this.validateSectionFields(section, data[section] as Record<string, unknown>, isJson);
45+
isValid = sectionResult.isValid && isValid;
46+
allMissingFields.push(...sectionResult.missingFields);
3447
}
3548
}
3649

37-
return isValid;
50+
return { isValid, coveragePercentage, missingFields: allMissingFields };
3851
}
3952

4053
private validateField(field: string, value: unknown, isJson: boolean): boolean {
@@ -51,7 +64,7 @@ export class ContextValidator {
5164
return isValid;
5265
}
5366

54-
private validateSectionFields(sectionName: string, data: Record<string, unknown>, isJson: boolean): boolean {
67+
private validateSectionFields(sectionName: string, data: Record<string, unknown>, isJson: boolean): ValidationResult {
5568
const checks = sectionChecks[sectionName];
5669
const coveredFields = new Set<string>();
5770
let isValid = true;
@@ -69,10 +82,21 @@ export class ContextValidator {
6982
}
7083
}
7184

72-
const coveragePercentage = (coveredFields.size / checks.size) * 100;
85+
const { coveragePercentage, missingFields } = this.calculateCoverage(coveredFields, checks);
7386
console.log(` ${sectionName} coverage: ${coveragePercentage.toFixed(2)}% (${coveredFields.size}/${checks.size} fields)`);
87+
if (missingFields.length > 0) {
88+
console.log(` Missing fields in '${sectionName}' section: ${missingFields.join(', ')}`);
89+
}
90+
91+
return { isValid, coveragePercentage, missingFields };
7492
}
7593

76-
return isValid;
94+
return { isValid: true, coveragePercentage: 100, missingFields: [] };
95+
}
96+
97+
private calculateCoverage(coveredFields: Set<string>, expectedFields: Set<string>): { coveragePercentage: number, missingFields: string[] } {
98+
const missingFields = Array.from(expectedFields).filter(field => !coveredFields.has(field));
99+
const coveragePercentage = (coveredFields.size / expectedFields.size) * 100;
100+
return { coveragePercentage, missingFields };
77101
}
78102
}

0 commit comments

Comments
 (0)