Skip to content

feat: Optimize intelligence latency and clean up dashboard-backend seams#26

Merged
rsalus merged 33 commits intomainfrom
feature/integration-dashboard-backend-optimization
Feb 21, 2026
Merged

feat: Optimize intelligence latency and clean up dashboard-backend seams#26
rsalus merged 33 commits intomainfrom
feature/integration-dashboard-backend-optimization

Conversation

@rsalus
Copy link
Contributor

@rsalus rsalus commented Feb 11, 2026

Summary

Optimizes the intelligence service from 15-35s to 3-5s per request by parallelizing LLM criterion evaluation, and establishes clean abstraction boundaries in the gateway with an IDataService interface. Consolidates dual token systems in the dashboard and adds approve/deny state transitions.

Changes

  • Intelligence Service — Parallel evidence extraction with asyncio.gather + semaphore, client singleton pooling with timeout/retry, structured error handling, thread-safe provider init, parallel PDF parsing via thread pool
  • Gateway API — IDataService interface extracted from MockDataService, approve/deny transitions with denial reason tracking, DI-injected Query/Mutation resolvers, LiveDataService skeleton with config toggle
  • Dashboard — Shared getAuthToken() replacing dual token systems, approve/deny GraphQL mutations and hooks, connection status indicator

Test Plan

All three service test suites pass: Python 27 passed, .NET 380 passed (3 skipped), TypeScript 206 passed. New tests cover parallel extraction, client pooling, error handling, IDataService operations, and auth token consolidation.


Results: Tests 613 ✓ · Build 0 errors
Design: dashboard-backend-optimization.md

ClAfonso and others added 26 commits February 7, 2026 18:39
Introduces a data service abstraction to decouple GraphQL resolvers from the
concrete MockDataService implementation. MockDataService now implements
IDataService, enabling future substitution with a live data service.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds ApprovePA and DenyPA methods to IDataService and MockDataService,
enabling status transitions from waiting_for_insurance to approved/denied.
Includes corresponding GraphQL mutations and 4 new TDD tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract duplicate session storage token reads from graphqlClient.ts and
customFetch.ts into a single auth.ts module with fallback chain
(authscript_session -> smart_session). Includes 6 unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract per-criterion LLM evaluation into standalone evaluate_criterion()
function and replace sequential loop with asyncio.gather bounded by a
configurable semaphore (LLM_MAX_CONCURRENT, default 4). This reduces
multi-criteria evaluation latency from O(n * latency) to O(latency)
while preventing LLM provider rate-limit storms.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add useApprovePARequest and useDenyPARequest mutation hooks with cache
invalidation, plus useConnectionStatus hook that polls paStats every 10s
to detect gateway connectivity. Includes 4 unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move HTTP client creation from per-request to provider __init__ (singleton
pattern) to reuse connection pools. Add configurable timeout (30s) and
retry (2x) to all OpenAI-compatible providers. Replace bare except with
typed handlers: APITimeoutError returns None, RateLimitError propagates,
APIError logs and returns None.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tion

Refactor parse_pdf to run synchronous PyMuPDF extraction via
run_in_executor to avoid blocking the async event loop. Extract
_extract_sync helper that encapsulates temp file lifecycle. Update
analyze_with_documents endpoint to parse multiple PDFs concurrently
with asyncio.gather.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e skeleton

Updates all GraphQL resolvers to inject IDataService instead of concrete
MockDataService. Adds UseMockData configuration toggle (defaults to true)
and a LiveDataService skeleton for future live data integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… up ProcessPARequestAsync

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

Warning

Rate limit exceeded

@rsalus has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 12 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive GraphQL-based API layer between the dashboard and gateway, replacing localStorage-backed mock data with server-driven queries and mutations. It adds GraphQL schema definitions, resolvers, model types, service abstractions, frontend hooks, new UI components for PDF generation and progress tracking, and enhanced backend services with improved concurrency control and error handling for LLM integration.

Changes

