Skip to content
2 changes: 2 additions & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,8 @@ This file is append-only. Never rewrite or delete an existing review record; app
| 2026-07-25 | open-pr-babysit-continuation-20260725 | multipass | Babysit continuation after 14 merges: #1177 landed; #1174/#1178/#1153 in progress; drafts #1187/#1192 skipped; large cluster #1162/#1185/#1186/#1188/#1190 content-conflicted (skip). | merge-tree inventory; no provider-backed checks. |
| 2026-07-25 | audit-remediation (PR #1153) | 5a731df5c25fed9b07fd2321a0ad4b6519471f4b | PR babysit: CodeRabbit thread fixes + merge | Before: MERGEABLE/BLOCKED on required_review_thread_resolution + pending CI; 6 CodeRabbit threads. After: fixed sync-skills pad/YAML escape, PDF temp cleanup, squash-aware rollback wording; dispositioned ledger mid-table + retained false-positive; approved CI; merged to main `191b17d2f` (merge commit); branch deleted; tip is ancestor of main. | Hosted CI green on tip; no provider-backed checks. |
| 2026-07-25 | cursor/local-presence-054-7cf3 (PR #1178) | 9135891bfd194394549cb480a7ec86de12b23ee7 | PR babysit: local-presence + /tools + CI/UI fixes + squash merge | Before: flaky Safety audit on package.json scripts, Production UI Sources autofocus flake, CodeRabbit short-env duplicate thread. After: ci-change-scope lockfile-only; strip stale short env keys; sheet open-focus retries + skip focus=1 reclaim under modal; squash-merged `d08ec2e8e`; branch deleted; key-file content-diff empty. | Hosted PR required SUCCESS (Production UI green on tip); focused local-presence vitest; no provider-backed checks. |
| 2026-07-25 | cursor/search-correctness-030-075-6273 (PR #1177) | 96ba61520aeea59647dcaec6671ccb82618553ef | CORRECTION: real SHA for the 2026-07-24 post-sync #1177 row | That row recorded `96ba6152c1f8e5e0000000000000000000000000`, a zero-padded placeholder that resolves to no Git object. The real commit is `96ba61520aeea59647dcaec6671ccb82618553ef` ("ci: remove PR_POLICY_BODY.md after sync"); the reviewed outcome itself is unchanged. | `git rev-parse` verification; `npm run check:branch-review-ledger` pass; no provider-backed checks run. |
| 2026-07-25 | cursor/eval-matching-distinct-identity-6273 | f5620e3a4a2d60fd8a67fd43d8046726f1761e5f | Self-review of the merged #030/#075 fixes against current main | Before: merged coverage matched expectations to distinct source *positions*, so one document repeated across `answer.citations` still filled both comparison slots, and first-come assignment made `allHit` depend on `expectedFiles` order; `PR_POLICY_BODY.md` was back on main (via #1153) ready to overwrite the next PR body; the #030/#075 queue row survived their archival. After: coverage dedupes by document text and assigns by maximum bipartite matching, label accumulation is linear, the stale template is removed, and #080 records the removed admission widening for approved eval re-test. Both new contracts were verified failing against main's matcher before the fix. RAG impact: no retrieval behaviour change — eval matching and label bookkeeping only. | Focused Vitest 39/39 (eval-document-matching, eval-utils, eval-search, search-scope, rag-imputation-contract); fail-closed proof 2 failed on the `origin/main` matcher; `npm run verify:cheap` pass (3379 passed, 5 skipped); `check:branch-review-ledger` and `check:pr-policy` pass; no OpenAI/Supabase/provider-backed checks run. |
| 2026-07-25 | execute-audit-code-remediation (PR #1162) | d5455837231f5cb6a927e8c4752ef5aa9c72767c | Merge conflict + CI + Bugbot review | Merged origin/main (164 behind). Fixed conflicts in ClinicalDashboard/global-search-shell/mode-home/search-scope/tests/pdf extractor. Renamed duplicate migration 20260724120000→20260724130200. CI: skills openai.yaml, owner-scope setup-status exemption, sitemap, drift hash, answer-render duplicate key, setup-status mock `.eq`. Bugbot: fixed Codex P1 view-only indexing + P2 differential back; also fixed P2 viewer visibility for retained images and duplicate-upload cleanup ledger fail-closed. | Focused Vitest (back-href/worker/skills/setup-status/owner-scope/drift/sitemap/search-scope/favourites/forms/therapy/document-detail/upload-ledger) + typecheck + check:skills/migration-role/sitemap; no provider-backed checks. |
| 2026-07-25 | execute-audit-code-remediation (PR #1162) | f07c711a0e1ee853b128733a51db182d8192c3c4 | CI unblock after Bugbot | Fixed Prettier (11 files), restored package-lock/.npmrc to main so blocking npm audit is advisory (lockfile_changed=false), updated mobile-composer-reserve contract for answer-home hero breakpoint. Prior tip e5c8c49c had Static/Safety/Unit failures. | format:check; focused Vitest 41; ci-change-scope lockfile_changed=false; no provider-backed checks. |
| 2026-07-25 | execute-audit-code-remediation (PR #1162) | 1de1b32f562c0a972997750a5dc97a02ab1a9c15 | Production UI fix | Fixed ui-tools services referral header test (H1/quick-filters contract). Prior tip 09c6eb2d had Static/Safety/Unit/Migration green; only Production UI failed. | Local Playwright chromium services referral test PASS; no provider-backed checks. |
Expand Down
5 changes: 3 additions & 2 deletions docs/outstanding-issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ removed after current-main verification; it is not missing recommended work.
| 31 | `#078` | A3 | Standard — reconciliation tooling | Before another broad reconciliation | 2–4 hours | Generate one deterministic, secret-safe evidence pack from the reconciliation lifecycle: disposition rows, operation markers, archive refs, bundle verification, hashes, worktree counts, and local/base equality. Never fetch, call providers, or delete implicitly. |
| 32 | `#079` | Optional | High — repository hygiene | In explicitly scheduled batches | 30–60 minutes per batch | Disposition at most ten retained worktrees per pass using owner, PR, review-ledger, ancestry, and patch evidence. Preserve every dirty, active, secret-bearing, post-freeze, or ambiguous worktree and stop rather than broad-cleaning. |

<!-- issues:next-id=080 -->
<!-- issues:next-id=081 -->

## Open items

Expand Down Expand Up @@ -133,6 +133,7 @@ removed after current-main verification; it is not missing recommended work.
| #077 | P2 | issue | Concurrent tasks can re-dirty the canonical primary checkout | **Outcome:** `C:\Dev\Apps\Database` remains a clean synchronization target while write work happens in task-owned worktrees. **Next:** add a cooperative owner/lease check to task-start and lifecycle transitions; before a primary write, branch switch, fast-forward, or cleanup, report owner, dirty state, and Git operation markers and fail closed on an active owner. Include stale-lease recovery. **Success:** a focused concurrency test refuses a second primary writer while read-only commands and separate worktrees continue normally. **Stop:** do not add an OS-wide lock, kill processes, discard existing dirty files, or serialize independent feature worktrees. | primary re-dirtied by another active task immediately after reconciliation proof; session 2026-07-24 | 2026-07-24 |
| #078 | P3 | task | Generate a deterministic reconciliation evidence pack | **Outcome:** one report-only command produces the final local evidence now assembled manually. **Next:** extend the reconciliation lifecycle with an explicit output path and include the frozen base/HEAD, per-worktree dispositions, operation markers resolved through Git, archive refs, bundle path/size/SHA-256/verify result, retained worktree count, and local/base tree equality. Accept remote PR state only as explicit approved input. **Success:** fixture tests prove deterministic output and redaction; an interrupted run leaves no false completion record. **Stop:** never fetch, call GitHub/providers, inspect secret values, mutate refs, or delete work implicitly. | `scripts/reconciliation-preflight.mjs`; `docs/reconciliation-playbook.md`; session 2026-07-24 | 2026-07-24 |
| #079 | P3 | task | Disposition retained worktrees in bounded cleanup batches | **Outcome:** the retained reconciliation tail is gradually classified without another disruptive all-worktree sweep. **Next:** process no more than ten worktrees per explicitly scheduled pass using current owner/process metadata, open-PR state, exact review-ledger coverage, ancestry, and cherry-pick-aware content proof. **Success:** remove only clean, inactive, bundled worktrees whose content is merged or explicitly rejected; record every disposition and retain recovery evidence. **Stop:** preserve dirty, active, secret-bearing, post-freeze, paused, or ambiguous work and never use reset, force deletion, broad clean, or process killing. | final reconciliation inventory retained 104 independent worktrees; session 2026-07-24 | 2026-07-24 |
| #080 | P3 | rec | Re-test the removed admission-to-discharge alias widening | **Outcome:** decide with measured evidence whether the user-approved 2026-07-21 widening (NMHS admission-to-discharge titles satisfying the admission expectation) belongs back on the wide-tier `AdmissionCommunityPts` list. #030 removed it so one document could not fill both comparison slots, but coverage now enforces distinct document identities by maximum matching, so the widening is no longer a false-pass route and its removal may instead cost legitimate admission credit. **Next:** with approval, run an eval-canary baseline/post pair over the admission-discharge comparison cases with and without those two titles. **Success:** restore only on measured non-regression (recall pinned 1.0, zero per-case reciprocal-rank regressions); otherwise record keep-removed and close. **Stop:** never restore on offline reasoning alone. | `src/lib/eval-document-matching.ts`; #030 archive row; session 2026-07-25 | 2026-07-25 |

## Resolved / archive

Expand All @@ -143,7 +144,7 @@ Move resolved rows here with the resolution date and a one-line outcome. Keep th
| #066 | task | Land and prove the streamlined six-item sidebar | Proven on `origin/main` via PR #1174 (`4dc76306 Land streamlined six-item sidebar`). Six-item rail shipped; open ledger row was stale post-merge. | 2026-07-25 |
| #067 | issue | Reconciliation preflight test times out under full-suite load | Fixed in PR #1191 (`e2488dbb`): JSON contract calls `collectReconciliationState()` in-process instead of spawning a second Node/Vite graph under full-suite load. No global timeout raise or heavy-test lock bypass. | 2026-07-25 |
| #007 | rec | `/tools` vs `/?mode=tools` parallel Tools entry points | Resolved as `/tools` canonical (PT-11 already documented on `/applications` redirect). Sidebar, appModeHomeHref, universal-search, prefetch, sitemap, and reachability now use `/tools`; `/?mode=tools` remains a dashboard-mode alias. Reachability allowlist entry removed. | 2026-07-24 |
| #030 | issue | Wide-tier alias lets one doc satisfy both comparison slots | Fixed on `cursor/search-correctness-030-075-6273`: removed dual-listed Admission-to-Discharge titles from AdmissionCommunityPts so one retrieved source cannot make allHit true for both comparison slots; fail-closed contracts in `tests/eval-document-matching.test.ts`. RAG impact: no retrieval behaviour change — eval matching only. | 2026-07-24 |
| #030 | issue | Wide-tier alias lets one doc satisfy both comparison slots | Fixed on `cursor/search-correctness-030-075-6273`: removed dual-listed Admission-to-Discharge titles from AdmissionCommunityPts so one retrieved source cannot make allHit true for both comparison slots; fail-closed contracts in `tests/eval-document-matching.test.ts`. RAG impact: no retrieval behaviour change — eval matching only. Hardened after merge: coverage dedupes by document identity and assigns by maximum matching (#080). | 2026-07-24 |
| #075 | issue | Search-scope label enumeration can truncate after 1,000 rows | Fixed on `cursor/search-correctness-030-075-6273`: `loadScopeLabels` pages document_labels with deterministic order/batching past the Supabase 1k cap; multi-page >1000 contracts in `tests/search-scope.test.ts`. Isolated from mixed PR #1132. RAG impact: no retrieval behaviour change — label pagination only. | 2026-07-24 |
| #009 | rec | Confirm `/api/jobs` is intentionally server/ops-only | Kept as deliberate administrator/ops listing: no client `fetch("/api/jobs")` (UI uses `/api/ingestion/jobs`); documented in `docs/api-jobs-ops-surface.md` plus wiring/codebase-index/site-map notes. Not abandoned — do not remove without updating API contract tests. | 2026-07-24 |
| #010 | task | Un-built "Coming soon" controls across forms/favourites | Audited forms/favourites/presentation placeholders: all use honest `disabled` or `aria-disabled` + coming-soon copy (or presentational `ToggleSwitch` without `onToggle`). No fake-interactive controls; leave unwired until features land. Recorded in `docs/wiring-conventions.md`. | 2026-07-24 |
Expand Down
44 changes: 31 additions & 13 deletions src/lib/eval-document-matching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,25 +114,43 @@ export function expectedFileCoverage(
sources: Array<Pick<SearchResult, "file_name" | "title">>,
limit = 3,
): ExpectedFileCoverage {
const topFiles = sources.slice(0, limit).map(resultDocumentText);
// Distinct source identities (#030): each retrieved top-file may satisfy at most one
// expectedFiles slot. Without this, a single combo-titled document (or overlapping
// aliases) can make allHit true even when a true second source is missing.
const usedSourceIndexes = new Set<number>();
const matchedFiles = expectedFiles.filter((expected) => {
// Distinct source identities (#030): dedupe the window by document text, because one
// physical document appears many times in answer.citations (one entry per chunk). Matching
// on positions alone would let the same document fill every expected slot.
const topFiles = Array.from(new Set(sources.slice(0, limit).map(resultDocumentText)));
const candidateSourcesByExpectation = expectedFiles.map((expected) => {
const alternatives = documentExpectationAlternatives(expected);
const matchIndex = topFiles.findIndex(
(file, index) => !usedSourceIndexes.has(index) && alternatives.some((alternative) => file.includes(alternative)),
);
if (matchIndex < 0) return false;
usedSourceIndexes.add(matchIndex);
return true;
return topFiles.reduce<number[]>((indexes, file, index) => {
if (alternatives.some((alternative) => file.includes(alternative))) indexes.push(index);
return indexes;
}, []);
});

// Maximum bipartite matching (Kuhn's augmenting path) rather than first-come assignment:
// a combo-titled document that matches two expectations must not consume the only source a
// narrower expectation could have used, which would make coverage depend on expectedFiles order.
const expectationBySource = new Map<number, number>();
const claimSource = (expectationIndex: number, visitedSources: Set<number>): boolean => {
for (const sourceIndex of candidateSourcesByExpectation[expectationIndex]) {
if (visitedSources.has(sourceIndex)) continue;
visitedSources.add(sourceIndex);
const holder = expectationBySource.get(sourceIndex);
if (holder === undefined || claimSource(holder, visitedSources)) {
expectationBySource.set(sourceIndex, expectationIndex);
return true;
}
}
return false;
};
expectedFiles.forEach((_, expectationIndex) => claimSource(expectationIndex, new Set()));

const matchedExpectations = new Set(expectationBySource.values());
const matchedFiles = expectedFiles.filter((_, index) => matchedExpectations.has(index));

return {
expectedFiles,
matchedFiles,
missingFiles: expectedFiles.filter((expected) => !matchedFiles.includes(expected)),
missingFiles: expectedFiles.filter((_, index) => !matchedExpectations.has(index)),
anyHit: matchedFiles.length > 0,
allHit: expectedFiles.length > 0 && matchedFiles.length === expectedFiles.length,
};
Expand Down
6 changes: 5 additions & 1 deletion src/lib/search-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,11 @@ export async function resolveSearchScope(args: {
const labelRows = await loadScopeLabels({ supabase: args.supabase, candidateIds, signal: args.signal });
labelsByDocument = new Map();
for (const label of labelRows) {
labelsByDocument.set(label.document_id, [...(labelsByDocument.get(label.document_id) ?? []), label]);
// Append in place: paging past the 1,000-row cap means this loop now sees the full label
// set, and rebuilding each document's array per row is quadratic in that volume.
const documentLabels = labelsByDocument.get(label.document_id);
if (documentLabels) documentLabels.push(label);
else labelsByDocument.set(label.document_id, [label]);
}
}

Expand Down
Loading
Loading