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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ node_modules
.devkit/prefix-cache.json
.devkit/decisions-verdict-cache.json
.devkit/review-cache.json
.devkit/sentry-verdict-cache.json
.devkit/review-progress-*.json
.devkit/review-runs/
.devkit/last-ship-gates-*.log
Expand Down
4 changes: 3 additions & 1 deletion cli/__tests__/review-cache-session.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,15 @@ describe('review cache session', () => {
expect(prepared.status, prepared.stderr.toString()).toBe(0);
expect(prepared.stdout.toString().split('\0')).toEqual([
'devkit-review-cache-session-v1',
'3',
'4',
'review-cache.json',
'',
'decisions-verdict-cache.json',
'',
'prefix-cache.json',
'',
'sentry-verdict-cache.json',
'',
'',
]);

Expand Down
1 change: 1 addition & 0 deletions cli/lib/install/gitignore-cache.mts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const DEVKIT_CACHE_IGNORES = [
'.devkit/prefix-cache.json',
'.devkit/decisions-verdict-cache.json',
'.devkit/review-cache.json',
'.devkit/sentry-verdict-cache.json',
'.devkit/review-progress-*.json',
'.devkit/review-runs/',
'.devkit/last-ship-gates-*.log',
Expand Down
10 changes: 5 additions & 5 deletions cli/lib/ship/review-target.sh
Original file line number Diff line number Diff line change
Expand Up @@ -752,14 +752,14 @@ node "$CACHE_SESSION_TOOL" prepare "$PERSISTENT_CACHE_ROOT" "$PRIVATE_DATA_ROOT"
> "$CACHE_FIELDS_FILE"
CACHE_FIELDS=()
while IFS= read -r -d '' field; do CACHE_FIELDS+=("$field"); done < "$CACHE_FIELDS_FILE"
[ "${#CACHE_FIELDS[@]}" -eq 8 ] && \
[ "${#CACHE_FIELDS[@]}" -eq 10 ] && \
[ "${CACHE_FIELDS[0]}" = devkit-review-cache-session-v1 ] && \
[ "${CACHE_FIELDS[1]}" = 3 ] || {
[ "${CACHE_FIELDS[1]}" = 4 ] || {
echo 'devkit review: cache session returned a malformed protocol.' >&2
exit 1
}
CACHE_NAMES=("${CACHE_FIELDS[2]}" "${CACHE_FIELDS[4]}" "${CACHE_FIELDS[6]}")
CACHE_GENERATIONS=("${CACHE_FIELDS[3]}" "${CACHE_FIELDS[5]}" "${CACHE_FIELDS[7]}")
CACHE_NAMES=("${CACHE_FIELDS[2]}" "${CACHE_FIELDS[4]}" "${CACHE_FIELDS[6]}" "${CACHE_FIELDS[8]}")
CACHE_GENERATIONS=("${CACHE_FIELDS[3]}" "${CACHE_FIELDS[5]}" "${CACHE_FIELDS[7]}" "${CACHE_FIELDS[9]}")

export DEVKIT_RUN_MODE=review
export DEVKIT_REVIEW_GUARDS="$GUARDS"
Expand Down Expand Up @@ -901,7 +901,7 @@ review_phase cache-promote
CACHE_RESET=0
if [ "$AUTHORITY_OK" -eq 1 ]; then
index=0
while [ "$index" -lt 3 ]; do
while [ "$index" -lt "${#CACHE_NAMES[@]}" ]; do
promotion_status=0
node "$CACHE_SESSION_TOOL" promote "$PERSISTENT_CACHE_ROOT" "$PRIVATE_DATA_ROOT" \
"${CACHE_NAMES[$index]}" "${CACHE_GENERATIONS[$index]}" >/dev/null || promotion_status=$?
Expand Down
6 changes: 2 additions & 4 deletions cli/lib/ship/review/cache/session.mts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '../../../../../gate-engine/judge/verdict-store.mts';
import { runDirectReviewCli } from '../run-direct.mts';
import { reviewPathWithin } from '../runtime-paths.mts';
import { fail } from '../shared/common.mts';

const PREPARE_PROTOCOL = 'devkit-review-cache-session-v1';
const PROMOTION_PROTOCOL = 'devkit-review-cache-promotion-v1';
Expand All @@ -21,6 +22,7 @@ export const REVIEW_CACHE_STORE_NAMES = [
'review-cache.json',
'decisions-verdict-cache.json',
'prefix-cache.json',
'sentry-verdict-cache.json',
] as const;

export type ReviewCacheStoreName = (typeof REVIEW_CACHE_STORE_NAMES)[number];
Expand All @@ -31,10 +33,6 @@ export interface ReviewCacheCheckpoint {
generation: string | null;
}

function fail(message: string): never {
throw new Error(`devkit review: ${message}`);
}

function physicalRoot(requestedPath: string, label: string): string {
if (!requestedPath || requestedPath.includes('\0'))
fail(`${label} must be a physical directory.`);
Expand Down
1 change: 1 addition & 0 deletions dist/cli/lib/install/gitignore-cache.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const DEVKIT_CACHE_IGNORES = [
'.devkit/prefix-cache.json',
'.devkit/decisions-verdict-cache.json',
'.devkit/review-cache.json',
'.devkit/sentry-verdict-cache.json',
'.devkit/review-progress-*.json',
'.devkit/review-runs/',
'.devkit/last-ship-gates-*.log',
Expand Down
10 changes: 5 additions & 5 deletions dist/cli/lib/ship/review-target.sh
Original file line number Diff line number Diff line change
Expand Up @@ -752,14 +752,14 @@ node "$CACHE_SESSION_TOOL" prepare "$PERSISTENT_CACHE_ROOT" "$PRIVATE_DATA_ROOT"
> "$CACHE_FIELDS_FILE"
CACHE_FIELDS=()
while IFS= read -r -d '' field; do CACHE_FIELDS+=("$field"); done < "$CACHE_FIELDS_FILE"
[ "${#CACHE_FIELDS[@]}" -eq 8 ] && \
[ "${#CACHE_FIELDS[@]}" -eq 10 ] && \
[ "${CACHE_FIELDS[0]}" = devkit-review-cache-session-v1 ] && \
[ "${CACHE_FIELDS[1]}" = 3 ] || {
[ "${CACHE_FIELDS[1]}" = 4 ] || {
echo 'devkit review: cache session returned a malformed protocol.' >&2
exit 1
}
CACHE_NAMES=("${CACHE_FIELDS[2]}" "${CACHE_FIELDS[4]}" "${CACHE_FIELDS[6]}")
CACHE_GENERATIONS=("${CACHE_FIELDS[3]}" "${CACHE_FIELDS[5]}" "${CACHE_FIELDS[7]}")
CACHE_NAMES=("${CACHE_FIELDS[2]}" "${CACHE_FIELDS[4]}" "${CACHE_FIELDS[6]}" "${CACHE_FIELDS[8]}")
CACHE_GENERATIONS=("${CACHE_FIELDS[3]}" "${CACHE_FIELDS[5]}" "${CACHE_FIELDS[7]}" "${CACHE_FIELDS[9]}")

export DEVKIT_RUN_MODE=review
export DEVKIT_REVIEW_GUARDS="$GUARDS"
Expand Down Expand Up @@ -901,7 +901,7 @@ review_phase cache-promote
CACHE_RESET=0
if [ "$AUTHORITY_OK" -eq 1 ]; then
index=0
while [ "$index" -lt 3 ]; do
while [ "$index" -lt "${#CACHE_NAMES[@]}" ]; do
promotion_status=0
node "$CACHE_SESSION_TOOL" promote "$PERSISTENT_CACHE_ROOT" "$PRIVATE_DATA_ROOT" \
"${CACHE_NAMES[$index]}" "${CACHE_GENERATIONS[$index]}" >/dev/null || promotion_status=$?
Expand Down
5 changes: 2 additions & 3 deletions dist/cli/lib/ship/review/cache/session.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { join, resolve } from 'node:path';
import { loadEntries, replaceEntries, saveEntriesIfGeneration, verdictStoreGeneration, } from "../../../../../gate-engine/judge/verdict-store.mjs";
import { runDirectReviewCli } from "../run-direct.mjs";
import { reviewPathWithin } from "../runtime-paths.mjs";
import { fail } from "../shared/common.mjs";
const PREPARE_PROTOCOL = 'devkit-review-cache-session-v1';
const PROMOTION_PROTOCOL = 'devkit-review-cache-promotion-v1';
const STABLE_READ_ATTEMPTS = 8;
Expand All @@ -12,10 +13,8 @@ export const REVIEW_CACHE_STORE_NAMES = [
'review-cache.json',
'decisions-verdict-cache.json',
'prefix-cache.json',
'sentry-verdict-cache.json',
];
function fail(message) {
throw new Error(`devkit review: ${message}`);
}
function physicalRoot(requestedPath, label) {
if (!requestedPath || requestedPath.includes('\0'))
fail(`${label} must be a physical directory.`);
Expand Down
100 changes: 100 additions & 0 deletions dist/gate-engine/judge/diff-focus.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,103 @@ export function focusHunks(diff, isRelevant, omitNoun = 'unrelated') {
const note = omitted ? `[${omitted} ${omitNoun} hunk(s) omitted]\n` : '';
return `${header}${note}${kept.join('\n')}`.trim();
}
// A purely-additive Sentry instrumentation line — matched CONSERVATIVELY, because every match is
// erased from the cache identity and can therefore never invalidate an earned verdict:
// - The call must be `Sentry.`-QUALIFIED. NO bare names, wrappers included: a bare
// `captureException(...)` or `captureMainMessage(...)` from an arbitrary module may be a local
// function carrying real logic and must invalidate — origin cannot be checked from one diff
// line, so unqualified calls simply cost a re-review.
// - Arguments must be inert: identifier/member chains or plain string literals, comma-separated.
// No nested calls (`captureException(mutate(err))` is REAL logic riding along), no template
// literals, no object payloads — a multi-line capture's opening line ends in `{`/`(` and fails
// the whole-line match anyway.
// - Imports ARE origin-checked, so they may name the wrapper: anything from an `@sentry/*`
// package, or a named import of only the recognized capture names from a path naming sentry.
const SENTRY_ARG = /(?:[\w$.]+|'[^']*'|"[^"]*")/;
const SENTRY_CAPTURE_RE = new RegExp(`^(?:await\\s+)?(?:void\\s+)?Sentry\\.capture(?:Exception|Message)\\(\\s*(?:${SENTRY_ARG.source}(?:\\s*,\\s*${SENTRY_ARG.source})*\\s*)?\\);?$`);
// The named-import alternative's path must NAME sentry as its final segment's leading word
// (`./sentry`, `../lib/sentry-client`, `electron/sentry.main`) — a substring match would also strip
// e.g. `../utils/presentry-shim`, a local module whose real logic must invalidate.
const SENTRY_IMPORT_RE = /^import\s+(?:type\s+)?(?:\*\s+as\s+\w+|\w+|\{[^}]*\})\s+from\s+['"]@sentry\/[^'"]+['"];?$|^import\s+\{\s*capture(?:Exception|Message|MainMessage)(?:\s*,\s*capture(?:Exception|Message|MainMessage))*\s*\}\s+from\s+['"](?:[^'"]*\/)?[sS]entry(?:[-.][^'"]*)?['"];?$/;
const HUNK_HEADER_RE = /^@@ /;
const HUNK_FUNC_RE = /^@@ [^@]*@@ ?/;
const HUNK_OLD_START_RE = /^@@ -(\d+)/;
const INDEX_LINE_RE = /^index [0-9a-f]+\.\.[0-9a-f]+/;
function sentryAdditive(content) {
return SENTRY_CAPTURE_RE.test(content) || SENTRY_IMPORT_RE.test(content);
}
/**
* The diff's CACHE IDENTITY: the text hashed into a gate's verdict-cache key, normalized so that a
* restage whose only delta is purely-additive Sentry instrumentation (the exact fix the sentry gate
* blocks for) hashes identically to the pre-fix diff — earned reviewer PASSes survive the fix
* commit instead of re-billing the whole fleet for one capture line.
*
* Why headers AND context must go: inserting one line shifts every later `@@` coordinate, can
* extend a hunk's trailing context, materialize a brand-new hunk (~6 context lines the old diff
* never showed), or merge adjacent hunks — so any surviving context/`@@`/`index` byte would still
* miss. What remains per file is its header identity plus the ordered `±` content lines, with
* sentry-additive `+` lines dropped (`-` lines NEVER dropped: removing instrumentation is a real
* change). A file whose surviving body is empty (touched only by sentry lines) drops entirely.
*
* What survives per file: its header identity, plus each hunk's anchor followed by that hunk's
* surviving `±` lines. The anchor is git's FUNCTION CONTEXT (the text after the second `@@` —
* stable under line-number shifts); where git emits none (JSON, hunks above a file's first
* anchor-matching line) it is the hunk's OLD-side start line, which a purely-additive restage
* cannot move (both diffs share the pre-image). Consecutive identical anchors collapse to one, so
* two same-function hunks merging (a capture inserted between them) keeps the identity stable,
* while relocating a change into a different function — or, for anchorless types, a different
* old-side position — voids the key. Deliberate residual weakening: context bytes and hunked
* files' blob shas are excluded, so relocating a change WITHIN one anchor span (same anchor, same
* `±` sequence) does not invalidate — a narrow residual, and every consumer still salts the key
* with reviewer identity, Targets, lens group, and devkit version. NOT for judge evidence —
* judges read the raw diff.
*
* Fixpoint guarantee: a hunk-less segment (binary / mode-only / rename-only / non-diff text)
* passes through VERBATIM, `index` shas included — a binary blob's sha is its only content
* identity — so unexpected input degrades to exact-bytes keying.
*/
export function diffCacheIdentity(diff) {
const out = [];
for (const seg of splitDiffByFile(diff)) {
const lines = seg.split('\n');
const firstHunk = lines.findIndex((l) => HUNK_HEADER_RE.test(l));
if (firstHunk === -1) {
// Hunk-less segment (binary / mode-only / rename-only / non-diff text): keep it VERBATIM,
// `index` shas included — for a git-binary file those shas are the only content identity,
// and they move only when the blob does. This is the exact-bytes degradation path.
if (seg.trim())
out.push(seg.trim());
continue;
}
const header = lines.slice(0, firstHunk).filter((l) => !INDEX_LINE_RE.test(l));
const body = [];
let anchor = ''; // current hunk's anchor; emitted lazily, deduped consecutively
let emitted = null;
for (const line of lines.slice(firstHunk)) {
if (HUNK_HEADER_RE.test(line)) {
// Anchor on git's function context; where git emits none (JSON, top-of-file hunks, .sh
// preambles), fall back to the OLD-side start line — stable across a purely-additive
// restage (the pre-image is shared), yet distinct across relocations within the file.
const func = line.replace(HUNK_FUNC_RE, '');
anchor = func ? `@ ${func}` : `@ :${line.match(HUNK_OLD_START_RE)?.[1] ?? '?'}`;
continue;
}
if (line.startsWith('-') || (line.startsWith('+') && !sentryAdditive(line.slice(1).trim()))) {
// `+++` can't reach here — file headers all precede the first `@@`, so every `+` is an add.
if (anchor !== emitted) {
body.push(anchor);
emitted = anchor;
}
body.push(line);
}
// everything else — ` ` context, blank context, `\ No newline` — is dropped
}
if (body.length)
out.push([...header, ...body].join('\n'));
}
// A diff whose EVERY line normalized away (a wholly-sentry-additive commit) must not collapse to
// the one shared empty identity — two unrelated capture-only commits would collide on a key and
// share a verdict/waiver. There is no prior verdict such a commit could converge to anyway, so
// degrade it to exact-bytes keying.
return out.length ? out.join('\n') : String(diff);
}
9 changes: 7 additions & 2 deletions dist/gate-engine/review/lens/split.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
* name, and gate-verdict-attribution expects ONE review_result row per reviewer — so renaming the
* derived clones would void every committed waiver and split the telemetry in two.
*/
import { diffCacheIdentity } from "../../judge/diff-focus.mjs";
import { emitGateEvent } from "../../judge/gate-events.mjs";
import { composeTranscript, saveTranscript } from "../../judge/transcript-store.mjs";
import { itemFields, mergeItemVectors } from "../evidence/items.mjs";
Expand Down Expand Up @@ -239,17 +240,21 @@ export function planReviewWork(selected, diffs, cache, salts, keyOf, groups = re
const sel = selected[i];
const name = sel.reviewer.name;
const salt = salts.get(name) ?? '';
// Keys hash the diff's CACHE IDENTITY (sentry-additive lines normalized out) so a restage whose
// only delta is the capture the sentry gate demanded keeps every earned PASS. Judges, transcripts
// and scope rows still get the RAW diffs[i] — only the key input is normalized.
const idText = diffCacheIdentity(diffs[i]);
const split = groups && name === 'correctness-reviewer' && sel.reviewer.skill ? groups : null;
const parts = split
? split.map((g) => ({
sel: { ...sel, reviewer: deriveLensReviewer(sel.reviewer, g) },
key: keyOf(name, diffs[i], `${salt}|split:${lensGroupId(g)}`),
key: keyOf(name, idText, `${salt}|split:${lensGroupId(g)}`),
diffText: diffs[i],
splitOf: name,
group: lensGroupId(g),
base: sel,
}))
: [{ sel, key: keyOf(name, diffs[i], salt), diffText: diffs[i], base: sel }];
: [{ sel, key: keyOf(name, idText, salt), diffText: diffs[i], base: sel }];
const allCached = parts.every((p) => Boolean(cache[p.key]));
scope.push({ sel, diff: diffs[i], cached: allCached });
if (allCached) {
Expand Down
Binary file modified dist/gate-engine/review/overrides.mjs
Binary file not shown.
23 changes: 13 additions & 10 deletions dist/gate-engine/sentry/check-sentry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,9 @@
* The diff directive also self-clears a fix that ELIMINATES the silent path (measured on the
* elimination tier: 15/23 -> 19/23 with the clause, no real-slice recall loss under K=3).
* - few-shot >> zero-shot, chain-of-thought does not help commit classification (arXiv 2605.02033).
* - self-consistency (sample N, majority-vote) reliably lifts a model; reasoning tiers give no
* advantage and are slow (arXiv 2510.22389) — so prefer *_SENTRY_SAMPLES over a reasoning tier.
* The eval/ benchmark sweeps {model, context, shots, samples}; the env defaults below are the cell the
* seed corpus picks (haiku + diff — see the CONTEXT_TIER note) — re-run the sweep on your own corpus.
* - self-consistency (majority vote) lifts a model; reasoning tiers don't and are slow (arXiv
* 2510.22389) — prefer *_SENTRY_SAMPLES. The eval/ benchmark sweeps {model, context, shots,
* samples}; the defaults below are the seed corpus's pick (haiku + diff) — re-sweep on your own.
*
* --gate : exit 0 = SKIP / warn-only / skipped / fail-open · exit 1 = hard mode + confident MONITOR · exit 2 = could-not-run
* (no flag) : report mode — judge the given message and PRINT the verdict, exit 0.
Expand All @@ -60,9 +59,8 @@
*
* GOVERNING RULE (devkit "ship the generator, never the data"): every runtime path resolves against
* the CONSUMER cwd, never __dirname. The WATCHLIST + the BASELINE stay the consumer's data (born in
* their repo, never shipped). The eval `cases.jsonl` DOES ship — 127 cases (104 real-derived + 23
* authored elimination-tier) — but it is a
* dev-only SEED the gate never reads at runtime; a consumer copies + grows it with their own commits.
* their repo, never shipped). The eval `cases.jsonl` DOES ship (127 cases: 104 real-derived + 23
* authored elimination-tier) but is a dev-only SEED the gate never reads at runtime.
*/
import { execSync } from 'node:child_process';
import { appendFileSync, existsSync, readFileSync, realpathSync } from 'node:fs';
Expand All @@ -73,6 +71,7 @@ import { focusHunks } from "../judge/diff-focus.mjs";
import { JUDGE_ISOLATION, JUDGE_READ_ONLY } from "../judge/judge-isolation.mjs";
import { reportGateInfraFailure } from "../judge/odb-probe.mjs";
import { execJudge } from "../judge/run-judge.mjs";
import { judgeSentryWithCache } from "./verdict-cache.mjs";
// Read a GUARD_* env var, falling back to its FRINK_* alias for back-compat with the original frink
// gate. Mirrors the config loader's envVar so every devkit gate reads env the same way.
function envVar(name) {
Expand Down Expand Up @@ -412,9 +411,13 @@ export function run(gate) {
// Hard-by-default (envBool distinguishes unset → hard from an explicit =0 soften); resolve it
// BEFORE judging so the samples default can follow it. Report mode never blocks → warn tier.
const hard = gate && effectiveHard(envBool('SENTRY_HARD') ?? true, CONTEXT_TIER, diff);
const result = judge(buildContext(message, nameStatus, diff, CONTEXT_TIER), {
samples: resolveSamples(hard),
});
const input = buildContext(message, nameStatus, diff, CONTEXT_TIER);
const opts = { model: MODEL, samples: resolveSamples(hard), prompt: SENTRY_JUDGE_PROMPT };
// Diff-tier only: message/names evidence can't change with the demanded FIX, so a cached hard
// MONITOR would replay forever there. A bypassed run (SENTRY_NO_LLM) earns and replays nothing.
const result = envVar('SENTRY_NO_LLM') || CONTEXT_TIER !== 'diff'
? judge(input, opts)
: judgeSentryWithCache(CWD, input, opts, () => judge(input, opts));
if (!gate) {
console.log(reportLine(result));
process.exit(0);
Expand Down
Loading
Loading