Cohort / File(s) Summary
Frontend Dependencies & Configuration
apps/dashboard/package.json, apps/gateway/Gateway.API.csproj, apps/intelligence/src/config.py
Added GraphQL client dependencies (graphql, graphql-request) and PDF libraries (html2canvas, jspdf). Backend added HotChocolate.AspNetCore 15.1.0. Intelligence service added LLM concurrency, timeout, and retry configuration fields.
Frontend GraphQL Client & Service
apps/dashboard/src/api/graphqlClient.ts, apps/dashboard/src/api/graphqlService.ts, apps/dashboard/src/api/auth.ts, apps/dashboard/src/api/customFetch.ts
Implemented new GraphQL client with Bearer token authentication, refactored token retrieval into centralized getAuthToken utility, introduced 13 React Query hooks for data fetching and mutations with cache invalidation, and GraphQL fragments/operations for PA management workflows.
Frontend GraphQL Tests
apps/dashboard/src/api/__tests__/auth.test.ts, apps/dashboard/src/api/__tests__/graphqlService.test.ts, apps/dashboard/src/api/__tests__/graphqlService.integration.test.ts
Added comprehensive test coverage for auth token retrieval, GraphQL hooks (mutations and queries), and integration tests against live GraphQL endpoint with cleanup logic for created resources.
Frontend Modal & Overlay Components
apps/dashboard/src/components/PdfViewerModal.tsx, apps/dashboard/src/components/SubmissionProgressOverlay.tsx
Introduced new modal components for PDF viewing with print/download capabilities using blob-based PDF generation, and a multi-phase submission progress overlay with timed transitions and dynamic labeling.
Frontend Route & Page Components
apps/dashboard/src/routes/index.tsx, apps/dashboard/src/routes/analysis.$transactionId.tsx, apps/dashboard/src/routes/help.tsx
Migrated dashboard from local mock data to GraphQL-driven queries, added analytics subsystem with time-range filtering and metrics computation, replaced analysis page with real-time data hooks and PDF viewer integration, updated help text for new PDF generation flow.
Frontend Form & Modal Logic
apps/dashboard/src/components/NewPAModal.tsx
Restructured multi-step PA creation flow with new confirm and success steps, integrated GraphQL mutations for creation and processing, added processing state animation with error handling and success navigation.
Frontend Data Layer Migration
apps/dashboard/src/lib/mockData.ts, apps/dashboard/src/lib/store.ts, apps/dashboard/src/lib/patients.ts
Removed 300+ lines of mock data generation and localStorage-backed store, replaced with frontend-only test patient definitions.
Frontend PDF Generation
apps/dashboard/src/lib/pdfGenerator.ts
Refactored from in-window printing to blob-based PDF generation using html2canvas and jsPDF, added HTML escaping for security, introduced async API for opening and downloading PDFs.
Backend Service Abstractions
apps/gateway/Gateway.API/Services/IDataService.cs, apps/gateway/Gateway.API/Services/LiveDataService.cs
Introduced IDataService interface defining contract for PA data access with 15+ methods spanning queries, mutations, and reference data; added LiveDataService stub implementation for future live data integration.
Backend Data Service Implementation
apps/gateway/Gateway.API/Services/MockDataService.cs
Implemented comprehensive in-memory data service with seeded PA requests, lifecycle state transitions (draft→ready→waiting_for_insurance→approved/denied), dynamic criteria evaluation with confidence scoring, and activity log generation.
Backend GraphQL Models
apps/gateway/Gateway.API/GraphQL/Models/*
Added 12 GraphQL model types (PARequestModel, PatientModel, ProcedureModel, MedicationModel, etc.) as immutable sealed records for type safety and schema definition.
Backend GraphQL Input Types
apps/gateway/Gateway.API/GraphQL/Inputs/CreatePARequestInput.cs, apps/gateway/Gateway.API/GraphQL/Inputs/PatientInput.cs, apps/gateway/Gateway.API/GraphQL/Inputs/UpdatePARequestInput.cs
Added input type definitions for GraphQL mutations with nested structures for patient data, criteria updates, and PA request modifications.
Backend GraphQL Resolvers
apps/gateway/Gateway.API/GraphQL/Queries/Query.cs, apps/gateway/Gateway.API/GraphQL/Mutations/Mutation.cs
Implemented 9 query resolvers for PA data, reference data, and statistics; 8 mutation resolvers for creation, updates, state transitions, and approval workflows.
Backend Service Integration
apps/gateway/Gateway.API/Program.cs, apps/gateway/Gateway.API/Services/DocumentUploader.cs, apps/gateway/Gateway.API/Services/Fhir/FhirHttpClient.cs, apps/gateway/Gateway.API/Services/Fhir/HttpResponseErrorFactory.cs
Enabled GraphQL server with Query and Mutation wiring, updated Result type references to use fully-qualified Gateway.API.Contracts namespace across FHIR and upload services.
Backend Data Service Tests
apps/gateway/Gateway.API.Tests/Services/MockDataServiceTests.cs, apps/gateway/Gateway.API.Tests/Services/LiveDataServiceTests.cs, apps/gateway/Gateway.API.Tests/Services/Polling/AthenaPollingServiceTests.cs
Added 40+ test methods covering MockDataService state transitions, field preservation, and success rate metrics; stubbed LiveDataService tests; improved polling test async coordination.
Intelligence Service Enhancements
apps/intelligence/src/llm_client.py, apps/intelligence/src/reasoning/evidence_extractor.py, apps/intelligence/src/api/analyze.py, apps/intelligence/src/parsers/pdf_parser.py
Implemented LLM provider singleton pattern with per-provider HTTP client pooling, timeout and retry configuration; refactored criterion evaluation with bounded concurrency via semaphore; added async PDF parsing via thread pool executor.
Intelligence Service Tests
apps/intelligence/src/tests/test_llm_client.py, apps/intelligence/src/tests/test_evidence_extractor.py, apps/intelligence/src/tests/test_pdf_parser.py
Added tests for singleton provider behavior, timeout/rate-limit handling, concurrent criterion evaluation with semaphore bounds, and async PDF parsing without event loop blocking.
Documentation
docs/ARCHITECTURE.md
Updated architecture documentation to reflect GraphQL-centric API layer, new folder structure for GraphQL (schema, queries, mutations, models), and data flow through MockDataService.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Dashboard Client
    participant GQL as GraphQL Client
    participant Gateway as Gateway API
    participant DataService as IDataService
    participant LLM as LLM Service

    Client->>GQL: usePARequests()
    GQL->>GQL: getAuthToken()
    GQL->>Gateway: query GetPARequests (with Bearer token)
    Gateway->>DataService: GetPARequests()
    DataService-->>Gateway: PARequestModel[]
    Gateway-->>GQL: GraphQL response
    GQL-->>Client: Cached data via React Query

    Client->>GQL: useCreatePARequest mutation
    GQL->>Gateway: mutation CreatePARequest
    Gateway->>DataService: CreatePARequest(patient, procedure, diagnosis)
    DataService-->>Gateway: PARequestModel
    Gateway->>LLM: ProcessPARequestAsync (AI evaluation)
    LLM->>LLM: evaluate_criterion (with bounded concurrency)
    LLM-->>Gateway: confidence + criteria results
    DataService-->>Gateway: Updated PARequestModel
    Gateway-->>GQL: Mutation response
    GQL->>GQL: invalidate(paRequests, paStats)
    GQL-->>Client: Success + refetched data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🚀 From localStorage whispers to GraphQL calls,
Mock data bows as the real API stands tall,
PDF blobs dance, criteria evaluate with grace,
Semaphores guide LLMs through their race,
The dashboard awakens—no more sleepy mock dreams! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: optimizing intelligence service latency and establishing cleaner abstraction boundaries in the dashboard-backend integration.
Description check ✅ Passed The description provides a comprehensive overview of changes across three services (intelligence, gateway, dashboard), test results, and optimization outcomes. It is directly related to and supports the changeset.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/integration-dashboard-backend-optimization

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/gateway/Gateway.API/Services/MockDataService.cs (1)

284-309: ⚠️ Potential issue | 🟡 Minor

Handle cancellation to avoid leaving requests stuck in processing state.

If Task.Delay is cancelled, the request remains in the "processing" status indefinitely with no path to completion or rollback. Wrap the delay in a try-catch to revert the status on cancellation.

Suggested fix
-        lock (_lock)
+        string? previousStatus = null;
+        lock (_lock)
         {
             var idx = _paRequests.FindIndex(r => r.Id == id);
             if (idx < 0) return null;

             var existing = _paRequests[idx];
+            previousStatus = existing.Status;
             _paRequests[idx] = existing with { Status = "processing", UpdatedAt = DateTime.UtcNow.ToString("O") };
         }

-        await Task.Delay(2000, ct);
+        try
+        {
+            await Task.Delay(2000, ct);
+        }
+        catch (OperationCanceledException)
+        {
+            lock (_lock)
+            {
+                var idx = _paRequests.FindIndex(r => r.Id == id);
+                if (idx >= 0 && previousStatus is not null)
+                {
+                    var existing = _paRequests[idx];
+                    _paRequests[idx] = existing with { Status = previousStatus, UpdatedAt = DateTime.UtcNow.ToString("O") };
+                }
+            }
+            return null;
+        }
🤖 Fix all issues with AI agents
In `@apps/dashboard/src/api/graphqlService.ts`:
- Around line 487-511: In both useApprovePARequest and useDenyPARequest add
invalidation for the specific PA request key (QUERY_KEYS.paRequest(id)) in
onSuccess by using the mutation's variables parameter to extract the id (for
useApprovePARequest variables is the id string, for useDenyPARequest variables
is an object with id), then call queryClient.invalidateQueries({ queryKey:
QUERY_KEYS.paRequest(id) }) along with the existing list/stat/activity
invalidations; update onSuccess signatures to accept (_data, variables) and
derive id accordingly.

In `@apps/gateway/Gateway.API/GraphQL/Mutations/Mutation.cs`:
- Around line 78-84: Replace the expression-bodied mutation methods with
block-bodied methods: change ApprovePARequest(string id, [Service] IDataService
dataService) => dataService.ApprovePA(id); to a block that calls
dataService.ApprovePA(id) and returns the result, and similarly change
DenyPARequest(string id, string reason, [Service] IDataService dataService) =>
dataService.DenyPA(id, reason); to a block that invokes dataService.DenyPA(id,
reason) and returns its result; ensure method signatures (ApprovePARequest and
DenyPARequest) and return types remain unchanged and use standard braces and
return statements.

In `@apps/gateway/Gateway.API/GraphQL/Queries/Query.cs`:
- Around line 17-51: Convert each expression-bodied resolver (GetProcedures,
GetMedications, GetProviders, GetPayers, GetDiagnoses, GetPARequests,
GetPARequest, GetPAStats, GetActivity) to a block-bodied method with braces; add
a guard clause at the top validating the [Service] IDataService parameter (throw
new ArgumentNullException(nameof(dataService)) if null) and for GetPARequest
also validate the id (throw new ArgumentException if null or empty) before
calling the corresponding dataService member or method, then return the result.

In `@apps/gateway/Gateway.API/Services/LiveDataService.cs`:
- Around line 18-84: The review requires replacing expression-bodied members
with block-bodied implementations: for each property and method that currently
uses the "=>" throw new NotImplementedException(...) (e.g., Procedures,
Medications, Providers, Payers, Diagnoses, GetPARequests, GetPARequest,
GetPAStats, GetActivity, CreatePARequest, UpdatePARequest,
ProcessPARequestAsync, SubmitPARequest, AddReviewTime, DeletePARequest,
ApprovePA, DenyPA) change the arrow-bodied form to a normal block body with
braces and a single throw new NotImplementedException("...") statement inside
(preserve the existing exception message and member signatures), ensuring async
methods like ProcessPARequestAsync keep their Task<> return type and
CancellationToken parameter; update each member accordingly so no
expression-bodied members remain.

In `@apps/intelligence/src/api/analyze.py`:
- Line 6: The try/except in _safe_parse is redundant because parse_pdf already
catches all errors and returns a string; remove the broad except Exception block
from _safe_parse (or entirely delete the try/except wrapper) so that _safe_parse
simply awaits/returns parse_pdf directly, or if you want defensive layering,
replace the broad catch with documented, specific exceptions that can actually
propagate from parse_pdf—refer to the _safe_parse and parse_pdf functions to
make the change and update or add a brief comment explaining the chosen
approach.

In `@apps/intelligence/src/config.py`:
- Around line 45-48: The LLM performance config fields (llm_max_concurrent,
llm_timeout, llm_max_retries) lack validation and can be set to invalid values;
update their declarations in the Pydantic model to use pydantic.Field with
sensible constraints (e.g., gt=0 for llm_max_concurrent and llm_max_retries,
gt=0.0 for llm_timeout and possibly le=some_max) so invalid environment inputs
are rejected and defaults remain; locate the fields by name in the config class
and replace the plain type/assignment with Field(...) including appropriate
gt/le bounds and helpful descriptions.

In `@apps/intelligence/src/tests/test_evidence_extractor.py`:
- Around line 75-205: Add explicit return type annotations: annotate all async
test coroutines (test_evaluate_criterion_returns_met_evidence_item,
test_evaluate_criterion_parses_not_met,
test_evaluate_criterion_handles_none_response,
test_extract_evidence_calls_criteria_concurrently,
test_extract_evidence_respects_semaphore_limit) with -> None, and annotate
helper LLM coroutines slow_llm and counting_llm with -> str; update their async
def signatures accordingly so the file complies with the apps/intelligence
type-hint rule.

In `@apps/intelligence/src/tests/test_llm_client.py`:
- Around line 1-187: The tests lack explicit return type annotations; update
every test function (test_get_provider_returns_singleton,
test_openai_provider_creates_client_once,
test_chat_completion_uses_provider_singleton,
test_chat_completion_returns_none_on_timeout,
test_chat_completion_raises_on_rate_limit,
test_chat_completion_returns_none_on_api_error, test_get_provider_thread_safe)
and the helper get_provider_thread to include the return type -> None (e.g., def
test_xxx(...) -> None:) to match the codebase convention for complete type
annotations; no other behavior changes are needed.

In `@apps/intelligence/src/tests/test_pdf_parser.py`:
- Around line 1-49: Add missing type annotations: annotate both async test
functions test_parse_pdf_does_not_block_event_loop and
test_parse_pdf_multiple_docs_parallel with return type -> None, and add type
hints to the helper slow_extract as (pdf_bytes: bytes) -> str; update its
signature and any local variables' annotations if necessary so the functions
follow the codebase pattern of complete function type annotations.
🧹 Nitpick comments (5)
apps/dashboard/src/api/__tests__/auth.test.ts (1)

1-37: Well-structured test suite with good edge case coverage.

Tests follow the Arrange-Act-Assert pattern with clear, descriptive names. Coverage includes the key scenarios: primary source, fallback, precedence, and various null/invalid cases.

One minor gap: consider adding a test for invalid JSON in smart_session when authscript_session is absent, to verify the fallback path's error handling.

🧪 Optional: Add fallback invalid JSON test
   it('should return null when session has no access_token field', () => {
     sessionStorage.setItem('authscript_session', JSON.stringify({ user: 'test' }));
     expect(getAuthToken()).toBeNull();
   });
+
+  it('should return null when smart_session contains invalid JSON', () => {
+    sessionStorage.setItem('smart_session', 'not-valid-json');
+    expect(getAuthToken()).toBeNull();
+  });
 });
apps/dashboard/src/api/customFetch.ts (1)

8-9: Good refactor to centralize token retrieval.

The delegation to getAuthToken() eliminates duplication. The wrapper function getStoredAccessToken is now a single-line delegation—this maintains backward compatibility but could be simplified by calling getAuthToken() directly at line 104 if no future customization is planned.

Also applies to: 143-150

apps/dashboard/src/api/graphqlService.ts (1)

469-485: Mutation constants are not exported, unlike others in this file.

APPROVE_PA_REQUEST and DENY_PA_REQUEST use const without export, while existing mutations like CREATE_PA_REQUEST (line 140) are exported. For consistency:

♻️ Export mutation constants for consistency
-const APPROVE_PA_REQUEST = gql`
+export const APPROVE_PA_REQUEST = gql`
   mutation ApprovePARequest($id: String!) {
     approvePARequest(id: $id) {
       ...PARequestFields
     }
   }
   ${PA_REQUEST_FRAGMENT}
 `;

-const DENY_PA_REQUEST = gql`
+export const DENY_PA_REQUEST = gql`
   mutation DenyPARequest($id: String!, $reason: String!) {
     denyPARequest(id: $id, reason: $reason) {
       ...PARequestFields
     }
   }
   ${PA_REQUEST_FRAGMENT}
 `;
apps/gateway/Gateway.API/Program.cs (1)

38-47: Fail fast if LiveDataService is enabled before implementation.

LiveDataService currently throws NotImplementedException for all members, so a mis-set flag will break runtime requests. Consider a startup guard or environment gate to catch this early.

🔧 Possible guard
 var useMockData = builder.Configuration.GetValue<bool>("UseMockData", true);
 if (useMockData)
 {
     builder.Services.AddSingleton<IDataService, MockDataService>();
 }
 else
 {
+    if (builder.Environment.IsProduction())
+        throw new InvalidOperationException("LiveDataService is not implemented yet. Set UseMockData=true.");
     builder.Services.AddSingleton<IDataService, LiveDataService>();
 }
apps/gateway/Gateway.API.Tests/Services/LiveDataServiceTests.cs (1)

14-22: Prefer type-safe interface checks.

String comparisons can yield false positives; a type-based check is safer.

✅ Safer reflection check
-        var implements = typeof(LiveDataService).GetInterfaces()
-            .Any(i => i.Name == "IDataService");
+        var implements = typeof(IDataService).IsAssignableFrom(typeof(LiveDataService));

Comment on lines +487 to +511
export function useApprovePARequest() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) =>
graphqlClient.request(APPROVE_PA_REQUEST, { id }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.paRequests });
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.paStats });
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.activity });
},
});
}

export function useDenyPARequest() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, reason }: { id: string; reason: string }) =>
graphqlClient.request(DENY_PA_REQUEST, { id, reason }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.paRequests });
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.paStats });
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.activity });
},
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing invalidation of specific paRequest(id) query on success.

Other mutation hooks like useProcessPARequest (lines 393-399) and useSubmitPARequest (lines 442-448) invalidate the specific QUERY_KEYS.paRequest(id) query on success. These new hooks only invalidate the list queries, which could leave stale data in the cache if a component is viewing the specific request.

🐛 Add specific query invalidation
 export function useApprovePARequest() {
   const queryClient = useQueryClient();
   return useMutation({
     mutationFn: (id: string) =>
       graphqlClient.request(APPROVE_PA_REQUEST, { id }),
-    onSuccess: () => {
+    onSuccess: (_, id) => {
       queryClient.invalidateQueries({ queryKey: QUERY_KEYS.paRequests });
+      queryClient.invalidateQueries({ queryKey: QUERY_KEYS.paRequest(id) });
       queryClient.invalidateQueries({ queryKey: QUERY_KEYS.paStats });
       queryClient.invalidateQueries({ queryKey: QUERY_KEYS.activity });
     },
   });
 }

 export function useDenyPARequest() {
   const queryClient = useQueryClient();
   return useMutation({
     mutationFn: ({ id, reason }: { id: string; reason: string }) =>
       graphqlClient.request(DENY_PA_REQUEST, { id, reason }),
-    onSuccess: () => {
+    onSuccess: (_, { id }) => {
       queryClient.invalidateQueries({ queryKey: QUERY_KEYS.paRequests });
+      queryClient.invalidateQueries({ queryKey: QUERY_KEYS.paRequest(id) });
       queryClient.invalidateQueries({ queryKey: QUERY_KEYS.paStats });
       queryClient.invalidateQueries({ queryKey: QUERY_KEYS.activity });
     },
   });
 }
🤖 Prompt for AI Agents
In `@apps/dashboard/src/api/graphqlService.ts` around lines 487 - 511, In both
useApprovePARequest and useDenyPARequest add invalidation for the specific PA
request key (QUERY_KEYS.paRequest(id)) in onSuccess by using the mutation's
variables parameter to extract the id (for useApprovePARequest variables is the
id string, for useDenyPARequest variables is an object with id), then call
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.paRequest(id) }) along with
the existing list/stat/activity invalidations; update onSuccess signatures to
accept (_data, variables) and derive id accordingly.

Comment on lines +78 to +84
/// <summary>Approves a PA request that is waiting for insurance.</summary>
public PARequestModel? ApprovePARequest(string id, [Service] IDataService dataService)
=> dataService.ApprovePA(id);

/// <summary>Denies a PA request that is waiting for insurance.</summary>
public PARequestModel? DenyPARequest(string id, string reason, [Service] IDataService dataService)
=> dataService.DenyPA(id, reason);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "Mutation.cs" | grep -i gateway

Repository: lvlup-sw/authscript

Length of output: 119


🏁 Script executed:

cat -n ./apps/gateway/Gateway.API/GraphQL/Mutations/Mutation.cs | sed -n '70,90p'

Repository: lvlup-sw/authscript

Length of output: 806


Switch to block bodies for these mutations.

These methods use expression-bodied members (arrow syntax), which violates the "no arrow code" standard specified in the coding guidelines for this file path.

✅ Block-bodied versions
    /// <summary>Approves a PA request that is waiting for insurance.</summary>
    public PARequestModel? ApprovePARequest(string id, [Service] IDataService dataService)
-       => dataService.ApprovePA(id);
+   {
+       return dataService.ApprovePA(id);
+   }

    /// <summary>Denies a PA request that is waiting for insurance.</summary>
    public PARequestModel? DenyPARequest(string id, string reason, [Service] IDataService dataService)
-       => dataService.DenyPA(id, reason);
+   {
+       return dataService.DenyPA(id, reason);
+   }
📝 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
/// <summary>Approves a PA request that is waiting for insurance.</summary>
public PARequestModel? ApprovePARequest(string id, [Service] IDataService dataService)
=> dataService.ApprovePA(id);
/// <summary>Denies a PA request that is waiting for insurance.</summary>
public PARequestModel? DenyPARequest(string id, string reason, [Service] IDataService dataService)
=> dataService.DenyPA(id, reason);
/// <summary>Approves a PA request that is waiting for insurance.</summary>
public PARequestModel? ApprovePARequest(string id, [Service] IDataService dataService)
{
return dataService.ApprovePA(id);
}
/// <summary>Denies a PA request that is waiting for insurance.</summary>
public PARequestModel? DenyPARequest(string id, string reason, [Service] IDataService dataService)
{
return dataService.DenyPA(id, reason);
}
🤖 Prompt for AI Agents
In `@apps/gateway/Gateway.API/GraphQL/Mutations/Mutation.cs` around lines 78 - 84,
Replace the expression-bodied mutation methods with block-bodied methods: change
ApprovePARequest(string id, [Service] IDataService dataService) =>
dataService.ApprovePA(id); to a block that calls dataService.ApprovePA(id) and
returns the result, and similarly change DenyPARequest(string id, string reason,
[Service] IDataService dataService) => dataService.DenyPA(id, reason); to a
block that invokes dataService.DenyPA(id, reason) and returns its result; ensure
method signatures (ApprovePARequest and DenyPARequest) and return types remain
unchanged and use standard braces and return statements.

Comment on lines +17 to +51
/// <summary>Gets all available procedures.</summary>
public IReadOnlyList<ProcedureModel> GetProcedures([Service] IDataService dataService) =>
dataService.Procedures;

public IReadOnlyList<MedicationModel> GetMedications([Service] MockDataService mockData) =>
mockData.Medications;
/// <summary>Gets all available medications.</summary>
public IReadOnlyList<MedicationModel> GetMedications([Service] IDataService dataService) =>
dataService.Medications;

public IReadOnlyList<ProviderModel> GetProviders([Service] MockDataService mockData) =>
mockData.Providers;
/// <summary>Gets all available providers.</summary>
public IReadOnlyList<ProviderModel> GetProviders([Service] IDataService dataService) =>
dataService.Providers;

public IReadOnlyList<PayerModel> GetPayers([Service] MockDataService mockData) =>
mockData.Payers;
/// <summary>Gets all available payers.</summary>
public IReadOnlyList<PayerModel> GetPayers([Service] IDataService dataService) =>
dataService.Payers;

public IReadOnlyList<DiagnosisModel> GetDiagnoses([Service] MockDataService mockData) =>
mockData.Diagnoses;
/// <summary>Gets all available diagnoses.</summary>
public IReadOnlyList<DiagnosisModel> GetDiagnoses([Service] IDataService dataService) =>
dataService.Diagnoses;

public IReadOnlyList<PARequestModel> GetPARequests([Service] MockDataService mockData) =>
mockData.GetPARequests();
/// <summary>Gets all PA requests.</summary>
public IReadOnlyList<PARequestModel> GetPARequests([Service] IDataService dataService) =>
dataService.GetPARequests();

public PARequestModel? GetPARequest(string id, [Service] MockDataService mockData) =>
mockData.GetPARequest(id);
/// <summary>Gets a single PA request by ID.</summary>
public PARequestModel? GetPARequest(string id, [Service] IDataService dataService) =>
dataService.GetPARequest(id);

public PAStatsModel GetPAStats([Service] MockDataService mockData) =>
mockData.GetPAStats();
/// <summary>Gets aggregated PA statistics.</summary>
public PAStatsModel GetPAStats([Service] IDataService dataService) =>
dataService.GetPAStats();

public IReadOnlyList<ActivityItemModel> GetActivity([Service] MockDataService mockData) =>
mockData.GetActivity();
/// <summary>Gets recent activity items.</summary>
public IReadOnlyList<ActivityItemModel> GetActivity([Service] IDataService dataService) =>
dataService.GetActivity();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Replace expression-bodied members to comply with “no arrow code.”
The resolver methods are all expression-bodied, which violates the guideline.

🔧 Proposed change (block-bodied methods)
     /// <summary>Gets all available procedures.</summary>
-    public IReadOnlyList<ProcedureModel> GetProcedures([Service] IDataService dataService) =>
-        dataService.Procedures;
+    public IReadOnlyList<ProcedureModel> GetProcedures([Service] IDataService dataService)
+    {
+        return dataService.Procedures;
+    }

     /// <summary>Gets all available medications.</summary>
-    public IReadOnlyList<MedicationModel> GetMedications([Service] IDataService dataService) =>
-        dataService.Medications;
+    public IReadOnlyList<MedicationModel> GetMedications([Service] IDataService dataService)
+    {
+        return dataService.Medications;
+    }

     /// <summary>Gets all available providers.</summary>
-    public IReadOnlyList<ProviderModel> GetProviders([Service] IDataService dataService) =>
-        dataService.Providers;
+    public IReadOnlyList<ProviderModel> GetProviders([Service] IDataService dataService)
+    {
+        return dataService.Providers;
+    }

     /// <summary>Gets all available payers.</summary>
-    public IReadOnlyList<PayerModel> GetPayers([Service] IDataService dataService) =>
-        dataService.Payers;
+    public IReadOnlyList<PayerModel> GetPayers([Service] IDataService dataService)
+    {
+        return dataService.Payers;
+    }

     /// <summary>Gets all available diagnoses.</summary>
-    public IReadOnlyList<DiagnosisModel> GetDiagnoses([Service] IDataService dataService) =>
-        dataService.Diagnoses;
+    public IReadOnlyList<DiagnosisModel> GetDiagnoses([Service] IDataService dataService)
+    {
+        return dataService.Diagnoses;
+    }

     /// <summary>Gets all PA requests.</summary>
-    public IReadOnlyList<PARequestModel> GetPARequests([Service] IDataService dataService) =>
-        dataService.GetPARequests();
+    public IReadOnlyList<PARequestModel> GetPARequests([Service] IDataService dataService)
+    {
+        return dataService.GetPARequests();
+    }

     /// <summary>Gets a single PA request by ID.</summary>
-    public PARequestModel? GetPARequest(string id, [Service] IDataService dataService) =>
-        dataService.GetPARequest(id);
+    public PARequestModel? GetPARequest(string id, [Service] IDataService dataService)
+    {
+        return dataService.GetPARequest(id);
+    }

     /// <summary>Gets aggregated PA statistics.</summary>
-    public PAStatsModel GetPAStats([Service] IDataService dataService) =>
-        dataService.GetPAStats();
+    public PAStatsModel GetPAStats([Service] IDataService dataService)
+    {
+        return dataService.GetPAStats();
+    }

     /// <summary>Gets recent activity items.</summary>
-    public IReadOnlyList<ActivityItemModel> GetActivity([Service] IDataService dataService) =>
-        dataService.GetActivity();
+    public IReadOnlyList<ActivityItemModel> GetActivity([Service] IDataService dataService)
+    {
+        return dataService.GetActivity();
+    }

As per coding guidelines, “Control: Guard clauses first, early return, no arrow code.”

🤖 Prompt for AI Agents
In `@apps/gateway/Gateway.API/GraphQL/Queries/Query.cs` around lines 17 - 51,
Convert each expression-bodied resolver (GetProcedures, GetMedications,
GetProviders, GetPayers, GetDiagnoses, GetPARequests, GetPARequest, GetPAStats,
GetActivity) to a block-bodied method with braces; add a guard clause at the top
validating the [Service] IDataService parameter (throw new
ArgumentNullException(nameof(dataService)) if null) and for GetPARequest also
validate the id (throw new ArgumentException if null or empty) before calling
the corresponding dataService member or method, then return the result.

Comment on lines +18 to +84
/// <inheritdoc />
public IReadOnlyList<ProcedureModel> Procedures =>
throw new NotImplementedException("LiveDataService.Procedures not yet implemented.");

/// <inheritdoc />
public IReadOnlyList<MedicationModel> Medications =>
throw new NotImplementedException("LiveDataService.Medications not yet implemented.");

/// <inheritdoc />
public IReadOnlyList<ProviderModel> Providers =>
throw new NotImplementedException("LiveDataService.Providers not yet implemented.");

/// <inheritdoc />
public IReadOnlyList<PayerModel> Payers =>
throw new NotImplementedException("LiveDataService.Payers not yet implemented.");

/// <inheritdoc />
public IReadOnlyList<DiagnosisModel> Diagnoses =>
throw new NotImplementedException("LiveDataService.Diagnoses not yet implemented.");

/// <inheritdoc />
public IReadOnlyList<PARequestModel> GetPARequests() =>
throw new NotImplementedException("LiveDataService.GetPARequests not yet implemented.");

/// <inheritdoc />
public PARequestModel? GetPARequest(string id) =>
throw new NotImplementedException("LiveDataService.GetPARequest not yet implemented.");

/// <inheritdoc />
public PAStatsModel GetPAStats() =>
throw new NotImplementedException("LiveDataService.GetPAStats not yet implemented.");

/// <inheritdoc />
public IReadOnlyList<ActivityItemModel> GetActivity() =>
throw new NotImplementedException("LiveDataService.GetActivity not yet implemented.");

/// <inheritdoc />
public PARequestModel CreatePARequest(PatientModel patient, string procedureCode, string diagnosisCode, string diagnosisName, string providerId = "DR001") =>
throw new NotImplementedException("LiveDataService.CreatePARequest not yet implemented.");

/// <inheritdoc />
public PARequestModel? UpdatePARequest(string id, string? diagnosis, string? diagnosisCode, string? serviceDate, string? placeOfService, string? clinicalSummary, IReadOnlyList<CriterionModel>? criteria) =>
throw new NotImplementedException("LiveDataService.UpdatePARequest not yet implemented.");

/// <inheritdoc />
public Task<PARequestModel?> ProcessPARequestAsync(string id, CancellationToken ct = default) =>
throw new NotImplementedException("LiveDataService.ProcessPARequestAsync not yet implemented.");

/// <inheritdoc />
public PARequestModel? SubmitPARequest(string id, int addReviewTimeSeconds = 0) =>
throw new NotImplementedException("LiveDataService.SubmitPARequest not yet implemented.");

/// <inheritdoc />
public PARequestModel? AddReviewTime(string id, int seconds) =>
throw new NotImplementedException("LiveDataService.AddReviewTime not yet implemented.");

/// <inheritdoc />
public bool DeletePARequest(string id) =>
throw new NotImplementedException("LiveDataService.DeletePARequest not yet implemented.");

/// <inheritdoc />
public PARequestModel? ApprovePA(string id) =>
throw new NotImplementedException("LiveDataService.ApprovePA not yet implemented.");

/// <inheritdoc />
public PARequestModel? DenyPA(string id, string reason) =>
throw new NotImplementedException("LiveDataService.DenyPA not yet implemented.");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the LiveDataService.cs file
fd -t f "LiveDataService.cs" apps/gateway

# Check file size and content around lines 18-84
if [ -f "apps/gateway/Gateway.API/Services/LiveDataService.cs" ]; then
  echo "=== File found, checking content ==="
  wc -l apps/gateway/Gateway.API/Services/LiveDataService.cs
  echo "=== Lines 15-90 (to see context) ==="
  sed -n '15,90p' apps/gateway/Gateway.API/Services/LiveDataService.cs | cat -n
fi

Repository: lvlup-sw/authscript

Length of output: 4116


Convert stubs to block-bodied members.

This file matches the apps/gateway/**/*.cs pattern, which requires "no arrow code." These expression-bodied members should be converted to block bodies.

