Skip to content

fix(search,validate): exact-ID rank guarantee across modes + parent-hierarchy cycle detection (GH-281, GH-280) - #282

Merged
unbraind merged 4 commits into
mainfrom
fix/search-exactid-rank-validate-parent-cycle
Jun 18, 2026
Merged

unbraind merged 4 commits into
mainfrom
fix/search-exactid-rank-validate-parent-cycle

Conversation

@unbraind

Copy link
Copy Markdown
Owner

Summary

Two agent-trust correctness fixes for pm's core read and validate primitives, both surfaced from dogfooding the large (1463-item) 2d-bots tracker. Bundled because both determine whether agents can trust pm's primitives (project philosophy: project management = context management). Tracked under Feature pm-rkie (children pm-oqgf, pm-8vul).

GH-281 — exact-ID search rank guarantee in every mode (pm-oqgf)

pm search <exact-id> already ranks the exact match #1 in keyword mode (EXACT_ID_MATCH_SCORE=1000 / SHORT_ID_MATCH_SCORE=900, covered by an existing test). The residual gap was hybrid/semantic mode: combineHybridHits() normalizes keyword scores to [0,1] and blends semantic*w + keyword*(1-w), so at the default hybrid_semantic_weight=0.7 the exact-ID keyword boost caps at 0.3 and a high-semantic body-mention item can out-rank the exact target; in pure semantic mode an exact-ID item with no vector hit vanished entirely.

  • New exact_id_match flag on SearchHit (set on the exact full/short-ID early-return in scoreDocument()).
  • New forceExactIdHitsToTop() reattaches exact-ID hits in a reserved band above every blended hit (full-ID above short-ID), applied at both the semantic and hybrid return points (after rerank), plus a score-threshold exemption — so the exact target is always #1 and never dropped, in every mode.

GH-280 — parent-hierarchy cycle detection in pm validate (pm-8vul)

pm validate ran Tarjan SCC only over the dependency graph (blocked_by/definition_of_ready) and never traversed the parent composition graph, so a parent cycle (A<->B, or any-length ring) reported dependency_cycle_count: 0 with no warning while silently breaking pm list --tree, orphan detection, and rollups.

  • buildLifecycleParentGraph (child→parent over all items — a parent cycle among closed items is still corruption) + detectLifecycleParentCycles, reusing the generic findLifecycleDependencyCycleComponents Tarjan helper.
  • Folds parent_cycle_* diagnostics into the lifecycle check, emits validate_hierarchy_parent_cycle[_error] warnings, adds --parent-cycle-severity off|warn|error (default warn, mirrors dependency-cycle policy) with full contract/help/completion wiring, and a remediation entry (pm update <id> --parent <other-id>).

Verification

  • pnpm typecheck clean; full suite 4536 tests pass; coverage 100/100/100/100 (29079 stmts, 22771 branches); quality:static + contracts:check green.
  • Live sandbox: parent A<->B cycle → validate_hierarchy_parent_cycle:1 (warn, ok:true) / _error:1 (error, ok:false, exit 1), sample path A->B->A; off suppresses; normal hierarchy = 0 false positives; fix-hint surfaces the remediation; exact-ID search still scores 1000 at #1.

Closes #281, closes #280.

🤖 Generated with Claude Code

unbraind and others added 2 commits June 18, 2026 12:21
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019JDymqzh8VsuJzFqtit5PD
… + detect parent-hierarchy cycles

Two agent-trust correctness fixes for pm's core read/validate primitives,
surfaced from dogfooding the large 2d-bots tracker (pm-rkie).

GH-281 (pm-oqgf): exact-ID search was only guaranteed #1 in keyword mode.
combineHybridHits() normalizes keyword scores to [0,1] and blends them under
hybrid_semantic_weight, so an exact full/short-ID match's keyword boost
(EXACT_ID_MATCH_SCORE=1000 / SHORT_ID_MATCH_SCORE=900) was capped at the
keyword weight and could be out-ranked by a high-semantic body mention; in pure
semantic mode an exact-ID item with no vector hit vanished entirely. Added an
exact_id_match flag on SearchHit and forceExactIdHitsToTop(), applied at both
the semantic and hybrid return points (after rerank), which reattaches exact-ID
hits in a reserved band above every blended hit (full-ID above short-ID) and
exempts them from the score threshold, so the exact target is always #1 and is
never dropped — in every mode.

