Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable changes to this project will be documented in this file.

## [Unreleased]

### Added
- **todo-fixme drift checker** — flags unresolved `TODO` / `FIXME` markers in scaffold markdown.

### Changed
- README and CONTRIBUTING now list all 10 drift checkers (including `tool-config-sync` and `todo-fixme`).

## [0.3.5] - 2026-05-14

### Added
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Thanks for your interest in contributing! Here's how to get started.

**New here?** The best starting point is an issue labeled [`good first issue`](https://github.com/theDakshJaitly/mex/labels/good%20first%20issue) — most are self-contained drift checkers, and there are 9 existing checkers to copy from. See [Adding a drift checker](#adding-a-drift-checker) below.
**New here?** The best starting point is an issue labeled [`good first issue`](https://github.com/theDakshJaitly/mex/labels/good%20first%20issue) — most are self-contained drift checkers, and there are 10 existing checkers to copy from. See [Adding a drift checker](#adding-a-drift-checker) below.

## Setup

Expand Down Expand Up @@ -50,7 +50,7 @@ test/ # Vitest tests

## Adding a drift checker

New checkers are the most newcomer-friendly contribution. A checker is a small function that inspects scaffold files (or extracted claims) and returns `DriftIssue[]`. There are 9 existing checkers in `src/drift/checkers/` — pick the closest as a template.
New checkers are the most newcomer-friendly contribution. A checker is a small function that inspects scaffold files (or extracted claims) and returns `DriftIssue[]`. There are 10 existing checkers in `src/drift/checkers/` — pick the closest as a template.

1. **Create `src/drift/checkers/<name>.ts`.** There are two shapes:
- **Claim-based** — operates on extracted claims, e.g. `checkPaths(claims, projectRoot, scaffoldRoot)` in `path.ts`.
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Editable source: [docs/diagrams/context-routing.excalidraw](docs/diagrams/contex

## Drift Detection

Eight checkers validate your scaffold against the real codebase. Zero tokens, zero AI.
Ten checkers validate your scaffold against the real codebase. Zero tokens, zero AI.

| Checker | What it catches |
|---------|----------------|
Expand All @@ -108,6 +108,8 @@ Eight checkers validate your scaffold against the real codebase. Zero tokens, ze
| **dependency** | Claimed dependencies missing from `package.json` |
| **cross-file** | Same dependency with different versions across files |
| **script-coverage** | `package.json` scripts not mentioned in any scaffold file |
| **tool-config-sync** | Installed AI tool config files (e.g. `CLAUDE.md`, `.cursorrules`) out of sync with each other |
| **todo-fixme** | Unresolved `TODO` / `FIXME` markers left in scaffold markdown |

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

Expand Down
42 changes: 42 additions & 0 deletions src/drift/checkers/todo-fixme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { readFileSync } from "node:fs";
import { relative } from "node:path";
import type { DriftIssue } from "../../types.js";

const MARKER_RE = /\b(TODO|FIXME)\b/g;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, just flagging two behaviors of the match for posterity:

  • False positives: this matches TODO/FIXME literally anywhere, including inside fenced code blocks or a patterns/*.md that documents the convention (e.g. an example showing // TODO). That's exactly what Add a TODO/FIXME drift checker #54 asked for (warning per occurrence), so it's correct as-is — just noting that code-fence/inline-code skipping could be a nice v2 to cut noise.
  • Case-sensitive: only uppercase TODO/FIXME match; todo/Fixme are ignored. That's the conventional marker form, so probably intended — confirming it was a deliberate choice.

(FYI the MARKER_RE.lastIndex = 0 reset per line is a good defensive touch on a reused /g regex.)


/** Scan scaffold markdown for unresolved TODO/FIXME markers. */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny cosmetic note: the comment says "scaffold markdown," but scaffoldFiles can include root .cursorrules / .windsurfrules, which aren't markdown. Harmless — the warning message reads fine either way.

export function checkTodoFixme(
scaffoldFiles: string[],
projectRoot: string
): DriftIssue[] {
const issues: DriftIssue[] = [];

for (const filePath of scaffoldFiles) {
const source = relative(projectRoot, filePath);
let content: string;
try {
content = readFileSync(filePath, "utf-8");
} catch {
continue;
}

const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
MARKER_RE.lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = MARKER_RE.exec(line)) !== null) {
const marker = match[1];
issues.push({
code: "TODO_FIXME",
severity: "warning",
file: source,
line: i + 1,
message: `Unresolved ${marker} marker in scaffold`,
});
}
}
}

return issues;
}
5 changes: 5 additions & 0 deletions src/drift/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { checkDependencies } from "./checkers/dependency.js";
import { checkCrossFile } from "./checkers/cross-file.js";
import { checkScriptCoverage } from "./checkers/script-coverage.js";
import { checkToolConfigSync } from "./checkers/tool-config-sync.js";
import { checkTodoFixme } from "./checkers/todo-fixme.js";

