Skip to content

feat(review): commit message as advisory reviewer context (sc-1442) - #334

Merged
norvalbv merged 1 commit into
mainfrom
feat/sc-1442-commit-msg-context
Aug 4, 2026
Merged

feat(review): commit message as advisory reviewer context (sc-1442)#334
norvalbv merged 1 commit into
mainfrom
feat/sc-1442-commit-msg-context

Conversation

@norvalbv

@norvalbv norvalbv commented Aug 4, 2026

Copy link
Copy Markdown
Owner

What

sc-1442 (epic 1435): the domain reviewers judged diffs blind to the change's stated intent — the one context advantage that let the completeness gate catch the frink deny-floor bug that api-security and correctness PASSed. This PR hands them the commit message as advisory context, everywhere a message (or a synthesizable intent) exists:

  • devkit ship (commit-with-gate-capture.sh, shared by ship + reship): writes the composed title\n\nbody to a mktemp file and exports DEVKIT_COMMIT_MSG_FILE before git commit; removed + unset immediately after, above both return sites. Never reads .git/COMMIT_EDITMSG (at pre-commit it holds the previous message) — the loader refuses that basename outright.
  • devkit review (review-target.sh): synthesizes the reviewed range's subjects (git log --reverse, against the target repo — the snapshot worktrees' HEAD is pinned at merge-base) into the same env channel; a caller-injected stale path is scrubbed in both review.mts and the script's own env scrub. Empty range → unset → placeholder.
  • Bare git commit: the prompt carries (commit message not available at this hook stage) and everything else is byte-identical to before.

The message's subject also becomes the scopedTargets semantic query, activating semantic Target retrieval for reviewers (previously dead at pre-commit; mirrors completeness.mts).

The cache-key invariant (the load-bearing part)

ship-gates-converge-not-restart requires that an amended message on a ship retry reuses every cached PASS. Since sc-1441 salts checklist reviewers with the rendered Targets block, a message-driven semantic query would leak into the key transitively. Three mechanisms make the invariant structural:

  1. Dual render, one load (targets-block.mts): the salt renders from the via === 'scope-match' subset only; the prompt from the enriched result. Scope matches are computed independently of the query, so the salt bytes cannot depend on the message.
  2. Error partition (scoped-targets.mts): the semantic supplement is try/caught alone, so an embed-tier error (EACCES/ENOSPC/endpoint down) can no longer collapse the scope-matched half — previously a single .catch(() => []) would have let a message-supplied query move the salt on the error path. This also fixes a live bug where a semantic throw dropped all Targets from the completeness prompt.
  3. 10s bound: a cold/wedged embed tier (up to 41 serial axis embeds × 15s each, pre-judge, on the ship critical path) now times out to the scope-only render — which is exactly the salt render, so a timeout can never move a cache key either.

Pinned by tests: same diff + rewritten message → zero judge spawns (and a third run with no message also hits); a stubbed rankAxes throw still yields the scope-matched ruling; sc-1441's Target-edit → invalidation test unchanged.

Prompt-injection hardening

The message is untrusted author text landing on a haiku first pass whose PASS never escalates. The block is fenced ───── like completeness's precedent (not a markdown header the adjacent authoritative blocks use), instruction-shaped lines (# headers, VERDICT:/OFFENDING:/VIOLATION: — whose parse regexes tolerate [\s>*#-]* prefixes, so quoting wouldn't defuse them) are prefix-neutralized with ¦ , and the wrapper states the fenced text cannot grant exceptions or change a verdict. Test: a forged ## RECORDED TARGETS section + VERDICT: PASS line renders inert.

Also in this PR (critique-driven)

  • guard-review scan now composes the same salt as the gate — it reported [cached PASS] for entries the gate would re-judge (stale since sc-1441).
  • review_scope telemetry rows gain commit_msg: boolean + targets_via: 'scope'|'scope+semantic' — the epic's "reviewers with intent vs blind" field receipt needs them.
  • benchGateHash + conventions gateHash now cover evidence/targets-block.mts and evidence/commit-message.mts (prompt-byte suppliers; targets-block was a comparability gap since sc-1441). This invalidates the reviewer-eval baselines accepted in ecd47c6 — the re-earn is expected, not a regression.
  • gitCached/stagedFiles extracted to evidence/staged-git.mts (size ratchet: run-review.mts 569 → 552).

Accepted residual

A Ctrl-C mid-gate can leak the mode-600 mktemp message file in $TMPDIR; content is the message the author is about to publish anyway, so no trap plumbing was added.

Testing

