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
11 changes: 11 additions & 0 deletions .gittensory.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,12 @@ review:
# AI reviewer as additive reference context.
# impact_map: false

# When true, the AI reviewer's prompt gains an additive "repo quality-culture profile" reference block --
# typical merged-PR size + common accepted labels, derived from this repo's own merge history. Reference-only
# grounding; never a gate/scoring input. Requires operator flag GITTENSORY_REVIEW_CULTURE_PROFILE. Bool or
# null. Default: null/false — byte-identical. (#2995)
# culture_profile: false

# Display-only floor for inline AI findings (`critical` | `major` | `minor` | `nitpick`). Findings below the
# configured level are suppressed from inline comments — never from gate blockers. Default: null (show all).
# min_finding_severity: major
Expand Down Expand Up @@ -825,6 +831,11 @@ settings:
# # symbols -- is computed, rendered as a compact unified-comment section, and fed to the AI reviewer
# # as additive reference context. Bool or null. Default: null/false (#2184, part of #1971).
# impact_map: false
# # When true, the AI reviewer's prompt gains an additive "repo quality-culture profile" reference block --
# # typical merged-PR size + common accepted labels, derived from this repo's OWN recent merge history
# # (recent_merged_pull_requests). Reference-only grounding, never a gate/scoring input; requires the operator
# # flag GITTENSORY_REVIEW_CULTURE_PROFILE. Bool or null. Default: null/false. (#2995)
# culture_profile: false
# # When true, an inline finding is ALSO tagged with a category (security/correctness/performance/
# # maintainability/tests/style) -- the AI reviewer self-categorizes, with a deterministic path/keyword
# # fallback for whatever it omits. Only takes effect when inline_comments is already on. Bool or null.
Expand Down
1 change: 1 addition & 0 deletions apps/gittensory-ui/src/routes/docs.privacy-security.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ GITTENSORY_REVIEW_SAFETY="true" # prompt-injection defang + sec
GITTENSORY_REVIEW_GROUNDING="true" # CI status + full changed-file content
GITTENSORY_REVIEW_RAG="true" # codebase vector-index context (needs index)
GITTENSORY_REVIEW_IMPACT_MAP="true" # deterministic impact map (needs review.impact_map too)
GITTENSORY_REVIEW_CULTURE_PROFILE="true" # repo quality-culture profile (needs review.culture_profile: true)
GITTENSORY_REVIEW_REPUTATION="true" # submitter-reputation spend control (never shown)
GITTENSORY_REVIEW_UNIFIED_COMMENT="true" # one in-place unified PR comment
GITTENSORY_REVIEW_ENRICHMENT="true" # external analyzer registry (REES) findings
Expand Down
7 changes: 7 additions & 0 deletions apps/gittensory-ui/src/routes/docs.tuning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ function Tuning() {
comment (also feeds it to the AI reviewer as additive reference context). ANDed with the
per-repo <code>review.impact_map</code> opt-in — neither alone is sufficient. Per-PR.
</li>
<li>
<code>GITTENSORY_REVIEW_CULTURE_PROFILE</code> — appends a "repo quality-culture profile"
reference block to the reviewer prompt: typical merged-PR size and common accepted labels,
derived from this repo's own merge history. Additive reference only — never a gate or
scoring input. Also requires the per-repo <code>review.culture_profile: true</code> opt-in
in <code>.gittensory.yml</code>. Per-PR.
</li>
<li>
<code>GITTENSORY_REVIEW_REPUTATION</code> — submitter-reputation spend control. A new,
burst, or low-reputation submitter is downgraded to a deterministic-only review; good
Expand Down
11 changes: 11 additions & 0 deletions config/examples/gittensory.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,12 @@ review:
# AI reviewer as additive reference context.
# impact_map: false

# When true, the AI reviewer's prompt gains an additive "repo quality-culture profile" reference block --
# typical merged-PR size + common accepted labels, derived from this repo's own merge history. Reference-only
# grounding; never a gate/scoring input. Requires operator flag GITTENSORY_REVIEW_CULTURE_PROFILE. Bool or
# null. Default: null/false — byte-identical. (#2995)
# culture_profile: false

# Display-only floor for inline AI findings (`critical` | `major` | `minor` | `nitpick`). Findings below the
# configured level are suppressed from inline comments — never from gate blockers. Default: null (show all).
# min_finding_severity: major
Expand Down Expand Up @@ -838,6 +844,11 @@ settings:
# # symbols -- is computed, rendered as a compact unified-comment section, and fed to the AI reviewer
# # as additive reference context. Bool or null. Default: null/false (#2184, part of #1971).
# impact_map: false
# # When true, the AI reviewer's prompt gains an additive "repo quality-culture profile" reference block --
# # typical merged-PR size + common accepted labels, derived from this repo's OWN recent merge history
# # (recent_merged_pull_requests). Reference-only grounding, never a gate/scoring input; requires the operator
# # flag GITTENSORY_REVIEW_CULTURE_PROFILE. Bool or null. Default: null/false. (#2995)
# culture_profile: false
# # When true, an inline finding is ALSO tagged with a category (security/correctness/performance/
# # maintainability/tests/style) -- the AI reviewer self-categorizes, with a deterministic path/keyword
# # fallback for whatever it omits. Only takes effect when inline_comments is already on. Bool or null.
Expand Down
8 changes: 8 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,14 @@ declare global {
* shouldComputeImpactMap). Default OFF — unset/false performs NO symbol extraction, NO RAG query, and adds
* NO comment/prompt section, byte-identical to today. */
GITTENSORY_REVIEW_IMPACT_MAP?: string;
/** Repo quality-culture profile (#2995): when truthy, the AI reviewer prompt gains an ADDITIVE "REPO
* QUALITY-CULTURE PROFILE" reference block — typical merged-PR size + common accepted labels, derived
* deterministically from this repo's OWN `recent_merged_pull_requests` history (see
* review/repo-culture-profile.ts + repo-culture-profile-wire.ts). Also requires the per-repo
* `.gittensory.yml` `review.culture_profile: true` opt-in — this is the global kill-switch only. Default
* OFF — unset/false performs NO extra D1 read and keeps the reviewer prompt byte-identical (the new branch
* is unreachable when off). ADVISORY GROUNDING ONLY: never a gate/scoring input. */
GITTENSORY_REVIEW_CULTURE_PROFILE?: string;
/** Review-enrichment service (REES): when truthy, the self-host review engine POSTs the PR diff/files to
* REES and splices any public-safe brief into the AI reviewer prompt. Requires REES_URL and the repo in
* GITTENSORY_REVIEW_REPOS. REES_ANALYZERS is an optional exact comma-list; unset/"all"/"*" lets REES run its
Expand Down
29 changes: 28 additions & 1 deletion src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,10 @@ import { createReviewAdapters } from "../review/adapters";
import { extractChangedSymbols } from "../review/impact-symbols";
import { computeImpactMap } from "../review/impact-map";
import { formatImpactMapPromptSection, shouldComputeImpactMap } from "../review/impact-map-wire";
import {
buildRepoCultureProfileContext,
isRepoCultureProfileEnabled,
} from "../review/repo-culture-profile-wire";
import {
buildReviewEnrichment,
isEnrichmentEnabled,
Expand Down Expand Up @@ -6669,6 +6673,11 @@ export async function runAiReviewForAdvisory(
// compute the deterministic impact map and splice it into the reviewer prompt as additive reference
// context. Absent/false ⇒ byte-identical reviewer prompt (no impact-map computation, no RAG query for it).
reviewImpactMap?: boolean | undefined;
// `.gittensory.yml` review.culture_profile (#2995), resolved by the caller from the cached manifest. ANDed
// here with the GITTENSORY_REVIEW_CULTURE_PROFILE global flag to decide whether to append the repo's
// quality-culture reference block (typical merged-PR size + common labels) to the reviewer prompt. Absent/
// false ⇒ byte-identical (no section, no extra D1 read).
reviewCultureProfile?: boolean | undefined;
// The inbound webhook delivery id that triggered this review (#codex-timeout-fields) — forwarded to a
// self-host provider's failure log purely for operator correlation; never read by any review logic. Absent
// (e.g. a sweep/repair fan-out with no single originating delivery, or a unit test) ⇒ the log line omits it.
Expand Down Expand Up @@ -6874,6 +6883,15 @@ export async function runAiReviewForAdvisory(
});
impactMapContext = formatImpactMapPromptSection(impactMap);
}
// Repo quality-culture profile (#2995, flag-gated by GITTENSORY_REVIEW_CULTURE_PROFILE AND the per-repo
// `review.culture_profile` opt-in). Derives a compact reference block from the repo's OWN merge history
// (typical PR size, common accepted labels) and appends it as additive grounding — exactly like RAG. Both
// gates OFF (default) → NO new branch: no D1 read, and `cultureProfileContext` is left undefined so the
// prompt is byte-identical to today. Fully fail-safe (any error/insufficient-history degrades to "").
const cultureProfileContext =
isRepoCultureProfileEnabled(env) && args.reviewCultureProfile === true
? await buildRepoCultureProfileContext(env, args.repoFullName)
: undefined;
// Review-enrichment (#1472, flag-gated by GITTENSORY_REVIEW_ENRICHMENT + REES_URL). POST the PR to the external
// REES for the heavy/external analysis the reviewer can't run (dependency CVEs, secrets, license/EOL/supply-chain);
// its public-safe brief splices into the prompt next to grounding + RAG. Flag-OFF (default) → no call, no branch,
Expand Down Expand Up @@ -6933,6 +6951,7 @@ export async function runAiReviewForAdvisory(
providerKey,
grounding,
ragContext: ragContextResult?.text,
cultureProfileContext,
observability: { rag: ragTelemetry },
impactMapContext,
enrichment,
Expand Down Expand Up @@ -8554,6 +8573,7 @@ async function maybePublishPrPublicSurface(
pathFilters: reviewPathFilters,
selfHostAiModel: reviewSelfHostAiModel,
impactMap: reviewImpactMap,
cultureProfile: reviewCultureProfile,
} = resolveReviewPromptOverrides(reviewManifest);
inlineCommentsEnabledForReview = shouldRequestInlineFindings(
env,
Expand Down Expand Up @@ -8603,12 +8623,18 @@ async function maybePublishPrPublicSurface(
"reputation",
repoFullName,
),
// Repo quality-culture profile (#2995): its own cache (signal_snapshots, TTL + merged-PR-count
// invalidation) can refresh independently of this PR's head SHA, exactly like RAG's vector index —
// so a repo with it active also bypasses the AI-review result cache rather than fingerprinting a
// value that can't prove freshness.
cultureProfile: isRepoCultureProfileEnabled(env) && reviewCultureProfile === true,
};
const dynamicReviewContextActive =
dynamicReviewFeatures.grounding ||
dynamicReviewFeatures.rag ||
dynamicReviewFeatures.enrichment ||
dynamicReviewFeatures.reputation;
dynamicReviewFeatures.reputation ||
dynamicReviewFeatures.cultureProfile;
const inputFingerprint = await aiReviewCacheInputFingerprint({
title: pr.title,
mode: settings.aiReviewMode,
Expand Down Expand Up @@ -8761,6 +8787,7 @@ async function maybePublishPrPublicSurface(
reviewFindingCategories,
reviewSelfHostAiModel,
reviewImpactMap,
reviewCultureProfile,
deliveryId: webhook.deliveryId,
});
// `persistable === false` (only the lock-contention placeholder — see runAiReviewForAdvisory's return
Expand Down
25 changes: 19 additions & 6 deletions src/review/ai-review-cache-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import type {
} from "../signals/focus-manifest";
import { sha256Hex } from "../utils/crypto";

export const AI_REVIEW_CACHE_INPUT_VERSION = "ai-review-input:v1";
// Bumped v1→v2 (#2995): `features` gained a `cultureProfile` member. Every prior cached review's fingerprint was
// computed without that key, so bumping the version guarantees a clean cache miss on the first review after
// upgrade rather than silently reusing a hash computed under a different payload shape.
export const AI_REVIEW_CACHE_INPUT_VERSION = "ai-review-input:v2";

// #regate-churn (root cause, confirmed in production): this fingerprint USED to also hash the PR's live
// `baseSha`, on the theory that a rebase/retarget can change the diff GitHub reports for an otherwise-unchanged
Expand Down Expand Up @@ -95,15 +98,19 @@ export type AiReviewCacheInput = {
additions: number;
deletions: number;
}[];
// grounding/rag/enrichment/reputation each pull TIME-VARYING external context that can change for an
// unchanged head SHA without any of these booleans flipping (live CI checks, the vector index, REES/CVE data,
// the submitter's evolving reputation) -- a boolean can't detect that drift, so the caller bypasses the cache
// entirely whenever any of these is true rather than relying on this fingerprint to catch a content change.
// grounding/rag/enrichment/reputation/cultureProfile each pull TIME-VARYING external context that can change
// for an unchanged head SHA without any of these booleans flipping (live CI checks, the vector index,
// REES/CVE data, the submitter's evolving reputation, the repo's own merge-history cache) -- a boolean can't
// detect that drift, so the caller bypasses the cache entirely whenever any of these is true rather than
// relying on this fingerprint to catch a content change.
features: {
grounding: boolean;
rag: boolean;
enrichment: boolean;
reputation: boolean;
// #2995: added alongside the repo quality-culture profile. Explicitly enumerated below (not passed through
// raw) so a FUTURE new feature key can't silently change every existing cache entry's fingerprint again.
cultureProfile: boolean;
};
};

Expand Down Expand Up @@ -184,7 +191,13 @@ export async function aiReviewCacheInputFingerprint(input: AiReviewCacheInput):
deletions: file.deletions,
}))
.sort((left, right) => left.path.localeCompare(right.path)),
features: input.features,
features: {
grounding: input.features.grounding,
rag: input.features.rag,
enrichment: input.features.enrichment,
reputation: input.features.reputation,
cultureProfile: input.features.cultureProfile,
},
};
return `${AI_REVIEW_CACHE_INPUT_VERSION}:${await sha256Hex(stableStringify(payload))}`;
}
Expand Down
66 changes: 66 additions & 0 deletions src/review/repo-culture-profile-wire.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Repo quality-culture profile wiring (#2995): feeds the AI reviewer a compact, additive grounding block
// derived from the repo's OWN merge history (typical PR size, common accepted labels) so a verdict reads as
// grounded in how THIS repo actually operates, instead of generic boilerplate. Exactly the same shape/seam as
// `./rag-wire.ts` (retrieval) and `./grounding-wire.ts` (CI/file grounding): a thin HOST adapter over the
// self-contained, fixture-testable extractor (`./repo-culture-profile.ts`), splicing a pre-formatted block into
// the reviewer's USER prompt as reference context only.
//
// Two independent switches, same precedence as every other converged review knob in this codebase (see
// `review/feature-activation.ts`'s doc comment): a GLOBAL env kill-switch (GITTENSORY_REVIEW_CULTURE_PROFILE,
// default OFF) gates whether the capability exists AT ALL, and the per-repo `.gittensory.yml`
// `review.culture_profile` boolean (see signals/focus-manifest.ts) opts a specific repo in once the global
// switch is on. Both default OFF/absent ⇒ this module is never invoked, no D1 read happens, and the reviewer
// prompt is byte-identical to today.
//
// ADVISORY GROUNDING ONLY (house rule + #2995 requirement): this NEVER becomes a gate/scoring input. It only
// ever appends a reference-only block to the AI reviewer's USER prompt, exactly like the RAG/grounding/
// enrichment sections it sits alongside in `services/ai-review.ts`'s buildUserPrompt.
import { extractRepoCultureProfile, type RepoCultureProfile } from "./repo-culture-profile";
import { neutralizePromptInjection } from "./prompt-injection";

/** True when the culture-profile grounding capability is enabled at all. Flag-OFF (default) → the per-repo
* override below is never even consulted (mirrors isRagEnabled / isGroundingEnabled / isReputationEnabled). */
export function isRepoCultureProfileEnabled(env: { GITTENSORY_REVIEW_CULTURE_PROFILE?: string | undefined }): boolean {
return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_CULTURE_PROFILE ?? "");
}