GH-280 (pm-8vul): pm validate ran Tarjan SCC only over the dependency graph
(blocked_by/definition_of_ready) and never traversed the parent composition
graph, so a parent cycle (A<->B, or any-length ring) reported
dependency_cycle_count:0 with no warning while silently breaking pm list --tree,
orphan detection, and rollups. Added buildLifecycleParentGraph (child->parent
over ALL items) + detectLifecycleParentCycles reusing the generic
findLifecycleDependencyCycleComponents helper. Folds parent_cycle_* diagnostics
into the lifecycle check, emits validate_hierarchy_parent_cycle[_error]
warnings, adds --parent-cycle-severity off|warn|error (default warn, mirrors
dependency-cycle policy) with full contract/help/completion wiring, and a
remediation registry entry (pm update <id> --parent <other-id>).

Coverage 100/100/100/100 (4536 tests); quality:static + contracts:check green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019JDymqzh8VsuJzFqtit5PD

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @unbraind, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@unbraind

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@unbraind

Copy link
Copy Markdown
Owner Author

/gemini review

@unbraind

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@unbraind, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 25 minutes and 1 second. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 99764ec5-3d18-4fe2-b9ee-6af26d1317c3

📥 Commits

Reviewing files that changed from the base of the PR and between cccc5d2 and 5dc42f6.

📒 Files selected for processing (5)
  • .agents/pm/features/pm-rkie.toon
  • .agents/pm/history/pm-rkie.jsonl
  • src/cli/commands/search.ts
  • src/cli/commands/validate.ts
  • tests/unit/commands/validate-command.spec.ts
📝 Walkthrough

Walkthrough

Two correctness fixes are implemented: GH-281 adds an exact_id_match flag to SearchHit and a forceExactIdHitsToTop helper that promotes exact-ID keyword hits above semantic/hybrid reranked results and bypasses --min-score filtering. GH-280 adds buildLifecycleParentGraph, detectLifecycleParentCycles, and a --parent-cycle-severity option to pm validate for detecting circular parent references using the existing Tarjan SCC helper. Both fixes include CLI surface updates, tests, and agent PM tracking records.

Changes

GH-281: Exact-ID Search Ranking Fix

Layer / File(s) Summary
SearchHit type, scoreDocument flag, forceExactIdHitsToTop, and min-score bypass
src/cli/commands/search.ts
SearchHit gains exact_id_match?: boolean; scoreDocument() sets it on exact-ID hits; forceExactIdHitsToTop() reinserts those hits at a reserved top band with matched_fields: ["id"] in both semantic and hybrid paths; runSearch() retains exact-ID hits regardless of scoreThreshold.
Search ranking regression tests
tests/unit/commands/search-command.spec.ts
Three new tests cover hybrid exact full-ID forced to rank #1 over a higher-semantic rival, full-ID band ranking above short-ID band, and semantic-mode exact-ID ranking #1 with no vector hit and a raised --min-score.
Agent PM issue tracking for GH-281
.agents/pm/issues/pm-oqgf.toon, .agents/pm/history/pm-oqgf.jsonl
New issue file and full JSONL history (create, claim, comment) documenting the root cause, fix approach, and test coverage for the exact-ID ranking bug.

GH-280: Parent Hierarchy Cycle Detection in pm validate

