Skip to content
Closed
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
5 changes: 1 addition & 4 deletions docs/site-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `/dsm` - DSM-5 Diagnosis home. Source: `src/app/dsm/page.tsx`.
- `/dsm/compare` - DSM diagnosis comparison. Source: `src/app/dsm/compare/page.tsx`.
- `/dsm/search` - DSM diagnosis search and catalogue browser. Source: `src/app/dsm/search/page.tsx`.
- `/factsheets` - Patient information sheet library. Source: `src/app/factsheets/page.tsx`.
- `/factsheets/[slug]` - Patient information sheet detail. Source: `src/app/factsheets/[slug]/page.tsx`.
- `/factsheets/search` - Patient information sheet search. Source: `src/app/factsheets/search/page.tsx`.
- `/favourites` - Saved clinical items and sets. Source: `src/app/favourites/page.tsx`.
- `/forms` - Forms home and search surface. Source: `src/app/forms/page.tsx`.
- `/formulation` - Clinical formulation home and local mechanism search surface. Source: `src/app/formulation/page.tsx`.
Expand Down Expand Up @@ -849,7 +846,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
## Redirects

- `/applications` - Redirects to `/tools`. Source: `src/app/applications/route.ts`.
- `/differentials/presentations` - Redirects to `/differentials/presentations/[slug]`. Source: `src/app/differentials/presentations/route.ts`.
- `/differentials/presentations` - Redirects to `/differentials/presentations/[workflow-slug]`. Source: `src/app/differentials/presentations/route.ts`.
- `/documents/source` - Redirects to `/documents/search`. Source: `src/app/documents/source/page.tsx`.
- `/medications` - Redirects to `/?mode=prescribing`. Source: `src/app/medications/route.ts`.
- `/mockups/favourites-hub` - Redirects to `/favourites`. Source: `src/app/mockups/favourites-hub/page.tsx`.
Expand Down
1 change: 1 addition & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
grepInvert: mockupTag,
use: {
...devices["Desktop Chrome"],
reducedMotion: "no-preference",

Check failure on line 58 in playwright.config.ts

View workflow job for this annotation

GitHub Actions / Static PR checks

No overload matches this call.
...(chromiumExecutablePath ? { launchOptions: { executablePath: chromiumExecutablePath } } : {}),
},
},
Expand Down
5 changes: 1 addition & 4 deletions scripts/generate-site-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const productRouteHandlerPaths = new Set(["/applications", "/differentials/prese

const documentedRedirectTargets: Record<string, string> = {
"/applications": "/tools",
"/differentials/presentations": "/differentials/presentations/[slug]",
"/differentials/presentations": "/differentials/presentations/[workflow-slug]",
"/medications": "/?mode=prescribing",
};

Expand All @@ -61,9 +61,6 @@ const routeDescriptions: Record<string, string> = {
"/documents/search": "Documents search command centre.",
"/documents/source": "Compatibility redirect to the canonical live document viewer when a valid id is supplied.",
"/documents/source/evidence": "Compatibility redirect sharing the canonical live document viewer handoff.",
"/factsheets": "Patient information sheet library.",
"/factsheets/[slug]": "Patient information sheet detail.",
"/factsheets/search": "Patient information sheet search.",
"/favourites": "Saved clinical items and sets.",
"/forms": "Forms home and search surface.",
"/forms/[slug]": "Registry-backed form detail.",
Expand Down
5 changes: 1 addition & 4 deletions src/app/differentials/presentations/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import { type NextRequest, NextResponse } from "next/server";
import { getPresentationWorkflowSelectionForDiagnosisIds } from "@/lib/differentials";

export function GET(request: NextRequest) {
const rawQuery = request.nextUrl.searchParams.get("query");
const legacyQuery = request.nextUrl.searchParams.get("q");
// Prefer `query`, but fall through on empty/whitespace so `q` remains usable.
const query = (rawQuery?.trim() || legacyQuery?.trim())?.trim();
const query = (request.nextUrl.searchParams.get("query") ?? request.nextUrl.searchParams.get("q"))?.trim();
const selectedIds = (request.nextUrl.searchParams.get("ids") ?? "")
.split(",")
.map((id) => id.trim())
Expand Down
4 changes: 3 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import { appModeHomeHref, isAppModeId, isAppModeVisible, type AppModeId } from "@/lib/app-modes";

type HomeProps = {
searchParams?: Promise<Record<string, string | string[] | undefined>>;
searchParams?: Promise<{
mode?: string | string[];
}>;
};

function firstSearchParam(value: string | string[] | undefined) {
Expand All @@ -27,31 +29,31 @@
// dashboard mode param would otherwise open a divergent hub view.
if (initialSearchMode === "favourites") {
const favouriteParams = new URLSearchParams();
const query = firstSearchParam(params.q)?.trim();

Check failure on line 32 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / Static PR checks

Property 'q' does not exist on type '{ mode?: string | string[] | undefined; }'.
if (query) favouriteParams.set("q", query);
if (firstSearchParam(params.focus) === "1") favouriteParams.set("focus", "1");

Check failure on line 34 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / Static PR checks

Property 'focus' does not exist on type '{ mode?: string | string[] | undefined; }'.
if (firstSearchParam(params.run) === "1") favouriteParams.set("run", "1");

Check failure on line 35 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / Static PR checks

Property 'run' does not exist on type '{ mode?: string | string[] | undefined; }'.
const suffix = favouriteParams.toString();
redirect(suffix ? `/favourites?${suffix}` : "/favourites");
}

if (initialSearchMode === "differentials") {
const differentialParams = new URLSearchParams();
const query = firstSearchParam(params.q)?.trim();

Check failure on line 42 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / Static PR checks

Property 'q' does not exist on type '{ mode?: string | string[] | undefined; }'.
if (query) differentialParams.set("q", query);
if (firstSearchParam(params.focus) === "1") differentialParams.set("focus", "1");

Check failure on line 44 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / Static PR checks

Property 'focus' does not exist on type '{ mode?: string | string[] | undefined; }'.
if (firstSearchParam(params.run) === "1") differentialParams.set("run", "1");

Check failure on line 45 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / Static PR checks

Property 'run' does not exist on type '{ mode?: string | string[] | undefined; }'.
const suffix = differentialParams.toString();
redirect(suffix ? `/differentials?${suffix}` : "/differentials");
}

if (initialSearchMode === "dsm") {
const query = firstSearchParam(params.q)?.trim();

Check failure on line 51 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / Static PR checks

Property 'q' does not exist on type '{ mode?: string | string[] | undefined; }'.
redirect(
appModeHomeHref("dsm", {
query,
focus: firstSearchParam(params.focus) === "1",

Check failure on line 55 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / Static PR checks

Property 'focus' does not exist on type '{ mode?: string | string[] | undefined; }'.
run: firstSearchParam(params.run) === "1",

Check failure on line 56 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / Static PR checks

Property 'run' does not exist on type '{ mode?: string | string[] | undefined; }'.
}),
);
}
Expand Down
8 changes: 6 additions & 2 deletions src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3648,7 +3648,7 @@ export function ClinicalDashboard({
) : error ? (
<EmptyState
icon={CircleAlert}
title={activeModeResultKind === "answer" ? "Answer unavailable" : "Search unavailable"}
title="Answer unavailable"
body={error}
live="assertive"
tone="danger"
Expand Down Expand Up @@ -3876,7 +3876,11 @@ export function ClinicalDashboard({
/>
) : null}

{showUniversalAlsoMatches ? (
{showUniversalAlsoMatches && activeModeResultKind !== "answer" ? (
<UniversalSearchAlsoMatches modeId={searchMode} query={universalAlsoMatchesQuery} />
) : null}

{showUniversalAlsoMatches && activeModeResultKind === "answer" ? (
<UniversalSearchAlsoMatches modeId={searchMode} query={universalAlsoMatchesQuery} />
) : null}
</section>
Expand Down
18 changes: 3 additions & 15 deletions src/components/clinical-dashboard/master-search-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1440,7 +1440,7 @@ export function MasterSearchHeader({
closeButtonClassName="grid h-11 w-11 shrink-0 place-items-center rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-muted)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
contentClassName={cn(
"bg-[color:var(--surface-lux)]",
scopeSheetFullscreen ? "max-h-full" : "max-h-[min(84dvh,42rem)]",
scopeSheetFullscreen ? "max-h-dvh" : "max-h-[min(84dvh,42rem)]",
"sm:max-h-[min(88dvh,44rem)] sm:max-w-xl",
)}
bodyClassName={cn(
Expand Down Expand Up @@ -1583,13 +1583,7 @@ export function MasterSearchHeader({
onBlur={(event) => {
const nextFocusedElement = event.relatedTarget;
if (nextFocusedElement instanceof Node && event.currentTarget.contains(nextFocusedElement)) return;
// Defer dismiss so a pointer activation on a menuitem can land before
// unmount; keyboard leave (Tab/Shift+Tab) still closes on the next frame.
const menuRoot = event.currentTarget;
window.requestAnimationFrame(() => {
if (menuRoot.contains(document.activeElement)) return;
setModeMenuOpen(false);
});
setModeMenuOpen(false);
}}
className={cn("relative z-[60] min-w-0", isWorkflowHeader ? "justify-self-start" : "justify-self-center")}
>
Expand All @@ -1598,7 +1592,6 @@ export function MasterSearchHeader({
type="button"
onClick={() => {
setActionMenuOpen(false);
setCommandDropdownOpen(false);
closeScope(false);
setModeMenuOpen((open) => !open);
}}
Expand Down Expand Up @@ -1644,11 +1637,6 @@ export function MasterSearchHeader({
id="app-mode-menu"
role="menu"
aria-label="Choose app mode"
onMouseDown={(event) => {
// Keep focus on the mode trigger so blur-to-dismiss does not
// unmount options before the click lands.
event.preventDefault();
}}
className="polished-scroll fixed left-[max(0.5rem,var(--safe-area-left))] right-[max(0.5rem,var(--safe-area-right))] top-[calc(4.25rem+env(safe-area-inset-top))] z-50 max-h-[min(20rem,calc(100dvh-5.5rem))] overflow-y-auto rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] p-1.5 text-[color:var(--text)] shadow-[var(--shadow-lux)] ring-1 ring-white/25 backdrop-blur-md dark:ring-white/10 sm:absolute sm:left-0 sm:right-auto sm:top-[calc(100%+0.5rem)] sm:w-[min(21rem,calc(100vw-2rem))]"
>
{visibleAppModeOptions.map((mode, index) => {
Expand Down Expand Up @@ -1702,7 +1690,7 @@ export function MasterSearchHeader({
) : null}
</div>

<div className="relative flex min-w-0 shrink-0 items-center justify-end gap-2 justify-self-end">
<div className="relative flex min-w-0 shrink-0 items-center justify-end gap-1.5 justify-self-end sm:gap-2">
{isWorkflowHeader ? (
<>
<button
Expand Down
8 changes: 2 additions & 6 deletions src/components/services/services-navigator-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import { appModeHomeHref } from "@/lib/app-modes";
import { recordMatchesCommandScopes } from "@/lib/search-command-surface";
import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";
import { rankServiceRecords, type ServiceRecord, type ServiceStatusChip } from "@/lib/service-ranker";
import { canCompareServices, serviceNavigatorMetrics } from "@/lib/service-navigator-metrics";
import { useRegistryRecords } from "@/lib/use-registry-records";
import { sortResultItems } from "@/lib/result-sort";
import { UniversalSearchAlsoMatches } from "@/components/clinical-dashboard/universal-search-also-matches";
Expand Down Expand Up @@ -301,10 +300,6 @@ function RightRail({
const comparisonExpanded = showComparison && comparisonAvailable;
const confidenceTotal = counts.high + counts.medium + counts.low + counts.unknown;

// Keep rail toggles mounted across selection-count changes. Derived expanded
// flags below already hide checklist/comparison when the current selection
// cannot support them, so no effect-driven setState is required.

const rows: Array<[string, number, LucideIcon, string]> = [
["Meets", counts.meets, CircleCheck, "text-[color:var(--success)]"],
["Caution", counts.cautions, CircleAlert, "text-[color:var(--warning)]"],
Expand Down Expand Up @@ -605,10 +600,12 @@ export function ServicesNavigatorPage() {
sortValue={sortValue}
onSortChange={setSortValue}
/>
<UniversalSearchAlsoMatches modeId="services" query={query} />
</>
}
sidebar={
<RightRail
key={selected.length === 0 ? "empty" : selected.length === 1 ? "single" : "multiple"}
matches={displayedMatches}
selected={selected}
onClearSelected={() => setSelectedSlugs([])}
Expand Down Expand Up @@ -733,7 +730,6 @@ export function ServicesNavigatorPage() {
</div>
</>
)}
<UniversalSearchAlsoMatches modeId="services" query={query} />
</SearchResultsLayout>
);
}
22 changes: 5 additions & 17 deletions src/lib/rag-candidate-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,6 @@ import type { DocumentIndexUnitMatch, DocumentMemoryCard, SearchResult } from "@
// the floor, which is how the live schema drift (42702) went unnoticed. Log it structurally and,
// where telemetry is in scope, record the failing RPC + code so it shows up in rag_retrieval_logs.
export type SupabaseRpcError = { message?: string; code?: string; details?: string; hint?: string } | null;
type RpcResult<T> = Promise<{ data: T | null; error: SupabaseRpcError }>;
type AbortableRpc<T> = RpcResult<T> & {
abortSignal?: (signal: AbortSignal) => RpcResult<T>;
};
type SupabaseRpcClient = {
rpc: (name: string, rpcArgs: Record<string, unknown>) => AbortableRpc<unknown[]> | PromiseLike<unknown>;
};

function legacyRankFields(versionedName: string) {
if (versionedName === "match_document_chunks_v2") return ["similarity"];
Expand Down Expand Up @@ -88,20 +81,15 @@ export async function callVersionedRetrievalRpc<T extends unknown[] = unknown[]>
versionedName: string,
legacyName: string,
args: Record<string, unknown>,
signal?: AbortSignal,
): Promise<{ data: T | null; error: SupabaseRpcError }> {
const client = supabase as unknown as SupabaseRpcClient;
const executeRpc = async (name: string, rpcArgs: Record<string, unknown>) => {
const pending = client.rpc(name, rpcArgs) as AbortableRpc<T>;
const pendingWithAbort =
signal && typeof pending.abortSignal === "function" ? pending.abortSignal(signal) : pending;
return await pendingWithAbort;
const client = supabase as unknown as {
rpc: (name: string, rpcArgs: Record<string, unknown>) => Promise<{ data: T | null; error: SupabaseRpcError }>;
};
const versioned = await executeRpc(versionedName, args);
const versioned = await client.rpc(versionedName, args);
if (versioned && !isMissingRetrievalRpcError(versioned.error)) return versioned;
const legacyArgs = { ...args };
delete legacyArgs.include_public;
const ownerResult = await executeRpc(legacyName, legacyArgs);
const ownerResult = await client.rpc(legacyName, legacyArgs);
const ownerFilter = String(args.owner_filter ?? "");
if (
ownerResult.error ||
Expand All @@ -111,7 +99,7 @@ export async function callVersionedRetrievalRpc<T extends unknown[] = unknown[]>
) {
return ownerResult;
}
const publicResult = await executeRpc(legacyName, {
const publicResult = await client.rpc(legacyName, {
...legacyArgs,
owner_filter: PUBLIC_OWNER_FILTER_SENTINEL,
});
Expand Down
49 changes: 3 additions & 46 deletions src/lib/rag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { retrievalAccessScopeForArgs, retrievalRpcScopeArgs } from "@/lib/owner-scope";
import {
callVersionedRetrievalRpc,
createChunkLoadCache,
memoryCardChunkScore,
mergeSearchResults,
recordHybridRpcError,
Expand Down Expand Up @@ -120,11 +119,6 @@
retrievalPlanCacheQuery,
} from "@/lib/rag-cache";
import { classifySearchCacheOutcome, recordCacheLookup } from "@/lib/observability/cache-metrics";
import {
recordAnswerOrigination,
recordAnswerOriginationFinished,
recordCoalescedAnswerWaiter,
} from "@/lib/observability/answer-coalescing-metrics";
import { buildRagSourceBlock, compactContextText, neutralizeIdentityField } from "@/lib/rag-source-block";
export { buildRagSourceBlock, truncateForModel } from "@/lib/rag-source-block";
import {
Expand Down Expand Up @@ -428,26 +422,6 @@
}
}

function awaitWithCallerSignal<T>(pending: Promise<T>, signal?: AbortSignal): Promise<T> {
if (!signal) return pending;
if (signal.aborted) throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");

return new Promise<T>((resolve, reject) => {
const onAbort = () => reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
signal.addEventListener("abort", onAbort, { once: true });
pending.then(
(value) => {
signal.removeEventListener("abort", onAbort);
resolve(value);
},
(error) => {
signal.removeEventListener("abort", onAbort);
reject(error);
},
);
});
}

export type AnswerProgressEvent = {
stage:
| "retrieved"
Expand Down Expand Up @@ -1315,7 +1289,6 @@
// owner_filter retrieval will use so grounding can never see documents retrieval cannot.
corpusGrounding?: { supabase: ReturnType<typeof createAdminClient>; ownerFilter: string | null };
ownerId?: string | null;
signal?: AbortSignal;
},
) {
if (
Expand Down Expand Up @@ -1387,16 +1360,10 @@
}

try {
const verdict = await awaitWithCallerSignal(pending, opts?.signal);
const verdict = await pending;
storeClassifierVerdictMemo(memoKey, verdict);
return applyClassifierVerdict(analysis, verdict);
} catch (error) {
if (
error &&
(error instanceof DOMException || typeof error === "object") &&
(error as { name?: string }).name === "AbortError"
)
throw error;
} catch {
// Transport/parse failures are deliberately NOT memoized: fall back to the deterministic
// analysis for this request only, and let the next request retry the classifier.
return analysis;
Expand Down Expand Up @@ -2403,7 +2370,6 @@
// A3: shared across every withMemoryBoostedCandidates call in this request so the same
// owner/query memory cards are fetched at most once per (query, embedding-present, count).
const memoryCardCache: MemoryCardCache = new Map();
const chunkLoadCache = createChunkLoadCache();
const documentRankingMetadataCache = createDocumentRankingMetadataCache();
const modeQueryClass = queryClassForClinicalMode(args.queryMode ?? "auto");
const documentFilterList = args.documentIds?.length
Expand All @@ -2430,7 +2396,6 @@
const queryAnalysis = await analyzeQueryWithClassifierFallback(retrievalQuery, analyzeClinicalQuery(retrievalQuery), {
corpusGrounding: corpusGroundingScope,
ownerId: args.ownerId,
signal: args.signal,
});
throwIfAborted(args.signal);
if (modeQueryClass) queryAnalysis.queryClass = modeQueryClass;
Expand Down Expand Up @@ -2625,7 +2590,7 @@
allowGlobalSearch: args.allowGlobalSearch,
matchCount: Math.min(candidateCount, 48),
telemetry,
cache: chunkLoadCache,

Check failure on line 2593 in src/lib/rag.ts

View workflow job for this annotation

GitHub Actions / Unit coverage

[node] tests/public-access-deep.test.ts > test-runtime anonymous retrieval scope > allows anonymous global retrieval in test runtime where owner scope stays permissive

ReferenceError: chunkLoadCache is not defined ❯ searchChunksWithTelemetry src/lib/rag.ts:2593:14 ❯ tests/public-access-deep.test.ts:258:20

Check failure on line 2593 in src/lib/rag.ts

View workflow job for this annotation

GitHub Actions / Unit coverage

[node] tests/public-access-deep.test.ts > production anonymous retrieval scope > scopes anonymous global retrieval to public documents when allowGlobalSearch is true

ReferenceError: chunkLoadCache is not defined ❯ searchChunksWithTelemetry src/lib/rag.ts:2593:14 ❯ tests/public-access-deep.test.ts:201:20
});
const tableFactLatencyMs = Date.now() - tableFactStartedAt;
telemetry.supabase_rpc_latency_ms += tableFactLatencyMs;
Expand Down Expand Up @@ -2812,7 +2777,7 @@
allowGlobalSearch: args.allowGlobalSearch,
matchCount: Math.min(candidateCount, 48),
telemetry,
cache: chunkLoadCache,

Check failure on line 2780 in src/lib/rag.ts

View workflow job for this annotation

GitHub Actions / Unit coverage

[node] tests/private-access-routes.test.ts > private document API access > uses the DB-backed document lookup RPC with owner scope

ReferenceError: chunkLoadCache is not defined ❯ src/lib/rag.ts:2780:16 ❯ searchChunksWithTelemetry src/lib/rag.ts:2783:6 ❯ searchChunks src/lib/rag.ts:3005:23 ❯ tests/private-access-routes.test.ts:4912:5

Check failure on line 2780 in src/lib/rag.ts

View workflow job for this annotation

GitHub Actions / Unit coverage

[node] tests/private-access-routes.test.ts > private document API access > passes owner scope into retrieval RPCs

ReferenceError: chunkLoadCache is not defined ❯ src/lib/rag.ts:2780:16 ❯ searchChunksWithTelemetry src/lib/rag.ts:2783:6 ❯ searchChunks src/lib/rag.ts:3005:23 ❯ tests/private-access-routes.test.ts:4864:21
});
return { candidates, latencyMs: Date.now() - startedAt };
})(),
Expand Down Expand Up @@ -3156,7 +3121,6 @@
let existing = inflightKey ? answerInflight.get(inflightKey) : undefined;

while (existing) {
recordCoalescedAnswerWaiter();
await args.onProgress?.({
stage: "cached",
message: "Waiting for an identical cited answer request already in progress.",
Expand Down Expand Up @@ -3188,15 +3152,8 @@
}
}

// Only coalescible requests belong in this process-local signal. Requests
// that intentionally bypass cache/coalescing must not make a replica look
// ineffective, and neither keys nor clinical content leave this function.
if (inflightKey) recordAnswerOrigination();
const pending = answerQuestionWithScopeUncoalesced(args, startedAt).finally(() => {
if (inflightKey) {
answerInflight.delete(inflightKey);
recordAnswerOriginationFinished();
}
if (inflightKey) answerInflight.delete(inflightKey);
});
if (inflightKey) answerInflight.set(inflightKey, pending);
return pending;
Expand Down
Loading
Loading