@quazardous/qdadm 2.4.3 → 2.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,7 +33,6 @@ import { useConfirm } from 'primevue/useconfirm'
33
33
  import { useHooks } from './useHooks.js'
34
34
  import { useEntityItemPage, type ParentConfig, type UseEntityItemPageReturn } from './useEntityItemPage.js'
35
35
  import { useActiveStack } from '../chain/useActiveStack.js'
36
- import { FilterQuery } from '../query/FilterQuery'
37
36
  import { useI18n } from '../i18n/useI18n'
38
37
  import { formatDateOnly } from '../utils/formatters'
39
38
 
@@ -85,17 +84,15 @@ export type {
85
84
  // Stateless utilities (cookies, session storage, formatters, constants).
86
85
  import {
87
86
  PAGE_SIZE_OPTIONS,
88
- SMART_FILTER_THRESHOLD,
89
- clearSessionFilters,
90
87
  getSavedPageSize,
91
88
  getSessionFilters,
92
89
  getSessionSort,
93
90
  setSessionSort,
94
91
  persistPageSize,
95
- setSessionFilters,
96
- snakeToTitle,
97
92
  } from './useListPage.utils'
98
93
  import { useActionRegistry } from './useActionRegistry'
94
+ import { useListFilters } from './useListPage.filters'
95
+ import { useListAlterHooks } from './useListPage.alterHooks'
99
96
  import { useOrchestrator } from '../orchestrator/useOrchestrator.js'
100
97
  import { createOrchestratorToast } from './useOrchestratorToast'
101
98
 
@@ -514,323 +511,45 @@ export function useListPage<T = unknown>(config: UseListPageOptions<T>): UseList
514
511
 
515
512
  const cards = computed(() => Array.from(cardsMap.value.values()))
516
513
 
517
- // ============ FILTERS ============
518
- const filtersMap = ref<Map<string, FilterConfig>>(new Map())
519
- const filterValues = ref<Record<string, unknown>>(savedFilters || {})
520
-
521
- function addFilter(name: string, filterConfig: Omit<FilterConfig, 'name'>): void {
522
- filtersMap.value.set(name, {
523
- name,
524
- type: 'select',
525
- placeholder: name,
526
- ...filterConfig,
527
- })
528
- if (filterValues.value[name] === undefined) {
529
- filterValues.value[name] = filterConfig.default ?? null
530
- }
531
- }
532
-
533
- function removeFilter(name: string): void {
534
- filtersMap.value.delete(name)
535
- delete filterValues.value[name]
536
- }
537
-
538
- function setFilterValue(name: string, value: unknown): void {
539
- filterValues.value = { ...filterValues.value, [name]: value }
540
- }
541
-
542
- function updateFilters(newValues: Record<string, unknown>): void {
543
- filterValues.value = { ...filterValues.value, ...newValues }
544
- onFiltersChanged()
545
- }
546
-
547
- function onFiltersChanged(): void {
548
- page.value = 1
549
- loadItems()
550
- if (persistFilters) {
551
- const toPersist: Record<string, unknown> = {}
552
- for (const [name, value] of Object.entries(filterValues.value)) {
553
- const filterDef = filtersMap.value.get(name)
554
- if (
555
- filterDef?.persist !== false &&
556
- value !== null &&
557
- value !== undefined &&
558
- value !== ''
559
- ) {
560
- toPersist[name] = value
561
- }
562
- }
563
- if (searchQuery.value) {
564
- toPersist._search = searchQuery.value
565
- }
566
- setSessionFilters(filterSessionKey, toPersist)
567
- }
568
- if (syncUrlParams) {
569
- const query = { ...route.query } as Record<string, string>
570
- for (const [name, value] of Object.entries(filterValues.value)) {
571
- if (value !== null && value !== undefined && value !== '') {
572
- query[name] = String(value)
573
- } else {
574
- delete query[name]
575
- }
576
- }
577
- if (searchQuery.value) {
578
- query.search = searchQuery.value
579
- } else {
580
- delete query.search
581
- }
582
- router.replace({ query })
583
- }
584
- }
585
-
586
- function clearFilters(): void {
587
- const cleared: Record<string, unknown> = {}
588
- for (const [key, filterDef] of filtersMap.value.entries()) {
589
- cleared[key] = filterDef.default ?? null
590
- }
591
- filterValues.value = cleared
592
- searchQuery.value = ''
593
- if (persistFilters) {
594
- clearSessionFilters(filterSessionKey)
595
- }
596
- if (syncUrlParams) {
597
- const query = { ...route.query } as Record<string, string>
598
- for (const key of filtersMap.value.keys()) {
599
- delete query[key]
600
- }
601
- delete query.search
602
- router.replace({ query })
603
- }
604
- page.value = 1
605
- loadItems()
606
- }
607
-
608
- const filters = computed(() => Array.from(filtersMap.value.values()))
609
-
610
- /**
611
- * Check if a filter is at its default value
612
- * Used for styling: default = blue (info), modified = orange (warning)
613
- */
614
- function isFilterAtDefault(name: string): boolean {
615
- const filterDef = filtersMap.value.get(name)
616
- if (!filterDef) return true
617
- const currentValue = filterValues.value[name]
618
- const defaultValue = filterDef.default ?? null
619
- return currentValue === defaultValue
620
- }
621
-
622
- /**
623
- * Check if any filter is NOT at its default value (or search is active)
624
- * Useful to show a "clear filters" button only when needed
625
- */
626
- const hasActiveFilters = computed(() => {
627
- if (searchQuery.value) return true
628
- for (const [name, filterDef] of filtersMap.value.entries()) {
629
- const currentValue = filterValues.value[name]
630
- const defaultValue = filterDef.default ?? null
631
- if (currentValue !== defaultValue) return true
632
- }
633
- return false
514
+ // ============ FILTERS (#1195 — extracted subsystem) ============
515
+ // invokeFilterAlterHook is a thunk: alterHooks is declared after the
516
+ // actions section (it needs the action registry) and only runs post-setup.
517
+ const listFilters = useListFilters({
518
+ entityName,
519
+ manager,
520
+ orchestrator,
521
+ items: items as Ref<unknown[]>,
522
+ page,
523
+ searchQuery,
524
+ route,
525
+ router,
526
+ savedFilters,
527
+ persistFilters,
528
+ syncUrlParams,
529
+ autoLoadFilters,
530
+ filterSessionKey,
531
+ entityFilters,
532
+ loadItems: () => loadItems(),
533
+ setSearch: (searchCfg) => setSearch(searchCfg),
534
+ invokeFilterAlterHook: () => alterHooks.invokeFilterAlterHook(),
634
535
  })
635
-
636
- // ============ AUTO-LOAD FROM REGISTRY ============
637
-
638
- function initFromRegistry(): void {
639
- if (!autoLoadFilters) return
640
-
641
- const entityConfig = entityFilters[entityName]
642
- if (!entityConfig) return
643
-
644
- if (entityConfig.search) {
645
- setSearch(entityConfig.search)
646
- }
647
-
648
- if (entityConfig.filters) {
649
- for (const filterDef of entityConfig.filters) {
650
- addFilter(filterDef.name, filterDef)
651
- }
652
- }
653
- }
654
-
655
- async function loadFilterOptions(): Promise<void> {
656
- // Process filters configured directly via addFilter() (smart filter modes)
657
- for (const [filterName, filterDef] of filtersMap.value) {
658
- if (filterDef.options && filterDef.options.length > 1) continue
659
- if (filterDef.optionsFromCache) continue
660
-
661
- try {
662
- let rawOptions: Array<{ label: string; value: unknown }> | null = null
663
-
664
- // Mode 1: optionsEntity - fetch from related EntityManager via FilterQuery
665
- if (filterDef.optionsEntity) {
666
- const filterQuery = new FilterQuery({
667
- source: 'entity',
668
- entity: filterDef.optionsEntity,
669
- label: filterDef.optionLabel || 'name',
670
- value: filterDef.optionValue || 'id',
671
- })
672
-
673
- rawOptions = (await filterQuery.getOptions(orchestrator)) as Array<{
674
- label: string
675
- value: unknown
676
- }>
677
- filterDef._filterQuery = filterQuery
678
- }
679
- // Mode 2: optionsEndpoint - fetch from API endpoint
680
- else if (filterDef.optionsEndpoint) {
681
- const endpoint =
682
- filterDef.optionsEndpoint === true
683
- ? `distinct/${filterName}`
684
- : filterDef.optionsEndpoint
685
- const response = await manager.request('GET', endpoint as string)
686
- const data = Array.isArray(response)
687
- ? response
688
- : ((response as Record<string, unknown>)?.items as unknown[]) || []
689
- rawOptions = data.map((opt) => {
690
- if (typeof opt === 'object' && opt !== null) {
691
- const o = opt as Record<string, unknown>
692
- return {
693
- label: (o.label || o.name || String(o.value ?? o.id)) as string,
694
- value: o.value ?? o.id,
695
- }
696
- }
697
- return { label: snakeToTitle(String(opt)), value: opt }
698
- })
699
- }
700
-
701
- if (rawOptions !== null) {
702
- const cacheOptions = filterDef.cacheOptions ?? 'auto'
703
- let shouldCache = cacheOptions === true
704
-
705
- if (cacheOptions === 'auto') {
706
- shouldCache = rawOptions.length <= SMART_FILTER_THRESHOLD
707
- }
708
-
709
- const componentType =
710
- filterDef.component || (shouldCache ? 'dropdown' : 'autocomplete')
711
- const allLabel =
712
- filterDef.allLabel || filterDef.placeholder || `All ${snakeToTitle(filterName)}`
713
- let finalOptions = [{ label: allLabel, value: null as unknown }, ...rawOptions]
714
-
715
- if (typeof filterDef.processor === 'function') {
716
- finalOptions = filterDef.processor(finalOptions)
717
- }
718
-
719
- const updatedFilter: FilterConfig = {
720
- ...filterDef,
721
- options: finalOptions,
722
- type: componentType,
723
- _cacheOptions: shouldCache,
724
- _optionsLoaded: shouldCache,
725
- }
726
- delete updatedFilter.optionLabel
727
- delete updatedFilter.optionValue
728
- filtersMap.value.set(filterName, updatedFilter)
729
- }
730
- } catch (error) {
731
- console.warn(`[qdadm] Failed to load options for filter "${filterName}":`, error)
732
- }
733
- }
734
-
735
- // Invoke filter:alter hooks after all options are loaded
736
- await invokeFilterAlterHook()
737
-
738
- // Trigger Vue reactivity
739
- filtersMap.value = new Map(filtersMap.value)
740
- }
741
-
742
- async function updateCacheBasedFilters(): Promise<void> {
743
- if (items.value.length === 0) return
744
-
745
- let hasChanges = false
746
-
747
- for (const [filterName, filterDef] of filtersMap.value) {
748
- if (!filterDef.optionsFromCache) continue
749
- if (filterDef._optionsLoaded) continue
750
- // Skip if filter has an explicit query property (advanced usage)
751
- if (filterDef.query) continue
752
-
753
- const currentValue = filterValues.value[filterName]
754
- if (currentValue !== null && currentValue !== undefined && currentValue !== '') {
755
- continue
756
- }
757
-
758
- const fieldName =
759
- typeof filterDef.optionsFromCache === 'string' ? filterDef.optionsFromCache : filterName
760
-
761
- const filterQuery = new FilterQuery({
762
- source: 'field',
763
- field: fieldName,
764
- })
765
-
766
- // Provide a minimal mock manager with cached data for field-based filtering
767
- filterQuery.setParentManager({
768
- _cache: items.value,
769
- list: async () => ({ items: items.value }),
770
- })
771
-
772
- const rawOptions = (await filterQuery.getOptions()) as Array<{ label: string; value: unknown }>
773
-
774
- const cacheOptions = filterDef.cacheOptions ?? 'auto'
775
- let shouldCache = true
776
-
777
- if (cacheOptions === 'auto') {
778
- shouldCache = rawOptions.length <= SMART_FILTER_THRESHOLD
779
- } else if (cacheOptions === false) {
780
- shouldCache = false
781
- }
782
-
783
- const componentType =
784
- filterDef.component ||
785
- (rawOptions.length <= SMART_FILTER_THRESHOLD ? 'dropdown' : 'autocomplete')
786
-
787
- const allLabel =
788
- filterDef.allLabel || filterDef.placeholder || `All ${snakeToTitle(filterName)}`
789
- let finalOptions = [
790
- { label: allLabel, value: null as unknown },
791
- ...rawOptions.map((opt) => ({
792
- label: snakeToTitle(String(opt.label)),
793
- value: opt.value,
794
- })),
795
- ]
796
-
797
- if (typeof filterDef.processor === 'function') {
798
- finalOptions = filterDef.processor(finalOptions)
799
- }
800
-
801
- const updatedFilter: FilterConfig = {
802
- ...filterDef,
803
- options: finalOptions,
804
- type: componentType,
805
- _cacheOptions: shouldCache,
806
- _optionsLoaded: true,
807
- _filterQuery: filterQuery,
808
- }
809
-
810
- filtersMap.value.set(filterName, updatedFilter)
811
- hasChanges = true
812
- }
813
-
814
- if (hasChanges) {
815
- filtersMap.value = new Map(filtersMap.value)
816
- }
817
- }
818
-
819
- function restoreFilters(): void {
820
- for (const key of filtersMap.value.keys()) {
821
- if (route.query[key] !== undefined) {
822
- let value: unknown = route.query[key]
823
- if (value === 'true') value = true
824
- else if (value === 'false') value = false
825
- else if (value === 'null') value = null
826
- else if (!isNaN(Number(value)) && value !== '') value = Number(value)
827
- filterValues.value[key] = value
828
- }
829
- }
830
- if (route.query.search) {
831
- searchQuery.value = route.query.search as string
832
- }
833
- }
536
+ const {
537
+ filtersMap,
538
+ filterValues,
539
+ filters,
540
+ hasActiveFilters,
541
+ addFilter,
542
+ removeFilter,
543
+ setFilterValue,
544
+ updateFilters,
545
+ onFiltersChanged,
546
+ clearFilters,
547
+ isFilterAtDefault,
548
+ initFromRegistry,
549
+ loadFilterOptions,
550
+ updateCacheBasedFilters,
551
+ restoreFilters,
552
+ } = listFilters
834
553
 
835
554
  // ============ ACTIONS ============
836
555
  function resolveValue<T>(value: T | ((row: unknown) => T), row: unknown): T {
@@ -852,7 +571,6 @@ export function useListPage<T = unknown>(config: UseListPageOptions<T>): UseList
852
571
  }
853
572
  },
854
573
  })
