@kaikybrofc/omnizap-system 2.2.9 → 2.3.0

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.
Files changed (121) hide show
  1. package/README.md +20 -18
  2. package/app/config/adminIdentity.js +1 -3
  3. package/app/connection/socketController.js +10 -20
  4. package/app/controllers/messageController.js +7 -28
  5. package/app/modules/aiModule/catCommand.js +29 -192
  6. package/app/modules/broadcastModule/noticeCommand.js +28 -97
  7. package/app/modules/gameModule/diceCommand.js +6 -32
  8. package/app/modules/playModule/playCommand.js +57 -258
  9. package/app/modules/quoteModule/quoteCommand.js +2 -4
  10. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1 -13
  11. package/app/modules/statsModule/noMessageCommand.js +16 -84
  12. package/app/modules/statsModule/rankingCommand.js +5 -25
  13. package/app/modules/statsModule/rankingCommon.js +1 -9
  14. package/app/modules/stickerModule/convertToWebp.js +4 -27
  15. package/app/modules/stickerModule/stickerCommand.js +13 -24
  16. package/app/modules/stickerModule/stickerTextCommand.js +13 -25
  17. package/app/modules/stickerPackModule/autoPackCollectorService.js +16 -7
  18. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +20 -36
  19. package/app/modules/stickerPackModule/domainEvents.js +2 -11
  20. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +13 -50
  21. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +2 -15
  22. package/app/modules/stickerPackModule/semanticThemeClusterService.js +14 -41
  23. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +25 -95
  24. package/app/modules/stickerPackModule/stickerAssetRepository.js +12 -31
  25. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +13 -18
  26. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +284 -709
  27. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +27 -106
  28. package/app/modules/stickerPackModule/stickerClassificationService.js +46 -77
  29. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +13 -53
  30. package/app/modules/stickerPackModule/stickerDomainEventBus.js +10 -16
  31. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +13 -34
  32. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +1 -4
  33. package/app/modules/stickerPackModule/stickerObjectStorageService.js +26 -26
  34. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +32 -187
  35. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +6 -15
  36. package/app/modules/stickerPackModule/stickerPackItemRepository.js +6 -32
  37. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +12 -36
  38. package/app/modules/stickerPackModule/stickerPackMessageService.js +12 -40
  39. package/app/modules/stickerPackModule/stickerPackRepository.js +23 -66
  40. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +9 -21
  41. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +10 -40
  42. package/app/modules/stickerPackModule/stickerPackService.js +50 -115
  43. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +2 -21
  44. package/app/modules/stickerPackModule/stickerPackUtils.js +13 -3
  45. package/app/modules/stickerPackModule/stickerStorageService.js +16 -65
  46. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +4 -22
  47. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +14 -29
  48. package/app/modules/systemMetricsModule/pingCommand.js +9 -39
  49. package/app/modules/tiktokModule/tiktokCommand.js +17 -109
  50. package/app/modules/userModule/userCommand.js +2 -88
  51. package/app/observability/metrics.js +5 -16
  52. package/app/services/captchaService.js +1 -6
  53. package/app/services/dbWriteQueue.js +3 -18
  54. package/app/services/featureFlagService.js +2 -8
  55. package/app/services/newsBroadcastService.js +0 -1
  56. package/app/services/queueUtils.js +2 -4
  57. package/app/services/whatsappLoginLinkService.js +7 -9
  58. package/app/store/premiumUserStore.js +1 -2
  59. package/app/utils/antiLink/antiLinkModule.js +3 -233
  60. package/app/utils/logger/loggerModule.js +9 -34
  61. package/app/utils/systemMetrics/systemMetricsModule.js +1 -4
  62. package/database/init.js +1 -8
  63. package/docker-compose.yml +27 -27
  64. package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +220 -0
  65. package/docs/seo/satellite-page-template.md +91 -0
  66. package/docs/seo/satellite-pages-phase1.json +349 -0
  67. package/eslint.config.js +2 -15
  68. package/index.js +8 -36
  69. package/ml/clip_classifier/README.md +4 -6
  70. package/observability/alert-rules.yml +12 -12
  71. package/observability/grafana/provisioning/dashboards/dashboards.yml +1 -1
  72. package/package.json +8 -3
  73. package/public/api-docs/index.html +224 -141
  74. package/public/bot-whatsapp-para-grupo/index.html +306 -0
  75. package/public/bot-whatsapp-sem-programar/index.html +306 -0
  76. package/public/comandos/index.html +428 -0
  77. package/public/como-automatizar-avisos-no-whatsapp/index.html +306 -0
  78. package/public/como-criar-comandos-whatsapp/index.html +306 -0
  79. package/public/como-evitar-spam-no-whatsapp/index.html +306 -0
  80. package/public/como-moderar-grupo-whatsapp/index.html +306 -0
  81. package/public/como-organizar-comunidade-whatsapp/index.html +306 -0
  82. package/public/css/github-project-panel.css +20 -15
  83. package/public/css/stickers-admin.css +55 -39
  84. package/public/css/styles.css +37 -29
  85. package/public/index.html +1060 -1417
  86. package/public/js/apps/apiDocsApp.js +36 -153
  87. package/public/js/apps/createPackApp.js +69 -332
  88. package/public/js/apps/homeApp.js +201 -434
  89. package/public/js/apps/loginApp.js +3 -12
  90. package/public/js/apps/stickersAdminApp.js +190 -181
  91. package/public/js/apps/stickersApp.js +507 -1366
  92. package/public/js/catalog.js +11 -74
  93. package/public/js/github-panel/components/ErrorState.js +1 -8
  94. package/public/js/github-panel/components/GithubProjectPanel.js +2 -9
  95. package/public/js/github-panel/components/SkeletonPanel.js +1 -11
  96. package/public/js/github-panel/components/StatCard.js +1 -7
  97. package/public/js/github-panel/vendor/react.js +1 -9
  98. package/public/js/runtime/react-runtime.js +1 -9
  99. package/public/licenca/index.html +104 -86
  100. package/public/login/index.html +315 -321
  101. package/public/melhor-bot-whatsapp-para-grupos/index.html +306 -0
  102. package/public/sitemap.xml +45 -0
  103. package/public/stickers/admin/index.html +14 -19
  104. package/public/stickers/create/index.html +39 -43
  105. package/public/stickers/index.html +97 -41
  106. package/public/termos-de-uso/index.html +142 -115
  107. package/public/user/index.html +347 -346
  108. package/scripts/cache-bust.mjs +5 -24
  109. package/scripts/generate-seo-satellite-pages.mjs +431 -0
  110. package/scripts/run-prettier-all.mjs +25 -0
  111. package/scripts/sticker-catalog-loadtest.mjs +13 -11
  112. package/scripts/sticker-worker-task.mjs +1 -4
  113. package/scripts/sync-readme-snapshot.mjs +3 -2
  114. package/server/controllers/stickerCatalogController.js +407 -704
  115. package/server/http/httpServer.js +2 -10
  116. package/server/routes/stickerCatalog/catalogHandlers/catalogAdminHttp.js +1 -8
  117. package/server/routes/stickerCatalog/catalogHandlers/catalogAuthHttp.js +1 -9
  118. package/server/routes/stickerCatalog/catalogHandlers/catalogPublicHttp.js +10 -11
  119. package/server/routes/stickerCatalog/catalogHandlers/catalogUploadHttp.js +1 -10
  120. package/server/routes/stickerCatalog/catalogRouter.js +11 -13
  121. package/kaikybrofc-omnizap-system-2.2.9.tgz +0 -0
@@ -3,30 +3,11 @@ import { getActiveSocket } from '../../services/socketState.js';
3
3
  import { normalizeJid, resolveBotJid } from '../../config/baileysConfig.js';
4
4
  import { recordStickerAutoPackCycle } from '../../observability/metrics.js';
5
5
  import stickerPackService from './stickerPackServiceRuntime.js';
6
- import {
7
- countClassifiedStickerAssetsWithoutPack,
8
- listClassifiedStickerAssetsForCuration,
9
- } from './stickerAssetRepository.js';
10
- import {
11
- listClipImageEmbeddingsByImageHashes,
12
- listStickerClassificationsByAssetIds,
13
- } from './stickerAssetClassificationRepository.js';
14
- import {
15
- decorateStickerClassification,
16
- submitStickerClassificationFeedback,
17
- } from './stickerClassificationService.js';
18
- import {
19
- findStickerPackById,
20
- listStickerAutoPacksForCuration,
21
- listStickerPacksByOwner,
22
- softDeleteStickerPack,
23
- updateStickerPackFields,
24
- } from './stickerPackRepository.js';
25
- import {
26
- listStickerPackItems,
27
- listStickerPackItemsByPackIds,
28
- removeStickerPackItemsByPackId,
29
- } from './stickerPackItemRepository.js';
6
+ import { countClassifiedStickerAssetsWithoutPack, listClassifiedStickerAssetsForCuration } from './stickerAssetRepository.js';
7
+ import { listClipImageEmbeddingsByImageHashes, listStickerClassificationsByAssetIds } from './stickerAssetClassificationRepository.js';
8
+ import { decorateStickerClassification, submitStickerClassificationFeedback } from './stickerClassificationService.js';
9
+ import { findStickerPackById, listStickerAutoPacksForCuration, listStickerPacksByOwner, softDeleteStickerPack, updateStickerPackFields } from './stickerPackRepository.js';
10
+ import { listStickerPackItems, listStickerPackItemsByPackIds, removeStickerPackItemsByPackId } from './stickerPackItemRepository.js';
30
11
  import { listStickerPackEngagementByPackIds } from './stickerPackEngagementRepository.js';
31
12
 
32
13
  const parseEnvBool = (value, fallback) => {
@@ -58,16 +39,10 @@ const INTERVAL_MIN_MS_RAW = Number(process.env.STICKER_AUTO_PACK_BY_TAGS_INTERVA
58
39
  const INTERVAL_MAX_MS_RAW = Number(process.env.STICKER_AUTO_PACK_BY_TAGS_INTERVAL_MAX_MS);
59
40
  const DEFAULT_INTERVAL_MIN_MS = 5 * 60_000;
60
41
  const DEFAULT_INTERVAL_MAX_MS = 10 * 60_000;
61
- const INTERVAL_MIN_MS = Number.isFinite(INTERVAL_MIN_MS_RAW)
62
- ? Math.max(60_000, Math.min(3_600_000, INTERVAL_MIN_MS_RAW))
63
- : DEFAULT_INTERVAL_MIN_MS;
64
- const INTERVAL_MAX_MS_FROM_ENV = Number.isFinite(INTERVAL_MAX_MS_RAW)
65
- ? Math.max(60_000, Math.min(3_600_000, INTERVAL_MAX_MS_RAW))
66
- : DEFAULT_INTERVAL_MAX_MS;
42
+ const INTERVAL_MIN_MS = Number.isFinite(INTERVAL_MIN_MS_RAW) ? Math.max(60_000, Math.min(3_600_000, INTERVAL_MIN_MS_RAW)) : DEFAULT_INTERVAL_MIN_MS;
43
+ const INTERVAL_MAX_MS_FROM_ENV = Number.isFinite(INTERVAL_MAX_MS_RAW) ? Math.max(60_000, Math.min(3_600_000, INTERVAL_MAX_MS_RAW)) : DEFAULT_INTERVAL_MAX_MS;
67
44
  const INTERVAL_MAX_MS = Math.max(INTERVAL_MIN_MS, INTERVAL_MAX_MS_FROM_ENV);
68
- const LEGACY_FIXED_INTERVAL_MS = Number.isFinite(LEGACY_INTERVAL_MS) && LEGACY_INTERVAL_MS > 0
69
- ? Math.max(60_000, Math.min(3_600_000, LEGACY_INTERVAL_MS))
70
- : null;
45
+ const LEGACY_FIXED_INTERVAL_MS = Number.isFinite(LEGACY_INTERVAL_MS) && LEGACY_INTERVAL_MS > 0 ? Math.max(60_000, Math.min(3_600_000, LEGACY_INTERVAL_MS)) : null;
71
46
  const EFFECTIVE_INTERVAL_MIN_MS = LEGACY_FIXED_INTERVAL_MS || INTERVAL_MIN_MS;
72
47
  const EFFECTIVE_INTERVAL_MAX_MS = LEGACY_FIXED_INTERVAL_MS || INTERVAL_MAX_MS;
73
48
  const TARGET_PACK_SIZE = Math.max(5, Math.min(30, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_TARGET_SIZE) || 30));
@@ -76,307 +51,119 @@ const MAX_TAG_GROUPS = Math.max(0, Math.min(500, Number(process.env.STICKER_AUTO
76
51
  const ENABLE_SEMANTIC_CLUSTERING = parseEnvBool(process.env.ENABLE_SEMANTIC_CLUSTERING, false);
77
52
  const MAX_SCAN_ASSETS = Math.max(0, Math.min(250_000, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_SCAN_ASSETS) || 0));
78
53
  const MAX_ADDITIONS_PER_CYCLE = Math.max(10, Math.min(2000, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_ADDITIONS_PER_CYCLE) || 300));
79
- const AUTO_PACK_VISIBILITY = String(process.env.STICKER_AUTO_PACK_BY_TAGS_VISIBILITY || 'public').trim().toLowerCase() || 'public';
54
+ const AUTO_PACK_VISIBILITY =
55
+ String(process.env.STICKER_AUTO_PACK_BY_TAGS_VISIBILITY || 'public')
56
+ .trim()
57
+ .toLowerCase() || 'public';
80
58
  const AUTO_PUBLISHER = String(process.env.STICKER_AUTO_PACK_BY_TAGS_PUBLISHER || 'OmniZap Auto').trim() || 'OmniZap Auto';
81
59
  const TOP_TAGS_PER_ASSET = Math.max(1, Math.min(5, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_TOP_TAGS_PER_ASSET) || 3));
82
60
  const SCAN_PASSES = Math.max(1, Math.min(9, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_SCAN_PASSES) || 3));
83
- const SCAN_PASS_JITTER_PERCENT = Math.max(
84
- 0,
85
- Math.min(35, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_SCAN_PASS_JITTER_PERCENT) || 15),
86
- );
87
- const STABILITY_Z_SCORE = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_STABILITY_Z_SCORE))
88
- ? Math.max(0, Math.min(3.5, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_STABILITY_Z_SCORE)))
89
- : 1.282;
90
- const MIN_ASSET_ACCEPTANCE_RATE = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MIN_ASSET_ACCEPTANCE_RATE))
91
- ? Math.max(0, Math.min(1, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MIN_ASSET_ACCEPTANCE_RATE)))
92
- : 0.5;
93
- const MIN_THEME_DOMINANCE_RATIO = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MIN_THEME_DOMINANCE_RATIO))
94
- ? Math.max(0, Math.min(1, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MIN_THEME_DOMINANCE_RATIO)))
95
- : 0.55;
96
- const SCORE_STDDEV_PENALTY = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_SCORE_STDDEV_PENALTY))
97
- ? Math.max(0, Math.min(1.2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_SCORE_STDDEV_PENALTY)))
98
- : 0.18;
99
- const NSFW_THRESHOLD = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_NSFW_THRESHOLD))
100
- ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_NSFW_THRESHOLD)
101
- : 0.7;
102
- const NSFW_SUGGESTIVE_THRESHOLD = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_NSFW_SUGGESTIVE_THRESHOLD))
103
- ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_NSFW_SUGGESTIVE_THRESHOLD)
104
- : 0.4;
105
- const NSFW_EXPLICIT_THRESHOLD = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_NSFW_EXPLICIT_THRESHOLD))
106
- ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_NSFW_EXPLICIT_THRESHOLD)
107
- : 0.78;
61
+ const SCAN_PASS_JITTER_PERCENT = Math.max(0, Math.min(35, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_SCAN_PASS_JITTER_PERCENT) || 15));
62
+ const STABILITY_Z_SCORE = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_STABILITY_Z_SCORE)) ? Math.max(0, Math.min(3.5, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_STABILITY_Z_SCORE))) : 1.282;
63
+ const MIN_ASSET_ACCEPTANCE_RATE = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MIN_ASSET_ACCEPTANCE_RATE)) ? Math.max(0, Math.min(1, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MIN_ASSET_ACCEPTANCE_RATE))) : 0.5;
64
+ const MIN_THEME_DOMINANCE_RATIO = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MIN_THEME_DOMINANCE_RATIO)) ? Math.max(0, Math.min(1, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MIN_THEME_DOMINANCE_RATIO))) : 0.55;
65
+ const SCORE_STDDEV_PENALTY = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_SCORE_STDDEV_PENALTY)) ? Math.max(0, Math.min(1.2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_SCORE_STDDEV_PENALTY))) : 0.18;
66
+ const NSFW_THRESHOLD = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_NSFW_THRESHOLD)) ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_NSFW_THRESHOLD) : 0.7;
67
+ const NSFW_SUGGESTIVE_THRESHOLD = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_NSFW_SUGGESTIVE_THRESHOLD)) ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_NSFW_SUGGESTIVE_THRESHOLD) : 0.4;
68
+ const NSFW_EXPLICIT_THRESHOLD = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_NSFW_EXPLICIT_THRESHOLD)) ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_NSFW_EXPLICIT_THRESHOLD) : 0.78;
108
69
  const MIN_ASSET_EDGE = Math.max(32, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MIN_ASSET_EDGE) || 192);
