Repository navigation
tech-debt(slm-frontend): 114 hand-declared types still shadow a generated OpenAPI schema (from #12420) #13138
Description
Activity
Measurement correction: 94 remain, not 114
The scan described in the issue over-counts.
^\s*(?:export\s+)?(interface|type)\s+(\w+)also matches the members of a type-only import:import { useMfaApi, type MFASetupResponse, // <- counted as a declaration } from '@/composables/useMfaApi'
Requiring a real declaration body (
interface X … {ortype X … =) gives, on pristineDev_new_gui(5a4c3a4ae):generated schemas: 309 hand-declared type/interface names: 414 exact-name collisions: 129 already derived by #13137: 35 (39 minus the 4 with no response_model) still hand-written: 94Most of the entries the issue lists as third declarations —
MFASetupResponseinSecuritySettings.vue,SSOProviderResponseinSSOSettings.vue,SecretCreateinSecretsSettings.vue, etc. — are alreadyimport typeof the derived composable alias, i.e. already correct. The genuine multi-declaration cases areRoleInfo(3),LogEntry(3),PlaybookInfo(2),ThreatSummary(3) and the eight/security/*shapes (2 each).Not every collision is drift — 4 need renaming, not derivation
Name Declared at Why deriving it would be wrong DashboardOverviewcomposables/usePrometheusMetrics.ts:97A client-side view-model built by remapping /monitoring/dashboard(usePrometheusMetrics.ts:224-241). The endpoint's real response model isDashboardOverview(autobot-slm-backend/api/monitoring.py:704) and is separately declared attypes/api-responses.ts:287.SystemMetricscomposables/usePrometheusMetrics.ts:26Same — synthesised from fleet_metrics. The schema belongs to/health/metrics(autobot-slm-backend/api/health.py:59).LogEntryviews/monitoring/LogViewer.vue:23View-model; LogViewer.vue:113-127explicitly mapsseverity→level.LogEntrycomponents/DeploymentLogViewer.vue:21A WebSocket message envelope ( type/log_type,timestamp: Date), not an HTTP response at all.These four should be renamed so the collision disappears. Deriving them would replace a working local shape with an unrelated wire shape.
Genuine disagreement found: the credential write paths (filed as #13145)
NodeUpdate,NodeCreateandConnectionTestRequestdeclare fields the contract does not have. Confirmed against backend source: the surplus fields are silently dropped by Pydantic's defaultextra="ignore", the request 200s, and the UI reports success. Worst case: the Edit-Node modal's "Deploy PKI certificates" and "Re-run enrollment tasks" checkboxes are no-ops, and validation forces an SSH password to enable them.Which side is wrong is a product decision, so #13145 records it with
file:lineevidence rather than changing the server contract. These three are blocked on #13145 and cannot be derived until it is resolved — deriving them turns the surplus fields into compile errors whose only silent fix is deleting UI controls.Delivered: PR #13146 — 18 shapes, security/auth surface (94 -> 77)
TokenResponse+MfaChallengeResponse(stores/auth.ts), the eight/security/*shapes (collapsed from two identical declarations to one derivation), and the nine VNC/TLS credential shapes. Drift corrected along the way:TokenResponsemissingtoken,TLSCredentialResponsemissingca_cert/server_cert, both*CredentialCreatemissingextra_data, an unguarded.lengthon the optionalSecurityOverviewResponse.recent_events, and/api/auth/loginmodelled as a flattened blob instead of its declared union.Remaining 77, grouped by risk for whoever picks this up
- Blocked on bug(slm): Edit Node silently discards all SSH/PKI/enrollment fields — NodeUpdate accepts 5 of 14; ssh_key dropped on register and connection-test too #13145 —
NodeCreate,NodeUpdate,ConnectionTestRequest. - Rename, do not derive —
DashboardOverview,SystemMetrics,LogEntry×2 (table above). - Shape-changing —
useCodeSync.ts(10),useOrchestration.ts(5),useRoles.ts(4),useOrchestrationManagement.ts(2). Mostly literal-unionstatusfields that the contract widens tostring, plusFleetSyncJobStatusmissingfailure_reasonandUpdateAllJobmissingskipped_fleet_nodes. Note the widening direction: the hand-written unions are narrower than the contract, so plain derivation loses a real UI-side guarantee — check whether the backend actually constrains the value before widening. - Long tail —
types/api-responses.tsmonitoring/observability read models (~30) and the remainingtypes/slm.tsshapes (~20: NPU, maintenance windows, services, external agents, roles).
RoleInfo(3 declarations,types/slm.ts:223+DeploymentWizard.vue:12+SetupWizardView.vue:594) is the highest-value item left in group 4: all three disagree with each other and with the contract, andtypes/slm.ts:223typesname/category/dependenciesas the localNodeRole/RoleCategoryunions where the contract saysstring.- Blocked on bug(slm): Edit Node silently discards all SSH/PKI/enrollment fields — NodeUpdate accepts 5 of 14; ssh_key dropped on register and connection-test too #13145 —
- added a commit that references this issue
on Jul 31, 2026 Closed by PR #13146, merged to
Dev_new_guiasc0a45b77a. 18 shapes derived; the shadowed-type count drops 94 → 77. #13138 stays open for the remainder.The count in #13138 was wrong, and so is its measurement method
94, not 114. The scan regex
^\s*(?:export\s+)?(interface|type)\s+(\w+)also matches members of a type-only import:import { useMfaApi, type MFASetupResponse } from '@/composables/useMfaApi'
So most entries listed as "declared in three places" (
MFASetupResponseinSecuritySettings.vue,SSOProviderResponseinSSOSettings.vue,SecretCreateinSecretsSettings.vue) were already correct — they areimport typeof the derived alias. Requiring a real declaration body on pristineDev_new_gui: 309 schemas, 414 hand-declared names, 129 collisions, 35 already derived by #13137 → 94 remaining. The method needs fixing in #13138, or every future pass re-derives the same wrong number.Four entries must NOT be derived
DashboardOverviewandSystemMetrics(usePrometheusMetrics.ts:97,:26) andLogEntry(LogViewer.vue:23,DeploymentLogViewer.vue:21) are local view-models that deliberately remap the very endpoint whose schema shares their name —LogViewer.vue:113-127mapsseverity→level,usePrometheusMetrics.ts:224-241reshapes the response. Deriving them would be actively harmful; they need renaming instead. A blanket "derive everything" sweep would have broken all four.Frontend-side mismatches found and fixed
SecurityOverviewResponse.recent_eventsisdefault_factory=list(schemas.py:1601) → optional in the contract, butSecurityView.vue:643read.lengthunguarded.TLSEndpointResponse.days_until_expiryis optional and nullable;formatExpiryStatustested only=== null, so an absent value fell through to the "expired" branch./api/auth/loginreturns the unionTokenResponse | MfaChallengeResponse(api/auth.py:81), modelled as one flattened all-optional blob — producing two!assertions on the live login path. Now a real union, with 4 new tests.TokenResponsewas missingtoken;TLSCredentialResponsemissingca_cert/server_cert; both*CredentialCreatemissingextra_data.
A duplicate nothing used
The eight
/security/*shapes were declared identically twice —types/slm.ts:706-821andtypes/api-responses.ts:538-653— and theapi-responses.tscopy had zero importers. Every consumer used theslm.tscopy. So had the two diverged, nothing would have surfaced it. Now one definition plus a re-export.Inline
{ credentials, total }returns replaced with the realVNCCredentialListResponse/TLSCredentialListResponse.Deferred with reasons
Credential write paths (
NodeCreate,NodeUpdate,ConnectionTestRequest) are blocked on #13145 — a genuine server-side disagreement where the server is wrong, and which side to change is a product decision. Envelope/enum-widening groups (useCodeSync×10,useOrchestration×5,useRoles×4,useOrchestrationManagement×2) and the ~50-type monitoring long tail remain in #13138.Verification
type-checkclean both sides; vitest 207 → 211 (+4 login-union tests);lintfinding set byte-identical at 18 lines — all measured against acp-swapped pristinesrc/fromgit archive, nevergit stash.git diff --exit-code src/types/generated/api.tsclean, soverify-generated-types-slmholds. No AutoBot application code was run (#13090). Rebased onto163933b75mid-work after another session pushed #13144, then re-verified.- added a commit that references this issue
on Jul 31, 2026 Slice 3 delivered: PR #13152, merged as
66279b115— 23 shapes, the envelope / enum-widening group (76 → 53)Closes the discrete slice issue #13155. #13138 stays open with 53 remaining.
The count, a third time — and the rule that settles it
Measured on
c0a45b77a(post-#13146): 76 still hand-written, not 77.generated schemas: 309 hand-declared names: 414 exact-name collisions: 131 already fully derived: 55 still hand-written: 76The one-unit gap against the previous comment is
ThreatSummary, and it is a methodology gap rather than an arithmetic one. It is derived attypes/slm.ts:727, butSecurityView.vue:43re-declares it locally. Counting per name puts it in the hand-written bucket; counting per declaration puts it in the derived one.So the scan needs a third rule on top of the two already recorded (require a declaration body; exclude
import { type X }):A name counts as remaining if any of its declarations lacks a
components['schemas'][…]body — a body-requiring regex still matchestype X = components['schemas']['X'], so derived aliases must be excluded explicitly or the collision count never falls.With all three rules the number is reproducible and moves monotonically. On the merged tip: 309 schemas, 419 hand-declared names (up 5 — the new non-colliding status unions), 131 collisions, 78 derived, 53 remaining.
Defect found: a partial update-all reports no outcome at all
The pattern held again — the disagreement was the finding, and the server was right.
UpdateAllJob.statuswas declared'pending' | 'running' | 'completed' | 'failed' | 'already_current'. Since #11511 the backend has a sixth terminal status:# autobot-slm-backend/api/code_sync.py:4940 job.status = "partial" if skipped else "completed"
set whenever the fleet stage skips a non-operational node.
'partial'matched neither banner — failure testsstatus === 'failed'(CodeSyncView.vue:913), success tests'completed' | 'already_current'(:921). The pipeline stopped, the CTA re-armed, and the user got no outcome whatsoever. The backend's own explanation —"Updated 2/3 nodes (1 skipped — not operational)"(code_sync.py:4933-4935) — goes intostage.message, which the template never renders anywhere, so it had no route to the screen either.skipped_fleet_nodes(code_sync.py:4270) was absent from the frontend type entirely, so there was nothing to count even in a hand-written fix. Deriving the type restored the field and immediately broke both test fixtures that build anUpdateAllJob— they had been mocking the truncated shape, the same failure mode as #13137's scope picker. Confirmed the 4 new tests fail against the pre-fix view before fixing it.Second instance of the same class:
UpdateAllStage.statusomitted'current'(_StageStatus.CURRENT,code_sync.py:4245) even thoughCodeSyncView.vue:127and:145already map it — the type contradicted the view's own rendering, andcurrentwas silently displayed as "done".The widening direction, resolved per type
Every
statushere isstrserver-side with the value set living only in a trailing#comment. Blanket derivation would have discarded a real guarantee; blanket retention would have preserved two provably false claims. Resolved against the backend's assignment sites instead:Case Types Resolution Unconstrained but exhaustively enumerable PostSyncAction.category(4 construction sites),FleetSyncNodeStatus.status,FleetSyncJobStatus.status,ComponentSyncJobStatus.statusderive the shape, keep the union, cite the sites Constrained in a way the schema cannot express ServiceCategoryUpdate.category(pattern="^(autobot|system)$")derive, keep the union — anything else is a guaranteed 422 Frontend union simply wrong UpdateAllJob.status,UpdateAllStage.statusthe defect above Fields the frontend type could not reach
FleetSyncJobStatus.failure_reason(schemas.py:1795, populated on every failure atcode_sync.py:401-402);DriftResolveResponse.deps_changed/post_steps(schemas.py:1689-1690);PortInfo.address(schemas.py:106, GH#11224's bind interface). AlsoPendingNodeResponse.current_versionis optional and nullable, butformatVersion(CodeSyncView.vue:248) narrowed its parameter tostring | null— narrower than thegetCommitHashDisplayit delegates to.Two traps for whoever takes the long tail
- The generated types are intersected with an
additionalPropertiesindex signature, sokeyof Tisstring | numberandOmit/Picksilently collapse every named member into the index signature —Omit<ServiceActionRequest, 'force'>.node_idtypes asunknown, with no error at the definition site.Partialis homomorphic and preserves them; plain intersection narrows a widenedstringcorrectly. - openapi-typescript emits a field with a server-side default as
required— right for a response, backwards for a request body.ServiceActionRequest.forceandBulkActionRequest.excludeare optional to send.
Also:
FleetStatusResponse.servicesandBulkActionResponse.resultsare baredictserver-side (api/orchestration.py:79,:97), so the contract can only say{ [key: string]: unknown }while every consumer reads.status/.host/.port. Derived for the scalar fields and intersected to pin the element shape — the long tail intypes/api-responses.tswill hit this shape repeatedly.No new view-models in this group
Re-checked all 23 against the #13146 "rename, do not derive" list. Every one is a straight wire model; none remaps its endpoint. The four known view-models (
DashboardOverview,SystemMetrics,LogEntry×2) are untouched and still need renaming.Remaining 53
- Blocked on bug(slm): Edit Node silently discards all SSH/PKI/enrollment fields — NodeUpdate accepts 5 of 14; ssh_key dropped on register and connection-test too #13145 —
NodeCreate,NodeUpdate,ConnectionTestRequest. Unchanged. - Rename, do not derive —
DashboardOverview,SystemMetrics,LogEntry×2. - Long tail —
types/api-responses.tsmonitoring/observability read models (~30, expect the bare-dictnarrowing question repeatedly) and the rest oftypes/slm.ts(~20: NPU, maintenance windows, services, external agents).RoleInfo(3 declarations) is still the highest-value item.
Recorded, not acted on
stage.messageis never rendered by the update-all template even though the backend writes a useful per-stage summary into it on every stage, andgetJobStatus/getRecentJobsare exported fromuseCodeSyncwith no consumer outside tests — sofailure_reasonhas no route to the screen regardless of the type. Both left alone to keep the diff to contract derivation; say the word and I will file them.Note for future slices: the SLM admin app bootstraps a single
enlocale (src/i18n/index.ts:13;src/locales/contains onlyen.json) — it is not the 11-locale main frontend.- The generated types are intersected with an
Slice 4 open: PR #13159 — 50 shapes, the monitoring long tail (53 → 4)
#13138stays open with 4 remaining, 3 of which are blocked on #13145.The count, a fourth time — the three rules hold
Measured on
66279b115(post-#13152) with the rules recorded in the previous comments (require a declaration body · excludeimport { type X }· a name counts as remaining if any of its declarations lacks acomponents['schemas'][…]body):generated schemas: 309 hand-declared names: 422 exact-name collisions: 132 already derived: 78 still hand-written: 5454, one above slice 3's 53. The unit is
ThreatSummary:SecurityView.vue:43istype ThreatSummary = ThreatSummaryType, an alias of the derived import, which the strict per-name rule counts as remaining while it is correct in substance. So the rules reproduce and the residual gap is a known artifact, not arithmetic. On the branch tip: 309 schemas, 428 names, 132 collisions, 128 derived, 4 remaining.The pattern held: four defects, the server right every time
1. A blue-green deployment in post-deploy monitoring reported nothing at all.
BlueGreenStatus(types/slm.ts) waspending | borrowing | deploying | verifying | switching | active | rolling_back | rolled_back | completed | failed. The backend has an eleventh state:# autobot-slm-backend/services/blue_green.py:780 deployment.status = BlueGreenStatus.MONITORING.value deployment.monitoring_started_at = datetime.now(timezone.utc)
set after the traffic switch whenever
post_deploy_monitor_duration > 0 and auto_rollback(:766), and held for that many seconds — 1800 by default.'monitoring'matched none of the fourbgStatsbuckets (DeploymentsView.vue:202-205) and fell throughgetStatusClassto the neutral gray default, so for up to half an hour a deployment under active health-watch was absent from the Total-adjacent Active/Completed/Failed/Rolled-back tiles and its badge asserted that nothing was happening.Worse, the fields the health-watch actually reports —
health_failures(written every poll,blue_green.py:1055),health_failure_threshold,monitoring_started_at,post_deploy_monitor_duration— were absent from the frontend type entirely, so there was nothing to render even in a hand-written fix. DerivingBlueGreenResponserestored all four. Confirmed the 4 new tests fail against the pre-fix view (expected '0' to be '1'; badge class absent).2.
NodeRolewas missing'docker'.constants/node-roles.tsstates, in its own header, "Source of truth:autobot-slm-backend/services/role_registry.py:DEFAULT_ROLES" and "Keep in sync". The registry carries 21 roles; the union carried 20.docker(_INFRA_ROLES,role_registry.py:372) has always been returned byGET /deployments/rolesand has always rendered with no label and no description. Correcting the union madeRecord<NodeRole, RoleMetadata>demand the missing metadata entry — the mirror now type-checks itself.3.
PlaybookInfowas declared identically twice —InfrastructureWizard.vue:19andInfrastructureView.vue:20. Same class as the/security/*duplicate #13146 found. Both omittedtags, and typedcategoryas a barestringwhere the contract has thePlaybookCategoryenum.4. The blue-green shapes were declared twice as well, in
types/slm.tsandtypes/api-responses.ts, andDeploymentsView.vue:335bridged the two withresponse.deployments as BlueGreenDeployment[]— a cast that would have swallowed any divergence between them. One derivation plus aliases now; the cast is gone.Two more view-models — and one that looked like one but was not
The four already on the "rename, do not derive" list (
DashboardOverview,SystemMetrics,LogEntry×2) are joined by a fifth found in this group:Name Declared at Why deriving it would be wrong RoleInfoviews/SetupWizardView.vue:594Declares a display_namethe contract does not have.loadRoles(:818-832) builds it fromdescription, because the backend already folds the registry'sdisplay_nameintodescription(api/deployments.py:157-160).All five are now renamed —
LogRow,DeploymentLogMessage,SystemMetricsViewModel,DashboardViewModel,WizardRoleOption— each carrying a comment naming the schema it used to collide with, so the collision cannot silently return.RoleInfoatDeploymentWizard.vue:12looked like a sixth but is a straight projection, not a remap. It keeps a local four-field type because the offlineNODE_ROLE_METADATAfallback cannot supplyansible_roleorrequired— a fullRoleInfothere would be a lie — but the member types are now derived by indexed access, so a contract rename still breaks the build. Itsdependenciesis normalised at the fetch site: the field isdefault_factory=listand the template reads.lengthunguarded.Widening, resolved per type
Case Types Resolution Constrained in a way the schema cannot express RestartAllServicesRequest.category(pattern="^(autobot|system|all)$",schemas.py:900),NodeService.categoryderive, keep the union Unconstrained but exhaustively enumerable ErrorStatistics.trend(3 returns,errors.py:245-273),ErrorHealthResponse.status(3 branches,:509-518),BlueGreenStatus,NPULoadBalancingConfig.strategyderive, keep the union, cite the sites Frontend-side guarantee NPUWorkerConfig.failure_action— sole construction site is the four-option<select>atNPUDetailsPanel.vue:383-386derive, keep the union Frontend claim unverifiable NPUCapabilities.deviceType— copied verbatim out of an external worker's/healthpayload (api/npu.py:83), and all three renderers already fall back to the raw stringwidened Frontend claim describes a field never populated RoleInfo.dependencies—deployments.py:166passes[]unconditionallywidened Fields the frontend type could not reach
RoleInfo.ansible_role·NodeService.endpoint_path/port/protocol/is_discoverable·BlueGreenCreate.health_failure_threshold/post_deploy_monitor_duration(so the New Blue-Green modal could never configure post-deploy monitoring at all) ·PlaybookInfo.tags· plus the long tail of optional-and-nullable timestamps and gauges declared as required, of which two had live consequences:temperature_celsiustested only=== nullso an absent reading renderedundefined°Cin the "cool" colour, andNPUWorkerConfig.assigned_modelsisdefault_factory=listyettoggleModelindexed it unguarded.A third trap for whoever takes the remainder
The two recorded in #13152 both held. A new one, and it is the quiet kind:
A[] & B[]keeps bothmapsignatures and TypeScript resolves the callback against the first, sobase & { items: Narrow[] }types the callback parameter as the wide element, with no error at the definition site. That is exactly howuseNodeServices.ts:52ended up mappingcategoryasstring. Writing{ items: Narrow[] } & basepicks the narrowed element.Every list envelope in this slice uses that order, and the rule is recorded on
RoleListResponseintypes/slm.ts.Remaining 4
- Blocked on bug(slm): Edit Node silently discards all SSH/PKI/enrollment fields — NodeUpdate accepts 5 of 14; ssh_key dropped on register and connection-test too #13145 —
NodeCreate,NodeUpdate,ConnectionTestRequest. Unchanged: a genuine server-side disagreement whose resolution is a product decision. ThreatSummary(SecurityView.vue:43) — an alias of the derived import, inside a block of five such aliases. Correct in substance; renaming it churns the file to satisfy a counting rule.
That is the end of the derivable surface. Once #13145 lands, #13138 closes.
Verification
type-checkclean; vitest 215 → 219 (+4 blue-green tests, confirmed failing against the pre-fix view);lintfinding set identical at 16 problems / 0 errors — all measured against acp-swapped pristinesrc/fromgit archive, nevergit stash.git diff --exit-code src/types/generated/api.tsclean, soverify-generated-types-slmholds. Rebased ontoa1fc379d7mid-work after another session pushed #13153, then re-verified. No AutoBot application code was run (#13090).- Blocked on bug(slm): Edit Node silently discards all SSH/PKI/enrollment fields — NodeUpdate accepts 5 of 14; ssh_key dropped on register and connection-test too #13145 —
- added 4 commits that reference this issue
on Jul 31, 2026 - addedarea: api-contractWave 2 · cluster C — Frontend↔backend API contractWave 2 · cluster C — Frontend↔backend API contract
on Sep 1, 2026 - added a commit that references this issue
on Sep 2, 2026
114 hand-declared SLM frontend types still shadow a generated OpenAPI schema
#13075 stood up the SLM frontend's
openapi-typescriptpipeline and #13137 (from #12420) wired 39 of the generated schemas into the seven composables already routed through the canonicalslmApiClient. This issue tracks the remainder.Measurement
Reproduce with a name scan: collect every
export interface X/export type Xunderautobot-slm-frontend/src(excludingsrc/types/generated/), collect every key of theschemasblock insrc/types/generated/api.ts, and intersect.At the time of filing (post-#13137):
A name collision is a strong drift signal, not proof — but every pair checked so far had real drift (see below).
Where they cluster
autobot-slm-frontend/src/types/api-responses.tsAlertItem:259,AlertsResponse:270,AppLogEntry:314,AppLogsResponse:320,DashboardOverview:286,ErrorStatistics:406,MetricsSummary:469,TLSCredentialResponse:135,TLSEndpointsResponse:160,VNCCredentialResponse:76,VNCEndpointsResponse:105,TimelineResponse:486,TopErrorsResponse:501(~45 names)autobot-slm-frontend/src/types/slm.tsNodeCreate:129,NodeUpdate:145,RoleInfo:223,RoleListResponse:237,ServiceListResponse:448,SecurityEventResponse:705,SecurityPolicyResponse:792,AuditLogResponse:743,NPUWorkerConfig:641,MaintenanceWindowCreate:369(~30 names)autobot-slm-frontend/src/composables/useCodeSync.tsPendingNodesResponse:55,FleetSyncResponse:68,FleetSyncJobStatus:85,DriftedFile:165,FileDriftReport:171,DriftResolveResponse:182,UpdateAllStage:214,UpdateAllJob:225autobot-slm-frontend/src/composables/useRoles.tsPortInfo:40,PostSyncAction:85,NodeActionsResponse:93,DecommissionRoleInfo:108autobot-slm-frontend/src/composables/useOrchestration.tsServiceActionRequest:38,ServiceMigrateRequest:43,FleetStatusResponse:64,BulkActionRequest:71,BulkActionResponse:84autobot-slm-frontend/src/composables/usePrometheusMetrics.tsSystemMetrics:25,DashboardOverview:96Several names are declared in three places at once (
RoleInfointypes/slm.ts:223,DeploymentWizard.vue:11andSetupWizardView.vue:593;LogEntryintypes/api-responses.ts:294,LogViewer.vue:22andDeploymentLogViewer.vue:20), so one backend change has three independent places to drift from.Why it matters — drift already found
Every hand/generated pair inspected during #13137 had at least one mismatch, and two were user-visible defects (both fixed there):
Lower-severity drift found in the same pass and corrected by derivation:
SecretResponsewas missingwarning;SSOProviderHealthResponsewas missingsecret_staleness;SSOProviderUpdatewas missingis_socialandorg_id, so the UI could set fields on create that it could never edit; plus a long tail of "declared non-null, actually optional" timestamps.Definition of done
components['schemas'][...]or carries a comment stating why it cannot be (endpoint has noresponse_model)..vue-local copies import it.npm run type-checkgreen, SLM vitest suite green, and any drift the derivation exposes is fixed rather than cast away.src/types/generated/api.ts—verify-generated-types-slmdiffs it.Suggested batching
useCodeSync.ts->useRoles.ts/useOrchestration.ts->types/slm.ts->types/api-responses.ts(largest, mostly monitoring/observability read models).Parent: #12420 (umbrella #12645). Do not merge the SLM client with the main frontend's — ADR-008 keeps one client per (app, backend) pair.