@kaikybrofc/omnizap-system 2.1.8

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 (166) hide show
  1. package/.env.example +534 -0
  2. package/LICENSE +21 -0
  3. package/README.md +431 -0
  4. package/RELEASE-v2.1.2.md +83 -0
  5. package/app/config/adminIdentity.js +87 -0
  6. package/app/config/baileysConfig.js +693 -0
  7. package/app/config/groupUtils.js +388 -0
  8. package/app/connection/socketController.js +992 -0
  9. package/app/controllers/messageController.js +354 -0
  10. package/app/modules/adminModule/groupCommandHandlers.js +1294 -0
  11. package/app/modules/adminModule/groupEventHandlers.js +355 -0
  12. package/app/modules/aiModule/catCommand.js +1006 -0
  13. package/app/modules/broadcastModule/noticeCommand.js +416 -0
  14. package/app/modules/gameModule/diceCommand.js +67 -0
  15. package/app/modules/menuModule/common.js +311 -0
  16. package/app/modules/menuModule/menus.js +59 -0
  17. package/app/modules/playModule/playCommand.js +1615 -0
  18. package/app/modules/quoteModule/quoteCommand.js +851 -0
  19. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +786 -0
  20. package/app/modules/rpgPokemonModule/rpgBattleService.js +2082 -0
  21. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +760 -0
  22. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  23. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +172 -0
  24. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  25. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  26. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  27. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  28. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1859 -0
  29. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6738 -0
  30. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  31. package/app/modules/statsModule/globalRankingCommand.js +65 -0
  32. package/app/modules/statsModule/noMessageCommand.js +288 -0
  33. package/app/modules/statsModule/rankingCommand.js +60 -0
  34. package/app/modules/statsModule/rankingCommon.js +889 -0
  35. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  36. package/app/modules/stickerModule/convertToWebp.js +390 -0
  37. package/app/modules/stickerModule/stickerCommand.js +454 -0
  38. package/app/modules/stickerModule/stickerConvertCommand.js +156 -0
  39. package/app/modules/stickerModule/stickerTextCommand.js +657 -0
  40. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  41. package/app/modules/stickerPackModule/autoPackCollectorService.js +284 -0
  42. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +466 -0
  43. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +88 -0
  44. package/app/modules/stickerPackModule/semanticThemeClusterService.js +571 -0
  45. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +449 -0
  46. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  47. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +180 -0
  48. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +4078 -0
  49. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +598 -0
  50. package/app/modules/stickerPackModule/stickerClassificationService.js +588 -0
  51. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +102 -0
  52. package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +7506 -0
  53. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1095 -0
  54. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +108 -0
  55. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  56. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +110 -0
  57. package/app/modules/stickerPackModule/stickerPackItemRepository.js +440 -0
  58. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +337 -0
  59. package/app/modules/stickerPackModule/stickerPackMessageService.js +296 -0
  60. package/app/modules/stickerPackModule/stickerPackRepository.js +442 -0
  61. package/app/modules/stickerPackModule/stickerPackService.js +788 -0
  62. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +51 -0
  63. package/app/modules/stickerPackModule/stickerPackUtils.js +97 -0
  64. package/app/modules/stickerPackModule/stickerStorageService.js +507 -0
  65. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +233 -0
  66. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +205 -0
  67. package/app/modules/systemMetricsModule/pingCommand.js +421 -0
  68. package/app/modules/tiktokModule/tiktokCommand.js +798 -0
  69. package/app/modules/userModule/userCommand.js +1217 -0
  70. package/app/modules/waifuPicsModule/waifuPicsCommand.js +177 -0
  71. package/app/observability/metrics.js +734 -0
  72. package/app/services/captchaService.js +492 -0
  73. package/app/services/dbWriteQueue.js +572 -0
  74. package/app/services/groupMetadataService.js +279 -0
  75. package/app/services/lidMapService.js +663 -0
  76. package/app/services/messagePersistenceService.js +56 -0
  77. package/app/services/newsBroadcastService.js +351 -0
  78. package/app/services/pokeApiService.js +398 -0
  79. package/app/services/queueUtils.js +57 -0
  80. package/app/services/socketState.js +7 -0
  81. package/app/store/aiPromptStore.js +38 -0
  82. package/app/store/groupConfigStore.js +58 -0
  83. package/app/store/premiumUserStore.js +36 -0
  84. package/app/utils/antiLink/antiLinkModule.js +804 -0
  85. package/app/utils/http/getImageBufferModule.js +18 -0
  86. package/app/utils/json/jsonSanitizer.js +113 -0
  87. package/app/utils/json/jsonSanitizer.test.js +40 -0
  88. package/app/utils/logger/loggerModule.js +262 -0
  89. package/app/utils/systemMetrics/systemMetricsModule.js +91 -0
  90. package/database/index.js +2052 -0
  91. package/database/init.js +516 -0
  92. package/database/migrations/20260203_0001_sticker_packs.sql +54 -0
  93. package/database/migrations/20260210_0003_rpg_pokemon.sql +58 -0
  94. package/database/migrations/20260210_0004_rpg_shiny_biome.sql +9 -0
  95. package/database/migrations/20260210_0005_rpg_missions.sql +14 -0
  96. package/database/migrations/20260210_0006_rpg_world_pokedex_traits.sql +27 -0
  97. package/database/migrations/20260210_0007_rpg_raid_pvp.sql +56 -0
  98. package/database/migrations/20260210_0008_rpg_social_system.sql +195 -0
  99. package/database/migrations/20260211_0009_rpg_social_xp.sql +36 -0
  100. package/database/migrations/20260222_0010_remove_message_xp.sql +2 -0
  101. package/database/migrations/20260226_0011_sticker_asset_classification.sql +17 -0
  102. package/database/migrations/20260226_0012_sticker_pack_engagement.sql +16 -0
  103. package/database/migrations/20260226_0013_sticker_marketplace_intelligence.sql +19 -0
  104. package/database/migrations/20260226_0014_sticker_pack_publish_flow.sql +30 -0
  105. package/database/migrations/20260226_0014_sticker_worker_queues.sql +42 -0
  106. package/database/migrations/20260226_0015_sticker_auto_pack_curation_integrity.sql +18 -0
  107. package/database/migrations/20260226_0016_sticker_web_google_auth_persistence.sql +34 -0
  108. package/database/migrations/20260226_0017_sticker_web_admin_ban.sql +22 -0
  109. package/database/migrations/20260226_0018_sticker_web_admin_moderator.sql +18 -0
  110. package/database/migrations/20260227_0019_sticker_classification_v2_signals.sql +12 -0
  111. package/database/migrations/20260227_0020_semantic_theme_clusters.sql +35 -0
  112. package/docker-compose.yml +103 -0
  113. package/ecosystem.prod.config.cjs +35 -0
  114. package/eslint.config.js +61 -0
  115. package/index.js +437 -0
  116. package/ml/clip_classifier/Dockerfile +16 -0
  117. package/ml/clip_classifier/README.md +120 -0
  118. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  119. package/ml/clip_classifier/classifier.py +654 -0
  120. package/ml/clip_classifier/embedding_store.py +481 -0
  121. package/ml/clip_classifier/env_loader.py +15 -0
  122. package/ml/clip_classifier/llm_label_expander.py +144 -0
  123. package/ml/clip_classifier/main.py +213 -0
  124. package/ml/clip_classifier/requirements.txt +10 -0
  125. package/ml/clip_classifier/similarity_engine.py +74 -0
  126. package/observability/alert-rules.yml +60 -0
  127. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  128. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  129. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  130. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  131. package/observability/loki-config.yml +38 -0
  132. package/observability/mysql-exporter.cnf +5 -0
  133. package/observability/mysql-setup.sql +46 -0
  134. package/observability/prometheus.yml +32 -0
  135. package/observability/promtail-config.yml +84 -0
  136. package/package.json +109 -0
  137. package/public/api-docs/index.html +144 -0
  138. package/public/css/github-project-panel.css +297 -0
  139. package/public/css/stickers-admin.css +1272 -0
  140. package/public/css/styles.css +671 -0
  141. package/public/index.html +1311 -0
  142. package/public/js/apps/apiDocsApp.js +310 -0
  143. package/public/js/apps/createPackApp.js +2069 -0
  144. package/public/js/apps/homeApp.js +396 -0
  145. package/public/js/apps/stickersAdminApp.js +1744 -0
  146. package/public/js/apps/stickersApp.js +4830 -0
  147. package/public/js/catalog.js +1019 -0
  148. package/public/js/github-panel/components/CommitList.js +34 -0
  149. package/public/js/github-panel/components/ErrorState.js +16 -0
  150. package/public/js/github-panel/components/GithubProjectPanel.js +106 -0
  151. package/public/js/github-panel/components/ReleaseList.js +38 -0
  152. package/public/js/github-panel/components/SkeletonPanel.js +22 -0
  153. package/public/js/github-panel/components/StatCard.js +15 -0
  154. package/public/js/github-panel/index.js +15 -0
  155. package/public/js/github-panel/useGithubRepoData.js +154 -0
  156. package/public/js/github-panel/vendor/react.js +11 -0
  157. package/public/js/runtime/react-runtime.js +19 -0
  158. package/public/licenca/index.html +106 -0
  159. package/public/stickers/admin/index.html +23 -0
  160. package/public/stickers/create/index.html +47 -0
  161. package/public/stickers/index.html +48 -0
  162. package/public/termos-de-uso/index.html +125 -0
  163. package/scripts/cache-bust.mjs +107 -0
  164. package/scripts/deploy.sh +458 -0
  165. package/scripts/github-deploy-notify.mjs +174 -0
  166. package/scripts/release.sh +129 -0