855
- const actionsMap = actionRegistry.actionsMap
856
574
 
857
575
  function addAction(name: string, actionConfig: Omit<ActionConfig, 'name'>): void {
858
576
  actionRegistry.add({ name, ...actionConfig } as ActionConfig)
@@ -1215,96 +933,18 @@ export function useListPage<T = unknown>(config: UseListPageOptions<T>): UseList
1215
933
  }
1216
934
  })
1217
935
 
1218
- // ============ HOOKS ============
1219
-
1220
- async function invokeListAlterHook(): Promise<void> {
1221
- if (!hooks) return
1222
-
1223
- const configSnapshot = {
1224
- entity,
1225
- columns: Array.from(columnsMap.value.values()),
1226
- filters: Array.from(filtersMap.value.values()),
1227
- actions: Array.from(actionsMap.value.values()),
1228
- headerActions: Array.from(headerActionsMap.value.values()),
1229
- }
1230
-
1231
- // Include entity context in the snapshot for hooks
1232
- const configWithContext = { ...configSnapshot, entity, manager }
1233
-
1234
- let alteredConfig = await hooks.alter('list:alter', configWithContext)
1235
-
1236
- const entityHookName = `${entity}:list:alter`
1237
- if (hooks.hasHook(entityHookName)) {
1238
- alteredConfig = await hooks.alter(entityHookName, alteredConfig)
1239
- }
1240
-
1241
- applyAlteredConfig(alteredConfig)
1242
- }
1243
-
1244
- async function invokeFilterAlterHook(): Promise<void> {
1245
- if (!hooks) return
1246
-
1247
- const filterSnapshot = {
1248
- entity,
1249
- filters: Array.from(filtersMap.value.values()),
1250
- }
1251
-
1252
- let alteredFilters = (await hooks.alter('filter:alter', filterSnapshot)) as typeof filterSnapshot
1253
-
1254
- const entityHookName = `${entity}:filter:alter`
1255
- if (hooks.hasHook(entityHookName)) {
1256
- alteredFilters = (await hooks.alter(entityHookName, alteredFilters)) as typeof filterSnapshot
1257
- }
1258
-
1259
- if (alteredFilters.filters) {
1260
- filtersMap.value.clear()
1261
- for (const filter of alteredFilters.filters) {
1262
- filtersMap.value.set(filter.name, filter)
1263
- if (filterValues.value[filter.name] === undefined) {
1264
- filterValues.value[filter.name] = filter.default ?? null
1265
- }
1266
- }
1267
- }
1268
- }
1269
-
1270
- function applyAlteredConfig(alteredConfig: {
1271
- columns?: ColumnConfig[]
1272
- filters?: FilterConfig[]
1273
- actions?: ActionConfig[]
1274
- headerActions?: HeaderActionConfig[]
1275
- }): void {
1276
- if (alteredConfig.columns) {
1277
- columnsMap.value.clear()
1278
- for (const col of alteredConfig.columns) {
1279
- columnsMap.value.set(col.field, col)
1280
- }
1281
- }
1282
-
1283
- if (alteredConfig.filters) {
1284
- filtersMap.value.clear()
1285
- for (const filter of alteredConfig.filters) {
1286
- filtersMap.value.set(filter.name, filter)
1287
- if (filterValues.value[filter.name] === undefined) {
1288
- filterValues.value[filter.name] = filter.default ?? null
1289
- }
1290
- }
1291
- }
1292
-
1293
- if (alteredConfig.actions) {
1294
- // Route through the registry so the order list stays in sync (#1193)
1295
- actionRegistry.clear()
1296
- for (const action of alteredConfig.actions) {
1297
- actionRegistry.add(action)
1298
- }
1299
- }
1300
-
1301
- if (alteredConfig.headerActions) {
1302
- headerActionsMap.value.clear()
1303
- for (const action of alteredConfig.headerActions) {
1304
- headerActionsMap.value.set(action.name, action)
1305
- }
1306
- }
1307
- }
936
+ // ============ HOOKS (#1195 — extracted subsystem) ============
937
+ const alterHooks = useListAlterHooks({
938
+ hooks,
939
+ entity,
940
+ manager,
941
+ columnsMap,
942
+ filtersMap,
943
+ filterValues,
944
+ headerActionsMap,
945
+ actionRegistry,
946
+ })
947
+ const { invokeListAlterHook } = alterHooks
1308
948
 
