Skip to content
Merged
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
8 changes: 8 additions & 0 deletions packages/gittensory-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,14 @@ export {
// Issue-centric RAG query composition (#2320, extracted in #4254): the pure query builder + the shared
// minimum-query floor; the Vectorize/D1 retrieval backend intentionally stays in the backend.
export { MIN_QUERY_CHARS, buildIssueRagQuery, type IssueRagQueryInput } from "./issue-rag-query.js";
// #782 deterministic local scorer (extracted in #4253): pure token-scoring from changed-file metadata,
// shared by the published CLIs and the hosted Worker. The Node-coupled local-branch.ts stays in the backend.
export {
computeLocalScorerTokens,
type LocalScorerChangedFile,
type LocalScorerValidation,
type LocalScorerResult,
} from "./local-scorer.js";
export {
buildPredictedGateVerdict,
predictedGateNote,
Expand Down
67 changes: 67 additions & 0 deletions packages/gittensory-engine/src/local-scorer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// #782 deterministic local scorer, extracted from src/signals/local-scorer.ts (#4253) so the published
// gittensory-mcp / gittensory-miner CLIs and the hosted Worker share one implementation. Replicates the
// gittensor-root token-scoring view from changed-file METADATA (paths + line counts) — never source content,
// so the no-upload boundary holds and it runs in every surface. It mirrors buildScorePreview's
// source/test/non-code classification, so feeding its output back in as `localScorer` (mode external_command)
// flips the preview off metadata-only with numbers it would otherwise have derived itself.
//
// The 3 dependent type shapes are narrowly duplicated here (the issue explicitly allows this) rather than
// moving the large, Node-coupled local-branch.ts; they are structurally identical to local-branch.ts's
// definitions. isCodeFile/isTestPath are the same portable classifiers local-branch.ts already delegates to.

import { isCodeFile, isTestPath } from "./signals/test-evidence.js";

export type LocalScorerChangedFile = {
path: string;
previousPath?: string | undefined;
additions?: number | undefined;
deletions?: number | undefined;
status?: "added" | "modified" | "deleted" | "renamed" | "copied" | "unknown" | undefined;
binary?: boolean | undefined;
};

export type LocalScorerValidation = {
command: string;
status: "passed" | "failed" | "not_run" | "skipped" | "focused" | "unknown";
summary?: string | undefined;
durationMs?: number | undefined;
exitCode?: number | undefined;
};

export type LocalScorerResult = {
mode: "metadata_only" | "external_command" | "gittensor_root";
activeModel?: string | undefined;
sourceTokenScore?: number | undefined;
totalTokenScore?: number | undefined;
sourceLines?: number | undefined;
testTokenScore?: number | undefined;
nonCodeTokenScore?: number | undefined;
warnings?: string[] | undefined;
};

const fileLines = (file: LocalScorerChangedFile): number => Math.max(0, file.additions ?? 0) + Math.max(0, file.deletions ?? 0);

/**
* Compute token scores from changed-file metadata + the local validation results. `isCodeFile` already excludes
* tests, so source / test / non-code are disjoint. Binary files carry no token value and are dropped. A failed
* validation does not change the scores (they describe the diff) but is surfaced as a warning. Pure.
*/
export function computeLocalScorerTokens(input: { changedFiles: LocalScorerChangedFile[]; validation?: LocalScorerValidation[] | undefined }): LocalScorerResult {
const files = input.changedFiles.filter((file) => !file.binary);
const testTokenScore = files.filter((file) => isTestPath(file.path)).reduce((sum, file) => sum + fileLines(file), 0);
const sourceTokenScore = files.filter((file) => isCodeFile(file.path)).reduce((sum, file) => sum + fileLines(file), 0);
const totalTokenScore = files.reduce((sum, file) => sum + fileLines(file), 0);
const nonCodeTokenScore = Math.max(0, totalTokenScore - sourceTokenScore - testTokenScore);
const failed = (input.validation ?? []).some((entry) => entry.status === "failed");
const warnings = failed ? ["Local validation reported failures — token scores describe the diff, not a passing build."] : [];
return {
mode: "external_command",
activeModel: "gittensory-deterministic",
sourceTokenScore,
totalTokenScore,
sourceLines: Math.max(1, sourceTokenScore || totalTokenScore || 1),
testTokenScore,
nonCodeTokenScore,
...(warnings.length > 0 ? { warnings } : {}),
};
}
40 changes: 40 additions & 0 deletions packages/gittensory-engine/test/local-scorer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { test } from "node:test";
import assert from "node:assert/strict";

import { computeLocalScorerTokens } from "../dist/index.js";

test("barrel: the public entrypoint re-exports computeLocalScorerTokens", () => {
assert.equal(typeof computeLocalScorerTokens, "function");
});

test("classifies source / test / non-code disjointly from changed-file metadata", () => {
const r = computeLocalScorerTokens({
changedFiles: [
{ path: "src/a.ts", additions: 10, deletions: 2 }, // source: 12
{ path: "src/a.test.ts", additions: 5, deletions: 1 }, // test: 6
{ path: "README.md", additions: 4, deletions: 0 }, // non-code: 4
],
});
assert.equal(r.mode, "external_command");
assert.equal(r.activeModel, "gittensory-deterministic");
assert.equal(r.sourceTokenScore, 12);
assert.equal(r.testTokenScore, 6);
assert.equal(r.nonCodeTokenScore, 4);
assert.equal(r.totalTokenScore, 22);
assert.equal(r.warnings, undefined);
});

test("drops binary files and floors sourceLines at 1", () => {
const r = computeLocalScorerTokens({ changedFiles: [{ path: "img.png", additions: 999, binary: true }] });
assert.equal(r.totalTokenScore, 0);
assert.equal(r.sourceLines, 1);
});

test("surfaces a warning when a validation entry failed, without changing scores", () => {
const r = computeLocalScorerTokens({
changedFiles: [{ path: "src/a.ts", additions: 3 }],
validation: [{ command: "npm test", status: "failed" }],
});
assert.equal(r.sourceTokenScore, 3);
assert.ok(r.warnings && r.warnings.length === 1);
});
40 changes: 6 additions & 34 deletions src/signals/local-scorer.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,6 @@
import { isCodeFile, isTestFile, type LocalBranchChangedFile, type LocalBranchScorer, type LocalBranchValidation } from "./local-branch";

// #782 deterministic local scorer. Replicates the gittensor-root token-scoring view from changed-file METADATA
// (paths + line counts) — never source content, so the no-upload boundary holds and it runs in every surface
// (stdio package AND hosted Worker). It mirrors buildScorePreview's source/test/non-code classification, so
// feeding its output back in as `localScorer` (mode external_command) flips the preview off metadata-only with
// numbers it would otherwise have derived itself — closing the "miner runs the scorer manually" gap.

const fileLines = (file: LocalBranchChangedFile): number => Math.max(0, file.additions ?? 0) + Math.max(0, file.deletions ?? 0);

/**
* Compute token scores from changed-file metadata + the local validation results. `isCodeFile` already excludes
* tests, so source / test / non-code are disjoint. Binary files carry no token value and are dropped. A failed
* validation does not change the scores (they describe the diff) but is surfaced as a warning. Pure.
*/
export function computeLocalScorerTokens(input: { changedFiles: LocalBranchChangedFile[]; validation?: LocalBranchValidation[] | undefined }): LocalBranchScorer {
const files = input.changedFiles.filter((file) => !file.binary);
const testTokenScore = files.filter((file) => isTestFile(file.path)).reduce((sum, file) => sum + fileLines(file), 0);
const sourceTokenScore = files.filter((file) => isCodeFile(file.path)).reduce((sum, file) => sum + fileLines(file), 0);
const totalTokenScore = files.reduce((sum, file) => sum + fileLines(file), 0);
const nonCodeTokenScore = Math.max(0, totalTokenScore - sourceTokenScore - testTokenScore);
const failed = (input.validation ?? []).some((entry) => entry.status === "failed");
const warnings = failed ? ["Local validation reported failures — token scores describe the diff, not a passing build."] : [];
return {
mode: "external_command",
activeModel: "gittensory-deterministic",
sourceTokenScore,
totalTokenScore,
sourceLines: Math.max(1, sourceTokenScore || totalTokenScore || 1),
testTokenScore,
nonCodeTokenScore,
...(warnings.length > 0 ? { warnings } : {}),
};
}
// #782 deterministic local scorer — extracted to `@jsonbored/gittensory-engine` (#4253) so the published
// gittensory-mcp / gittensory-miner CLIs and the hosted Worker import the identical, versioned scoring logic
// instead of drifting. The Vectorize/Node-coupled local-branch.ts is intentionally NOT moved; this shim only
// re-exports the pure scorer. packages/gittensory-engine/src/local-scorer.ts (imported via relative source
// path, matching the #2278/#2282/#4254 extraction shims) is the source of truth.
export { computeLocalScorerTokens } from "../../packages/gittensory-engine/src/local-scorer";