109
70
  const MIN_ASSET_AREA = Math.max(32 * 32, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MIN_ASSET_AREA) || 192 * 192);
110
71
  const MIN_ASSET_BYTES = Math.max(512, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MIN_ASSET_BYTES) || 6 * 1024);
111
- const MAX_BLURRY_SCORE = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_BLURRY_SCORE))
112
- ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_BLURRY_SCORE)
113
- : 0.82;
114
- const MAX_LOW_QUALITY_SCORE = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_LOW_QUALITY_SCORE))
115
- ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_LOW_QUALITY_SCORE)
116
- : 0.82;
72
+ const MAX_BLURRY_SCORE = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_BLURRY_SCORE)) ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_BLURRY_SCORE) : 0.82;
73
+ const MAX_LOW_QUALITY_SCORE = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_LOW_QUALITY_SCORE)) ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_LOW_QUALITY_SCORE) : 0.82;
117
74
  const REBUILD_ENABLED = parseEnvBool(process.env.STICKER_AUTO_PACK_BY_TAGS_REBUILD_ENABLED, true);
118
- const INCLUDE_PACKED_WHEN_REBUILD_DISABLED = parseEnvBool(
119
- process.env.STICKER_AUTO_PACK_BY_TAGS_INCLUDE_PACKED_WHEN_REBUILD_DISABLED,
120
- false,
121
- );
75
+ const INCLUDE_PACKED_WHEN_REBUILD_DISABLED = parseEnvBool(process.env.STICKER_AUTO_PACK_BY_TAGS_INCLUDE_PACKED_WHEN_REBUILD_DISABLED, false);
122
76
  const MAX_REMOVALS_PER_CYCLE = Math.max(0, Math.min(500, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_REMOVALS_PER_CYCLE) || 120));
123
- const DEDUPE_SIMILARITY_THRESHOLD = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_DEDUPE_SIMILARITY_THRESHOLD))
124
- ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_DEDUPE_SIMILARITY_THRESHOLD)
125
- : 0.985;
126
- const COHESION_REBUILD_THRESHOLD = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_COHESION_REBUILD_THRESHOLD))
127
- ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_COHESION_REBUILD_THRESHOLD)
128
- : 0.56;
129
- const MOVE_OUT_THEME_SCORE_THRESHOLD = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MOVE_OUT_THRESHOLD))
130
- ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MOVE_OUT_THRESHOLD)
131
- : 0.22;
132
- const MOVE_IN_THEME_SCORE_THRESHOLD = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MOVE_IN_THRESHOLD))
133
- ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MOVE_IN_THRESHOLD)
134
- : 0.12;
135
- const ENTROPY_THRESHOLD = Number.isFinite(Number(process.env.ENTROPY_THRESHOLD))
136
- ? Number(process.env.ENTROPY_THRESHOLD)
137
- : 2.5;
138
- const ENTROPY_NORMALIZED_THRESHOLD = Number.isFinite(Number(process.env.ENTROPY_NORMALIZED_THRESHOLD))
139
- ? Math.max(0, Math.min(1, Number(process.env.ENTROPY_NORMALIZED_THRESHOLD)))
140
- : 0.76;
141
- const ENTROPY_WEIGHT = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ENTROPY_WEIGHT))
142
- ? Math.max(0, Math.min(1.5, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ENTROPY_WEIGHT)))
143
- : 0.09;
144
- const AMBIGUOUS_FLAG_PENALTY = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_AMBIGUOUS_FLAG_PENALTY))
145
- ? Math.max(0, Math.min(0.5, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_AMBIGUOUS_FLAG_PENALTY)))
146
- : 0.06;
147
- const ADAPTIVE_BONUS_WEIGHT = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ADAPTIVE_BONUS_WEIGHT))
148
- ? Math.max(0, Math.min(1.2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ADAPTIVE_BONUS_WEIGHT)))
149
- : 0.18;
150
- const MARGIN_BONUS_WEIGHT = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MARGIN_BONUS_WEIGHT))
151
- ? Math.max(0, Math.min(1.2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MARGIN_BONUS_WEIGHT)))
152
- : 0.12;
153
- const SIMILAR_IMAGES_PENALTY_WEIGHT = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_SIMILAR_IMAGES_PENALTY_WEIGHT))
154
- ? Math.max(0, Math.min(1.2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_SIMILAR_IMAGES_PENALTY_WEIGHT)))
155
- : 0.08;
156
- const LLM_TRAIT_WEIGHT = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_LLM_TRAIT_WEIGHT))
157
- ? Math.max(0, Math.min(0.6, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_LLM_TRAIT_WEIGHT)))
158
- : 0.1;
159
- const ASSET_QUALITY_W1 = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W1))
160
- ? Math.max(0, Math.min(2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W1)))
161
- : 0.34;
162
- const ASSET_QUALITY_W2 = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W2))
163
- ? Math.max(0, Math.min(2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W2)))
164
- : 0.24;
165
- const ASSET_QUALITY_W3 = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W3))
166
- ? Math.max(0, Math.min(2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W3)))
167
- : 0.18;
168
- const ASSET_QUALITY_W4 = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W4))
169
- ? Math.max(0, Math.min(2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W4)))
170
- : 0.24;
171
- const AFFINITY_WEIGHT_CAP = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_AFFINITY_CAP))
172
- ? Math.max(0, Math.min(1, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_AFFINITY_CAP)))
173
- : 0.85;
77
+ const DEDUPE_SIMILARITY_THRESHOLD = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_DEDUPE_SIMILARITY_THRESHOLD)) ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_DEDUPE_SIMILARITY_THRESHOLD) : 0.985;
78
+ const COHESION_REBUILD_THRESHOLD = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_COHESION_REBUILD_THRESHOLD)) ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_COHESION_REBUILD_THRESHOLD) : 0.56;
79
+ const MOVE_OUT_THEME_SCORE_THRESHOLD = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MOVE_OUT_THRESHOLD)) ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MOVE_OUT_THRESHOLD) : 0.22;
80
+ const MOVE_IN_THEME_SCORE_THRESHOLD = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MOVE_IN_THRESHOLD)) ? Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MOVE_IN_THRESHOLD) : 0.12;
81
+ const ENTROPY_THRESHOLD = Number.isFinite(Number(process.env.ENTROPY_THRESHOLD)) ? Number(process.env.ENTROPY_THRESHOLD) : 2.5;
82
+ const ENTROPY_NORMALIZED_THRESHOLD = Number.isFinite(Number(process.env.ENTROPY_NORMALIZED_THRESHOLD)) ? Math.max(0, Math.min(1, Number(process.env.ENTROPY_NORMALIZED_THRESHOLD))) : 0.76;
83
+ const ENTROPY_WEIGHT = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ENTROPY_WEIGHT)) ? Math.max(0, Math.min(1.5, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ENTROPY_WEIGHT))) : 0.09;
84
+ const AMBIGUOUS_FLAG_PENALTY = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_AMBIGUOUS_FLAG_PENALTY)) ? Math.max(0, Math.min(0.5, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_AMBIGUOUS_FLAG_PENALTY))) : 0.06;
85
+ const ADAPTIVE_BONUS_WEIGHT = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ADAPTIVE_BONUS_WEIGHT)) ? Math.max(0, Math.min(1.2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ADAPTIVE_BONUS_WEIGHT))) : 0.18;
86
+ const MARGIN_BONUS_WEIGHT = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MARGIN_BONUS_WEIGHT)) ? Math.max(0, Math.min(1.2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MARGIN_BONUS_WEIGHT))) : 0.12;
87
+ const SIMILAR_IMAGES_PENALTY_WEIGHT = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_SIMILAR_IMAGES_PENALTY_WEIGHT)) ? Math.max(0, Math.min(1.2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_SIMILAR_IMAGES_PENALTY_WEIGHT))) : 0.08;
88
+ const LLM_TRAIT_WEIGHT = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_LLM_TRAIT_WEIGHT)) ? Math.max(0, Math.min(0.6, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_LLM_TRAIT_WEIGHT))) : 0.1;
89
+ const ASSET_QUALITY_W1 = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W1)) ? Math.max(0, Math.min(2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W1))) : 0.34;
90
+ const ASSET_QUALITY_W2 = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W2)) ? Math.max(0, Math.min(2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W2))) : 0.24;
91
+ const ASSET_QUALITY_W3 = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W3)) ? Math.max(0, Math.min(2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W3))) : 0.18;
92
+ const ASSET_QUALITY_W4 = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W4)) ? Math.max(0, Math.min(2, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_ASSET_QUALITY_W4))) : 0.24;
93
+ const AFFINITY_WEIGHT_CAP = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_AFFINITY_CAP)) ? Math.max(0, Math.min(1, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_AFFINITY_CAP))) : 0.85;
174
94
  const ENABLE_AFFINITY_LOG_SCALING = parseEnvBool(process.env.STICKER_AUTO_PACK_BY_TAGS_ENABLE_AFFINITY_LOG_SCALING, true);
175
- const AFFINITY_LOG_SCALE = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_AFFINITY_LOG_SCALE))
176
- ? Math.max(0.1, Math.min(20, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_AFFINITY_LOG_SCALE)))
177
- : 4;
178
- const REVIEW_SAMPLE_PERCENT = Math.max(
179
- 0,
180
- Math.min(100, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_RECHECK_SAMPLE_PERCENT) || 5),
181
- );
95
+ const AFFINITY_LOG_SCALE = Number.isFinite(Number(process.env.STICKER_AUTO_PACK_BY_TAGS_AFFINITY_LOG_SCALE)) ? Math.max(0.1, Math.min(20, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_AFFINITY_LOG_SCALE))) : 4;
96
+ const REVIEW_SAMPLE_PERCENT = Math.max(0, Math.min(100, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_RECHECK_SAMPLE_PERCENT) || 5));
182
97
  const REVIEW_VERSION_TARGET = String(process.env.STICKER_AUTO_PACK_BY_TAGS_REVIEW_CLASSIFICATION_VERSION || '').trim();
183
- const CURATION_OWNERS_POOL_RAW = String(
184
- process.env.CURATION_OWNERS_POOL || process.env.STICKER_AUTO_PACK_CURATION_OWNERS_POOL || '',
185
- ).trim();
186
- const MAX_PACKS_PER_OWNER = parseMaxPacksPerOwnerLimit(
187
- process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_PACKS_PER_OWNER || process.env.STICKER_PACK_MAX_PACKS_PER_OWNER,
188
- 50,
189
- );
190
- const HARD_MIN_GROUP_SIZE = Math.max(
191
- 3,
192
- Math.min(TARGET_PACK_SIZE, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_HARD_MIN_GROUP_SIZE) || 12),
193
- );
194
- const SEMANTIC_CLUSTER_MIN_SIZE_FOR_PACK = Math.max(
195
- HARD_MIN_GROUP_SIZE,
196
- Math.min(TARGET_PACK_SIZE, Number(process.env.SEMANTIC_CLUSTER_MIN_SIZE_FOR_PACK) || HARD_MIN_GROUP_SIZE),
197
- );
198
- const EFFECTIVE_HARD_MIN_GROUP_SIZE = ENABLE_SEMANTIC_CLUSTERING
199
- ? Math.max(HARD_MIN_GROUP_SIZE, SEMANTIC_CLUSTER_MIN_SIZE_FOR_PACK)
200
- : HARD_MIN_GROUP_SIZE;
201
- const HARD_MIN_PACK_ITEMS = Math.max(
202
- 1,
203
- Math.min(TARGET_PACK_SIZE, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_HARD_MIN_PACK_ITEMS) || 12),
204
- );
205
- const READY_PACK_MIN_ITEMS = Math.max(
206
- HARD_MIN_PACK_ITEMS,
207
- Math.min(TARGET_PACK_SIZE, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_READY_MIN_ITEMS) || 20),
208
- );
209
- const MAX_PACKS_PER_THEME = Math.max(
210
- 1,
211
- Math.min(10, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_PACKS_PER_THEME) || 1),
212
- );
213
- const GLOBAL_AUTO_PACK_LIMIT = Math.max(
214
- 10,
215
- Math.min(10_000, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_GLOBAL_PACK_LIMIT) || 300),
216
- );
217
- const DYNAMIC_GROUP_LIMIT_BASE = Math.max(
218
- 3,
219
- Math.min(500, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_DYNAMIC_GROUP_LIMIT_BASE) || 30),
220
- );
221
- const RETRO_CONSOLIDATION_ENABLED = parseEnvBool(
222
- process.env.STICKER_AUTO_PACK_BY_TAGS_RETRO_CONSOLIDATION_ENABLED,
223
- true,
224
- );
225
- const RETRO_CONSOLIDATION_THEME_LIMIT = Math.max(
226
- 1,
227
- Math.min(2000, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_RETRO_CONSOLIDATION_THEME_LIMIT) || 1000),
228
- );
229
- const RETRO_CONSOLIDATION_MUTATION_LIMIT = Math.max(
230
- 10,
231
- Math.min(10_000, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_RETRO_CONSOLIDATION_MUTATION_LIMIT) || 2000),
232
- );
98
+ const CURATION_OWNERS_POOL_RAW = String(process.env.CURATION_OWNERS_POOL || process.env.STICKER_AUTO_PACK_CURATION_OWNERS_POOL || '').trim();
99
+ const MAX_PACKS_PER_OWNER = parseMaxPacksPerOwnerLimit(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_PACKS_PER_OWNER || process.env.STICKER_PACK_MAX_PACKS_PER_OWNER, 50);
100
+ const HARD_MIN_GROUP_SIZE = Math.max(3, Math.min(TARGET_PACK_SIZE, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_HARD_MIN_GROUP_SIZE) || 12));
101
+ const SEMANTIC_CLUSTER_MIN_SIZE_FOR_PACK = Math.max(HARD_MIN_GROUP_SIZE, Math.min(TARGET_PACK_SIZE, Number(process.env.SEMANTIC_CLUSTER_MIN_SIZE_FOR_PACK) || HARD_MIN_GROUP_SIZE));
102
+ const EFFECTIVE_HARD_MIN_GROUP_SIZE = ENABLE_SEMANTIC_CLUSTERING ? Math.max(HARD_MIN_GROUP_SIZE, SEMANTIC_CLUSTER_MIN_SIZE_FOR_PACK) : HARD_MIN_GROUP_SIZE;
103
+ const HARD_MIN_PACK_ITEMS = Math.max(1, Math.min(TARGET_PACK_SIZE, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_HARD_MIN_PACK_ITEMS) || 12));
104
+ const READY_PACK_MIN_ITEMS = Math.max(HARD_MIN_PACK_ITEMS, Math.min(TARGET_PACK_SIZE, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_READY_MIN_ITEMS) || 20));
105
+ const MAX_PACKS_PER_THEME = Math.max(1, Math.min(10, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_PACKS_PER_THEME) || 1));
106
+ const GLOBAL_AUTO_PACK_LIMIT = Math.max(10, Math.min(10_000, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_GLOBAL_PACK_LIMIT) || 300));
107
+ const DYNAMIC_GROUP_LIMIT_BASE = Math.max(3, Math.min(500, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_DYNAMIC_GROUP_LIMIT_BASE) || 30));
108
+ const RETRO_CONSOLIDATION_ENABLED = parseEnvBool(process.env.STICKER_AUTO_PACK_BY_TAGS_RETRO_CONSOLIDATION_ENABLED, true);
109
+ const RETRO_CONSOLIDATION_THEME_LIMIT = Math.max(1, Math.min(2000, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_RETRO_CONSOLIDATION_THEME_LIMIT) || 1000));
110
+ const RETRO_CONSOLIDATION_MUTATION_LIMIT = Math.max(10, Math.min(10_000, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_RETRO_CONSOLIDATION_MUTATION_LIMIT) || 2000));
233
111
  const PRIORITIZE_COMPLETION = parseEnvBool(process.env.STICKER_AUTO_PACK_BY_TAGS_PRIORITIZE_COMPLETION, true);
