From c4dedae6373ce777c1ecd081bbc115228b81ad7f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 19 Sep 2026 16:07:24 +0000 Subject: [PATCH] fix(drift): treat qualified-name symbol notation as not a path Drop tokens like `src/auth/login.validateToken` in isNotAPath when the final slash-segment is identifier.identifier and is not a known file extension. Real paths (`src/auth/login.ts`, `.mex/ROUTER.md`, `package.json`) stay claims. Addresses #202 bullet 1 only. Co-authored-by: David --- .../__tests__/path-false-positives.test.ts | 27 +++++++++++++++++++ src/drift/claims.ts | 23 ++++++++++++++++ test/claims.test.ts | 18 +++++++++++++ 3 files changed, 68 insertions(+) diff --git a/src/drift/__tests__/path-false-positives.test.ts b/src/drift/__tests__/path-false-positives.test.ts index 6dd8713d..c6364e3c 100644 --- a/src/drift/__tests__/path-false-positives.test.ts +++ b/src/drift/__tests__/path-false-positives.test.ts @@ -106,4 +106,31 @@ describe("MISSING_PATH false positives", () => { ".mex/local/", ]); }); + + it("does not treat qualified-name symbol notation as a path (#202 bullet 1)", () => { + const projectRoot = mkdtempSync(join(tmpdir(), "mex-drift-")); + execFileSync("git", ["init", "-q"], { cwd: projectRoot }); + const scaffoldRoot = join(projectRoot, ".mex"); + mkdirSync(scaffoldRoot, { recursive: true }); + const docPath = join(scaffoldRoot, "ROUTER.md"); + writeFileSync( + docPath, + "# Graph\n\n" + + "- Store the `qualified_name` string (**e.g.** `src/auth/login.validateToken`).\n" + + "- A similar example is `lib/user.authenticate`.\n" + + "- The real module is `src/auth/login.ts`.\n" + ); + + const claims = extractClaims(docPath, ".mex/ROUTER.md"); + const pathValues = claims.filter((c) => c.kind === "path").map((c) => c.value); + expect(pathValues).not.toContain("src/auth/login.validateToken"); + expect(pathValues).not.toContain("lib/user.authenticate"); + expect(pathValues).toContain("src/auth/login.ts"); + + expect( + checkPaths(claims, projectRoot, scaffoldRoot) + .filter((issue) => issue.code === "MISSING_PATH") + .map((issue) => issue.claim?.value) + ).toEqual(["src/auth/login.ts"]); + }); }); diff --git a/src/drift/claims.ts b/src/drift/claims.ts index f63b064a..1f198268 100644 --- a/src/drift/claims.ts +++ b/src/drift/claims.ts @@ -27,6 +27,17 @@ const EXTENSION_ONLY = /^\.[A-Za-z0-9]+$/; /** Common shell commands that can contain path-like arguments. */ const SHELL_COMMAND_PREFIX = /^(?:sudo\s+)?(?:ls|cd|cat|grep|find|kubectl|helm|docker|git)\s+/; +/** + * Final slash-segment that is a JS/TS-style `identifier.identifier` + * (method or property), not a filename. Matches + * `src/auth/login.validateToken` / `lib/user.authenticate`. + * Real files keep a known extension (`src/auth/login.ts`, `.mex/ROUTER.md`, + * `package.json`) and stay claims. Extra dotted segments such as + * `foo.d.ts` are left for compound-extension handling (#216). + */ +const QUALIFIED_SYMBOL_SEGMENT = + /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/; + /** * Dotted config keys or annotations can contain slashes but are not paths: * `argocd.argoproj.io/sync-wave`, `k8s.io/api`. The dotted segment must start @@ -63,6 +74,10 @@ function isNotAPath(value: string): boolean { // Annotation/config keys with slash-separated namespaces: argocd.argoproj.io/sync-wave if (DOTTED_KEY_WITH_SLASH.test(value)) return true; + // Qualified-name / symbol notation: `src/auth/login.validateToken`. + // Drop before it becomes a path claim (#202 bullet 1). + if (isQualifiedSymbolNotation(value)) return true; + // Code snippets: contains =, (), ;, or other code-like characters if (/[=();,]/.test(value)) return true; @@ -88,6 +103,14 @@ function isNotAPath(value: string): boolean { return false; } +/** True when the last slash-segment looks like `file.method`, not a known file type. */ +function isQualifiedSymbolNotation(value: string): boolean { + if (KNOWN_EXTENSIONS.test(value)) return false; + const slash = value.lastIndexOf("/"); + if (slash === -1) return false; + return QUALIFIED_SYMBOL_SEGMENT.test(value.slice(slash + 1)); +} + /** Extract all claims from a markdown file */ export function extractClaims(filePath: string, source: string): Claim[] { let content: string; diff --git a/test/claims.test.ts b/test/claims.test.ts index 8185d674..84febff5 100644 --- a/test/claims.test.ts +++ b/test/claims.test.ts @@ -94,6 +94,24 @@ describe("extractClaims — paths", () => { expect(paths).toHaveLength(0); }); + it("skips qualified-name symbol notation (#202 bullet 1)", () => { + const path = writeFixture( + "test.md", + "# Graph\n\n" + + "Store the `qualified_name` string (**e.g.** `src/auth/login.validateToken`). " + + "A similar example is `lib/user.authenticate`. " + + "The real module is `src/auth/login.ts`. " + + "Scaffold files stay claims: `.mex/ROUTER.md` and `package.json`." + ); + const claims = extractClaims(path, "test.md"); + const paths = claims.filter((c) => c.kind === "path").map((c) => c.value); + expect(paths).not.toContain("src/auth/login.validateToken"); + expect(paths).not.toContain("lib/user.authenticate"); + expect(paths).toContain("src/auth/login.ts"); + expect(paths).toContain(".mex/ROUTER.md"); + expect(paths).toContain("package.json"); + }); + it("skips non-path inline code values", () => { const path = writeFixture( "test.md",