Layer / File(s) Summary
Cycle detection helpers, ValidateCommandOptions, buildLifecycleCheck wiring
src/cli/commands/validate.ts
Adds parentCycleSeverity?: string to ValidateCommandOptions, resolveParentCycleSeverity resolver, buildLifecycleParentGraph, and detectLifecycleParentCycles using Tarjan SCC; extends buildLifecycleCheck to compute parent_cycle_* diagnostics and emit warning/error tokens; wires the resolved severity through runValidate.
CLI registration, flag contracts, completion, help, and remediation
src/cli/register-operations.ts, src/sdk/cli-contracts.ts, src/cli/commands/completion.ts, src/cli/help-content.ts, src/core/diagnostics/remediation.ts, tests/fixtures/contracts/full.json
Registers --parent-cycle-severity <value> in the CLI, adds it to VALIDATE_FLAG_CONTRACTS, updates bash/zsh/fish completion scripts, adds a help example, and adds validate_hierarchy_parent_cycle and validate_hierarchy_parent_cycle_error remediation entries.
Validate parent-cycle unit and integration tests
tests/unit/commands/validate-command.spec.ts, tests/unit/commands/register-commands.spec.ts, tests/unit/commands/completion-command.spec.ts
Adds seedParentCycle helper, edge-branch coverage for severity resolution and graph/detection helpers, and integration tests for 2-item/3-item cycles, severity overrides (error/off), acyclic chains, and unknown severity rejection.
Agent PM issue tracking for GH-280
.agents/pm/issues/pm-8vul.toon, .agents/pm/history/pm-8vul.jsonl
New issue file and JSONL history with create, claim, and two comments covering root cause and implementation summary for parent-cycle detection.

Agent PM Feature and History Metadata

Layer / File(s) Summary
pm-rkie feature record and cross-PR history entries
.agents/pm/features/pm-rkie.toon, .agents/pm/history/pm-rkie.jsonl, .agents/pm/features/pm-6bz1.toon, .agents/pm/history/pm-6bz1.jsonl
Creates pm-rkie feature entry describing both integrity fixes with create/claim/comment history events; updates pm-6bz1 with a merged-to-main audit comment.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • unbraind/pm-cli#40: Both PRs modify src/cli/commands/search.ts to alter semantic/hybrid search behavior at the same search-mode code paths (fallback/ranking logic).
  • unbraind/pm-cli#238: Both PRs change search ranking and --min-score thresholding in src/cli/commands/search.ts, directly overlapping on result ordering and score-filter logic.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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
