Project: doctolib-doctolib · Branch: .invalid · Mode: ` · **Model:**claude-sonnet-4-6`
Stats: 1 user messages, 16 tool calls, tools used: Read, Bash.
Conversation
Turn 1 — User
Review the frontend architecture compliance of PR #241133 (FHIR-2224) from the doctolib/doctolib monorepo.
Worktree path: /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/
Changed files (TypeScript packages under packages/@doctolib/):
1. packages/@doctolib/dmp/src/types/DmpApi.ts
2. packages/@doctolib/dmp/src/business/DmpApi.ts
3. packages/@doctolib/dmp/src/index.ts
4. packages/@doctolib/national-health-records/src/business/adapters/DmpNhrAdapter.ts
5. packages/@doctolib/dmp/src/business/DmpApi.test.ts
6. packages/@doctolib/national-health-records/src/business/adapters/DmpNhrAdapter.test.ts
Architecture concerns to evaluate:
-
Package API surface:
DocumentStatusUpdateResultis exported fromindex.ts. Is the export surface appropriate? Are there any types exposed that should remain internal? -
Cross-package dependency:
DmpNhrAdapter.tsin@doctolib/national-health-recordsimportsDmpApiErrorfrom@doctolib/dmp. Is this import through the proper public API (viaindex.ts)? Does importing an Error class from a sibling package violate any separation of concerns? -
Mock inlining anti-pattern in tests:
DmpNhrAdapter.test.tsinlines a manual replica ofDmpApiErrorconstructor insidejest.mock('@doctolib/dmp')to avoid loading the module graph. The comment says "Inlined to avoid loading the full @doctolib/dmp module graph (which pulls @doctolib/tpf-consent)". Is this an UpFront architecture violation? Should there be a lighter test export or a factory to avoid this? -
Naming conventions:
DocumentStatusUpdateResultvsSearchResult— are these naming patterns consistent with Doctolib UpFront conventions? -
Error handling architecture: The PR creates an asymmetry —
updateDocumentStatuscan both return structured errors AND throw (for non-BusinessError cases and missing session). Is mixing return-value errors with exceptions consistent with the adapter pattern in@doctolib/national-health-records? -
File organization: Types are in
src/types/DmpApi.ts, implementation insrc/business/DmpApi.ts, error class insrc/api/DmpApiError.ts. Is this organization consistent with UpFront package structure conventions?
Read the actual files at the worktree path and report findings in Conventional Comments format with file:line references.
Turn 1 — Assistant
I'll read all the relevant files to perform a thorough architecture review.
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/types/DmpApi.ts
Tool results:
→ result (ok): 1 import type { Patient } from 'monolith_DEPRECATED/types/doctor/api/Patient'
2 import type { ClinicalDocument } from 'engines/cdar2/types/ClinicalDocument'
3 import type { DmpDocumentToAdd, DmpDocumentToDelete } from '../business/FrDmpConfig'
4 import type { DmpWorkflow, BillingSituation, GetDocumentsContentParams, PracticeLocationOverride } from './FrDmpTypes'
5
6 export type PatientConsent = 'error' | 'authorized' | 'expired' | 'forbidden' | 'noAuthorization'
7
8 export type PatientConsentU
…(truncated, 5382 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/business/DmpApi.ts
Tool results:
→ result (ok): 1 / eslint-disable @typescript-eslint/no-non-null-assertion / 2 import type { Patient } from 'monolith_DEPRECATED/types/doctor/api/Patient' 3 import type { MedicalFolder } from '@doctolib/ehr-core' 4 import { getLoincCode } from 'monolith_DEPRECATED/models/document/document' 5 import { sendMonitoringEvent } from 'monolith_DEPRECATED/services/monitoring' 6 import { isMinor } from 'monolith_DEPRECATED/models/patient/patient' 7 import { isMedicalSynthesis } from 'monolith_DEPRECATED/components/do …(truncated, 38564 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/index.ts
Tool results:
→ result (ok): 1 export {
2 type DmpDocumentItem,
3 type DmpDocumentToAdd,
4 type DmpDocumentToDelete,
5 type FrDmpDocument,
6 ItemType,
7 type UploadedDmpDocumentItem,
8 } from './business/FrDmpConfig'
9 export { getErrorMessage, getWarningMessage } from './business/DmpI18nMessage'
10 export type {
11 DmpApi,
12 DmpAuthor,
13 DmpDocument,
14 DmpDocumentContent,
15 DmpSetUpProps,
16 ShareProps,
17 UnshareProps,
18 SearchProps,
19 SearchResult,
20 DocumentStatusUpdateProps,
21
…(truncated, 5772 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/national-health-records/src/business/adapters/DmpNhrAdapter.ts
Tool results:
→ result (ok): 1 import type { 2 DmpApi, 3 DmpAuthor, 4 DmpDocument, 5 DmpDocumentContent, 6 SearchProps, 7 SearchResult as DmpSearchResult, 8 } from '@doctolib/dmp' 9 import { DmpApiError, DOCUMENT_STATUS, DOCUMENT_VISIBILITY, getPatientInsForDMP } from '@doctolib/dmp' 10 import { isUnstructured } from '@doctolib/cda/types/UnstructuredBody' 11 import { isStructured } from '@doctolib/cda/types/StructuredBody' 12 import type { Patient } from 'monolith_DEPRECATED/types/doctor/api/Patient' 13 import t …(truncated, 9948 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/business/DmpApi.test.ts
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/national-health-records/src/business/adapters/DmpNhrAdapter.test.ts
Tool results:
→ result (ok): 1 import { getPatientInsForDMP } from '@doctolib/dmp'
2 import type { DmpApi, DmpDocumentContent, SearchResult as DmpSearchResult } from '@doctolib/dmp'
3 import type { NhrDocumentSearchProps } from '@doctolib/tpf-national-health-records'
4 import type { DocumentCategorizationConcept, DocumentCategorizationFacet } from '@doctolib/documents-ui/types/Document'
5 import { DmpNhrAdapter } from './DmpNhrAdapter'
6 import { findFacetForCDAType } from '../utils/findFacetForCDAType'
7
8 jest.mock('../u
…(truncated, 26876 more chars)
Tool results:
→ result (ok): 1 import type { Patient } from 'monolith_DEPRECATED/types/doctor/api/Patient' 2 import { ConsultationDocumentKind } from 'monolith_DEPRECATED/types/doctor/api/ConsultationDocument' 3 import { getLoincCode } from 'monolith_DEPRECATED/models/document/document' 4 import { blobToBase64 } from 'monolith_DEPRECATED/shared/BlobHelper' 5 import { FeatureSwitchMocker } from '@doctolib/variation_management' 6 import { DmpAction, BusinessError } from '@doctolib/tpf-french-teleservices' 7 import { parse as …(truncated, 85045 more chars)
Now let me look at the DmpApiError class and the package structure:
Tools used:
Bash:find /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/api -type f | sort
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/api/auth-types.ts /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/api/connectToDmp.test.ts /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/api/connectToDmp.ts /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/api/DmpApiError.test.ts /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packa …(truncated, 40 more chars)
Tools used:
Bash:find /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src -type f | sort
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/api/auth-types.ts /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/api/connectToDmp.test.ts /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/api/connectToDmp.ts /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/api/DmpApiError.test.ts /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packa …(truncated, 6863 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/api/DmpApiError.ts
Tool results:
→ result (ok): 1 import type { DmpApiErrorInfo } from '../types/DmpApi'
2
3 export class DmpApiError extends Error {
4 readonly apiErrorCode: string | undefined
5
6 readonly apiErrorType: string | undefined
7
8 readonly apiErrorContext: string | undefined
9
10 readonly apiErrorDescription: string | undefined
11
12 readonly apiErrorExtendedInformations: string | undefined
13
14 readonly apiErrorTypeDescription: string | undefined
15
16 readonly status: string
17
18 constructor(info: DmpA
…(truncated, 730 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/test-index.ts
Tool results:
→ result (ok): 1 export { mockFetchFrDmpDocument } from './data/frDmpDocuments.mock' 2
Tools used:
Bash:find /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/national-health-records/src -type f | sort
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/national-health-records/src/business/adapters/DmpNhrAdapter.test.ts /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/national-health-records/src/business/adapters/DmpNhrAdapter.ts /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/national-health-records/src/business/adapters/parseHl7DateToIso.test.ts /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/ …(truncated, 5537 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/national-health-records/src/business/adapters/DmpNhrAdapter.ts(1–10)
Tool results:
→ result (ok): 1 import type { 2 DmpApi, 3 DmpAuthor, 4 DmpDocument, 5 DmpDocumentContent, 6 SearchProps, 7 SearchResult as DmpSearchResult, 8 } from '@doctolib/dmp' 9 import { DmpApiError, DOCUMENT_STATUS, DOCUMENT_VISIBILITY, getPatientInsForDMP } from '@doctolib/dmp' 10 import { isUnstructured } from '@doctolib/cda/types/UnstructuredBody'
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/api/DmpApiError.test.ts
Tool results:
→ result (ok): 1 import { DmpApiError } from './DmpApiError'
2 import type { DmpApiErrorInfo } from '../types/DmpApi'
3
4 describe('DmpApiError', () => {
5 describe('message assembly', () => {
6 it('combines description and extended info when both are present', () => {
7 const info: DmpApiErrorInfo = {
8 apiErrorDescription: 'Technical software error',
9 apiErrorExtendedInformations: 'Operation timed out.',
10 status: 'ERROR',
11 }
12 const error = new DmpApiErr
…(truncated, 3083 more chars)
Tools used:
Bash:cd /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224 && git log --oneline -5 -- packages/@doctolib/dmp/src/api/DmpApiError.ts packages/@doctolib/dmp/src/types/DmpApi.ts packages/@doctol …(truncated, 159 more chars)
Tool results:
→ result (ERROR): Permission to use Bash has been denied. IMPORTANT: You may attempt to accomplish this action using other tools that might naturally be used to accomplish this goal, e.g. using head instead of cat. But you should not attempt to work around this denial in malicious ways, e.g. do not use your ability to run tests to execute non-test actions. You should only try to work around this restriction in reasonable ways that do not attempt to bypass the intent behind this denial. If you believe this cap …(truncated, 178 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/business/DmpApi.ts(1–5)
Tool results:
→ result (ok): 1 / eslint-disable @typescript-eslint/no-non-null-assertion / 2 import type { Patient } from 'monolith_DEPRECATED/types/doctor/api/Patient' 3 import type { MedicalFolder } from '@doctolib/ehr-core' 4 import { getLoincCode } from 'monolith_DEPRECATED/models/document/document' 5 import { sendMonitoringEvent } from 'monolith_DEPRECATED/services/monitoring'
Tools used:
Read:/Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/national-health-records/src/index.ts
Tool results:
→ result (ok): 1 export { useNationalHealthRecords, type UseNhrOperations } from './hooks/useNationalHealthRecords' 2 export { useDmpNhr } from './hooks/useDmpNhr' 3 export { useNhrDocumentListVariant } from './variants/useNhrDocumentListVariant' 4 export type { DocumentSearchOptions, NhrDocumentListVariant } from './variants/types' 5 export { actionsBuilder, type NhrActionsBuilder } from './business/builders/actionsBuilder' 6 export { useGetDocumentsContent } from './hooks/useGetDocumentsContent' 7
I now have everything I need for a complete review. Here is my analysis.
Architecture Review: PR #241133 (FHIR-2224)
Critical Issues (must fix)
[issue] DmpApiError placed in src/api/ — wrong layer for a domain value object
File: /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/api/DmpApiError.ts
DmpApiError is a pure class that wraps DmpApiErrorInfo — no network call, no session, no side-effect. It belongs in src/business/ alongside the domain types it enriches, not in src/api/. The src/api/ directory in this package contains connection-level concerns (connectToDmp.ts, auth-types.ts). Placing an error value object there breaks the intended layer semantics.
The coupling is observable: DmpApiError.ts imports from ../types/DmpApi (correct direction for a business artifact), but its own location makes it appear to be part of the connection layer. The test at src/api/DmpApiError.test.ts is correctly colocated with its source, but it will need to move when the source moves.
Recommended placement: /packages/@doctolib/dmp/src/business/DmpApiError.ts (and update the index.ts re-export path accordingly).
[issue] DmpNhrAdapter.test.ts inlines a manual replica of DmpApiError — creates a dual-maintenance contract
File: /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/national-health-records/src/business/adapters/DmpNhrAdapter.test.ts, lines 23–33
// Inlined to avoid loading the full @doctolib/dmp module graph (which pulls @doctolib/tpf-consent)
DmpApiError: class DmpApiError extends Error {
constructor(info: { apiErrorDescription?: string; apiErrorExtendedInformations?: string; status: string }) {
...
}
},
The comment honestly explains the motivation, but the solution is wrong. This is a classic case where the test infrastructure itself reveals an architectural problem: DmpApiError has no runtime dependencies — it extends Error and wraps a plain-data type. Its constructor signature and message-assembly logic are now duplicated across two packages. Any change to the real DmpApiError (adding a new field, changing fallback text) will silently diverge from this replica.
The root cause is that DmpApiError lives inside the same module graph as @doctolib/tpf-consent. The correct fix is to move DmpApiError (and potentially DmpApiErrorInfo) to a separate, dependency-free entry point or to a dedicated tiny package. The @doctolib/dmp package already has a src/test-index.ts pattern; a src/errors.ts subpath export would solve this without any duplication.
A minimal fix without restructuring the package: expose DmpApiError from a dedicated subpath export in package.json (e.g. @doctolib/dmp/errors) that does not transitively import tpf-consent. That subpath can export DmpApiError and DmpApiErrorInfo alone. The test mock can then import from that subpath rather than inlining a replica.
Warnings (should fix)
[warning] DmpApiImpl exported from src/index.ts — concrete class leaked from public API
File: /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/index.ts, line 23
export { DmpApiImpl } from './business/DmpApi'
DmpApiImpl is the concrete implementation of the DmpApi interface. Exporting the implementation class through index.ts invites consumers to depend on the concrete type rather than the interface, bypassing the interface abstraction entirely. The public surface of @doctolib/dmp should only expose the DmpApi interface and the createDmpApi factory function (which is already not exported here — that is a separate gap). If downstream packages need the class (e.g. for instanceof checks in tests), that is a test-concern and belongs in the test-index, not the production API surface.
createDmpApi is defined in src/business/DmpApi.ts (line 1031) but is not exported from index.ts. The factory is the correct public entry point; the class is not.
[warning] src/business/DmpApi.ts re-exports types it only imports — unnecessary barrel layer
File: /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/business/DmpApi.ts, lines 102–117
export type {
DmpApi,
DmpSetUpProps,
...
DocumentStatusUpdateResult,
} from '../types/DmpApi'
src/business/DmpApi.ts imports these types for its own use and then re-exports them so src/index.ts can pick them up transitively. This is a barrel anti-pattern: index.ts should import directly from src/types/DmpApi.ts, as it already partially does (line 84 imports DmpApiErrorInfo directly from ./types/DmpApi). The re-export in DmpApi.ts adds a superfluous indirection that makes it harder to trace where types actually live.
[warning] updateDocumentStatus uses a mixed error model inconsistent with search
Files:
- /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/business/DmpApi.ts, lines 346–400
- /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/types/DmpApi.ts, line 186–188
search returns SearchResult which has an optional error?: DmpApiErrorInfo field. updateDocumentStatus returns DocumentStatusUpdateResult with the same optional error field. So far consistent. However, updateDocumentStatus also throws new Error('No DMP session found') when the session is absent, while search does the same (throws, line 681). The inconsistency is actually between the tpf path and the iCanopée path for updateDocumentStatus:
- tpf path (lines 349–367):
BusinessErroris caught and returned as{ error: ... }— soft result. - iCanopée path (lines 369–399): missing session throws; non-OK status returns structured error.
This means the same method can throw (missing session, non-BusinessError) or return a structured error depending on path and failure type. The DmpNhrAdapter handles this correctly by wrapping the entire call in a try/catch (line 219 in DmpNhrAdapter.ts), but the asymmetry is a leaky design: callers must simultaneously handle both the return type and thrown exceptions for the same operation.
The UpFront adapter pattern (as seen in searchDocuments) expects adapters to translate provider errors into NhrResult-style returns. The current structure forces DmpNhrAdapter to be a partial error-normalizer rather than a clean translator. This should be resolved by making updateDocumentStatus return a structured result for all failure modes, or by documenting the throw contract explicitly in the DmpApi interface.
[warning] DocumentStatusUpdateResult is exported from index.ts but its symmetric type SearchResult is also exported — verify intentionality
File: /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/index.ts, lines 19–22
Both SearchResult and DocumentStatusUpdateResult are now part of the public API surface. This is correct if consumers outside @doctolib/dmp need to type variables holding the return value of those methods. The DmpNhrAdapter is the current sole consumer and it imports SearchResult as DmpSearchResult already. Verify that the export of DocumentStatusUpdateResult was intentional and that no internal type has been accidentally leaked. DmpApiErrorInfo is also now explicitly exported at line 84 — confirm this is intentional since it was previously internal.
Suggestions (consider improving)
[suggestion] src/api/ mixes two very different concerns
File: /Users/USER/workspace/doctolib/worktrees/doctolib/FHIR-2224/packages/@doctolib/dmp/src/api/
The src/api/ directory currently contains: the CPS card connection layer (connectToDmp.ts, auth-types.ts) and now also an error value object (DmpApiError.ts). These are conceptually unrelated. Even if DmpApiError is not moved to
…(truncated, 2442 more chars)