@@ -0,0 +1,4078 @@
1
+ import logger from '../../utils/logger/loggerModule.js';
2
+ import { getActiveSocket } from '../../services/socketState.js';
3
+ import { normalizeJid, resolveBotJid } from '../../config/baileysConfig.js';
4
+ import { recordStickerAutoPackCycle } from '../../observability/metrics.js';
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';
30
+ import { listStickerPackEngagementByPackIds } from './stickerPackEngagementRepository.js';
31
+
32
+ const parseEnvBool = (value, fallback) => {
33
+ if (value === undefined || value === null || value === '') return fallback;
34
+ const normalized = String(value).trim().toLowerCase();
35
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
36
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
37
+ return fallback;
38
+ };
39
+ const parseMaxPacksPerOwnerLimit = (value, fallback = 50) => {
40
+ if (value === undefined || value === null || value === '') {
41
+ return Math.max(1, Number(fallback) || 50);
42
+ }
43
+ const normalized = String(value).trim().toLowerCase();
44
+ if (['0', '-1', 'inf', 'infinity', 'unlimited', 'sem-limite'].includes(normalized)) {
45
+ return Number.POSITIVE_INFINITY;
46
+ }
47
+ const parsed = Number(normalized);
48
+ if (Number.isFinite(parsed) && parsed > 0) {
49
+ return Math.max(1, Math.floor(parsed));
50
+ }
51
+ return Math.max(1, Number(fallback) || 50);
52
+ };
53
+
54
+ const AUTO_ENABLED = parseEnvBool(process.env.STICKER_AUTO_PACK_BY_TAGS_ENABLED, true);
55
+ const STARTUP_DELAY_MS = Math.max(1_000, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_STARTUP_DELAY_MS) || 20_000);
56
+ const LEGACY_INTERVAL_MS = Number(process.env.STICKER_AUTO_PACK_BY_TAGS_INTERVAL_MS);
57
+ const INTERVAL_MIN_MS_RAW = Number(process.env.STICKER_AUTO_PACK_BY_TAGS_INTERVAL_MIN_MS);
58
+ const INTERVAL_MAX_MS_RAW = Number(process.env.STICKER_AUTO_PACK_BY_TAGS_INTERVAL_MAX_MS);
59
+ const DEFAULT_INTERVAL_MIN_MS = 5 * 60_000;
60
+ 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;
67
+ 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;
71
+ const EFFECTIVE_INTERVAL_MIN_MS = LEGACY_FIXED_INTERVAL_MS || INTERVAL_MIN_MS;
72
+ const EFFECTIVE_INTERVAL_MAX_MS = LEGACY_FIXED_INTERVAL_MS || INTERVAL_MAX_MS;
73
+ const TARGET_PACK_SIZE = Math.max(5, Math.min(30, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_TARGET_SIZE) || 30));
74
+ const MIN_GROUP_SIZE = Math.max(3, Math.min(100, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MIN_GROUP_SIZE) || 8));
75
+ const MAX_TAG_GROUPS = Math.max(0, Math.min(500, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_GROUPS) || 0));
76
+ const ENABLE_SEMANTIC_CLUSTERING = parseEnvBool(process.env.ENABLE_SEMANTIC_CLUSTERING, false);
77
+ const MAX_SCAN_ASSETS = Math.max(0, Math.min(250_000, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MAX_SCAN_ASSETS) || 0));
78
+ 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';
80
+ const AUTO_PUBLISHER = String(process.env.STICKER_AUTO_PACK_BY_TAGS_PUBLISHER || 'OmniZap Auto').trim() || 'OmniZap Auto';
81
+ 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
+ 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;
108
+ const MIN_ASSET_EDGE = Math.max(32, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MIN_ASSET_EDGE) || 192);
109
+ const MIN_ASSET_AREA = Math.max(32 * 32, Number(process.env.STICKER_AUTO_PACK_BY_TAGS_MIN_ASSET_AREA) || 192 * 192);
110
+ 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;
117
+ 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
+ );
122
+ 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;
174
+ 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
+ );
182
+ 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
+ );
233
+ const PRIORITIZE_COMPLETION = parseEnvBool(process.env.STICKER_AUTO_PACK_BY_TAGS_PRIORITIZE_COMPLETION, true);
234
+ 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
+ );
243
+ const ENABLE_GLOBAL_OPTIMIZATION = parseEnvBool(process.env.ENABLE_GLOBAL_OPTIMIZATION, true);
244
+ 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;
258
+ 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();
320
+ 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;
368
+ 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;
372
+ const EFFECTIVE_PRIORITIZE_COMPLETION = PRIORITIZE_COMPLETION || IS_AGGRESSIVE_PROFILE;
373
+ 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;
380
+
381
+ const EXPLICIT_OWNER = String(process.env.STICKER_AUTO_PACK_OWNER_JID || process.env.USER_ADMIN || '').trim();
382
+
383
+ const LABEL_TO_TAG = {
384
+ 'anime illustration': 'anime',
385
+ 'video game screenshot': 'game',
386
+ 'real life photo': 'foto-real',
387
+ 'nsfw content': 'nsfw',
388
+ cartoon: 'cartoon',
389
+ };
390
+
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
+ ]);
399
+
400
+ const normalizeTag = (value) =>
401
+ String(value || '')
402
+ .trim()
403
+ .toLowerCase()
404
+ .normalize('NFD')
405
+ .replace(/[\u0300-\u036f]/g, '')
406
+ .replace(/[^a-z0-9]+/g, '-')
407
+ .replace(/^-+|-+$/g, '')
408
+ .slice(0, 40);
409
+
410
+ const toNumericClusterId = (value) => {
411
+ const numeric = Number(value);
412
+ if (!Number.isInteger(numeric) || numeric <= 0) return null;
413
+ return numeric;
414
+ };
415
+
416
+ const isSemanticClusterSubthemeTag = (value) => /^cluster-\d+$/.test(normalizeTag(value));
417
+
418
+ const toTagFromLabel = (label) => {
419
+ const key = String(label || '').trim().toLowerCase();
420
+ return LABEL_TO_TAG[key] || normalizeTag(key);
421
+ };
422
+
423
+ const toPackTitleTag = (tag) =>
424
+ String(tag || '')
425
+ .split('-')
426
+ .filter(Boolean)
427
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
428
+ .join(' ') || 'Outros';
429
+
430
+ const resolveOwnerJid = () => {
431
+ const sock = getActiveSocket?.();
432
+ const botJid = resolveBotJid(sock?.user?.id || '');
433
+ if (botJid) return botJid;
434
+
435
+ if (EXPLICIT_OWNER) {
436
+ if (EXPLICIT_OWNER.includes('@')) return normalizeJid(EXPLICIT_OWNER);
437
+ const digits = EXPLICIT_OWNER.replace(/\D+/g, '');
438
+ if (digits) return normalizeJid(`${digits}@s.whatsapp.net`);
439
+ }
440
+
441
+ return null;
442
+ };
443
+
444
+ const normalizeOwnerCandidate = (value) => {
445
+ const raw = String(value || '').trim();
446
+ if (!raw) return '';
447
+ if (raw.includes('@')) return normalizeJid(raw);
448
+ const digits = raw.replace(/\D+/g, '');
449
+ return digits ? normalizeJid(`${digits}@s.whatsapp.net`) : '';
450
+ };
451
+
452
+ const parseOwnerPool = (raw) =>
453
+ Array.from(
454
+ new Set(
455
+ String(raw || '')
456
+ .split(/[,\n;]+/)
457
+ .map((entry) => normalizeOwnerCandidate(entry))
458
+ .filter(Boolean),
459
+ ),
460
+ );
461
+
462
+ const resolveCurationOwnerPool = () => {
463
+ const poolFromEnv = parseOwnerPool(CURATION_OWNERS_POOL_RAW);
464
+ const resolvedPrimary = resolveOwnerJid();
465
+ const owners = [...poolFromEnv];
466
+ if (resolvedPrimary && !owners.includes(resolvedPrimary)) {
467
+ owners.unshift(resolvedPrimary);
468
+ }
469
+ return Array.from(new Set(owners.filter(Boolean)));
470
+ };
471
+
472
+ const buildThemeKey = (theme, subtheme = '') => {
473
+ const normalizedTheme = normalizeTag(theme);
474
+ const normalizedSubtheme = normalizeTag(subtheme);
475
+ if (!normalizedTheme) return '';
476
+ return normalizedSubtheme ? `${normalizedTheme}:${normalizedSubtheme}` : normalizedTheme;
477
+ };
478
+ const parseThemeKey = (raw) => {
479
+ const normalized = String(raw || '').trim().toLowerCase();
480
+ if (!normalized) return { theme: '', subtheme: '' };
481
+ const [theme = '', subtheme = ''] = normalized.split(':', 2);
482
+ return {
483
+ theme: normalizeTag(theme),
484
+ subtheme: normalizeTag(subtheme),
485
+ };
486
+ };
487
+
488
+ const sanitizeDisplaySubtheme = (subtheme) => {
489
+ const normalized = normalizeTag(subtheme);
490
+ if (!normalized) return '';
491
+ if (isSemanticClusterSubthemeTag(normalized)) return '';
492
+ return normalized;
493
+ };
494
+
495
+ const buildAutoPackName = (theme, subtheme, index) => {
496
+ const base = `[AUTO] ${toPackTitleTag(theme)}${subtheme ? ` • ${toPackTitleTag(subtheme)}` : ''}`;
497
+ return `${base} • Vol. ${index}`;
498
+ };
499
+ 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)}.`;
504
+
505
+ const clampNumber = (value, min, max) => Math.max(min, Math.min(max, Number(value)));
506
+
507
+ const normalizeAffinityWeight = (value) => {
508
+ const raw = clampNumber(Number(value || 0), 0, 1);
509
+ const capped = Math.min(raw, AFFINITY_WEIGHT_CAP);
510
+ if (!ENABLE_AFFINITY_LOG_SCALING) return capped;
511
+ return Math.log1p(capped * AFFINITY_LOG_SCALE) / Math.log1p(AFFINITY_LOG_SCALE);
512
+ };
513
+
514
+ const normalizeEntropy = ({ entropy = 0, entropyNormalized = null, topLabelCount = 0 }) => {
515
+ const explicit = Number(entropyNormalized);
516
+ if (Number.isFinite(explicit)) return clampNumber(explicit, 0, 1);
517
+
518
+ const safeEntropy = Math.max(0, Number(entropy || 0));
519
+ if (!safeEntropy) return 0;
520
+
521
+ const kFromLabels = Math.max(0, Number(topLabelCount || 0));
522
+ if (kFromLabels > 1) {
523
+ const maxEntropy = Math.log(kFromLabels);
524
+ if (maxEntropy > 0) {
525
+ return clampNumber(safeEntropy / maxEntropy, 0, 1);
526
+ }
527
+ }
528
+
529
+ return clampNumber(safeEntropy / Math.max(0.000001, ENTROPY_THRESHOLD), 0, 1);
530
+ };
531
+
532
+ const deterministicUnitInterval = (seed) => {
533
+ let hash = 2166136261;
534
+ const input = String(seed || '');
535
+ for (let i = 0; i < input.length; i += 1) {
536
+ hash ^= input.charCodeAt(i);
537
+ hash = Math.imul(hash, 16777619);
538
+ }
539
+ return (hash >>> 0) / 4294967295;
540
+ };
541
+
542
+ const resolvePassScoreWeight = (assetId, passIndex) => {
543
+ const amplitude = SCAN_PASS_JITTER_PERCENT / 100;
544
+ if (amplitude <= 0) return 1;
545
+ const deterministicNoise = deterministicUnitInterval(`${assetId}:${passIndex}`) * 2 - 1;
546
+ return clampNumber(1 + deterministicNoise * amplitude, 0.65, 1.35);
547
+ };
548
+
549
+ const sumArray = (values) => values.reduce((sum, value) => sum + Number(value || 0), 0);
550
+
551
+ const meanArray = (values) => {
552
+ if (!Array.isArray(values) || !values.length) return 0;
553
+ return sumArray(values) / values.length;
554
+ };
555
+
556
+ const varianceArray = (values) => {
557
+ if (!Array.isArray(values) || values.length <= 1) return 0;
558
+ 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;
563
+ };
564
+
565
+ const percentileValue = (values, percentile) => {
566
+ if (!Array.isArray(values) || !values.length) return 0;
567
+ const sorted = [...values].map((value) => Number(value || 0)).sort((left, right) => left - right);
568
+ const p = clampNumber(percentile, 0, 100) / 100;
569
+ const idx = Math.floor((sorted.length - 1) * p);
570
+ return Number(sorted[Math.max(0, Math.min(sorted.length - 1, idx))] || 0);
571
+ };
572
+
573
+ const accumulateVector = (target, source, weight = 1) => {
574
+ if (!source || typeof source !== 'object') return;
575
+ const w = Number(weight || 1);
576
+ if (!Number.isFinite(w) || w <= 0) return;
577
+ for (const [key, rawValue] of Object.entries(source)) {
578
+ const numeric = Number(rawValue);
579
+ if (!Number.isFinite(numeric)) continue;
580
+ target[key] = (target[key] || 0) + numeric * w;
581
+ }
582
+ };
583
+
584
+ const scaleVector = (vector, denominator) => {
585
+ const d = Number(denominator || 0);
586
+ if (!Number.isFinite(d) || d <= 0) return {};
587
+ const scaled = {};
588
+ for (const [key, value] of Object.entries(vector || {})) {
589
+ const numeric = Number(value);
590
+ if (!Number.isFinite(numeric)) continue;
591
+ scaled[key] = numeric / d;
592
+ }
593
+ return scaled;
594
+ };
595
+
596
+ const buildStickerFeatureVector = (classification) => {
597
+ if (!classification || typeof classification !== 'object') return {};
598
+ const vector = {};
599
+ accumulateVector(vector, classification.all_scores || {}, 1);
600
+ for (const topLabel of getTopLabelEntries(classification)) {
601
+ const mapped = toTagFromLabel(topLabel.label);
602
+ if (!mapped) continue;
603
+ vector[`top:${mapped}`] = Math.max(vector[`top:${mapped}`] || 0, Number(topLabel.score || 0));
604
+ }
605
+ for (const trait of getLlmTraitTokens(classification)) {
606
+ vector[`trait:${trait}`] = Math.max(vector[`trait:${trait}`] || 0, 0.12);
607
+ }
608
+ if (classification.category) {
609
+ const normalized = toTagFromLabel(classification.category);
610
+ if (normalized) {
611
+ vector[`cat:${normalized}`] = Number(classification.confidence || 0);
612
+ }
613
+ }
614
+ if (classification.is_nsfw) {
615
+ vector['cat:nsfw'] = Math.max(vector['cat:nsfw'] || 0, Number(classification.nsfw_score || 0));
616
+ }
617
+ return vector;
618
+ };
619
+
620
+ const buildPackCentroidVector = (stickerIds, classificationByAssetId) => {
621
+ const sum = {};
622
+ let count = 0;
623
+ for (const stickerId of stickerIds) {
624
+ const classification = classificationByAssetId.get(stickerId);
625
+ if (!classification) continue;
626
+ accumulateVector(sum, buildStickerFeatureVector(classification), 1);
627
+ count += 1;
628
+ }
629
+ return scaleVector(sum, Math.max(1, count));
630
+ };
631
+
632
+ const dominantTagRatio = (stickerIds, classificationByAssetId) => {
633
+ const votes = new Map();
634
+ let total = 0;
635
+ for (const stickerId of stickerIds) {
636
+ const classification = classificationByAssetId.get(stickerId);
637
+ if (!classification) continue;
638
+ const topTag = buildTopTags(classification).find((entry) => entry?.tag && !TECHNICAL_TAGS.has(entry.tag))?.tag;
639
+ if (!topTag) continue;
640
+ votes.set(topTag, (votes.get(topTag) || 0) + 1);
641
+ total += 1;
642
+ }
643
+ if (total <= 0) return 0;
644
+ const best = Math.max(0, ...votes.values());
645
+ return best / total;
646
+ };
647
+
648
+ const computeStickerPackMatrixScore = ({
649
+ stickerId,
650
+ packStickerIds,
651
+ classificationByAssetId,
652
+ centroidVector,
653
+ }) => {
654
+ const classification = classificationByAssetId.get(stickerId);
655
+ if (!classification) return 0;
656
+ const stickerVector = buildStickerFeatureVector(classification);
657
+ const others = packStickerIds.filter((entryId) => entryId !== stickerId);
658
+ const semanticSimilarity = cosineSimilarity(stickerVector, centroidVector);
659
+
660
+ let cohesion = semanticSimilarity;
661
+ if (others.length) {
662
+ const sample = others.slice(0, 10);
663
+ const sims = sample.map((otherId) => {
664
+ const otherClassification = classificationByAssetId.get(otherId);
665
+ return cosineSimilarity(stickerVector, buildStickerFeatureVector(otherClassification));
666
+ });
667
+ cohesion = meanArray(sims);
668
+ }
669
+
670
+ const confidence = Number(classification.confidence || 0);
671
+ 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
+ });
677
+ const affinityWeight = normalizeAffinityWeight(classification.affinity_weight);
678
+ const impactOnGroupScore = confidence * 0.4 + themeStrength * 0.35 + entropyStability * 0.15 + affinityWeight * 0.1;
679
+
680
+ let duplicationPenalty = 0;
681
+ if (others.length) {
682
+ const dupHits = others.reduce((acc, otherId) => {
683
+ const otherClassification = classificationByAssetId.get(otherId);
684
+ const sim = cosineSimilarity(stickerVector, buildStickerFeatureVector(otherClassification));
685
+ return acc + (sim >= DEDUPE_SIMILARITY_THRESHOLD ? 1 : 0);
686
+ }, 0);
687
+ duplicationPenalty = dupHits / Math.max(1, others.length);
688
+ }
689
+
690
+ const matrixScore =
691
+ MATRIX_ALPHA * semanticSimilarity
692
+ + MATRIX_BETA * cohesion
693
+ + MATRIX_GAMMA * impactOnGroupScore
694
+ - MATRIX_DELTA * duplicationPenalty;
695
+
696
+ return clampNumber(matrixScore, 0, 1.6);
697
+ };
698
+
699
+ const computePackEngagementScore = (engagement) => {
700
+ const opens = Math.max(0, Number(engagement?.open_count || 0));
701
+ const likes = Math.max(0, Number(engagement?.like_count || 0));
702
+ const dislikes = Math.max(0, Number(engagement?.dislike_count || 0));
703
+ if (!opens && !likes && !dislikes) return 0;
704
+ const positive = likes * 2 + opens * 0.15;
705
+ const negative = dislikes * 1.25;
706
+ return clampNumber((positive - negative) / 100, 0, 1.2);
707
+ };
708
+
709
+ const normalizeZScoreToUnit = (value) => clampNumber(0.5 + Number(value || 0) / 6, 0, 1);
710
+
711
+ const buildNormalizedZScoreMap = (valueByKey = new Map()) => {
712
+ const entries = Array.from(valueByKey.entries());
713
+ if (!entries.length) return new Map();
714
+ const values = entries.map(([, value]) => Number(value || 0));
715
+ const mean = meanArray(values);
716
+ const variance = varianceArray(values);
717
+ const stddev = variance > 0 ? Math.sqrt(variance) : 0;
718
+ const normalized = new Map();
719
+ for (const [key, value] of entries) {
720
+ const numeric = Number(value || 0);
721
+ const zScore = stddev > 0 ? (numeric - mean) / stddev : 0;
722
+ normalized.set(key, Number(normalizeZScoreToUnit(zScore).toFixed(6)));
723
+ }
724
+ return normalized;
725
+ };
726
+
727
+ const computePackCohesionScore = (profile) => clampNumber(
728
+ Number(profile?.semanticCohesion || 0) * 0.7 + Number(profile?.topicDominance || 0) * 0.3,
729
+ 0,
730
+ 1,
731
+ );
732
+
733
+ const computePackObjectiveScore = ({ profile, engagementScore = 0 }) => {
734
+ const meanAssetQuality = clampNumber(Number(profile?.meanAssetQuality || 0), 0, 1);
735
+ const cohesionScore = computePackCohesionScore(profile);
736
+ const volumeScore = clampNumber(Number(profile?.volumeScore || 0), 0, 1);
737
+ const engagementComponent = clampNumber(Number(engagementScore || 0), 0, 1);
738
+
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
+ );
747
+ };
748
+
749
+ const computePackOfficialQualityScore = ({ profile, engagementZscore = 0 }) => {
750
+ const meanAssetQuality = clampNumber(Number(profile?.meanAssetQuality || 0), 0, 1);
751
+ const cohesionScore = computePackCohesionScore(profile);
752
+ const completionRatio = clampNumber(Number(profile?.volumeScore || 0), 0, 1);
753
+ const stabilityIndex = clampNumber(1 - Math.max(0, Number(profile?.internalVariance || 0)), 0, 1);
754
+ 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;
761
+ return clampNumber(raw, 0, 1.2);
762
+ };
763
+
764
+ const buildPackPairKey = (leftPackId, rightPackId) => {
765
+ const left = String(leftPackId || '');
766
+ const right = String(rightPackId || '');
767
+ return left < right ? `${left}::${right}` : `${right}::${left}`;
768
+ };
769
+
770
+ const computePackSemanticSimilarity = (leftProfile, rightProfile) =>
771
+ clampNumber(cosineSimilarity(leftProfile?.centroidVector || {}, rightProfile?.centroidVector || {}), 0, 1);
772
+
773
+ const buildInterPackSimilarityMatrix = (profiles) => {
774
+ const profileList = Array.from((profiles instanceof Map ? profiles.values() : []))
775
+ .filter((profile) => Array.isArray(profile?.stickerIds) && profile.stickerIds.length > 0);
776
+ const matrix = new Map();
777
+ let sum = 0;
778
+ let count = 0;
779
+
780
+ for (let i = 0; i < profileList.length; i += 1) {
781
+ for (let j = i + 1; j < profileList.length; j += 1) {
782
+ const left = profileList[i];
783
+ const right = profileList[j];
784
+ const similarity = computePackSemanticSimilarity(left, right);
785
+ matrix.set(buildPackPairKey(left.packId, right.packId), similarity);
786
+ sum += similarity;
787
+ count += 1;
788
+ }
789
+ }
790
+
791
+ return {
792
+ matrix,
793
+ pair_count: count,
794
+ similarity_mean: Number((count > 0 ? sum / count : 0).toFixed(6)),
795
+ };
796
+ };
797
+
798
+ const computeMeanNormalizedEntropy = (stickerIds, classificationByAssetId) => {
799
+ if (!Array.isArray(stickerIds) || !stickerIds.length) return 0;
800
+ let total = 0;
801
+ let count = 0;
802
+ for (const stickerId of stickerIds) {
803
+ const classification = classificationByAssetId.get(stickerId);
804
+ if (!classification) continue;
805
+ total += normalizeEntropy({
806
+ entropy: classification.entropy,
807
+ entropyNormalized: classification.entropy_normalized,
808
+ topLabelCount: Array.isArray(classification.top_labels) ? classification.top_labels.length : 0,
809
+ });
810
+ count += 1;
811
+ }
812
+ return count > 0 ? total / count : 0;
813
+ };
814
+
815
+ const computePackEnergyDelta = ({
816
+ baseEnergySnapshot,
817
+ profiles,
818
+ profileScores,
819
+ changes,
820
+ }) => {
821
+ const updates = changes instanceof Map ? changes : new Map();
822
+ if (!updates.size) {
823
+ return {
824
+ deltaEnergy: 0,
825
+ nextSnapshot: baseEnergySnapshot,
826
+ };
827
+ }
828
+
829
+ const changedIds = Array.from(updates.keys());
830
+ const changedSet = new Set(changedIds);
831
+ const unchangedIds = Array.from(profiles.keys()).filter((packId) => !changedSet.has(packId));
832
+
833
+ let qualityDelta = 0;
834
+ for (const packId of changedIds) {
835
+ const oldScore = Number(profileScores.get(packId) || 0);
836
+ const newScore = Number(updates.get(packId)?.score || 0);
837
+ qualityDelta += newScore - oldScore;
838
+ }
839
+
840
+ let overlapDelta = 0;
841
+ for (const packId of changedIds) {
842
+ const oldProfile = profiles.get(packId);
843
+ const newProfile = updates.get(packId)?.profile || oldProfile;
844
+ for (const otherId of unchangedIds) {
845
+ const otherProfile = profiles.get(otherId);
846
+ overlapDelta += computePackOverlap(newProfile, otherProfile) - computePackOverlap(oldProfile, otherProfile);
847
+ }
848
+ }
849
+ for (let index = 0; index < changedIds.length; index += 1) {
850
+ const leftId = changedIds[index];
851
+ const leftOld = profiles.get(leftId);
852
+ const leftNew = updates.get(leftId)?.profile || leftOld;
853
+ for (let j = index + 1; j < changedIds.length; j += 1) {
854
+ const rightId = changedIds[j];
855
+ const rightOld = profiles.get(rightId);
856
+ const rightNew = updates.get(rightId)?.profile || rightOld;
857
+ overlapDelta += computePackOverlap(leftNew, rightNew) - computePackOverlap(leftOld, rightOld);
858
+ }
859
+ }
860
+
861
+ const qualitySum = Number(baseEnergySnapshot?.qualitySum || 0) + qualityDelta;
862
+ const overlapPairs = Math.max(0, Number(baseEnergySnapshot?.overlapPairs || 0));
863
+ const overlapSum = Number(baseEnergySnapshot?.overlapSum || 0) + overlapDelta;
864
+ const redundancy = overlapPairs > 0 ? overlapSum / overlapPairs : 0;
865
+ const redundancyPenalty = SYSTEM_REDUNDANCY_LAMBDA * GLOBAL_ENERGY_W5 * redundancy;
866
+ const energy = qualitySum - redundancyPenalty;
867
+ const deltaEnergy = energy - Number(baseEnergySnapshot?.energy || 0);
868
+
869
+ return {
870
+ deltaEnergy: Number(deltaEnergy.toFixed(6)),
871
+ nextSnapshot: {
872
+ qualitySum: Number(qualitySum.toFixed(6)),
873
+ overlapSum: Number(overlapSum.toFixed(6)),
874
+ overlapPairs,
875
+ redundancy: Number(redundancy.toFixed(6)),
876
+ redundancyPenalty: Number(redundancyPenalty.toFixed(6)),
877
+ energy: Number(energy.toFixed(6)),
878
+ profileScores,
879
+ },
880
+ };
881
+ };
882
+
883
+ const computePackProfile = ({
884
+ packId,
885
+ stickerIds,
886
+ themeKey,
887
+ classificationByAssetId,
888
+ }) => {
889
+ const cleanStickerIds = Array.from(new Set((Array.isArray(stickerIds) ? stickerIds : []).filter(Boolean)));
890
+ const centroidVector = buildPackCentroidVector(cleanStickerIds, classificationByAssetId);
891
+ const matrixScores = cleanStickerIds.map((stickerId) =>
892
+ computeStickerPackMatrixScore({
893
+ stickerId,
894
+ packStickerIds: cleanStickerIds,
895
+ classificationByAssetId,
896
+ centroidVector,
897
+ }));
898
+ 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
+ }));
905
+ const parsedThemeKey = parseThemeKey(themeKey);
906
+ const meanAssetQuality = meanArray(
907
+ cleanStickerIds.map((stickerId) => {
908
+ const classification = classificationByAssetId.get(stickerId);
909
+ if (!classification) return 0;
910
+ const topTags = buildTopTags(classification);
911
+ return computeAssetQualityForTheme({
912
+ classification,
913
+ theme: parsedThemeKey.theme,
914
+ subtheme: parsedThemeKey.subtheme,
915
+ topTags,
916
+ }).assetQuality;
917
+ }),
918
+ );
919
+ const topicDominance = dominantTagRatio(cleanStickerIds, classificationByAssetId);
920
+ const volumeScore = clampNumber(cleanStickerIds.length / Math.max(1, TARGET_PACK_SIZE), 0, 1);
921
+ const internalVariance = varianceArray(matrixScores);
922
+
923
+ let duplicatePairs = 0;
924
+ let pairCount = 0;
925
+ for (let i = 0; i < cleanStickerIds.length; i += 1) {
926
+ for (let j = i + 1; j < cleanStickerIds.length; j += 1) {
927
+ pairCount += 1;
928
+ const leftClassification = classificationByAssetId.get(cleanStickerIds[i]);
929
+ const rightClassification = classificationByAssetId.get(cleanStickerIds[j]);
930
+ const sim = cosineSimilarity(
931
+ buildStickerFeatureVector(leftClassification),
932
+ buildStickerFeatureVector(rightClassification),
933
+ );
934
+ if (sim >= DEDUPE_SIMILARITY_THRESHOLD) duplicatePairs += 1;
935
+ }
936
+ }
937
+ const duplicationRatio = pairCount > 0 ? duplicatePairs / pairCount : 0;
938
+
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;
946
+ const packQuality = clampNumber(qualityRaw, 0, 2.5);
947
+
948
+ return {
949
+ packId,
950
+ themeKey,
951
+ stickerIds: cleanStickerIds,
952
+ centroidVector,
953
+ packQuality: Number(packQuality.toFixed(6)),
954
+ meanAssetQuality: Number(meanAssetQuality.toFixed(6)),
955
+ meanStickerScore: Number(meanStickerScore.toFixed(6)),
956
+ semanticCohesion: Number(semanticCohesion.toFixed(6)),
957
+ topicDominance: Number(topicDominance.toFixed(6)),
958
+ volumeScore: Number(volumeScore.toFixed(6)),
959
+ internalVariance: Number(internalVariance.toFixed(6)),
960
+ duplicationRatio: Number(duplicationRatio.toFixed(6)),
961
+ };
962
+ };
963
+
964
+ 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));
967
+ const intersection = Array.from(leftTags).filter((tag) => rightTags.has(tag)).length;
968
+ const union = new Set([...leftTags, ...rightTags]).size;
969
+ const jaccard = union > 0 ? intersection / union : 0;
970
+ const centroidSimilarity = computePackSemanticSimilarity(leftProfile, rightProfile);
971
+ return clampNumber(centroidSimilarity * 0.65 + jaccard * 0.35, 0, 1);
972
+ };
973
+
974
+ const parseFloat32EmbeddingBuffer = (rawBuffer, expectedDim = 0) => {
975
+ if (!Buffer.isBuffer(rawBuffer) || rawBuffer.length < 4) return null;
976
+ const total = Math.floor(rawBuffer.length / 4);
977
+ if (total <= 0) return null;
978
+ const size = expectedDim > 0 && expectedDim <= total ? expectedDim : total;
979
+ const vector = new Array(size);
980
+ for (let index = 0; index < size; index += 1) {
981
+ vector[index] = rawBuffer.readFloatLE(index * 4);
982
+ }
983
+ return vector;
984
+ };
985
+
986
+ const cosineSimilarityDense = (leftVector, rightVector) => {
987
+ if (!Array.isArray(leftVector) || !Array.isArray(rightVector) || !leftVector.length || !rightVector.length) return 0;
988
+ const size = Math.min(leftVector.length, rightVector.length);
989
+ if (size <= 0) return 0;
990
+
991
+ let dot = 0;
992
+ let leftNorm = 0;
993
+ let rightNorm = 0;
994
+ for (let index = 0; index < size; index += 1) {
995
+ const leftValue = Number(leftVector[index] || 0);
996
+ const rightValue = Number(rightVector[index] || 0);
997
+ dot += leftValue * rightValue;
998
+ leftNorm += leftValue * leftValue;
999
+ rightNorm += rightValue * rightValue;
1000
+ }
1001
+ if (leftNorm <= 0 || rightNorm <= 0) return 0;
1002
+ return Math.max(0, Math.min(1, dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm))));
1003
+ };
1004
+
1005
+ const getTopLabelEntries = (classification) => {
1006
+ if (!Array.isArray(classification?.top_labels)) return [];
1007
+ return classification.top_labels
1008
+ .map((entry) => ({
1009
+ label: String(entry?.label || '').trim(),
1010
+ score: Number(entry?.score),
1011
+ }))
1012
+ .filter((entry) => entry.label && Number.isFinite(entry.score))
1013
+ .sort((left, right) => right.score - left.score);
1014
+ };
1015
+
1016
+ const getLlmTraitTokens = (classification) => {
1017
+ const tokens = [];
1018
+ const addToken = (value, prefix = '') => {
1019
+ const normalized = normalizeTag(value);
1020
+ if (!normalized) return;
1021
+ const token = prefix ? `${prefix}:${normalized}` : normalized;
1022
+ if (!tokens.includes(token)) tokens.push(token);
1023
+ };
1024
+
1025
+ for (const item of Array.isArray(classification?.llm_subtags) ? classification.llm_subtags : []) {
1026
+ addToken(item, 'sub');
1027
+ }
1028
+ for (const item of Array.isArray(classification?.llm_style_traits) ? classification.llm_style_traits : []) {
1029
+ addToken(item, 'style');
1030
+ }
1031
+ for (const item of Array.isArray(classification?.llm_emotions) ? classification.llm_emotions : []) {
1032
+ addToken(item, 'emo');
1033
+ }
1034
+ return tokens;
1035
+ };
1036
+
1037
+ const getTagScoreEntries = (classification) => {
1038
+ if (!classification || typeof classification !== 'object') return [];
1039
+ const byTag = new Map();
1040
+
1041
+ const register = (tag, score) => {
1042
+ const normalizedTag = normalizeTag(tag);
1043
+ const numericScore = Number(score);
1044
+ if (!normalizedTag) return;
1045
+ if (!Number.isFinite(numericScore)) return;
1046
+ byTag.set(normalizedTag, Math.max(byTag.get(normalizedTag) || 0, numericScore));
1047
+ };
1048
+
1049
+ for (const [label, score] of Object.entries(classification.all_scores || {})) {
1050
+ register(toTagFromLabel(label), score);
1051
+ }
1052
+
1053
+ if (classification.category) {
1054
+ register(toTagFromLabel(classification.category), Number(classification.confidence || 0));
1055
+ }
1056
+
1057
+ for (const entry of getTopLabelEntries(classification)) {
1058
+ register(toTagFromLabel(entry.label), Number(entry.score || 0));
1059
+ }
1060
+
1061
+ if (classification.is_nsfw) {
1062
+ register('nsfw', Math.max(Number(classification.nsfw_score || 0), 0.7));
1063
+ }
1064
+
1065
+ for (const trait of getLlmTraitTokens(classification)) {
1066
+ register(trait, 0.12);
1067
+ }
1068
+
1069
+ return Array.from(byTag.entries())
1070
+ .map(([tag, score]) => [tag, Number(Number(score).toFixed(6))])
1071
+ .sort((left, right) => right[1] - left[1]);
1072
+ };
1073
+
1074
+ const vectorNorm = (vector) =>
1075
+ Math.sqrt(
1076
+ Object.values(vector || {})
1077
+ .map((value) => Number(value))
1078
+ .filter((value) => Number.isFinite(value))
1079
+ .reduce((sum, value) => sum + value * value, 0),
1080
+ );
1081
+
1082
+ const cosineSimilarity = (left, right) => {
1083
+ if (!left || !right) return 0;
1084
+ const leftKeys = Object.keys(left || {});
1085
+ const rightKeys = Object.keys(right || {});
1086
+ if (!leftKeys.length || !rightKeys.length) return 0;
1087
+
1088
+ const leftNorm = vectorNorm(left);
1089
+ const rightNorm = vectorNorm(right);
1090
+ if (leftNorm <= 0 || rightNorm <= 0) return 0;
1091
+
1092
+ let dot = 0;
1093
+ for (const key of leftKeys) {
1094
+ const leftValue = Number(left[key]);
1095
+ const rightValue = Number(right[key]);
1096
+ if (!Number.isFinite(leftValue) || !Number.isFinite(rightValue)) continue;
1097
+ dot += leftValue * rightValue;
1098
+ }
1099
+ return Math.max(0, Math.min(1, dot / (leftNorm * rightNorm)));
1100
+ };
1101
+
1102
+ const getScoreByTag = (classification, tag) => {
1103
+ const normalizedTag = normalizeTag(tag);
1104
+ if (!normalizedTag) return 0;
1105
+ const entries = getTagScoreEntries(classification);
1106
+ const found = entries.find(([entryTag]) => entryTag === normalizedTag);
1107
+ return Number(found?.[1] || 0);
1108
+ };
1109
+
1110
+ const buildTopTags = (classification) => {
1111
+ const decorated = decorateStickerClassification(classification || null);
1112
+ const decoratedTags = Array.isArray(decorated?.tags) ? decorated.tags.map((tag) => normalizeTag(tag)).filter(Boolean) : [];
1113
+ const ranked = getTagScoreEntries(classification)
1114
+ .filter(([tag]) => !TECHNICAL_TAGS.has(tag))
1115
+ .map(([tag, score]) => ({ tag, score }));
1116
+
1117
+ const tags = [];
1118
+ for (const entry of ranked) {
1119
+ if (tags.some((item) => item.tag === entry.tag)) continue;
1120
+ tags.push(entry);
1121
+ if (tags.length >= TOP_TAGS_PER_ASSET) break;
1122
+ }
1123
+
1124
+ for (const tag of decoratedTags) {
1125
+ if (TECHNICAL_TAGS.has(tag)) continue;
1126
+ if (tags.some((item) => item.tag === tag)) continue;
1127
+ tags.push({ tag, score: Number(classification?.confidence || 0) });
1128
+ if (tags.length >= TOP_TAGS_PER_ASSET) break;
1129
+ }
1130
+
1131
+ for (const topLabel of getTopLabelEntries(classification)) {
1132
+ const mappedTag = toTagFromLabel(topLabel.label);
1133
+ if (!mappedTag || TECHNICAL_TAGS.has(mappedTag)) continue;
1134
+ if (tags.some((item) => item.tag === mappedTag)) continue;
1135
+ tags.push({ tag: mappedTag, score: Number(topLabel.score || 0) });
1136
+ if (tags.length >= TOP_TAGS_PER_ASSET) break;
1137
+ }
1138
+
1139
+ return tags.slice(0, TOP_TAGS_PER_ASSET);
1140
+ };
1141
+
1142
+ const evaluateQualityGate = (asset, classification) => {
1143
+ const width = Number(asset?.width || 0);
1144
+ const height = Number(asset?.height || 0);
1145
+ const sizeBytes = Number(asset?.size_bytes || 0);
1146
+ const area = width > 0 && height > 0 ? width * height : 0;
1147
+ const blurryScore = getScoreByTag(classification, 'blurry-image');
1148
+ const lowQualityScore = getScoreByTag(classification, 'low-quality-compressed-image');
1149
+
1150
+ if ((width && width < MIN_ASSET_EDGE) || (height && height < MIN_ASSET_EDGE)) {
1151
+ return { accepted: false, reason: 'min_edge', qualityScore: 0 };
1152
+ }
1153
+ if (area && area < MIN_ASSET_AREA) {
1154
+ return { accepted: false, reason: 'min_area', qualityScore: 0 };
1155
+ }
1156
+ if (sizeBytes && sizeBytes < MIN_ASSET_BYTES) {
1157
+ return { accepted: false, reason: 'min_size_bytes', qualityScore: 0 };
1158
+ }
1159
+ if (blurryScore > MAX_BLURRY_SCORE) {
1160
+ return { accepted: false, reason: 'blurry', qualityScore: 0 };
1161
+ }
1162
+ if (lowQualityScore > MAX_LOW_QUALITY_SCORE) {
1163
+ return { accepted: false, reason: 'low_quality', qualityScore: 0 };
1164
+ }
1165
+
1166
+ const qualityPenalty = Math.min(0.6, blurryScore * 0.35 + lowQualityScore * 0.35);
1167
+ const qualityScore = Number(Math.max(0.2, 1 - qualityPenalty).toFixed(6));
1168
+ return { accepted: true, reason: null, qualityScore };
1169
+ };
1170
+
1171
+ const withThemeInTopTags = (topTags, theme, scoreHint = 0) => {
1172
+ const normalizedTheme = normalizeTag(theme);
1173
+ if (!normalizedTheme) return Array.isArray(topTags) ? topTags.slice(0, TOP_TAGS_PER_ASSET) : [];
1174
+
1175
+ const list = [];
1176
+ const seen = new Set();
1177
+ const seed = {
1178
+ tag: normalizedTheme,
1179
+ score: Number(Number(clampNumber(Number(scoreHint || 0), 0, 1.2)).toFixed(6)),
1180
+ };
1181
+ for (const entry of [seed, ...(Array.isArray(topTags) ? topTags : [])]) {
1182
+ const tag = normalizeTag(entry?.tag || entry);
1183
+ if (!tag || seen.has(tag)) continue;
1184
+ seen.add(tag);
1185
+ list.push({
1186
+ tag,
1187
+ score: Number(Number(entry?.score || 0).toFixed(6)),
1188
+ });
1189
+ }
1190
+
1191
+ if (!list.length) return [{ tag: normalizedTheme, score: seed.score }];
1192
+ list.sort((left, right) => {
1193
+ if (left.tag === normalizedTheme) return -1;
1194
+ if (right.tag === normalizedTheme) return 1;
1195
+ return Number(right.score || 0) - Number(left.score || 0);
1196
+ });
1197
+ return list.slice(0, TOP_TAGS_PER_ASSET);
1198
+ };
1199
+
1200
+ const deriveThemeFromClassification = (classification) => {
1201
+ const topTags = buildTopTags(classification);
1202
+ const nsfwScore = Number(classification?.nsfw_score || 0);
1203
+ const isNsfw = classification?.is_nsfw === true || nsfwScore >= NSFW_THRESHOLD || nsfwScore >= NSFW_SUGGESTIVE_THRESHOLD;
1204
+ if (isNsfw) {
1205
+ const nsfwLevel = nsfwScore >= NSFW_EXPLICIT_THRESHOLD ? 'explicit' : 'suggestive';
1206
+ return {
1207
+ theme: `nsfw-${nsfwLevel}`,
1208
+ subtheme: topTags.find((entry) => entry.tag !== 'nsfw')?.tag || '',
1209
+ topTags,
1210
+ nsfwScore,
1211
+ nsfwLevel,
1212
+ };
1213
+ }
1214
+
1215
+ const semanticClusterId = toNumericClusterId(classification?.semantic_cluster_id);
1216
+ const semanticClusterSlug = normalizeTag(classification?.semantic_cluster_slug || '');
1217
+ const categoryTag = classification?.category ? toTagFromLabel(classification.category) : '';
1218
+ const fallbackPrimary = topTags.find((entry) => entry.tag !== 'nsfw')?.tag || '';
1219
+
1220
+ if (ENABLE_SEMANTIC_CLUSTERING && semanticClusterId) {
1221
+ const primary = semanticClusterSlug || categoryTag || fallbackPrimary || `cluster-${semanticClusterId}`;
1222
+ const secondary = topTags.find((entry) => entry.tag !== primary && entry.tag !== 'nsfw')?.tag || '';
1223
+ 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
+ );
1233
+ return {
1234
+ theme: primary,
1235
+ subtheme: secondary,
1236
+ topTags: semanticTopTags,
1237
+ nsfwScore,
1238
+ nsfwLevel: 'safe',
1239
+ semanticClusterId,
1240
+ semanticThemeKey,
1241
+ };
1242
+ }
1243
+
1244
+ const primary = (ENABLE_SEMANTIC_CLUSTERING ? categoryTag : '') || fallbackPrimary;
1245
+ 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
+ );
1255
+ return {
1256
+ theme: primary,
1257
+ subtheme: secondary,
1258
+ topTags: fallbackTopTags,
1259
+ nsfwScore,
1260
+ nsfwLevel: 'safe',
1261
+ };
1262
+ };
1263
+
1264
+ const dedupeCandidatesByEmbedding = (candidates, { embeddingByImageHash = new Map() } = {}) => {
1265
+ if (!Array.isArray(candidates) || candidates.length <= 1) {
1266
+ return { deduped: Array.isArray(candidates) ? candidates : [], duplicateRate: 0, dropped: 0 };
1267
+ }
1268
+
1269
+ const deduped = [];
1270
+ let dropped = 0;
1271
+ for (const candidate of candidates) {
1272
+ const candidateImageHash = String(candidate?.classification?.image_hash || '').trim().toLowerCase();
1273
+ const candidateDense = candidateImageHash ? embeddingByImageHash.get(candidateImageHash) : null;
1274
+ const candidateSparse = candidate?.classification?.all_scores || {};
1275
+ const isDuplicate = deduped.some((entry) => {
1276
+ const entryImageHash = String(entry?.classification?.image_hash || '').trim().toLowerCase();
1277
+ const entryDense = entryImageHash ? embeddingByImageHash.get(entryImageHash) : null;
1278
+ if (candidateDense && entryDense) {
1279
+ return cosineSimilarityDense(candidateDense, entryDense) >= DEDUPE_SIMILARITY_THRESHOLD;
1280
+ }
1281
+ const entrySparse = entry?.classification?.all_scores || {};
1282
+ return cosineSimilarity(candidateSparse, entrySparse) >= DEDUPE_SIMILARITY_THRESHOLD;
1283
+ });
1284
+
1285
+ if (isDuplicate) {
1286
+ dropped += 1;
1287
+ continue;
1288
+ }
1289
+ deduped.push(candidate);
1290
+ }
1291
+
1292
+ const duplicateRate = candidates.length > 0 ? dropped / candidates.length : 0;
1293
+ return {
1294
+ deduped,
1295
+ duplicateRate: Number(duplicateRate.toFixed(6)),
1296
+ dropped,
1297
+ };
1298
+ };
1299
+
1300
+ const computeAssetQualityForTheme = ({ classification, theme, subtheme, topTags = [] }) => {
1301
+ const confidence = Number(classification?.confidence || 0);
1302
+ const affinityWeight = normalizeAffinityWeight(classification?.affinity_weight);
1303
+ const entropyNormalized = normalizeEntropy({
1304
+ entropy: classification?.entropy,
1305
+ entropyNormalized: classification?.entropy_normalized,
1306
+ topLabelCount: Array.isArray(classification?.top_labels) ? classification.top_labels.length : 0,
1307
+ });
1308
+ const topLabels = getTopLabelEntries(classification);
1309
+ const topLabelThemeScore = topLabels.reduce((sum, entry) => {
1310
+ const mapped = toTagFromLabel(entry.label);
1311
+ if (mapped === theme || (subtheme && mapped === subtheme)) {
1312
+ return sum + Number(entry.score || 0);
1313
+ }
1314
+ return sum;
1315
+ }, 0);
1316
+ const rankedThemeScore = Number((Array.isArray(topTags) ? topTags : []).find((entry) => entry.tag === theme)?.score || 0);
1317
+ 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;
1323
+
1324
+ return {
1325
+ confidence,
1326
+ affinityWeight,
1327
+ entropyNormalized,
1328
+ thematicAlignment,
1329
+ assetQuality,
1330
+ };
1331
+ };
1332
+
1333
+ const scoreCandidate = ({ classification, theme, subtheme, topTags, qualityScore }) => {
1334
+ const {
1335
+ affinityWeight,
1336
+ entropyNormalized,
1337
+ assetQuality,
1338
+ } = computeAssetQualityForTheme({ classification, theme, subtheme, topTags });
1339
+ const confidenceMargin = Math.max(0, Number(classification?.confidence_margin || 0));
1340
+ const ambiguityPenalty = (classification?.ambiguous ? AMBIGUOUS_FLAG_PENALTY : 0) + entropyNormalized * ENTROPY_WEIGHT;
1341
+
1342
+ const similarImages = Array.isArray(classification?.similar_images) ? classification.similar_images : [];
1343
+ const maxSimilarity = similarImages.reduce((max, entry) => {
1344
+ const value = Number(entry?.similarity || 0);
1345
+ return Number.isFinite(value) ? Math.max(max, value) : max;
1346
+ }, 0);
1347
+ const similarImagesPenalty = maxSimilarity * SIMILAR_IMAGES_PENALTY_WEIGHT;
1348
+
1349
+ const adaptiveBonus = affinityWeight * ADAPTIVE_BONUS_WEIGHT;
1350
+ 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;
1358
+ return Number(clampNumber(finalScore, 0, 1.2).toFixed(6));
1359
+ };
1360
+
1361
+ const collectCuratableCandidates = async ({ includePacked = true, includeUnpacked = true } = {}) => {
1362
+ if (!includePacked && !includeUnpacked) {
1363
+ return {
1364
+ grouped: new Map(),
1365
+ stats: {
1366
+ assets_scanned: 0,
1367
+ assets_unique_scanned: 0,
1368
+ assets_rejected_quality: 0,
1369
+ assets_rejected_no_theme: 0,
1370
+ assets_grouped: 0,
1371
+ review_sample_percent: REVIEW_SAMPLE_PERCENT,
1372
+ review_version_target: REVIEW_VERSION_TARGET || null,
1373
+ review_mode: 'disabled',
1374
+ assets_total_seen: 0,
1375
+ assets_version_mismatch_scanned: 0,
1376
+ reject_reason_counts: {},
1377
+ include_packed: false,
1378
+ include_unpacked: false,
1379
+ scan_passes_requested: SCAN_PASSES,
1380
+ scan_passes_effective: 0,
1381
+ },
1382
+ };
1383
+ }
1384
+
1385
+ const grouped = new Map();
1386
+ const pageLimit = 400;
1387
+ const passCount = Math.max(1, SCAN_PASSES);
1388
+ const effectiveCapPerPass = MAX_SCAN_ASSETS > 0 ? MAX_SCAN_ASSETS : Number.POSITIVE_INFINITY;
1389
+ const globalSeenAssetIds = new Set();
1390
+ const assetStatsById = new Map();
1391
+
1392
+ const stats = {
1393
+ assets_scanned: 0,
1394
+ assets_unique_scanned: 0,
1395
+ assets_rejected_quality: 0,
1396
+ assets_rejected_no_theme: 0,
1397
+ assets_grouped: 0,
1398
+ review_sample_percent: REVIEW_SAMPLE_PERCENT,
1399
+ review_version_target: REVIEW_VERSION_TARGET || null,
1400
+ review_mode: MAX_SCAN_ASSETS === 0 ? 'full_scan' : 'bounded_scan',
1401
+ assets_total_seen: 0,
1402
+ assets_version_mismatch_scanned: 0,
1403
+ reject_reason_counts: {},
1404
+ include_packed: Boolean(includePacked),
1405
+ include_unpacked: Boolean(includeUnpacked),
1406
+ scan_passes_requested: SCAN_PASSES,
1407
+ scan_passes_effective: passCount,
1408
+ scan_pass_jitter_percent: SCAN_PASS_JITTER_PERCENT,
1409
+ stability_z_score: STABILITY_Z_SCORE,
1410
+ min_asset_acceptance_rate: EFFECTIVE_MIN_ASSET_ACCEPTANCE_RATE,
1411
+ min_theme_dominance_ratio: EFFECTIVE_MIN_THEME_DOMINANCE_RATIO,
1412
+ score_stddev_penalty: EFFECTIVE_SCORE_STDDEV_PENALTY,
1413
+ pass_assets_scanned: [],
1414
+ pass_assets_unique: [],
1415
+ };
1416
+
1417
+ const ensureAssetStats = (asset, classification) => {
1418
+ let current = assetStatsById.get(asset.id);
1419
+ if (!current) {
1420
+ current = {
1421
+ asset,
1422
+ classification,
1423
+ acceptedCount: 0,
1424
+ themes: new Map(),
1425
+ };
1426
+ assetStatsById.set(asset.id, current);
1427
+ }
1428
+ return current;
1429
+ };
1430
+
1431
+ const ensureThemeStats = (assetStats, { themeKey, theme, subtheme }) => {
1432
+ let current = assetStats.themes.get(themeKey);
1433
+ if (!current) {
1434
+ current = {
1435
+ themeKey,
1436
+ theme,
1437
+ subtheme,
1438
+ votes: 0,
1439
+ scoreSum: 0,
1440
+ scoreSqSum: 0,
1441
+ qualitySum: 0,
1442
+ confidenceSum: 0,
1443
+ themeScoreSum: 0,
1444
+ nsfwScoreSum: 0,
1445
+ entropySum: 0,
1446
+ confidenceMarginSum: 0,
1447
+ affinityWeightSum: 0,
1448
+ ambiguityVotes: 0,
1449
+ similarPenaltySum: 0,
1450
+ subthemeVotes: new Map(),
1451
+ topTagVotes: new Map(),
1452
+ };
1453
+ assetStats.themes.set(themeKey, current);
1454
+ }
1455
+ return current;
1456
+ };
1457
+
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
+ }) => {
1477
+ const assetStats = ensureAssetStats(asset, classification);
1478
+ const themeStats = ensureThemeStats(assetStats, { themeKey, theme, subtheme });
1479
+ const passWeight = resolvePassScoreWeight(asset.id, passIndex);
1480
+ const weightedScore = clampNumber(score * passWeight, 0, 1.2);
1481
+
1482
+ themeStats.votes += 1;
1483
+ themeStats.scoreSum += weightedScore;
1484
+ themeStats.scoreSqSum += weightedScore * weightedScore;
1485
+ themeStats.qualitySum += Number(qualityScore || 0);
1486
+ themeStats.confidenceSum += Number(classification?.confidence || 0);
1487
+ themeStats.themeScoreSum += Number(themeScore || 0);
1488
+ themeStats.nsfwScoreSum += Number(nsfwScore || 0);
1489
+ themeStats.entropySum += Number(entropy || 0);
1490
+ themeStats.confidenceMarginSum += Number(confidenceMargin || 0);
1491
+ themeStats.affinityWeightSum += Number(affinityWeight || 0);
1492
+ themeStats.similarPenaltySum += Number(similarPenalty || 0);
1493
+ if (ambiguous) themeStats.ambiguityVotes += 1;
1494
+ if (subtheme) {
1495
+ themeStats.subthemeVotes.set(subtheme, (themeStats.subthemeVotes.get(subtheme) || 0) + 1);
1496
+ }
1497
+ for (const tagEntry of Array.isArray(topTags) ? topTags : []) {
1498
+ const tag = normalizeTag(tagEntry?.tag || tagEntry);
1499
+ if (!tag) continue;
1500
+ themeStats.topTagVotes.set(tag, (themeStats.topTagVotes.get(tag) || 0) + 1);
1501
+ }
1502
+
1503
+ assetStats.acceptedCount += 1;
1504
+ assetStats.classification = classification;
1505
+ assetStats.asset = asset;
1506
+ assetStats.lastNsfwLevel = nsfwLevel;
1507
+ };
1508
+
1509
+ const processAssetsPage = async ({ page, passIndex, passSeenIds, scannedInPass }) => {
1510
+ const assets = Array.isArray(page?.assets) ? page.assets : [];
1511
+ if (!assets.length) return { processedRows: 0, scannedUnique: 0, done: true };
1512
+
1513
+ const classifications = await listStickerClassificationsByAssetIds(assets.map((asset) => asset.id));
1514
+ const byAssetId = new Map(classifications.map((entry) => [entry.asset_id, entry]));
1515
+ let scannedUnique = 0;
1516
+
1517
+ for (const asset of assets) {
1518
+ if (!asset?.id || passSeenIds.has(asset.id)) continue;
1519
+ if (scannedInPass + scannedUnique >= effectiveCapPerPass) {
1520
+ return { processedRows: assets.length, scannedUnique, done: true };
1521
+ }
1522
+
1523
+ passSeenIds.add(asset.id);
1524
+ scannedUnique += 1;
1525
+ stats.assets_scanned += 1;
1526
+ globalSeenAssetIds.add(asset.id);
1527
+ stats.assets_unique_scanned = globalSeenAssetIds.size;
1528
+ stats.assets_total_seen = globalSeenAssetIds.size;
1529
+
1530
+ const classification = byAssetId.get(asset.id);
1531
+ if (!classification) {
1532
+ stats.reject_reason_counts.missing_classification = (stats.reject_reason_counts.missing_classification || 0) + 1;
1533
+ continue;
1534
+ }
1535
+
1536
+ if (REVIEW_VERSION_TARGET && String(classification.classification_version || '').trim() !== REVIEW_VERSION_TARGET) {
1537
+ stats.assets_version_mismatch_scanned += 1;
1538
+ }
1539
+
1540
+ const quality = evaluateQualityGate(asset, classification);
1541
+ if (!quality.accepted) {
1542
+ const reason = quality.reason || 'unknown';
1543
+ stats.assets_rejected_quality += 1;
1544
+ stats.reject_reason_counts[reason] = (stats.reject_reason_counts[reason] || 0) + 1;
1545
+ continue;
1546
+ }
1547
+
1548
+ const {
1549
+ theme,
1550
+ subtheme,
1551
+ topTags,
1552
+ nsfwScore,
1553
+ nsfwLevel,
1554
+ semanticThemeKey = '',
1555
+ } = deriveThemeFromClassification(classification);
1556
+ if (!theme) {
1557
+ stats.assets_rejected_no_theme += 1;
1558
+ continue;
1559
+ }
1560
+
1561
+ const themeKey = semanticThemeKey || buildThemeKey(theme, subtheme);
1562
+ const themeScore = Number(topTags.find((entry) => entry.tag === theme)?.score || 0);
1563
+ if (themeScore < MOVE_IN_THEME_SCORE_THRESHOLD) {
1564
+ stats.reject_reason_counts.low_theme_match = (stats.reject_reason_counts.low_theme_match || 0) + 1;
1565
+ continue;
1566
+ }
1567
+
1568
+ const score = scoreCandidate({
1569
+ classification,
1570
+ theme,
1571
+ subtheme,
1572
+ topTags,
1573
+ qualityScore: quality.qualityScore,
1574
+ });
1575
+ const entropyNormalized = normalizeEntropy({
1576
+ entropy: classification?.entropy,
1577
+ entropyNormalized: classification?.entropy_normalized,
1578
+ topLabelCount: Array.isArray(classification?.top_labels) ? classification.top_labels.length : 0,
1579
+ });
1580
+ const similarImages = Array.isArray(classification?.similar_images) ? classification.similar_images : [];
1581
+ const maxSimilarity = similarImages.reduce((max, entry) => {
1582
+ const value = Number(entry?.similarity || 0);
1583
+ return Number.isFinite(value) ? Math.max(max, value) : max;
1584
+ }, 0);
1585
+ registerCandidate({
1586
+ asset,
1587
+ classification,
1588
+ passIndex,
1589
+ theme,
1590
+ subtheme,
1591
+ themeKey,
1592
+ topTags,
1593
+ qualityScore: quality.qualityScore,
1594
+ themeScore,
1595
+ score,
1596
+ nsfwScore,
1597
+ nsfwLevel,
1598
+ entropy: entropyNormalized,
1599
+ confidenceMargin: Number(classification?.confidence_margin || 0),
1600
+ affinityWeight: normalizeAffinityWeight(classification?.affinity_weight),
1601
+ ambiguous:
1602
+ classification?.ambiguous === true
1603
+ || classification?.ambiguous === 1
1604
+ || entropyNormalized > ENTROPY_NORMALIZED_THRESHOLD,
1605
+ similarPenalty: maxSimilarity * SIMILAR_IMAGES_PENALTY_WEIGHT,
1606
+ });
1607
+ }
1608
+
1609
+ return {
1610
+ processedRows: assets.length,
1611
+ scannedUnique,
1612
+ done: !page?.hasMore || scannedInPass + scannedUnique >= effectiveCapPerPass,
1613
+ };
1614
+ };
1615
+
1616
+ const runSingleScanPass = async (passIndex) => {
1617
+ let scannedInPass = 0;
1618
+ let offset = 0;
1619
+ let versionOffset = 0;
1620
+ const passSeenIds = new Set();
1621
+
1622
+ if (REVIEW_VERSION_TARGET) {
1623
+ while (scannedInPass < effectiveCapPerPass) {
1624
+ const remaining = Number.isFinite(effectiveCapPerPass)
1625
+ ? Math.max(0, effectiveCapPerPass - scannedInPass)
1626
+ : pageLimit;
1627
+ const limit = Math.max(1, Math.min(pageLimit, remaining || pageLimit));
1628
+ const page = await listClassifiedStickerAssetsForCuration({
1629
+ limit,
1630
+ offset: versionOffset,
1631
+ includePacked,
1632
+ includeUnpacked,
1633
+ onlyVersionMismatch: REVIEW_VERSION_TARGET,
1634
+ });
1635
+ const result = await processAssetsPage({
1636
+ page,
1637
+ passIndex,
1638
+ passSeenIds,
1639
+ scannedInPass,
1640
+ });
1641
+ scannedInPass += result.scannedUnique;
1642
+ if (!result.processedRows || result.done) break;
1643
+ versionOffset += result.processedRows;
1644
+ }
1645
+ }
1646
+
1647
+ while (scannedInPass < effectiveCapPerPass) {
1648
+ const remaining = Number.isFinite(effectiveCapPerPass)
1649
+ ? Math.max(0, effectiveCapPerPass - scannedInPass)
1650
+ : pageLimit;
1651
+ const limit = Math.max(1, Math.min(pageLimit, remaining || pageLimit));
1652
+ const page = await listClassifiedStickerAssetsForCuration({
1653
+ limit,
1654
+ offset,
1655
+ includePacked,
1656
+ includeUnpacked,
1657
+ });
1658
+ const result = await processAssetsPage({
1659
+ page,
1660
+ passIndex,
1661
+ passSeenIds,
1662
+ scannedInPass,
1663
+ });
1664
+ scannedInPass += result.scannedUnique;
1665
+ if (!result.processedRows || result.done) break;
1666
+ offset += result.processedRows;
1667
+ }
1668
+
1669
+ return {
1670
+ scannedInPass,
1671
+ uniqueSeenInPass: passSeenIds.size,
1672
+ };
1673
+ };
1674
+
1675
+ for (let passIndex = 0; passIndex < passCount; passIndex += 1) {
1676
+ const passResult = await runSingleScanPass(passIndex);
1677
+ stats.pass_assets_scanned.push(passResult.scannedInPass);
1678
+ stats.pass_assets_unique.push(passResult.uniqueSeenInPass);
1679
+ }
1680
+
1681
+ const meanPassScan =
1682
+ stats.pass_assets_scanned.reduce((sum, value) => sum + Number(value || 0), 0) / Math.max(1, stats.pass_assets_scanned.length);
1683
+ stats.assets_scanned_avg_per_pass = Number(meanPassScan.toFixed(2));
1684
+ stats.assets_unique_scanned = globalSeenAssetIds.size;
1685
+
1686
+ for (const assetStats of assetStatsById.values()) {
1687
+ const themeEntries = Array.from(assetStats.themes.values());
1688
+ if (!themeEntries.length) continue;
1689
+
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];
1699
+
1700
+ if (!dominantTheme || dominantTheme.votes <= 0) continue;
1701
+
1702
+ const acceptedVotes = Number(dominantTheme.votes || 0);
1703
+ const totalThemeVotes = themeEntries.reduce((sum, entry) => sum + Number(entry.votes || 0), 0);
1704
+ const acceptanceRate = acceptedVotes / Math.max(1, passCount);
1705
+ const dominanceRatio = acceptedVotes / Math.max(1, totalThemeVotes);
1706
+ if (acceptanceRate < EFFECTIVE_MIN_ASSET_ACCEPTANCE_RATE) {
1707
+ stats.reject_reason_counts.low_acceptance_rate = (stats.reject_reason_counts.low_acceptance_rate || 0) + 1;
1708
+ continue;
1709
+ }
1710
+ if (dominanceRatio < EFFECTIVE_MIN_THEME_DOMINANCE_RATIO) {
1711
+ stats.reject_reason_counts.unstable_theme_vote = (stats.reject_reason_counts.unstable_theme_vote || 0) + 1;
1712
+ continue;
1713
+ }
1714
+
1715
+ const meanScore = dominantTheme.scoreSum / acceptedVotes;
1716
+ const variance = Math.max(0, dominantTheme.scoreSqSum / acceptedVotes - meanScore * meanScore);
1717
+ const stdDev = Math.sqrt(variance);
1718
+ const stdError = stdDev / Math.sqrt(Math.max(1, acceptedVotes));
1719
+ const lowerBoundScore = meanScore - STABILITY_Z_SCORE * stdError;
1720
+ const avgQuality = dominantTheme.qualitySum / acceptedVotes;
1721
+ const avgConfidence = dominantTheme.confidenceSum / acceptedVotes;
1722
+ const avgThemeScore = dominantTheme.themeScoreSum / acceptedVotes;
1723
+ const avgNsfwScore = dominantTheme.nsfwScoreSum / acceptedVotes;
1724
+ const avgEntropy = dominantTheme.entropySum / acceptedVotes;
1725
+ const avgConfidenceMargin = dominantTheme.confidenceMarginSum / acceptedVotes;
1726
+ const avgAffinityWeight = dominantTheme.affinityWeightSum / acceptedVotes;
1727
+ const avgSimilarPenalty = dominantTheme.similarPenaltySum / acceptedVotes;
1728
+ const ambiguousRatio = dominantTheme.ambiguityVotes / acceptedVotes;
1729
+ 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;
1746
+ const stabilityMultiplier = 0.72 + stabilityFactor * 0.28;
1747
+ const variancePenalty = Math.max(0.4, 1 - Math.min(0.55, stdDev * EFFECTIVE_SCORE_STDDEV_PENALTY));
1748
+ const robustScore = clampNumber(robustScoreRaw * stabilityMultiplier * variancePenalty, 0, 1.2);
1749
+
1750
+ const dominantSubtheme = Array.from(dominantTheme.subthemeVotes.entries())
1751
+ .sort((left, right) => right[1] - left[1])[0]?.[0] || dominantTheme.subtheme || '';
1752
+ const topTags = buildTopTags(assetStats.classification);
1753
+ if (!topTags.some((entry) => entry?.tag === dominantTheme.theme)) {
1754
+ topTags.unshift({ tag: dominantTheme.theme, score: avgThemeScore });
1755
+ }
1756
+ if (dominantSubtheme && !topTags.some((entry) => entry?.tag === dominantSubtheme)) {
1757
+ topTags.splice(1, 0, { tag: dominantSubtheme, score: avgThemeScore * 0.85 });
1758
+ }
1759
+
1760
+ const themeKey = buildThemeKey(dominantTheme.theme, dominantSubtheme);
1761
+ const list = grouped.get(themeKey) || [];
1762
+ list.push({
1763
+ asset: assetStats.asset,
1764
+ classification: assetStats.classification,
1765
+ theme: dominantTheme.theme,
1766
+ subtheme: dominantSubtheme,
1767
+ themeKey,
1768
+ topTags: topTags.slice(0, TOP_TAGS_PER_ASSET),
1769
+ qualityScore: Number(avgQuality.toFixed(6)),
1770
+ themeScore: Number(avgThemeScore.toFixed(6)),
1771
+ score: Number(robustScore.toFixed(6)),
1772
+ nsfwScore: Number(avgNsfwScore.toFixed(6)),
1773
+ nsfwLevel: assetStats.lastNsfwLevel || 'safe',
1774
+ acceptanceRate: Number(acceptanceRate.toFixed(6)),
1775
+ dominanceRatio: Number(dominanceRatio.toFixed(6)),
1776
+ scoreStdDev: Number(stdDev.toFixed(6)),
1777
+ scoreMean: Number(meanScore.toFixed(6)),
1778
+ scoreLowerBound: Number(lowerBoundScore.toFixed(6)),
1779
+ stabilityFactor: Number(stabilityFactor.toFixed(6)),
1780
+ harmonicSignal: Number(harmonicSignal.toFixed(6)),
1781
+ passVotes: acceptedVotes,
1782
+ avgEntropy: Number(avgEntropy.toFixed(6)),
1783
+ avgConfidenceMargin: Number(avgConfidenceMargin.toFixed(6)),
1784
+ avgAffinityWeight: Number(avgAffinityWeight.toFixed(6)),
1785
+ ambiguousRatio: Number(ambiguousRatio.toFixed(6)),
1786
+ });
1787
+ grouped.set(themeKey, list);
1788
+ stats.assets_grouped += 1;
1789
+ }
1790
+
1791
+ const imageHashes = Array.from(
1792
+ new Set(
1793
+ Array.from(grouped.values())
1794
+ .flat()
1795
+ .map((candidate) => String(candidate?.classification?.image_hash || '').trim().toLowerCase())
1796
+ .filter((hash) => hash.length === 64),
1797
+ ),
1798
+ );
1799
+ const embeddingByImageHash = new Map();
1800
+ if (imageHashes.length) {
1801
+ const rows = await listClipImageEmbeddingsByImageHashes(imageHashes);
1802
+ for (const row of rows) {
1803
+ const imageHash = String(row?.image_hash || '').trim().toLowerCase();
1804
+ if (!imageHash) continue;
1805
+ const embedding = parseFloat32EmbeddingBuffer(row?.embedding, Number(row?.embedding_dim || 0));
1806
+ if (!embedding?.length) continue;
1807
+ embeddingByImageHash.set(imageHash, embedding);
1808
+ }
1809
+ }
1810
+
1811
+ let dedupeDropped = 0;
1812
+ for (const [groupKey, list] of grouped.entries()) {
1813
+ const { deduped, duplicateRate, dropped } = dedupeCandidatesByEmbedding(list, { embeddingByImageHash });
1814
+ dedupeDropped += dropped;
1815
+ stats.reject_reason_counts.duplicate_embedding = (stats.reject_reason_counts.duplicate_embedding || 0) + dropped;
1816
+
1817
+ const normalizedList = deduped.map((candidate) => ({
1818
+ ...candidate,
1819
+ duplicateRate,
1820
+ }));
1821
+ normalizedList.sort((left, right) => {
1822
+ if (right.score !== left.score) return right.score - left.score;
1823
+ return String(right.asset?.created_at || '').localeCompare(String(left.asset?.created_at || ''));
1824
+ });
1825
+ grouped.set(groupKey, normalizedList);
1826
+ }
1827
+
1828
+ stats.assets_deduped = dedupeDropped;
1829
+ stats.assets_unique_scanned = globalSeenAssetIds.size;
1830
+ stats.assets_total_seen = globalSeenAssetIds.size;
1831
+
1832
+ return { grouped, stats };
1833
+ };
1834
+
1835
+ const computeGroupMetrics = (themeKey, candidates) => {
1836
+ const first = candidates[0] || {};
1837
+ const theme = first.theme || parseThemeKey(themeKey).theme || '';
1838
+ const subtheme = first.subtheme || parseThemeKey(themeKey).subtheme || '';
1839
+ const size = candidates.length;
1840
+ if (!size) {
1841
+ return { theme, subtheme, themeKey, groupScore: 0, cohesion: 0, avgConfidence: 0, avgQuality: 0 };
1842
+ }
1843
+
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;
1854
+ 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;
1858
+ const avgQuality = candidates.reduce((sum, candidate) => sum + Number(candidate.qualityScore || 0), 0) / size;
1859
+ const avgDuplicateRate = candidates.reduce((sum, candidate) => sum + Number(candidate.duplicateRate || 0), 0) / size;
1860
+ let semanticSimilaritySum = 0;
1861
+ let semanticPairs = 0;
1862
+ for (let i = 0; i < candidates.length; i += 1) {
1863
+ for (let j = i + 1; j < candidates.length; j += 1) {
1864
+ semanticPairs += 1;
1865
+ semanticSimilaritySum += cosineSimilarity(candidates[i]?.classification?.all_scores || {}, candidates[j]?.classification?.all_scores || {});
1866
+ }
1867
+ }
1868
+ const semanticCohesion = semanticPairs > 0 ? semanticSimilaritySum / semanticPairs : 1;
1869
+ const cooccurrence = new Map();
1870
+ for (const candidate of candidates) {
1871
+ const localSecondary = (candidate.topTags || [])
1872
+ .map((entry) => entry.tag)
1873
+ .find((tag) => tag && tag !== theme && tag !== 'nsfw');
1874
+ if (!localSecondary) continue;
1875
+ cooccurrence.set(localSecondary, (cooccurrence.get(localSecondary) || 0) + 1);
1876
+ }
1877
+ const bestCooccurrence = Math.max(0, ...cooccurrence.values());
1878
+ const topicalCohesion = size ? bestCooccurrence / size : 0;
1879
+ const cohesion = topicalCohesion * 0.45 + semanticCohesion * 0.55;
1880
+ const traitVotes = new Map();
1881
+ for (const candidate of candidates) {
1882
+ for (const token of getLlmTraitTokens(candidate.classification)) {
1883
+ traitVotes.set(token, (traitVotes.get(token) || 0) + 1);
1884
+ }
1885
+ }
1886
+ const strongestTraitRatio = size
1887
+ ? Math.max(0, ...Array.from(traitVotes.values()).map((value) => value / size))
1888
+ : 0;
1889
+ const semanticBoost = strongestTraitRatio * LLM_TRAIT_WEIGHT;
1890
+ const subthemeFromCooccurrence = Array.from(cooccurrence.entries()).sort((left, right) => right[1] - left[1])[0]?.[0] || subtheme;
1891
+ const volumeBoost = Math.min(1, size / Math.max(MIN_GROUP_SIZE, TARGET_PACK_SIZE));
1892
+ const duplicatePenalty = Math.max(0.65, 1 - avgDuplicateRate * 0.8);
1893
+ const entropyPenalty = Math.min(0.35, avgEntropy * ENTROPY_WEIGHT);
1894
+ const marginBoost = Math.max(0, avgMargin * MARGIN_BONUS_WEIGHT);
1895
+ 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
+ );
1907
+
1908
+ return {
1909
+ theme,
1910
+ subtheme: subthemeFromCooccurrence,
1911
+ themeKey,
1912
+ groupScore,
1913
+ cohesion: Number(cohesion.toFixed(6)),
1914
+ topical_cohesion: Number(topicalCohesion.toFixed(6)),
1915
+ semantic_cohesion: Number(semanticCohesion.toFixed(6)),
1916
+ semantic_boost: Number(semanticBoost.toFixed(6)),
1917
+ avg_entropy: Number(avgEntropy.toFixed(6)),
1918
+ avg_confidence_margin: Number(avgMargin.toFixed(6)),
1919
+ avg_affinity_weight: Number(avgAffinity.toFixed(6)),
1920
+ avgConfidence: Number(avgConfidence.toFixed(6)),
1921
+ avgQuality: Number(avgQuality.toFixed(6)),
1922
+ duplicateRate: Number(avgDuplicateRate.toFixed(6)),
1923
+ };
1924
+ };
1925
+
1926
+ const buildCurationPlan = ({ grouped, stats }) => {
1927
+ const rawGroups = Array.from(grouped.entries())
1928
+ .map(([themeKey, candidates]) => {
1929
+ const metrics = computeGroupMetrics(themeKey, candidates);
1930
+ return { ...metrics, candidates };
1931
+ })
1932
+ .filter((group) => group.theme && group.candidates.length >= EFFECTIVE_HARD_MIN_GROUP_SIZE)
1933
+ .sort((left, right) => {
1934
+ if (right.groupScore !== left.groupScore) return right.groupScore - left.groupScore;
1935
+ return right.candidates.length - left.candidates.length;
1936
+ });
1937
+
1938
+ let curatedGroups = rawGroups;
1939
+ if (MAX_TAG_GROUPS > 0) {
1940
+ curatedGroups = curatedGroups.slice(0, MAX_TAG_GROUPS);
1941
+ }
1942
+
1943
+ return {
1944
+ groups: curatedGroups,
1945
+ stats: {
1946
+ ...stats,
1947
+ hard_min_group_size: EFFECTIVE_HARD_MIN_GROUP_SIZE,
1948
+ hard_min_group_size_base: HARD_MIN_GROUP_SIZE,
1949
+ semantic_clustering_enabled: ENABLE_SEMANTIC_CLUSTERING,
1950
+ semantic_cluster_min_size_for_pack: SEMANTIC_CLUSTER_MIN_SIZE_FOR_PACK,
1951
+ groups_filtered_hard_min: Math.max(0, Number(grouped?.size || 0) - rawGroups.length),
1952
+ groups_formed: curatedGroups.length,
1953
+ },
1954
+ };
1955
+ };
1956
+
1957
+ const extractThemeKeyFromPack = (pack) => {
1958
+ const direct = normalizeTag(pack?.pack_theme_key || '');
1959
+ if (direct) {
1960
+ const parsedDirect = parseThemeKey(pack.pack_theme_key);
1961
+ return buildThemeKey(parsedDirect.theme, parsedDirect.subtheme) || direct;
1962
+ }
1963
+
1964
+ const description = String(pack?.description || '');
1965
+ const themeMarker = description.match(/\[auto-theme:([^\]]+)\]/i);
1966
+ const legacyTagMarker = description.match(/\[auto-tag:([^\]]+)\]/i);
1967
+ const markerValue = themeMarker?.[1] || legacyTagMarker?.[1] || '';
1968
+ if (!markerValue) return '';
1969
+ const parsed = parseThemeKey(markerValue);
1970
+ const themeKey = buildThemeKey(parsed.theme, parsed.subtheme);
1971
+ return themeKey || normalizeTag(markerValue);
1972
+ };
1973
+
1974
+ const extractVolumeFromPack = (pack) => {
1975
+ const directVolume = Number(pack?.pack_volume || 0);
1976
+ if (Number.isInteger(directVolume) && directVolume > 0) return directVolume;
1977
+
1978
+ const name = String(pack?.name || '');
1979
+ const match = name.match(/vol\.\s*(\d+)/i);
1980
+ const parsed = Number(match?.[1] || 0);
1981
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : 1;
1982
+ };
1983
+
1984
+ const sortAutoThemePacks = (packs) =>
1985
+ [...(Array.isArray(packs) ? packs : [])].sort((left, right) => {
1986
+ const leftVol = extractVolumeFromPack(left);
1987
+ const rightVol = extractVolumeFromPack(right);
1988
+ if (leftVol !== rightVol) return leftVol - rightVol;
1989
+ return String(left.id || '').localeCompare(String(right.id || ''));
1990
+ });
1991
+
1992
+ const buildAutoPackIndex = (packs) => {
1993
+ const byTheme = new Map();
1994
+ const byId = new Map();
1995
+ for (const pack of Array.isArray(packs) ? packs : []) {
1996
+ if (!pack?.id) continue;
1997
+ byId.set(pack.id, pack);
1998
+ const themeKey = extractThemeKeyFromPack(pack);
1999
+ if (!themeKey) continue;
2000
+ const list = byTheme.get(themeKey) || [];
2001
+ list.push(pack);
2002
+ byTheme.set(themeKey, list);
2003
+ }
2004
+
2005
+ for (const [themeKey, list] of byTheme.entries()) {
2006
+ byTheme.set(themeKey, sortAutoThemePacks(list));
2007
+ }
2008
+
2009
+ return { byTheme, byId };
2010
+ };
2011
+
2012
+ const buildOwnerCapacityState = async (ownerPool) => {
2013
+ const hasOwnerPackLimit = Number.isFinite(MAX_PACKS_PER_OWNER);
2014
+ const ownerPackScanLimit = hasOwnerPackLimit ? Math.max(100, MAX_PACKS_PER_OWNER + 20) : 1000;
2015
+ const states = [];
2016
+ for (const ownerJid of ownerPool) {
2017
+ try {
2018
+ const packs = hasOwnerPackLimit ? await listStickerPacksByOwner(ownerJid, { limit: ownerPackScanLimit }) : [];
2019
+ states.push({
2020
+ ownerJid,
2021
+ totalPacks: hasOwnerPackLimit ? (Array.isArray(packs) ? packs.length : 0) : 0,
2022
+ });
2023
+ } catch (error) {
2024
+ logger.warn('Falha ao calcular capacidade de owner para auto-curadoria.', {
2025
+ action: 'sticker_auto_pack_by_tags_owner_capacity_failed',
2026
+ owner_jid: ownerJid,
2027
+ error: error?.message,
2028
+ });
2029
+ states.push({
2030
+ ownerJid,
2031
+ totalPacks: hasOwnerPackLimit ? MAX_PACKS_PER_OWNER : 0,
2032
+ });
2033
+ }
2034
+ }
2035
+
2036
+ return states.map((entry) => ({
2037
+ ...entry,
2038
+ available: hasOwnerPackLimit ? Math.max(0, MAX_PACKS_PER_OWNER - Math.max(0, Number(entry.totalPacks || 0))) : Number.POSITIVE_INFINITY,
2039
+ }));
2040
+ };
2041
+
2042
+ const pickOwnerWithCapacity = (ownerStates) => {
2043
+ const candidates = (Array.isArray(ownerStates) ? ownerStates : [])
2044
+ .filter((entry) => Number(entry.available || 0) > 0)
2045
+ .sort((left, right) => {
2046
+ if (left.available !== right.available) return right.available - left.available;
2047
+ if (left.totalPacks !== right.totalPacks) return left.totalPacks - right.totalPacks;
2048
+ return String(left.ownerJid || '').localeCompare(String(right.ownerJid || ''));
2049
+ });
2050
+ return candidates[0] || null;
2051
+ };
2052
+
2053
+ const chunkArray = (list, size) => {
2054
+ const safeSize = Math.max(1, Number(size) || 1);
2055
+ const chunks = [];
2056
+ for (let index = 0; index < list.length; index += safeSize) {
2057
+ chunks.push(list.slice(index, index + safeSize));
2058
+ }
2059
+ return chunks;
2060
+ };
2061
+
2062
+ const countPackItems = (itemsByPackId, packId) => Number(itemsByPackId.get(packId)?.length || 0);
2063
+
2064
+ const deleteAutoPackWithItems = async (packId, itemsByPackId) => {
2065
+ await removeStickerPackItemsByPackId(packId);
2066
+ await softDeleteStickerPack(packId);
2067
+ itemsByPackId.set(packId, []);
2068
+ };
2069
+
2070
+ const runRetroConsolidationCycle = async ({ ownerPool }) => {
2071
+ if (!RETRO_CONSOLIDATION_ENABLED) {
2072
+ return {
2073
+ enabled: false,
2074
+ processed_themes: 0,
2075
+ merged_themes: 0,
2076
+ deleted_packs: 0,
2077
+ moved_stickers: 0,
2078
+ trimmed_stickers: 0,
2079
+ mutations: 0,
2080
+ theme_limit_reached: false,
2081
+ mutation_limit_reached: false,
2082
+ };
2083
+ }
2084
+
2085
+ const autoPacks = await listStickerAutoPacksForCuration({
2086
+ ownerJids: ownerPool,
2087
+ includeArchived: false,
2088
+ limit: 5000,
2089
+ });
2090
+ if (!autoPacks.length) {
2091
+ return {
2092
+ enabled: true,
2093
+ processed_themes: 0,
2094
+ merged_themes: 0,
2095
+ deleted_packs: 0,
2096
+ moved_stickers: 0,
2097
+ trimmed_stickers: 0,
2098
+ mutations: 0,
2099
+ theme_limit_reached: false,
2100
+ mutation_limit_reached: false,
2101
+ };
2102
+ }
2103
+
2104
+ const packIds = autoPacks.map((pack) => pack.id).filter(Boolean);
2105
+ const allItems = packIds.length ? await listStickerPackItemsByPackIds(packIds) : [];
2106
+ const itemsByPackId = new Map();
2107
+ for (const item of allItems) {
2108
+ const list = itemsByPackId.get(item.pack_id) || [];
2109
+ list.push(item);
2110
+ itemsByPackId.set(item.pack_id, list);
2111
+ }
2112
+
2113
+ const packsByTheme = new Map();
2114
+ for (const pack of autoPacks) {
2115
+ const themeKey = extractThemeKeyFromPack(pack);
2116
+ if (!themeKey) continue;
2117
+ const list = packsByTheme.get(themeKey) || [];
2118
+ list.push(pack);
2119
+ packsByTheme.set(themeKey, list);
2120
+ }
2121
+
2122
+ let processedThemes = 0;
2123
+ let mergedThemes = 0;
2124
+ let deletedPacks = 0;
2125
+ let movedStickers = 0;
2126
+ let trimmedStickers = 0;
2127
+ let mutations = 0;
2128
+ let themeLimitReached = false;
2129
+ let mutationLimitReached = false;
2130
+
2131
+ const themeEntries = Array.from(packsByTheme.entries())
2132
+ .sort((left, right) => right[1].length - left[1].length);
2133
+
2134
+ for (const [themeKey, themePacksRaw] of themeEntries) {
2135
+ if (processedThemes >= RETRO_CONSOLIDATION_THEME_LIMIT) {
2136
+ themeLimitReached = true;
2137
+ break;
2138
+ }
2139
+ if (mutations >= RETRO_CONSOLIDATION_MUTATION_LIMIT) {
2140
+ mutationLimitReached = true;
2141
+ break;
2142
+ }
2143
+ processedThemes += 1;
2144
+
2145
+ const themePacks = sortAutoThemePacks(themePacksRaw).sort((left, right) => {
2146
+ const leftCount = countPackItems(itemsByPackId, left.id);
2147
+ const rightCount = countPackItems(itemsByPackId, right.id);
2148
+ if (rightCount !== leftCount) return rightCount - leftCount;
2149
+ return extractVolumeFromPack(left) - extractVolumeFromPack(right);
2150
+ });
2151
+ if (!themePacks.length) continue;
2152
+
2153
+ const smallPacks = themePacks.filter((pack) => countPackItems(itemsByPackId, pack.id) < HARD_MIN_PACK_ITEMS);
2154
+ const overflowPacks = themePacks.slice(MAX_PACKS_PER_THEME);
2155
+ const needsReadinessCorrection = themePacks.some((pack) => {
2156
+ const count = countPackItems(itemsByPackId, pack.id);
2157
+ const packStatus = String(pack?.pack_status || 'ready').toLowerCase();
2158
+ return packStatus === 'ready' && count < READY_PACK_MIN_ITEMS;
2159
+ });
2160
+ if (!smallPacks.length && !overflowPacks.length && !needsReadinessCorrection) continue;
2161
+
2162
+ const anchorPack = themePacks[0];
2163
+ 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);
2172
+ const desiredAnchorSet = new Set(desiredAnchorIds);
2173
+
2174
+ let anchorItems = itemsByPackId.get(anchorPack.id) || await listStickerPackItems(anchorPack.id);
2175
+ const anchorCurrentSet = new Set(anchorItems.map((item) => item.sticker_id).filter(Boolean));
2176
+
2177
+ for (const item of anchorItems) {
2178
+ if (mutations >= RETRO_CONSOLIDATION_MUTATION_LIMIT) break;
2179
+ if (desiredAnchorSet.has(item.sticker_id)) continue;
2180
+ try {
2181
+ await stickerPackService.removeStickerFromPack({
2182
+ ownerJid: anchorPack.owner_jid,
2183
+ identifier: anchorPack.id,
2184
+ selector: item.sticker_id,
2185
+ });
2186
+ mutations += 1;
2187
+ trimmedStickers += 1;
2188
+ } catch (error) {
2189
+ logger.warn('Falha ao podar sticker excedente durante consolidação retroativa.', {
2190
+ action: 'sticker_auto_pack_retro_trim_failed',
2191
+ pack_id: anchorPack.id,
2192
+ sticker_id: item.sticker_id,
2193
+ theme_key: themeKey,
2194
+ error: error?.message,
2195
+ error_code: error?.code,
2196
+ });
2197
+ }
2198
+ }
2199
+
2200
+ anchorItems = await listStickerPackItems(anchorPack.id);
2201
+ itemsByPackId.set(anchorPack.id, anchorItems);
2202
+ anchorCurrentSet.clear();
2203
+ for (const item of anchorItems) anchorCurrentSet.add(item.sticker_id);
2204
+
2205
+ for (const stickerId of desiredAnchorIds) {
2206
+ if (mutations >= RETRO_CONSOLIDATION_MUTATION_LIMIT) break;
2207
+ if (anchorCurrentSet.has(stickerId)) continue;
2208
+ try {
2209
+ await stickerPackService.addStickerToPack({
2210
+ ownerJid: anchorPack.owner_jid,
2211
+ identifier: anchorPack.id,
2212
+ asset: { id: stickerId },
2213
+ emojis: [],
2214
+ accessibilityLabel: `Auto-theme ${themeKey}`,
2215
+ expectedErrorCodes: ['PACK_LIMIT_REACHED'],
2216
+ });
2217
+ mutations += 1;
2218
+ movedStickers += 1;
2219
+ anchorCurrentSet.add(stickerId);
2220
+ } catch (error) {
2221
+ if (error?.code === 'PACK_LIMIT_REACHED') break;
2222
+ if (error?.code === 'DUPLICATE_STICKER') continue;
2223
+ logger.warn('Falha ao mover sticker para pack âncora na consolidação retroativa.', {
2224
+ action: 'sticker_auto_pack_retro_move_failed',
2225
+ pack_id: anchorPack.id,
2226
+ sticker_id: stickerId,
2227
+ theme_key: themeKey,
2228
+ error: error?.message,
2229
+ error_code: error?.code,
2230
+ });
2231
+ }
2232
+ }
2233
+
2234
+ anchorItems = await listStickerPackItems(anchorPack.id);
2235
+ itemsByPackId.set(anchorPack.id, anchorItems);
2236
+ const anchorOrder = anchorItems.map((item) => item.sticker_id);
2237
+ 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));
2242
+ if (needsReorder) {
2243
+ try {
2244
+ await stickerPackService.reorderPackItems({
2245
+ ownerJid: anchorPack.owner_jid,
2246
+ identifier: anchorPack.id,
2247
+ orderStickerIds: finalDesiredOrder,
2248
+ });
2249
+ anchorItems = await listStickerPackItems(anchorPack.id);
2250
+ itemsByPackId.set(anchorPack.id, anchorItems);
2251
+ } catch (error) {
2252
+ logger.warn('Falha ao reordenar pack âncora na consolidação retroativa.', {
2253
+ action: 'sticker_auto_pack_retro_reorder_failed',
2254
+ pack_id: anchorPack.id,
2255
+ theme_key: themeKey,
2256
+ error: error?.message,
2257
+ error_code: error?.code,
2258
+ });
2259
+ }
2260
+ }
2261
+
2262
+ for (const donor of donorPacks) {
2263
+ if (mutations >= RETRO_CONSOLIDATION_MUTATION_LIMIT) break;
2264
+ try {
2265
+ await deleteAutoPackWithItems(donor.id, itemsByPackId);
2266
+ mutations += 1;
2267
+ deletedPacks += 1;
2268
+ } catch (error) {
2269
+ logger.warn('Falha ao excluir pack doador na consolidação retroativa.', {
2270
+ action: 'sticker_auto_pack_retro_delete_donor_failed',
2271
+ pack_id: donor.id,
2272
+ theme_key: themeKey,
2273
+ error: error?.message,
2274
+ });
2275
+ }
2276
+ }
2277
+
2278
+ anchorItems = itemsByPackId.get(anchorPack.id) || await listStickerPackItems(anchorPack.id);
2279
+ const anchorCount = anchorItems.length;
2280
+ if (anchorCount < HARD_MIN_PACK_ITEMS) {
2281
+ if (mutations < RETRO_CONSOLIDATION_MUTATION_LIMIT) {
2282
+ try {
2283
+ await deleteAutoPackWithItems(anchorPack.id, itemsByPackId);
2284
+ mutations += 1;
2285
+ deletedPacks += 1;
2286
+ } catch (error) {
2287
+ logger.warn('Falha ao excluir pack âncora abaixo do mínimo na consolidação retroativa.', {
2288
+ action: 'sticker_auto_pack_retro_delete_anchor_failed',
2289
+ pack_id: anchorPack.id,
2290
+ theme_key: themeKey,
2291
+ error: error?.message,
2292
+ });
2293
+ }
2294
+ } else {
2295
+ mutationLimitReached = true;
2296
+ }
2297
+ continue;
2298
+ }
2299
+
2300
+ const parsedTheme = parseThemeKey(themeKey);
2301
+ const resolvedTheme = parsedTheme.theme || normalizeTag(anchorPack.pack_theme_key || '') || 'outros';
2302
+ const resolvedSubtheme = sanitizeDisplaySubtheme(parsedTheme.subtheme);
2303
+ const packStatus = anchorCount >= READY_PACK_MIN_ITEMS ? 'ready' : 'building';
2304
+ await updateAutoPackMetadata(anchorPack.id, {
2305
+ name: buildAutoPackName(resolvedTheme, resolvedSubtheme, 1),
2306
+ description: buildAutoPackDescription({
2307
+ theme: resolvedTheme,
2308
+ subtheme: resolvedSubtheme,
2309
+ themeKey,
2310
+ groupScore: 0,
2311
+ }),
2312
+ themeKey,
2313
+ volume: 1,
2314
+ pack_status: packStatus,
2315
+ status: packStatus === 'ready' ? 'published' : 'draft',
2316
+ cover_sticker_id: anchorItems[0]?.sticker_id || null,
2317
+ });
2318
+ mergedThemes += 1;
2319
+ }
2320
+
2321
+ return {
2322
+ enabled: true,
2323
+ processed_themes: processedThemes,
2324
+ merged_themes: mergedThemes,
2325
+ deleted_packs: deletedPacks,
2326
+ moved_stickers: movedStickers,
2327
+ trimmed_stickers: trimmedStickers,
2328
+ mutations,
2329
+ theme_limit_reached: themeLimitReached,
2330
+ mutation_limit_reached: mutationLimitReached,
2331
+ };
2332
+ };
2333
+
2334
+ const optimizePackEcosystem = ({
2335
+ operations,
2336
+ itemsByPackId,
2337
+ classificationByAssetId,
2338
+ packEngagementByPackId = new Map(),
2339
+ }) => {
2340
+ if (!ENABLE_GLOBAL_OPTIMIZATION) {
2341
+ return {
2342
+ enabled: false,
2343
+ cycles_effective: 0,
2344
+ transfer_moves: 0,
2345
+ merge_moves: 0,
2346
+ matrix_merge_moves: 0,
2347
+ archived_packs: 0,
2348
+ tier_gold: 0,
2349
+ tier_silver: 0,
2350
+ tier_bronze: 0,
2351
+ energy_initial: 0,
2352
+ energy_final: 0,
2353
+ energy_gain: 0,
2354
+ cycle_gains: [],
2355
+ stable_gain_cycles: 0,
2356
+ tier_mean_asset_quality: { gold: 0, silver: 0, bronze: 0 },
2357
+ tier_archive: 0,
2358
+ interpack_similarity_mean: 0,
2359
+ entropy_mean_global: 0,
2360
+ };
2361
+ }
2362
+
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 || '')));
2366
+
2367
+ if (!scopedOps.length) {
2368
+ return {
2369
+ enabled: true,
2370
+ cycles_effective: 0,
2371
+ transfer_moves: 0,
2372
+ merge_moves: 0,
2373
+ matrix_merge_moves: 0,
2374
+ archived_packs: 0,
2375
+ tier_gold: 0,
2376
+ tier_silver: 0,
2377
+ tier_bronze: 0,
2378
+ energy_initial: 0,
2379
+ energy_final: 0,
2380
+ energy_gain: 0,
2381
+ cycle_gains: [],
2382
+ stable_gain_cycles: 0,
2383
+ tier_mean_asset_quality: { gold: 0, silver: 0, bronze: 0 },
2384
+ tier_archive: 0,
2385
+ interpack_similarity_mean: 0,
2386
+ entropy_mean_global: 0,
2387
+ };
2388
+ }
2389
+
2390
+ const stateByPackId = new Map(
2391
+ scopedOps.map((op) => {
2392
+ const initialItems = itemsByPackId.get(op.existingPackId) || [];
2393
+ 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
+ }];
2401
+ }),
2402
+ );
2403
+
2404
+ const computeProfiles = () => {
2405
+ const profiles = new Map();
2406
+ for (const [packId, state] of stateByPackId.entries()) {
2407
+ profiles.set(packId, computePackProfile({
2408
+ packId,
2409
+ stickerIds: Array.from(state.stickers),
2410
+ themeKey: state.themeKey,
2411
+ classificationByAssetId,
2412
+ }));
2413
+ }
2414
+ return profiles;
2415
+ };
2416
+
2417
+ 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
+ );
2421
+ const engagementZscoreByPackId = buildNormalizedZScoreMap(engagementScoreByPackId);
2422
+ const getEngagementZscore = (packId) => Number(engagementZscoreByPackId.get(packId) || 0);
2423
+
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
+ });
2432
+
2433
+ const buildProfileScoreMap = (profiles) => {
2434
+ const map = new Map();
2435
+ for (const [packId, profile] of profiles.entries()) {
2436
+ map.set(packId, Number(scoreProfile(profile).toFixed(6)));
2437
+ }
2438
+ return map;
2439
+ };
2440
+
2441
+ const buildQualityScoreMap = (profiles) => {
2442
+ const map = new Map();
2443
+ for (const [packId, profile] of profiles.entries()) {
2444
+ map.set(packId, Number(scoreQualityProfile(profile).toFixed(6)));
2445
+ }
2446
+ return map;
2447
+ };
2448
+
2449
+ const computeSystemEnergy = (profiles, profileScores = buildProfileScoreMap(profiles)) => {
2450
+ const profileList = Array.from(profiles.values());
2451
+ const qualitySum = sumArray(profileList.map((entry) => Number(profileScores.get(entry.packId) || 0)));
2452
+ let overlapSum = 0;
2453
+ let overlapPairs = 0;
2454
+ for (let i = 0; i < profileList.length; i += 1) {
2455
+ for (let j = i + 1; j < profileList.length; j += 1) {
2456
+ overlapSum += computePackOverlap(profileList[i], profileList[j]);
2457
+ overlapPairs += 1;
2458
+ }
2459
+ }
2460
+ const redundancy = overlapPairs > 0 ? overlapSum / overlapPairs : 0;
2461
+ const redundancyPenalty = SYSTEM_REDUNDANCY_LAMBDA * GLOBAL_ENERGY_W5 * redundancy;
2462
+ const energy = qualitySum - redundancyPenalty;
2463
+ return {
2464
+ qualitySum: Number(qualitySum.toFixed(6)),
2465
+ overlapSum: Number(overlapSum.toFixed(6)),
2466
+ overlapPairs,
2467
+ redundancy: Number(redundancy.toFixed(6)),
2468
+ redundancyPenalty: Number(redundancyPenalty.toFixed(6)),
2469
+ energy: Number(energy.toFixed(6)),
2470
+ profileScores,
2471
+ };
2472
+ };
2473
+
2474
+ const assignTiers = (profiles, qualityScores) => {
2475
+ let gold = 0;
2476
+ let silver = 0;
2477
+ let bronze = 0;
2478
+ let archive = 0;
2479
+ for (const [packId] of profiles.entries()) {
2480
+ const state = stateByPackId.get(packId);
2481
+ if (!state) continue;
2482
+ const score = Number(qualityScores.get(packId) || 0);
2483
+ state.qualityScore = score;
2484
+ if (score >= PACK_TIER_GOLD_THRESHOLD) {
2485
+ state.tier = 'GOLD';
2486
+ gold += 1;
2487
+ } else if (score >= PACK_TIER_SILVER_THRESHOLD) {
2488
+ state.tier = 'SILVER';
2489
+ silver += 1;
2490
+ } else if (score >= PACK_TIER_BRONZE_THRESHOLD) {
2491
+ state.tier = 'BRONZE';
2492
+ bronze += 1;
2493
+ } else {
2494
+ state.tier = 'ARCHIVE';
2495
+ archive += 1;
2496
+ }
2497
+ }
2498
+ return { gold, silver, bronze, archive };
2499
+ };
2500
+
2501
+ const computeTierMeanAssetQuality = () => {
2502
+ const totals = new Map([
2503
+ ['GOLD', { sum: 0, count: 0 }],
2504
+ ['SILVER', { sum: 0, count: 0 }],
2505
+ ['BRONZE', { sum: 0, count: 0 }],
2506
+ ]);
2507
+
2508
+ for (const state of stateByPackId.values()) {
2509
+ const tier = ['GOLD', 'SILVER', 'BRONZE'].includes(state.tier) ? state.tier : 'BRONZE';
2510
+ const parsedTheme = parseThemeKey(state.themeKey);
2511
+ for (const stickerId of state.stickers) {
2512
+ const classification = classificationByAssetId.get(stickerId);
2513
+ if (!classification) continue;
2514
+ const topTags = buildTopTags(classification);
2515
+ const quality = computeAssetQualityForTheme({
2516
+ classification,
2517
+ theme: parsedTheme.theme,
2518
+ subtheme: parsedTheme.subtheme,
2519
+ topTags,
2520
+ });
2521
+ const bucket = totals.get(tier);
2522
+ bucket.sum += Number(quality.assetQuality || 0);
2523
+ bucket.count += 1;
2524
+ }
2525
+ }
2526
+
2527
+ const meanByTier = {};
2528
+ for (const [tier, bucket] of totals.entries()) {
2529
+ const mean = bucket.count ? bucket.sum / bucket.count : 0;
2530
+ meanByTier[tier.toLowerCase()] = Number(mean.toFixed(6));
2531
+ }
2532
+ return meanByTier;
2533
+ };
2534
+
2535
+ const collectStickerIds = () => {
2536
+ const ids = new Set();
2537
+ for (const state of stateByPackId.values()) {
2538
+ for (const stickerId of state.stickers) {
2539
+ if (stickerId) ids.add(stickerId);
2540
+ }
2541
+ }
2542
+ return Array.from(ids);
2543
+ };
2544
+
2545
+ let profiles = computeProfiles();
2546
+ let profileScores = buildProfileScoreMap(profiles);
2547
+ let qualityScores = buildQualityScoreMap(profiles);
2548
+ let energySnapshot = computeSystemEnergy(profiles, profileScores);
2549
+ const energyInitial = energySnapshot.energy;
2550
+ let transferMoves = 0;
2551
+ let mergeMoves = 0;
2552
+ let matrixMergeMoves = 0;
2553
+ let archivedPacks = 0;
2554
+ let cyclesEffective = 0;
2555
+ const cycleGains = [];
2556
+ let stableGainCycles = 0;
2557
+ let tierSnapshot = assignTiers(profiles, qualityScores);
2558
+ let interPackSimilaritySnapshot = buildInterPackSimilarityMatrix(profiles);
2559
+
2560
+ for (let cycle = 0; cycle < OPTIMIZATION_CYCLES; cycle += 1) {
2561
+ cyclesEffective += 1;
2562
+ const cycleStartEnergy = energySnapshot.energy;
2563
+ let cycleTransfers = 0;
2564
+ let cycleMerges = 0;
2565
+ let cycleArchives = 0;
2566
+
2567
+ const packStates = Array.from(stateByPackId.values());
2568
+ const orderedSources = [...packStates].sort((left, right) => {
2569
+ const leftQuality = Number(qualityScores.get(left.packId) || 0);
2570
+ const rightQuality = Number(qualityScores.get(right.packId) || 0);
2571
+ if (leftQuality !== rightQuality) return leftQuality - rightQuality;
2572
+ if (right.stickers.size !== left.stickers.size) return right.stickers.size - left.stickers.size;
2573
+ return String(left.packId || '').localeCompare(String(right.packId || ''));
2574
+ });
2575
+
2576
+ for (const source of orderedSources) {
2577
+ const sourceStickerIds = Array.from(source.stickers);
2578
+ for (const stickerId of sourceStickerIds) {
2579
+ if (!source.stickers.has(stickerId)) continue;
2580
+ if (source.stickers.size <= 1) break;
2581
+
2582
+ const sourceProfile = profiles.get(source.packId);
2583
+ const sourceScore = Number(profileScores.get(source.packId) || 0);
2584
+ const sourceQuality = Number(qualityScores.get(source.packId) || 0);
2585
+ if (!sourceProfile || !Number.isFinite(sourceScore) || !Number.isFinite(sourceQuality)) continue;
2586
+
2587
+ const recipientCandidates = packStates
2588
+ .filter((recipient) =>
2589
+ recipient.packId !== source.packId
2590
+ && !recipient.stickers.has(stickerId)
2591
+ && recipient.stickers.size < TARGET_PACK_SIZE)
2592
+ .map((recipient) => {
2593
+ const recipientProfile = profiles.get(recipient.packId);
2594
+ const recipientScore = Number(profileScores.get(recipient.packId) || 0);
2595
+ const recipientQuality = Number(qualityScores.get(recipient.packId) || 0);
2596
+ if (!recipientProfile || !Number.isFinite(recipientScore) || !Number.isFinite(recipientQuality)) {
2597
+ return null;
2598
+ }
2599
+ const semanticSimilarity = computePackSemanticSimilarity(sourceProfile, recipientProfile);
2600
+ const sameTheme = source.themeKey && recipient.themeKey && source.themeKey === recipient.themeKey;
2601
+ if (!sameTheme && semanticSimilarity < EFFECTIVE_TRANSFER_CANDIDATE_SIMILARITY_FLOOR) {
2602
+ return null;
2603
+ }
2604
+ const candidateRank =
2605
+ semanticSimilarity * 0.85
2606
+ + (sameTheme ? 0.15 : 0)
2607
+ + Math.max(0, recipientQuality - sourceQuality) * 0.1;
2608
+ return {
2609
+ recipient,
2610
+ recipientScore,
2611
+ recipientQuality,
2612
+ candidateRank,
2613
+ };
2614
+ })
2615
+ .filter(Boolean)
2616
+ .sort((left, right) => right.candidateRank - left.candidateRank)
2617
+ .slice(0, EFFECTIVE_MIGRATION_CANDIDATE_LIMIT);
2618
+
2619
+ let bestMove = null;
2620
+ for (const candidate of recipientCandidates) {
2621
+ const sourceNext = new Set(source.stickers);
2622
+ sourceNext.delete(stickerId);
2623
+ if (sourceNext.size === 0) continue;
2624
+
2625
+ const recipientNext = new Set(candidate.recipient.stickers);
2626
+ recipientNext.add(stickerId);
2627
+
2628
+ const sourceNextProfile = computePackProfile({
2629
+ packId: source.packId,
2630
+ stickerIds: Array.from(sourceNext),
2631
+ themeKey: source.themeKey,
2632
+ classificationByAssetId,
2633
+ });
2634
+ const recipientNextProfile = computePackProfile({
2635
+ packId: candidate.recipient.packId,
2636
+ stickerIds: Array.from(recipientNext),
2637
+ themeKey: candidate.recipient.themeKey,
2638
+ classificationByAssetId,
2639
+ });
2640
+
2641
+ const sourceNextScore = Number(scoreProfile(sourceNextProfile).toFixed(6));
2642
+ const recipientNextScore = Number(scoreProfile(recipientNextProfile).toFixed(6));
2643
+ const sourceNextQuality = Number(scoreQualityProfile(sourceNextProfile).toFixed(6));
2644
+ const recipientNextQuality = Number(scoreQualityProfile(recipientNextProfile).toFixed(6));
2645
+
2646
+ const changes = new Map([
2647
+ [source.packId, { profile: sourceNextProfile, score: sourceNextScore }],
2648
+ [candidate.recipient.packId, { profile: recipientNextProfile, score: recipientNextScore }],
2649
+ ]);
2650
+ const deltaPreview = computePackEnergyDelta({
2651
+ baseEnergySnapshot: energySnapshot,
2652
+ profiles,
2653
+ profileScores,
2654
+ changes,
2655
+ });
2656
+ if (!bestMove || deltaPreview.deltaEnergy > bestMove.deltaEnergy) {
2657
+ bestMove = {
2658
+ recipient: candidate.recipient,
2659
+ sourceNext,
2660
+ recipientNext,
2661
+ sourceNextProfile,
2662
+ recipientNextProfile,
2663
+ sourceNextScore,
2664
+ recipientNextScore,
2665
+ sourceNextQuality,
2666
+ recipientNextQuality,
2667
+ deltaEnergy: deltaPreview.deltaEnergy,
2668
+ nextSnapshot: deltaPreview.nextSnapshot,
2669
+ };
2670
+ }
2671
+ }
2672
+
2673
+ if (bestMove && bestMove.deltaEnergy > EFFECTIVE_TRANSFER_THRESHOLD) {
2674
+ source.stickers = bestMove.sourceNext;
2675
+ bestMove.recipient.stickers = bestMove.recipientNext;
2676
+ profiles.set(source.packId, bestMove.sourceNextProfile);
2677
+ profiles.set(bestMove.recipient.packId, bestMove.recipientNextProfile);
2678
+ profileScores.set(source.packId, bestMove.sourceNextScore);
2679
+ profileScores.set(bestMove.recipient.packId, bestMove.recipientNextScore);
2680
+ qualityScores.set(source.packId, bestMove.sourceNextQuality);
2681
+ qualityScores.set(bestMove.recipient.packId, bestMove.recipientNextQuality);
2682
+ energySnapshot = {
2683
+ ...bestMove.nextSnapshot,
2684
+ profileScores,
2685
+ };
2686
+ cycleTransfers += 1;
2687
+ transferMoves += 1;
2688
+ }
2689
+ }
2690
+ }
2691
+
2692
+ profiles = computeProfiles();
2693
+ profileScores = buildProfileScoreMap(profiles);
2694
+ qualityScores = buildQualityScoreMap(profiles);
2695
+ energySnapshot = computeSystemEnergy(profiles, profileScores);
2696
+
2697
+ interPackSimilaritySnapshot = buildInterPackSimilarityMatrix(profiles);
2698
+ const packStatesForMerge = Array.from(stateByPackId.values());
2699
+ for (let i = 0; i < packStatesForMerge.length; i += 1) {
2700
+ for (let j = i + 1; j < packStatesForMerge.length; j += 1) {
2701
+ const left = packStatesForMerge[i];
2702
+ const right = packStatesForMerge[j];
2703
+ if (!left.stickers.size || !right.stickers.size) continue;
2704
+
2705
+ const pairKey = buildPackPairKey(left.packId, right.packId);
2706
+ const similarity = Number(interPackSimilaritySnapshot.matrix.get(pairKey) || 0);
2707
+ if (similarity < EFFECTIVE_INTER_PACK_SIMILARITY_THRESHOLD) continue;
2708
+
2709
+ const leftQuality = Number(qualityScores.get(left.packId) || 0);
2710
+ const rightQuality = Number(qualityScores.get(right.packId) || 0);
2711
+ if (leftQuality === rightQuality) continue;
2712
+ const recipient = leftQuality > rightQuality ? left : right;
2713
+ const donor = recipient.packId === left.packId ? right : left;
2714
+
2715
+ const recipientProfile = profiles.get(recipient.packId);
2716
+ const donorProfile = profiles.get(donor.packId);
2717
+ if (!recipientProfile || !donorProfile) continue;
2718
+
2719
+ const mergedIds = Array.from(new Set([...recipient.stickers, ...donor.stickers]));
2720
+ const mergedProfileRaw = computePackProfile({
2721
+ packId: recipient.packId,
2722
+ stickerIds: mergedIds,
2723
+ themeKey: recipient.themeKey,
2724
+ classificationByAssetId,
2725
+ });
2726
+
2727
+ let keptIds = mergedIds;
2728
+ if (mergedIds.length > TARGET_PACK_SIZE) {
2729
+ const scored = mergedIds
2730
+ .map((stickerId) => ({
2731
+ stickerId,
2732
+ score: computeStickerPackMatrixScore({
2733
+ stickerId,
2734
+ packStickerIds: mergedIds,
2735
+ classificationByAssetId,
2736
+ centroidVector: mergedProfileRaw.centroidVector,
2737
+ }),
2738
+ }))
2739
+ .sort((a, b) => b.score - a.score)
2740
+ .slice(0, TARGET_PACK_SIZE);
2741
+ keptIds = scored.map((entry) => entry.stickerId);
2742
+ }
2743
+
2744
+ const mergedProfile = computePackProfile({
2745
+ packId: recipient.packId,
2746
+ stickerIds: keptIds,
2747
+ themeKey: recipient.themeKey,
2748
+ classificationByAssetId,
2749
+ });
2750
+ const donorNextProfile = computePackProfile({
2751
+ packId: donor.packId,
2752
+ stickerIds: [],
2753
+ themeKey: donor.themeKey,
2754
+ classificationByAssetId,
2755
+ });
2756
+ const mergedScore = Number(scoreProfile(mergedProfile).toFixed(6));
2757
+ const donorNextScore = Number(scoreProfile(donorNextProfile).toFixed(6));
2758
+ const mergedQuality = Number(scoreQualityProfile(mergedProfile).toFixed(6));
2759
+ const donorNextQuality = Number(scoreQualityProfile(donorNextProfile).toFixed(6));
2760
+
2761
+ const changes = new Map([
2762
+ [recipient.packId, { profile: mergedProfile, score: mergedScore }],
2763
+ [donor.packId, { profile: donorNextProfile, score: donorNextScore }],
2764
+ ]);
2765
+ const deltaPreview = computePackEnergyDelta({
2766
+ baseEnergySnapshot: energySnapshot,
2767
+ profiles,
2768
+ profileScores,
2769
+ changes,
2770
+ });
2771
+ if (deltaPreview.deltaEnergy <= EFFECTIVE_TRANSFER_THRESHOLD) continue;
2772
+
2773
+ recipient.stickers = new Set(keptIds);
2774
+ donor.stickers = new Set();
2775
+ profiles.set(recipient.packId, mergedProfile);
2776
+ profiles.set(donor.packId, donorNextProfile);
2777
+ profileScores.set(recipient.packId, mergedScore);
2778
+ profileScores.set(donor.packId, donorNextScore);
2779
+ qualityScores.set(recipient.packId, mergedQuality);
2780
+ qualityScores.set(donor.packId, donorNextQuality);
2781
+ energySnapshot = {
2782
+ ...deltaPreview.nextSnapshot,
2783
+ profileScores,
2784
+ };
2785
+ cycleMerges += 1;
2786
+ mergeMoves += 1;
2787
+ matrixMergeMoves += 1;
2788
+ }
2789
+ }
2790
+
2791
+ profiles = computeProfiles();
2792
+ profileScores = buildProfileScoreMap(profiles);
2793
+ qualityScores = buildQualityScoreMap(profiles);
2794
+ energySnapshot = computeSystemEnergy(profiles, profileScores);
2795
+
2796
+ if (ARCHIVE_LOW_SCORE_PACKS) {
2797
+ const qualityValues = Array.from(qualityScores.values()).filter((value) => Number.isFinite(value));
2798
+ const archiveCut = percentileValue(qualityValues, EFFECTIVE_AUTO_ARCHIVE_BELOW_PERCENTILE);
2799
+ for (const state of stateByPackId.values()) {
2800
+ if (!state.stickers.size) continue;
2801
+ if (state.stickers.size >= MIN_PACK_SIZE) continue;
2802
+ const qualityScore = Number(qualityScores.get(state.packId) || 0);
2803
+ if (qualityScore > archiveCut) continue;
2804
+
2805
+ const nextProfile = computePackProfile({
2806
+ packId: state.packId,
2807
+ stickerIds: [],
2808
+ themeKey: state.themeKey,
2809
+ classificationByAssetId,
2810
+ });
2811
+ const nextScore = Number(scoreProfile(nextProfile).toFixed(6));
2812
+ const nextQuality = Number(scoreQualityProfile(nextProfile).toFixed(6));
2813
+ const changes = new Map([
2814
+ [state.packId, { profile: nextProfile, score: nextScore }],
2815
+ ]);
2816
+ const deltaPreview = computePackEnergyDelta({
2817
+ baseEnergySnapshot: energySnapshot,
2818
+ profiles,
2819
+ profileScores,
2820
+ changes,
2821
+ });
2822
+ if (deltaPreview.deltaEnergy <= EFFECTIVE_TRANSFER_THRESHOLD) continue;
2823
+
2824
+ state.stickers = new Set();
2825
+ profiles.set(state.packId, nextProfile);
2826
+ profileScores.set(state.packId, nextScore);
2827
+ qualityScores.set(state.packId, nextQuality);
2828
+ energySnapshot = {
2829
+ ...deltaPreview.nextSnapshot,
2830
+ profileScores,
2831
+ };
2832
+ cycleArchives += 1;
2833
+ archivedPacks += 1;
2834
+ }
2835
+ }
2836
+
2837
+ profiles = computeProfiles();
2838
+ profileScores = buildProfileScoreMap(profiles);
2839
+ qualityScores = buildQualityScoreMap(profiles);
2840
+ tierSnapshot = assignTiers(profiles, qualityScores);
2841
+ const nextEnergySnapshot = computeSystemEnergy(profiles, profileScores);
2842
+ interPackSimilaritySnapshot = buildInterPackSimilarityMatrix(profiles);
2843
+ const gain = nextEnergySnapshot.energy - cycleStartEnergy;
2844
+ cycleGains.push(Number(gain.toFixed(6)));
2845
+ energySnapshot = nextEnergySnapshot;
2846
+ if (Math.abs(gain) < OPTIMIZATION_EPSILON) {
2847
+ stableGainCycles += 1;
2848
+ } else {
2849
+ stableGainCycles = 0;
2850
+ }
2851
+
2852
+ if (stableGainCycles >= OPTIMIZATION_STABLE_CYCLES) {
2853
+ break;
2854
+ }
2855
+
2856
+ if (Math.abs(gain) < OPTIMIZATION_EPSILON && cycleTransfers === 0 && cycleMerges === 0 && cycleArchives === 0) {
2857
+ break;
2858
+ }
2859
+ }
2860
+
2861
+ for (const state of stateByPackId.values()) {
2862
+ const op = state.op;
2863
+ if (!op) continue;
2864
+ const finalStickerIds = Array.from(state.stickers);
2865
+ const finalProfile = computePackProfile({
2866
+ packId: state.packId,
2867
+ stickerIds: finalStickerIds,
2868
+ themeKey: state.themeKey,
2869
+ classificationByAssetId,
2870
+ });
2871
+ const ordered = finalStickerIds
2872
+ .map((stickerId) => ({
2873
+ stickerId,
2874
+ score: computeStickerPackMatrixScore({
2875
+ stickerId,
2876
+ packStickerIds: finalStickerIds,
2877
+ classificationByAssetId,
2878
+ centroidVector: finalProfile.centroidVector,
2879
+ }),
2880
+ }))
2881
+ .sort((left, right) => right.score - left.score)
2882
+ .map((entry) => entry.stickerId);
2883
+
2884
+ op.desiredAssetIds = ordered.slice(0, TARGET_PACK_SIZE);
2885
+ op.fillAssetIds = [];
2886
+ op.qualityTier = state.tier === 'ARCHIVE' ? 'BRONZE' : state.tier;
2887
+ const currentItems = itemsByPackId.get(state.packId) || [];
2888
+ const currentSet = new Set(currentItems.map((item) => item?.sticker_id).filter(Boolean));
2889
+ const desiredSet = new Set(op.desiredAssetIds);
2890
+ const removedByOptimization = Array.from(currentSet).filter((stickerId) => !desiredSet.has(stickerId));
2891
+ if (removedByOptimization.length) {
2892
+ op.forceRemoveAssetIds = Array.from(
2893
+ new Set([...(Array.isArray(op.forceRemoveAssetIds) ? op.forceRemoveAssetIds : []), ...removedByOptimization]),
2894
+ );
2895
+ }
2896
+ if (state.tier === 'ARCHIVE') {
2897
+ op.desiredAssetIds = [];
2898
+ op.fillAssetIds = [];
2899
+ op.type = 'archive_volume';
2900
+ } else if (!op.desiredAssetIds.length) {
2901
+ op.type = 'archive_volume';
2902
+ }
2903
+ }
2904
+
2905
+ const entropyMeanGlobal = Number(
2906
+ computeMeanNormalizedEntropy(collectStickerIds(), classificationByAssetId).toFixed(6),
2907
+ );
2908
+
2909
+ return {
2910
+ enabled: true,
2911
+ cycles_effective: cyclesEffective,
2912
+ transfer_moves: transferMoves,
2913
+ merge_moves: mergeMoves,
2914
+ matrix_merge_moves: matrixMergeMoves,
2915
+ archived_packs: archivedPacks,
2916
+ tier_gold: tierSnapshot.gold,
2917
+ tier_silver: tierSnapshot.silver,
2918
+ tier_bronze: tierSnapshot.bronze,
2919
+ tier_archive: tierSnapshot.archive,
2920
+ energy_initial: Number(energyInitial.toFixed(6)),
2921
+ energy_final: Number(energySnapshot.energy.toFixed(6)),
2922
+ energy_gain: Number((energySnapshot.energy - energyInitial).toFixed(6)),
2923
+ cycle_gains: cycleGains,
2924
+ stable_gain_cycles: stableGainCycles,
2925
+ tier_mean_asset_quality: computeTierMeanAssetQuality(),
2926
+ interpack_similarity_mean: Number(interPackSimilaritySnapshot.similarity_mean || 0),
2927
+ entropy_mean_global: entropyMeanGlobal,
2928
+ };
2929
+ };
2930
+
2931
+ const buildCurationExecutionPlan = async ({ curatedGroups, ownerPool, enableAdditions }) => {
2932
+ const autoPacks = await listStickerAutoPacksForCuration({
2933
+ ownerJids: ownerPool,
2934
+ includeArchived: true,
2935
+ limit: 5000,
2936
+ });
2937
+ const autoPackIndex = buildAutoPackIndex(autoPacks);
2938
+ const ownerStates = await buildOwnerCapacityState(ownerPool);
2939
+
2940
+ const packIds = autoPacks.map((pack) => pack.id).filter(Boolean);
2941
+ const engagementByPackId = packIds.length ? await listStickerPackEngagementByPackIds(packIds) : new Map();
2942
+ const allItems = packIds.length ? await listStickerPackItemsByPackIds(packIds) : [];
2943
+ const itemsByPackId = new Map();
2944
+ for (const item of allItems) {
2945
+ const list = itemsByPackId.get(item.pack_id) || [];
2946
+ list.push(item);
2947
+ itemsByPackId.set(item.pack_id, list);
2948
+ }
2949
+
2950
+ const classificationByAssetId = new Map();
2951
+ const operations = [];
2952
+ let plannedCreates = 0;
2953
+ let reuseOnlyMode = false;
2954
+ let overflowVolumesSkipped = 0;
2955
+ let completionPriorityGroups = 0;
2956
+ let plannedCompletionTransfers = 0;
2957
+ const staticGroupLimit = MAX_TAG_GROUPS > 0 ? MAX_TAG_GROUPS : Number.POSITIVE_INFINITY;
2958
+ const dynamicGroupLimit = Math.max(3, DYNAMIC_GROUP_LIMIT_BASE - autoPackIndex.byTheme.size);
2959
+ const effectiveGroupLimit = Math.max(0, Math.min(curatedGroups.length, staticGroupLimit, dynamicGroupLimit));
2960
+ const effectiveCuratedGroups = curatedGroups.slice(0, effectiveGroupLimit);
2961
+ const groupsSkippedByDynamicLimit = Math.max(0, curatedGroups.length - effectiveCuratedGroups.length);
2962
+ let creationBlockedByGlobalCap = 0;
2963
+
2964
+ for (const group of effectiveCuratedGroups) {
2965
+ for (const candidate of group.candidates) {
2966
+ if (candidate?.asset?.id && candidate?.classification) {
2967
+ classificationByAssetId.set(candidate.asset.id, candidate.classification);
2968
+ }
2969
+ }
2970
+
2971
+ const currentThemePacks = sortAutoThemePacks(autoPackIndex.byTheme.get(group.themeKey) || []);
2972
+ const rankedThemePacks = [...currentThemePacks].sort((left, right) => {
2973
+ const leftArchived = String(left?.pack_status || 'ready').toLowerCase() === 'archived' ? 1 : 0;
2974
+ const rightArchived = String(right?.pack_status || 'ready').toLowerCase() === 'archived' ? 1 : 0;
2975
+ if (leftArchived !== rightArchived) return leftArchived - rightArchived;
2976
+ const leftCount = Number(itemsByPackId.get(left.id)?.length || 0);
2977
+ const rightCount = Number(itemsByPackId.get(right.id)?.length || 0);
2978
+ if (rightCount !== leftCount) return rightCount - leftCount;
2979
+ return extractVolumeFromPack(left) - extractVolumeFromPack(right);
2980
+ });
2981
+ 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
+ );
2991
+ const incompleteExistingCount = retainedThemePacks.reduce((sum, pack) => {
2992
+ const count = Number(packCountById.get(pack.id) || 0);
2993
+ return sum + (count < TARGET_PACK_SIZE ? 1 : 0);
2994
+ }, 0);
2995
+ const prioritizeGroupCompletion = EFFECTIVE_PRIORITIZE_COMPLETION && enableAdditions && incompleteExistingCount > 0;
2996
+ const completionPriorityPacks = prioritizeGroupCompletion
2997
+ ? [...retainedThemePacks].sort((left, right) => {
2998
+ const leftCount = Number(packCountById.get(left.id) || 0);
2999
+ const rightCount = Number(packCountById.get(right.id) || 0);
3000
+ if (rightCount !== leftCount) return rightCount - leftCount;
3001
+ return extractVolumeFromPack(left) - extractVolumeFromPack(right);
3002
+ })
3003
+ : [];
3004
+ const volumeCandidateChunks = chunkArray(
3005
+ group.candidates.map((candidate) => candidate.asset.id).filter(Boolean),
3006
+ TARGET_PACK_SIZE,
3007
+ );
3008
+ const groupFillAssetIds = group.candidates.map((candidate) => candidate.asset.id).filter(Boolean);
3009
+
3010
+ const ownerCreateCapacity = ownerStates.reduce((sum, owner) => sum + Math.max(0, Number(owner.available || 0)), 0);
3011
+ const hasIncompleteExisting = incompleteExistingCount > 0;
3012
+ const themeCreateCapacity = Math.max(0, MAX_PACKS_PER_THEME - retainedThemePacks.length);
3013
+ 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;
3017
+ if (enableAdditions && !hasIncompleteExisting && themeCreateCapacity > 0 && globalCreateCapacity <= 0) {
3018
+ creationBlockedByGlobalCap += 1;
3019
+ }
3020
+ const maxTotalVolumes = retainedThemePacks.length + maxCreatableForGroup;
3021
+ let targetVolumeCount = Math.min(volumeCandidateChunks.length, Math.max(0, maxTotalVolumes));
3022
+ if (prioritizeGroupCompletion) {
3023
+ targetVolumeCount = Math.max(targetVolumeCount, retainedThemePacks.length);
3024
+ completionPriorityGroups += 1;
3025
+ }
3026
+
3027
+ if (volumeCandidateChunks.length > targetVolumeCount) {
3028
+ reuseOnlyMode = true;
3029
+ overflowVolumesSkipped += volumeCandidateChunks.length - targetVolumeCount;
3030
+ }
3031
+
3032
+ const targetChunks = volumeCandidateChunks.slice(0, targetVolumeCount);
3033
+ const groupOpStartIndex = operations.length;
3034
+ const selectedExistingPackIds = new Set();
3035
+
3036
+ for (let volumeIndex = 0; volumeIndex < targetVolumeCount; volumeIndex += 1) {
3037
+ 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;
3046
+ if (existingPack?.id) {
3047
+ selectedExistingPackIds.add(existingPack.id);
3048
+ }
3049
+
3050
+ let createOwnerJid = null;
3051
+ if (!existingPack && enableAdditions) {
3052
+ if (autoPacks.length + plannedCreates >= GLOBAL_AUTO_PACK_LIMIT) {
3053
+ reuseOnlyMode = true;
3054
+ creationBlockedByGlobalCap += 1;
3055
+ } else {
3056
+ const selectedOwner = pickOwnerWithCapacity(ownerStates);
3057
+ if (selectedOwner) {
3058
+ createOwnerJid = selectedOwner.ownerJid;
3059
+ selectedOwner.available = Math.max(0, Number(selectedOwner.available || 0) - 1);
3060
+ selectedOwner.totalPacks = Number(selectedOwner.totalPacks || 0) + 1;
3061
+ plannedCreates += 1;
3062
+ } else {
3063
+ reuseOnlyMode = true;
3064
+ }
3065
+ }
3066
+ }
3067
+
3068
+ operations.push({
3069
+ type: 'reconcile_volume',
3070
+ sort_key: `${group.themeKey}#${String(volume).padStart(6, '0')}`,
3071
+ theme: group.theme,
3072
+ subtheme: group.subtheme,
3073
+ themeKey: group.themeKey,
3074
+ volume,
3075
+ groupScore: group.groupScore,
3076
+ cohesion: group.cohesion,
3077
+ desiredAssetIds: targetChunks[volumeIndex] || [],
3078
+ fillAssetIds: groupFillAssetIds,
3079
+ existingPackId: existingPack?.id || null,
3080
+ 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),
3084
+ });
3085
+ }
3086
+
3087
+ for (const pack of currentThemePacks) {
3088
+ if (selectedExistingPackIds.has(pack.id)) continue;
3089
+ const volume = extractVolumeFromPack(pack);
3090
+
3091
+ operations.push({
3092
+ type: 'archive_volume',
3093
+ sort_key: `${group.themeKey}#${String(volume).padStart(6, '0')}#archive`,
3094
+ theme: group.theme,
3095
+ subtheme: group.subtheme,
3096
+ themeKey: group.themeKey,
3097
+ volume,
3098
+ groupScore: group.groupScore,
3099
+ cohesion: group.cohesion,
3100
+ desiredAssetIds: [],
3101
+ existingPackId: pack.id,
3102
+ ownerJid: pack.owner_jid,
3103
+ ownerCandidates: [pack.owner_jid].filter(Boolean),
3104
+ });
3105
+ }
3106
+
3107
+ if (prioritizeGroupCompletion && EFFECTIVE_COMPLETION_TRANSFER_ENABLED && retainedThemePacks.length > 1) {
3108
+ const groupOps = operations.slice(groupOpStartIndex).filter((entry) => entry.type === 'reconcile_volume' && entry.existingPackId);
3109
+ const recipientOps = [...groupOps].sort((left, right) => {
3110
+ const leftCount = Number(packCountById.get(left.existingPackId) || 0);
3111
+ const rightCount = Number(packCountById.get(right.existingPackId) || 0);
3112
+ if (rightCount !== leftCount) return rightCount - leftCount;
3113
+ return Number(left.volume || 0) - Number(right.volume || 0);
3114
+ });
3115
+ const donorOps = [...groupOps].sort((left, right) => {
3116
+ const leftCount = Number(packCountById.get(left.existingPackId) || 0);
3117
+ const rightCount = Number(packCountById.get(right.existingPackId) || 0);
3118
+ if (leftCount !== rightCount) return leftCount - rightCount;
3119
+ return Number(left.volume || 0) - Number(right.volume || 0);
3120
+ });
3121
+
3122
+ let groupTransfers = 0;
3123
+ for (const recipientOp of recipientOps) {
3124
+ const recipientPackId = recipientOp.existingPackId;
3125
+ const recipientCount = Number(packCountById.get(recipientPackId) || 0);
3126
+ if (recipientCount <= 0) continue;
3127
+ const recipientDesired = new Set(
3128
+ (Array.isArray(recipientOp.desiredAssetIds) ? recipientOp.desiredAssetIds : []).filter(Boolean),
3129
+ );
3130
+ if (!recipientDesired.size) continue;
3131
+
3132
+ for (const assetId of recipientDesired) {
3133
+ for (const donorOp of donorOps) {
3134
+ const donorPackId = donorOp.existingPackId;
3135
+ if (!donorPackId || donorPackId === recipientPackId) continue;
3136
+
3137
+ const donorCount = Number(packCountById.get(donorPackId) || 0);
3138
+ if (donorCount >= recipientCount) continue;
3139
+ if (donorCount < COMPLETION_TRANSFER_MIN_DONOR_ITEMS) continue;
3140
+
3141
+ const donorItems = packItemsById.get(donorPackId);
3142
+ if (!donorItems || !donorItems.has(assetId)) continue;
3143
+
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);
3151
+ packCountById.set(donorPackId, Math.max(0, donorCount - 1));
3152
+ groupTransfers += 1;
3153
+ break;
3154
+ }
3155
+ }
3156
+ }
3157
+
3158
+ plannedCompletionTransfers += groupTransfers;
3159
+ }
3160
+ }
3161
+
3162
+ operations.sort((left, right) => String(left.sort_key || '').localeCompare(String(right.sort_key || '')));
3163
+ const optimizationAssetIds = new Set();
3164
+ for (const op of operations) {
3165
+ if (op?.existingPackId && op?.type === 'reconcile_volume') {
3166
+ const currentItems = itemsByPackId.get(op.existingPackId) || [];
3167
+ for (const item of currentItems) {
3168
+ if (item?.sticker_id) optimizationAssetIds.add(item.sticker_id);
3169
+ }
3170
+ }
3171
+ for (const assetId of Array.isArray(op?.desiredAssetIds) ? op.desiredAssetIds : []) {
3172
+ if (assetId) optimizationAssetIds.add(assetId);
3173
+ }
3174
+ }
3175
+
3176
+ const missingClassificationAssetIds = Array.from(optimizationAssetIds).filter((assetId) => !classificationByAssetId.has(assetId));
3177
+ for (const batch of chunkArray(missingClassificationAssetIds, 400)) {
3178
+ const rows = await listStickerClassificationsByAssetIds(batch);
3179
+ for (const row of rows) {
3180
+ if (row?.asset_id) classificationByAssetId.set(row.asset_id, row);
3181
+ }
3182
+ }
3183
+
3184
+ const optimizationStats = optimizePackEcosystem({
3185
+ operations,
3186
+ itemsByPackId,
3187
+ classificationByAssetId,
3188
+ packEngagementByPackId: engagementByPackId,
3189
+ });
3190
+
3191
+ return {
3192
+ operations,
3193
+ itemsByPackId,
3194
+ classificationByAssetId,
3195
+ autoPackIndex,
3196
+ stats: {
3197
+ 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),
3201
+ planned_creates: plannedCreates,
3202
+ completion_priority_groups: completionPriorityGroups,
3203
+ completion_transfers_planned: plannedCompletionTransfers,
3204
+ reuse_only_mode: reuseOnlyMode,
3205
+ overflow_volumes_skipped: overflowVolumesSkipped,
3206
+ hard_min_pack_items: HARD_MIN_PACK_ITEMS,
3207
+ max_packs_per_theme: MAX_PACKS_PER_THEME,
3208
+ global_auto_pack_limit: GLOBAL_AUTO_PACK_LIMIT,
3209
+ creation_blocked_global_cap: creationBlockedByGlobalCap,
3210
+ group_limit_static: Number.isFinite(staticGroupLimit) ? staticGroupLimit : null,
3211
+ group_limit_dynamic: dynamicGroupLimit,
3212
+ groups_input: curatedGroups.length,
3213
+ groups_effective: effectiveCuratedGroups.length,
3214
+ groups_skipped_dynamic: groupsSkippedByDynamicLimit,
3215
+ existing_auto_packs: autoPacks.length,
3216
+ auto_pack_items_indexed: allItems.length,
3217
+ optimization_scope_assets: optimizationAssetIds.size,
3218
+ optimization_missing_classifications: missingClassificationAssetIds.length,
3219
+ optimization_classifications_available: classificationByAssetId.size,
3220
+ optimization_enabled: optimizationStats.enabled,
3221
+ optimization_cycles_effective: optimizationStats.cycles_effective,
3222
+ optimization_transfer_moves: optimizationStats.transfer_moves,
3223
+ optimization_merge_moves: optimizationStats.merge_moves,
3224
+ optimization_matrix_merge_moves: optimizationStats.matrix_merge_moves,
3225
+ optimization_archived_packs: optimizationStats.archived_packs,
3226
+ optimization_tier_gold: optimizationStats.tier_gold,
3227
+ optimization_tier_silver: optimizationStats.tier_silver,
3228
+ optimization_tier_bronze: optimizationStats.tier_bronze,
3229
+ optimization_tier_archive: optimizationStats.tier_archive,
3230
+ optimization_energy_initial: optimizationStats.energy_initial,
3231
+ optimization_energy_final: optimizationStats.energy_final,
3232
+ optimization_energy_gain: optimizationStats.energy_gain,
3233
+ optimization_cycle_gains: optimizationStats.cycle_gains,
3234
+ optimization_stable_gain_cycles: optimizationStats.stable_gain_cycles,
3235
+ optimization_tier_mean_asset_quality: optimizationStats.tier_mean_asset_quality,
3236
+ optimization_interpack_similarity_mean: optimizationStats.interpack_similarity_mean,
3237
+ optimization_entropy_mean_global: optimizationStats.entropy_mean_global,
3238
+ },
3239
+ };
3240
+ };
3241
+
3242
+ const updateAutoPackMetadata = async (packId, payload) => {
3243
+ const normalizedPackStatus = String(payload?.pack_status || 'building').trim().toLowerCase() || 'building';
3244
+ const resolvedWebStatus = String(payload?.status || (normalizedPackStatus === 'ready' ? 'published' : 'draft'))
3245
+ .trim()
3246
+ .toLowerCase();
3247
+ const fields = {
3248
+ name: payload.name,
3249
+ publisher: AUTO_PUBLISHER,
3250
+ description: payload.description,
3251
+ visibility: AUTO_PACK_VISIBILITY,
3252
+ status: resolvedWebStatus,
3253
+ pack_status: normalizedPackStatus,
3254
+ pack_theme_key: payload.themeKey,
3255
+ pack_volume: payload.volume,
3256
+ is_auto_pack: 1,
3257
+ last_rebalanced_at: new Date(),
3258
+ };
3259
+ if ('cover_sticker_id' in payload) {
3260
+ fields.cover_sticker_id = payload.cover_sticker_id ?? null;
3261
+ }
3262
+ return updateStickerPackFields(packId, fields);
3263
+ };
3264
+
3265
+ const createAutoPackVolume = async ({
3266
+ ownerJid,
3267
+ theme,
3268
+ subtheme,
3269
+ themeKey,
3270
+ groupScore,
3271
+ volume,
3272
+ }) => {
3273
+ return stickerPackService.createPack({
3274
+ ownerJid,
3275
+ name: buildAutoPackName(theme, subtheme, volume),
3276
+ publisher: AUTO_PUBLISHER,
3277
+ description: buildAutoPackDescription({ theme, subtheme, themeKey, groupScore }),
3278
+ visibility: AUTO_PACK_VISIBILITY,
3279
+ status: 'draft',
3280
+ packStatus: 'building',
3281
+ packThemeKey: themeKey,
3282
+ packVolume: volume,
3283
+ isAutoPack: true,
3284
+ lastRebalancedAt: new Date(),
3285
+ });
3286
+ };
3287
+
3288
+ const getThemeScoreForPack = (classification, theme) => getScoreByTag(classification, theme);
3289
+
3290
+ const reconcileAutoPackVolume = async ({
3291
+ op,
3292
+ enableAdditions,
3293
+ enableRebuild,
3294
+ budgets,
3295
+ itemsByPackId,
3296
+ classificationByAssetId,
3297
+ }) => {
3298
+ let pack = op.existingPackId ? await findStickerPackById(op.existingPackId) : null;
3299
+
3300
+ if (!pack && op.type === 'reconcile_volume') {
3301
+ const ownerCandidates = Array.from(new Set((Array.isArray(op.ownerCandidates) ? op.ownerCandidates : [op.ownerJid]).filter(Boolean)));
3302
+ if (!enableAdditions || !ownerCandidates.length || budgets.added >= MAX_ADDITIONS_PER_CYCLE) {
3303
+ return { status: 'skipped_no_pack', created: 0, added: 0, removed: 0, duplicateSkips: 0, packLimitSkips: 0 };
3304
+ }
3305
+
3306
+ let ownerFullCount = 0;
3307
+ for (const ownerCandidate of ownerCandidates) {
3308
+ try {
3309
+ pack = await createAutoPackVolume({
3310
+ ownerJid: ownerCandidate,
3311
+ theme: op.theme,
3312
+ subtheme: op.subtheme,
3313
+ themeKey: op.themeKey,
3314
+ groupScore: op.groupScore,
3315
+ volume: op.volume,
3316
+ });
3317
+ break;
3318
+ } catch (error) {
3319
+ if (error?.code === 'PACK_LIMIT_REACHED') {
3320
+ ownerFullCount += 1;
3321
+ continue;
3322
+ }
3323
+ throw error;
3324
+ }
3325
+ }
3326
+
3327
+ if (!pack) {
3328
+ if (ownerFullCount > 0) {
3329
+ return {
3330
+ status: 'owner_full',
3331
+ created: 0,
3332
+ added: 0,
3333
+ removed: 0,
3334
+ duplicateSkips: 0,
3335
+ packLimitSkips: ownerFullCount,
3336
+ };
3337
+ }
3338
+ return { status: 'skipped_missing_pack', created: 0, added: 0, removed: 0, duplicateSkips: 0, packLimitSkips: 0 };
3339
+ }
3340
+ }
3341
+
3342
+ if (!pack) {
3343
+ return { status: 'skipped_missing_pack', created: 0, added: 0, removed: 0, duplicateSkips: 0, packLimitSkips: 0 };
3344
+ }
3345
+
3346
+ 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));
3349
+ const plannedIds = desiredPrimaryIds.slice();
3350
+ if (plannedIds.length < TARGET_PACK_SIZE) {
3351
+ for (const assetId of fillPoolIds) {
3352
+ if (plannedIds.length >= TARGET_PACK_SIZE) break;
3353
+ plannedIds.push(assetId);
3354
+ }
3355
+ }
3356
+ const desiredSet = new Set(plannedIds);
3357
+ let removed = 0;
3358
+ let added = 0;
3359
+ let duplicateSkips = 0;
3360
+ let packLimitSkips = 0;
3361
+ const created = op.existingPackId ? 0 : 1;
3362
+ const feedbackPromises = [];
3363
+ const queueFeedback = ({ classification, accepted, assetId }) => {
3364
+ const imageHash = String(classification?.image_hash || '').trim().toLowerCase();
3365
+ if (!imageHash) return;
3366
+ feedbackPromises.push(
3367
+ submitStickerClassificationFeedback({
3368
+ imageHash,
3369
+ theme: op.themeKey || op.theme,
3370
+ accepted,
3371
+ assetId,
3372
+ }),
3373
+ );
3374
+ };
3375
+
3376
+ await updateAutoPackMetadata(pack.id, {
3377
+ name: buildAutoPackName(op.theme, op.subtheme, op.volume),
3378
+ description: buildAutoPackDescription({ theme: op.theme, subtheme: op.subtheme, themeKey: op.themeKey, groupScore: op.groupScore }),
3379
+ themeKey: op.themeKey,
3380
+ volume: op.volume,
3381
+ pack_status: 'building',
3382
+ cover_sticker_id: pack.cover_sticker_id || null,
3383
+ });
3384
+
3385
+ let currentItems = itemsByPackId.get(pack.id) || await listStickerPackItems(pack.id);
3386
+ const currentById = new Map(currentItems.map((item) => [item.sticker_id, item]));
3387
+ const forceRemoveSet = new Set((Array.isArray(op.forceRemoveAssetIds) ? op.forceRemoveAssetIds : []).filter(Boolean));
3388
+
3389
+ const shouldRebuildVolume =
3390
+ enableRebuild
3391
+ || op.type === 'archive_volume'
3392
+ || Number(op.cohesion || 0) < COHESION_REBUILD_THRESHOLD
3393
+ || forceRemoveSet.size > 0;
3394
+ if (shouldRebuildVolume && budgets.removed < MAX_REMOVALS_PER_CYCLE) {
3395
+ for (const item of currentItems) {
3396
+ if (budgets.removed >= MAX_REMOVALS_PER_CYCLE) break;
3397
+ if (desiredSet.has(item.sticker_id)) continue;
3398
+
3399
+ const classification = classificationByAssetId.get(item.sticker_id);
3400
+ const themeScore = getThemeScoreForPack(classification, op.theme);
3401
+ const forcedMoveOut = forceRemoveSet.has(item.sticker_id);
3402
+ if (!forcedMoveOut && !enableRebuild && op.type !== 'archive_volume' && themeScore >= MOVE_OUT_THEME_SCORE_THRESHOLD) {
3403
+ continue;
3404
+ }
3405
+
3406
+ try {
3407
+ await stickerPackService.removeStickerFromPack({
3408
+ ownerJid: pack.owner_jid,
3409
+ identifier: pack.id,
3410
+ selector: item.sticker_id,
3411
+ });
3412
+ budgets.removed += 1;
3413
+ removed += 1;
3414
+ currentById.delete(item.sticker_id);
3415
+ queueFeedback({
3416
+ classification,
3417
+ accepted: false,
3418
+ assetId: item.sticker_id,
3419
+ });
3420
+ } catch (error) {
3421
+ logger.warn('Falha ao remover sticker no rebalance de auto-pack por tags.', {
3422
+ action: 'sticker_auto_pack_by_tags_rebalance_remove_failed',
3423
+ pack_id: pack.id,
3424
+ asset_id: item.sticker_id,
3425
+ theme_key: op.themeKey,
3426
+ error: error?.message,
3427
+ error_code: error?.code,
3428
+ });
3429
+ }
3430
+ }
3431
+
3432
+ currentItems = await listStickerPackItems(pack.id);
3433
+ itemsByPackId.set(pack.id, currentItems);
3434
+ currentById.clear();
3435
+ for (const item of currentItems) currentById.set(item.sticker_id, item);
3436
+ }
3437
+
3438
+ if (enableAdditions && budgets.added < MAX_ADDITIONS_PER_CYCLE && plannedIds.length) {
3439
+ const hasPendingCandidates = plannedIds.some((assetId) => !currentById.has(assetId));
3440
+ let availableSlots = Math.max(0, TARGET_PACK_SIZE - currentById.size);
3441
+
3442
+ if (availableSlots <= 0) {
3443
+ if (hasPendingCandidates) {
3444
+ packLimitSkips += 1;
3445
+ }
3446
+ } else {
3447
+ for (const assetId of plannedIds) {
3448
+ if (budgets.added >= MAX_ADDITIONS_PER_CYCLE) break;
3449
+ if (currentById.has(assetId)) continue;
3450
+ if (availableSlots <= 0) {
3451
+ packLimitSkips += 1;
3452
+ break;
3453
+ }
3454
+
3455
+ try {
3456
+ await stickerPackService.addStickerToPack({
3457
+ ownerJid: pack.owner_jid,
3458
+ identifier: pack.id,
3459
+ asset: { id: assetId },
3460
+ emojis: [],
3461
+ accessibilityLabel: `Auto-theme ${op.theme}${op.subtheme ? `/${op.subtheme}` : ''}`,
3462
+ expectedErrorCodes: ['PACK_LIMIT_REACHED'],
3463
+ });
3464
+ budgets.added += 1;
3465
+ added += 1;
3466
+ availableSlots = Math.max(0, availableSlots - 1);
3467
+ queueFeedback({
3468
+ classification: classificationByAssetId.get(assetId),
3469
+ accepted: true,
3470
+ assetId,
3471
+ });
3472
+ } catch (error) {
3473
+ if (error?.code === 'DUPLICATE_STICKER') {
3474
+ duplicateSkips += 1;
3475
+ continue;
3476
+ }
3477
+ if (error?.code === 'PACK_LIMIT_REACHED') {
3478
+ packLimitSkips += 1;
3479
+ availableSlots = 0;
3480
+ break;
3481
+ }
3482
+
3483
+ logger.warn('Falha ao adicionar sticker em auto-pack por tags.', {
3484
+ action: 'sticker_auto_pack_by_tags_add_failed',
3485
+ theme: op.theme,
3486
+ subtheme: op.subtheme || null,
3487
+ theme_key: op.themeKey,
3488
+ pack_id: pack.id,
3489
+ asset_id: assetId,
3490
+ error: error?.message,
3491
+ error_code: error?.code,
3492
+ });
3493
+ }
3494
+ }
3495
+ }
3496
+
3497
+ currentItems = await listStickerPackItems(pack.id);
3498
+ itemsByPackId.set(pack.id, currentItems);
3499
+ currentById.clear();
3500
+ for (const item of currentItems) currentById.set(item.sticker_id, item);
3501
+ }
3502
+
3503
+ const finalDesiredOrder = plannedIds.filter((assetId) => currentById.has(assetId));
3504
+ const currentOrder = currentItems.map((item) => item.sticker_id);
3505
+ 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);
3514
+
3515
+ if (needsReorder) {
3516
+ try {
3517
+ await stickerPackService.reorderPackItems({
3518
+ ownerJid: pack.owner_jid,
3519
+ identifier: pack.id,
3520
+ orderStickerIds: finalDesiredOrder,
3521
+ });
3522
+ currentItems = await listStickerPackItems(pack.id);
3523
+ itemsByPackId.set(pack.id, currentItems);
3524
+ } catch (error) {
3525
+ logger.warn('Falha ao reordenar auto-pack por tags.', {
3526
+ action: 'sticker_auto_pack_by_tags_reorder_failed',
3527
+ pack_id: pack.id,
3528
+ theme_key: op.themeKey,
3529
+ error: error?.message,
3530
+ error_code: error?.code,
3531
+ });
3532
+ }
3533
+ }
3534
+
3535
+ const finalItems = itemsByPackId.get(pack.id) || await listStickerPackItems(pack.id);
3536
+ const finalCount = finalItems.length;
3537
+ const finalCover = finalItems[0]?.sticker_id || null;
3538
+ const finalDesiredPresentCount = finalItems.filter((item) => desiredSet.has(item.sticker_id)).length;
3539
+ const allDesiredPresent = finalDesiredPresentCount === plannedIds.length;
3540
+ const hasExtraItems = finalItems.some((item) => !desiredSet.has(item.sticker_id));
3541
+ const allowExtraItemsForReady = !enableRebuild && op.type === 'reconcile_volume';
3542
+ const meetsHardMinimum = finalCount >= HARD_MIN_PACK_ITEMS;
3543
+ 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);
3555
+ if (shouldDeletePack) {
3556
+ try {
3557
+ await deleteAutoPackWithItems(pack.id, itemsByPackId);
3558
+ if (feedbackPromises.length) {
3559
+ await Promise.allSettled(feedbackPromises);
3560
+ }
3561
+ return {
3562
+ status: 'archived',
3563
+ pack: null,
3564
+ created,
3565
+ added,
3566
+ removed,
3567
+ deleted: 1,
3568
+ duplicateSkips,
3569
+ packLimitSkips,
3570
+ finalCount: 0,
3571
+ };
3572
+ } catch (error) {
3573
+ logger.warn('Falha ao excluir auto-pack arquivado durante consolidação.', {
3574
+ action: 'sticker_auto_pack_delete_archived_failed',
3575
+ pack_id: pack.id,
3576
+ theme_key: op.themeKey,
3577
+ error: error?.message,
3578
+ });
3579
+ }
3580
+ }
3581
+
3582
+ const updated = await updateAutoPackMetadata(pack.id, {
3583
+ name: buildAutoPackName(op.theme, op.subtheme, op.volume),
3584
+ description: buildAutoPackDescription({ theme: op.theme, subtheme: op.subtheme, themeKey: op.themeKey, groupScore: op.groupScore }),
3585
+ themeKey: op.themeKey,
3586
+ volume: op.volume,
3587
+ pack_status: packStatus,
3588
+ cover_sticker_id: finalCover,
3589
+ });
3590
+
3591
+ if (feedbackPromises.length) {
3592
+ await Promise.allSettled(feedbackPromises);
3593
+ }
3594
+
3595
+ itemsByPackId.set(pack.id, finalItems);
3596
+ return {
3597
+ status: packStatus,
3598
+ pack: updated,
3599
+ created,
3600
+ added,
3601
+ removed,
3602
+ deleted: 0,
3603
+ duplicateSkips,
3604
+ packLimitSkips,
3605
+ finalCount,
3606
+ };
3607
+ };
3608
+
3609
+ let cycleHandle = null;
3610
+ let startupHandle = null;
3611
+ let running = false;
3612
+ let schedulerEnabled = false;
3613
+
3614
+ const clearCycleHandle = () => {
3615
+ if (!cycleHandle) return;
3616
+ clearTimeout(cycleHandle);
3617
+ cycleHandle = null;
3618
+ };
3619
+
3620
+ const countClassifiedWithoutPackSafely = async ({ phase = 'unknown' } = {}) => {
3621
+ try {
3622
+ return await countClassifiedStickerAssetsWithoutPack();
3623
+ } catch (error) {
3624
+ logger.warn('Falha ao contar assets classificados sem pack.', {
3625
+ action: 'sticker_auto_pack_by_tags_without_pack_count_failed',
3626
+ phase,
3627
+ error: error?.message,
3628
+ });
3629
+ return null;
3630
+ }
3631
+ };
3632
+
3633
+ const resolveNextCycleDelayMs = () => {
3634
+ if (EFFECTIVE_INTERVAL_MAX_MS <= EFFECTIVE_INTERVAL_MIN_MS) {
3635
+ return EFFECTIVE_INTERVAL_MIN_MS;
3636
+ }
3637
+
3638
+ return EFFECTIVE_INTERVAL_MIN_MS
3639
+ + Math.floor(Math.random() * (EFFECTIVE_INTERVAL_MAX_MS - EFFECTIVE_INTERVAL_MIN_MS + 1));
3640
+ };
3641
+
3642
+ const scheduleNextCycle = () => {
3643
+ if (!schedulerEnabled) return;
3644
+ clearCycleHandle();
3645
+
3646
+ const safeDelay = Math.max(1_000, resolveNextCycleDelayMs());
3647
+ cycleHandle = setTimeout(() => {
3648
+ cycleHandle = null;
3649
+ if (!schedulerEnabled) return;
3650
+ scheduleNextCycle();
3651
+ void runStickerAutoPackByTagsCycle().catch((error) => {
3652
+ logger.error('Falha ao executar ciclo agendado do auto-pack por tags.', {
3653
+ action: 'sticker_auto_pack_by_tags_cycle_schedule_failed',
3654
+ error: error?.message,
3655
+ stack: error?.stack,
3656
+ });
3657
+ });
3658
+ }, safeDelay);
3659
+
3660
+ if (typeof cycleHandle.unref === 'function') {
3661
+ cycleHandle.unref();
3662
+ }
3663
+ };
3664
+
3665
+ export const runStickerAutoPackByTagsCycle = async ({
3666
+ enableAdditions = true,
3667
+ enableRebuild = REBUILD_ENABLED,
3668
+ } = {}) => {
3669
+ if (running) {
3670
+ return {
3671
+ executed: false,
3672
+ reason: 'already_running',
3673
+ added_stickers: 0,
3674
+ };
3675
+ }
3676
+ if (!AUTO_ENABLED) {
3677
+ return {
3678
+ executed: false,
3679
+ reason: 'disabled',
3680
+ added_stickers: 0,
3681
+ };
3682
+ }
3683
+
3684
+ const ownerPool = resolveCurationOwnerPool();
3685
+ if (!ownerPool.length) {
3686
+ logger.warn('Auto-pack por tags: owner_jid indisponível, ciclo ignorado.', {
3687
+ action: 'sticker_auto_pack_by_tags_owner_missing',
3688
+ });
3689
+ return {
3690
+ executed: false,
3691
+ reason: 'owner_missing',
3692
+ added_stickers: 0,
3693
+ };
3694
+ }
3695
+
3696
+ running = true;
3697
+ const startedAt = Date.now();
3698
+ const budgets = { added: 0, removed: 0 };
3699
+ let createdPacks = 0;
3700
+ let processedGroups = 0;
3701
+ let processedVolumes = 0;
3702
+ let duplicateSkips = 0;
3703
+ let packLimitSkips = 0;
3704
+ let readyPacks = 0;
3705
+ let buildingPacks = 0;
3706
+ let archivedPacks = 0;
3707
+ let ownerFullSkips = 0;
3708
+ let deletedPacks = 0;
3709
+ let consolidationStats = {
3710
+ enabled: RETRO_CONSOLIDATION_ENABLED,
3711
+ processed_themes: 0,
3712
+ merged_themes: 0,
3713
+ deleted_packs: 0,
3714
+ moved_stickers: 0,
3715
+ trimmed_stickers: 0,
3716
+ mutations: 0,
3717
+ theme_limit_reached: false,
3718
+ mutation_limit_reached: false,
3719
+ };
3720
+ const classifiedWithoutPackBefore = await countClassifiedWithoutPackSafely({ phase: 'before_cycle' });
3721
+ const finalizeCycleResult = async (payload) => {
3722
+ const withoutPackBefore = Number.isFinite(classifiedWithoutPackBefore)
3723
+ ? Number(classifiedWithoutPackBefore)
3724
+ : null;
3725
+ if (!Number.isFinite(withoutPackBefore)) {
3726
+ return {
3727
+ ...payload,
3728
+ without_pack_before: null,
3729
+ without_pack_after: null,
3730
+ without_pack_delta: null,
3731
+ };
3732
+ }
3733
+
3734
+ 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;
3741
+
3742
+ return {
3743
+ ...payload,
3744
+ without_pack_before: withoutPackBefore,
3745
+ without_pack_after: withoutPackAfter,
3746
+ without_pack_delta: Number.isFinite(withoutPackDelta) ? withoutPackDelta : null,
3747
+ };
3748
+ };
3749
+
3750
+ try {
3751
+ if (enableAdditions) {
3752
+ consolidationStats = await runRetroConsolidationCycle({ ownerPool });
3753
+ } else {
3754
+ consolidationStats = {
3755
+ ...consolidationStats,
3756
+ enabled: false,
3757
+ };
3758
+ }
3759
+ const includePackedForCycle = enableRebuild || INCLUDE_PACKED_WHEN_REBUILD_DISABLED;
3760
+ const curationInput = await collectCuratableCandidates({
3761
+ includePacked: includePackedForCycle,
3762
+ includeUnpacked: true,
3763
+ });
3764
+ const { groups: curatedGroups, stats } = buildCurationPlan(curationInput);
3765
+
3766
+ if (!curatedGroups.length) {
3767
+ const idleResult = await finalizeCycleResult({
3768
+ executed: true,
3769
+ reason: 'idle',
3770
+ added_stickers: 0,
3771
+ removed_stickers: 0,
3772
+ created_packs: 0,
3773
+ pack_limit_skips: 0,
3774
+ processed_groups: 0,
3775
+ processed_volumes: 0,
3776
+ duration_ms: Date.now() - startedAt,
3777
+ });
3778
+ logger.debug('Auto-pack por tags: nenhum grupo elegível neste ciclo.', {
3779
+ action: 'sticker_auto_pack_by_tags_idle',
3780
+ without_pack_before: idleResult.without_pack_before,
3781
+ without_pack_after: idleResult.without_pack_after,
3782
+ without_pack_delta: idleResult.without_pack_delta,
3783
+ retro_consolidation: consolidationStats,
3784
+ ...stats,
3785
+ });
3786
+ return idleResult;
3787
+ }
3788
+
3789
+ const planner = await buildCurationExecutionPlan({
3790
+ curatedGroups,
3791
+ ownerPool,
3792
+ enableAdditions,
3793
+ });
3794
+ processedGroups = Number(planner?.stats?.groups_effective || curatedGroups.length || 0);
3795
+
3796
+ for (const op of planner.operations) {
3797
+ const result = await reconcileAutoPackVolume({
3798
+ op,
3799
+ enableAdditions,
3800
+ enableRebuild,
3801
+ budgets,
3802
+ itemsByPackId: planner.itemsByPackId,
3803
+ classificationByAssetId: planner.classificationByAssetId,
3804
+ });
3805
+
3806
+ processedVolumes += 1;
3807
+ createdPacks += Number(result?.created || 0);
3808
+ duplicateSkips += Number(result?.duplicateSkips || 0);
3809
+ packLimitSkips += Number(result?.packLimitSkips || 0);
3810
+ deletedPacks += Number(result?.deleted || 0);
3811
+ if (result?.status === 'owner_full') {
3812
+ ownerFullSkips += 1;
3813
+ }
3814
+ if (result?.status === 'ready') readyPacks += 1;
3815
+ if (result?.status === 'building') buildingPacks += 1;
3816
+ if (result?.status === 'archived') archivedPacks += 1;
3817
+ }
3818
+
3819
+ const scanReferenceCount = Math.max(
3820
+ 1,
3821
+ Number(stats.assets_unique_scanned || stats.assets_total_seen || stats.assets_scanned || 0),
3822
+ );
3823
+ const rejectionReferenceCount = Math.max(1, Number(stats.assets_scanned || 0));
3824
+ const duplicateRate = Number(stats.assets_deduped || 0) / scanReferenceCount;
3825
+ const rejectedCount = Number(stats.assets_rejected_quality || 0) + Number(stats.assets_rejected_no_theme || 0);
3826
+ const rejectionRate = rejectedCount / rejectionReferenceCount;
3827
+ const fillRate = budgets.added / Math.max(1, processedVolumes * TARGET_PACK_SIZE);
3828
+
3829
+ recordStickerAutoPackCycle({
3830
+ durationMs: Date.now() - startedAt,
3831
+ assetsScanned: Number(stats.assets_scanned || 0),
3832
+ assetsAdded: budgets.added,
3833
+ duplicateRate,
3834
+ rejectionRate,
3835
+ fillRate,
3836
+ });
3837
+
3838
+ const cycleResult = await finalizeCycleResult({
3839
+ executed: true,
3840
+ reason: 'ok',
3841
+ added_stickers: Number(budgets.added || 0),
3842
+ removed_stickers: Number(budgets.removed || 0),
3843
+ created_packs: Number(createdPacks || 0),
3844
+ pack_limit_skips: Number(packLimitSkips || 0),
3845
+ processed_groups: Number(processedGroups || 0),
3846
+ processed_volumes: Number(processedVolumes || 0),
3847
+ duration_ms: Date.now() - startedAt,
3848
+ });
3849
+
3850
+ logger.info('Auto-pack por tags executado.', {
3851
+ action: 'sticker_auto_pack_by_tags_cycle',
3852
+ owner_jid: ownerPool[0],
3853
+ owner_pool: ownerPool,
3854
+ processed_groups: processedGroups,
3855
+ processed_volumes: processedVolumes,
3856
+ created_packs: createdPacks,
3857
+ added_stickers: budgets.added,
3858
+ removed_stickers: budgets.removed,
3859
+ deleted_packs: deletedPacks,
3860
+ ready_packs_touched: readyPacks,
3861
+ building_packs_touched: buildingPacks,
3862
+ archived_packs_touched: archivedPacks,
3863
+ owner_full_skips: ownerFullSkips,
3864
+ rebuild_enabled_cycle: Boolean(enableRebuild),
3865
+ additions_enabled_cycle: Boolean(enableAdditions),
3866
+ include_packed_cycle: Boolean(includePackedForCycle),
3867
+ include_unpacked_cycle: true,
3868
+ cohesion_rebuild_threshold: Number(COHESION_REBUILD_THRESHOLD.toFixed(6)),
3869
+ duplicate_skips: duplicateSkips,
3870
+ pack_limit_skips: packLimitSkips,
3871
+ duration_ms: Date.now() - startedAt,
3872
+ duplicate_rate: Number(duplicateRate.toFixed(6)),
3873
+ rejection_rate: Number(rejectionRate.toFixed(6)),
3874
+ pack_fill_rate: Number(fillRate.toFixed(6)),
3875
+ without_pack_before: cycleResult.without_pack_before,
3876
+ without_pack_after: cycleResult.without_pack_after,
3877
+ without_pack_delta: cycleResult.without_pack_delta,
3878
+ min_group_size: MIN_GROUP_SIZE,
3879
+ target_pack_size: TARGET_PACK_SIZE,
3880
+ max_additions_per_cycle: MAX_ADDITIONS_PER_CYCLE,
3881
+ max_removals_per_cycle: MAX_REMOVALS_PER_CYCLE,
3882
+ move_out_theme_score_threshold: Number(MOVE_OUT_THEME_SCORE_THRESHOLD.toFixed(6)),
3883
+ move_in_theme_score_threshold: Number(MOVE_IN_THEME_SCORE_THRESHOLD.toFixed(6)),
3884
+ ready_pack_min_items: READY_PACK_MIN_ITEMS,
3885
+ hard_min_group_size: EFFECTIVE_HARD_MIN_GROUP_SIZE,
3886
+ hard_min_group_size_base: HARD_MIN_GROUP_SIZE,
3887
+ hard_min_pack_items: HARD_MIN_PACK_ITEMS,
3888
+ semantic_clustering_enabled: ENABLE_SEMANTIC_CLUSTERING,
3889
+ semantic_cluster_min_size_for_pack: SEMANTIC_CLUSTER_MIN_SIZE_FOR_PACK,
3890
+ max_packs_per_theme: MAX_PACKS_PER_THEME,
3891
+ global_auto_pack_limit: GLOBAL_AUTO_PACK_LIMIT,
3892
+ dynamic_group_limit_base: DYNAMIC_GROUP_LIMIT_BASE,
3893
+ retro_consolidation_enabled: RETRO_CONSOLIDATION_ENABLED,
3894
+ retro_consolidation_theme_limit: RETRO_CONSOLIDATION_THEME_LIMIT,
3895
+ retro_consolidation_mutation_limit: RETRO_CONSOLIDATION_MUTATION_LIMIT,
3896
+ retro_consolidation: consolidationStats,
3897
+ ...planner.stats,
3898
+ ...stats,
3899
+ });
3900
+ return cycleResult;
3901
+ } catch (error) {
3902
+ logger.error('Falha no ciclo do auto-pack por tags.', {
3903
+ action: 'sticker_auto_pack_by_tags_cycle_failed',
3904
+ error: error?.message,
3905
+ stack: error?.stack,
3906
+ });
3907
+ return finalizeCycleResult({
3908
+ executed: true,
3909
+ reason: 'failed',
3910
+ added_stickers: Number(budgets.added || 0),
3911
+ removed_stickers: Number(budgets.removed || 0),
3912
+ created_packs: Number(createdPacks || 0),
3913
+ pack_limit_skips: Number(packLimitSkips || 0),
3914
+ processed_groups: Number(processedGroups || 0),
3915
+ processed_volumes: Number(processedVolumes || 0),
3916
+ duration_ms: Date.now() - startedAt,
3917
+ });
3918
+ } finally {
3919
+ running = false;
3920
+ }
3921
+ };
3922
+
3923
+ export const startStickerAutoPackByTagsBackground = () => {
3924
+ if (startupHandle || cycleHandle || schedulerEnabled) return;
3925
+
3926
+ if (!AUTO_ENABLED) {
3927
+ logger.info('Auto-pack por tags desabilitado.', {
3928
+ action: 'sticker_auto_pack_by_tags_disabled',
3929
+ });
3930
+ return;
3931
+ }
3932
+ schedulerEnabled = true;
3933
+
3934
+ logger.info('Iniciando auto-pack por tags em background.', {
3935
+ action: 'sticker_auto_pack_by_tags_start',
3936
+ startup_delay_ms: STARTUP_DELAY_MS,
3937
+ interval_min_ms: EFFECTIVE_INTERVAL_MIN_MS,
3938
+ interval_max_ms: EFFECTIVE_INTERVAL_MAX_MS,
3939
+ scheduler_mode: 'timer_non_chained_random_window',
3940
+ interval_source: LEGACY_FIXED_INTERVAL_MS ? 'legacy_fixed_interval_ms' : 'interval_window',
3941
+ target_pack_size: TARGET_PACK_SIZE,
3942
+ min_group_size: MIN_GROUP_SIZE,
3943
+ hard_min_group_size: EFFECTIVE_HARD_MIN_GROUP_SIZE,
3944
+ hard_min_group_size_base: HARD_MIN_GROUP_SIZE,
3945
+ hard_min_pack_items: HARD_MIN_PACK_ITEMS,
3946
+ semantic_clustering_enabled: ENABLE_SEMANTIC_CLUSTERING,
3947
+ semantic_cluster_min_size_for_pack: SEMANTIC_CLUSTER_MIN_SIZE_FOR_PACK,
3948
+ ready_pack_min_items: READY_PACK_MIN_ITEMS,
3949
+ max_packs_per_theme: MAX_PACKS_PER_THEME,
3950
+ global_auto_pack_limit: GLOBAL_AUTO_PACK_LIMIT,
3951
+ dynamic_group_limit_base: DYNAMIC_GROUP_LIMIT_BASE,
3952
+ max_groups: MAX_TAG_GROUPS,
3953
+ max_scan_assets: MAX_SCAN_ASSETS,
3954
+ max_additions_per_cycle: MAX_ADDITIONS_PER_CYCLE,
3955
+ max_packs_per_owner: Number.isFinite(MAX_PACKS_PER_OWNER) ? MAX_PACKS_PER_OWNER : 'unlimited',
3956
+ retro_consolidation_enabled: RETRO_CONSOLIDATION_ENABLED,
3957
+ retro_consolidation_theme_limit: RETRO_CONSOLIDATION_THEME_LIMIT,
3958
+ retro_consolidation_mutation_limit: RETRO_CONSOLIDATION_MUTATION_LIMIT,
3959
+ auto_pack_profile: AUTO_PACK_PROFILE,
3960
+ aggressive_profile: IS_AGGRESSIVE_PROFILE,
3961
+ prioritize_completion: EFFECTIVE_PRIORITIZE_COMPLETION,
3962
+ completion_transfer_enabled: EFFECTIVE_COMPLETION_TRANSFER_ENABLED,
3963
+ completion_transfer_min_donor_items: COMPLETION_TRANSFER_MIN_DONOR_ITEMS,
3964
+ archive_low_score_packs: ARCHIVE_LOW_SCORE_PACKS,
3965
+ visibility: AUTO_PACK_VISIBILITY,
3966
+ top_tags_per_asset: TOP_TAGS_PER_ASSET,
3967
+ scan_passes: SCAN_PASSES,
3968
+ scan_pass_jitter_percent: SCAN_PASS_JITTER_PERCENT,
3969
+ stability_z_score: STABILITY_Z_SCORE,
3970
+ min_asset_acceptance_rate: EFFECTIVE_MIN_ASSET_ACCEPTANCE_RATE,
3971
+ min_theme_dominance_ratio: EFFECTIVE_MIN_THEME_DOMINANCE_RATIO,
3972
+ score_stddev_penalty: EFFECTIVE_SCORE_STDDEV_PENALTY,
3973
+ nsfw_threshold: NSFW_THRESHOLD,
3974
+ nsfw_suggestive_threshold: NSFW_SUGGESTIVE_THRESHOLD,
3975
+ nsfw_explicit_threshold: NSFW_EXPLICIT_THRESHOLD,
3976
+ rebuild_enabled: REBUILD_ENABLED,
3977
+ include_packed_when_rebuild_disabled: INCLUDE_PACKED_WHEN_REBUILD_DISABLED,
3978
+ max_removals_per_cycle: MAX_REMOVALS_PER_CYCLE,
3979
+ dedupe_similarity_threshold: DEDUPE_SIMILARITY_THRESHOLD,
3980
+ global_optimization_enabled: ENABLE_GLOBAL_OPTIMIZATION,
3981
+ optimization_cycles: OPTIMIZATION_CYCLES,
3982
+ optimization_epsilon: OPTIMIZATION_EPSILON,
3983
+ optimization_stable_cycles: OPTIMIZATION_STABLE_CYCLES,
3984
+ transfer_threshold: EFFECTIVE_TRANSFER_THRESHOLD,
3985
+ merge_threshold: EFFECTIVE_MERGE_THRESHOLD,
3986
+ migration_candidate_limit: EFFECTIVE_MIGRATION_CANDIDATE_LIMIT,
3987
+ transfer_candidate_similarity_floor: EFFECTIVE_TRANSFER_CANDIDATE_SIMILARITY_FLOOR,
3988
+ inter_pack_similarity_threshold: EFFECTIVE_INTER_PACK_SIMILARITY_THRESHOLD,
3989
+ entropy_threshold: ENTROPY_THRESHOLD,
3990
+ entropy_normalized_threshold: ENTROPY_NORMALIZED_THRESHOLD,
3991
+ entropy_weight: ENTROPY_WEIGHT,
3992
+ ambiguous_flag_penalty: AMBIGUOUS_FLAG_PENALTY,
3993
+ adaptive_bonus_weight: ADAPTIVE_BONUS_WEIGHT,
3994
+ margin_bonus_weight: MARGIN_BONUS_WEIGHT,
3995
+ affinity_weight_cap: AFFINITY_WEIGHT_CAP,
3996
+ affinity_log_scaling: ENABLE_AFFINITY_LOG_SCALING,
3997
+ affinity_log_scale: AFFINITY_LOG_SCALE,
3998
+ similar_images_penalty_weight: SIMILAR_IMAGES_PENALTY_WEIGHT,
3999
+ llm_trait_weight: LLM_TRAIT_WEIGHT,
4000
+ asset_quality_weights: {
4001
+ w1: ASSET_QUALITY_W1,
4002
+ w2: ASSET_QUALITY_W2,
4003
+ w3: ASSET_QUALITY_W3,
4004
+ w4: ASSET_QUALITY_W4,
4005
+ },
4006
+ global_energy_weights: {
4007
+ w1: GLOBAL_ENERGY_W1,
4008
+ w2: GLOBAL_ENERGY_W2,
4009
+ w3: GLOBAL_ENERGY_W3,
4010
+ w4: GLOBAL_ENERGY_W4,
4011
+ w5: GLOBAL_ENERGY_W5,
4012
+ },
4013
+ pack_tier_thresholds: {
4014
+ gold: PACK_TIER_GOLD_THRESHOLD,
4015
+ silver: PACK_TIER_SILVER_THRESHOLD,
4016
+ bronze: PACK_TIER_BRONZE_THRESHOLD,
4017
+ },
4018
+ min_pack_size: MIN_PACK_SIZE,
4019
+ auto_archive_below_percentile: EFFECTIVE_AUTO_ARCHIVE_BELOW_PERCENTILE,
4020
+ system_redundancy_lambda: SYSTEM_REDUNDANCY_LAMBDA,
4021
+ matrix_weights: {
4022
+ alpha: MATRIX_ALPHA,
4023
+ beta: MATRIX_BETA,
4024
+ gamma: MATRIX_GAMMA,
4025
+ delta: MATRIX_DELTA,
4026
+ },
4027
+ pack_quality_weights: {
4028
+ w1: PACK_QUALITY_W1,
4029
+ w2: PACK_QUALITY_W2,
4030
+ w3: PACK_QUALITY_W3,
4031
+ w4: PACK_QUALITY_W4,
4032
+ w5: PACK_QUALITY_W5,
4033
+ w6: PACK_QUALITY_W6,
4034
+ },
4035
+ pack_tier_quality_weights: {
4036
+ mean_asset_quality: PACK_TIER_QUALITY_W1,
4037
+ cohesion_score: PACK_TIER_QUALITY_W2,
4038
+ engagement_zscore: PACK_TIER_QUALITY_W3,
4039
+ completion_ratio: PACK_TIER_QUALITY_W4,
4040
+ stability_index: PACK_TIER_QUALITY_W5,
4041
+ },
4042
+ quality_gate: {
4043
+ min_asset_edge: MIN_ASSET_EDGE,
4044
+ min_asset_area: MIN_ASSET_AREA,
4045
+ min_asset_bytes: MIN_ASSET_BYTES,
4046
+ max_blurry_score: MAX_BLURRY_SCORE,
4047
+ max_low_quality_score: MAX_LOW_QUALITY_SCORE,
4048
+ },
4049
+ });
4050
+
4051
+ startupHandle = setTimeout(() => {
4052
+ startupHandle = null;
4053
+ if (!schedulerEnabled) return;
4054
+ scheduleNextCycle();
4055
+ void runStickerAutoPackByTagsCycle().catch((error) => {
4056
+ logger.error('Falha ao executar ciclo inicial do auto-pack por tags.', {
4057
+ action: 'sticker_auto_pack_by_tags_initial_cycle_failed',
4058
+ error: error?.message,
4059
+ stack: error?.stack,
4060
+ });
4061
+ });
4062
+ }, STARTUP_DELAY_MS);
4063
+
4064
+ if (typeof startupHandle.unref === 'function') {
4065
+ startupHandle.unref();
4066
+ }
4067
+ };
4068
+
4069
+ export const stopStickerAutoPackByTagsBackground = () => {
4070
+ schedulerEnabled = false;
4071
+
4072
+ if (startupHandle) {
4073
+ clearTimeout(startupHandle);
4074
+ startupHandle = null;
4075
+ }
4076
+
4077
+ clearCycleHandle();
4078
+ };