/** Format a present profile into the reviewer-prompt block. Mirrors `formatRetrievedContext`'s
* self-labelled, reference-only framing so the model treats it the same way it treats RAG context. */
export function formatRepoCultureProfileSection(profile: RepoCultureProfile): string {
if (!profile.present) return "";
const { pullRequestNorms, commonLabels } = profile;
const lines = [
"=== REPO QUALITY-CULTURE PROFILE (reference, NOT a rule — derived from this repo's own merge history) ===",
`Based on ${pullRequestNorms.sampleSize} recently merged pull request(s) in this repository:`,
`- Typical merged PR size: ${pullRequestNorms.medianSizeBand} (median ${pullRequestNorms.medianChangedFiles} changed file(s)).`,
`- Typical PR description length: ~${pullRequestNorms.medianDescriptionLength} characters.`,
];
if (commonLabels.length > 0) {
// entry.label is author/maintainer-controlled GitHub label text from merged PRs -- neutralize it the same
// way safeReviewTitle neutralizes an untrusted PR title before it reaches the reviewer prompt (#271).
const labelSummary = commonLabels
.map((entry) => `${neutralizePromptInjection(entry.label).text} (${Math.round(entry.frequency * 100)}%)`)
.join(", ");
lines.push(`- Common labels on merged PRs: ${labelSummary}.`);
}
lines.push(
Comment thread
JSONbored marked this conversation as resolved.
"Use this ONLY as soft context for what's typical here (e.g. don't flag a PR as unusually large if it matches this repo's own norm); it is NOT a rule and must never be treated as a blocker on its own.",
"=== END REPO QUALITY-CULTURE PROFILE ===",
);
return lines.join("\n");
}

/**
* Build the culture-profile grounding block to splice into the AI reviewer's USER prompt (flag-gated by the
* CALLER via `isRepoCultureProfileEnabled` + the per-repo `review.culture_profile` override, fully fail-safe).
* Returns "" — and the prompt stays byte-identical — whenever the profile is insufficient-data or anything
* errors. This NEVER throws.
*/
export async function buildRepoCultureProfileContext(env: Env, repoFullName: string): Promise<string> {
try {
const profile = await extractRepoCultureProfile(env, repoFullName);
return formatRepoCultureProfileSection(profile);
} catch {
return ""; // any error → review proceeds without this grounding (fail-safe)
}
}
Loading
Loading