diff --git a/packages/gittensory-engine/src/scoring/preview.ts b/packages/gittensory-engine/src/scoring/preview.ts index c9f93ed0df..b7aa3cd185 100644 --- a/packages/gittensory-engine/src/scoring/preview.ts +++ b/packages/gittensory-engine/src/scoring/preview.ts @@ -1,10 +1,14 @@ import type { ContributorEvidenceRecord, JsonValue, RepositoryRecord, RepoTimeDecayOverrides, ScoringModelSnapshotRecord, ScorePreviewRecord } from "./types.js"; import { DEFAULT_SCORING_CONSTANTS } from "./model.js"; +import { hasUnsafeWildcardCount } from "../signals/change-guardrail.js"; // Deterministic score-preview builder extracted verbatim from the backend's `src/scoring/preview.ts` // (#2282) — this file has no D1/network/env dependency in the original, so it ports unchanged aside from -// its imports and the two tiny pure helpers (`nowIso`, `hasUnsafeWildcardCount`) inlined below, which the -// backend sources from `src/utils/json.ts` and `src/signals/change-guardrail.ts` respectively. +// its imports and one tiny pure helper (`nowIso`) inlined below, which the backend sources from +// `src/utils/json.ts`. `hasUnsafeWildcardCount` is imported from this package's own +// `signals/change-guardrail.ts` (#4611) rather than re-derived here — that file is a verbatim port of the +// backend's `src/signals/change-guardrail.ts`, kept in sync by the engine-parity contract test, so importing +// it carries the same ReDoS-safety guarantee without a third hand-maintained copy. // The package's tsconfig sets `types: []` (no ambient DOM/Node globals, keeping the engine's type surface // independent of any consumer's lib config), so the Web Crypto global needs a minimal local declaration. @@ -16,28 +20,6 @@ function nowIso(): string { return new Date().toISOString(); } -// Mirrors `src/signals/change-guardrail.ts`'s `hasUnsafeWildcardCount`/wildcard-group counting exactly — -// see that file for the full ReDoS-safety rationale. Duplicated here (rather than imported) because this -// package cannot reach into `src/`; keep the two in sync by hand. -const MAX_GLOB_WILDCARD_GROUPS = 2; - -function countWildcardGroups(glob: string): number { - let count = 0; - for (let i = 0; i < glob.length; i += 1) { - if (glob.charAt(i) !== "*") continue; - count += 1; - if (glob.charAt(i + 1) === "*") { - i += 1; // consume the second star of the "**" pair — one group, not two - if (glob.charAt(i + 1) === "/") i += 1; // `**/` also matches zero segments, mirroring globToRegExp - } - } - return count; -} - -function hasUnsafeWildcardCount(glob: string): boolean { - return countWildcardGroups(glob) > MAX_GLOB_WILDCARD_GROUPS; -} - export type ScorePreviewInput = { repoFullName: string; targetType?: ScorePreviewRecord["targetType"]; diff --git a/review-enrichment/src/analyzers/a11y-regression.ts b/review-enrichment/src/analyzers/a11y-regression.ts index 0d4b2beeaf..a7b39a470a 100644 --- a/review-enrichment/src/analyzers/a11y-regression.ts +++ b/review-enrichment/src/analyzers/a11y-regression.ts @@ -4,6 +4,7 @@ import type { A11yFinding, EnrichRequest } from "../types.js"; import { isTestPath } from "./test-ratio.js"; import { DEFAULT_MAX_FINDINGS, DEFAULT_MAX_LINE_CHARS } from "./limits.js"; +import { isBasicCommentLine } from "./diff-lines.js"; const MAX_FINDINGS = DEFAULT_MAX_FINDINGS; const MAX_LINE_CHARS = DEFAULT_MAX_LINE_CHARS; @@ -24,9 +25,12 @@ const NON_INTERACTIVE_CLICK_TARGET_RE = const FORM_CONTROL_RE = /<(?:input|select|textarea)\b/i; const LABEL_ASSOC_RE = /\b(?:aria-label|aria-labelledby|id)\s*=| MAX_LINE_CHARS) return null; + if (isBasicCommentLine(line) || line.length > MAX_LINE_CHARS) return null; const code = codeOnly(line); if (/\bas any\b/.test(code)) return "cast"; if (//.test(code)) return "assertion"; diff --git a/review-enrichment/test/diff-lines.test.ts b/review-enrichment/test/diff-lines.test.ts index b186be5f39..b638f2947a 100644 --- a/review-enrichment/test/diff-lines.test.ts +++ b/review-enrichment/test/diff-lines.test.ts @@ -2,7 +2,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { isDiffFileHeaderLine } from "../dist/analyzers/diff-lines.js"; +import { isBasicCommentLine, isDiffFileHeaderLine } from "../dist/analyzers/diff-lines.js"; test("isDiffFileHeaderLine matches real file headers only, not ++/--- content", () => { // Real unified-diff file headers → skipped. @@ -15,3 +15,18 @@ test("isDiffFileHeaderLine matches real file headers only, not ++/--- content", assert.equal(isDiffFileHeaderLine(content), false, content); } }); + +test("isBasicCommentLine matches //, /*, and * comment openers, leading whitespace included", () => { + for (const line of ["// a note", " // indented", "/* block open", "* jsdoc continuation", " * indented continuation"]) { + assert.equal(isBasicCommentLine(line), true, line); + } + // Not real code either, but outside this shared base's scope — analyzers that need these layer their own + // override on top (hardcoded-url.ts's `#`/`", "import x from 'y'", "from y import x"]) { + assert.equal(isBasicCommentLine(line), false, line); + } + // Real code → never flagged. + for (const line of ["const x = 1;", " return a && b;", "export function run() {"]) { + assert.equal(isBasicCommentLine(line), false, line); + } +}); diff --git a/src/api/routes.ts b/src/api/routes.ts index f6293414b6..e074d17300 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -166,6 +166,7 @@ import { decidePendingAgentAction } from "../services/agent-approval-queue"; import { explainScoreBreakdown } from "../services/score-breakdown"; import { buildMcpClientTelemetry } from "../services/client-telemetry"; import { + authoritativeContributorRepoStats, buildAndPersistContributorDecisionPack, CONTRIBUTOR_DECISION_PACK_SIGNAL, loadContributorDecisionPackForServing, @@ -5204,14 +5205,6 @@ function parseBackfillSegment(value: unknown): Extract>, - cachedRepoStats: Awaited>, -) { - const officialRepoStats = contributorRepoStatsFromGittensor(gittensorSnapshot); - return officialRepoStats.length > 0 ? officialRepoStats : cachedRepoStats; -} - async function persistSignal( env: Env, signalType: string, diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 9de690e595..e9c3c0df35 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -86,7 +86,7 @@ import { preparePrPacketWithAgent, startAgentRun, } from "../services/agent-orchestrator"; -import { loadContributorDecisionPackForServing, repoDecisionFromPack } from "../services/decision-pack"; +import { authoritativeContributorRepoStats, loadContributorDecisionPackForServing, repoDecisionFromPack } from "../services/decision-pack"; import { buildPublicPrBodyDraft } from "../services/pr-body-draft"; import { buildRemediationPlan } from "../services/remediation-plan"; import { deriveEligibilityPlan } from "../services/eligibility-plan"; @@ -3782,14 +3782,6 @@ function redactSensitiveForMcp(value: unknown): unknown { ); } -function authoritativeContributorRepoStats( - gittensorSnapshot: Awaited>, - cachedRepoStats: Awaited>, -) { - const officialRepoStats = contributorRepoStatsFromGittensor(gittensorSnapshot); - return officialRepoStats.length > 0 ? officialRepoStats : cachedRepoStats; -} - async function authenticateMcpRequest(c: AppContext): Promise { const identity = await authenticatePrivateToken(c.env, extractBearerToken(c.req.header("authorization"))); if (!identity || identity.kind !== "session") return identity; diff --git a/src/queue/processors.ts b/src/queue/processors.ts index c83e6abc66..a16f5898f2 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -228,6 +228,7 @@ import { refreshScoringModelSnapshot, } from "../scoring/model"; import { + authoritativeContributorRepoStats, buildAndPersistContributorDecisionPack, loadDecisionPackSharedInputs, } from "../services/decision-pack"; @@ -15558,17 +15559,6 @@ function officialGittensorContributorDetection( }; } -function authoritativeContributorRepoStats( - gittensorSnapshot: Awaited< - ReturnType - >, - cachedRepoStats: Awaited>, -) { - const officialRepoStats = - contributorRepoStatsFromGittensor(gittensorSnapshot); - return officialRepoStats.length > 0 ? officialRepoStats : cachedRepoStats; -} - /** Split `owner/name` into the project/repo key shape shared by RAG indexing and retrieval. */ export function splitRepoForRag(repoFullName: string): [string, string] { const slash = repoFullName.indexOf("/"); diff --git a/src/services/decision-pack.ts b/src/services/decision-pack.ts index 27a4adb390..3bb323cffc 100644 --- a/src/services/decision-pack.ts +++ b/src/services/decision-pack.ts @@ -1794,7 +1794,11 @@ function snapshotAgeMs(generatedAt: string): number { return Number.isFinite(parsed) ? Date.now() - parsed : Number.POSITIVE_INFINITY; } -function authoritativeContributorRepoStats( +/** The gittensor-official snapshot's repo stats when present, falling back to the last cached copy — + * gittensor is the authoritative source when reachable, the cache is only a degrade-gracefully fallback for + * when it isn't. Shared by every site that resolves a contributor's repo stats (#4611) — mcp/server.ts, + * api/routes.ts, and queue/processors.ts all import this rather than redefining it. */ +export function authoritativeContributorRepoStats( gittensorSnapshot: Awaited>, cachedRepoStats: ContributorRepoStatRecord[], ) {