🔧 Pattern to apply across members
-    public IReadOnlyList<ProcedureModel> Procedures =>
-        throw new NotImplementedException("LiveDataService.Procedures not yet implemented.");
+    public IReadOnlyList<ProcedureModel> Procedures
+    {
+        get
+        {
+            throw new NotImplementedException("LiveDataService.Procedures not yet implemented.");
+        }
+    }

-    public PARequestModel? ApprovePA(string id) =>
-        throw new NotImplementedException("LiveDataService.ApprovePA not yet implemented.");
+    public PARequestModel? ApprovePA(string id)
+    {
+        throw new NotImplementedException("LiveDataService.ApprovePA not yet implemented.");
+    }
🤖 Prompt for AI Agents
In `@apps/gateway/Gateway.API/Services/LiveDataService.cs` around lines 18 - 84,
The review requires replacing expression-bodied members with block-bodied
implementations: for each property and method that currently uses the "=>" throw
new NotImplementedException(...) (e.g., Procedures, Medications, Providers,
Payers, Diagnoses, GetPARequests, GetPARequest, GetPAStats, GetActivity,
CreatePARequest, UpdatePARequest, ProcessPARequestAsync, SubmitPARequest,
AddReviewTime, DeletePARequest, ApprovePA, DenyPA) change the arrow-bodied form
to a normal block body with braces and a single throw new
NotImplementedException("...") statement inside (preserve the existing exception
message and member signatures), ensuring async methods like
ProcessPARequestAsync keep their Task<> return type and CancellationToken
parameter; update each member accordingly so no expression-bodied members
remain.