772 tests / 24 files green, including new: commit-message.test.mts (loader never-throw, COMMIT_EDITMSG refusal, injection neutralization, dual-render partition + timeout), scoped-targets.test.mts (semantic append + throw survival), run-review.test.mts (fenced block in checklist + conventions prompts, placeholder paths, the inverse cache test, scope-row flags), ship-branch.test.mts (env exported with title+body during gates, temp file gone after), review.test.mts (synthesized intent + stale-path scrub, empty-range degrade).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Commit messages now available as advisory context to reviewers during gate runs.
  • Bug Fixes

    • Improved error handling for semantic target ranking to prevent cache invalidation during failures.
  • Tests

    • Added comprehensive test coverage for commit message handling, review flows, and target ranking scenarios.
  • Documentation

    • Updated decision documentation for commit message availability in review gates.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change passes commit intent through temporary files to review gates. The gate engine loads and sanitizes the message, adds it to reviewer prompts as advisory context, separates semantic Target data from cache salts, and preserves scope results when semantic lookup fails.

Changes

Commit intent review flow

Layer / File(s) Summary
Intent file lifecycle
cli/lib/ship/*, cli/commands/review.mts, cli/__tests__/*, docs/decisions/review-gate-in-chain.md
Ship and review flows create, export, clear, and remove DEVKIT_COMMIT_MSG_FILE. Tests cover populated and empty commit ranges, stale paths, and cleanup.
Reviewer evidence and cache contracts
gate-engine/review/evidence/*, gate-engine/review/__tests__/commit-message.test.mts, gate-engine/decisions/*
The gate engine loads commit messages from explicit files, rejects stale COMMIT_EDITMSG files, neutralizes instruction-shaped text, renders separate Target blocks, adds scope telemetry, and keeps cache salts scope-based.
Review gate prompt wiring
gate-engine/review/reviewers.mts, gate-engine/review/run-review.mts, gate-engine/review/cli.mts, gate-engine/review/__tests__/*, gate-engine/review/eval/*, eslint/baselines/size-lines.json
Review planning loads shared context and passes commit-message and Target blocks to checklist and conventions prompts. Tests cover prompt output, placeholders, telemetry, cache stability, and gate hashes.
Semantic Target fail-open behavior
gate-engine/decisions/scoped-targets.mts, gate-engine/decisions/__tests__/scoped-targets.test.mts
Semantic ranking and ruling lookup failures are caught. Scope-matched Targets remain available.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ShipFlow
  participant ReviewContext
  participant TargetLoader
  participant ReviewerPrompts
  participant GateScope
  ShipFlow->>ReviewContext: provide DEVKIT_COMMIT_MSG_FILE
  ReviewContext->>TargetLoader: load message subject and Target blocks
  TargetLoader-->>ReviewContext: return salt block and prompt block
  ReviewContext->>ReviewerPrompts: provide commit message and Target prompt extras
  ReviewerPrompts-->>GateScope: run review with advisory context
  GateScope-->>ReviewContext: emit commit_msg and targets_via fields
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the primary change: exposing commit messages as advisory reviewer context.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sc-1442-commit-msg-context

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The domain reviewers run at pre-commit, where no commit message exists — so they judged
diffs blind to the change's stated intent, the one context advantage that let the
completeness gate catch a real deny-floor bug they PASSed. Now devkit ship writes its
composed message to a temp file and exports DEVKIT_COMMIT_MSG_FILE before git commit;
devkit review synthesizes the reviewed range's subjects into the same channel; a bare
git commit degrades to a placeholder. The message renders as a FENCED, neutralized
advisory block in every judge prompt (forged headers and VERDICT/OFFENDING tokens are
prefix-defused — untrusted author text lands on a haiku pass whose PASS never escalates),
and its subject activates semantic Target retrieval.

The message NEVER enters a reviewer cache key (ship-gates-converge-not-restart): the
Targets block renders twice from one scopedTargets call — the salt from the scope-match
subset only, the prompt from the enriched result — and the partition is structural, not
incidental: scoped-targets catches semantic-tier errors around the supplement alone (also
fixing completeness dropping ALL Targets on a semantic throw), and a 10s bound falls back
to the scope-only render, which IS the salt render. guard-review scan now composes the
same salt as the gate instead of reporting stale '[cached PASS]'. review_scope rows gain
commit_msg/targets_via for the epic's field receipt, and the bench gate hashes now cover
targets-block.mts, commit-message.mts AND staged-git.mts (comparability gaps; the last
was the correctness judge's own in-gate finding on this very commit).

gitCached/stagedFiles moved to evidence/staged-git.mts to keep run-review under its
ratchet (569 → 552).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@norvalbv
norvalbv force-pushed the feat/sc-1442-commit-msg-context branch from 92f57e7 to b92b9b2 Compare August 4, 2026 15:06
@norvalbv
norvalbv merged commit 4ee850f into main Aug 4, 2026
0 of 2 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/lib/ship/commit-with-gate-capture.sh`:
- Around line 100-109: In the commit-message setup flow before the mktemp call,
clear any inherited DEVKIT_COMMIT_MSG_FILE so failures cannot preserve stale
state. Keep the existing temporary-file and cleanup behavior, and add a test
covering an injected DEVKIT_COMMIT_MSG_FILE together with forced temporary-file
creation failure, verifying the placeholder path is used.

In `@gate-engine/review/evidence/commit-message.mts`:
- Around line 43-49: Update loadCommitMessage and the corresponding paths at the
referenced call sites to validate that DEVKIT_COMMIT_MSG_FILE resolves to a
regular file before opening it, then read at most the 2 KB limit and derive both
subject and text from that bounded content. Avoid unbounded readFileSync calls
and preserve the existing null behavior for missing, empty, or COMMIT_EDITMSG
inputs.
- Line 39: Update INSTRUCTION_SHAPED_LINE to normalize Markdown structure before
matching verdict-related tokens, covering block quotes, list markers,
indentation, and fence-marker lines such as > ## RECORDED TARGETS and - ##
RECORDED TARGETS. Preserve detection of existing heading and token-prefixed
forms, and add coverage for quoted, listed, and fenced instruction-shaped lines.

In `@gate-engine/review/evidence/targets-block.mts`:
- Around line 112-126: Update loadReviewerTargetsBlocks to create an
AbortController for the semantic query, pass its signal through scopedTargets
and rankAxes, and abort it when the SEMANTIC_BUDGET_MS timer wins. Ensure
semantic embedding/retrieval observes the signal and terminates promptly before
the scope-only fallback runs. Add a cancellation test using an active operation
with retained handles, not only a permanently pending promise.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b50c235-b66f-46e4-a721-67110b8db80e

📥 Commits

Reviewing files that changed from the base of the PR and between 5862fb8 and b92b9b2.

⛔ Files ignored due to path filters (11)
  • dist/cli/commands/review.mjs is excluded by !**/dist/**
  • dist/cli/lib/ship/commit-with-gate-capture.sh is excluded by !**/dist/**
  • dist/cli/lib/ship/review-target.sh is excluded by !**/dist/**
  • dist/gate-engine/decisions/scoped-targets.mjs is excluded by !**/dist/**
  • dist/gate-engine/review/cli.mjs is excluded by !**/dist/**
  • dist/gate-engine/review/evidence/commit-message.mjs is excluded by !**/dist/**
  • dist/gate-engine/review/evidence/scope.mjs is excluded by !**/dist/**
  • dist/gate-engine/review/evidence/staged-git.mjs is excluded by !**/dist/**
  • dist/gate-engine/review/evidence/targets-block.mjs is excluded by !**/dist/**
  • dist/gate-engine/review/reviewers.mjs is excluded by !**/dist/**
  • dist/gate-engine/review/run-review.mjs is excluded by !**/dist/**
📒 Files selected for processing (21)
  • cli/__tests__/review.test.mts
  • cli/__tests__/ship-branch.test.mts
  • cli/commands/review.mts
  • cli/lib/ship/commit-with-gate-capture.sh
  • cli/lib/ship/review-target.sh
  • docs/decisions/review-gate-in-chain.md
  • eslint/baselines/size-lines.json
  • gate-engine/decisions/__tests__/scoped-targets.test.mts
  • gate-engine/decisions/scoped-targets.mts
  • gate-engine/review/__tests__/commit-message.test.mts
  • gate-engine/review/__tests__/reviewers.test.mts
  • gate-engine/review/__tests__/run-review.test.mts
  • gate-engine/review/cli.mts
  • gate-engine/review/eval/conventions/bench.mts
  • gate-engine/review/eval/reviewers/corpus.mts
  • gate-engine/review/evidence/commit-message.mts
  • gate-engine/review/evidence/scope.mts
  • gate-engine/review/evidence/staged-git.mts
  • gate-engine/review/evidence/targets-block.mts
  • gate-engine/review/reviewers.mts
  • gate-engine/review/run-review.mts

Comment on lines +100 to +109
local msgf=""
msgf=$(mktemp "${TMPDIR:-/tmp}/devkit-ship-msg.XXXXXX" 2>/dev/null) || msgf=""
if [ -n "$msgf" ]; then
if printf '%s\n\n%s\n' "$title" "$body" > "$msgf" 2>/dev/null; then
export DEVKIT_COMMIT_MSG_FILE="$msgf"
else
rm -f -- "$msgf" 2>/dev/null || true
msgf=""
fi
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear inherited commit-message state before creating the temporary file.

If mktemp or printf fails, the function leaves an inherited DEVKIT_COMMIT_MSG_FILE unchanged. The gates can then read stale intent instead of using the placeholder.

Unset the variable before mktemp. Add a test that combines an injected path with a forced temporary-file failure.

Proposed fix
   local msgf=""
+  unset DEVKIT_COMMIT_MSG_FILE
   msgf=$(mktemp "${TMPDIR:-/tmp}/devkit-ship-msg.XXXXXX" 2>/dev/null) || msgf=""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
local msgf=""
msgf=$(mktemp "${TMPDIR:-/tmp}/devkit-ship-msg.XXXXXX" 2>/dev/null) || msgf=""
if [ -n "$msgf" ]; then
if printf '%s\n\n%s\n' "$title" "$body" > "$msgf" 2>/dev/null; then
export DEVKIT_COMMIT_MSG_FILE="$msgf"
else
rm -f -- "$msgf" 2>/dev/null || true
msgf=""
fi
fi
local msgf=""
unset DEVKIT_COMMIT_MSG_FILE
msgf=$(mktemp "${TMPDIR:-/tmp}/devkit-ship-msg.XXXXXX" 2>/dev/null) || msgf=""
if [ -n "$msgf" ]; then
if printf '%s\n\n%s\n' "$title" "$body" > "$msgf" 2>/dev/null; then
export DEVKIT_COMMIT_MSG_FILE="$msgf"
else
rm -f -- "$msgf" 2>/dev/null || true
msgf=""
fi
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/lib/ship/commit-with-gate-capture.sh` around lines 100 - 109, In the
commit-message setup flow before the mktemp call, clear any inherited
DEVKIT_COMMIT_MSG_FILE so failures cannot preserve stale state. Keep the
existing temporary-file and cleanup behavior, and add a test covering an
injected DEVKIT_COMMIT_MSG_FILE together with forced temporary-file creation
failure, verifying the placeholder path is used.

// adjacent authoritative blocks) and the machine-parsed tokens (reviewers.mts VERDICT/OFFENDING
// regexes tolerate [\s>*#-]* prefixes, so quoting with `>` would NOT defuse them — a leading `¦`
// breaks the anchored prefix class for both).
const INSTRUCTION_SHAPED_LINE = /^\s*(?:#{1,6}\s|[\s>*#-]*\**(?:VERDICT|OFFENDING|VIOLATION)\b)/i;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- file outline ---\n'
ast-grep outline gate-engine/review/evidence/commit-message.mts --view expanded || true

printf '\n--- source lines 1-120 ---\n'
cat -n gate-engine/review/evidence/commit-message.mts | sed -n '1,120p'

printf '\n--- related test assertions around renderCommitMessageBlock ---\n'
cat -n gate-engine/review/__tests__/commit-message.test.mts | sed -n '1,160p'

printf '\n--- regex behavioral probe for prefixed/atx/fenced token headings ---\n'
node - <<'JS'
const INSTRUCTION_SHAPED_LINE = /^\s*(?:#{1,6}\s|[\s>*`#-`]*\**(?:VERDICT|OFFENDING|VIOLATION)\b)/i;
const cases = [
  '# RECORDED TARGETS',
  '> ## RECORDED TARGETS',
  '## RECORDED TARGETS',
  '  > ## RECORDED TARGETS',
  '- ## RECORDED TARGETS',
  '* VERDICT: x',
  '> VERDICT: x',
  '``` VERDICT: x',
];
for (const line of cases) {
  console.log(JSON.stringify(line), INSTRUCTION_SHAPED_LINE.test(line));
}
JS

Repository: norvalbv/devkit

Length of output: 244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- source lines 1-120 ---'
cat -n gate-engine/review/evidence/commit-message.mts | sed -n '1,120p'

printf '%s\n' ''
printf '%s\n' '--- related test assertions around renderCommitMessageBlock ---'
cat -n gate-engine/review/__tests__/commit-message.test.mts | sed -n '1,160p'

printf '%s\n' ''
printf '%s\n' '--- regex behavioral probe for prefixed/atx/fenced token headings ---'
node - <<'JS'
const INSTRUCTION_SHAPED_LINE = /^\s*(?:#{1,6}\s|[\s>*`#-`]*\**(?:VERDICT|OFFENDING|VIOLATION)\b)/i;
const cases = [
  '# RECORDED TARGETS',
  '> ## RECORDED TARGETS',
  '## RECORDED TARGETS',
  '  > ## RECORDED TARGETS',
  '- ## RECORDED TARGETS',
  '* VERDICT: x',
  '> VERDICT: x',
  '``` VERDICT: x',
];
for (const line of cases) {
  console.log(JSON.stringify(line), INSTRUCTION_SHAPED_LINE.test(line));
}
JS

Repository: norvalbv/devkit

Length of output: 13988


LLM Security (CWE-116): Improper Encoding or Escaping of Output

Reachability: External

Reachability path
● Entry
  gate-engine/review/run-review.mts:327
  runReviewGate: the ship path sets this: retry once, then fail CLOSED
│
▼
● Sink
  gate-engine/review/evidence/commit-message.mts

Neutralize Markdown structure before prefixing verdict tokens.

INSTRUCTION_SHAPED_LINE skips lines where the heading starts after block-quote or list markers, e.g. > ## RECORDED TARGETS, > ## RECORDED TARGETS, and - ## RECORDED TARGETS. The untrusted-input warning does not turn Active Markdown off. Normalize instruction-shaped Markdown structure before applying token neutralization, and include cases for block quotes, lists, and fence-marker lines.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gate-engine/review/evidence/commit-message.mts` at line 39, Update
INSTRUCTION_SHAPED_LINE to normalize Markdown structure before matching
verdict-related tokens, covering block quotes, list markers, indentation, and
fence-marker lines such as > ## RECORDED TARGETS and - ## RECORDED TARGETS.
Preserve detection of existing heading and token-prefixed forms, and add
coverage for quoted, listed, and fenced instruction-shaped lines.

Comment on lines +43 to +49
export function loadCommitMessage(env: NodeJS.ProcessEnv = process.env): CommitMessage | null {
const file = env.DEVKIT_COMMIT_MSG_FILE;
if (!file || basename(file) === 'COMMIT_EDITMSG') return null;
try {
const text = readFileSync(file, 'utf8').trim();
if (!text) return null;
return { subject: (text.split('\n')[0] ?? '').trim(), text };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Apply the byte limit before reading and querying.

readFileSync loads the complete file before the 2 KB rendering cap applies. A large intent file can consume excessive memory. A FIFO or device path can block the gate indefinitely.

The uncapped first line also becomes the semantic query. Open only regular files and read a bounded number of bytes before deriving subject and text.

Also applies to: 59-62, 92-93

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gate-engine/review/evidence/commit-message.mts` around lines 43 - 49, Update
loadCommitMessage and the corresponding paths at the referenced call sites to
validate that DEVKIT_COMMIT_MSG_FILE resolves to a regular file before opening
it, then read at most the 2 KB limit and derive both subject and text from that
bounded content. Avoid unbounded readFileSync calls and preserve the existing
null behavior for missing, empty, or COMMIT_EDITMSG inputs.

Comment on lines +112 to +126
export async function loadReviewerTargetsBlocks(
cwd: string,
files: string[],
query = '',
): Promise<string> {
const targets = await scopedTargets(files, query, 6, cwd).catch(() => []);
return renderTargets(targets, REVIEWER_TARGETS_FRAMING, 8_192);
): Promise<ReviewerTargetsBlocks> {
let targets: GoverningTarget[] | null = null;
if (query.trim()) {
targets = await Promise.race([
scopedTargets(files, query, 6, cwd).catch((): null => null),
new Promise<null>((resolve) => setTimeout(() => resolve(null), SEMANTIC_BUDGET_MS).unref()),
]);
}
// No query, supplement timed out, or the load itself threw → the scope-only load (cheap, sync
// file reads; scopedTargets already survives semantic-tier errors internally).
if (targets === null) targets = await scopedTargets(files, '', 6, cwd).catch(() => []);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Cancel semantic retrieval when the budget expires.

Promise.race stops awaiting scopedTargets, but it does not cancel the losing operation. Active embedding requests can continue after scope-only fallback. They can retain event-loop resources and consume the semantic service after the 10-second budget.

Pass an AbortSignal through scopedTargets and rankAxes. Abort the semantic operation when the timer expires. Test cancellation with an active operation, not a permanently pending promise without handles.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gate-engine/review/evidence/targets-block.mts` around lines 112 - 126, Update
loadReviewerTargetsBlocks to create an AbortController for the semantic query,
pass its signal through scopedTargets and rankAxes, and abort it when the
SEMANTIC_BUDGET_MS timer wins. Ensure semantic embedding/retrieval observes the
signal and terminates promptly before the scope-only fallback runs. Add a
cancellation test using an active operation with retained handles, not only a
permanently pending promise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant