Skip to content
Open
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
27 changes: 27 additions & 0 deletions src/drift/__tests__/path-false-positives.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
});
});
23 changes: 23 additions & 0 deletions src/drift/claims.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions test/claims.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down