/**
* Default glob patterns used to locate scaffold markdown files, relative to
Expand Down Expand Up @@ -119,6 +120,10 @@ export async function runDriftCheck(
allIssues.push(...toolConfigSyncIssues);
checkerIssueCounts.push(["tool-config-sync", toolConfigSyncIssues.length]);

const todoFixmeIssues = checkTodoFixme(scaffoldFiles, projectRoot);
allIssues.push(...todoFixmeIssues);
checkerIssueCounts.push(["todo-fixme", todoFixmeIssues.length]);

const score = computeScore(allIssues);
const verboseLog = opts.verbose
? buildVerboseLog(scaffoldFiles.length, allClaims, checkerIssueCounts)
Expand Down
2 changes: 2 additions & 0 deletions src/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ function remediationFor(code: DriftIssue["code"]): string | null {
return "Document the script in AGENTS.md, SETUP.md, or context/setup.md.";
case "TOOL_CONFIG_DRIFT":
return "Copy the intended tool config text across installed agent config files.";
case "TODO_FIXME":
return "Resolve the TODO/FIXME or remove the marker from the scaffold.";
default:
return null;
}
Expand Down
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ export type IssueCode =
| "INDEX_MISSING_ENTRY"
| "INDEX_ORPHAN_ENTRY"
| "UNDOCUMENTED_SCRIPT"
| "TOOL_CONFIG_DRIFT";
| "TOOL_CONFIG_DRIFT"
| "TODO_FIXME";

export interface DriftIssue {
code: IssueCode;
Expand Down
44 changes: 44 additions & 0 deletions test/checkers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { checkDependencies } from "../src/drift/checkers/dependency.js";
import { checkCrossFile } from "../src/drift/checkers/cross-file.js";
import { checkIndexSync } from "../src/drift/checkers/index-sync.js";
import { checkToolConfigSync } from "../src/drift/checkers/tool-config-sync.js";
import { checkTodoFixme } from "../src/drift/checkers/todo-fixme.js";
import type { Claim, ScaffoldFrontmatter } from "../src/types.js";

vi.mock("../src/git.js", () => ({
Expand Down Expand Up @@ -424,3 +425,46 @@ describe("checkToolConfigSync", () => {
expect(issues[0].file).toBe(".github/copilot-instructions.md");
});
});

// ── TODO/FIXME Checker ──

describe("checkTodoFixme", () => {
it("flags TODO and FIXME with file and line", () => {
const file = join(tmpDir, "context/notes.md");
mkdirSync(join(tmpDir, "context"), { recursive: true });
writeFileSync(
file,
"# Notes\n\n- TODO: wire auth\n\n## Later\n\nFIXME: broken link in ROUTER\n"
);
const issues = checkTodoFixme([file], tmpDir);
expect(issues).toHaveLength(2);
expect(issues[0]).toMatchObject({
code: "TODO_FIXME",
severity: "warning",
file: "context/notes.md",
line: 3,
message: "Unresolved TODO marker in scaffold",
});
expect(issues[1]).toMatchObject({
code: "TODO_FIXME",
file: "context/notes.md",
line: 7,
message: "Unresolved FIXME marker in scaffold",
});
});

it("returns empty when scaffold files have no markers", () => {
const file = join(tmpDir, "ROUTER.md");
writeFileSync(file, "# Router\n\nAll tasks done.\n");
const issues = checkTodoFixme([file], tmpDir);
expect(issues).toHaveLength(0);
});

it("flags multiple markers on the same line separately", () => {
const file = join(tmpDir, "SETUP.md");
writeFileSync(file, "TODO: a FIXME: b\n");
const issues = checkTodoFixme([file], tmpDir);
expect(issues).toHaveLength(2);
expect(issues.map((i) => i.line)).toEqual([1, 1]);
});
});
Loading