|
1 | 1 | --- |
2 | 2 | name: sce-drift-analyzer |
3 | | -description: Use when user wants to analyze drift between context and code using structured collectors. |
| 3 | +description: Compares project documentation against actual code implementation to identify outdated, missing, or mismatched content — then produces a prioritized report with actionable fixes. Use when the user says docs are out of date, wants to sync documentation with code, suspects the spec no longer matches implementation, notices code comments or context files are stale, or asks whether documentation reflects the current codebase. Third-person: analyzes documentation-vs-implementation alignment using JavaScript collectors, writes a structured drift report, and asks the user before applying any changes. |
4 | 4 | compatibility: opencode |
5 | 5 | --- |
6 | 6 |
|
7 | 7 | ## What I do |
8 | 8 | - Collect context and code signals with pure JavaScript collectors. |
9 | | -- Analyze semantic drift between documented state and implemented state. |
| 9 | +- Compare documented state against implemented state to find mismatches. |
10 | 10 | - Produce a clear drift report with actionable fixes. |
11 | 11 | - Ask the user what to do next before making edits. |
12 | 12 |
|
13 | 13 | ## How to run this |
14 | 14 | - If `context/` is missing, ask once whether to bootstrap SCE baseline. |
15 | 15 | - If yes, create baseline and continue. |
16 | 16 | - If no, stop and explain drift analysis requires `context/`. |
17 | | -- Collect data: |
| 17 | +- Collect data using standard Node.js APIs: |
18 | 18 |
|
19 | 19 | ```javascript |
20 | | -const collectors = require("../../lib/drift-collectors.js"); |
21 | | -const data = await collectors.collectAll(process.cwd(), { |
22 | | - sources: ["context", "code"], |
23 | | -}); |
| 20 | +const fs = require("fs"); |
| 21 | +const path = require("path"); |
| 22 | +const { execSync } = require("child_process"); |
| 23 | + |
| 24 | +const root = process.cwd(); |
| 25 | + |
| 26 | +// --- Collect context/ claims, paths, topics, and completed tasks --- |
| 27 | +function readContextFiles(contextDir) { |
| 28 | + const results = { claims: [], paths: [], topics: [], completedTasks: [] }; |
| 29 | + if (!fs.existsSync(contextDir)) return results; |
| 30 | + |
| 31 | + const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).forEach(entry => { |
| 32 | + const full = path.join(dir, entry.name); |
| 33 | + if (entry.isDirectory()) return walk(full); |
| 34 | + if (!entry.name.endsWith(".md")) return; |
| 35 | + const text = fs.readFileSync(full, "utf8"); |
| 36 | + const lines = text.split(" |
| 37 | +"); |
| 38 | + lines.forEach((line, i) => { |
| 39 | + // Collect quoted file/path claims (e.g. "handled by src/foo.ts") |
| 40 | + const pathClaim = line.match(/"([^"]*\.[a-z]{2,4})"/i); |
| 41 | + if (pathClaim) results.claims.push({ file: full, line: i + 1, raw: line.trim(), ref: pathClaim[1] }); |
| 42 | + // Collect explicit path mentions (src/..., lib/..., etc.) |
| 43 | + const pathRef = line.match(/\b(src|lib|app|test|tests|dist)\/[\w\/\.\-]+/); |
| 44 | + if (pathRef) results.paths.push({ file: full, line: i + 1, ref: pathRef[0] }); |
| 45 | + // Collect completed task markers |
| 46 | + if (/^\s*-\s*\[x\]/i.test(line)) results.completedTasks.push({ file: full, line: i + 1, raw: line.trim() }); |
| 47 | + // Collect section headings as documented topics |
| 48 | + const heading = line.match(/^#{1,3}\s+(.+)/); |
| 49 | + if (heading) results.topics.push(heading[1].trim()); |
| 50 | + }); |
| 51 | + }); |
| 52 | + walk(contextDir); |
| 53 | + return results; |
| 54 | +} |
| 55 | + |
| 56 | +// --- Collect exported symbols and module paths from source code --- |
| 57 | +function readCodeSymbols(srcDirs) { |
| 58 | + const symbols = []; // { name, file } |
| 59 | + const allPaths = new Set(); |
| 60 | + srcDirs.forEach(dir => { |
| 61 | + if (!fs.existsSync(dir)) return; |
| 62 | + const walk = (d) => fs.readdirSync(d, { withFileTypes: true }).forEach(entry => { |
| 63 | + const full = path.join(d, entry.name); |
| 64 | + if (entry.isDirectory()) return walk(full); |
| 65 | + const rel = path.relative(root, full); |
| 66 | + allPaths.add(rel); |
| 67 | + if (!/\.(js|ts|mjs|cjs)$/.test(entry.name)) return; |
| 68 | + const text = fs.readFileSync(full, "utf8"); |
| 69 | + // Named exports: export function foo, export const foo, export class Foo |
| 70 | + const exportMatches = text.matchAll(/^export\s+(?:async\s+)?(?:function|const|class|let|var)\s+(\w+)/gm); |
| 71 | + for (const m of exportMatches) symbols.push({ name: m[1], file: rel }); |
| 72 | + // module.exports.foo = ... or exports.foo = ... |
| 73 | + const cjsMatches = text.matchAll(/(?:module\.exports|exports)\.(\w+)\s*=/g); |
| 74 | + for (const m of cjsMatches) symbols.push({ name: m[1], file: rel }); |
| 75 | + }); |
| 76 | + }); |
| 77 | + return { symbols, allPaths }; |
| 78 | +} |
| 79 | + |
| 80 | +const context = readContextFiles(path.join(root, "context")); |
| 81 | +const code = readCodeSymbols(["src", "lib", "app"].map(d => path.join(root, d))); |
24 | 82 | ``` |
25 | 83 |
|
26 | 84 | - Analyze for these drift classes: |
27 | | - - missing documentation (code capability not represented in `context/`) |
28 | | - - outdated context (context claim no longer matches code) |
29 | | - - structure drift (paths and boundaries changed) |
30 | | - - completion drift (checked tasks with no supporting implementation) |
| 85 | + |
| 86 | + - **Missing documentation** - a function or module exists in code but has no corresponding entry in `context/` |
| 87 | + ```javascript |
| 88 | + const undocumented = code.symbols.filter( |
| 89 | + sym => !context.topics.some(t => t.toLowerCase().includes(sym.name.toLowerCase())) |
| 90 | + ); |
| 91 | + ``` |
| 92 | + |
| 93 | + - **Outdated context** - a path or file referenced in `context/` no longer exists on disk |
| 94 | + ```javascript |
| 95 | + const stale = context.claims.filter( |
| 96 | + claim => { |
| 97 | + const abs = path.resolve(root, claim.ref); |
| 98 | + return !fs.existsSync(abs) && !code.allPaths.has(claim.ref); |
| 99 | + } |
| 100 | + ); |
| 101 | + ``` |
| 102 | + |
| 103 | + - **Structure drift** - file paths mentioned in `context/` have changed or moved |
| 104 | + ```javascript |
| 105 | + const moved = context.paths.filter( |
| 106 | + p => !fs.existsSync(path.resolve(root, p.ref)) && !code.allPaths.has(p.ref) |
| 107 | + ); |
| 108 | + ``` |
| 109 | + |
| 110 | + - **Completion drift** - tasks marked `[x]` in `context/` reference paths or symbols with no evidence in code |
| 111 | + ```javascript |
| 112 | + const phantom = context.completedTasks.filter(task => { |
| 113 | + const ref = task.raw.match(/\b(src|lib|app)\/[\w\/\.\-]+/); |
| 114 | + if (!ref) return false; |
| 115 | + return !fs.existsSync(path.resolve(root, ref[0])) && !code.allPaths.has(ref[0]); |
| 116 | + }); |
| 117 | + ``` |
| 118 | + |
31 | 119 | - Write findings to `context/tmp/drift-analysis-YYYY-MM-DD.md`. |
32 | 120 | - Ask user: "Apply these fixes?" with options: |
33 | 121 | - Yes, apply all |
34 | 122 | - Selectively |
35 | 123 | - No, document only |
36 | 124 |
|
| 125 | +## Expected drift finding format |
| 126 | + |
| 127 | +Each finding in the report should follow this structure: |
| 128 | + |
| 129 | +``` |
| 130 | +[outdated-context] ARCHITECTURE.md line 12 claims "auth handled by middleware/session.js" |
| 131 | + -> File no longer exists. Auth now lives in src/auth/tokenGuard.ts. |
| 132 | + -> Fix: update ARCHITECTURE.md line 12 to reference src/auth/tokenGuard.ts. |
| 133 | + |
| 134 | +[missing-documentation] src/payments/reconciler.ts exports `reconcileDaily()` |
| 135 | + -> No entry found in context/ for this capability. |
| 136 | + -> Fix: add reconciler capability to context/modules.md. |
| 137 | +``` |
| 138 | +
|
37 | 139 | ## Rules |
38 | 140 | - Treat code as source of truth when context and code disagree. |
39 | 141 | - Keep findings concrete with file-level evidence. |
|
0 commit comments