Title check ✅ Passed The title accurately summarizes the two main changes: exact-ID rank guarantee and parent-hierarchy cycle detection, with corresponding issue references (GH-281, GH-280).
Description check ✅ Passed The description provides clear context for both fixes, explains the problems, solutions, and includes verification results directly aligned with the changeset.
Linked Issues check ✅ Passed Both GH-281 and GH-280 requirements are fully met: exact-ID hits are now forced to rank #1 in all modes via the new exact_id_match flag and forceExactIdHitsToTop function; parent-hierarchy cycles are detected via buildLifecycleParentGraph and detectLifecycleParentCycles with full CLI/contract wiring.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the two linked issues: search ranking fixes, validate cycle detection, CLI contracts, help content, completion flags, and corresponding tests—no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces core read and validate integrity fixes. It guarantees that exact-ID search matches rank first across all search modes (hybrid, semantic, and keyword) by preserving an exact-ID match flag and forcing these hits to the top of the results. Additionally, it adds parent-hierarchy cycle detection to pm validate using Tarjan's SCC algorithm, along with a new --parent-cycle-severity flag and remediation registry entries. Feedback on these changes suggests resolving parent IDs case-insensitively to prevent missing cycle edges due to casing mismatches, and replacing the spread operator in Math.max with Array.prototype.reduce to avoid potential stack overflow errors on large search result sets.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +1230 to +1243
function buildLifecycleParentGraph(items: ItemWithBody[]): Map<string, string[]> {
const knownItemIds = new Set(items.map((item) => item.id));
const graph = new Map<string, string[]>();
const sortedItems = [...items].sort((left, right) => left.id.localeCompare(right.id));
for (const item of sortedItems) {
const edges: string[] = [];
const parentId = toMeaningfulString(item.parent);
if (parentId && knownItemIds.has(parentId)) {
edges.push(parentId);
}
graph.set(item.id, edges);
}
return graph;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Since parent matching was updated to be case-insensitive in PR #279, using a case-sensitive Set (knownItemIds) and knownItemIds.has(parentId) can fail to detect cycle edges if the parent reference casing does not exactly match the canonical item ID casing (e.g., parent: PM-FK49 vs id: pm-fk49). To ensure all cycle edges are correctly captured, we should resolve parent IDs case-insensitively to their canonical IDs.

function buildLifecycleParentGraph(items: ItemWithBody[]): Map<string, string[]> {
  const canonicalIdMap = new Map<string, string>();
  for (const item of items) {
    canonicalIdMap.set(item.id.toLowerCase(), item.id);
  }
  const graph = new Map<string, string[]>();
  const sortedItems = [...items].sort((left, right) => left.id.localeCompare(right.id));
  for (const item of sortedItems) {
    const edges: string[] = [];
    const parentId = toMeaningfulString(item.parent);
    if (parentId) {
      const canonicalParentId = canonicalIdMap.get(parentId.toLowerCase());
      if (canonicalParentId) {
        edges.push(canonicalParentId);
      }
    }
    graph.set(item.id, edges);
  }
  return graph;
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Already addressed in f4cdb23 (this review reflects the earlier commit cccc5d2). buildLifecycleParentGraph now builds a lowercase->canonical id map and resolves parentId case-insensitively, with a regression assertion (uppercase PM-PA resolves to pm-pa). Resolved.

Comment thread src/cli/commands/search.ts Outdated
}
const exactIdIds = new Set(exactIdHits.map((hit) => hit.item.id));
const remaining = rankedHits.filter((hit) => !exactIdIds.has(hit.item.id));
const maxRemainingScore = Math.max(0, ...remaining.map((hit) => hit.score));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using the spread operator (...) with Math.max on an array of arbitrary size can cause a stack overflow (RangeError: Maximum call stack size exceeded) if the array is very large. Since search results can potentially be large before truncation, it is safer to use Array.prototype.reduce to find the maximum score.

Suggested change
const maxRemainingScore = Math.max(0, ...remaining.map((hit) => hit.score));
const maxRemainingScore = remaining.reduce((max, hit) => Math.max(max, hit.score), 0);

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces two core read and validate integrity improvements: parent-hierarchy cycle detection in pm validate (GH-280) and an exact-ID search rank guarantee across hybrid and semantic modes (GH-281). The validation update adds a --parent-cycle-severity flag and leverages Tarjan's SCC algorithm to identify composition loops across all items, while the search update ensures exact full-ID and short-ID matches are prioritized at the top of results. The review feedback is highly constructive, identifying a potential case-sensitivity bug in the parent graph builder that could lead to missed cycles, and recommending robustness improvements in the search ranking helper to avoid call stack size errors on large datasets and preserve original search hit metadata.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +1230 to +1243
function buildLifecycleParentGraph(items: ItemWithBody[]): Map<string, string[]> {
const knownItemIds = new Set(items.map((item) => item.id));
const graph = new Map<string, string[]>();
const sortedItems = [...items].sort((left, right) => left.id.localeCompare(right.id));
for (const item of sortedItems) {
const edges: string[] = [];
const parentId = toMeaningfulString(item.parent);
if (parentId && knownItemIds.has(parentId)) {
edges.push(parentId);
}
graph.set(item.id, edges);
}
return graph;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Since case-insensitive parent matching was introduced and accepted (as noted in the PR description and commit c48f6782), building the parent graph using a case-sensitive Set lookup (knownItemIds.has(parentId)) can lead to false negatives. If an item's parent reference uses a different casing than the target item's ID, the edge will be silently ignored, and parent cycles involving that edge won't be detected.

Using a case-insensitive lookup map (e.g., mapping lowercase IDs to their actual IDs) correctly resolves parent edges regardless of casing.

function buildLifecycleParentGraph(items: ItemWithBody[]): Map<string, string[]> {
  const idLowerToActual = new Map(items.map((item) => [item.id.toLowerCase(), item.id]));
  const graph = new Map<string, string[]>();
  const sortedItems = [...items].sort((left, right) => left.id.localeCompare(right.id));
  for (const item of sortedItems) {
    const edges: string[] = [];
    const parentId = toMeaningfulString(item.parent);
    if (parentId) {
      const actualParentId = idLowerToActual.get(parentId.toLowerCase());
      if (actualParentId) {
        edges.push(actualParentId);
      }
    }
    graph.set(item.id, edges);
  }
  return graph;
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Already addressed in f4cdb23 (this review reflects the earlier commit cccc5d2). buildLifecycleParentGraph now builds a lowercase->canonical id map and resolves parentId case-insensitively, with a regression assertion (uppercase PM-PA resolves to pm-pa). Resolved.

Comment thread src/cli/commands/search.ts Outdated
Comment on lines +1567 to +1581
const maxRemainingScore = Math.max(0, ...remaining.map((hit) => hit.score));
// Full-ID hits (score 1000) must rank above short-ID hits (score 900): rank
// within the exact-ID band by the original keyword score (descending) so a
// full-ID match always precedes a short-ID match for the same query, then
// offset the whole band above every remaining hit. ids are unique, so two
// exact-ID hits can never tie on score — sorting on score alone is total.
const orderedExactHits = [...exactIdHits].sort((left, right) => right.score - left.score);
const bandBase = maxRemainingScore + orderedExactHits.length + 1;
const promoted = orderedExactHits.map((hit, index) => ({
item: hit.item,
// Higher band slot for earlier (higher original keyword score) hits.
score: bandBase - index,
matched_fields: ["id"],
exact_id_match: true,
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

There are two improvement opportunities in this function:

  1. Robustness & Memory Efficiency: Spreading a mapped array into Math.max (i.e., Math.max(0, ...remaining.map(...))) can trigger a RangeError: Maximum call stack size exceeded if the remaining array is very large, and it creates an unnecessary intermediate array. Using reduce is safer and more memory-efficient.
  2. Property Preservation: Re-creating the promoted hit objects manually drops other properties of SearchHit (such as matched_all_terms: true which was explicitly set in scoreDocument). Spreading the original hit (i.e., { ...hit, score: ... }) ensures all metadata is preserved and makes the code more future-proof.
  const maxRemainingScore = remaining.reduce((max, hit) => Math.max(max, hit.score), 0);
  // Full-ID hits (score 1000) must rank above short-ID hits (score 900): rank
  // within the exact-ID band by the original keyword score (descending) so a
  // full-ID match always precedes a short-ID match for the same query, then
  // offset the whole band above every remaining hit. ids are unique, so two
  // exact-ID hits can never tie on score — sorting on score alone is total.
  const orderedExactHits = [...exactIdHits].sort((left, right) => right.score - left.score);
  const bandBase = maxRemainingScore + orderedExactHits.length + 1;
  const promoted = orderedExactHits.map((hit, index) => ({
    ...hit,
    // Higher band slot for earlier (higher original keyword score) hits.
    score: bandBase - index,
    matched_fields: ["id"],
  }));

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
tests/unit/commands/search-command.spec.ts (2)

2887-2911: 💤 Low value

Same variable shadowing pattern as the first test.

The target variable inside the fetch mock shadows the outer test fixture. Same suggestion applies: rename to fetchTarget for consistency and clarity.

🤖 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 `@tests/unit/commands/search-command.spec.ts` around lines 2887 - 2911, The
`target` variable declared inside the fetch mock function shadows an outer test
fixture variable with the same name, reducing code clarity. Rename the `target`
variable to `fetchTarget` throughout the fetch mock function (where it is
assigned from `resolveFetchTarget(url)` and used in the conditional checks with
`endsWith()`). This change will maintain consistency with the naming convention
used elsewhere and eliminate variable shadowing.

2729-2756: 💤 Low value

Variable target shadows the outer test fixture.

The const target = resolveFetchTarget(url) inside the fetch mock shadows the outer target fixture variable defined at line 2685. While technically correct due to different scopes, it reduces readability. Consider renaming to fetchTarget or resolvedUrl for clarity.

♻️ Suggested fix
     globalThis.fetch = (async (url: unknown) => {
-      const target = resolveFetchTarget(url);
-      if (target.endsWith("/v1/embeddings")) {
+      const fetchTarget = resolveFetchTarget(url);
+      if (fetchTarget.endsWith("/v1/embeddings")) {
         return {
           ok: true,
           // ...
         } as unknown as Response;
       }
-      if (target.endsWith("/collections/pm_items/points/search")) {
+      if (fetchTarget.endsWith("/collections/pm_items/points/search")) {
         return {
           // ...
         } as unknown as Response;
       }
-      throw new Error(`Unexpected fetch target: ${target}`);
+      throw new Error(`Unexpected fetch target: ${fetchTarget}`);
     }) as typeof globalThis.fetch;
🤖 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 `@tests/unit/commands/search-command.spec.ts` around lines 2729 - 2756, The
variable `target` inside the fetch mock function shadows the outer `target`
fixture variable, reducing code clarity. Rename the local variable from `target`
to `fetchTarget` or `resolvedUrl` throughout the fetch mock function, updating
both the `const target = resolveFetchTarget(url)` assignment and all subsequent
references to this variable in the endsWith() method calls that check for
"/v1/embeddings" and "/collections/pm_items/points/search" endpoints.
src/cli/commands/search.ts (1)

1980-1984: 💤 Low value

exact_id_match is not stripped from output, contradicting the interface comment.

The SearchHit interface comment (line 242) states exact_id_match is "Not projected into output rows," but projectSearchHits only strips matched_all_terms. For consistency with the GH-181 pattern and the documented contract, exact_id_match should also be excluded from full-mode output.

♻️ Suggested fix
 function projectSearchHits(hits: SearchHit[], projection: SearchProjectionConfig): SearchResultItem[] {
   if (projection.mode === "full") {
     // matched_all_terms is an internal ranking signal (GH-181); strip it from
-    // full-mode output rows so the public hit shape stays { item, score, matched_fields }.
-    return hits.map(({ matched_all_terms: _matchedAllTerms, ...hit }) => hit);
+    // full-mode output rows so the public hit shape stays { item, score, matched_fields }.
+    // exact_id_match is likewise internal (GH-281).
+    return hits.map(({ matched_all_terms: _matchedAllTerms, exact_id_match: _exactIdMatch, ...hit }) => hit);
   }
🤖 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 `@src/cli/commands/search.ts` around lines 1980 - 1984, The projectSearchHits
function currently only strips matched_all_terms from the hit objects in full
mode, but according to the SearchHit interface documentation, exact_id_match
should also not be included in output rows. Update the destructuring pattern in
the hits.map call within projectSearchHits to exclude both matched_all_terms and
exact_id_match from the returned hit objects, similar to how matched_all_terms
is already being stripped using the destructuring assignment pattern.
🤖 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.

Nitpick comments:
In `@src/cli/commands/search.ts`:
- Around line 1980-1984: The projectSearchHits function currently only strips
matched_all_terms from the hit objects in full mode, but according to the
SearchHit interface documentation, exact_id_match should also not be included in
output rows. Update the destructuring pattern in the hits.map call within
projectSearchHits to exclude both matched_all_terms and exact_id_match from the
returned hit objects, similar to how matched_all_terms is already being stripped
using the destructuring assignment pattern.

In `@tests/unit/commands/search-command.spec.ts`:
- Around line 2887-2911: The `target` variable declared inside the fetch mock
function shadows an outer test fixture variable with the same name, reducing
code clarity. Rename the `target` variable to `fetchTarget` throughout the fetch
mock function (where it is assigned from `resolveFetchTarget(url)` and used in
the conditional checks with `endsWith()`). This change will maintain consistency
with the naming convention used elsewhere and eliminate variable shadowing.
- Around line 2729-2756: The variable `target` inside the fetch mock function
shadows the outer `target` fixture variable, reducing code clarity. Rename the
local variable from `target` to `fetchTarget` or `resolvedUrl` throughout the
fetch mock function, updating both the `const target = resolveFetchTarget(url)`
assignment and all subsequent references to this variable in the endsWith()
method calls that check for "/v1/embeddings" and
"/collections/pm_items/points/search" endpoints.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 9579f314-0de5-4751-9217-3ca87fd49cdf

📥 Commits

Reviewing files that changed from the base of the PR and between 0797a40 and cccc5d2.

📒 Files selected for processing (20)
  • .agents/pm/features/pm-6bz1.toon
  • .agents/pm/features/pm-rkie.toon
  • .agents/pm/history/pm-6bz1.jsonl
  • .agents/pm/history/pm-8vul.jsonl
  • .agents/pm/history/pm-oqgf.jsonl
  • .agents/pm/history/pm-rkie.jsonl
  • .agents/pm/issues/pm-8vul.toon
  • .agents/pm/issues/pm-oqgf.toon
  • src/cli/commands/completion.ts
  • src/cli/commands/search.ts
  • src/cli/commands/validate.ts
  • src/cli/help-content.ts
  • src/cli/register-operations.ts
  • src/core/diagnostics/remediation.ts
  • src/sdk/cli-contracts.ts
  • tests/fixtures/contracts/full.json
  • tests/unit/commands/completion-command.spec.ts
  • tests/unit/commands/register-commands.spec.ts
  • tests/unit/commands/search-command.spec.ts
  • tests/unit/commands/validate-command.spec.ts

… resolution + Math.max stack safety

- validate.ts (HIGH): buildLifecycleParentGraph resolves parent refs via a
  lowercase->canonical id map so a casing mismatch (PR #279 made parent
  matching case-insensitive) can never silently drop a cycle edge and hide a
  parent cycle. Added a regression assertion (uppercase PM-PA resolves to pm-pa).
- search.ts (MEDIUM): forceExactIdHitsToTop uses reduce instead of
  Math.max(0, ...spread) to avoid a RangeError on a very large pre-truncation
  candidate set, and promoted hits spread the original keyword hit so
  matched_all_terms / exact_id_match are preserved.

Coverage 100/100/100/100 (4536 tests); static + contracts green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019JDymqzh8VsuJzFqtit5PD
@unbraind

Copy link
Copy Markdown
Owner Author

/gemini review

@unbraind

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces two core read and validate integrity improvements. First, it guarantees that exact-ID search matches always rank first in hybrid and semantic search modes by preserving an exact-ID match flag and forcing those hits to the top of the results. Second, it adds parent-hierarchy cycle detection to the validation suite using a Tarjan SCC algorithm, supported by a new --parent-cycle-severity configuration flag and corresponding remediation suggestions. The review feedback suggests a minor optimization in the search command implementation to avoid a redundant array copy before sorting.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/cli/commands/search.ts Outdated
// full-ID match always precedes a short-ID match for the same query, then
// offset the whole band above every remaining hit. ids are unique, so two
// exact-ID hits can never tie on score — sorting on score alone is total.
const orderedExactHits = [...exactIdHits].sort((left, right) => right.score - left.score);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The exactIdHits array is already a newly allocated array returned by keywordHits.filter(...) on line 1561. Therefore, creating a shallow copy using the spread operator [...exactIdHits] before calling .sort() is redundant. You can safely sort exactIdHits in-place to avoid the unnecessary array allocation.

Suggested change
const orderedExactHits = [...exactIdHits].sort((left, right) => right.score - left.score);
const orderedExactHits = exactIdHits.sort((left, right) => right.score - left.score);

…ID hits

Gemini review: exactIdHits is already a fresh array returned by
keywordHits.filter(...), so the [...exactIdHits] spread before .sort() is an
unnecessary allocation — sort in place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019JDymqzh8VsuJzFqtit5PD
@unbraind

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces two core read and validate integrity features: guaranteeing that exact-ID search matches rank first across all search modes (including hybrid and semantic) by bypassing score normalization and thresholding, and implementing parent-hierarchy cycle detection in pm validate using a Tarjan SCC-based cycle check. It adds the --parent-cycle-severity flag to configure policy severity, registers shell completions, defines remediation steps, and includes comprehensive unit and integration tests for both features. No review comments were provided, and the changes are well-implemented with full test coverage.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@unbraind
unbraind merged commit daada34 into main Jun 18, 2026
14 checks passed
@unbraind
unbraind deleted the fix/search-exactid-rank-validate-parent-cycle branch June 18, 2026 11:40
unbraind added a commit that referenced this pull request Jun 18, 2026
Closes the core read/validate integrity bundle (GH-281 exact-ID search rank
guarantee across modes, GH-280 parent-hierarchy cycle detection) shipped in
PR #282 (daada34), and regenerates CHANGELOG.md from the newly-closed items
via pm-changelog 2026.6.14.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019JDymqzh8VsuJzFqtit5PD
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant