Skip to content

Commit fdd6c99

Browse files
rootcursoragent
andcommitted
feat: add frontmatter-completeness drift checker
Warn when context/ or patterns/ files lack recommended name, description, or last_updated. Closes #53. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 63f76e4 commit fdd6c99

6 files changed

Lines changed: 81 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ All notable changes to this project will be documented in this file.
66

77
### Added
88
- **todo-fixme drift checker** — flags unresolved `TODO` / `FIXME` markers in scaffold markdown.
9+
- **frontmatter-completeness drift checker** — warns when `context/` or `patterns/` files lack recommended `name`, `description`, or `last_updated` frontmatter.
910

1011
### Changed
11-
- README and CONTRIBUTING now list all 10 drift checkers (including `tool-config-sync` and `todo-fixme`).
12+
- README and CONTRIBUTING now list all drift checkers (including `tool-config-sync`, `todo-fixme`, and `frontmatter-completeness`).
1213

1314
## [0.3.5] - 2026-05-14
1415

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ Editable source: [docs/diagrams/context-routing.excalidraw](docs/diagrams/contex
9696

9797
## Drift Detection
9898

99-
Ten checkers validate your scaffold against the real codebase. Zero tokens, zero AI.
99+
Eleven checkers validate your scaffold against the real codebase. Zero tokens, zero AI.
100100

101101
| Checker | What it catches |
102102
|---------|----------------|
@@ -110,6 +110,7 @@ Ten checkers validate your scaffold against the real codebase. Zero tokens, zero
110110
| **script-coverage** | `package.json` scripts not mentioned in any scaffold file |
111111
| **tool-config-sync** | Installed AI tool config files (e.g. `CLAUDE.md`, `.cursorrules`) out of sync with each other |
112112
| **todo-fixme** | Unresolved `TODO` / `FIXME` markers left in scaffold markdown |
113+
| **frontmatter-completeness** | Missing `name`, `description`, or `last_updated` in `context/` and `patterns/` files |
113114

114115
Scoring starts at 100. mex deducts 10 per error, 3 per warning, and 1 per info.
115116

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { DriftIssue, ScaffoldFrontmatter } from "../../types.js";
2+
3+
const RECOMMENDED_FIELDS = ["name", "description", "last_updated"] as const;
4+
5+
/** Warn when context/ or patterns/ files lack recommended frontmatter fields. */
6+
export function checkFrontmatterCompleteness(
7+
frontmatter: ScaffoldFrontmatter | null,
8+
source: string
9+
): DriftIssue[] {
10+
if (!isContextOrPatternFile(source)) return [];
11+
12+
const issues: DriftIssue[] = [];
13+
const fm = frontmatter ?? {};
14+
15+
for (const field of RECOMMENDED_FIELDS) {
16+
const value = fm[field];
17+
if (typeof value !== "string" || value.trim() === "") {
18+
issues.push({
19+
code: "INCOMPLETE_FRONTMATTER",
20+
severity: "warning",
21+
file: source,
22+
line: null,
23+
message: `Missing recommended frontmatter field: ${field}`,
24+
});
25+
}
26+
}
27+
28+
return issues;
29+
}
30+
31+
function isContextOrPatternFile(source: string): boolean {
32+
return source.startsWith("context/") || source.startsWith("patterns/");
33+
}

src/drift/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { checkCrossFile } from "./checkers/cross-file.js";
1515
import { checkScriptCoverage } from "./checkers/script-coverage.js";
1616
import { checkToolConfigSync } from "./checkers/tool-config-sync.js";
1717
import { checkTodoFixme } from "./checkers/todo-fixme.js";
18+
import { checkFrontmatterCompleteness } from "./checkers/frontmatter-completeness.js";
1819

1920
/**
2021
* Default glob patterns used to locate scaffold markdown files, relative to
@@ -75,6 +76,9 @@ export async function runDriftCheck(
7576
const edgeIssues = checkEdges(frontmatter, filePath, source, projectRoot, scaffoldRoot);
7677
allIssues.push(...edgeIssues);
7778

79+
const frontmatterIssues = checkFrontmatterCompleteness(frontmatter, source);
80+
allIssues.push(...frontmatterIssues);
81+
7882
// Staleness check
7983
const stalenessIssues = await checkStaleness(
8084
source,
@@ -86,6 +90,7 @@ export async function runDriftCheck(
8690
allIssues.push(...stalenessIssues);
8791

8892
checkerIssueCounts.push([`edges:${source}`, edgeIssues.length]);
93+
checkerIssueCounts.push([`frontmatter-completeness:${source}`, frontmatterIssues.length]);
8994
checkerIssueCounts.push([`staleness:${source}`, stalenessIssues.length]);
9095
}
9196

src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ export type IssueCode =
9595
| "INDEX_ORPHAN_ENTRY"
9696
| "UNDOCUMENTED_SCRIPT"
9797
| "TOOL_CONFIG_DRIFT"
98-
| "TODO_FIXME";
98+
| "TODO_FIXME"
99+
| "INCOMPLETE_FRONTMATTER";
99100

100101
export interface DriftIssue {
101102
code: IssueCode;

test/checkers.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { checkCrossFile } from "../src/drift/checkers/cross-file.js";
1515
import { checkIndexSync } from "../src/drift/checkers/index-sync.js";
1616
import { checkToolConfigSync } from "../src/drift/checkers/tool-config-sync.js";
1717
import { checkTodoFixme } from "../src/drift/checkers/todo-fixme.js";
18+
import { checkFrontmatterCompleteness } from "../src/drift/checkers/frontmatter-completeness.js";
1819
import type { Claim, ScaffoldFrontmatter } from "../src/types.js";
1920

2021
vi.mock("../src/git.js", () => ({
@@ -468,3 +469,39 @@ describe("checkTodoFixme", () => {
468469
expect(issues.map((i) => i.line)).toEqual([1, 1]);
469470
});
470471
});
472+
473+
// ── Frontmatter completeness ──
474+
475+
describe("checkFrontmatterCompleteness", () => {
476+
it("warns on missing recommended fields in context/", () => {
477+
const issues = checkFrontmatterCompleteness(
478+
{ name: "Auth" },
479+
"context/auth.md"
480+
);
481+
expect(issues).toHaveLength(2);
482+
expect(issues.every((i) => i.severity === "warning")).toBe(true);
483+
expect(issues.map((i) => i.message)).toEqual(
484+
expect.arrayContaining([
485+
"Missing recommended frontmatter field: description",
486+
"Missing recommended frontmatter field: last_updated",
487+
])
488+
);
489+
});
490+
491+
it("passes when all recommended fields are present", () => {
492+
const issues = checkFrontmatterCompleteness(
493+
{
494+
name: "Auth",
495+
description: "Authentication overview",
496+
last_updated: "2026-06-01",
497+
},
498+
"patterns/auth.md"
499+
);
500+
expect(issues).toHaveLength(0);
501+
});
502+
503+
it("ignores ROUTER.md and other scaffold files", () => {
504+
const issues = checkFrontmatterCompleteness(null, "ROUTER.md");
505+
expect(issues).toHaveLength(0);
506+
});
507+
});

0 commit comments

Comments
 (0)