1309
949
  // ============ LIFECYCLE ============
1310
950
  initFromRegistry()
@@ -501,10 +501,7 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
501
501
  * Check if SecurityChecker is configured via authAdapter
502
502
  */
503
503
  protected _hasSecurityChecker(): boolean {
504
- return (
505
- (this.authAdapter as unknown as { _securityChecker?: unknown })
506
- ?._securityChecker != null
507
- )
504
+ return this.authAdapter?.hasSecurityChecker?.() ?? false
508
505
  }
509
506
 
510
507
  /**
@@ -41,6 +41,16 @@ export class EntityAuthAdapter {
41
41
  this._securityChecker = checker
42
42
  }
43
43
 
44
+ /**
45
+ * Whether a SecurityChecker is configured (#1196).
46
+ *
47
+ * Public capability probe — EntityManager used to reach into the
48
+ * private _securityChecker field through a cast.
49
+ */
50
+ hasSecurityChecker(): boolean {
51
+ return this._securityChecker != null
52
+ }
53
+
44
54
  /**
45
55
  * Check if current user is granted an attribute (Symfony-like contract)
46
56
  *
@@ -52,28 +52,24 @@ export interface SecurityCheckerConfig {
52
52
  */
53
53
  export class SecurityChecker {
54
54
  protected _rolesProvider: RoleProvider
55
- protected _legacyRoleHierarchy: RoleHierarchy | null
56
55
  readonly getCurrentUser: () => AuthUser | null
57
56
 
58
57
  constructor({ rolesProvider, roleHierarchy, rolePermissions, getCurrentUser }: SecurityCheckerOptions) {
59
- // Support both new rolesProvider and legacy rolePermissions
60
58
  if (rolesProvider) {
61
59
  this._rolesProvider = rolesProvider
62
- this._legacyRoleHierarchy = null
63
60
  } else {
64
- // Legacy: create static provider from rolePermissions
61
+ // Legacy options (roleHierarchy/rolePermissions) are normalized ONCE
62
+ // into a static provider (#1196) — the parallel _legacyRoleHierarchy
63
+ // branch is gone; only the provider path remains.
65
64
  const hierarchyConfig =
66
- roleHierarchy instanceof RoleHierarchy ? {} : (roleHierarchy as RoleHierarchyConfig) || {}
65
+ roleHierarchy instanceof RoleHierarchy
66
+ ? roleHierarchy.map
67
+ : (roleHierarchy as RoleHierarchyConfig) || {}
67
68
 
68
69
  this._rolesProvider = new StaticRoleProvider({
69
70
  role_hierarchy: hierarchyConfig,
70
71
  role_permissions: rolePermissions || {},
71
72
  })
72
-
73
- this._legacyRoleHierarchy =
74
- roleHierarchy instanceof RoleHierarchy
75
- ? roleHierarchy
76
- : new RoleHierarchy(hierarchyConfig)
77
73
  }
78
74
 
79
75
  this.getCurrentUser = getCurrentUser
@@ -83,9 +79,6 @@ export class SecurityChecker {
83
79
  * Get role hierarchy (dynamically resolved from rolesProvider)
84
80
  */
85
81
  get roleHierarchy(): RoleHierarchy {
86
- if (this._legacyRoleHierarchy) {
87
- return this._legacyRoleHierarchy
88
- }
89
82
  return new RoleHierarchy(this._rolesProvider.getHierarchy())
90
83
  }
91
84
 
@@ -1,14 +1,14 @@
1
1
  import type { AxiosLikeClient, AxiosError } from './Kernel.types'
2
+ import type { Kernel } from './Kernel'
2
3
 
3
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
- type Self = any
4
+ // #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
5
+ type Self = Kernel
5
6
 
6
7
  /**
7
8
  * Patch Kernel prototype with API client methods.
8
9
  */
9
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
- export function applyApiMethods(KernelClass: { prototype: any }): void {
11
- const proto = KernelClass.prototype as Self
10
+ export function applyApiMethods(KernelClass: { prototype: Kernel }): void {
11
+ const proto = KernelClass.prototype
12
12
 
13
13
  /**
14
14
  * Setup an axios client with automatic auth and error handling
@@ -1,14 +1,14 @@
1
1
  import { authFactory, CompositeAuthAdapter } from '../entity/auth'
2
+ import type { Kernel } from './Kernel'
2
3
 
3
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
- type Self = any
4
+ // #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
5
+ type Self = Kernel
5
6
 
6
7
  /**
7
8
  * Patch Kernel prototype with auth-related methods.
8
9
  */
9
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
- export function applyAuthMethods(KernelClass: { prototype: any }): void {
11
- const proto = KernelClass.prototype as Self
10
+ export function applyAuthMethods(KernelClass: { prototype: Kernel }): void {
11
+ const proto = KernelClass.prototype
12
12
 
13
13
  /**
14
14
  * Resolve entityAuthAdapter through authFactory
@@ -9,13 +9,13 @@
9
9
 
10
10
  import { I18n } from '../i18n/I18n'
11
11
  import type { I18nOptions } from '../i18n/types'
12
+ import type { Kernel } from './Kernel'
12
13
 
13
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
- type Self = any
14
+ // #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
15
+ type Self = Kernel
15
16
 
16
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
- export function applyI18nMethods(KernelClass: { prototype: any }): void {
18
- const proto = KernelClass.prototype as Self
17
+ export function applyI18nMethods(KernelClass: { prototype: Kernel }): void {
18
+ const proto = KernelClass.prototype
19
19
 
20
20
  proto._createI18n = function (this: Self): void {
21
21
  const opts = (this.options.i18n as I18nOptions | undefined) ?? {}
@@ -1,15 +1,15 @@
1
1
  import { initModules, setSectionOrder } from '../module/moduleRegistry'
2
2
  import { createModuleLoader, type ModuleLike } from './ModuleLoader'
3
3
  import { createKernelContext, type KernelContext } from './KernelContext'
4
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
- type Self = any
4
+ import type { Kernel } from './Kernel'
5
+ // #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
6
+ type Self = Kernel
6
7
 
7
8
  /**
8
9
  * Patch Kernel prototype with module-related methods.
9
10
  */
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- export function applyModuleMethods(KernelClass: { prototype: any }): void {
12
- const proto = KernelClass.prototype as Self
11
+ export function applyModuleMethods(KernelClass: { prototype: Kernel }): void {
12
+ const proto = KernelClass.prototype
13
13
 
14
14
  /**
15
15
  * Fire entity cache warmups
@@ -13,15 +13,15 @@ import { ActiveStack } from '../chain/ActiveStack.js'
13
13
  import { StackHydrator } from '../chain/StackHydrator.js'
14
14
  import { Orchestrator } from '../orchestrator/Orchestrator'
15
15
  import type { EntityAuthAdapter } from '../entity/auth/EntityAuthAdapter'
16
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
- type Self = any
16
+ import type { Kernel } from './Kernel'
17
+ // #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
18
+ type Self = Kernel
18
19
 
19
20
  /**
20
21
  * Patch Kernel prototype with registry-related methods.
21
22
  */
22
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
- export function applyRegistryMethods(KernelClass: { prototype: any }): void {
24
- const proto = KernelClass.prototype as Self
23
+ export function applyRegistryMethods(KernelClass: { prototype: Kernel }): void {
24
+ const proto = KernelClass.prototype
25
25
 
26
26
  /**
27
27
  * Create signal bus for event-driven communication.