234
112
  const COMPLETION_TRANSFER_ENABLED = parseEnvBool(process.env.STICKER_AUTO_PACK_BY_TAGS_COMPLETION_TRANSFER_ENABLED, true);
235
- const COMPLETION_TRANSFER_MIN_DONOR_ITEMS = Math.max(
236
- 1,
237
- Math.min(
238
- TARGET_PACK_SIZE - 1,
239
- Number(process.env.STICKER_AUTO_PACK_BY_TAGS_COMPLETION_TRANSFER_MIN_DONOR_ITEMS)
240
- || Math.max(2, Math.min(TARGET_PACK_SIZE - 1, 8)),
241
- ),
242
- );
113
+ const COMPLETION_TRANSFER_MIN_DONOR_ITEMS = Math.max(1, Math.min(TARGET_PACK_SIZE - 1, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_COMPLETION_TRANSFER_MIN_DONOR_ITEMS) || Math.max(2, Math.min(TARGET_PACK_SIZE - 1, 8))));
243
114
  const ENABLE_GLOBAL_OPTIMIZATION = parseEnvBool(process.env.ENABLE_GLOBAL_OPTIMIZATION, true);
244
115
  const OPTIMIZATION_CYCLES = Math.max(1, Math.min(8, Number(process.env.OPTIMIZATION_CYCLES) || 3));
245
- const OPTIMIZATION_EPSILON = Number.isFinite(Number(process.env.OPTIMIZATION_EPSILON))
246
- ? Math.max(0.000001, Math.min(0.5, Number(process.env.OPTIMIZATION_EPSILON)))
247
- : 0.001;
248
- const OPTIMIZATION_STABLE_CYCLES = Math.max(
249
- 1,
250
- Math.min(5, Number(process.env.OPTIMIZATION_STABLE_CYCLES) || 2),
251
- );
252
- const TRANSFER_THRESHOLD = Number.isFinite(Number(process.env.TRANSFER_THRESHOLD))
253
- ? Math.max(-0.2, Math.min(1, Number(process.env.TRANSFER_THRESHOLD)))
254
- : 0.02;
255
- const MERGE_THRESHOLD = Number.isFinite(Number(process.env.MERGE_THRESHOLD))
256
- ? Math.max(0, Math.min(1, Number(process.env.MERGE_THRESHOLD)))
257
- : 0.75;
116
+ const OPTIMIZATION_EPSILON = Number.isFinite(Number(process.env.OPTIMIZATION_EPSILON)) ? Math.max(0.000001, Math.min(0.5, Number(process.env.OPTIMIZATION_EPSILON))) : 0.001;
117
+ const OPTIMIZATION_STABLE_CYCLES = Math.max(1, Math.min(5, Number(process.env.OPTIMIZATION_STABLE_CYCLES) || 2));
118
+ const TRANSFER_THRESHOLD = Number.isFinite(Number(process.env.TRANSFER_THRESHOLD)) ? Math.max(-0.2, Math.min(1, Number(process.env.TRANSFER_THRESHOLD))) : 0.02;
119
+ const MERGE_THRESHOLD = Number.isFinite(Number(process.env.MERGE_THRESHOLD)) ? Math.max(0, Math.min(1, Number(process.env.MERGE_THRESHOLD))) : 0.75;
258
120
  const MIN_PACK_SIZE = Math.max(1, Math.min(TARGET_PACK_SIZE, Number(process.env.MIN_PACK_SIZE) || 8));
259
- const AUTO_ARCHIVE_BELOW_PERCENTILE = Number.isFinite(Number(process.env.AUTO_ARCHIVE_BELOW_PERCENTILE))
260
- ? Math.max(0, Math.min(100, Number(process.env.AUTO_ARCHIVE_BELOW_PERCENTILE)))
261
- : 20;
262
- const SYSTEM_REDUNDANCY_LAMBDA = Number.isFinite(Number(process.env.SYSTEM_REDUNDANCY_LAMBDA))
263
- ? Math.max(0, Math.min(2, Number(process.env.SYSTEM_REDUNDANCY_LAMBDA)))
264
- : 0.2;
265
- const MATRIX_ALPHA = Number.isFinite(Number(process.env.STICKER_MATRIX_ALPHA))
266
- ? Math.max(0, Math.min(3, Number(process.env.STICKER_MATRIX_ALPHA)))
267
- : 0.38;
268
- const MATRIX_BETA = Number.isFinite(Number(process.env.STICKER_MATRIX_BETA))
269
- ? Math.max(0, Math.min(3, Number(process.env.STICKER_MATRIX_BETA)))
270
- : 0.27;
271
- const MATRIX_GAMMA = Number.isFinite(Number(process.env.STICKER_MATRIX_GAMMA))
272
- ? Math.max(0, Math.min(3, Number(process.env.STICKER_MATRIX_GAMMA)))
273
- : 0.23;
274
- const MATRIX_DELTA = Number.isFinite(Number(process.env.STICKER_MATRIX_DELTA))
275
- ? Math.max(0, Math.min(3, Number(process.env.STICKER_MATRIX_DELTA)))
276
- : 0.18;
277
- const PACK_QUALITY_W1 = Number.isFinite(Number(process.env.PACK_QUALITY_W1))
278
- ? Math.max(0, Math.min(3, Number(process.env.PACK_QUALITY_W1)))
279
- : 0.32;
280
- const PACK_QUALITY_W2 = Number.isFinite(Number(process.env.PACK_QUALITY_W2))
281
- ? Math.max(0, Math.min(3, Number(process.env.PACK_QUALITY_W2)))
282
- : 0.24;
283
- const PACK_QUALITY_W3 = Number.isFinite(Number(process.env.PACK_QUALITY_W3))
284
- ? Math.max(0, Math.min(3, Number(process.env.PACK_QUALITY_W3)))
285
- : 0.16;
286
- const PACK_QUALITY_W4 = Number.isFinite(Number(process.env.PACK_QUALITY_W4))
287
- ? Math.max(0, Math.min(3, Number(process.env.PACK_QUALITY_W4)))
288
- : 0.14;
289
- const PACK_QUALITY_W5 = Number.isFinite(Number(process.env.PACK_QUALITY_W5))
290
- ? Math.max(0, Math.min(3, Number(process.env.PACK_QUALITY_W5)))
291
- : 0.09;
292
- const PACK_QUALITY_W6 = Number.isFinite(Number(process.env.PACK_QUALITY_W6))
293
- ? Math.max(0, Math.min(3, Number(process.env.PACK_QUALITY_W6)))
294
- : 0.07;
295
- const MIGRATION_CANDIDATE_LIMIT = Number.isFinite(Number(process.env.MIGRATION_CANDIDATE_LIMIT))
296
- ? Math.max(4, Math.min(64, Number(process.env.MIGRATION_CANDIDATE_LIMIT)))
297
- : 16;
298
- const TRANSFER_CANDIDATE_SIMILARITY_FLOOR = Number.isFinite(Number(process.env.TRANSFER_CANDIDATE_SIMILARITY_FLOOR))
299
- ? Math.max(0, Math.min(1, Number(process.env.TRANSFER_CANDIDATE_SIMILARITY_FLOOR)))
300
- : 0.35;
301
- const INTER_PACK_SIMILARITY_THRESHOLD = Number.isFinite(Number(process.env.INTER_PACK_SIMILARITY_THRESHOLD))
302
- ? Math.max(0, Math.min(1, Number(process.env.INTER_PACK_SIMILARITY_THRESHOLD)))
303
- : 0.85;
304
- const PACK_TIER_QUALITY_W1 = Number.isFinite(Number(process.env.PACK_TIER_QUALITY_W1))
305
- ? Math.max(0, Math.min(1, Number(process.env.PACK_TIER_QUALITY_W1)))
306
- : 0.40;
307
- const PACK_TIER_QUALITY_W2 = Number.isFinite(Number(process.env.PACK_TIER_QUALITY_W2))
308
- ? Math.max(0, Math.min(1, Number(process.env.PACK_TIER_QUALITY_W2)))
309
- : 0.25;
310
- const PACK_TIER_QUALITY_W3 = Number.isFinite(Number(process.env.PACK_TIER_QUALITY_W3))
311
- ? Math.max(0, Math.min(1, Number(process.env.PACK_TIER_QUALITY_W3)))
312
- : 0.15;
313
- const PACK_TIER_QUALITY_W4 = Number.isFinite(Number(process.env.PACK_TIER_QUALITY_W4))
314
- ? Math.max(0, Math.min(1, Number(process.env.PACK_TIER_QUALITY_W4)))
315
- : 0.10;
316
- const PACK_TIER_QUALITY_W5 = Number.isFinite(Number(process.env.PACK_TIER_QUALITY_W5))
317
- ? Math.max(0, Math.min(1, Number(process.env.PACK_TIER_QUALITY_W5)))
318
- : 0.10;
319
- const AUTO_PACK_PROFILE = String(process.env.AUTO_PACK_PROFILE || 'BALANCED').trim().toUpperCase();
121
+ const AUTO_ARCHIVE_BELOW_PERCENTILE = Number.isFinite(Number(process.env.AUTO_ARCHIVE_BELOW_PERCENTILE)) ? Math.max(0, Math.min(100, Number(process.env.AUTO_ARCHIVE_BELOW_PERCENTILE))) : 20;
122
+ const SYSTEM_REDUNDANCY_LAMBDA = Number.isFinite(Number(process.env.SYSTEM_REDUNDANCY_LAMBDA)) ? Math.max(0, Math.min(2, Number(process.env.SYSTEM_REDUNDANCY_LAMBDA))) : 0.2;
123
+ const MATRIX_ALPHA = Number.isFinite(Number(process.env.STICKER_MATRIX_ALPHA)) ? Math.max(0, Math.min(3, Number(process.env.STICKER_MATRIX_ALPHA))) : 0.38;
124
+ const MATRIX_BETA = Number.isFinite(Number(process.env.STICKER_MATRIX_BETA)) ? Math.max(0, Math.min(3, Number(process.env.STICKER_MATRIX_BETA))) : 0.27;
125
+ const MATRIX_GAMMA = Number.isFinite(Number(process.env.STICKER_MATRIX_GAMMA)) ? Math.max(0, Math.min(3, Number(process.env.STICKER_MATRIX_GAMMA))) : 0.23;
126
+ const MATRIX_DELTA = Number.isFinite(Number(process.env.STICKER_MATRIX_DELTA)) ? Math.max(0, Math.min(3, Number(process.env.STICKER_MATRIX_DELTA))) : 0.18;
127
+ const PACK_QUALITY_W1 = Number.isFinite(Number(process.env.PACK_QUALITY_W1)) ? Math.max(0, Math.min(3, Number(process.env.PACK_QUALITY_W1))) : 0.32;
128
+ const PACK_QUALITY_W2 = Number.isFinite(Number(process.env.PACK_QUALITY_W2)) ? Math.max(0, Math.min(3, Number(process.env.PACK_QUALITY_W2))) : 0.24;
129
+ const PACK_QUALITY_W3 = Number.isFinite(Number(process.env.PACK_QUALITY_W3)) ? Math.max(0, Math.min(3, Number(process.env.PACK_QUALITY_W3))) : 0.16;
130
+ const PACK_QUALITY_W4 = Number.isFinite(Number(process.env.PACK_QUALITY_W4)) ? Math.max(0, Math.min(3, Number(process.env.PACK_QUALITY_W4))) : 0.14;
131
+ const PACK_QUALITY_W5 = Number.isFinite(Number(process.env.PACK_QUALITY_W5)) ? Math.max(0, Math.min(3, Number(process.env.PACK_QUALITY_W5))) : 0.09;
132
+ const PACK_QUALITY_W6 = Number.isFinite(Number(process.env.PACK_QUALITY_W6)) ? Math.max(0, Math.min(3, Number(process.env.PACK_QUALITY_W6))) : 0.07;
133
+ const MIGRATION_CANDIDATE_LIMIT = Number.isFinite(Number(process.env.MIGRATION_CANDIDATE_LIMIT)) ? Math.max(4, Math.min(64, Number(process.env.MIGRATION_CANDIDATE_LIMIT))) : 16;
134
+ const TRANSFER_CANDIDATE_SIMILARITY_FLOOR = Number.isFinite(Number(process.env.TRANSFER_CANDIDATE_SIMILARITY_FLOOR)) ? Math.max(0, Math.min(1, Number(process.env.TRANSFER_CANDIDATE_SIMILARITY_FLOOR))) : 0.35;
135
+ const INTER_PACK_SIMILARITY_THRESHOLD = Number.isFinite(Number(process.env.INTER_PACK_SIMILARITY_THRESHOLD)) ? Math.max(0, Math.min(1, Number(process.env.INTER_PACK_SIMILARITY_THRESHOLD))) : 0.85;
136
+ const PACK_TIER_QUALITY_W1 = Number.isFinite(Number(process.env.PACK_TIER_QUALITY_W1)) ? Math.max(0, Math.min(1, Number(process.env.PACK_TIER_QUALITY_W1))) : 0.4;
137
+ const PACK_TIER_QUALITY_W2 = Number.isFinite(Number(process.env.PACK_TIER_QUALITY_W2)) ? Math.max(0, Math.min(1, Number(process.env.PACK_TIER_QUALITY_W2))) : 0.25;
138
+ const PACK_TIER_QUALITY_W3 = Number.isFinite(Number(process.env.PACK_TIER_QUALITY_W3)) ? Math.max(0, Math.min(1, Number(process.env.PACK_TIER_QUALITY_W3))) : 0.15;
139
+ const PACK_TIER_QUALITY_W4 = Number.isFinite(Number(process.env.PACK_TIER_QUALITY_W4)) ? Math.max(0, Math.min(1, Number(process.env.PACK_TIER_QUALITY_W4))) : 0.1;
140
+ const PACK_TIER_QUALITY_W5 = Number.isFinite(Number(process.env.PACK_TIER_QUALITY_W5)) ? Math.max(0, Math.min(1, Number(process.env.PACK_TIER_QUALITY_W5))) : 0.1;
141
+ const AUTO_PACK_PROFILE = String(process.env.AUTO_PACK_PROFILE || 'BALANCED')
142
+ .trim()
143
+ .toUpperCase();
320
144
  const IS_AGGRESSIVE_PROFILE = AUTO_PACK_PROFILE === 'AGGRESSIVE';
