diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0840a00bc4..7b8559883b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,7 +101,10 @@ jobs: # their shared test/unit/support/mcp-cli-harness.ts import nothing but node:* builtins + vitest, # and mcp-discovery.test.ts spawns packages/gittensory-mcp/bin/gittensory-mcp.js as a real # subprocess via StdioClientTransport -- none of the 6 ever load root src/ in-process. This mirrors - # the `mcp` filter's own established trust boundary above (a self-contained package build). + # the `mcp` filter's own established trust boundary above (a self-contained package build). They are + # NOT self-contained w.r.t. packages/gittensory-engine/**, though: gittensory-mcp now has a real + # dependency on @jsonbored/gittensory-engine (isTestFile/isCodeFile), which the bin subprocess and + # local-branch.js load at runtime -- hence that path below, alongside the mcp package's own. # test/unit/mcp-output-schemas.test.ts is DELIBERATELY NOT in this filter: unlike the other 6, it # imports src/mcp/server.ts in-process, and server.ts alone directly imports ~40 other src/ modules # (src/github/app.ts, src/signals/slop.ts, src/settings/autonomy.ts, src/orb/analytics.ts, and @@ -121,6 +124,7 @@ jobs: - 'migrations/**' - '.github/workflows/**' - 'packages/gittensory-mcp/**' + - 'packages/gittensory-engine/**' - 'test/helpers/**' - 'test/unit/mcp-cli-*.test.ts' - 'test/unit/mcp-discovery.test.ts' @@ -396,11 +400,14 @@ jobs: - name: Worker runtime tests if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} run: npm run test:workers + # gittensory-mcp now depends on @jsonbored/gittensory-engine for real (isTestFile/isCodeFile), so a + # PR that only touches packages/gittensory-engine/** must also rebuild + pack-check the mcp package, + # not just the mcp filter's own (deliberately narrower) path list. - name: Build MCP - if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' }} + if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' || needs.changes.outputs.engine == 'true' }} run: npm run build:mcp - name: MCP package check - if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' }} + if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' || needs.changes.outputs.engine == 'true' }} run: npm run test:mcp-pack - name: Build miner CLI if: ${{ github.event_name == 'push' || needs.changes.outputs.miner == 'true' }} diff --git a/package-lock.json b/package-lock.json index 5c30fe29dd..98c7ab69dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15899,6 +15899,7 @@ "version": "0.7.0", "license": "AGPL-3.0-only", "dependencies": { + "@jsonbored/gittensory-engine": "^0.1.0", "@modelcontextprotocol/sdk": "1.29.0", "zod": "^4.4.3" }, diff --git a/packages/gittensory-mcp/lib/local-branch.js b/packages/gittensory-mcp/lib/local-branch.js index 492e8b4dcf..edf3a91225 100644 --- a/packages/gittensory-mcp/lib/local-branch.js +++ b/packages/gittensory-mcp/lib/local-branch.js @@ -2,6 +2,9 @@ import { execFileSync } from "node:child_process"; import { realpathSync } from "node:fs"; import { dirname, isAbsolute, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { isCodeFile, isTestPath as isTestFile } from "@jsonbored/gittensory-engine/signals/test-evidence"; + +export { isCodeFile, isTestFile }; const packageRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); @@ -587,36 +590,6 @@ function firstCommitTitle(messages) { return messages.find((message) => message.trim().length > 0)?.split("\n")[0]?.trim(); } -// Must mirror the canonical server-side matcher in src/signals/test-evidence.ts (isTestPath); the -// server local-branch analysis delegates to it, so this client copy classifies the same diff and must -// agree. The last two branches (Cypress/e2e `*.cy.*`/`*.e2e.*` and `__snapshots__/`) were missing here, -// so a changed `Button.cy.ts` / snapshot file was misclassified as source (isCodeFile) and dropped from -// testFiles, inflating source-line/token counts and under-reporting test evidence in the local packet. -export function isTestFile(file) { - return ( - /(^|\/)(test|tests|spec|__tests__)\//i.test(file) || - /(^|\/)src\/test\//i.test(file) || - /(^|\/)[^/]+_test\.(go|py|rb|dart)$/i.test(file) || // Dart/Flutter `foo_test.dart` co-located with source - /(^|\/)test_[^/]*\.py$/i.test(file) || // pytest's default `test_*.py` prefix convention (the suffix rule above only catches `*_test.py`) - /(^|\/)[^/]+_spec\.rb$/i.test(file) || - /\.(test|spec)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs)$/i.test(file) || - /(^|\/)[^/]+\.(cy|e2e)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/i.test(file) || - // JVM / C# / Swift / PHP `SomethingTest(s)`/`SomethingSpec` class-suffix convention (JUnit, Kotlin/ScalaTest, - // Spock, xUnit/NUnit, XCTest, PHPUnit/PHPSpec). Case-sensitive on the PascalCase suffix so it can't false-positive on words - // that merely end in "test"/"spec" (Latest.java, Contest.cs, manifest.scala, Latest.php). - /(^|\/)\w*(Tests?|Spec)\.(java|kt|kts|scala|cs|swift|groovy|php)$/.test(file) || - /(^|\/)__snapshots__\//i.test(file) - ); -} - -function isGeneratedCodeFile(file) { - return /\.(g|freezed|gr)\.dart$/i.test(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|dart)$/i.test(file) && !isTestFile(file) && !isGeneratedCodeFile(file); -} - function numberValue(value) { const number = Number(value); return Number.isFinite(number) ? number : undefined; diff --git a/packages/gittensory-mcp/package.json b/packages/gittensory-mcp/package.json index 69d6ffc25a..20ffe8a77f 100644 --- a/packages/gittensory-mcp/package.json +++ b/packages/gittensory-mcp/package.json @@ -38,6 +38,7 @@ "build": "node --check bin/gittensory-mcp.js && node --check lib/local-branch.js && node --check scripts/gittensor-score-preview.mjs" }, "dependencies": { + "@jsonbored/gittensory-engine": "^0.1.0", "@modelcontextprotocol/sdk": "1.29.0", "zod": "^4.4.3" }, diff --git a/packages/gittensory-mcp/scripts/gittensor-score-preview.mjs b/packages/gittensory-mcp/scripts/gittensor-score-preview.mjs index ed2c55a7bd..7e936b51a6 100644 --- a/packages/gittensory-mcp/scripts/gittensor-score-preview.mjs +++ b/packages/gittensory-mcp/scripts/gittensor-score-preview.mjs @@ -1,30 +1,7 @@ #!/usr/bin/env node import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; - -function isTestFile(file) { - return ( - /(^|\/)(test|tests|spec|__tests__)\//i.test(file) || - /(^|\/)src\/test\//i.test(file) || - /(^|\/)[^/]+_test\.(go|py|rb|dart)$/i.test(file) || // Dart/Flutter `foo_test.dart` co-located with source - /(^|\/)test_[^/]*\.py$/i.test(file) || // pytest's default `test_*.py` prefix (the suffix rule above only catches `*_test.py`) - /(^|\/)[^/]+_spec\.rb$/i.test(file) || - /\.(test|spec)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs)$/i.test(file) || - /(^|\/)[^/]+\.(cy|e2e)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/i.test(file) || - // JVM/.NET/Swift/PHP PascalCase test-class suffix (case-sensitive, matching the - // signal classifiers) so C#/Swift/Groovy/PHP tests aren't counted as source. - /(^|\/)\w*(Tests?|Spec)\.(java|kt|kts|scala|cs|swift|groovy|php)$/.test(file) || - /(^|\/)__snapshots__\//i.test(file) - ); -} - -function isGeneratedCodeFile(file) { - return /\.(g|freezed|gr)\.dart$/i.test(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|dart)$/i.test(file) && !isTestFile(file) && !isGeneratedCodeFile(file); -} +import { isCodeFile, isTestPath as isTestFile } from "@jsonbored/gittensory-engine/signals/test-evidence"; function lineCount(file) { const additions = Number(file.additions ?? 0);