diff --git a/packages/gittensory-mcp/lib/local-branch.js b/packages/gittensory-mcp/lib/local-branch.js index 1da2f0de86..fc51cd74fd 100644 --- a/packages/gittensory-mcp/lib/local-branch.js +++ b/packages/gittensory-mcp/lib/local-branch.js @@ -610,7 +610,7 @@ export function isTestFile(file) { } export function isCodeFile(file) { - return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs|kt|scala|java|go|sql|cs|swift|groovy|php|cpp|cc|c|h|hpp|m|vue|svelte|astro)$/i.test(file) && !isTestFile(file); + return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs|kt|scala|java|go|sql|cs|swift|groovy|php|cpp|cc|c|h|hpp|m|vue|svelte|astro|dart)$/i.test(file) && !isTestFile(file); } function numberValue(value) { diff --git a/packages/gittensory-mcp/scripts/gittensor-score-preview.mjs b/packages/gittensory-mcp/scripts/gittensor-score-preview.mjs index 106ea088f6..86b83376d3 100644 --- a/packages/gittensory-mcp/scripts/gittensor-score-preview.mjs +++ b/packages/gittensory-mcp/scripts/gittensor-score-preview.mjs @@ -19,7 +19,7 @@ function isTestFile(file) { } function isCodeFile(file) { - return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs|kt|scala|java|go|sql|cs|swift|groovy|php|cpp|cc|c|h|hpp|m|vue|svelte|astro)$/i.test(file) && !isTestFile(file); + return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs|kt|scala|java|go|sql|cs|swift|groovy|php|cpp|cc|c|h|hpp|m|vue|svelte|astro|dart)$/i.test(file) && !isTestFile(file); } function lineCount(file) { diff --git a/packages/gittensory-mcp/scripts/gittensor-score-preview.py b/packages/gittensory-mcp/scripts/gittensor-score-preview.py index 0a7139f959..6aad8cfe7d 100644 --- a/packages/gittensory-mcp/scripts/gittensor-score-preview.py +++ b/packages/gittensory-mcp/scripts/gittensor-score-preview.py @@ -142,7 +142,7 @@ def metadata_fallback(metadata: dict) -> dict: lines = max(int(entry.get("additions") or 0) + int(entry.get("deletions") or 0), 0) if is_test_file(path): tests += lines - elif lower_path.endswith((".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rb", ".rs", ".go", ".java", ".kt", ".scala", ".sql", ".cs", ".swift", ".groovy", ".php", ".cpp", ".cc", ".c", ".h", ".hpp", ".m", ".vue", ".svelte", ".astro")): + elif lower_path.endswith((".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rb", ".rs", ".go", ".java", ".kt", ".scala", ".sql", ".cs", ".swift", ".groovy", ".php", ".cpp", ".cc", ".c", ".h", ".hpp", ".m", ".vue", ".svelte", ".astro", ".dart")): source += lines else: non_code += lines diff --git a/src/rules/advisory.ts b/src/rules/advisory.ts index 1788834635..5511105051 100644 --- a/src/rules/advisory.ts +++ b/src/rules/advisory.ts @@ -290,7 +290,7 @@ function severityToAnnotationLevel(severity: AdvisorySeverity): CheckRunAnnotati } function isCodePath(path: string): boolean { - return /\.(ts|tsx|js|jsx|py|go|rs|java|rb|php|cs|cpp|cc|c|h|hpp|swift|kt|m|sql|yaml|yml|json|toml|md|vue|svelte|astro)$/i.test(path); + return /\.(ts|tsx|js|jsx|py|go|rs|java|rb|php|cs|cpp|cc|c|h|hpp|swift|kt|m|sql|yaml|yml|json|toml|md|vue|svelte|astro|dart)$/i.test(path); } function collisionClustersForPull(collisions: CollisionReport, pullNumber: number): CollisionCluster[] { diff --git a/src/signals/engine.ts b/src/signals/engine.ts index cf61f9c8c5..5611655f99 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -5539,9 +5539,10 @@ function isCodeFile(file: string): boolean { // Mirrors isCodeFile in local-branch.ts — kept in sync (cs/swift/groovy/php and C/C++/Objective-C added // so native/C#/Swift/Groovy/PHP source counts as code, matching the test conventions // isTestPath already recognizes; vue/svelte/astro match rag.ts, visual paths, and isCodePath; - // cc/hpp complete the C++ extension set alongside cpp/c/h). + // cc/hpp complete the C++ extension set alongside cpp/c/h; dart matches rag.ts and + // test-evidence's *_test.dart test convention). return ( - /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs|kt|scala|java|go|sql|cs|swift|groovy|php|cpp|cc|c|h|hpp|m|vue|svelte|astro)$/i.test( + /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs|kt|scala|java|go|sql|cs|swift|groovy|php|cpp|cc|c|h|hpp|m|vue|svelte|astro|dart)$/i.test( file, ) && !isTestFile(file) ); diff --git a/src/signals/local-branch.ts b/src/signals/local-branch.ts index 03d08fc6bd..028380311a 100644 --- a/src/signals/local-branch.ts +++ b/src/signals/local-branch.ts @@ -1273,8 +1273,10 @@ export function isCodeFile(file: string): boolean { // nor code in the local scorer. vue/svelte/astro align with review/rag.ts CODE_EXT_RE, // review/visual/paths.ts, and rules/advisory.ts isCodePath so every classifier agrees. // cc/hpp round out the C++ set alongside cpp/c/h (rag.ts already indexes all four). + // dart aligns with rag.ts and test-evidence's *_test.dart convention (hand-authored + // .dart is source; generated .g.dart/.freezed.dart stay non-code via isGeneratedFile). return ( - /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs|kt|scala|java|go|sql|cs|swift|groovy|php|cpp|cc|c|h|hpp|m|vue|svelte|astro)$/i.test( + /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs|kt|scala|java|go|sql|cs|swift|groovy|php|cpp|cc|c|h|hpp|m|vue|svelte|astro|dart)$/i.test( file, ) && !isTestFile(file) ); diff --git a/test/unit/local-branch-file-classifiers.test.ts b/test/unit/local-branch-file-classifiers.test.ts index 07570540cb..d8c7b252cb 100644 --- a/test/unit/local-branch-file-classifiers.test.ts +++ b/test/unit/local-branch-file-classifiers.test.ts @@ -139,6 +139,9 @@ describe("isCodeFile", () => { "src/App.vue", "src/Widget.svelte", "src/pages/index.astro", + // Dart/Flutter hand-authored source — rag.ts indexes .dart; *_test.dart stays test. + "lib/models/user.dart", + "lib/widgets/card.dart", ]) { expect(isCodeFile(path)).toBe(true); } @@ -160,6 +163,8 @@ describe("isCodeFile", () => { "AppTests/LoginTests.swift", // PHP class-suffix test file (PHPUnit) — code extension, but a test, not code. "app/Service/PaymentTest.php", + // Dart co-located *_test.dart is test evidence, not source. + "lib/models/user_test.dart", ]) { expect(isCodeFile(path)).toBe(false); } diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts index af7d855bc4..7282a80881 100644 --- a/test/unit/local-branch.test.ts +++ b/test/unit/local-branch.test.ts @@ -1784,6 +1784,10 @@ describe("local MCP git metadata collection", () => { expect(isTestFile(file)).toBe(false); expect(isCodeFile(file)).toBe(true); } + // Dart/Flutter hand-authored source; *_test.dart remains test-only. + expect(isCodeFile("lib/models/user.dart")).toBe(true); + expect(isTestFile("lib/models/user_test.dart")).toBe(true); + expect(isCodeFile("lib/models/user_test.dart")).toBe(false); }); it("extracts linked issues only from standalone closing keywords, not keyword substrings", async () => { diff --git a/test/unit/rules.test.ts b/test/unit/rules.test.ts index 19aad2d3c9..b52e48071b 100644 --- a/test/unit/rules.test.ts +++ b/test/unit/rules.test.ts @@ -833,6 +833,27 @@ describe("advisory rules", () => { } }); + it("flags Missing test evidence for Dart source via isCodePath + isCodeFile parity", () => { + const advisory = buildPullRequestAdvisory(repo, { + repoFullName: repo.fullName, number: 25, title: "Add Dart widget without tests", state: "open", + authorLogin: "contributor", authorAssociation: "NONE", labels: [], linkedIssues: [], + }); + const sourcePaths = ["lib/models/user.dart", "lib/widgets/card.dart"]; + const files: PullRequestFileRecord[] = sourcePaths.map((path) => ({ + repoFullName: repo.fullName, pullNumber: 25, path, additions: 10, deletions: 0, changes: 10, payload: {}, + })); + const collisions: CollisionReport = { + repoFullName: repo.fullName, generatedAt: "2026-06-10T00:00:00.000Z", + summary: { clusterCount: 0, highRiskCount: 0, itemsReviewed: 0 }, clusters: [], + }; + + const { annotations } = buildCheckRunAnnotations(advisory, { files, collisions, pullNumber: 25 }, "standard"); + + for (const path of sourcePaths) { + expect(annotations.some((entry) => entry.title === "Missing test evidence" && entry.path === path)).toBe(true); + } + }); + it("buildCheckRunAnnotations uses notice level for medium-risk collisions and critical public finding text", () => { const advisory = { ...buildPullRequestAdvisory(repo, null), diff --git a/test/unit/score-preview-script.test.ts b/test/unit/score-preview-script.test.ts index 4e7fe00780..0078c5961f 100644 --- a/test/unit/score-preview-script.test.ts +++ b/test/unit/score-preview-script.test.ts @@ -114,17 +114,17 @@ describe("gittensor-score-preview.mjs classifier parity with the server", () => expect(py.nonCodeTokenScore).toBe(0); }); - it("classifies Dart/Flutter *_test.dart as a test in both .mjs and .py previews", () => { + it("classifies Dart/Flutter source and *_test.dart tests in both .mjs and .py previews", () => { // Parity with src/signals/test-evidence.ts: co-located `foo_test.dart` must count as test evidence, - // not source/non-code, in every mirrored classifier. + // while hand-authored `.dart` source counts as code (rag.ts already indexes .dart). const files = [ { path: "lib/models/user_test.dart", additions: 8, deletions: 0 }, - { path: "lib/models/user.dart", additions: 3, deletions: 0 }, // non-code (dart not in isCodeFile) + { path: "lib/models/user.dart", additions: 3, deletions: 0 }, ]; const mjs = runPreview(files); expect(mjs.testTokenScore).toBe(8); - expect(mjs.sourceTokenScore).toBe(0); - expect(mjs.nonCodeTokenScore).toBe(3); + expect(mjs.sourceTokenScore).toBe(3); + expect(mjs.nonCodeTokenScore).toBe(0); const python = findPython(); if (!python) return; @@ -134,8 +134,8 @@ describe("gittensor-score-preview.mjs classifier parity with the server", () => expect(res.status, res.stderr).toBe(0); const py = JSON.parse(res.stdout); expect(py.testTokenScore).toBe(8); - expect(py.sourceTokenScore).toBe(0); - expect(py.nonCodeTokenScore).toBe(3); + expect(py.sourceTokenScore).toBe(3); + expect(py.nonCodeTokenScore).toBe(0); }); it("classifies Vue/Svelte/Astro source as code in both .mjs and .py previews", () => {