321
- const AGGRESSIVE_MIGRATION_THRESHOLD = Number.isFinite(Number(process.env.AGGRESSIVE_MIGRATION_THRESHOLD))
322
- ? Math.max(-0.1, Math.min(1, Number(process.env.AGGRESSIVE_MIGRATION_THRESHOLD)))
323
- : 0.01;
324
- const ARCHIVE_LOW_SCORE_PACKS = parseEnvBool(
325
- process.env.ARCHIVE_LOW_SCORE_PACKS,
326
- IS_AGGRESSIVE_PROFILE,
327
- );
328
- const GLOBAL_ENERGY_W1 = Number.isFinite(Number(process.env.GLOBAL_ENERGY_W1))
329
- ? Math.max(0, Math.min(2, Number(process.env.GLOBAL_ENERGY_W1)))
330
- : 0.40;
331
- const GLOBAL_ENERGY_W2 = Number.isFinite(Number(process.env.GLOBAL_ENERGY_W2))
332
- ? Math.max(0, Math.min(2, Number(process.env.GLOBAL_ENERGY_W2)))
333
- : 0.25;
334
- const GLOBAL_ENERGY_W3 = Number.isFinite(Number(process.env.GLOBAL_ENERGY_W3))
335
- ? Math.max(0, Math.min(2, Number(process.env.GLOBAL_ENERGY_W3)))
336
- : 0.10;
337
- const GLOBAL_ENERGY_W4 = Number.isFinite(Number(process.env.GLOBAL_ENERGY_W4))
338
- ? Math.max(0, Math.min(2, Number(process.env.GLOBAL_ENERGY_W4)))
339
- : 0.15;
340
- const GLOBAL_ENERGY_W5 = Number.isFinite(Number(process.env.GLOBAL_ENERGY_W5))
341
- ? Math.max(0, Math.min(2, Number(process.env.GLOBAL_ENERGY_W5)))
342
- : 0.10;
343
- const PACK_TIER_GOLD_THRESHOLD = Number.isFinite(Number(process.env.PACK_TIER_GOLD_THRESHOLD))
344
- ? Math.max(0, Math.min(2, Number(process.env.PACK_TIER_GOLD_THRESHOLD)))
345
- : 0.80;
346
- const PACK_TIER_SILVER_THRESHOLD = Number.isFinite(Number(process.env.PACK_TIER_SILVER_THRESHOLD))
347
- ? Math.max(0, Math.min(PACK_TIER_GOLD_THRESHOLD, Number(process.env.PACK_TIER_SILVER_THRESHOLD)))
348
- : 0.65;
349
- const PACK_TIER_BRONZE_THRESHOLD = Number.isFinite(Number(process.env.PACK_TIER_BRONZE_THRESHOLD))
350
- ? Math.max(0, Math.min(PACK_TIER_SILVER_THRESHOLD, Number(process.env.PACK_TIER_BRONZE_THRESHOLD)))
351
- : 0.50;
352
-
353
- const EFFECTIVE_MIN_ASSET_ACCEPTANCE_RATE = IS_AGGRESSIVE_PROFILE
354
- ? Math.max(0.3, MIN_ASSET_ACCEPTANCE_RATE * 0.82)
355
- : MIN_ASSET_ACCEPTANCE_RATE;
356
- const EFFECTIVE_MIN_THEME_DOMINANCE_RATIO = IS_AGGRESSIVE_PROFILE
357
- ? Math.max(0.35, MIN_THEME_DOMINANCE_RATIO * 0.82)
358
- : MIN_THEME_DOMINANCE_RATIO;
359
- const EFFECTIVE_SCORE_STDDEV_PENALTY = IS_AGGRESSIVE_PROFILE
360
- ? Math.max(0.05, SCORE_STDDEV_PENALTY * 0.82)
361
- : SCORE_STDDEV_PENALTY;
362
- const EFFECTIVE_TRANSFER_THRESHOLD = IS_AGGRESSIVE_PROFILE
363
- ? Math.min(TRANSFER_THRESHOLD, AGGRESSIVE_MIGRATION_THRESHOLD)
364
- : TRANSFER_THRESHOLD;
365
- const EFFECTIVE_MERGE_THRESHOLD = IS_AGGRESSIVE_PROFILE
366
- ? Math.max(MERGE_THRESHOLD, 0.85)
367
- : MERGE_THRESHOLD;
145
+ const AGGRESSIVE_MIGRATION_THRESHOLD = Number.isFinite(Number(process.env.AGGRESSIVE_MIGRATION_THRESHOLD)) ? Math.max(-0.1, Math.min(1, Number(process.env.AGGRESSIVE_MIGRATION_THRESHOLD))) : 0.01;
146
+ const ARCHIVE_LOW_SCORE_PACKS = parseEnvBool(process.env.ARCHIVE_LOW_SCORE_PACKS, IS_AGGRESSIVE_PROFILE);
147
+ const GLOBAL_ENERGY_W1 = Number.isFinite(Number(process.env.GLOBAL_ENERGY_W1)) ? Math.max(0, Math.min(2, Number(process.env.GLOBAL_ENERGY_W1))) : 0.4;
148
+ const GLOBAL_ENERGY_W2 = Number.isFinite(Number(process.env.GLOBAL_ENERGY_W2)) ? Math.max(0, Math.min(2, Number(process.env.GLOBAL_ENERGY_W2))) : 0.25;
149
+ const GLOBAL_ENERGY_W3 = Number.isFinite(Number(process.env.GLOBAL_ENERGY_W3)) ? Math.max(0, Math.min(2, Number(process.env.GLOBAL_ENERGY_W3))) : 0.1;
150
+ const GLOBAL_ENERGY_W4 = Number.isFinite(Number(process.env.GLOBAL_ENERGY_W4)) ? Math.max(0, Math.min(2, Number(process.env.GLOBAL_ENERGY_W4))) : 0.15;
151
+ const GLOBAL_ENERGY_W5 = Number.isFinite(Number(process.env.GLOBAL_ENERGY_W5)) ? Math.max(0, Math.min(2, Number(process.env.GLOBAL_ENERGY_W5))) : 0.1;
152
+ const PACK_TIER_GOLD_THRESHOLD = Number.isFinite(Number(process.env.PACK_TIER_GOLD_THRESHOLD)) ? Math.max(0, Math.min(2, Number(process.env.PACK_TIER_GOLD_THRESHOLD))) : 0.8;
153
+ const PACK_TIER_SILVER_THRESHOLD = Number.isFinite(Number(process.env.PACK_TIER_SILVER_THRESHOLD)) ? Math.max(0, Math.min(PACK_TIER_GOLD_THRESHOLD, Number(process.env.PACK_TIER_SILVER_THRESHOLD))) : 0.65;
154
+ const PACK_TIER_BRONZE_THRESHOLD = Number.isFinite(Number(process.env.PACK_TIER_BRONZE_THRESHOLD)) ? Math.max(0, Math.min(PACK_TIER_SILVER_THRESHOLD, Number(process.env.PACK_TIER_BRONZE_THRESHOLD))) : 0.5;
155
+
156
+ const EFFECTIVE_MIN_ASSET_ACCEPTANCE_RATE = IS_AGGRESSIVE_PROFILE ? Math.max(0.3, MIN_ASSET_ACCEPTANCE_RATE * 0.82) : MIN_ASSET_ACCEPTANCE_RATE;
157
+ const EFFECTIVE_MIN_THEME_DOMINANCE_RATIO = IS_AGGRESSIVE_PROFILE ? Math.max(0.35, MIN_THEME_DOMINANCE_RATIO * 0.82) : MIN_THEME_DOMINANCE_RATIO;
158
+ const EFFECTIVE_SCORE_STDDEV_PENALTY = IS_AGGRESSIVE_PROFILE ? Math.max(0.05, SCORE_STDDEV_PENALTY * 0.82) : SCORE_STDDEV_PENALTY;
159
+ const EFFECTIVE_TRANSFER_THRESHOLD = IS_AGGRESSIVE_PROFILE ? Math.min(TRANSFER_THRESHOLD, AGGRESSIVE_MIGRATION_THRESHOLD) : TRANSFER_THRESHOLD;
160
+ const EFFECTIVE_MERGE_THRESHOLD = IS_AGGRESSIVE_PROFILE ? Math.max(MERGE_THRESHOLD, 0.85) : MERGE_THRESHOLD;
368
161
  const EFFECTIVE_INTER_PACK_SIMILARITY_THRESHOLD = Math.max(EFFECTIVE_MERGE_THRESHOLD, INTER_PACK_SIMILARITY_THRESHOLD);
369
- const EFFECTIVE_AUTO_ARCHIVE_BELOW_PERCENTILE = ARCHIVE_LOW_SCORE_PACKS && IS_AGGRESSIVE_PROFILE
370
- ? Math.max(AUTO_ARCHIVE_BELOW_PERCENTILE, 35)
371
- : AUTO_ARCHIVE_BELOW_PERCENTILE;
162
+ const EFFECTIVE_AUTO_ARCHIVE_BELOW_PERCENTILE = ARCHIVE_LOW_SCORE_PACKS && IS_AGGRESSIVE_PROFILE ? Math.max(AUTO_ARCHIVE_BELOW_PERCENTILE, 35) : AUTO_ARCHIVE_BELOW_PERCENTILE;
372
163
  const EFFECTIVE_PRIORITIZE_COMPLETION = PRIORITIZE_COMPLETION || IS_AGGRESSIVE_PROFILE;
373
164
  const EFFECTIVE_COMPLETION_TRANSFER_ENABLED = COMPLETION_TRANSFER_ENABLED || IS_AGGRESSIVE_PROFILE;
374
- const EFFECTIVE_MIGRATION_CANDIDATE_LIMIT = IS_AGGRESSIVE_PROFILE
375
- ? Math.max(MIGRATION_CANDIDATE_LIMIT, 24)
376
- : MIGRATION_CANDIDATE_LIMIT;
377
- const EFFECTIVE_TRANSFER_CANDIDATE_SIMILARITY_FLOOR = IS_AGGRESSIVE_PROFILE
378
- ? Math.max(0.15, TRANSFER_CANDIDATE_SIMILARITY_FLOOR * 0.72)
379
- : TRANSFER_CANDIDATE_SIMILARITY_FLOOR;
165
+ const EFFECTIVE_MIGRATION_CANDIDATE_LIMIT = IS_AGGRESSIVE_PROFILE ? Math.max(MIGRATION_CANDIDATE_LIMIT, 24) : MIGRATION_CANDIDATE_LIMIT;
166
+ const EFFECTIVE_TRANSFER_CANDIDATE_SIMILARITY_FLOOR = IS_AGGRESSIVE_PROFILE ? Math.max(0.15, TRANSFER_CANDIDATE_SIMILARITY_FLOOR * 0.72) : TRANSFER_CANDIDATE_SIMILARITY_FLOOR;
380
167
 
381
168
  const EXPLICIT_OWNER = String(process.env.STICKER_AUTO_PACK_OWNER_JID || process.env.USER_ADMIN || '').trim();
382
169
 
@@ -388,14 +175,7 @@ const LABEL_TO_TAG = {
388
175
  cartoon: 'cartoon',
389
176
  };
390
177
 
391
- const TECHNICAL_TAGS = new Set([
392
- 'low-quality-compressed-image',
393
- 'blurry-image',
394
- 'text-only-image',
395
- 'sticker-style-image',
396
- 'whatsapp-sticker-style',
397
- 'telegram-sticker-style',
398
- ]);
178
+ const TECHNICAL_TAGS = new Set(['low-quality-compressed-image', 'blurry-image', 'text-only-image', 'sticker-style-image', 'whatsapp-sticker-style', 'telegram-sticker-style']);
399
179
 
400
180
  const normalizeTag = (value) =>
401
181
  String(value || '')