Uses LLM-powered reasoning to extract evidence and generate PA form responses.
"""

import asyncio
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -path "*/apps/intelligence/src/api/analyze.py" -type f

Repository: lvlup-sw/authscript

Length of output: 101


🏁 Script executed:

cat -n ./apps/intelligence/src/api/analyze.py

Repository: lvlup-sw/authscript

Length of output: 6215


🏁 Script executed:

find . -path "*/src/parsers/pdf_parser.py" -type f

Repository: lvlup-sw/authscript

Length of output: 108


🏁 Script executed:

cat -n ./apps/intelligence/src/parsers/pdf_parser.py

Repository: lvlup-sw/authscript

Length of output: 2754


Remove or refactor the redundant exception handling in _safe_parse.

The broad except Exception at line 106 violates the specific-exception guideline. However, parse_pdf already handles all exceptions internally and never raises to its caller—it always returns a string. The proposed fix to catch specific exceptions like OSError or RuntimeError won't work because those exceptions won't reach this code.

Either remove the try-except entirely (since parse_pdf is fault-tolerant), or if defensive layering is intentional, document why and catch only exceptions that can actually propagate from the underlying PDF operations.

🤖 Prompt for AI Agents
In `@apps/intelligence/src/api/analyze.py` at line 6, The try/except in
_safe_parse is redundant because parse_pdf already catches all errors and
returns a string; remove the broad except Exception block from _safe_parse (or
entirely delete the try/except wrapper) so that _safe_parse simply
awaits/returns parse_pdf directly, or if you want defensive layering, replace
the broad catch with documented, specific exceptions that can actually propagate
from parse_pdf—refer to the _safe_parse and parse_pdf functions to make the
change and update or add a brief comment explaining the chosen approach.

Comment on lines +75 to +205
# --- A1: evaluate_criterion tests ---


@pytest.mark.asyncio
async def test_evaluate_criterion_returns_met_evidence_item():
"""Test that evaluate_criterion returns an EvidenceItem with MET status."""
criterion = {"id": "crit-1", "description": "Patient has documented symptoms", "required": True}
clinical_summary = "Patient presents with chronic lower back pain for 8 weeks."

mock_llm = AsyncMock(return_value="Based on the clinical data, this criterion is MET. The patient has documented symptoms of chronic lower back pain.")
with patch("src.reasoning.evidence_extractor.chat_completion", mock_llm):
result = await evaluate_criterion(criterion, clinical_summary)

assert isinstance(result, EvidenceItem)
assert result.criterion_id == "crit-1"
assert result.status == "MET"
assert result.confidence == 0.8


@pytest.mark.asyncio
async def test_evaluate_criterion_parses_not_met():
"""Test that evaluate_criterion correctly parses NOT_MET response."""
criterion = {"id": "crit-2", "description": "Conservative therapy completed", "required": True}
clinical_summary = "Patient has not attempted physical therapy."

mock_llm = AsyncMock(return_value="This criterion is NOT MET. No evidence of conservative therapy.")
with patch("src.reasoning.evidence_extractor.chat_completion", mock_llm):
result = await evaluate_criterion(criterion, clinical_summary)

assert result.status == "NOT_MET"
assert result.confidence == 0.8


@pytest.mark.asyncio
async def test_evaluate_criterion_handles_none_response():
"""Test that evaluate_criterion handles LLM returning None gracefully."""
criterion = {"id": "crit-3", "description": "Valid diagnosis", "required": False}
clinical_summary = "Patient data."

mock_llm = AsyncMock(return_value=None)
with patch("src.reasoning.evidence_extractor.chat_completion", mock_llm):
result = await evaluate_criterion(criterion, clinical_summary)

assert result.status == "UNCLEAR"
assert result.confidence == 0.5


# --- A2: Parallel evidence extraction tests ---


@pytest.mark.asyncio
async def test_extract_evidence_calls_criteria_concurrently():
"""Test that evidence extraction runs criteria evaluation in parallel."""
import asyncio
import time

call_times: list[float] = []

async def slow_llm(*args, **kwargs):
call_times.append(time.monotonic())
await asyncio.sleep(0.1) # Simulate LLM latency
return "The criterion is MET based on clinical data."

bundle = ClinicalBundle(
patient_id="test-patient",
patient=PatientInfo(name="Test Patient", birth_date=date(1990, 1, 1)),
conditions=[Condition(code="M54.5", display="Low back pain")],
)
policy = {
"name": "Test Policy",
"criteria": [
{"id": f"crit-{i}", "description": f"Criterion {i}", "required": True}
for i in range(3)
],
"procedure_codes": ["72148"],
}

mock_llm = AsyncMock(side_effect=slow_llm)
with patch("src.reasoning.evidence_extractor.chat_completion", mock_llm):
start = time.monotonic()
results = await extract_evidence(bundle, policy)
duration = time.monotonic() - start

assert len(results) == 3
# If parallel: ~0.1s. If sequential: ~0.3s. Allow margin.
assert duration < 0.25, f"Expected parallel execution (<0.25s), got {duration:.2f}s"


@pytest.mark.asyncio
async def test_extract_evidence_respects_semaphore_limit():
"""Test that concurrent LLM calls are bounded by semaphore."""
import asyncio

max_concurrent = 0
current_concurrent = 0
lock = asyncio.Lock()

async def counting_llm(*args, **kwargs):
nonlocal max_concurrent, current_concurrent
async with lock:
current_concurrent += 1
max_concurrent = max(max_concurrent, current_concurrent)
await asyncio.sleep(0.05)
async with lock:
current_concurrent -= 1
return "The criterion is MET."

bundle = ClinicalBundle(
patient_id="test-patient",
patient=PatientInfo(name="Test", birth_date=date(1990, 1, 1)),
conditions=[Condition(code="M54.5", display="Low back pain")],
)
policy = {
"name": "Test",
"criteria": [
{"id": f"crit-{i}", "description": f"Criterion {i}", "required": True}
for i in range(6)
],
"procedure_codes": ["72148"],
}

mock_llm = AsyncMock(side_effect=counting_llm)
with (
patch("src.reasoning.evidence_extractor.chat_completion", mock_llm),
patch("src.reasoning.evidence_extractor._get_llm_semaphore", return_value=asyncio.Semaphore(2)),
):
results = await extract_evidence(bundle, policy)

assert len(results) == 6
assert max_concurrent <= 2, f"Expected max 2 concurrent, got {max_concurrent}"

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if file exists and examine the actual function signatures
if [ -f "apps/intelligence/src/tests/test_evidence_extractor.py" ]; then
  echo "File found. Checking function signatures around lines 75-205:"
  sed -n '75,205p' "apps/intelligence/src/tests/test_evidence_extractor.py" | grep -E "^\s*(async\s+)?def\s+\w+.*:\s*$" -A 1
else
  # Try alternative paths
  find . -name "test_evidence_extractor.py" -type f 2>/dev/null | head -5
fi

Repository: lvlup-sw/authscript

Length of output: 998


🏁 Script executed:

#!/bin/bash
# Search for the test file with flexible path matching
fd "test_evidence_extractor" --type f 2>/dev/null | head -5

Repository: lvlup-sw/authscript

Length of output: 117


🏁 Script executed:

#!/bin/bash
# Get exact function signatures with line numbers to confirm completeness
sed -n '75,205p' "apps/intelligence/src/tests/test_evidence_extractor.py" | grep -n "def " | head -20

Repository: lvlup-sw/authscript

Length of output: 465


🏁 Script executed:

#!/bin/bash
# Extract the exact lines around the function definitions to verify annotations
sed -n '78,80p; 85,87p; 93,95p; 103,105p; 106,108p; 120,122p; 123,125p' "apps/intelligence/src/tests/test_evidence_extractor.py"

Repository: lvlup-sw/authscript

Length of output: 622


🏁 Script executed:

#!/bin/bash
# Get the complete function signatures to confirm they lack return type annotations
sed -n '78p; 85p; 93p; 103p; 106p; 120p; 123p' "apps/intelligence/src/tests/test_evidence_extractor.py"

Repository: lvlup-sw/authscript

Length of output: 166


🏁 Script executed:

#!/bin/bash
# Check the context around slow_llm and counting_llm to verify their return types
sed -n '106,115p; 123,133p' "apps/intelligence/src/tests/test_evidence_extractor.py"

Repository: lvlup-sw/authscript

Length of output: 745


🏁 Script executed:

#!/bin/bash
# Extract exact function definitions from the file
cat -n "apps/intelligence/src/tests/test_evidence_extractor.py" | sed -n '78,180p' | grep -E "^\s*[0-9]+\s+(async\s+)?def\s+" | head -20

Repository: lvlup-sw/authscript

Length of output: 494


🏁 Script executed:

#!/bin/bash
# Get the full signatures by reading specific line ranges
python3 << 'EOF'
with open("apps/intelligence/src/tests/test_evidence_extractor.py") as f:
    lines = f.readlines()
    # Check lines 75-205 for function definitions
    for i in range(74, min(205, len(lines))):
        line = lines[i]
        if line.strip().startswith('async def ') or line.strip().startswith('def '):
            # Print the function definition line and check if it has return annotation
            print(f"Line {i+1}: {line.rstrip()}")
            if '->' not in line:
                print("  ✗ Missing return type annotation")
            else:
                print("  ✓ Has return type annotation")
EOF

Repository: lvlup-sw/authscript

Length of output: 758


🏁 Script executed:

#!/bin/bash
# Verify the context of return statements in helper functions
python3 << 'EOF'
with open("apps/intelligence/src/tests/test_evidence_extractor.py") as f:
    lines = f.readlines()
    
# Check slow_llm return type
print("=== slow_llm context ===")
for i in range(132, min(145, len(lines))):
    print(f"{i+1}: {lines[i].rstrip()}")

print("\n=== counting_llm context ===")
for i in range(171, min(184, len(lines))):
    print(f"{i+1}: {lines[i].rstrip()}")
EOF

Repository: lvlup-sw/authscript

Length of output: 1237


Add return type annotations for async test functions and helpers.

All test functions and helper coroutines in this section lack return type annotations, violating the type-hint requirement for apps/intelligence/**/*.py. Test functions should return -> None, while slow_llm and counting_llm should return -> str (both return string literals).

Example annotations
-async def test_evaluate_criterion_returns_met_evidence_item():
+async def test_evaluate_criterion_returns_met_evidence_item() -> None:

-async def test_evaluate_criterion_parses_not_met():
+async def test_evaluate_criterion_parses_not_met() -> None:

-async def test_evaluate_criterion_handles_none_response():
+async def test_evaluate_criterion_handles_none_response() -> None:

-async def test_extract_evidence_calls_criteria_concurrently():
+async def test_extract_evidence_calls_criteria_concurrently() -> None:

-    async def slow_llm(*args, **kwargs):
+    async def slow_llm(*args, **kwargs) -> str:

-async def test_extract_evidence_respects_semaphore_limit():
+async def test_extract_evidence_respects_semaphore_limit() -> None:

-    async def counting_llm(*args, **kwargs):
+    async def counting_llm(*args, **kwargs) -> str:
🤖 Prompt for AI Agents
In `@apps/intelligence/src/tests/test_evidence_extractor.py` around lines 75 -
205, Add explicit return type annotations: annotate all async test coroutines
(test_evaluate_criterion_returns_met_evidence_item,
test_evaluate_criterion_parses_not_met,
test_evaluate_criterion_handles_none_response,
test_extract_evidence_calls_criteria_concurrently,
test_extract_evidence_respects_semaphore_limit) with -> None, and annotate
helper LLM coroutines slow_llm and counting_llm with -> str; update their async
def signatures accordingly so the file complies with the apps/intelligence
type-hint rule.

Comment on lines +1 to +187
"""Tests for LLM client singleton pooling, timeout, and retry."""

import pytest
from unittest.mock import patch, MagicMock, AsyncMock


def test_get_provider_returns_singleton():
"""Test that _get_provider returns the same instance on repeated calls."""
import src.llm_client as llm_mod

# Reset cached provider
llm_mod._cached_provider = None

with patch.object(llm_mod, "settings") as mock_settings:
mock_settings.llm_configured = True
mock_settings.llm_provider = "openai"
mock_settings.openai_api_key = "test-key"
mock_settings.openai_org_id = ""
mock_settings.openai_model = "gpt-4.1"
mock_settings.llm_timeout = 30.0
mock_settings.llm_max_retries = 2

provider1 = llm_mod._get_provider()
provider2 = llm_mod._get_provider()

assert provider1 is provider2, "Expected same provider instance (singleton)"

# Cleanup
llm_mod._cached_provider = None


def test_openai_provider_creates_client_once():
"""Test that OpenAIProvider creates the client in __init__, not per call."""
from src.llm_client import OpenAIProvider

with patch("src.llm_client.settings") as mock_settings:
mock_settings.openai_api_key = "test-key"
mock_settings.openai_org_id = ""
mock_settings.openai_model = "gpt-4.1"
mock_settings.llm_timeout = 30.0
mock_settings.llm_max_retries = 2

provider = OpenAIProvider()

assert hasattr(provider, "_client"), "Provider should store client as _client"


@pytest.mark.asyncio
async def test_chat_completion_uses_provider_singleton():
"""Test that repeated chat_completion calls reuse the same provider."""
import src.llm_client as llm_mod

llm_mod._cached_provider = None

# Mock the provider's complete method
mock_provider = AsyncMock()
mock_provider.complete = AsyncMock(return_value="test response")
llm_mod._cached_provider = mock_provider

with patch.object(llm_mod, "settings") as mock_settings:
mock_settings.llm_configured = True

await llm_mod.chat_completion("system", "user")
await llm_mod.chat_completion("system", "user")

# Provider should be reused, not re-created
assert mock_provider.complete.call_count == 2

# Cleanup
llm_mod._cached_provider = None


# --- A4: Structured error handling tests ---


@pytest.mark.asyncio
async def test_chat_completion_returns_none_on_timeout():
"""Test that APITimeoutError is caught and returns None."""
import src.llm_client as llm_mod
from openai import APITimeoutError

mock_provider = AsyncMock()
mock_provider.complete = AsyncMock(side_effect=APITimeoutError(request=MagicMock()))
llm_mod._cached_provider = mock_provider

with patch.object(llm_mod, "settings") as mock_settings:
mock_settings.llm_configured = True
result = await llm_mod.chat_completion("system", "user")

assert result is None

# Cleanup
llm_mod._cached_provider = None


@pytest.mark.asyncio
async def test_chat_completion_raises_on_rate_limit():
"""Test that RateLimitError propagates instead of being swallowed."""
import src.llm_client as llm_mod
from openai import RateLimitError

mock_provider = AsyncMock()
mock_response = MagicMock()
mock_response.status_code = 429
mock_response.headers = {}
mock_provider.complete = AsyncMock(
side_effect=RateLimitError(
message="Rate limited",
response=mock_response,
body=None,
)
)
llm_mod._cached_provider = mock_provider

with (
patch.object(llm_mod, "settings") as mock_settings,
pytest.raises(RateLimitError),
):
mock_settings.llm_configured = True
await llm_mod.chat_completion("system", "user")

# Cleanup
llm_mod._cached_provider = None


@pytest.mark.asyncio
async def test_chat_completion_returns_none_on_api_error():
"""Test that APIError is caught, logged, and returns None."""
import src.llm_client as llm_mod
from openai import APIError

mock_provider = AsyncMock()
mock_provider.complete = AsyncMock(
side_effect=APIError(message="Server error", request=MagicMock(), body=None)
)
llm_mod._cached_provider = mock_provider

with patch.object(llm_mod, "settings") as mock_settings:
mock_settings.llm_configured = True
result = await llm_mod.chat_completion("system", "user")

assert result is None

# Cleanup
llm_mod._cached_provider = None


# --- H2: Thread-safe provider singleton ---


def test_get_provider_thread_safe():
"""Concurrent calls to _get_provider return the same instance."""
import threading

import src.llm_client as llm_mod

# Reset cached provider
llm_mod._cached_provider = None

results: list[int] = []
barrier = threading.Barrier(3)

def get_provider_thread():
barrier.wait() # Ensure all threads start simultaneously
provider = llm_mod._get_provider()
results.append(id(provider))

with patch.object(llm_mod, "settings") as mock_settings:
mock_settings.llm_configured = True
mock_settings.llm_provider = "openai"
mock_settings.openai_api_key = "test-key"
mock_settings.openai_org_id = ""
mock_settings.openai_model = "gpt-4.1"
mock_settings.llm_timeout = 30.0
mock_settings.llm_max_retries = 2

threads = [threading.Thread(target=get_provider_thread) for _ in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()

# All threads should get the same instance
assert len(set(results)) == 1, f"Expected 1 unique provider, got {len(set(results))}"

# Cleanup
llm_mod._cached_provider = None
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Verify the file path matches the patterns
echo "File: apps/intelligence/src/tests/test_llm_client.py"
echo "Pattern 1: apps/intelligence/**/*.py - matches: YES"
echo "Pattern 2: **/test_*.py - matches: YES"
echo ""
echo "Checking which guideline should apply when both patterns match..."

Repository: lvlup-sw/authscript

Length of output: 273


🏁 Script executed:

# Find other test files in apps/intelligence
fd "test_.*\.py" apps/intelligence --type f

# Check if other test files in this directory have type hints
echo "=== Checking for type hints in other test files ==="
fd "test_.*\.py" apps/intelligence --type f | head -5 | xargs -I {} sh -c 'echo "File: {}"; head -30 {}'

# Check production code in apps/intelligence to see if type hints are strictly enforced
echo ""
echo "=== Checking production code for type hints pattern ==="
fd "\.py" apps/intelligence/src --type f ! -path "*/tests/*" | head -3 | xargs -I {} sh -c 'echo "File: {}"; head -40 {}'

Repository: lvlup-sw/authscript

Length of output: 5165


🏁 Script executed:

# Search for test functions with explicit -> None return type
rg "def test_.*\(\)\s*->\s*None" apps/intelligence --type py

# Also check for async test functions with -> None
rg "async def test_.*\(\)\s*->\s*None" apps/intelligence --type py

# Count test functions without return type annotations
echo "=== Test functions in test_pdf_parser.py without return type ==="
rg "^\s*(async\s+)?def test_" apps/intelligence/src/tests/test_pdf_parser.py -A 1 --type py | head -20

Repository: lvlup-sw/authscript

Length of output: 1501


Add return type annotations to match codebase conventions.

Test functions and helpers lack -> None return type annotations. Other test files in apps/intelligence/src/tests/ (such as test_analyze.py, test_evidence_extractor.py, and test_form_generator.py) include these annotations consistently. Add -> None to all test functions and the get_provider_thread() helper to align with the established pattern and satisfy the guideline requirement for complete type annotations.

🛠️ Example update (apply to all test functions and helpers)
-def test_get_provider_returns_singleton():
+def test_get_provider_returns_singleton() -> None:
@@
-def test_openai_provider_creates_client_once():
+def test_openai_provider_creates_client_once() -> None:
@@
-@pytest.mark.asyncio
-async def test_chat_completion_uses_provider_singleton():
+@pytest.mark.asyncio
+async def test_chat_completion_uses_provider_singleton() -> None:
@@
-    def get_provider_thread():
+    def get_provider_thread() -> None:
🤖 Prompt for AI Agents
In `@apps/intelligence/src/tests/test_llm_client.py` around lines 1 - 187, The
tests lack explicit return type annotations; update every test function
(test_get_provider_returns_singleton, test_openai_provider_creates_client_once,
test_chat_completion_uses_provider_singleton,
test_chat_completion_returns_none_on_timeout,
test_chat_completion_raises_on_rate_limit,
test_chat_completion_returns_none_on_api_error, test_get_provider_thread_safe)
and the helper get_provider_thread to include the return type -> None (e.g., def
test_xxx(...) -> None:) to match the codebase convention for complete type
annotations; no other behavior changes are needed.

Comment on lines +1 to +49
"""Tests for PDF parser thread pool and parallel execution."""

import asyncio
import time

import pytest
from unittest.mock import patch, MagicMock


@pytest.mark.asyncio
async def test_parse_pdf_does_not_block_event_loop():
"""Test that parse_pdf uses run_in_executor for sync operations."""
from src.parsers.pdf_parser import parse_pdf

mock_extract = MagicMock(return_value="# Extracted Text\nSome content")

with patch("src.parsers.pdf_parser._extract_sync", mock_extract):
result = await parse_pdf(b"%PDF-1.4 fake content")

assert "Extracted Text" in result
mock_extract.assert_called_once()


@pytest.mark.asyncio
async def test_parse_pdf_multiple_docs_parallel():
"""Test that multiple PDFs can be parsed concurrently."""
from src.parsers.pdf_parser import parse_pdf

call_times: list[float] = []

def slow_extract(pdf_bytes):
call_times.append(time.monotonic())
import time as t

t.sleep(0.05) # Simulate extraction time
return f"# Content from {len(pdf_bytes)} bytes"

with patch("src.parsers.pdf_parser._extract_sync", side_effect=slow_extract):
start = time.monotonic()
results = await asyncio.gather(
parse_pdf(b"pdf1"),
parse_pdf(b"pdf2"),
parse_pdf(b"pdf3"),
)
duration = time.monotonic() - start

assert len(results) == 3
# If parallel (thread pool): ~0.05s. If sequential: ~0.15s
assert duration < 0.12, f"Expected parallel execution (<0.12s), got {duration:.2f}s"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the test file
find . -name "test_pdf_parser.py" -type f 2>/dev/null | head -20

Repository: lvlup-sw/authscript

Length of output: 111


🏁 Script executed:

# Check file size and read content if it exists
if [ -f "apps/intelligence/src/tests/test_pdf_parser.py" ]; then
  echo "=== File found at apps/intelligence/src/tests/test_pdf_parser.py ==="
  wc -l apps/intelligence/src/tests/test_pdf_parser.py
  echo "=== Content ==="
  head -60 apps/intelligence/src/tests/test_pdf_parser.py
else
  echo "File not found at expected path"
fi

Repository: lvlup-sw/authscript

Length of output: 1745


🏁 Script executed:

# Check if there are any other test_pdf_parser.py files in the repo
find . -path "*/test*" -name "*pdf*parser*" -type f 2>/dev/null

Repository: lvlup-sw/authscript

Length of output: 111


🏁 Script executed:

# Check other test files under apps/intelligence to see how they handle type hints
find ./apps/intelligence -name "test_*.py" -type f 2>/dev/null | head -5

Repository: lvlup-sw/authscript

Length of output: 316


🏁 Script executed:

# Let's examine a couple of other test files to see the pattern
for file in $(find ./apps/intelligence -name "test_*.py" -type f 2>/dev/null | head -3); do
  echo "=== File: $file ==="
  head -30 "$file" | grep -A 2 "^async def\|^def " || echo "No function definitions in first 30 lines"
done

Repository: lvlup-sw/authscript

Length of output: 804


🏁 Script executed:

# Let's look at actual test functions in other test files to see if they have return type annotations
for file in $(find ./apps/intelligence -name "test_*.py" -type f 2>/dev/null | head -3); do
  echo "=== File: $file ==="
  grep -n "^async def test_\|^def test_" "$file" | head -5
done

Repository: lvlup-sw/authscript

Length of output: 1252


🏁 Script executed:

# Get more context around test functions in one of the other test files
head -60 ./apps/intelligence/src/tests/test_analyze.py | tail -40

Repository: lvlup-sw/authscript

Length of output: 1432


Add return type annotations for the test functions and helper.

The codebase pattern in apps/intelligence/ requires complete type annotations on all functions. Both async test functions need -> None, and the slow_extract helper needs parameter and return type annotations.

Example update
-@pytest.mark.asyncio
-async def test_parse_pdf_does_not_block_event_loop():
+@pytest.mark.asyncio
+async def test_parse_pdf_does_not_block_event_loop() -> None:
@@
-@pytest.mark.asyncio
-async def test_parse_pdf_multiple_docs_parallel():
+@pytest.mark.asyncio
+async def test_parse_pdf_multiple_docs_parallel() -> None:
@@
-    def slow_extract(pdf_bytes):
+    def slow_extract(pdf_bytes: bytes) -> str:
🤖 Prompt for AI Agents
In `@apps/intelligence/src/tests/test_pdf_parser.py` around lines 1 - 49, Add
missing type annotations: annotate both async test functions
test_parse_pdf_does_not_block_event_loop and
test_parse_pdf_multiple_docs_parallel with return type -> None, and add type
hints to the helper slow_extract as (pdf_bytes: bytes) -> str; update its
signature and any local variables' annotations if necessary so the functions
follow the codebase pattern of complete function type annotations.

… handling, config validation

- Add specific paRequest(id) query invalidation to approve/deny mutation hooks
  to prevent stale cache when viewing individual requests
- Remove redundant _safe_parse wrapper in analyze.py since parse_pdf already
  handles errors internally
- Add Pydantic Field constraints (ge/gt) to LLM performance settings to reject
  invalid environment values at startup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@rsalus rsalus changed the base branch from feature/clafonso/dashboardBackendIntegration to main February 18, 2026 02:49
@rsalus
Copy link
Contributor Author

rsalus commented Feb 18, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

✅ Actions performed

Review triggered.

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/dashboard/src/api/customFetch.ts (1)

69-107: ⚠️ Potential issue | 🟡 Minor

Update the auth comment to reflect authscript_session priority.
The block says “SMART on FHIR session storage,” but the code now delegates to getAuthToken() which checks authscript_session first. This is a minor doc accuracy mismatch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/api/customFetch.ts` around lines 69 - 107, Update the
comment above customFetch to accurately state that authorization is obtained via
getAuthToken (which prefers the authscript_session value) rather than
generically saying “SMART on FHIR session storage”; mention getAuthToken by name
and that it checks authscript_session first so readers know the priority and
source of the token used for setting the Authorization header in customFetch.
apps/intelligence/src/llm_client.py (1)

198-216: ⚠️ Potential issue | 🟠 Major

Remove the generic except Exception catch-all from chat_completion.

The catch swallows unexpected failures and violates the coding guideline requiring specific exceptions over bare except. The function already handles all documented OpenAI client exceptions (APITimeoutError, RateLimitError, APIError). Any other exception should propagate rather than being silently logged and suppressed.

Proposed fix
-    except Exception as e:
-        logger.error("Unexpected LLM error: %s", e)
-        return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/intelligence/src/llm_client.py` around lines 198 - 216, The generic
"except Exception" block in chat_completion should be removed so unexpected
errors are not swallowed; keep the existing specific handlers for
APITimeoutError, RateLimitError and APIError (referencing CompletionRequest,
provider.complete, logger) and allow any other exceptions to propagate to the
caller rather than catching and logging them silently. Ensure the function still
returns None for handled cases (timeout/APIError) and re-raises RateLimitError
as currently implemented.
🧹 Nitpick comments (11)
apps/gateway/Gateway.API/appsettings.Development.json (1)

8-10: Keep OAuth client configuration environment-scoped.

Athena:ClientId is environment-specific; consider moving it to user-secrets/env vars to prevent accidental reuse across environments and keep appsettings as a non-sensitive template.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/Gateway.API/appsettings.Development.json` around lines 8 - 10,
The Athena:ClientId entry in the Athena configuration block is
environment-specific and should be removed from appsettings.Development.json and
moved to an environment-secret mechanism (user-secrets or environment variable);
update code that reads the client id (where you reference "Athena:ClientId" via
IConfiguration or options binding) to fall back to
IConfiguration["Athena:ClientId"] (or an injected
IOptions<AthenaOptions>.Value.ClientId) so it reads from env/user-secrets at
runtime, and leave the appsettings Athena section as a non-sensitive template
(e.g., keep FhirBaseUrl but not ClientId).
apps/gateway/Gateway.API.Tests/Services/Polling/AthenaPollingServiceTests.cs (1)

231-263: Consider applying the same TaskCompletionSource pattern here for consistency.

This test still uses Task.Delay(100) (line 247), while the updated test above uses the more deterministic TaskCompletionSource approach. Aligning the patterns across similar tests would improve consistency and reduce potential flakiness.

♻️ Optional refactor for consistency
     [Test]
     public async Task ExecuteAsync_WithRegisteredPatients_CallsGetActiveAsync()
     {
         // Arrange
+        var getActiveCalled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
         var patients = new List<RegisteredPatient>
         {
             new() { PatientId = "p1", EncounterId = "e1", PracticeId = "pr1", WorkItemId = "w1", RegisteredAt = DateTimeOffset.UtcNow },
             new() { PatientId = "p2", EncounterId = "e2", PracticeId = "pr2", WorkItemId = "w2", RegisteredAt = DateTimeOffset.UtcNow },
         };
         _patientRegistry.GetActiveAsync(Arg.Any<CancellationToken>())
-            .Returns(patients);
+            .Returns(patients)
+            .AndDoes(_ => getActiveCalled.TrySetResult());

         using var cts = new CancellationTokenSource();

         // Act
         var task = _sut.StartAsync(cts.Token);
-        await Task.Delay(100);
+        await Task.WhenAny(getActiveCalled.Task, Task.Delay(TimeSpan.FromSeconds(10)));
         cts.Cancel();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/Gateway.API.Tests/Services/Polling/AthenaPollingServiceTests.cs`
around lines 231 - 263, The test
ExecuteAsync_WithRegisteredPatients_CallsGetActiveAsync is using Task.Delay(100)
which is flaky; replace that timing-based wait with a TaskCompletionSource that
completes when _patientRegistry.GetActiveAsync is invoked. Update the
_patientRegistry.GetActiveAsync(Arg.Any<CancellationToken>()) setup to call
tcs.SetResult(true) (or similar) inside its Returns callback and have the test
await tcs.Task instead of Task.Delay before cancelling the
CancellationTokenSource; keep the rest of the test assertions
(StartAsync/StopAsync and Received() call) unchanged.
apps/dashboard/src/lib/patients.ts (1)

22-22: Consider marking the test data as readonly.

Per coding guidelines, readonly should be the default for types. Since this array is test fixture data that shouldn't be mutated at runtime, marking it immutable improves safety.

♻️ Proposed fix
-export const ATHENA_TEST_PATIENTS: Patient[] = [
+export const ATHENA_TEST_PATIENTS: readonly Patient[] = [

As per coding guidelines: "apps/dashboard/**/*.ts: Types: ... readonly default".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/lib/patients.ts` at line 22, The ATHENA_TEST_PATIENTS test
fixture is mutable but should be immutable; change its type annotation from
Patient[] to a readonly type (e.g., export const ATHENA_TEST_PATIENTS: readonly
Patient[] = […] or export const ATHENA_TEST_PATIENTS: ReadonlyArray<Patient> =
[…] ) so the array cannot be mutated at runtime and matches the
readonly-by-default guideline.
apps/gateway/Gateway.API/Services/DocumentUploader.cs (1)

35-35: Redundant fully qualified type names.

Gateway.API.Contracts is already imported (line 3), so Gateway.API.Contracts.Result<string> can be simplified to Result<string>. If there's a naming collision with another Result type, consider using a type alias for clarity.

🧹 Suggested simplification
-    public async Task<Gateway.API.Contracts.Result<string>> UploadDocumentAsync(
+    public async Task<Result<string>> UploadDocumentAsync(
-            return Gateway.API.Contracts.Result<string>.Failure(result.Error!);
+            return Result<string>.Failure(result.Error!);
-        return Gateway.API.Contracts.Result<string>.Success(documentId);
+        return Result<string>.Success(documentId);

Also applies to: 55-55, 72-72

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/Gateway.API/Services/DocumentUploader.cs` at line 35, The method
signatures (e.g., UploadDocumentAsync) and other places using the fully
qualified type Gateway.API.Contracts.Result<string> should be simplified to
Result<string> since Gateway.API.Contracts is already imported; update all
occurrences (including the other Result<string> uses in the same class) to the
shorter form, or if there is a naming collision with another Result type, add a
using alias (e.g., using ContractsResult = Gateway.API.Contracts.Result<string>)
and use that alias in the method signatures and return statements to keep intent
clear.
apps/gateway/Gateway.API/Services/Fhir/FhirHttpClient.cs (1)

33-33: Remove redundant fully qualified type names.

Gateway.API.Contracts is imported on line 4, so the fully qualified references to Result<T> are unnecessary. Using the imported namespace directly reduces visual noise across all four async methods.

Applies to lines: 33, 53, 68, 88, 103, 130, 145, 164

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/Gateway.API/Services/Fhir/FhirHttpClient.cs` at line 33, The
method signatures in FhirHttpClient use redundant fully qualified types like
Gateway.API.Contracts.Result<T>; update each async method signature (e.g.,
ReadAsync and the other async methods in the class) to use the imported
namespace type name (Result<T>) instead of Gateway.API.Contracts.Result<T> for
JsonElement, string, etc., and do the same for their return types and any
related local variable/type references so the code relies on the using import
and removes the visual noise.
apps/dashboard/src/components/SubmissionProgressOverlay.tsx (1)

9-10: Consolidate lucide-react imports.

Minor nit: These two imports can be merged into a single statement.

♻️ Suggested consolidation
-import { Search, Check, CheckCircle2 } from 'lucide-react';
-import { Sparkles } from 'lucide-react';
+import { Search, Check, CheckCircle2, Sparkles } from 'lucide-react';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/SubmissionProgressOverlay.tsx` around lines 9 -
10, The imports from 'lucide-react' are split; consolidate them into a single
import statement that imports Search, Check, CheckCircle2 and Sparkles together
(update the import that currently declares Search, Check, CheckCircle2 and the
separate Sparkles import in SubmissionProgressOverlay.tsx) so all lucide-react
icons are imported in one line.
apps/gateway/Gateway.API.Tests/Services/LiveDataServiceTests.cs (1)

11-22: Seal the test class to match gateway conventions.
This keeps consistency with the “sealed by default” guideline.

🛠️ Suggested change
-public class LiveDataServiceTests
+public sealed class LiveDataServiceTests

As per coding guidelines, “Types: Sealed by default, records for DTOs, nullable enabled”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/Gateway.API.Tests/Services/LiveDataServiceTests.cs` around lines
11 - 22, The test class LiveDataServiceTests should be declared sealed to follow
the "sealed by default" convention; update the class declaration for
LiveDataServiceTests to include the sealed modifier (i.e., make class
LiveDataServiceTests sealed) so the test type follows gateway conventions and
matches other test classes.
apps/dashboard/src/api/graphqlService.ts (1)

199-272: Default exported interfaces to readonly properties.
These shapes are reused across the app; marking fields readonly by default will better align with the project’s TypeScript standard.

♻️ Example adjustment (apply broadly)
 export interface PAStats {
-  ready: number;
-  submitted: number;
-  waitingForInsurance: number;
-  attention: number;
-  total: number;
+  readonly ready: number;
+  readonly submitted: number;
+  readonly waitingForInsurance: number;
+  readonly attention: number;
+  readonly total: number;
 }

As per coding guidelines, “Types: Interfaces for extendable shapes, readonly default, no any”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/api/graphqlService.ts` around lines 199 - 272, Make all
interface properties readonly to follow project TS conventions: update Patient,
Procedure, Medication, Criterion, PARequest, PAStats, and ActivityItem so every
field is prefixed with readonly (including nested object properties like
PARequest.patient) and convert arrays to readonly types (e.g.,
PARequest.criteria -> ReadonlyArray<Criterion>); preserve optional/union types
(e.g., readyAt?: string | null) but mark them readonly as well so the shapes are
immutable by default.
apps/dashboard/src/routes/analysis.$transactionId.tsx (1)

41-250: Consider extracting helper components and defining prop interfaces.
ProgressRing/EditableField/CriteriaItem/CriteriaReasonDialog are all inline with anonymous prop types; moving them to their own files and adding Props interfaces will better match the project standards.
As per coding guidelines, “Components: One component per file, use functional components with hooks” and “Props: Define interface for props, use destructuring”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/routes/analysis`.$transactionId.tsx around lines 41 - 250,
The four inline components (ProgressRing, EditableField, CriteriaItem,
CriteriaReasonDialog) should be moved to separate files and given explicit Props
interfaces: create ProgressRing.tsx, EditableField.tsx, CriteriaItem.tsx,
CriteriaReasonDialog.tsx; define and export an interface (e.g.,
ProgressRingProps, EditableFieldProps, CriteriaItemProps,
CriteriaReasonDialogProps) describing each prop shape (including types like
number, boolean, string, optional fields and callbacks), convert the anonymous
inline props to those interfaces and use destructured parameters in the
functional components, export them as default (or named) and import them back
into the parent route file; ensure any helper utilities used (displayConfidence,
cn, icon components, createPortal) are imported where needed and update
imports/exports accordingly to satisfy the “one component per file” and
prop-interface guidelines.
apps/dashboard/src/routes/index.tsx (1)

33-120: Extract helper components and define props interfaces.
StatCard/WorkflowPipeline (and other helpers in this file) are inline with anonymous prop types; splitting them into separate files with explicit Props interfaces will align with the standards.
As per coding guidelines, “Components: One component per file, use functional components with hooks” and “Props: Define interface for props, use destructuring”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/routes/index.tsx` around lines 33 - 120, Move the inline
components into their own files and add explicit props interfaces: create
StatCard.tsx exporting the StatCard component and define a StatCardProps
interface for { label: string; value: string | number; change?: string;
positive?: boolean; icon: React.ElementType }, then import and use it where
needed; similarly create WorkflowPipeline.tsx with a WorkflowPipelineProps
interface if it needs props (or export as a standalone component) and extract
the internal steps array or accept it via props; ensure each file uses a named
functional component (not anonymous) and default/consistent exports and update
any imports in the parent file accordingly (refer to StatCard and
WorkflowPipeline in the diff).
apps/intelligence/src/api/analyze.py (1)

100-103: Consider bounding PDF reads/parses to avoid memory spikes.
Reading all document bytes upfront keeps every PDF resident in memory; with many/large uploads this can spike RAM. Consider batching with a semaphore or streaming each file into parse tasks so bytes can be released sooner.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/intelligence/src/api/analyze.py` around lines 100 - 103, The code
currently reads all documents into pdf_bytes_list then calls document_texts =
list(await asyncio.gather(*[parse_pdf(b) for b in pdf_bytes_list])), which keeps
every PDF in memory; change to bounded concurrency so bytes are released sooner:
iterate documents and create tasks that call parse_pdf on each stream/read chunk
but gate concurrent parse_pdf calls with an asyncio.Semaphore (or process in
small batches) so you only hold a few pdf_bytes at once; reference
pdf_bytes_list, parse_pdf, documents, and document_texts and ensure each
read/parse task releases its bytes when done so memory usage is bounded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/dashboard/package.json`:
- Around line 33-36: The package.json entry for the dependency "graphql-request"
is pinned to "^7.0.0" and needs to be updated to "^7.4.0"; open
apps/dashboard/package.json, locate the "graphql-request" dependency line and
bump the version string to 7.4.0 (keeping the existing caret if desired), then
run yarn/npm install and update lockfile to ensure the new version is recorded.

In `@apps/dashboard/src/lib/patients.ts`:
- Around line 7-20: The exported ATHENA_TEST_PATIENTS array should be typed as
readonly to follow coding guidelines: update its annotation to readonly
Patient[] (or use as const) so the test data is immutable and TypeScript
enforces it; also to avoid future type collisions, rename the other Patient
interface in graphqlService.ts to AthenaTestPatient (update its usages/imports
accordingly) so the distinct shapes are clear and not accidentally mixed up.

In `@apps/dashboard/src/lib/pdfGenerator.ts`:
- Around line 365-418: The current generatePAPdfBlob function builds CSS by
using doc.head.innerHTML which can include non-CSS nodes like <title>, causing
invalid CSS in the injected <style>; change the logic that computes styles
(currently using doc.head.innerHTML.replace(...)) to instead collect only CSS
text from the document's <style> elements (e.g. iterate
doc.querySelectorAll('head style'), join their .textContent, then perform the
body selector replacement like .replace(/\bbody\b/g, `#${PDF_CONTAINER_ID}`)).
Keep the rest of the DOM insertion (container.id = PDF_CONTAINER_ID,
container.innerHTML) the same so only valid CSS is injected and other head nodes
(title/meta) aren’t wrapped into the <style>.

In `@apps/dashboard/src/routes/analysis`.$transactionId.tsx:
- Around line 926-940: The PDF modal is receiving the original request variable
(prop request) so it can show outdated data when reanalysis overrides
criteria/confidence; update the code so PdfViewerModal receives the effective
request used by the UI (e.g., a computed value like effectiveRequest,
requestWithOverrides, or the same variable you render elsewhere after applying
reanalysis overrides) instead of the raw request; locate the PdfViewerModal JSX
and replace the request prop with that effective request value (ensuring that
the effectiveRequest is computed/derived earlier in the component using the same
override logic as the UI).

In `@apps/dashboard/src/routes/index.tsx`:
- Around line 143-148: The formatDuration function can produce "0m 60s" because
it uses Math.round for seconds; change the logic in formatDuration to compute
seconds with Math.floor (e.g., secs = Math.floor((ms % 60000) / 1000)) or
otherwise guard against secs === 60 by incrementing mins and setting secs = 0,
so the output never shows "60s" (update the variables ms, mins, secs in function
formatDuration accordingly).

In `@apps/gateway/Gateway.API/appsettings.Development.json`:
- Around line 2-4: Remove the hardcoded credentials from the
"ConnectionStrings": { "authscript": ... } entry and replace the value with a
non-secret placeholder (e.g.,
"Host=localhost;Port=5433;Database=authscript;Username={DB_USER};Password={DB_PASS}")
in appsettings.Development.json, then load the real connection string from a
secure source at runtime (environment variable or user-secrets) by wiring the
IConfiguration to read the environment key used (e.g.,
Configuration["ConnectionStrings:authscript"] or an env var like
AUTH_SCRIPT_CONNECTIONSTRING) and ensure any code that references
ConnectionStrings:authscript uses that configuration value instead of the
committed secret.

In `@apps/gateway/Gateway.API/Gateway.API.csproj`:
- Around line 44-46: Update the HotChocolate.AspNetCore package reference in
Gateway.API.csproj from Version="15.1.0" to Version="15.1.12" to pick up the
latest 15.x patch fixes and improved .NET 10 compatibility; locate the
<PackageReference Include="HotChocolate.AspNetCore" Version="15.1.0" /> entry
and change the Version attribute to "15.1.12", then restore/update NuGet
packages and run the project/tests to verify no regressions.

In `@apps/gateway/Gateway.API/GraphQL/Inputs/UpdatePARequestInput.cs`:
- Around line 1-13: The file currently declares two public types
(UpdatePARequestInput and CriterionInput), violating the
one-public-type-per-file rule; fix by either moving CriterionInput into its own
file as a public sealed record named CriterionInput(bool? Met, string Label,
string? Reason = null) or change its accessibility to non-public (e.g., internal
sealed record CriterionInput(...)) so only UpdatePARequestInput remains public;
update any references/imports accordingly to compile.

In `@apps/intelligence/src/reasoning/evidence_extractor.py`:
- Around line 137-181: The loop in extract_evidence treats any BaseException
returned from asyncio.gather as a recoverable error and converts cancellations
into "UNCLEAR" results; change the handling to let asyncio.CancelledError
propagate by checking for asyncio.CancelledError first and re-raising it (or by
only treating instances of Exception as recoverable), i.e., in the results
processing inside extract_evidence (which handles values from _bounded_evaluate)
detect asyncio.CancelledError and raise it, otherwise log and convert other
exceptions into an EvidenceItem as before.

---

Outside diff comments:
In `@apps/dashboard/src/api/customFetch.ts`:
- Around line 69-107: Update the comment above customFetch to accurately state
that authorization is obtained via getAuthToken (which prefers the
authscript_session value) rather than generically saying “SMART on FHIR session
storage”; mention getAuthToken by name and that it checks authscript_session
first so readers know the priority and source of the token used for setting the
Authorization header in customFetch.

In `@apps/intelligence/src/llm_client.py`:
- Around line 198-216: The generic "except Exception" block in chat_completion
should be removed so unexpected errors are not swallowed; keep the existing
specific handlers for APITimeoutError, RateLimitError and APIError (referencing
CompletionRequest, provider.complete, logger) and allow any other exceptions to
propagate to the caller rather than catching and logging them silently. Ensure
the function still returns None for handled cases (timeout/APIError) and
re-raises RateLimitError as currently implemented.

---

Duplicate comments:
In `@apps/gateway/Gateway.API/GraphQL/Mutations/Mutation.cs`:
- Around line 78-84: The ApprovePARequest and DenyPARequest methods use
expression-bodied (arrow) syntax which violates the "no arrow code" guideline;
change both to regular block-bodied methods matching the surrounding style:
implement ApprovePARequest(string id, [Service] IDataService dataService) {
return dataService.ApprovePA(id); } and DenyPARequest(string id, string reason,
[Service] IDataService dataService) { return dataService.DenyPA(id, reason); }
so callers (and the file) use consistent method bodies while keeping calls to
IDataService.ApprovePA and IDataService.DenyPA unchanged.

In `@apps/gateway/Gateway.API/GraphQL/Queries/Query.cs`:
- Around line 17-51: Convert all GraphQL resolver methods (GetProcedures,
GetMedications, GetProviders, GetPayers, GetDiagnoses, GetPARequests,
GetPARequest, GetPAStats, GetActivity) from expression-bodied to block-bodied
methods and add guard clauses at the top: validate that the injected
IDataService parameter is not null (throw
ArgumentNullException(nameof(dataService)) or return an appropriate default) and
validate the id parameter in GetPARequest (throw ArgumentNullException or return
null if id is null/empty); then call the corresponding IDataService member and
return its result. Ensure each method uses an explicit block body (no =>) and
performs the early-return/guard-first pattern required by the coding guidelines.

In `@apps/gateway/Gateway.API/Services/LiveDataService.cs`:
- Around line 18-84: The members in LiveDataService (e.g., Procedures,
Medications, Providers, Payers, Diagnoses, GetPARequests, GetPARequest,
GetPAStats, GetActivity, CreatePARequest, UpdatePARequest,
ProcessPARequestAsync, SubmitPARequest, AddReviewTime, DeletePARequest,
ApprovePA, DenyPA) use expression-bodied syntax (=>) which violates the "no
arrow code" guideline—replace each expression-bodied property or method with a
block-bodied implementation that simply throws the same NotImplementedException
(preserving the existing message text), e.g. change the expression-bodied
property to a get { throw new
NotImplementedException("LiveDataService.Procedures not yet implemented."); }
and change methods to { throw new
NotImplementedException("LiveDataService.MethodName not yet implemented."); } so
all members use block bodies instead of arrow expressions.

In `@apps/intelligence/src/tests/test_evidence_extractor.py`:
- Around line 78-223: Tests and helper coroutines are missing explicit return
type annotations; add them to satisfy the project's type-hint rule. Update each
async test function (e.g., test_evaluate_criterion_returns_met_evidence_item,
test_evaluate_criterion_parses_not_met,
test_evaluate_criterion_handles_none_response,
test_extract_evidence_calls_criteria_concurrently,
test_extract_evidence_respects_semaphore_limit) to declare a return type of ->
None, and annotate helper async coroutines (slow_llm, counting_llm) with their
concrete return type (e.g., -> str); if you use Coroutine types instead, import
typing symbols (Any, Coroutine) and annotate accordingly. Ensure imports are
added if necessary and keep signatures consistent with their return values so
the module passes the type-annotation rule.

In `@apps/intelligence/src/tests/test_llm_client.py`:
- Around line 7-187: Several test functions and the helper get_provider_thread
are missing return type annotations; update each function definition
(test_get_provider_returns_singleton, test_openai_provider_creates_client_once,
test_chat_completion_uses_provider_singleton,
test_chat_completion_returns_none_on_timeout,
test_chat_completion_raises_on_rate_limit,
test_chat_completion_returns_none_on_api_error, test_get_provider_thread_safe,
and the inner get_provider_thread) to include explicit return types (use -> None
for regular and async functions) so every function has a complete type
annotation per the project guideline.

In `@apps/intelligence/src/tests/test_pdf_parser.py`:
- Around line 10-49: Add explicit type hints: annotate both async test functions
test_parse_pdf_does_not_block_event_loop and
test_parse_pdf_multiple_docs_parallel with a return type of -> None, and add
type annotations to the helper used in the second test (slow_extract) including
its parameter and return type (e.g., bytes -> str) so the functions comply with
the project-wide typing rule; update any local MagicMock definitions if needed
to include types (e.g., mock_extract: MagicMock) and import typing symbols if
not already present.

---

Nitpick comments:
In `@apps/dashboard/src/api/graphqlService.ts`:
- Around line 199-272: Make all interface properties readonly to follow project
TS conventions: update Patient, Procedure, Medication, Criterion, PARequest,
PAStats, and ActivityItem so every field is prefixed with readonly (including
nested object properties like PARequest.patient) and convert arrays to readonly
types (e.g., PARequest.criteria -> ReadonlyArray<Criterion>); preserve
optional/union types (e.g., readyAt?: string | null) but mark them readonly as
well so the shapes are immutable by default.

In `@apps/dashboard/src/components/SubmissionProgressOverlay.tsx`:
- Around line 9-10: The imports from 'lucide-react' are split; consolidate them
into a single import statement that imports Search, Check, CheckCircle2 and
Sparkles together (update the import that currently declares Search, Check,
CheckCircle2 and the separate Sparkles import in SubmissionProgressOverlay.tsx)
so all lucide-react icons are imported in one line.

In `@apps/dashboard/src/lib/patients.ts`:
- Line 22: The ATHENA_TEST_PATIENTS test fixture is mutable but should be
immutable; change its type annotation from Patient[] to a readonly type (e.g.,
export const ATHENA_TEST_PATIENTS: readonly Patient[] = […] or export const
ATHENA_TEST_PATIENTS: ReadonlyArray<Patient> = […] ) so the array cannot be
mutated at runtime and matches the readonly-by-default guideline.

In `@apps/dashboard/src/routes/analysis`.$transactionId.tsx:
- Around line 41-250: The four inline components (ProgressRing, EditableField,
CriteriaItem, CriteriaReasonDialog) should be moved to separate files and given
explicit Props interfaces: create ProgressRing.tsx, EditableField.tsx,
CriteriaItem.tsx, CriteriaReasonDialog.tsx; define and export an interface
(e.g., ProgressRingProps, EditableFieldProps, CriteriaItemProps,
CriteriaReasonDialogProps) describing each prop shape (including types like
number, boolean, string, optional fields and callbacks), convert the anonymous
inline props to those interfaces and use destructured parameters in the
functional components, export them as default (or named) and import them back
into the parent route file; ensure any helper utilities used (displayConfidence,
cn, icon components, createPortal) are imported where needed and update
imports/exports accordingly to satisfy the “one component per file” and
prop-interface guidelines.

In `@apps/dashboard/src/routes/index.tsx`:
- Around line 33-120: Move the inline components into their own files and add
explicit props interfaces: create StatCard.tsx exporting the StatCard component
and define a StatCardProps interface for { label: string; value: string |
number; change?: string; positive?: boolean; icon: React.ElementType }, then
import and use it where needed; similarly create WorkflowPipeline.tsx with a
WorkflowPipelineProps interface if it needs props (or export as a standalone
component) and extract the internal steps array or accept it via props; ensure
each file uses a named functional component (not anonymous) and
default/consistent exports and update any imports in the parent file accordingly
(refer to StatCard and WorkflowPipeline in the diff).

In `@apps/gateway/Gateway.API.Tests/Services/LiveDataServiceTests.cs`:
- Around line 11-22: The test class LiveDataServiceTests should be declared
sealed to follow the "sealed by default" convention; update the class
declaration for LiveDataServiceTests to include the sealed modifier (i.e., make
class LiveDataServiceTests sealed) so the test type follows gateway conventions
and matches other test classes.

In
`@apps/gateway/Gateway.API.Tests/Services/Polling/AthenaPollingServiceTests.cs`:
- Around line 231-263: The test
ExecuteAsync_WithRegisteredPatients_CallsGetActiveAsync is using Task.Delay(100)
which is flaky; replace that timing-based wait with a TaskCompletionSource that
completes when _patientRegistry.GetActiveAsync is invoked. Update the
_patientRegistry.GetActiveAsync(Arg.Any<CancellationToken>()) setup to call
tcs.SetResult(true) (or similar) inside its Returns callback and have the test
await tcs.Task instead of Task.Delay before cancelling the
CancellationTokenSource; keep the rest of the test assertions
(StartAsync/StopAsync and Received() call) unchanged.

In `@apps/gateway/Gateway.API/appsettings.Development.json`:
- Around line 8-10: The Athena:ClientId entry in the Athena configuration block
is environment-specific and should be removed from appsettings.Development.json
and moved to an environment-secret mechanism (user-secrets or environment
variable); update code that reads the client id (where you reference
"Athena:ClientId" via IConfiguration or options binding) to fall back to
IConfiguration["Athena:ClientId"] (or an injected
IOptions<AthenaOptions>.Value.ClientId) so it reads from env/user-secrets at
runtime, and leave the appsettings Athena section as a non-sensitive template
(e.g., keep FhirBaseUrl but not ClientId).

In `@apps/gateway/Gateway.API/Services/DocumentUploader.cs`:
- Line 35: The method signatures (e.g., UploadDocumentAsync) and other places
using the fully qualified type Gateway.API.Contracts.Result<string> should be
simplified to Result<string> since Gateway.API.Contracts is already imported;
update all occurrences (including the other Result<string> uses in the same
class) to the shorter form, or if there is a naming collision with another
Result type, add a using alias (e.g., using ContractsResult =
Gateway.API.Contracts.Result<string>) and use that alias in the method
signatures and return statements to keep intent clear.

In `@apps/gateway/Gateway.API/Services/Fhir/FhirHttpClient.cs`:
- Line 33: The method signatures in FhirHttpClient use redundant fully qualified
types like Gateway.API.Contracts.Result<T>; update each async method signature
(e.g., ReadAsync and the other async methods in the class) to use the imported
namespace type name (Result<T>) instead of Gateway.API.Contracts.Result<T> for
JsonElement, string, etc., and do the same for their return types and any
related local variable/type references so the code relies on the using import
and removes the visual noise.

In `@apps/intelligence/src/api/analyze.py`:
- Around line 100-103: The code currently reads all documents into
pdf_bytes_list then calls document_texts = list(await
asyncio.gather(*[parse_pdf(b) for b in pdf_bytes_list])), which keeps every PDF
in memory; change to bounded concurrency so bytes are released sooner: iterate
documents and create tasks that call parse_pdf on each stream/read chunk but
gate concurrent parse_pdf calls with an asyncio.Semaphore (or process in small
batches) so you only hold a few pdf_bytes at once; reference pdf_bytes_list,
parse_pdf, documents, and document_texts and ensure each read/parse task
releases its bytes when done so memory usage is bounded.

Comment on lines +33 to +36
"graphql": "^16.9.0",
"graphql-request": "^7.0.0",
"html2canvas": "^1.4.1",
"jspdf": "^4.1.0",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

graphql 16.9.0 security advisories vulnerabilities

💡 Result:

For the npm package graphql (GraphQL.js) version 16.9.0, there are no known direct vulnerabilities/security advisories specifically affecting 16.9.0 in common public trackers. Snyk reports “No direct vulnerabilities” for graphql@16.9.0. [1]

The main widely-referenced security issue for GraphQL.js in the 16.x line is:

  • CVE-2023-26144 / SNYK-JS-GRAPHQL-5905181 (DoS via query validation) — affects graphql >=16.3.0 and <16.8.1; fixed in 16.8.1, so 16.9.0 is not affected. [2][3]

Also, GitHub’s security page for graphql/graphql-js indicates there are no published GitHub Security Advisories in that repository. [4]

Bottom line: if you’re on graphql@16.9.0, you’re already past the known affected range for CVE-2023-26144, and there are no other commonly listed advisories targeting 16.9.0 in these sources. [1][2][3]

Sources: [1] Snyk package page for graphql@16.9.0 • [2] NVD CVE-2023-26144 • [3] Snyk advisory SNYK-JS-GRAPHQL-5905181 • [4] GitHub graphql-js security overview.


🌐 Web query:

graphql-request 7.0.0 latest version security

💡 Result:

  • Is graphql-request@7.0.0 the latest? No — the latest published version is 7.4.0. [1][2][3]
  • Known security issues in 7.x? Snyk reports no known direct vulnerabilities for graphql-request (latest/non-vulnerable: 7.4.0). [1]
  • What to do: Upgrade to 7.4.0 (and still run npm audit/your SCA scanner to catch transitive dependency issues). [1]

Sources: Snyk [1], Socket versions [2], jsDocs package metadata [3].


🌐 Web query:

html2canvas 1.4.1 security advisories

💡 Result:

  • GitHub Security Advisories (niklasvh/html2canvas): No published security advisories are listed for the upstream repository. [1]
  • Snyk (npm html2canvas): Reports no known direct vulnerabilities for html2canvas, including v1.4.1 (listed as latest/non-vulnerable). [2], [3]
  • ReversingLabs (npm html2canvas): Reports no known vulnerabilities detected for the package. [4], [5]
  • Meterian: Flags html2canvas@1.4.1 as “affected by 1 vulnerability” (severity shown as “Suggest”), but the public page does not provide details/IDs. [6]

Note: There are reported malicious-package advisories for similarly named packages like feedback-html2canvas, which is not the same as html2canvas. [7], [8]

Sources: [1]-[8] correspond to the citations above.


🌐 Web query:

jspdf 4.1.0 latest version security

💡 Result:

  • Latest jsPDF version: v4.1.0 (tagged Feb 2, 2026) and marked “Latest” on the project’s GitHub releases page. [1]

  • Security status of 4.1.0: the v4.1.0 release explicitly states it “fixes several security issues”, including:

    • PDF Injection in AcroForm (arbitrary JavaScript execution in generated PDFs)
    • Stored XMP metadata injection
    • Shared-state race condition in addJS
    • DoS via unvalidated BMP dimensions (ties to CVE-2026-24133, fixed in 4.1.0) [1][2]
  • Important prior issue (fixed earlier, but relevant if you’re on older versions):

    • Local File Inclusion / Path Traversal in Node.js builds (GHSA-f8cm-6447-x5h2 / CVE-2025-68428) is fixed in v4.0.0 by restricting filesystem access by default. [3][4]

Recommendation: Upgrade to jsPDF 4.1.0 (or newer, if available later) to pick up the latest security fixes. If you use jsPDF on Node.js, follow the project guidance to enforce filesystem restrictions with Node’s permission flags. [4]

Sources: [1] [2] [3] [4]


Upgrade graphql-request to 7.4.0 before release.

The pinned versions are largely secure: graphql@16.9.0 (no vulnerabilities; past CVE-2023-26144 range), html2canvas@1.4.1 (no direct issues), and jsPDF@4.1.0 (latest, with recent security fixes for PDF injection and DoS). However, graphql-request@7.0.0 is outdated—upgrade to 7.4.0 (patch-level compatible) to stay current.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/package.json` around lines 33 - 36, The package.json entry for
the dependency "graphql-request" is pinned to "^7.0.0" and needs to be updated
to "^7.4.0"; open apps/dashboard/package.json, locate the "graphql-request"
dependency line and bump the version string to 7.4.0 (keeping the existing caret
if desired), then run yarn/npm install and update lockfile to ensure the new
version is recorded.

Comment on lines +7 to +20
export interface Patient {
id: string;
/** Athena Patient ID (numeric) */
patientId: string;
/** FHIR R4 Logical ID */
fhirId: string;
name: string;
mrn: string;
dob: string;
memberId: string;
payer: string;
address: string;
phone: string;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find all Patient interface definitions
rg -n --type=ts "interface Patient|type Patient" apps/dashboard/src/

Repository: lvlup-sw/authscript

Length of output: 392


🏁 Script executed:

# Check the graphqlService.ts file specifically for the Patient interface
cat -n apps/dashboard/src/api/graphqlService.ts | sed -n '190,220p'

Repository: lvlup-sw/authscript

Length of output: 859


🏁 Script executed:

# Check the patients.ts file to see the full context
cat -n apps/dashboard/src/lib/patients.ts | head -30

Repository: lvlup-sw/authscript

Length of output: 943


🏁 Script executed:

# Search for imports of Patient across the codebase
rg -n --type=ts "import.*Patient.*from" apps/dashboard/src/

Repository: lvlup-sw/authscript

Length of output: 179


🏁 Script executed:

# See how Patient type is used in assignments and type annotations
rg -n --type=ts ": Patient|Patient\s*\[|as Patient" apps/dashboard/src/ | head -50

Repository: lvlup-sw/authscript

Length of output: 514


Apply readonly modifier to exported test patient array per coding guidelines.

The ATHENA_TEST_PATIENTS constant should be readonly by default. Change the type annotation to readonly Patient[] or use as const to align with TypeScript standards.

Additionally, there is a second Patient interface defined in apps/dashboard/src/api/graphqlService.ts with a different shape (missing patientId and fhirId). While the current imports are well-separated and there's no active type confusion, consider renaming this interface to AthenaTestPatient to improve clarity and prevent accidental misuse in the future, especially as the codebase grows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/lib/patients.ts` around lines 7 - 20, The exported
ATHENA_TEST_PATIENTS array should be typed as readonly to follow coding
guidelines: update its annotation to readonly Patient[] (or use as const) so the
test data is immutable and TypeScript enforces it; also to avoid future type
collisions, rename the other Patient interface in graphqlService.ts to
AthenaTestPatient (update its usages/imports accordingly) so the distinct shapes
are clear and not accidentally mixed up.

Comment on lines +365 to +418
export async function generatePAPdfBlob(request: PARequest): Promise<Blob> {
const html = generatePAPdf(request);
const printWindow = window.open('', '_blank');

if (printWindow) {
printWindow.document.write(html);
printWindow.document.close();

// Wait for content to load then trigger print
printWindow.onload = () => {
setTimeout(() => {
printWindow.print();
}, 250);
};
const doc = new DOMParser().parseFromString(html, 'text/html');
const styles = doc.head.innerHTML.replace(/\bbody\b/g, `#${PDF_CONTAINER_ID}`);
const bodyContent = doc.body.innerHTML;

const container = document.createElement('div');
container.id = PDF_CONTAINER_ID;
Object.assign(container.style, {
position: 'fixed',
left: '-9999px',
top: '0',
width: '210mm',
minHeight: '297mm',
background: 'white',
overflow: 'visible',
});
container.innerHTML = `<style>${styles}</style>${bodyContent}`;
document.body.appendChild(container);

try {
await new Promise((r) => setTimeout(r, 100));

const canvas = await html2canvas(container, {
scale: 2,
useCORS: true,
logging: false,
backgroundColor: '#ffffff',
});

const pdf = new jsPDF({ format: 'a4', unit: 'mm' });
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const imgWidth = pageWidth;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
const imgData = canvas.toDataURL('image/png');

let heightLeft = imgHeight;
let position = 0;
let page = 0;

pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;

while (heightLeft > 0) {
page++;
pdf.addPage();
position = -pageHeight * page;
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}

return pdf.output('blob');
} finally {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid injecting <title> into the generated <style> block.
doc.head.innerHTML includes <title>, and wrapping it inside <style> produces invalid CSS, which can drop styling in the PDF. Extract only the CSS text from style tags.

🐛 Proposed fix
-  const styles = doc.head.innerHTML.replace(/\bbody\b/g, `#${PDF_CONTAINER_ID}`);
+  const styles = Array.from(doc.querySelectorAll('style'))
+    .map((style) => style.innerHTML)
+    .join('\n')
+    .replace(/\bbody\b/g, `#${PDF_CONTAINER_ID}`);
📝 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
export async function generatePAPdfBlob(request: PARequest): Promise<Blob> {
const html = generatePAPdf(request);
const printWindow = window.open('', '_blank');
if (printWindow) {
printWindow.document.write(html);
printWindow.document.close();
// Wait for content to load then trigger print
printWindow.onload = () => {
setTimeout(() => {
printWindow.print();
}, 250);
};
const doc = new DOMParser().parseFromString(html, 'text/html');
const styles = doc.head.innerHTML.replace(/\bbody\b/g, `#${PDF_CONTAINER_ID}`);
const bodyContent = doc.body.innerHTML;
const container = document.createElement('div');
container.id = PDF_CONTAINER_ID;
Object.assign(container.style, {
position: 'fixed',
left: '-9999px',
top: '0',
width: '210mm',
minHeight: '297mm',
background: 'white',
overflow: 'visible',
});
container.innerHTML = `<style>${styles}</style>${bodyContent}`;
document.body.appendChild(container);
try {
await new Promise((r) => setTimeout(r, 100));
const canvas = await html2canvas(container, {
scale: 2,
useCORS: true,
logging: false,
backgroundColor: '#ffffff',
});
const pdf = new jsPDF({ format: 'a4', unit: 'mm' });
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const imgWidth = pageWidth;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
const imgData = canvas.toDataURL('image/png');
let heightLeft = imgHeight;
let position = 0;
let page = 0;
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
while (heightLeft > 0) {
page++;
pdf.addPage();
position = -pageHeight * page;
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
return pdf.output('blob');
} finally {
export async function generatePAPdfBlob(request: PARequest): Promise<Blob> {
const html = generatePAPdf(request);
const doc = new DOMParser().parseFromString(html, 'text/html');
const styles = Array.from(doc.querySelectorAll('style'))
.map((style) => style.innerHTML)
.join('\n')
.replace(/\bbody\b/g, `#${PDF_CONTAINER_ID}`);
const bodyContent = doc.body.innerHTML;
const container = document.createElement('div');
container.id = PDF_CONTAINER_ID;
Object.assign(container.style, {
position: 'fixed',
left: '-9999px',
top: '0',
width: '210mm',
minHeight: '297mm',
background: 'white',
overflow: 'visible',
});
container.innerHTML = `<style>${styles}</style>${bodyContent}`;
document.body.appendChild(container);
try {
await new Promise((r) => setTimeout(r, 100));
const canvas = await html2canvas(container, {
scale: 2,
useCORS: true,
logging: false,
backgroundColor: '#ffffff',
});
const pdf = new jsPDF({ format: 'a4', unit: 'mm' });
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const imgWidth = pageWidth;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
const imgData = canvas.toDataURL('image/png');
let heightLeft = imgHeight;
let position = 0;
let page = 0;
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
while (heightLeft > 0) {
page++;
pdf.addPage();
position = -pageHeight * page;
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
return pdf.output('blob');
} finally {
🧰 Tools
🪛 ast-grep (0.40.5)

[warning] 381-381: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: container.innerHTML = <style>${styles}</style>${bodyContent}
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)


[warning] 381-381: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: container.innerHTML = <style>${styles}</style>${bodyContent}
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/lib/pdfGenerator.ts` around lines 365 - 418, The current
generatePAPdfBlob function builds CSS by using doc.head.innerHTML which can
include non-CSS nodes like <title>, causing invalid CSS in the injected <style>;
change the logic that computes styles (currently using
doc.head.innerHTML.replace(...)) to instead collect only CSS text from the
document's <style> elements (e.g. iterate doc.querySelectorAll('head style'),
join their .textContent, then perform the body selector replacement like
.replace(/\bbody\b/g, `#${PDF_CONTAINER_ID}`)). Keep the rest of the DOM
insertion (container.id = PDF_CONTAINER_ID, container.innerHTML) the same so
only valid CSS is injected and other head nodes (title/meta) aren’t wrapped into
the <style>.

Comment on lines +926 to +940
{/* PDF Viewer Modal */}
<PdfViewerModal
isOpen={showPdfModal}
onClose={() => setShowPdfModal(false)}
request={request}
/>

{/* Criteria Reason Dialog */}
<CriteriaReasonDialog
isOpen={!!selectedCriterion}
onClose={() => setSelectedCriterion(null)}
met={selectedCriterion?.met ?? null}
label={selectedCriterion?.label ?? ''}
reason={selectedCriterion?.reason ?? ''}
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

PDF modal should use the same effective request shown in the UI.
When reanalysis overrides criteria/confidence, the modal still receives request, which can make the PDF diverge from what the user just reviewed.

🐛 Suggested fix
-      <PdfViewerModal
+      <PdfViewerModal
         isOpen={showPdfModal}
         onClose={() => setShowPdfModal(false)}
-        request={request}
+        request={effectiveRequest}
       />
📝 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
{/* PDF Viewer Modal */}
<PdfViewerModal
isOpen={showPdfModal}
onClose={() => setShowPdfModal(false)}
request={request}
/>
{/* Criteria Reason Dialog */}
<CriteriaReasonDialog
isOpen={!!selectedCriterion}
onClose={() => setSelectedCriterion(null)}
met={selectedCriterion?.met ?? null}
label={selectedCriterion?.label ?? ''}
reason={selectedCriterion?.reason ?? ''}
/>
{/* PDF Viewer Modal */}
<PdfViewerModal
isOpen={showPdfModal}
onClose={() => setShowPdfModal(false)}
request={effectiveRequest}
/>
{/* Criteria Reason Dialog */}
<CriteriaReasonDialog
isOpen={!!selectedCriterion}
onClose={() => setSelectedCriterion(null)}
met={selectedCriterion?.met ?? null}
label={selectedCriterion?.label ?? ''}
reason={selectedCriterion?.reason ?? ''}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/routes/analysis`.$transactionId.tsx around lines 926 -
940, The PDF modal is receiving the original request variable (prop request) so
it can show outdated data when reanalysis overrides criteria/confidence; update
the code so PdfViewerModal receives the effective request used by the UI (e.g.,
a computed value like effectiveRequest, requestWithOverrides, or the same
variable you render elsewhere after applying reanalysis overrides) instead of
the raw request; locate the PdfViewerModal JSX and replace the request prop with
that effective request value (ensuring that the effectiveRequest is
computed/derived earlier in the component using the same override logic as the
UI).

Comment on lines +143 to +148
function formatDuration(ms: number): string {
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
const mins = Math.floor(ms / 60000);
const secs = Math.round((ms % 60000) / 1000);
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid rounding seconds to 60 in formatDuration.
Math.round can yield 60s (e.g., 59.9s), producing 0m 60s. Flooring avoids this display glitch.

🐛 Suggested fix
-  const secs = Math.round((ms % 60000) / 1000);
+  const secs = Math.floor((ms % 60000) / 1000);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/routes/index.tsx` around lines 143 - 148, The
formatDuration function can produce "0m 60s" because it uses Math.round for
seconds; change the logic in formatDuration to compute seconds with Math.floor
(e.g., secs = Math.floor((ms % 60000) / 1000)) or otherwise guard against secs
=== 60 by incrementing mins and setting secs = 0, so the output never shows
"60s" (update the variables ms, mins, secs in function formatDuration
accordingly).

Comment on lines +2 to +4
"ConnectionStrings": {
"authscript": "Host=localhost;Port=5433;Database=authscript;Username=postgres;Password=postgres"
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid committing DB credentials in appsettings (even dev).

Hardcoded username/password in source control increases leak risk and can be reused outside local dev. Prefer user-secrets or environment variables for ConnectionStrings:authscript, and keep only a placeholder in the repo.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/Gateway.API/appsettings.Development.json` around lines 2 - 4,
Remove the hardcoded credentials from the "ConnectionStrings": { "authscript":
... } entry and replace the value with a non-secret placeholder (e.g.,
"Host=localhost;Port=5433;Database=authscript;Username={DB_USER};Password={DB_PASS}")
in appsettings.Development.json, then load the real connection string from a
secure source at runtime (environment variable or user-secrets) by wiring the
IConfiguration to read the environment key used (e.g.,
Configuration["ConnectionStrings:authscript"] or an env var like
AUTH_SCRIPT_CONNECTIONSTRING) and ensure any code that references
ConnectionStrings:authscript uses that configuration value instead of the
committed secret.

Comment on lines +44 to +46

<!-- GraphQL -->
<PackageReference Include="HotChocolate.AspNetCore" Version="15.1.0" />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify HotChocolate.AspNetCore 15.1.0 on NuGet

echo "=== Checking HotChocolate.AspNetCore 15.1.0 on NuGet ==="
curl -s "https://api.nuget.org/v3-flatcontainer/hotchocolate.aspnetcore/15.1.0/hotchocolate.aspnetcore.nuspec" | head -100

echo ""
echo "=== Listing recent HotChocolate.AspNetCore versions ==="
curl -s "https://api.nuget.org/v3-flatcontainer/hotchocolate.aspnetcore/index.json" | jq -r '.versions | .[-15:]' 2>/dev/null || echo "Failed to fetch version list"

Repository: lvlup-sw/authscript

Length of output: 3228


🌐 Web query:

HotChocolate.AspNetCore 15.1.0 .NET 10 compatibility

💡 Result:

HotChocolate.AspNetCore 15.1.0 does not target net10.0 directly—it ships with net8.0 and net9.0 TFMs only (included in the package). However, NuGet marks net10.0 as “computed” compatible, meaning a .NET 10 app can reference it via normal .NET compatibility rules. [1]

That said, ChilliCream’s stated support for Hot Chocolate 15 is .NET 8 and .NET 9 (not .NET 10), so .NET 10 usage is “should work” compatibility rather than an explicitly supported/targeted TFM for v15. [2]

Sources: [1] NuGet package page (HotChocolate.AspNetCore 15.1.0 frameworks/compatibility), [2] ChilliCream “What’s new for Hot Chocolate 15” (supported .NET versions)


🌐 Web query:

HotChocolate.AspNetCore version 15.1.0 NuGet package security

💡 Result:

As of Feb 18, 2026, there are no published security advisories on GitHub for ChilliCream/graphql-platform (the upstream repo for Hot Chocolate), i.e., GitHub shows “There aren’t any published security advisories”. [1]

That said, HotChocolate.AspNetCore 15.1.0 is not current (NuGet shows it was last updated Mar 19, 2025). [2] The latest stable on NuGet is 15.1.12 (last updated Jan 15, 2026), and there are also newer 16.0.0 prerelease builds. [3]

Practical guidance

  • If you’re using 15.1.0, prefer upgrading to ≥ 15.1.12 unless you have a compatibility reason not to. [3]
  • Verify in your own solution with: dotnet list package --vulnerable (uses known vulnerability feeds for your full dependency graph).

Sources
[1] GitHub – Security overview for ChilliCream/graphql-platform (no published advisories) (github.com)
[2] NuGet – HotChocolate.AspNetCore 15.1.0 (published/last updated Mar 19, 2025) (nuget.org)
[3] NuGet – HotChocolate.AspNetCore package page (current stable 15.1.12; prerelease versions listed) (nuget.org)

Citations:


Consider upgrading to HotChocolate.AspNetCore 15.1.12.

Version 15.1.0 exists on NuGet and will function on .NET 10 through computed compatibility. However, the package explicitly targets only .NET 8 and 9, and ChilliCream's official support for Hot Chocolate 15 does not include .NET 10. A newer patch version (15.1.12, released Jan 15, 2026) is available and recommended for optimal compatibility and latest bug fixes. No security advisories are reported for this package.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/Gateway.API/Gateway.API.csproj` around lines 44 - 46, Update the
HotChocolate.AspNetCore package reference in Gateway.API.csproj from
Version="15.1.0" to Version="15.1.12" to pick up the latest 15.x patch fixes and
improved .NET 10 compatibility; locate the <PackageReference
Include="HotChocolate.AspNetCore" Version="15.1.0" /> entry and change the
Version attribute to "15.1.12", then restore/update NuGet packages and run the
project/tests to verify no regressions.

Comment on lines +1 to +13
namespace Gateway.API.GraphQL.Inputs;

public sealed record UpdatePARequestInput(
string Id,
string? Diagnosis,
string? DiagnosisCode,
string? ServiceDate,
string? PlaceOfService,
string? ClinicalSummary,
IReadOnlyList<CriterionInput>? Criteria
);

public sealed record CriterionInput(bool? Met, string Label, string? Reason = null);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Split CriterionInput into its own file (or make it non-public).
This file declares two public types, which violates the one-public-type-per-file rule. As per coding guidelines, “SOLID: One public type per file, Strategy over switches, constructor injection.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/Gateway.API/GraphQL/Inputs/UpdatePARequestInput.cs` around lines
1 - 13, The file currently declares two public types (UpdatePARequestInput and
CriterionInput), violating the one-public-type-per-file rule; fix by either
moving CriterionInput into its own file as a public sealed record named
CriterionInput(bool? Met, string Label, string? Reason = null) or change its
accessibility to non-public (e.g., internal sealed record CriterionInput(...))
so only UpdatePARequestInput remains public; update any references/imports
accordingly to compile.

Comment on lines +137 to 181
async def extract_evidence(
clinical_bundle: ClinicalBundle,
policy: dict[str, Any],
) -> list[EvidenceItem]:
"""
Extract evidence from clinical bundle using LLM to evaluate policy criteria.