@@ -416,7 +196,9 @@ const toNumericClusterId = (value) => {
416
196
  const isSemanticClusterSubthemeTag = (value) => /^cluster-\d+$/.test(normalizeTag(value));
417
197
 
418
198
  const toTagFromLabel = (label) => {
419
- const key = String(label || '').trim().toLowerCase();
199
+ const key = String(label || '')
200
+ .trim()
201
+ .toLowerCase();
420
202
  return LABEL_TO_TAG[key] || normalizeTag(key);
421
203
  };
422
204
 
@@ -476,7 +258,9 @@ const buildThemeKey = (theme, subtheme = '') => {
476
258
  return normalizedSubtheme ? `${normalizedTheme}:${normalizedSubtheme}` : normalizedTheme;
477
259
  };
478
260
  const parseThemeKey = (raw) => {
479
- const normalized = String(raw || '').trim().toLowerCase();
261
+ const normalized = String(raw || '')
262
+ .trim()
263
+ .toLowerCase();
480
264
  if (!normalized) return { theme: '', subtheme: '' };
481
265
  const [theme = '', subtheme = ''] = normalized.split(':', 2);
482
266
  return {
@@ -497,10 +281,7 @@ const buildAutoPackName = (theme, subtheme, index) => {
497
281
  return `${base} • Vol. ${index}`;
498
282
  };
499
283
  const buildAutoPackMarker = (themeKey) => `[auto-theme:${themeKey}]`;
500
- const buildAutoPackDescription = ({ theme, subtheme, themeKey, groupScore }) =>
501
- `${buildAutoPackMarker(themeKey)} Curadoria automática por tema. Tema: ${theme}${
502
- subtheme ? ` / ${subtheme}` : ''
503
- }. score=${Number(groupScore || 0).toFixed(4)}.`;
284
+ const buildAutoPackDescription = ({ theme, subtheme, themeKey, groupScore }) => `${buildAutoPackMarker(themeKey)} Curadoria automática por tema. Tema: ${theme}${subtheme ? ` / ${subtheme}` : ''}. score=${Number(groupScore || 0).toFixed(4)}.`;
504
285
 
505
286
  const clampNumber = (value, min, max) => Math.max(min, Math.min(max, Number(value)));
506
287
 
@@ -556,10 +337,12 @@ const meanArray = (values) => {
556
337
  const varianceArray = (values) => {
557
338
  if (!Array.isArray(values) || values.length <= 1) return 0;
558
339
  const mean = meanArray(values);
559
- return values.reduce((sum, value) => {
560
- const delta = Number(value || 0) - mean;
561
- return sum + delta * delta;
562
- }, 0) / values.length;
340
+ return (
341
+ values.reduce((sum, value) => {
342
+ const delta = Number(value || 0) - mean;
343
+ return sum + delta * delta;
344
+ }, 0) / values.length
345
+ );
563
346
  };
564
347
 
565
348
  const percentileValue = (values, percentile) => {
@@ -645,12 +428,7 @@ const dominantTagRatio = (stickerIds, classificationByAssetId) => {
645
428
  return best / total;
646
429
  };
647
430
 
648
- const computeStickerPackMatrixScore = ({
649
- stickerId,
650
- packStickerIds,
651
- classificationByAssetId,
652
- centroidVector,
653
- }) => {
431
+ const computeStickerPackMatrixScore = ({ stickerId, packStickerIds, classificationByAssetId, centroidVector }) => {
654
432
  const classification = classificationByAssetId.get(stickerId);
655
433
  if (!classification) return 0;
656
434
  const stickerVector = buildStickerFeatureVector(classification);
@@ -669,11 +447,13 @@ const computeStickerPackMatrixScore = ({
669
447
 
670
448
  const confidence = Number(classification.confidence || 0);
671
449
  const themeStrength = Number(buildTopTags(classification)[0]?.score || 0);
672
- const entropyStability = 1 - normalizeEntropy({
673
- entropy: classification.entropy,
674
- entropyNormalized: classification.entropy_normalized,
675
- topLabelCount: Array.isArray(classification.top_labels) ? classification.top_labels.length : 0,
676
- });
450
+ const entropyStability =
451
+ 1 -
452
+ normalizeEntropy({
453
+ entropy: classification.entropy,
454
+ entropyNormalized: classification.entropy_normalized,
455
+ topLabelCount: Array.isArray(classification.top_labels) ? classification.top_labels.length : 0,
456
+ });
677
457
  const affinityWeight = normalizeAffinityWeight(classification.affinity_weight);
678
458
  const impactOnGroupScore = confidence * 0.4 + themeStrength * 0.35 + entropyStability * 0.15 + affinityWeight * 0.1;
679
459
 
@@ -687,11 +467,7 @@ const computeStickerPackMatrixScore = ({
687
467
  duplicationPenalty = dupHits / Math.max(1, others.length);
688
468
  }
689
469
 
690
- const matrixScore =
691
- MATRIX_ALPHA * semanticSimilarity
692
- + MATRIX_BETA * cohesion
693
- + MATRIX_GAMMA * impactOnGroupScore
694
- - MATRIX_DELTA * duplicationPenalty;
470
+ const matrixScore = MATRIX_ALPHA * semanticSimilarity + MATRIX_BETA * cohesion + MATRIX_GAMMA * impactOnGroupScore - MATRIX_DELTA * duplicationPenalty;
695
471
 
696
472
  return clampNumber(matrixScore, 0, 1.6);
697
473
  };
@@ -724,11 +500,7 @@ const buildNormalizedZScoreMap = (valueByKey = new Map()) => {
724
500
  return normalized;
725
501
  };
726
502
 
727
- const computePackCohesionScore = (profile) => clampNumber(
728
- Number(profile?.semanticCohesion || 0) * 0.7 + Number(profile?.topicDominance || 0) * 0.3,
729
- 0,
730
- 1,
731
- );
503
+ const computePackCohesionScore = (profile) => clampNumber(Number(profile?.semanticCohesion || 0) * 0.7 + Number(profile?.topicDominance || 0) * 0.3, 0, 1);
732
504
 
733
505
  const computePackObjectiveScore = ({ profile, engagementScore = 0 }) => {
734
506
  const meanAssetQuality = clampNumber(Number(profile?.meanAssetQuality || 0), 0, 1);
@@ -736,14 +508,7 @@ const computePackObjectiveScore = ({ profile, engagementScore = 0 }) => {
736
508
  const volumeScore = clampNumber(Number(profile?.volumeScore || 0), 0, 1);
737
509
  const engagementComponent = clampNumber(Number(engagementScore || 0), 0, 1);
738
510
 
739
- return clampNumber(
740
- GLOBAL_ENERGY_W1 * meanAssetQuality
741
- + GLOBAL_ENERGY_W2 * cohesionScore
742
- + GLOBAL_ENERGY_W3 * volumeScore
743
- + GLOBAL_ENERGY_W4 * engagementComponent,
744
- 0,
745
- 2,
746
- );
511
+ return clampNumber(GLOBAL_ENERGY_W1 * meanAssetQuality + GLOBAL_ENERGY_W2 * cohesionScore + GLOBAL_ENERGY_W3 * volumeScore + GLOBAL_ENERGY_W4 * engagementComponent, 0, 2);
747
512
  };
748
513
 
749
514
  const computePackOfficialQualityScore = ({ profile, engagementZscore = 0 }) => {
@@ -752,12 +517,7 @@ const computePackOfficialQualityScore = ({ profile, engagementZscore = 0 }) => {
752
517
  const completionRatio = clampNumber(Number(profile?.volumeScore || 0), 0, 1);
753
518
  const stabilityIndex = clampNumber(1 - Math.max(0, Number(profile?.internalVariance || 0)), 0, 1);
754
519
  const engagementComponent = clampNumber(Number(engagementZscore || 0), 0, 1);
755
- const raw =
756
- PACK_TIER_QUALITY_W1 * meanAssetQuality
757
- + PACK_TIER_QUALITY_W2 * cohesionScore
758
- + PACK_TIER_QUALITY_W3 * engagementComponent
759
- + PACK_TIER_QUALITY_W4 * completionRatio
760
- + PACK_TIER_QUALITY_W5 * stabilityIndex;
520
+ const raw = PACK_TIER_QUALITY_W1 * meanAssetQuality + PACK_TIER_QUALITY_W2 * cohesionScore + PACK_TIER_QUALITY_W3 * engagementComponent + PACK_TIER_QUALITY_W4 * completionRatio + PACK_TIER_QUALITY_W5 * stabilityIndex;
761
521
  return clampNumber(raw, 0, 1.2);
762
522
  };
763
523
 
@@ -767,12 +527,10 @@ const buildPackPairKey = (leftPackId, rightPackId) => {
767
527
  return left < right ? `${left}::${right}` : `${right}::${left}`;
768
528
  };
769
529
 
770
- const computePackSemanticSimilarity = (leftProfile, rightProfile) =>
771
- clampNumber(cosineSimilarity(leftProfile?.centroidVector || {}, rightProfile?.centroidVector || {}), 0, 1);
530
+ const computePackSemanticSimilarity = (leftProfile, rightProfile) => clampNumber(cosineSimilarity(leftProfile?.centroidVector || {}, rightProfile?.centroidVector || {}), 0, 1);
772
531
 
773
532
  const buildInterPackSimilarityMatrix = (profiles) => {
774
- const profileList = Array.from((profiles instanceof Map ? profiles.values() : []))
775
- .filter((profile) => Array.isArray(profile?.stickerIds) && profile.stickerIds.length > 0);
533
+ const profileList = Array.from(profiles instanceof Map ? profiles.values() : []).filter((profile) => Array.isArray(profile?.stickerIds) && profile.stickerIds.length > 0);
776
534
  const matrix = new Map();
777
535
  let sum = 0;
778
536
  let count = 0;
@@ -812,12 +570,7 @@ const computeMeanNormalizedEntropy = (stickerIds, classificationByAssetId) => {
812
570
  return count > 0 ? total / count : 0;
813
571
  };
814
572
 
815
- const computePackEnergyDelta = ({
816
- baseEnergySnapshot,
817
- profiles,
818
- profileScores,
819
- changes,
820
- }) => {
573
+ const computePackEnergyDelta = ({ baseEnergySnapshot, profiles, profileScores, changes }) => {
821
574
  const updates = changes instanceof Map ? changes : new Map();
822
575
  if (!updates.size) {
823
576
  return {
@@ -880,12 +633,7 @@ const computePackEnergyDelta = ({
880
633
  };
881
634
  };
882
635
 
883
- const computePackProfile = ({
884
- packId,
885
- stickerIds,
886
- themeKey,
887
- classificationByAssetId,
888
- }) => {
636
+ const computePackProfile = ({ packId, stickerIds, themeKey, classificationByAssetId }) => {
889
637
  const cleanStickerIds = Array.from(new Set((Array.isArray(stickerIds) ? stickerIds : []).filter(Boolean)));
890
638
  const centroidVector = buildPackCentroidVector(cleanStickerIds, classificationByAssetId);
891
639
  const matrixScores = cleanStickerIds.map((stickerId) =>
@@ -894,14 +642,18 @@ const computePackProfile = ({
894
642
  packStickerIds: cleanStickerIds,
895
643
  classificationByAssetId,
896
644
  centroidVector,
897
- }));
645
+ }),
646
+ );
898
647
  const meanStickerScore = meanArray(matrixScores);
899
- const semanticCohesion = cleanStickerIds.length <= 1
900
- ? 1
901
- : meanArray(cleanStickerIds.map((stickerId) => {
902
- const classification = classificationByAssetId.get(stickerId);
903
- return cosineSimilarity(buildStickerFeatureVector(classification), centroidVector);
904
- }));
648
+ const semanticCohesion =
649
+ cleanStickerIds.length <= 1
650
+ ? 1
651
+ : meanArray(
652
+ cleanStickerIds.map((stickerId) => {
653
+ const classification = classificationByAssetId.get(stickerId);
654
+ return cosineSimilarity(buildStickerFeatureVector(classification), centroidVector);
655
+ }),
656
+ );
905
657
  const parsedThemeKey = parseThemeKey(themeKey);
906
658
  const meanAssetQuality = meanArray(
907
659
  cleanStickerIds.map((stickerId) => {
@@ -927,22 +679,13 @@ const computePackProfile = ({
927
679
  pairCount += 1;
928
680
  const leftClassification = classificationByAssetId.get(cleanStickerIds[i]);
929
681
  const rightClassification = classificationByAssetId.get(cleanStickerIds[j]);
930
- const sim = cosineSimilarity(
931
- buildStickerFeatureVector(leftClassification),
932
- buildStickerFeatureVector(rightClassification),
933
- );
682
+ const sim = cosineSimilarity(buildStickerFeatureVector(leftClassification), buildStickerFeatureVector(rightClassification));
934
683
  if (sim >= DEDUPE_SIMILARITY_THRESHOLD) duplicatePairs += 1;
935
684
  }
936
685
  }
937
686
  const duplicationRatio = pairCount > 0 ? duplicatePairs / pairCount : 0;
938
687
 
939
- const qualityRaw =
940
- PACK_QUALITY_W1 * meanStickerScore
941
- + PACK_QUALITY_W2 * semanticCohesion
942
- + PACK_QUALITY_W3 * topicDominance
943
- + PACK_QUALITY_W4 * volumeScore
944
- - PACK_QUALITY_W5 * internalVariance
945
- - PACK_QUALITY_W6 * duplicationRatio;
688
+ const qualityRaw = PACK_QUALITY_W1 * meanStickerScore + PACK_QUALITY_W2 * semanticCohesion + PACK_QUALITY_W3 * topicDominance + PACK_QUALITY_W4 * volumeScore - PACK_QUALITY_W5 * internalVariance - PACK_QUALITY_W6 * duplicationRatio;
946
689
  const packQuality = clampNumber(qualityRaw, 0, 2.5);
947
690
 
948
691
  return {
@@ -962,8 +705,16 @@ const computePackProfile = ({
962
705
  };
963
706
 
964
707
  const computePackOverlap = (leftProfile, rightProfile) => {
965
- const leftTags = new Set(String(leftProfile?.themeKey || '').split(':').filter(Boolean));
966
- const rightTags = new Set(String(rightProfile?.themeKey || '').split(':').filter(Boolean));
708
+ const leftTags = new Set(
709
+ String(leftProfile?.themeKey || '')
710
+ .split(':')
711
+ .filter(Boolean),
712
+ );
713
+ const rightTags = new Set(
714
+ String(rightProfile?.themeKey || '')
715
+ .split(':')
716
+ .filter(Boolean),
717
+ );
967
718
  const intersection = Array.from(leftTags).filter((tag) => rightTags.has(tag)).length;
968
719
  const union = new Set([...leftTags, ...rightTags]).size;
969
720
  const jaccard = union > 0 ? intersection / union : 0;
@@ -1221,15 +972,7 @@ const deriveThemeFromClassification = (classification) => {
1221
972
  const primary = semanticClusterSlug || categoryTag || fallbackPrimary || `cluster-${semanticClusterId}`;
1222
973
  const secondary = topTags.find((entry) => entry.tag !== primary && entry.tag !== 'nsfw')?.tag || '';
1223
974
  const semanticThemeKey = buildThemeKey(primary, `cluster-${semanticClusterId}`);
1224
- const semanticTopTags = withThemeInTopTags(
1225
- topTags,
1226
- primary,
1227
- Math.max(
1228
- Number(classification?.confidence || 0),
1229
- Number(topTags.find((entry) => entry.tag === primary)?.score || 0),
1230
- MOVE_IN_THEME_SCORE_THRESHOLD,
1231
- ),
1232
- );
975
+ const semanticTopTags = withThemeInTopTags(topTags, primary, Math.max(Number(classification?.confidence || 0), Number(topTags.find((entry) => entry.tag === primary)?.score || 0), MOVE_IN_THEME_SCORE_THRESHOLD));
1233
976
  return {
1234
977
  theme: primary,
1235
978
  subtheme: secondary,
@@ -1243,15 +986,7 @@ const deriveThemeFromClassification = (classification) => {
1243
986
 
1244
987
  const primary = (ENABLE_SEMANTIC_CLUSTERING ? categoryTag : '') || fallbackPrimary;
1245
988
  const secondary = topTags.find((entry) => entry.tag !== primary && entry.tag !== 'nsfw')?.tag || '';
1246
- const fallbackTopTags = withThemeInTopTags(
1247
- topTags,
1248
- primary,
1249
- Math.max(
1250
- Number(classification?.confidence || 0),
1251
- Number(topTags.find((entry) => entry.tag === primary)?.score || 0),
1252
- MOVE_IN_THEME_SCORE_THRESHOLD,
1253
- ),
1254
- );
989
+ const fallbackTopTags = withThemeInTopTags(topTags, primary, Math.max(Number(classification?.confidence || 0), Number(topTags.find((entry) => entry.tag === primary)?.score || 0), MOVE_IN_THEME_SCORE_THRESHOLD));
1255
990
  return {
1256
991
  theme: primary,
1257
992
  subtheme: secondary,
@@ -1269,11 +1004,15 @@ const dedupeCandidatesByEmbedding = (candidates, { embeddingByImageHash = new Ma
1269
1004
  const deduped = [];
1270
1005
  let dropped = 0;
1271
1006
  for (const candidate of candidates) {
1272
- const candidateImageHash = String(candidate?.classification?.image_hash || '').trim().toLowerCase();
1007
+ const candidateImageHash = String(candidate?.classification?.image_hash || '')
1008
+ .trim()
1009
+ .toLowerCase();
1273
1010
  const candidateDense = candidateImageHash ? embeddingByImageHash.get(candidateImageHash) : null;
1274
1011
  const candidateSparse = candidate?.classification?.all_scores || {};
1275
1012
  const isDuplicate = deduped.some((entry) => {
1276
- const entryImageHash = String(entry?.classification?.image_hash || '').trim().toLowerCase();
1013
+ const entryImageHash = String(entry?.classification?.image_hash || '')
1014
+ .trim()
1015
+ .toLowerCase();
1277
1016
  const entryDense = entryImageHash ? embeddingByImageHash.get(entryImageHash) : null;
1278
1017
  if (candidateDense && entryDense) {
1279
1018
  return cosineSimilarityDense(candidateDense, entryDense) >= DEDUPE_SIMILARITY_THRESHOLD;
@@ -1315,11 +1054,7 @@ const computeAssetQualityForTheme = ({ classification, theme, subtheme, topTags
1315
1054
  }, 0);
1316
1055
  const rankedThemeScore = Number((Array.isArray(topTags) ? topTags : []).find((entry) => entry.tag === theme)?.score || 0);
1317
1056
  const thematicAlignment = clampNumber(Math.max(rankedThemeScore, topLabelThemeScore), 0, 1.2);
1318
- const assetQuality =
1319
- ASSET_QUALITY_W1 * confidence
1320
- + ASSET_QUALITY_W2 * (1 - entropyNormalized)
1321
- + ASSET_QUALITY_W3 * affinityWeight
1322
- + ASSET_QUALITY_W4 * thematicAlignment;
1057
+ const assetQuality = ASSET_QUALITY_W1 * confidence + ASSET_QUALITY_W2 * (1 - entropyNormalized) + ASSET_QUALITY_W3 * affinityWeight + ASSET_QUALITY_W4 * thematicAlignment;
1323
1058
 
1324
1059
  return {
1325
1060
  confidence,
@@ -1331,11 +1066,7 @@ const computeAssetQualityForTheme = ({ classification, theme, subtheme, topTags
1331
1066
  };
1332
1067
 
1333
1068
  const scoreCandidate = ({ classification, theme, subtheme, topTags, qualityScore }) => {
1334
- const {
1335
- affinityWeight,
1336
- entropyNormalized,
1337
- assetQuality,
1338
- } = computeAssetQualityForTheme({ classification, theme, subtheme, topTags });
1069
+ const { affinityWeight, entropyNormalized, assetQuality } = computeAssetQualityForTheme({ classification, theme, subtheme, topTags });
1339
1070
  const confidenceMargin = Math.max(0, Number(classification?.confidence_margin || 0));
1340
1071
  const ambiguityPenalty = (classification?.ambiguous ? AMBIGUOUS_FLAG_PENALTY : 0) + entropyNormalized * ENTROPY_WEIGHT;
1341
1072
 
@@ -1348,13 +1079,7 @@ const scoreCandidate = ({ classification, theme, subtheme, topTags, qualityScore
1348
1079
 
1349
1080
  const adaptiveBonus = affinityWeight * ADAPTIVE_BONUS_WEIGHT;
1350
1081
  const marginBonus = confidenceMargin * MARGIN_BONUS_WEIGHT;
1351
- const finalScore =
1352
- assetQuality * 0.65
1353
- + Number(qualityScore || 0) * 0.2
1354
- + adaptiveBonus
1355
- + marginBonus
1356
- - ambiguityPenalty
1357
- - similarImagesPenalty;
1082
+ const finalScore = assetQuality * 0.65 + Number(qualityScore || 0) * 0.2 + adaptiveBonus + marginBonus - ambiguityPenalty - similarImagesPenalty;
1358
1083
  return Number(clampNumber(finalScore, 0, 1.2).toFixed(6));
1359
1084
  };
1360
1085
 
@@ -1455,25 +1180,7 @@ const collectCuratableCandidates = async ({ includePacked = true, includeUnpacke
1455
1180
  return current;
1456
1181
  };
1457
1182
 
1458
- const registerCandidate = ({
1459
- asset,
1460
- classification,
1461
- passIndex,
1462
- theme,
1463
- subtheme,
1464
- themeKey,
1465
- topTags,
1466
- qualityScore,
1467
- themeScore,
1468
- score,
1469
- nsfwScore,
1470
- nsfwLevel,
1471
- entropy,
1472
- confidenceMargin,
1473
- affinityWeight,
1474
- ambiguous,
1475
- similarPenalty,
1476
- }) => {
1183
+ const registerCandidate = ({ asset, classification, passIndex, theme, subtheme, themeKey, topTags, qualityScore, themeScore, score, nsfwScore, nsfwLevel, entropy, confidenceMargin, affinityWeight, ambiguous, similarPenalty }) => {
1477
1184
  const assetStats = ensureAssetStats(asset, classification);
1478
1185
  const themeStats = ensureThemeStats(assetStats, { themeKey, theme, subtheme });
1479
1186
  const passWeight = resolvePassScoreWeight(asset.id, passIndex);
@@ -1545,14 +1252,7 @@ const collectCuratableCandidates = async ({ includePacked = true, includeUnpacke
1545
1252
  continue;
1546
1253
  }
1547
1254
 
1548
- const {
1549
- theme,
1550
- subtheme,
1551
- topTags,
1552
- nsfwScore,
1553
- nsfwLevel,
1554
- semanticThemeKey = '',
1555
- } = deriveThemeFromClassification(classification);
1255
+ const { theme, subtheme, topTags, nsfwScore, nsfwLevel, semanticThemeKey = '' } = deriveThemeFromClassification(classification);
1556
1256
  if (!theme) {
1557
1257
  stats.assets_rejected_no_theme += 1;
1558
1258
  continue;
@@ -1598,10 +1298,7 @@ const collectCuratableCandidates = async ({ includePacked = true, includeUnpacke
1598
1298
  entropy: entropyNormalized,
1599
1299
  confidenceMargin: Number(classification?.confidence_margin || 0),
1600
1300
  affinityWeight: normalizeAffinityWeight(classification?.affinity_weight),
1601
- ambiguous:
1602
- classification?.ambiguous === true
1603
- || classification?.ambiguous === 1
1604
- || entropyNormalized > ENTROPY_NORMALIZED_THRESHOLD,
1301
+ ambiguous: classification?.ambiguous === true || classification?.ambiguous === 1 || entropyNormalized > ENTROPY_NORMALIZED_THRESHOLD,
1605
1302
  similarPenalty: maxSimilarity * SIMILAR_IMAGES_PENALTY_WEIGHT,
1606
1303
  });
1607
1304
  }
@@ -1621,9 +1318,7 @@ const collectCuratableCandidates = async ({ includePacked = true, includeUnpacke
1621
1318
 
1622
1319
  if (REVIEW_VERSION_TARGET) {
1623
1320
  while (scannedInPass < effectiveCapPerPass) {
1624
- const remaining = Number.isFinite(effectiveCapPerPass)
1625
- ? Math.max(0, effectiveCapPerPass - scannedInPass)
1626
- : pageLimit;
1321
+ const remaining = Number.isFinite(effectiveCapPerPass) ? Math.max(0, effectiveCapPerPass - scannedInPass) : pageLimit;
1627
1322
  const limit = Math.max(1, Math.min(pageLimit, remaining || pageLimit));
1628
1323
  const page = await listClassifiedStickerAssetsForCuration({
1629
1324
  limit,
@@ -1645,9 +1340,7 @@ const collectCuratableCandidates = async ({ includePacked = true, includeUnpacke
1645
1340
  }
1646
1341
 
1647
1342
  while (scannedInPass < effectiveCapPerPass) {
1648
- const remaining = Number.isFinite(effectiveCapPerPass)
1649
- ? Math.max(0, effectiveCapPerPass - scannedInPass)
1650
- : pageLimit;
1343
+ const remaining = Number.isFinite(effectiveCapPerPass) ? Math.max(0, effectiveCapPerPass - scannedInPass) : pageLimit;
1651
1344
  const limit = Math.max(1, Math.min(pageLimit, remaining || pageLimit));
1652
1345
  const page = await listClassifiedStickerAssetsForCuration({
1653
1346
  limit,
@@ -1678,8 +1371,7 @@ const collectCuratableCandidates = async ({ includePacked = true, includeUnpacke
1678
1371
  stats.pass_assets_unique.push(passResult.uniqueSeenInPass);
1679
1372
  }
1680
1373
 
1681
- const meanPassScan =
1682
- stats.pass_assets_scanned.reduce((sum, value) => sum + Number(value || 0), 0) / Math.max(1, stats.pass_assets_scanned.length);
1374
+ const meanPassScan = stats.pass_assets_scanned.reduce((sum, value) => sum + Number(value || 0), 0) / Math.max(1, stats.pass_assets_scanned.length);
1683
1375
  stats.assets_scanned_avg_per_pass = Number(meanPassScan.toFixed(2));
1684
1376
  stats.assets_unique_scanned = globalSeenAssetIds.size;
1685
1377
 
@@ -1687,15 +1379,13 @@ const collectCuratableCandidates = async ({ includePacked = true, includeUnpacke
1687
1379
  const themeEntries = Array.from(assetStats.themes.values());
1688
1380
  if (!themeEntries.length) continue;
1689
1381
 
1690
- const dominantTheme = themeEntries
1691
- .slice()
1692
- .sort((left, right) => {
1693
- if (right.votes !== left.votes) return right.votes - left.votes;
1694
- const leftMean = left.scoreSum / Math.max(1, left.votes);
1695
- const rightMean = right.scoreSum / Math.max(1, right.votes);
1696
- if (rightMean !== leftMean) return rightMean - leftMean;
1697
- return right.themeScoreSum - left.themeScoreSum;
1698
- })[0];
1382
+ const dominantTheme = themeEntries.slice().sort((left, right) => {
1383
+ if (right.votes !== left.votes) return right.votes - left.votes;
1384
+ const leftMean = left.scoreSum / Math.max(1, left.votes);
1385
+ const rightMean = right.scoreSum / Math.max(1, right.votes);
1386
+ if (rightMean !== leftMean) return rightMean - leftMean;
1387
+ return right.themeScoreSum - left.themeScoreSum;
1388
+ })[0];
1699
1389
 
1700
1390
  if (!dominantTheme || dominantTheme.votes <= 0) continue;
1701
1391
 
@@ -1727,28 +1417,13 @@ const collectCuratableCandidates = async ({ includePacked = true, includeUnpacke
1727
1417
  const avgSimilarPenalty = dominantTheme.similarPenaltySum / acceptedVotes;
1728
1418
  const ambiguousRatio = dominantTheme.ambiguityVotes / acceptedVotes;
1729
1419
  const stabilityFactor = Math.sqrt(clampNumber(acceptanceRate, 0, 1));
1730
- const harmonicSignal = 3 / (
1731
- (1 / Math.max(0.01, avgQuality))
1732
- + (1 / Math.max(0.01, avgThemeScore))
1733
- + (1 / Math.max(0.01, avgConfidence))
1734
- );
1735
- const robustScoreRaw =
1736
- lowerBoundScore * 0.46
1737
- + meanScore * 0.16
1738
- + avgThemeScore * 0.14
1739
- + avgConfidence * 0.08
1740
- + harmonicSignal * 0.16
1741
- + avgConfidenceMargin * MARGIN_BONUS_WEIGHT * 0.45
1742
- + avgAffinityWeight * ADAPTIVE_BONUS_WEIGHT * 0.4
1743
- - Math.min(0.42, avgEntropy * ENTROPY_WEIGHT)
1744
- - ambiguousRatio * AMBIGUOUS_FLAG_PENALTY
1745
- - avgSimilarPenalty * 0.5;
1420
+ const harmonicSignal = 3 / (1 / Math.max(0.01, avgQuality) + 1 / Math.max(0.01, avgThemeScore) + 1 / Math.max(0.01, avgConfidence));
1421
+ const robustScoreRaw = lowerBoundScore * 0.46 + meanScore * 0.16 + avgThemeScore * 0.14 + avgConfidence * 0.08 + harmonicSignal * 0.16 + avgConfidenceMargin * MARGIN_BONUS_WEIGHT * 0.45 + avgAffinityWeight * ADAPTIVE_BONUS_WEIGHT * 0.4 - Math.min(0.42, avgEntropy * ENTROPY_WEIGHT) - ambiguousRatio * AMBIGUOUS_FLAG_PENALTY - avgSimilarPenalty * 0.5;
1746
1422
  const stabilityMultiplier = 0.72 + stabilityFactor * 0.28;
1747
1423
  const variancePenalty = Math.max(0.4, 1 - Math.min(0.55, stdDev * EFFECTIVE_SCORE_STDDEV_PENALTY));
1748
1424
  const robustScore = clampNumber(robustScoreRaw * stabilityMultiplier * variancePenalty, 0, 1.2);
1749
1425
 
1750
- const dominantSubtheme = Array.from(dominantTheme.subthemeVotes.entries())
1751
- .sort((left, right) => right[1] - left[1])[0]?.[0] || dominantTheme.subtheme || '';
1426
+ const dominantSubtheme = Array.from(dominantTheme.subthemeVotes.entries()).sort((left, right) => right[1] - left[1])[0]?.[0] || dominantTheme.subtheme || '';
1752
1427
  const topTags = buildTopTags(assetStats.classification);
1753
1428
  if (!topTags.some((entry) => entry?.tag === dominantTheme.theme)) {
1754
1429
  topTags.unshift({ tag: dominantTheme.theme, score: avgThemeScore });
@@ -1792,7 +1467,11 @@ const collectCuratableCandidates = async ({ includePacked = true, includeUnpacke
1792
1467
  new Set(
1793
1468
  Array.from(grouped.values())
1794
1469
  .flat()
1795
- .map((candidate) => String(candidate?.classification?.image_hash || '').trim().toLowerCase())
1470
+ .map((candidate) =>
1471
+ String(candidate?.classification?.image_hash || '')
1472
+ .trim()
1473
+ .toLowerCase(),
1474
+ )
1796
1475
  .filter((hash) => hash.length === 64),
1797
1476
  ),
1798
1477
  );
@@ -1800,7 +1479,9 @@ const collectCuratableCandidates = async ({ includePacked = true, includeUnpacke
1800
1479
  if (imageHashes.length) {
1801
1480
  const rows = await listClipImageEmbeddingsByImageHashes(imageHashes);
1802
1481
  for (const row of rows) {
1803
- const imageHash = String(row?.image_hash || '').trim().toLowerCase();
1482
+ const imageHash = String(row?.image_hash || '')
1483
+ .trim()
1484
+ .toLowerCase();
1804
1485
  if (!imageHash) continue;
1805
1486
  const embedding = parseFloat32EmbeddingBuffer(row?.embedding, Number(row?.embedding_dim || 0));
1806
1487
  if (!embedding?.length) continue;
@@ -1841,20 +1522,20 @@ const computeGroupMetrics = (themeKey, candidates) => {
1841
1522
  return { theme, subtheme, themeKey, groupScore: 0, cohesion: 0, avgConfidence: 0, avgQuality: 0 };
1842
1523
  }
1843
1524
 
1844
- const avgConfidence =
1845
- candidates.reduce((sum, candidate) => sum + Number(candidate.classification?.confidence || 0), 0) / size;
1846
- const avgEntropy = candidates.reduce((sum, candidate) => (
1847
- sum
1848
- + normalizeEntropy({
1849
- entropy: candidate.classification?.entropy,
1850
- entropyNormalized: candidate.classification?.entropy_normalized,
1851
- topLabelCount: Array.isArray(candidate.classification?.top_labels) ? candidate.classification.top_labels.length : 0,
1852
- })
1853
- ), 0) / size;
1525
+ const avgConfidence = candidates.reduce((sum, candidate) => sum + Number(candidate.classification?.confidence || 0), 0) / size;
1526
+ const avgEntropy =
1527
+ candidates.reduce(
1528
+ (sum, candidate) =>
1529
+ sum +
1530
+ normalizeEntropy({
1531
+ entropy: candidate.classification?.entropy,
1532
+ entropyNormalized: candidate.classification?.entropy_normalized,
1533
+ topLabelCount: Array.isArray(candidate.classification?.top_labels) ? candidate.classification.top_labels.length : 0,
1534
+ }),
1535
+ 0,
1536
+ ) / size;
1854
1537
  const avgMargin = candidates.reduce((sum, candidate) => sum + Number(candidate.classification?.confidence_margin || 0), 0) / size;
1855
- const avgAffinity = candidates.reduce((sum, candidate) => (
1856
- sum + normalizeAffinityWeight(candidate.classification?.affinity_weight)
1857
- ), 0) / size;
1538
+ const avgAffinity = candidates.reduce((sum, candidate) => sum + normalizeAffinityWeight(candidate.classification?.affinity_weight), 0) / size;
1858
1539
  const avgQuality = candidates.reduce((sum, candidate) => sum + Number(candidate.qualityScore || 0), 0) / size;
1859
1540
  const avgDuplicateRate = candidates.reduce((sum, candidate) => sum + Number(candidate.duplicateRate || 0), 0) / size;
1860
1541
  let semanticSimilaritySum = 0;
@@ -1868,9 +1549,7 @@ const computeGroupMetrics = (themeKey, candidates) => {
1868
1549
  const semanticCohesion = semanticPairs > 0 ? semanticSimilaritySum / semanticPairs : 1;
1869
1550
  const cooccurrence = new Map();
1870
1551
  for (const candidate of candidates) {
1871
- const localSecondary = (candidate.topTags || [])
1872
- .map((entry) => entry.tag)
1873
- .find((tag) => tag && tag !== theme && tag !== 'nsfw');
1552
+ const localSecondary = (candidate.topTags || []).map((entry) => entry.tag).find((tag) => tag && tag !== theme && tag !== 'nsfw');
1874
1553
  if (!localSecondary) continue;
1875
1554
  cooccurrence.set(localSecondary, (cooccurrence.get(localSecondary) || 0) + 1);
1876
1555
  }
@@ -1883,9 +1562,7 @@ const computeGroupMetrics = (themeKey, candidates) => {
1883
1562
  traitVotes.set(token, (traitVotes.get(token) || 0) + 1);
1884
1563
  }
1885
1564
  }
1886
- const strongestTraitRatio = size
1887
- ? Math.max(0, ...Array.from(traitVotes.values()).map((value) => value / size))
1888
- : 0;
1565
+ const strongestTraitRatio = size ? Math.max(0, ...Array.from(traitVotes.values()).map((value) => value / size)) : 0;
1889
1566
  const semanticBoost = strongestTraitRatio * LLM_TRAIT_WEIGHT;
1890
1567
  const subthemeFromCooccurrence = Array.from(cooccurrence.entries()).sort((left, right) => right[1] - left[1])[0]?.[0] || subtheme;
1891
1568
  const volumeBoost = Math.min(1, size / Math.max(MIN_GROUP_SIZE, TARGET_PACK_SIZE));
@@ -1893,17 +1570,7 @@ const computeGroupMetrics = (themeKey, candidates) => {
1893
1570
  const entropyPenalty = Math.min(0.35, avgEntropy * ENTROPY_WEIGHT);
1894
1571
  const marginBoost = Math.max(0, avgMargin * MARGIN_BONUS_WEIGHT);
1895
1572
  const affinityBoost = Math.max(0, avgAffinity * ADAPTIVE_BONUS_WEIGHT);
1896
- const groupScore = Number(
1897
- (
1898
- avgConfidence
1899
- * (0.55 + cohesion * 0.45 + semanticBoost)
1900
- * avgQuality
1901
- * (0.75 + volumeBoost * 0.25 + marginBoost)
1902
- * duplicatePenalty
1903
- * (1 + affinityBoost)
1904
- * Math.max(0.45, 1 - entropyPenalty)
1905
- ).toFixed(6),
1906
- );
1573
+ const groupScore = Number((avgConfidence * (0.55 + cohesion * 0.45 + semanticBoost) * avgQuality * (0.75 + volumeBoost * 0.25 + marginBoost) * duplicatePenalty * (1 + affinityBoost) * Math.max(0.45, 1 - entropyPenalty)).toFixed(6));
1907
1574
 
1908
1575
  return {
1909
1576
  theme,
@@ -2128,8 +1795,7 @@ const runRetroConsolidationCycle = async ({ ownerPool }) => {
2128
1795
  let themeLimitReached = false;
2129
1796
  let mutationLimitReached = false;
2130
1797
 
2131
- const themeEntries = Array.from(packsByTheme.entries())
2132
- .sort((left, right) => right[1].length - left[1].length);
1798
+ const themeEntries = Array.from(packsByTheme.entries()).sort((left, right) => right[1].length - left[1].length);
2133
1799
 
2134
1800
  for (const [themeKey, themePacksRaw] of themeEntries) {
2135
1801
  if (processedThemes >= RETRO_CONSOLIDATION_THEME_LIMIT) {
@@ -2161,17 +1827,12 @@ const runRetroConsolidationCycle = async ({ ownerPool }) => {
2161
1827
 
2162
1828
  const anchorPack = themePacks[0];
2163
1829
  const anchorItemsInitial = itemsByPackId.get(anchorPack.id) || [];
2164
- const donorPacks = themePacks.filter((pack) => pack.id !== anchorPack.id && (
2165
- smallPacks.some((entry) => entry.id === pack.id) || overflowPacks.some((entry) => entry.id === pack.id)
2166
- ));
2167
-
2168
- const desiredAnchorIds = Array.from(new Set([
2169
- ...anchorItemsInitial.map((item) => item.sticker_id).filter(Boolean),
2170
- ...donorPacks.flatMap((pack) => (itemsByPackId.get(pack.id) || []).map((item) => item.sticker_id).filter(Boolean)),
2171
- ])).slice(0, TARGET_PACK_SIZE);
1830
+ const donorPacks = themePacks.filter((pack) => pack.id !== anchorPack.id && (smallPacks.some((entry) => entry.id === pack.id) || overflowPacks.some((entry) => entry.id === pack.id)));
1831
+
1832
+ const desiredAnchorIds = Array.from(new Set([...anchorItemsInitial.map((item) => item.sticker_id).filter(Boolean), ...donorPacks.flatMap((pack) => (itemsByPackId.get(pack.id) || []).map((item) => item.sticker_id).filter(Boolean))])).slice(0, TARGET_PACK_SIZE);
2172
1833
  const desiredAnchorSet = new Set(desiredAnchorIds);
2173
1834
 
2174
- let anchorItems = itemsByPackId.get(anchorPack.id) || await listStickerPackItems(anchorPack.id);
1835
+ let anchorItems = itemsByPackId.get(anchorPack.id) || (await listStickerPackItems(anchorPack.id));
2175
1836
  const anchorCurrentSet = new Set(anchorItems.map((item) => item.sticker_id).filter(Boolean));
2176
1837
 
2177
1838
  for (const item of anchorItems) {
@@ -2235,10 +1896,7 @@ const runRetroConsolidationCycle = async ({ ownerPool }) => {
2235
1896
  itemsByPackId.set(anchorPack.id, anchorItems);
2236
1897
  const anchorOrder = anchorItems.map((item) => item.sticker_id);
2237
1898
  const finalDesiredOrder = desiredAnchorIds.filter((stickerId) => anchorOrder.includes(stickerId));
2238
- const needsReorder =
2239
- finalDesiredOrder.length > 1 &&
2240
- (finalDesiredOrder.length !== anchorOrder.length
2241
- || finalDesiredOrder.some((stickerId, index) => anchorOrder[index] !== stickerId));
1899
+ const needsReorder = finalDesiredOrder.length > 1 && (finalDesiredOrder.length !== anchorOrder.length || finalDesiredOrder.some((stickerId, index) => anchorOrder[index] !== stickerId));
2242
1900
  if (needsReorder) {
2243
1901
  try {
2244
1902
  await stickerPackService.reorderPackItems({
@@ -2275,7 +1933,7 @@ const runRetroConsolidationCycle = async ({ ownerPool }) => {
2275
1933
  }
2276
1934
  }
2277
1935
 
2278
- anchorItems = itemsByPackId.get(anchorPack.id) || await listStickerPackItems(anchorPack.id);
1936
+ anchorItems = itemsByPackId.get(anchorPack.id) || (await listStickerPackItems(anchorPack.id));
2279
1937
  const anchorCount = anchorItems.length;
2280
1938
  if (anchorCount < HARD_MIN_PACK_ITEMS) {
2281
1939
  if (mutations < RETRO_CONSOLIDATION_MUTATION_LIMIT) {
@@ -2331,12 +1989,7 @@ const runRetroConsolidationCycle = async ({ ownerPool }) => {
2331
1989
  };
2332
1990
  };
2333
1991
 
2334
- const optimizePackEcosystem = ({
2335
- operations,
2336
- itemsByPackId,
2337
- classificationByAssetId,
2338
- packEngagementByPackId = new Map(),
2339
- }) => {
1992
+ const optimizePackEcosystem = ({ operations, itemsByPackId, classificationByAssetId, packEngagementByPackId = new Map() }) => {
2340
1993
  if (!ENABLE_GLOBAL_OPTIMIZATION) {
2341
1994
  return {
2342
1995
  enabled: false,
@@ -2360,9 +2013,7 @@ const optimizePackEcosystem = ({
2360
2013
  };
2361
2014
  }
2362
2015
 
2363
- const scopedOps = (Array.isArray(operations) ? operations : [])
2364
- .filter((op) => op?.type === 'reconcile_volume' && op?.existingPackId)
2365
- .sort((left, right) => String(left.sort_key || '').localeCompare(String(right.sort_key || '')));
2016
+ const scopedOps = (Array.isArray(operations) ? operations : []).filter((op) => op?.type === 'reconcile_volume' && op?.existingPackId).sort((left, right) => String(left.sort_key || '').localeCompare(String(right.sort_key || '')));
2366
2017
 
2367
2018
  if (!scopedOps.length) {
2368
2019
  return {
@@ -2391,44 +2042,50 @@ const optimizePackEcosystem = ({
2391
2042
  scopedOps.map((op) => {
2392
2043
  const initialItems = itemsByPackId.get(op.existingPackId) || [];
2393
2044
  const stickers = new Set(initialItems.map((item) => item?.sticker_id).filter(Boolean));
2394
- return [op.existingPackId, {
2395
- packId: op.existingPackId,
2396
- op,
2397
- themeKey: String(op.themeKey || ''),
2398
- stickers,
2399
- tier: 'BRONZE',
2400
- }];
2045
+ return [
2046
+ op.existingPackId,
2047
+ {
2048
+ packId: op.existingPackId,
2049
+ op,
2050
+ themeKey: String(op.themeKey || ''),
2051
+ stickers,
2052
+ tier: 'BRONZE',
2053
+ },
2054
+ ];
2401
2055
  }),
2402
2056
  );
2403
2057
 
2404
2058
  const computeProfiles = () => {
2405
2059
  const profiles = new Map();
2406
2060
  for (const [packId, state] of stateByPackId.entries()) {
2407
- profiles.set(packId, computePackProfile({
2061
+ profiles.set(
2408
2062
  packId,
2409
- stickerIds: Array.from(state.stickers),
2410
- themeKey: state.themeKey,
2411
- classificationByAssetId,
2412
- }));
2063
+ computePackProfile({
2064
+ packId,
2065
+ stickerIds: Array.from(state.stickers),
2066
+ themeKey: state.themeKey,
2067
+ classificationByAssetId,
2068
+ }),
2069
+ );
2413
2070
  }
2414
2071
  return profiles;
2415
2072
  };
2416
2073
 
2417
2074
  const getEngagementScore = (packId) => computePackEngagementScore(packEngagementByPackId.get(packId));
2418
- const engagementScoreByPackId = new Map(
2419
- Array.from(stateByPackId.keys()).map((packId) => [packId, Number(getEngagementScore(packId) || 0)]),
2420
- );
2075
+ const engagementScoreByPackId = new Map(Array.from(stateByPackId.keys()).map((packId) => [packId, Number(getEngagementScore(packId) || 0)]));
2421
2076
  const engagementZscoreByPackId = buildNormalizedZScoreMap(engagementScoreByPackId);
2422
2077
  const getEngagementZscore = (packId) => Number(engagementZscoreByPackId.get(packId) || 0);
2423
2078
 
2424
- const scoreProfile = (profile) => computePackObjectiveScore({
2425
- profile,
2426
- engagementScore: Number(getEngagementScore(profile?.packId) || 0),
2427
- });
2428
- const scoreQualityProfile = (profile) => computePackOfficialQualityScore({
2429
- profile,
2430
- engagementZscore: getEngagementZscore(profile?.packId),
2431
- });
2079
+ const scoreProfile = (profile) =>
2080
+ computePackObjectiveScore({
2081
+ profile,
2082
+ engagementScore: Number(getEngagementScore(profile?.packId) || 0),
2083
+ });
2084
+ const scoreQualityProfile = (profile) =>
2085
+ computePackOfficialQualityScore({
2086
+ profile,
2087
+ engagementZscore: getEngagementZscore(profile?.packId),
2088
+ });
2432
2089
 
2433
2090
  const buildProfileScoreMap = (profiles) => {
2434
2091
  const map = new Map();
@@ -2585,10 +2242,7 @@ const optimizePackEcosystem = ({
2585
2242
  if (!sourceProfile || !Number.isFinite(sourceScore) || !Number.isFinite(sourceQuality)) continue;
2586
2243
 
2587
2244
  const recipientCandidates = packStates
2588
- .filter((recipient) =>
2589
- recipient.packId !== source.packId
2590
- && !recipient.stickers.has(stickerId)
2591
- && recipient.stickers.size < TARGET_PACK_SIZE)
2245
+ .filter((recipient) => recipient.packId !== source.packId && !recipient.stickers.has(stickerId) && recipient.stickers.size < TARGET_PACK_SIZE)
2592
2246
  .map((recipient) => {
2593
2247
  const recipientProfile = profiles.get(recipient.packId);
2594
2248
  const recipientScore = Number(profileScores.get(recipient.packId) || 0);
@@ -2601,10 +2255,7 @@ const optimizePackEcosystem = ({
2601
2255
  if (!sameTheme && semanticSimilarity < EFFECTIVE_TRANSFER_CANDIDATE_SIMILARITY_FLOOR) {
2602
2256
  return null;
2603
2257
  }
2604
- const candidateRank =
2605
- semanticSimilarity * 0.85
2606
- + (sameTheme ? 0.15 : 0)
2607
- + Math.max(0, recipientQuality - sourceQuality) * 0.1;
2258
+ const candidateRank = semanticSimilarity * 0.85 + (sameTheme ? 0.15 : 0) + Math.max(0, recipientQuality - sourceQuality) * 0.1;
2608
2259
  return {
2609
2260
  recipient,
2610
2261
  recipientScore,
@@ -2810,9 +2461,7 @@ const optimizePackEcosystem = ({
2810
2461
  });
2811
2462
  const nextScore = Number(scoreProfile(nextProfile).toFixed(6));
2812
2463
  const nextQuality = Number(scoreQualityProfile(nextProfile).toFixed(6));
2813
- const changes = new Map([
2814
- [state.packId, { profile: nextProfile, score: nextScore }],
2815
- ]);
2464
+ const changes = new Map([[state.packId, { profile: nextProfile, score: nextScore }]]);
2816
2465
  const deltaPreview = computePackEnergyDelta({
2817
2466
  baseEnergySnapshot: energySnapshot,
2818
2467
  profiles,
@@ -2889,9 +2538,7 @@ const optimizePackEcosystem = ({
2889
2538
  const desiredSet = new Set(op.desiredAssetIds);
2890
2539
  const removedByOptimization = Array.from(currentSet).filter((stickerId) => !desiredSet.has(stickerId));
2891
2540
  if (removedByOptimization.length) {
2892
- op.forceRemoveAssetIds = Array.from(
2893
- new Set([...(Array.isArray(op.forceRemoveAssetIds) ? op.forceRemoveAssetIds : []), ...removedByOptimization]),
2894
- );
2541
+ op.forceRemoveAssetIds = Array.from(new Set([...(Array.isArray(op.forceRemoveAssetIds) ? op.forceRemoveAssetIds : []), ...removedByOptimization]));
2895
2542
  }
2896
2543
  if (state.tier === 'ARCHIVE') {
2897
2544
  op.desiredAssetIds = [];
@@ -2902,9 +2549,7 @@ const optimizePackEcosystem = ({
2902
2549
  }
2903
2550
  }
2904
2551
 
2905
- const entropyMeanGlobal = Number(
2906
- computeMeanNormalizedEntropy(collectStickerIds(), classificationByAssetId).toFixed(6),
2907
- );
2552
+ const entropyMeanGlobal = Number(computeMeanNormalizedEntropy(collectStickerIds(), classificationByAssetId).toFixed(6));
2908
2553
 
2909
2554
  return {
2910
2555
  enabled: true,
@@ -2979,15 +2624,8 @@ const buildCurationExecutionPlan = async ({ curatedGroups, ownerPool, enableAddi
2979
2624
  return extractVolumeFromPack(left) - extractVolumeFromPack(right);
2980
2625
  });
2981
2626
  const retainedThemePacks = rankedThemePacks.slice(0, MAX_PACKS_PER_THEME);
2982
- const packCountById = new Map(
2983
- retainedThemePacks.map((pack) => [pack.id, Number(itemsByPackId.get(pack.id)?.length || 0)]),
2984
- );
2985
- const packItemsById = new Map(
2986
- retainedThemePacks.map((pack) => [
2987
- pack.id,
2988
- new Set((itemsByPackId.get(pack.id) || []).map((item) => item.sticker_id).filter(Boolean)),
2989
- ]),
2990
- );
2627
+ const packCountById = new Map(retainedThemePacks.map((pack) => [pack.id, Number(itemsByPackId.get(pack.id)?.length || 0)]));
2628
+ const packItemsById = new Map(retainedThemePacks.map((pack) => [pack.id, new Set((itemsByPackId.get(pack.id) || []).map((item) => item.sticker_id).filter(Boolean))]));
2991
2629
  const incompleteExistingCount = retainedThemePacks.reduce((sum, pack) => {
2992
2630
  const count = Number(packCountById.get(pack.id) || 0);
2993
2631
  return sum + (count < TARGET_PACK_SIZE ? 1 : 0);
@@ -3001,19 +2639,14 @@ const buildCurationExecutionPlan = async ({ curatedGroups, ownerPool, enableAddi
3001
2639
  return extractVolumeFromPack(left) - extractVolumeFromPack(right);
3002
2640
  })
3003
2641
  : [];
3004
- const volumeCandidateChunks = chunkArray(
3005
- group.candidates.map((candidate) => candidate.asset.id).filter(Boolean),
3006
- TARGET_PACK_SIZE,
3007
- );
2642
+ const volumeCandidateChunks = chunkArray(group.candidates.map((candidate) => candidate.asset.id).filter(Boolean), TARGET_PACK_SIZE);
3008
2643
  const groupFillAssetIds = group.candidates.map((candidate) => candidate.asset.id).filter(Boolean);
3009
2644
 
3010
2645
  const ownerCreateCapacity = ownerStates.reduce((sum, owner) => sum + Math.max(0, Number(owner.available || 0)), 0);
3011
2646
  const hasIncompleteExisting = incompleteExistingCount > 0;
3012
2647
  const themeCreateCapacity = Math.max(0, MAX_PACKS_PER_THEME - retainedThemePacks.length);
3013
2648
  const globalCreateCapacity = Math.max(0, GLOBAL_AUTO_PACK_LIMIT - (autoPacks.length + plannedCreates));
3014
- const maxCreatableForGroup = enableAdditions && !hasIncompleteExisting
3015
- ? Math.max(0, Math.min(ownerCreateCapacity, themeCreateCapacity, globalCreateCapacity))
3016
- : 0;
2649
+ const maxCreatableForGroup = enableAdditions && !hasIncompleteExisting ? Math.max(0, Math.min(ownerCreateCapacity, themeCreateCapacity, globalCreateCapacity)) : 0;
3017
2650
  if (enableAdditions && !hasIncompleteExisting && themeCreateCapacity > 0 && globalCreateCapacity <= 0) {
3018
2651
  creationBlockedByGlobalCap += 1;
3019
2652
  }
@@ -3035,14 +2668,7 @@ const buildCurationExecutionPlan = async ({ curatedGroups, ownerPool, enableAddi
3035
2668
 
3036
2669
  for (let volumeIndex = 0; volumeIndex < targetVolumeCount; volumeIndex += 1) {
3037
2670
  const volume = volumeIndex + 1;
3038
- const existingPack = prioritizeGroupCompletion
3039
- ? completionPriorityPacks[volumeIndex]
3040
- || retainedThemePacks.find((pack) => extractVolumeFromPack(pack) === volume)
3041
- || retainedThemePacks[volumeIndex]
3042
- || null
3043
- : retainedThemePacks.find((pack) => extractVolumeFromPack(pack) === volume)
3044
- || retainedThemePacks[volumeIndex]
3045
- || null;
2671
+ const existingPack = prioritizeGroupCompletion ? completionPriorityPacks[volumeIndex] || retainedThemePacks.find((pack) => extractVolumeFromPack(pack) === volume) || retainedThemePacks[volumeIndex] || null : retainedThemePacks.find((pack) => extractVolumeFromPack(pack) === volume) || retainedThemePacks[volumeIndex] || null;
3046
2672
  if (existingPack?.id) {
3047
2673
  selectedExistingPackIds.add(existingPack.id);
3048
2674
  }
@@ -3078,9 +2704,7 @@ const buildCurationExecutionPlan = async ({ curatedGroups, ownerPool, enableAddi
3078
2704
  fillAssetIds: groupFillAssetIds,
3079
2705
  existingPackId: existingPack?.id || null,
3080
2706
  ownerJid: existingPack?.owner_jid || createOwnerJid || null,
3081
- ownerCandidates: existingPack
3082
- ? [existingPack.owner_jid].filter(Boolean)
3083
- : [createOwnerJid, ...ownerPool.filter((owner) => owner && owner !== createOwnerJid)].filter(Boolean),
2707
+ ownerCandidates: existingPack ? [existingPack.owner_jid].filter(Boolean) : [createOwnerJid, ...ownerPool.filter((owner) => owner && owner !== createOwnerJid)].filter(Boolean),
3084
2708
  });
3085
2709
  }
3086
2710
 
@@ -3124,9 +2748,7 @@ const buildCurationExecutionPlan = async ({ curatedGroups, ownerPool, enableAddi
3124
2748
  const recipientPackId = recipientOp.existingPackId;
3125
2749
  const recipientCount = Number(packCountById.get(recipientPackId) || 0);
3126
2750
  if (recipientCount <= 0) continue;
3127
- const recipientDesired = new Set(
3128
- (Array.isArray(recipientOp.desiredAssetIds) ? recipientOp.desiredAssetIds : []).filter(Boolean),
3129
- );
2751
+ const recipientDesired = new Set((Array.isArray(recipientOp.desiredAssetIds) ? recipientOp.desiredAssetIds : []).filter(Boolean));
3130
2752
  if (!recipientDesired.size) continue;
3131
2753
 
3132
2754
  for (const assetId of recipientDesired) {
@@ -3141,13 +2763,9 @@ const buildCurationExecutionPlan = async ({ curatedGroups, ownerPool, enableAddi
3141
2763
  const donorItems = packItemsById.get(donorPackId);
3142
2764
  if (!donorItems || !donorItems.has(assetId)) continue;
3143
2765
 
3144
- donorOp.forceRemoveAssetIds = Array.from(
3145
- new Set([...(Array.isArray(donorOp.forceRemoveAssetIds) ? donorOp.forceRemoveAssetIds : []), assetId]),
3146
- );
3147
- donorOp.desiredAssetIds = (Array.isArray(donorOp.desiredAssetIds) ? donorOp.desiredAssetIds : [])
3148
- .filter((id) => id !== assetId);
3149
- donorOp.fillAssetIds = (Array.isArray(donorOp.fillAssetIds) ? donorOp.fillAssetIds : [])
3150
- .filter((id) => id !== assetId);
2766
+ donorOp.forceRemoveAssetIds = Array.from(new Set([...(Array.isArray(donorOp.forceRemoveAssetIds) ? donorOp.forceRemoveAssetIds : []), assetId]));
2767
+ donorOp.desiredAssetIds = (Array.isArray(donorOp.desiredAssetIds) ? donorOp.desiredAssetIds : []).filter((id) => id !== assetId);
2768
+ donorOp.fillAssetIds = (Array.isArray(donorOp.fillAssetIds) ? donorOp.fillAssetIds : []).filter((id) => id !== assetId);
3151
2769
  packCountById.set(donorPackId, Math.max(0, donorCount - 1));
3152
2770
  groupTransfers += 1;
3153
2771
  break;
@@ -3195,9 +2813,7 @@ const buildCurationExecutionPlan = async ({ curatedGroups, ownerPool, enableAddi
3195
2813
  autoPackIndex,
3196
2814
  stats: {
3197
2815
  owner_pool_size: ownerPool.length,
3198
- owner_available_total: ownerStates.some((owner) => !Number.isFinite(Number(owner.available)))
3199
- ? 'unlimited'
3200
- : ownerStates.reduce((sum, owner) => sum + Math.max(0, Number(owner.available || 0)), 0),
2816
+ owner_available_total: ownerStates.some((owner) => !Number.isFinite(Number(owner.available))) ? 'unlimited' : ownerStates.reduce((sum, owner) => sum + Math.max(0, Number(owner.available || 0)), 0),
3201
2817
  planned_creates: plannedCreates,
3202
2818
  completion_priority_groups: completionPriorityGroups,
3203
2819
  completion_transfers_planned: plannedCompletionTransfers,
@@ -3240,7 +2856,10 @@ const buildCurationExecutionPlan = async ({ curatedGroups, ownerPool, enableAddi
3240
2856
  };
3241
2857
 
3242
2858
  const updateAutoPackMetadata = async (packId, payload) => {
3243
- const normalizedPackStatus = String(payload?.pack_status || 'building').trim().toLowerCase() || 'building';
2859
+ const normalizedPackStatus =
2860
+ String(payload?.pack_status || 'building')
2861
+ .trim()
2862
+ .toLowerCase() || 'building';
3244
2863
  const resolvedWebStatus = String(payload?.status || (normalizedPackStatus === 'ready' ? 'published' : 'draft'))
3245
2864
  .trim()
3246
2865
  .toLowerCase();
@@ -3262,14 +2881,7 @@ const updateAutoPackMetadata = async (packId, payload) => {
3262
2881
  return updateStickerPackFields(packId, fields);
3263
2882
  };
3264
2883
 
3265
- const createAutoPackVolume = async ({
3266
- ownerJid,
3267
- theme,
3268
- subtheme,
3269
- themeKey,
3270
- groupScore,
3271
- volume,
3272
- }) => {
2884
+ const createAutoPackVolume = async ({ ownerJid, theme, subtheme, themeKey, groupScore, volume }) => {
3273
2885
  return stickerPackService.createPack({
3274
2886
  ownerJid,
3275
2887
  name: buildAutoPackName(theme, subtheme, volume),
@@ -3287,14 +2899,7 @@ const createAutoPackVolume = async ({
3287
2899
 
3288
2900
  const getThemeScoreForPack = (classification, theme) => getScoreByTag(classification, theme);
3289
2901
 
3290
- const reconcileAutoPackVolume = async ({
3291
- op,
3292
- enableAdditions,
3293
- enableRebuild,
3294
- budgets,
3295
- itemsByPackId,
3296
- classificationByAssetId,
3297
- }) => {
2902
+ const reconcileAutoPackVolume = async ({ op, enableAdditions, enableRebuild, budgets, itemsByPackId, classificationByAssetId }) => {
3298
2903
  let pack = op.existingPackId ? await findStickerPackById(op.existingPackId) : null;
3299
2904
 
3300
2905
  if (!pack && op.type === 'reconcile_volume') {
@@ -3344,8 +2949,7 @@ const reconcileAutoPackVolume = async ({
3344
2949
  }
3345
2950
 
3346
2951
  const desiredPrimaryIds = Array.from(new Set((Array.isArray(op.desiredAssetIds) ? op.desiredAssetIds : []).filter(Boolean)));
3347
- const fillPoolIds = Array.from(new Set((Array.isArray(op.fillAssetIds) ? op.fillAssetIds : []).filter(Boolean)))
3348
- .filter((assetId) => !desiredPrimaryIds.includes(assetId));
2952
+ const fillPoolIds = Array.from(new Set((Array.isArray(op.fillAssetIds) ? op.fillAssetIds : []).filter(Boolean))).filter((assetId) => !desiredPrimaryIds.includes(assetId));
3349
2953
  const plannedIds = desiredPrimaryIds.slice();
3350
2954
  if (plannedIds.length < TARGET_PACK_SIZE) {
3351
2955
  for (const assetId of fillPoolIds) {
@@ -3361,7 +2965,9 @@ const reconcileAutoPackVolume = async ({
3361
2965
  const created = op.existingPackId ? 0 : 1;
3362
2966
  const feedbackPromises = [];
3363
2967
  const queueFeedback = ({ classification, accepted, assetId }) => {
3364
- const imageHash = String(classification?.image_hash || '').trim().toLowerCase();
2968
+ const imageHash = String(classification?.image_hash || '')
2969
+ .trim()
2970
+ .toLowerCase();
3365
2971
  if (!imageHash) return;
3366
2972
  feedbackPromises.push(
3367
2973
  submitStickerClassificationFeedback({
@@ -3382,15 +2988,11 @@ const reconcileAutoPackVolume = async ({
3382
2988
  cover_sticker_id: pack.cover_sticker_id || null,
3383
2989
  });
3384
2990
 
3385
- let currentItems = itemsByPackId.get(pack.id) || await listStickerPackItems(pack.id);
2991
+ let currentItems = itemsByPackId.get(pack.id) || (await listStickerPackItems(pack.id));
3386
2992
  const currentById = new Map(currentItems.map((item) => [item.sticker_id, item]));
3387
2993
  const forceRemoveSet = new Set((Array.isArray(op.forceRemoveAssetIds) ? op.forceRemoveAssetIds : []).filter(Boolean));
3388
2994
 
3389
- const shouldRebuildVolume =
3390
- enableRebuild
3391
- || op.type === 'archive_volume'
3392
- || Number(op.cohesion || 0) < COHESION_REBUILD_THRESHOLD
3393
- || forceRemoveSet.size > 0;
2995
+ const shouldRebuildVolume = enableRebuild || op.type === 'archive_volume' || Number(op.cohesion || 0) < COHESION_REBUILD_THRESHOLD || forceRemoveSet.size > 0;
3394
2996
  if (shouldRebuildVolume && budgets.removed < MAX_REMOVALS_PER_CYCLE) {
3395
2997
  for (const item of currentItems) {
3396
2998
  if (budgets.removed >= MAX_REMOVALS_PER_CYCLE) break;
@@ -3503,14 +3105,8 @@ const reconcileAutoPackVolume = async ({
3503
3105
  const finalDesiredOrder = plannedIds.filter((assetId) => currentById.has(assetId));
3504
3106
  const currentOrder = currentItems.map((item) => item.sticker_id);
3505
3107
  const desiredReorderSet = new Set(finalDesiredOrder);
3506
- const projectedOrder = [
3507
- ...finalDesiredOrder,
3508
- ...currentOrder.filter((assetId) => !desiredReorderSet.has(assetId)),
3509
- ];
3510
- const needsReorder =
3511
- projectedOrder.length > 1
3512
- && projectedOrder.length === currentOrder.length
3513
- && projectedOrder.some((assetId, index) => currentOrder[index] !== assetId);
3108
+ const projectedOrder = [...finalDesiredOrder, ...currentOrder.filter((assetId) => !desiredReorderSet.has(assetId))];
3109
+ const needsReorder = projectedOrder.length > 1 && projectedOrder.length === currentOrder.length && projectedOrder.some((assetId, index) => currentOrder[index] !== assetId);
3514
3110
 
3515
3111
  if (needsReorder) {
3516
3112
  try {
@@ -3532,7 +3128,7 @@ const reconcileAutoPackVolume = async ({
3532
3128
  }
3533
3129
  }
3534
3130
 
3535
- const finalItems = itemsByPackId.get(pack.id) || await listStickerPackItems(pack.id);
3131
+ const finalItems = itemsByPackId.get(pack.id) || (await listStickerPackItems(pack.id));
3536
3132
  const finalCount = finalItems.length;
3537
3133
  const finalCover = finalItems[0]?.sticker_id || null;
3538
3134
  const finalDesiredPresentCount = finalItems.filter((item) => desiredSet.has(item.sticker_id)).length;
@@ -3541,17 +3137,9 @@ const reconcileAutoPackVolume = async ({
3541
3137
  const allowExtraItemsForReady = !enableRebuild && op.type === 'reconcile_volume';
3542
3138
  const meetsHardMinimum = finalCount >= HARD_MIN_PACK_ITEMS;
3543
3139
  const meetsReadyMinimum = finalCount >= READY_PACK_MIN_ITEMS;
3544
- const packStatus =
3545
- finalCount === 0
3546
- ? 'archived'
3547
- : allDesiredPresent && (allowExtraItemsForReady || !hasExtraItems) && meetsReadyMinimum
3548
- ? 'ready'
3549
- : meetsHardMinimum
3550
- ? 'building'
3551
- : 'archived';
3552
-
3553
- const shouldDeletePack =
3554
- packStatus === 'archived' && (op.type === 'archive_volume' || finalCount < HARD_MIN_PACK_ITEMS);
3140
+ const packStatus = finalCount === 0 ? 'archived' : allDesiredPresent && (allowExtraItemsForReady || !hasExtraItems) && meetsReadyMinimum ? 'ready' : meetsHardMinimum ? 'building' : 'archived';
3141
+
3142
+ const shouldDeletePack = packStatus === 'archived' && (op.type === 'archive_volume' || finalCount < HARD_MIN_PACK_ITEMS);
3555
3143
  if (shouldDeletePack) {
3556
3144
  try {
3557
3145
  await deleteAutoPackWithItems(pack.id, itemsByPackId);
@@ -3635,8 +3223,7 @@ const resolveNextCycleDelayMs = () => {
3635
3223
  return EFFECTIVE_INTERVAL_MIN_MS;
3636
3224
  }
3637
3225
 
3638
- return EFFECTIVE_INTERVAL_MIN_MS
3639
- + Math.floor(Math.random() * (EFFECTIVE_INTERVAL_MAX_MS - EFFECTIVE_INTERVAL_MIN_MS + 1));
3226
+ return EFFECTIVE_INTERVAL_MIN_MS + Math.floor(Math.random() * (EFFECTIVE_INTERVAL_MAX_MS - EFFECTIVE_INTERVAL_MIN_MS + 1));
3640
3227
  };
3641
3228
 
3642
3229
  const scheduleNextCycle = () => {
@@ -3662,10 +3249,7 @@ const scheduleNextCycle = () => {
3662
3249
  }
3663
3250
  };
3664
3251
 
3665
- export const runStickerAutoPackByTagsCycle = async ({
3666
- enableAdditions = true,
3667
- enableRebuild = REBUILD_ENABLED,
3668
- } = {}) => {
3252
+ export const runStickerAutoPackByTagsCycle = async ({ enableAdditions = true, enableRebuild = REBUILD_ENABLED } = {}) => {
3669
3253
  if (running) {
3670
3254
  return {
3671
3255
  executed: false,
@@ -3719,9 +3303,7 @@ export const runStickerAutoPackByTagsCycle = async ({
3719
3303
  };
3720
3304
  const classifiedWithoutPackBefore = await countClassifiedWithoutPackSafely({ phase: 'before_cycle' });
3721
3305
  const finalizeCycleResult = async (payload) => {
3722
- const withoutPackBefore = Number.isFinite(classifiedWithoutPackBefore)
3723
- ? Number(classifiedWithoutPackBefore)
3724
- : null;
3306
+ const withoutPackBefore = Number.isFinite(classifiedWithoutPackBefore) ? Number(classifiedWithoutPackBefore) : null;
3725
3307
  if (!Number.isFinite(withoutPackBefore)) {
3726
3308
  return {
3727
3309
  ...payload,
@@ -3732,12 +3314,8 @@ export const runStickerAutoPackByTagsCycle = async ({
3732
3314
  }
3733
3315
 
3734
3316
  const classifiedWithoutPackAfter = await countClassifiedWithoutPackSafely({ phase: 'after_cycle' });
3735
- const withoutPackAfter = Number.isFinite(classifiedWithoutPackAfter)
3736
- ? Number(classifiedWithoutPackAfter)
3737
- : null;
3738
- const withoutPackDelta = Number.isFinite(withoutPackAfter)
3739
- ? withoutPackBefore - withoutPackAfter
3740
- : null;
3317
+ const withoutPackAfter = Number.isFinite(classifiedWithoutPackAfter) ? Number(classifiedWithoutPackAfter) : null;
3318
+ const withoutPackDelta = Number.isFinite(withoutPackAfter) ? withoutPackBefore - withoutPackAfter : null;
3741
3319
 
3742
3320
  return {
3743
3321
  ...payload,
@@ -3816,10 +3394,7 @@ export const runStickerAutoPackByTagsCycle = async ({
3816
3394
  if (result?.status === 'archived') archivedPacks += 1;
3817
3395
  }
3818
3396
 
3819
- const scanReferenceCount = Math.max(
3820
- 1,
3821
- Number(stats.assets_unique_scanned || stats.assets_total_seen || stats.assets_scanned || 0),
3822
- );
3397
+ const scanReferenceCount = Math.max(1, Number(stats.assets_unique_scanned || stats.assets_total_seen || stats.assets_scanned || 0));
3823
3398
  const rejectionReferenceCount = Math.max(1, Number(stats.assets_scanned || 0));
3824
3399
  const duplicateRate = Number(stats.assets_deduped || 0) / scanReferenceCount;
3825
3400
  const rejectedCount = Number(stats.assets_rejected_quality || 0) + Number(stats.assets_rejected_no_theme || 0);