Evaluates criteria concurrently with a configurable concurrency limit.

Args:
clinical_bundle: FHIR clinical data bundle
policy: Policy definition with criteria

Returns:
List of evidence items, one per policy criterion
"""
criteria = policy.get("criteria", [])
if not criteria:
return []

clinical_summary = _build_clinical_summary(clinical_bundle)
semaphore = _get_llm_semaphore()

results = await asyncio.gather(
*[_bounded_evaluate(c, clinical_summary, semaphore) for c in criteria],
return_exceptions=True,
)

evidence_items: list[EvidenceItem] = []
for i, result in enumerate(results):
if isinstance(result, BaseException):
criterion_id = criteria[i].get("id", "unknown")
logger.error("Criterion %s evaluation failed: %s", criterion_id, result)
evidence_items.append(
EvidenceItem(
criterion_id=criterion_id,
status="UNCLEAR",
evidence=f"Evaluation error: {result}",
source="LLM analysis",
confidence=0.0,
)
)
)
else:
evidence_items.append(result)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "evidence_extractor.py" -path "*/intelligence/*"

Repository: lvlup-sw/authscript

Length of output: 118


🏁 Script executed:

cd apps/intelligence 2>/dev/null && find . -name "evidence_extractor.py" || find . -name "evidence_extractor.py" -type f

Repository: lvlup-sw/authscript

Length of output: 100


🏁 Script executed:

find . -type f -name "evidence_extractor.py"

Repository: lvlup-sw/authscript

Length of output: 118


🏁 Script executed:

sed -n '137,181p' ./apps/intelligence/src/reasoning/evidence_extractor.py

Repository: lvlup-sw/authscript

Length of output: 1527


🏁 Script executed:

head -50 ./apps/intelligence/src/reasoning/evidence_extractor.py

Repository: lvlup-sw/authscript

Length of output: 1373


🏁 Script executed:

grep -n "import asyncio\|from asyncio\|CancelledError" ./apps/intelligence/src/reasoning/evidence_extractor.py

Repository: lvlup-sw/authscript

Length of output: 79


🏁 Script executed:

python3 << 'EOF'
import asyncio

# Verify CancelledError hierarchy
print("asyncio.CancelledError bases:", asyncio.CancelledError.__bases__)
print("Is CancelledError an Exception?", issubclass(asyncio.CancelledError, Exception))
print("Is CancelledError a BaseException?", issubclass(asyncio.CancelledError, BaseException))

# In Python 3.8+, CancelledError is Exception, but we should still handle it specially
# as per asyncio best practices for gather with return_exceptions=True
EOF

Repository: lvlup-sw/authscript

Length of output: 197


🏁 Script executed:

grep -A 5 -B 5 "return_exceptions" ./apps/intelligence/src/reasoning/evidence_extractor.py

Repository: lvlup-sw/authscript

Length of output: 450


🏁 Script executed:

cat -n ./apps/intelligence/src/reasoning/evidence_extractor.py | sed -n '155,175p'

Repository: lvlup-sw/authscript

Length of output: 1014


🏁 Script executed:

python3 << 'EOF'
import asyncio
import sys

# Test scenario: what happens with return_exceptions=True and CancelledError
print(f"Python version: {sys.version}")
print()
print("Key facts about asyncio.CancelledError:")
print("- In Python 3.8+, CancelledError inherits directly from BaseException")
print("- With gather(return_exceptions=True), ALL exceptions are returned in results list")
print("- Including CancelledError")
print()
print("The concern:")
print("- isinstance(result, BaseException) will match CancelledError")
print("- Converting it to an UNCLEAR status prevents cancellation propagation")
print("- This breaks task cancellation semantics")
print()
print("The fix:")
print("- Check for asyncio.CancelledError first and re-raise")
print("- Use isinstance(result, Exception) instead of BaseException")
print("- This lets CancelledError propagate while handling other errors")
EOF

Repository: lvlup-sw/authscript

Length of output: 728


Re-raise asyncio.CancelledError to preserve cancellation semantics.

With return_exceptions=True, asyncio.gather() returns exceptions as values in the results list, including asyncio.CancelledError. The current code treats all BaseException instances as recoverable errors, converting cancellations into "UNCLEAR" items. This prevents request cancellations from propagating properly.

Check for asyncio.CancelledError first and re-raise it, or use isinstance(result, Exception) instead of BaseException to let cancellations propagate correctly:

Suggested fix
    for i, result in enumerate(results):
+       if isinstance(result, asyncio.CancelledError):
+           raise result
-       if isinstance(result, BaseException):
+       if isinstance(result, Exception):
            criterion_id = criteria[i].get("id", "unknown")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/intelligence/src/reasoning/evidence_extractor.py` around lines 137 -
181, The loop in extract_evidence treats any BaseException returned from
asyncio.gather as a recoverable error and converts cancellations into "UNCLEAR"
results; change the handling to let asyncio.CancelledError propagate by checking
for asyncio.CancelledError first and re-raising it (or by only treating
instances of Exception as recoverable), i.e., in the results processing inside
extract_evidence (which handles values from _bounded_evaluate) detect
asyncio.CancelledError and raise it, otherwise log and convert other exceptions
into an EvidenceItem as before.

rsalus and others added 6 commits February 20, 2026 23:01
- Add dashboard as ViteApp resource in AppHost, wired to gateway/intelligence
- Swap deprecated Aspire.Hosting.NodeJs for Aspire.Hosting.JavaScript 13.1.0
- Route GraphQL requests through Vite proxy in dev to avoid cross-origin issues
- Relax Node engine constraint to >=20.0.0 (remove upper bound)
- Remove platform-specific optionalDependencies; regenerate lock file with
  cross-platform native binding entries
- Add NVM symlink note for Aspire compatibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Restore approve/deny mutations and connection status hook in graphqlService
- Fix graphql-request mock type cast in test
- Fix ruff lint: line length in config.py, import sorting and long lines in
  test_evidence_extractor.py
- Sync generated schemas to resolve drift

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use explicit default= keyword in Field() for mypy compatibility
- Add type parameters to dict annotation in OpenAIProvider

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@rsalus rsalus merged commit ac6a747 into main Feb 21, 2026
6 checks passed
@github-project-automation github-project-automation bot moved this from Todo to Done in Authscript Demo Feb 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants