funifier-mcp 0.2.25 → 0.2.27

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 (211) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +5 -2
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +1011 -77
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/core/api-client.d.ts +21 -1
  66. package/dist/core/api-client.d.ts.map +1 -1
  67. package/dist/core/api-client.js +154 -1
  68. package/dist/core/api-client.js.map +1 -1
  69. package/dist/core/constants.d.ts +14 -0
  70. package/dist/core/constants.d.ts.map +1 -1
  71. package/dist/core/constants.js +14 -0
  72. package/dist/core/constants.js.map +1 -1
  73. package/dist/core/types/Folder.d.ts +16 -0
  74. package/dist/core/types/Folder.d.ts.map +1 -0
  75. package/dist/core/types/Folder.js +3 -0
  76. package/dist/core/types/Folder.js.map +1 -0
  77. package/dist/core/types/FolderContent.d.ts +10 -0
  78. package/dist/core/types/FolderContent.d.ts.map +1 -0
  79. package/dist/core/types/FolderContent.js +3 -0
  80. package/dist/core/types/FolderContent.js.map +1 -0
  81. package/dist/core/types/FolderContentType.d.ts +10 -0
  82. package/dist/core/types/FolderContentType.d.ts.map +1 -0
  83. package/dist/core/types/FolderContentType.js +3 -0
  84. package/dist/core/types/FolderContentType.js.map +1 -0
  85. package/dist/core/types/FolderLog.d.ts +11 -0
  86. package/dist/core/types/FolderLog.d.ts.map +1 -0
  87. package/dist/core/types/FolderLog.js +3 -0
  88. package/dist/core/types/FolderLog.js.map +1 -0
  89. package/dist/core/types/index.d.ts +4 -0
  90. package/dist/core/types/index.d.ts.map +1 -1
  91. package/dist/core/types/index.js +4 -0
  92. package/dist/core/types/index.js.map +1 -1
  93. package/dist/mcp/bundle.js +121 -87
  94. package/dist/mcp/check-update.d.ts +2 -0
  95. package/dist/mcp/check-update.d.ts.map +1 -0
  96. package/dist/mcp/check-update.js +44 -0
  97. package/dist/mcp/check-update.js.map +1 -0
  98. package/dist/mcp/index.js +5 -2
  99. package/dist/mcp/index.js.map +1 -1
  100. package/dist/mcp/resources/documentation.d.ts +1 -1
  101. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  102. package/dist/mcp/resources/documentation.js +39 -3
  103. package/dist/mcp/resources/documentation.js.map +1 -1
  104. package/dist/mcp/tools/_char-guard.js +1 -1
  105. package/dist/mcp/tools/_char-guard.js.map +1 -1
  106. package/dist/mcp/tools/_fetch-current.d.ts +1 -1
  107. package/dist/mcp/tools/_fetch-current.d.ts.map +1 -1
  108. package/dist/mcp/tools/_fetch-current.js +12 -0
  109. package/dist/mcp/tools/_fetch-current.js.map +1 -1
  110. package/dist/mcp/tools/connect.d.ts.map +1 -1
  111. package/dist/mcp/tools/connect.js +18 -8
  112. package/dist/mcp/tools/connect.js.map +1 -1
  113. package/dist/mcp/tools/database.d.ts.map +1 -1
  114. package/dist/mcp/tools/database.js +59 -47
  115. package/dist/mcp/tools/database.js.map +1 -1
  116. package/dist/mcp/tools/database.test.js +2 -2
  117. package/dist/mcp/tools/database.test.js.map +1 -1
  118. package/dist/mcp/tools/delete.d.ts.map +1 -1
  119. package/dist/mcp/tools/delete.js +33 -3
  120. package/dist/mcp/tools/delete.js.map +1 -1
  121. package/dist/mcp/tools/execute.d.ts.map +1 -1
  122. package/dist/mcp/tools/execute.js +20 -9
  123. package/dist/mcp/tools/execute.js.map +1 -1
  124. package/dist/mcp/tools/folder.d.ts +4 -0
  125. package/dist/mcp/tools/folder.d.ts.map +1 -0
  126. package/dist/mcp/tools/folder.js +68 -0
  127. package/dist/mcp/tools/folder.js.map +1 -0
  128. package/dist/mcp/tools/get.d.ts.map +1 -1
  129. package/dist/mcp/tools/get.js +16 -6
  130. package/dist/mcp/tools/get.js.map +1 -1
  131. package/dist/mcp/tools/index.d.ts +1 -1
  132. package/dist/mcp/tools/index.d.ts.map +1 -1
  133. package/dist/mcp/tools/index.js +5 -1
  134. package/dist/mcp/tools/index.js.map +1 -1
  135. package/dist/mcp/tools/list.d.ts.map +1 -1
  136. package/dist/mcp/tools/list.js +38 -14
  137. package/dist/mcp/tools/list.js.map +1 -1
  138. package/dist/mcp/tools/logs.d.ts.map +1 -1
  139. package/dist/mcp/tools/logs.js +15 -5
  140. package/dist/mcp/tools/logs.js.map +1 -1
  141. package/dist/mcp/tools/save.d.ts.map +1 -1
  142. package/dist/mcp/tools/save.js +26 -4
  143. package/dist/mcp/tools/save.js.map +1 -1
  144. package/dist/mcp/tools/save.test.js +192 -1
  145. package/dist/mcp/tools/save.test.js.map +1 -1
  146. package/dist/mcp/tools/search-docs.d.ts +3 -0
  147. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  148. package/dist/mcp/tools/search-docs.js +102 -0
  149. package/dist/mcp/tools/search-docs.js.map +1 -0
  150. package/package.json +6 -2
  151. package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
  152. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  153. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  154. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  155. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  156. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  157. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  158. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
  159. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  160. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  161. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  162. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  163. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  164. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  165. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  166. package/skills/funifier/SKILL.md +88 -0
  167. package/skills/funifier/references/configure-security.md +96 -0
  168. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  169. package/skills/funifier/references/create-aggregate.md +144 -0
  170. package/skills/funifier/references/create-challenge.md +116 -0
  171. package/skills/funifier/references/create-competition.md +98 -0
  172. package/skills/funifier/references/create-crossword.md +574 -0
  173. package/skills/funifier/references/create-custom-object.md +91 -0
  174. package/skills/funifier/references/create-custom-page.md +135 -0
  175. package/skills/funifier/references/create-folder.md +104 -0
  176. package/skills/funifier/references/create-lastmile.md +643 -0
  177. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  178. package/skills/funifier/references/create-level.md +94 -0
  179. package/skills/funifier/references/create-lottery.md +913 -0
  180. package/skills/funifier/references/create-mystery.md +769 -0
  181. package/skills/funifier/references/create-notification.md +75 -0
  182. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  183. package/skills/funifier/references/create-quiz.md +98 -0
  184. package/skills/funifier/references/create-scheduler.md +141 -0
  185. package/skills/funifier/references/create-story.md +636 -0
  186. package/skills/funifier/references/create-swap.md +95 -0
  187. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  188. package/skills/funifier/references/create-virtual-good.md +96 -0
  189. package/skills/funifier/references/create-webhook.md +72 -0
  190. package/skills/funifier/references/create-websocket.md +71 -0
  191. package/skills/funifier/references/create-widget.md +76 -0
  192. package/skills/funifier/references/debug.md +87 -0
  193. package/skills/funifier/references/help.md +81 -0
  194. package/skills/funifier/references/implement-frontend.md +106 -0
  195. package/skills/funifier/references/import-csv.md +75 -0
  196. package/skills/funifier/references/manage-player.md +82 -0
  197. package/skills/funifier/references/manage-team.md +76 -0
  198. package/skills/funifier/references/upload-file.md +91 -0
  199. package/datasource-funifier-docs/.search-index.json +0 -17318
  200. package/datasource-funifier-docs/.skills-map.json +0 -73
  201. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  202. package/skills/funifier-create-challenge/SKILL.md +0 -88
  203. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  204. package/skills/funifier-create-level/SKILL.md +0 -87
  205. package/skills/funifier-create-quiz/SKILL.md +0 -87
  206. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  207. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  208. package/skills/funifier-debug/SKILL.md +0 -92
  209. package/skills/funifier-help/SKILL.md +0 -86
  210. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  211. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,41 +1,517 @@
1
- # Crossword (Palavras Cruzadas)
1
+ # `crossword`
2
2
 
3
- **Acesso Studio:** `/studio/crossword`
4
- **API Endpoint:** `/v3/crossword`
3
+ **Acesso Studio:** Não há módulo Studio dedicado a `crossword`. Nenhuma rota `/studio/crossword` existe no código. O objeto gerado é, tipicamente, persistido/exibido pelo **chamador** (frontend / Studio) como parte de outro recurso — o backend não oferece tela nem CRUD próprios.
4
+ **API Endpoint:** `POST /v3/ai/build/crossword` — **único** endpoint. Classe `com.funifier.rest.v3.rest.AIRest` (`@Path("v3/ai")`), método `buildCrossword` (`@Path("/build/crossword")`, `AIRest.java:1861-1994`).
5
+ **Coleção MongoDB:** **Nenhuma.** O crossword é gerado em runtime e devolvido ao chamador; **não é persistido** pelo backend. Único side effect de escrita: na primeira chamada, o prompt `intro_build_crossword` é inserido em `Entity.AI_PROMPT.collection` (coleção de prompts da IA, global ao tenant) — **não** em uma coleção `crossword`.
5
6
 
6
- ## O que é
7
+ ---
7
8
 
8
- Configuração de jogos de palavras cruzadas. Permite criar jogos customizados, com dicas, posições e temas específicos para diferentes objetivos educacionais ou promocionais.
9
+ ## 1. Visão Geral
9
10
 
10
- ## Quando usar
11
+ > **Aviso de engenharia reversa.** A documentação anterior descrevia um módulo CRUD `/v3/crossword` com tela `/studio/crossword` e coleção MongoDB `crossword`. **Nada disso existe no código.** Verificação por busca em `funifier-service/src/main/java` (`grep -rn "/crossword\|\"crossword\""`) não retorna nenhuma rota `/v3/crossword` nem mapeamento de coleção fora do pacote `engine/crossword` e de `AIRest`. O conteúdo abaixo reflete **exclusivamente** o comportamento real.
11
12
 
12
- - Para onboarding de funcionários com termos técnicos
13
- - Para campanhas de marketing temáticas
14
- - Para jogos educacionais
15
- - Para engajamento com vocabulário específico
13
+ O `crossword` na plataforma Funifier é composto por **duas peças**, sem relação CRUD entre si:
16
14
 
17
- ## Checklist de Configuração no Studio
15
+ 1. **Motor de layout em memória** — pacote `com.funifier.engine.crossword`. Recebe um conjunto de palavras e produz um tabuleiro de palavras cruzadas posicionando-as com cruzamentos (interseções de letras). É um gerador algorítmico de quebra-cabeça, sem persistência.
16
+ 2. **Endpoint de IA** — `POST /v3/ai/build/crossword` em `AIRest`. Pede ao ChatGPT que gere palavras + dicas a partir de um tema em linguagem natural, executa o motor de layout sobre essas palavras e devolve o JSON do crossword montado.
18
17
 
19
- - [ ] Definir título do jogo
20
- - [ ] Criar palavras com dicas e posições
21
- - [ ] Configurar tema visual
18
+ Papel arquitetural:
22
19
 
23
- ## API Endpoints
20
+ - É **um entre ~30 geradores de IA** expostos por `AIRest` (`/build/quiz`, `/build/level`, `/build/challenge`, `/build/lottery`, `/build/mystery`, etc.). Todos seguem o mesmo padrão: prompt introdutório + function calling do OpenAI → POJO da plataforma.
21
+ - O crossword produzido é um **artefato transiente**: o backend o devolve e esquece. Cabe ao chamador decidir se salva (por exemplo, embutindo-o no `extra` de um `challenge` ou em um `database`/`custom_object`).
24
22
 
25
- ### Listar Crosswords
26
- **Método:** GET
27
- **Endpoint:** `/v3/crossword`
23
+ Dependências em runtime:
28
24
 
29
- ### Criar Crossword
30
- **Método:** POST
31
- **Endpoint:** `/v3/crossword`
25
+ - `com.funifier.engine.system.ai` — `Prompt`, `AIManager` (`findPrompt`/`insertPrompt`), `ChatCompletionRequest`/`ChatCompletionResult`.
26
+ - API externa **OpenAI** (`https://api.openai.com/v1/chat/completions`, modelo `gpt-3.5-turbo-1106`).
27
+ - `com.funifier.engine.guid.Guid` — geração do `_id`.
32
28
 
33
- ### Deletar Crossword
34
- **Método:** DELETE
35
- **Endpoint:** `/v3/crossword/:id`
29
+ Não há, em runtime, acoplamento com `challenge`, `player`, `achievement` ou qualquer outro módulo de gamificação.
36
30
 
37
- ## Validações e Testes
31
+ ---
38
32
 
39
- - [ ] Crossword aparece na lista
40
- - [ ] Palavras e dicas estão corretas
41
- - [ ] Jogador consegue interagir com o jogo
33
+ ## 2. Arquitetura e Fluxos
34
+
35
+ ### 2.1 Classes envolvidas
36
+
37
+ | Classe | Papel |
38
+ |---|---|
39
+ | `rest.v3.rest.AIRest.buildCrossword` | Entrada REST (`POST /v3/ai/build/crossword`). Orquestra ChatGPT + motor de layout |
40
+ | `engine.crossword.Crossword` | POJO de saída (documento raiz) + factory estática `create(...)` |
41
+ | `engine.crossword.Word` | Sub-objeto de saída — uma palavra posicionada |
42
+ | `engine.crossword.CrossBoard` | Motor de layout. Tabuleiro `Cell[60][60]`, posicionamento e ajuste |
43
+ | `engine.crossword.Checks` | Validação de posições candidatas (cruzamentos, limites, adjacências) |
44
+ | `engine.crossword.Coordinate` | Posição candidata `{x, y, isVertical}` |
45
+ | `engine.crossword.AddedWord` | Palavra já posicionada `{posicaoX, posicaoY, palavra, isVertical, casas[], isPrincipal}` |
46
+ | `engine.crossword.Cell` | Célula do tabuleiro `{letra, isOcupada, isInativa, isPrincipal}` |
47
+ | `engine.crossword.WordComparator` | `Comparator<String>` — ordena por **comprimento decrescente** |
48
+ | `engine.crossword.Utility` | Utilitários **legados** (carregar palavras de arquivo, gerar DOCX). **Não usados** no fluxo de IA |
49
+ | `engine.crossword.WordList` | Agrupamento de palavras por tamanho — usado **apenas** pela geração DOCX legada |
50
+ | `engine.crossword.CrosswordTest` | Classe de teste/`main` standalone |
51
+
52
+ Não há `Resource` CRUD, `Service`, `Repository`/`Dao`, `Manager` nem `Scheduler`/`Job` para crossword. A única "porta" é o método `buildCrossword`.
53
+
54
+ ### 2.2 Pipeline principal — `buildCrossword` (`AIRest.java:1861`)
55
+
56
+ Fluxo **síncrono** (bloqueia na chamada HTTP ao OpenAI via Unirest). Sem transação, sem persistência do resultado.
57
+
58
+ ```
59
+ [1] Lê do body: content (String, prompt do usuário) e current (Object, opcional)
60
+ [2] intro = IAManager.findPrompt("intro_build_crossword")
61
+ SE intro == null:
62
+ intro = new Prompt("intro_build_crossword", ..., <instrução hardcoded>)
63
+ IAManager.insertPrompt(intro) // SIDE EFFECT: grava em AI_PROMPT (global)
64
+ [3] Define a function OpenAI "build_crossword" (param obrigatório: crossword: string)
65
+ Monta system message "formatacao" instruindo o modelo a chamar a function
66
+ [4] ChatCompletionRequest:
67
+ model = gpt-3.5-turbo-1106 (MODEL_GPT_3_5)
68
+ system = intro.content
69
+ system = "Este é o crossword atual do usuário: ..." (SOMENTE se current != null)
70
+ system = formatacao
71
+ user = content
72
+ function_call = "auto"
73
+ [5] POST https://api.openai.com/v1/chat/completions
74
+ header Authorization = CHATGPT_TOKEN (TOKEN HARDCODED — AIRest.java:79 — ver §8)
75
+ [6] result = parse(response); call = result.choices[0].message.functionCall
76
+ json = call.getArguments()
77
+ [7] p = fromJson(json) // { "crossword": ... }
78
+ SE p.crossword é String → p = parse(p.crossword) // JSON aninhado em string
79
+ SENÃO → p = parse(toJson(p.crossword))
80
+ [8] Extrai com try/catch silencioso (apenas printStackTrace em falha):
81
+ title = String(p.title)
82
+ description = String(p.description)
83
+ select = parseInt(p.select) // default local = 5 se ausente/inválido
84
+ map = fromJson(p.words) // HashMap<palavra, dica>
85
+ [9] crossword = Crossword.create(title, description, map, select)
86
+ [10] return Callback 200 com toJson(crossword)
87
+ ```
88
+
89
+ Observações:
90
+
91
+ - A extração do passo [8] usa `try { ... } catch (Exception e) { e.printStackTrace(); }` para cada campo. Falha de parse **não** aborta a requisição — o campo simplesmente fica com o valor anterior (`null` / `5`). Ver modos de falha em §9.
92
+ - O bloco inline comentado em `AIRest.java:1949-1991` é uma **duplicata** antiga da lógica de `Crossword.create` (com clamp `> 10 ? 10`). Foi substituído pela chamada a `Crossword.create` (linha 1992), que usa clamp `> 20 ? 20`. Ver §7.
93
+
94
+ #### Fluxo — `buildCrossword`
95
+
96
+ ```mermaid
97
+ flowchart TB
98
+ A[POST /v3/ai/build/crossword<br/>content, current] --> B{findPrompt<br/>intro_build_crossword}
99
+ B -- null --> C[insertPrompt em AI_PROMPT]
100
+ B -- existe --> D[Monta ChatCompletionRequest]
101
+ C --> D
102
+ D --> E[POST OpenAI chat/completions<br/>token hardcoded]
103
+ E --> F[functionCall.arguments]
104
+ F --> G[Extrai title, description,<br/>select, words map]
105
+ G --> H[Crossword.create]
106
+ H --> I[200 + JSON do crossword]
107
+ ```
108
+
109
+ #### Interação com OpenAI
110
+
111
+ ```mermaid
112
+ sequenceDiagram
113
+ participant Cli as Chamador
114
+ participant AI as AIRest.buildCrossword
115
+ participant Mgr as AIManager
116
+ participant GPT as OpenAI
117
+ participant Eng as Crossword.create / CrossBoard
118
+
119
+ Cli->>AI: POST /v3/ai/build/crossword {content, current?}
120
+ AI->>Mgr: findPrompt("intro_build_crossword")
121
+ alt prompt ausente
122
+ AI->>Mgr: insertPrompt(intro) (AI_PROMPT)
123
+ end
124
+ AI->>GPT: chat/completions (function build_crossword)
125
+ GPT-->>AI: function_call.arguments (JSON: title, description, words, select)
126
+ AI->>Eng: create(title, description, words, select)
127
+ Eng-->>AI: Crossword {_id, title, description, words[]}
128
+ AI-->>Cli: 200 + JSON
129
+ ```
130
+
131
+ ### 2.3 Pipeline de layout — `Crossword.create` (`Crossword.java:32`)
132
+
133
+ ```
134
+ [1] numTotalPalavras = (select < 1 || select > 20) ? 20 : select // clamp [1,20], default 20
135
+ [2] tabuleiro = new CrossBoard(60, numTotalPalavras, true) // grid 60x60
136
+ [3] adicionadas = tabuleiro.gerarPalavrasAdicionadas(words.keySet(), numTotalPalavras)
137
+ [4] System.out de cada palavra adicionada + total // stdout
138
+ [5] tabuleiro.ajustarTabuleiro() // recorta bordas vazias
139
+ [6] tabuleiro.imprimirTabuleiro() // stdout
140
+ [7] PARA i = 0..adicionadas.size()-1:
141
+ Word(
142
+ number = i + 1,
143
+ direction = isVertical ? "vertical" : "horizontal",
144
+ row = posicaoX + 1, // 1-indexed
145
+ column = posicaoY + 1, // 1-indexed
146
+ hint = words.get(palavra), // dica pela chave exata
147
+ word = palavra
148
+ )
149
+ [8] Crossword.builder()._id(Guid.newShortGuid()) // ObjectId hex, 24 chars
150
+ .title(title).description(description).words(words).build()
151
+ ```
152
+
153
+ ### 2.4 Posicionamento das palavras — `CrossBoard`
154
+
155
+ `gerarPalavrasAdicionadas` (`CrossBoard.java:26`):
156
+
157
+ ```
158
+ [1] gerarTabuleiroVazio() // 60x60 Cells vazias
159
+ [2] limpa faltam / adicionadas / puladas; palavrasJaTrocadas = false
160
+ [3] faltam = cópia das palavras; Collections.sort(faltam, WordComparator) // maior → menor
161
+ [4] posicionarPalavraInicial()
162
+ [5] posicionarPalavras()
163
+ [6] return adicionadas
164
+ ```
165
+
166
+ `posicionarPalavraInicial` (`CrossBoard.java:112`):
167
+ - `escolherUmaPalavra()` seleciona a primeira palavra.
168
+ - SE comprimento > `tamanho` (60) → **retorna sem posicionar** (caso de borda).
169
+ - Posiciona próximo ao **centro** com `Coordinate(x, y, isVertical=true)` — a palavra inicial é **vertical** (apesar do comentário "para ser horizontal...").
170
+ - Marca `adicionadas.get(0).isPrincipal = true` (palavra "principal"; sem restrição de cruzamento).
171
+
172
+ `escolherUmaPalavra` (`CrossBoard.java:128`):
173
+ - SE `faltam.size() > 4` → índice aleatório em `[0, faltam.size()/2)` (favorece a metade mais longa).
174
+ - SENÃO → `faltam.get(0)` (a mais longa restante).
175
+
176
+ `posicionarPalavras` (`CrossBoard.java:179` — versão **ativa**):
177
+
178
+ ```
179
+ isVertical = true
180
+ maxTentativas = faltam.size() * 2
181
+ ENQUANTO faltam não vazio E adicionadas.size() < numPalavras E tentativas < maxTentativas:
182
+ palavra = escolherUmaPalavra()
183
+ isVertical = !isVertical // ALTERNA direção a cada iteração
184
+ adicionou = tentarAdicionarPalavra(palavra, isVertical)
185
+ SE não adicionou:
186
+ atualizarListasFaltamParaPuladas(palavra) // move de faltam → puladas
187
+ tentativas++
188
+ SENÃO:
189
+ tentativas = 0
190
+ SE puladas não vazio:
191
+ tentarPosicionarPuladas(isVertical) // tenta reencaixar puladas
192
+ ```
193
+
194
+ `verifDisponibilidade` (`Checks.java:13`) decide onde uma palavra pode entrar:
195
+
196
+ ```
197
+ posicoes = contarPossiveisPosicoes(palavra, isVertical)
198
+ // para CADA palavra já adicionada, verifPalavra() acha cruzamentos
199
+ // SOMENTE perpendiculares (palavraAdicionada.isVertical != isVertical)
200
+ // onde uma letra coincide
201
+ SE posicoes vazio → null
202
+ posicoes = removerPosicoesInadequadas(posicoes, palavra)
203
+ // mantém só as que passam em:
204
+ // verifLimites() → cabe dentro do tabuleiro
205
+ // verifCasasNoTabuleiro() → sem adjacência indevida; letras coincidem no cruzamento
206
+ SE posicoes vazio → null
207
+ SENÃO → posicoes[ Random.nextInt(posicoes.size()) ] // escolha ALEATÓRIA
208
+ ```
209
+
210
+ Consequência: a partir da segunda palavra, **só entram palavras que cruzam** uma palavra já posicionada compartilhando uma letra. Palavras sem cruzamento possível vão para `puladas` e podem nunca entrar.
211
+
212
+ #### Loop de posicionamento
213
+
214
+ ```mermaid
215
+ flowchart TB
216
+ S[faltam ordenado por tamanho] --> Ini[posicionarPalavraInicial<br/>vertical, centro, isPrincipal]
217
+ Ini --> L{"faltam vazio? OR<br/>adicionadas >= numPalavras? OR<br/>tentativas >= max?"}
218
+ L -- continuar --> P[escolherUmaPalavra]
219
+ P --> T[isVertical = !isVertical]
220
+ T --> V[verifDisponibilidade<br/>cruzamentos perpendiculares]
221
+ V -- posição válida --> Add[adicionarPalavraTabuleiro<br/>tentativas = 0]
222
+ V -- nenhuma --> Sk[move para puladas<br/>tentativas++]
223
+ Add --> R[tentarPosicionarPuladas]
224
+ Sk --> R
225
+ R --> L
226
+ L -- parar --> Fim[adicionadas]
227
+ ```
228
+
229
+ `ajustarTabuleiro` (`CrossBoard.java:49`): calcula `minX/minY/maxX/maxY` das palavras adicionadas, desloca todas para a origem (`posicaoX -= minX`, `posicaoY -= minY`) e recalcula `tamanho`. É depois desse passo que `row = posicaoX + 1` e `column = posicaoY + 1` ficam relativos ao canto do tabuleiro recortado.
230
+
231
+ ---
232
+
233
+ ## 3. Estrutura dos Objetos
234
+
235
+ ### 3.1 `Crossword` — documento raiz (saída)
236
+
237
+ `com.funifier.engine.crossword.Crossword`. **Não persistido pelo backend** — é o corpo da resposta.
238
+
239
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
240
+ |---|---|---|---|---|
241
+ | `_id` | String | `Guid.newShortGuid()` | Não (auto) | Gerado por `new org.bson.types.ObjectId().toString()` → **hex de 24 caracteres**, apesar do tipo `String`. Mapeado por `@JsonProperty("_id")` |
242
+ | `title` | String | `"null"` (string) se ausente | Não | Título vindo do JSON gerado pela IA. Se a IA omitir, `String.valueOf(p.get("title"))` produz a **string literal** `"null"` (não o valor nulo) — ver §9 |
243
+ | `description` | String | `"null"` (string) se ausente | Não | Descrição vinda do JSON da IA. Mesmo efeito `"null"` literal se ausente |
244
+ | `words` | List<`Word`> | `[]` | Sim (significativo) | Palavras efetivamente posicionadas no tabuleiro. **Tamanho ≤ `select`** (pode ser menor) |
245
+
246
+ Anotações: `@Data @Builder(toBuilder=true) @NoArgsConstructor @AllArgsConstructor @JsonIgnoreProperties(ignoreUnknown=true)`. `words` é `@Singular`.
247
+
248
+ **Campos computados / não persistidos:** todo o objeto é transiente — nada vai ao MongoDB por este fluxo.
249
+
250
+ ### 3.2 `Word` — sub-entidade (saída)
251
+
252
+ `com.funifier.engine.crossword.Word`. Um item de `Crossword.words`.
253
+
254
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
255
+ |---|---|---|---|---|
256
+ | `number` | int | — | Sim | Sequencial `1..N` na ordem de adição ao tabuleiro (não é ordem de leitura/grade) |
257
+ | `direction` | String | — | Sim | `"vertical"` ou `"horizontal"`. Derivado de `AddedWord.isVertical` |
258
+ | `row` | int | — | Sim | `posicaoX + 1` — **1-indexed**. Eixo das linhas (cresce para baixo) |
259
+ | `column` | int | — | Sim | `posicaoY + 1` — **1-indexed**. Eixo das colunas |
260
+ | `hint` | String | — | Sim | Dica da palavra (`words.get(palavra)`, busca pela chave exata) |
261
+ | `word` | String | — | Sim | A própria palavra |
262
+
263
+ **Semântica de coordenadas:** `x` é a linha, `y` é a coluna. `isVertical == true` → a palavra se estende ao longo de `x` (linhas, de cima para baixo). `row`/`column` são **1-indexados** na resposta (o motor é 0-indexed internamente).
264
+
265
+ ### 3.3 Estruturas internas (não saem na resposta)
266
+
267
+ | Classe | Campos | Observação |
268
+ |---|---|---|
269
+ | `Cell` | `letra` (char), `isOcupada` (bool), `isInativa` (bool), `isPrincipal` (bool) | `isInativa` é **lido** em `Checks.verifPalavra` mas **nunca escrito** em lugar nenhum → sempre `false` → ramo morto (ver §7) |
270
+ | `AddedWord` | `posicaoX`, `posicaoY`, `palavra`, `isVertical`, `casas[]`, `isPrincipal` | `isPrincipal` marca a primeira palavra; **não** é exportado para `Word` |
271
+ | `Coordinate` | `x`, `y`, `isVertical` | Posição candidata produzida por `Checks` |
272
+ | `CrossBoard` | `tamanho`, `numPalavras`, `faltam`, `adicionadas`, `puladas`, `tabuleiro[][]`, ... | Estado do motor; descartado ao fim de `create` |
273
+
274
+ ### 3.4 Estrutura do JSON gerado pela IA (entrada do motor)
275
+
276
+ O ChatGPT é instruído (prompt `intro_build_crossword`) a produzir um JSON com:
277
+
278
+ | Campo | Tipo | Descrição (conforme o prompt) |
279
+ |---|---|---|
280
+ | `title` | String | Título do jogo |
281
+ | `description` | String | Descrição |
282
+ | `words` | Map<palavra, dica> | Pares palavra→dica. O prompt manda gerar **o dobro** das palavras pedidas, limitado a **40** |
283
+ | `select` | int | Quantas palavras o usuário quer ver no jogo, limitado a **20** no prompt |
284
+
285
+ Notas do prompt: as palavras devem ser **MAIÚSCULAS** por padrão (outro formato só se o usuário pedir). O pool maior (`words`) que `select` dá folga ao algoritmo para achar boas interseções.
286
+
287
+ ---
288
+
289
+ ## 4. Endpoints
290
+
291
+ ### 4.1 `POST /v3/ai/build/crossword`
292
+
293
+ | Aspecto | Detalhe |
294
+ |---|---|
295
+ | Finalidade | Gerar um crossword (palavras + dicas + layout) a partir de um tema em linguagem natural, via IA |
296
+ | Autenticação | Bearer token (`@BeanParam AuthBean`) |
297
+ | Consumes | `application/json` |
298
+ | Produces | `application/json; charset=UTF-8` |
299
+ | Full replace ou patch | N/A — não é CRUD. Gera e devolve; **não persiste** |
300
+ | Idempotência | **Não** — saída varia entre chamadas idênticas (ver §5.2) |
301
+
302
+ **Body params:**
303
+
304
+ | Param | Tipo | Obrigatório | Descrição |
305
+ |---|---|---|---|
306
+ | `content` | String | Sim (de fato) | Pedido em linguagem natural (tema, quantidade, idioma…). Vira a mensagem `user` enviada ao modelo |
307
+ | `current` | Object | Não | Crossword atual do usuário. Se presente, é injetado como contexto (`system`) para o modelo **refinar/editar** em vez de criar do zero |
308
+
309
+ **Comportamento real:**
310
+
311
+ - O backend **não valida** `content`; se ausente, `content = null` e o modelo recebe uma mensagem `user` nula (comportamento indefinido do parse OpenAI).
312
+ - O resultado **não é salvo**. O chamador é responsável por persistir.
313
+ - `select` sofre dois níveis de tratamento: default local `5` em `AIRest` (se a IA omitir/enviar inválido) e clamp `[1,20]` (else `20`) em `Crossword.create`. Ver §5.1.
314
+ - Logs vão para **stdout** (`System.out.println`), não para logger estruturado.
315
+
316
+ **Exemplo de request:**
317
+
318
+ ```json
319
+ POST /v3/ai/build/crossword
320
+ Authorization: Bearer eyJhbGciOi...
321
+ Content-Type: application/json
322
+
323
+ {
324
+ "content": "Crie um crossword sobre frutas típicas do Brasil com 5 palavras"
325
+ }
326
+ ```
327
+
328
+ **Exemplo de response (200):**
329
+
330
+ ```json
331
+ {
332
+ "_id": "665f0a91c8b0000000abcdef",
333
+ "title": "Desafio de Frutas Brasileiras",
334
+ "description": "Encontre as frutas típicas do Brasil.",
335
+ "words": [
336
+ { "number": 1, "direction": "vertical", "row": 12, "column": 8, "hint": "Fruta roxa amazônica usada em sucos", "word": "AÇAÍ" },
337
+ { "number": 2, "direction": "horizontal", "row": 13, "column": 6, "hint": "Fruta pequena e roxa, nativa do Brasil", "word": "JABUTICABA" },
338
+ { "number": 3, "direction": "vertical", "row": 10, "column": 11, "hint": "Fruta em forma de estrela quando cortada", "word": "CARAMBOLA" }
339
+ ]
340
+ }
341
+ ```
342
+
343
+ > O número, a posição e a quantidade de palavras variam a cada chamada — o exemplo é ilustrativo.
344
+
345
+ ---
346
+
347
+ ## 5. Regras de Negócio
348
+
349
+ Regras presentes no **código** e ausentes de qualquer schema:
350
+
351
+ ### 5.1 `select` — dois níveis de tratamento
352
+
353
+ - Em `AIRest.buildCrossword`: `int select = 5;` e só é sobrescrito se `parseInt(p.select)` tiver sucesso. Logo, **omissão da IA → `select = 5`**.
354
+ - Em `Crossword.create`: `numTotalPalavras = (select < 1 || select > 20) ? 20 : select`. Ou seja, valores fora de `[1,20]` viram **20** (não 5). Exemplos: `0 → 20`, `30 → 20`, `7 → 7`.
355
+
356
+ ### 5.2 Saída não-determinística
357
+
358
+ `escolherUmaPalavra` e `verifDisponibilidade` usam `java.util.Random` **sem seed**. A mesma entrada (mesmo `content` e até o mesmo conjunto de palavras) gera **tabuleiros diferentes** em chamadas distintas. Não há como reproduzir um layout.
359
+
360
+ ### 5.3 Pode posicionar **menos** palavras que `select`
361
+
362
+ O laço para quando `faltam` esvazia, quando `adicionadas.size() == numPalavras` **ou** quando `tentativas >= faltam.size() * 2`. Palavras sem cruzamento válido vão para `puladas` e podem nunca entrar. Portanto `words.length ≤ select` — e frequentemente **menor**, sobretudo com poucas palavras no pool ou letras pouco compartilhadas.
363
+
364
+ ### 5.4 Primeira palavra é "principal" e sem restrição
365
+
366
+ A palavra inicial é posicionada **vertical**, próxima ao centro, e marcada `isPrincipal = true`. É a única que entra sem precisar cruzar outra. Se ela for maior que 60 caracteres, **nada** é posicionado (a inicial é abortada e nenhuma outra tem âncora).
367
+
368
+ ### 5.5 Direção alternada
369
+
370
+ `isVertical` é invertido a cada iteração do laço (`isVertical = !isVertical`), tendendo a alternar horizontal/vertical entre tentativas consecutivas.
371
+
372
+ ### 5.6 Cruzamento obrigatório e perpendicular
373
+
374
+ `Checks.verifPalavra` só considera interseções com palavras de **direção oposta** (`palavraAdicionada.isVertical != isVertical`) que **compartilham uma letra**. `verifCasasNoTabuleiro` rejeita adjacências indevidas (palavras paralelas coladas) e exige letras coincidentes nos cruzamentos.
375
+
376
+ ### 5.7 Multi-tenant
377
+
378
+ - Autenticação por `AuthBean` (Bearer). O crossword gerado **não é escopado** a nada (não persiste).
379
+ - O prompt `intro_build_crossword` é inserido **uma vez** na coleção `AI_PROMPT` do tenant. É **global** — não há variação por usuário. Editá-lo no banco altera o comportamento de **todas** as gerações daquele tenant.
380
+
381
+ ### 5.8 Falhas de parse são silenciosas
382
+
383
+ Cada extração de campo em `buildCrossword` é envolvida por `try/catch` que apenas faz `printStackTrace()`. Uma resposta malformada do modelo não retorna erro 4xx — degrada para valores default/`null`, podendo causar `NullPointerException` mais adiante (ver §9).
384
+
385
+ ---
386
+
387
+ ## 6. Comportamentos Automáticos
388
+
389
+ | Comportamento | Trigger | Impacto | Persistência |
390
+ |---|---|---|---|
391
+ | Inserção do prompt `intro_build_crossword` | Primeira chamada ao endpoint (quando `findPrompt` retorna `null`) | Cria o prompt introdutório hardcoded | **Sim** — coleção `AI_PROMPT` (global ao tenant) |
392
+ | Geração de `_id` | `Crossword.builder()` em `create` | `ObjectId` hex de 24 chars | Não (só no objeto devolvido) |
393
+ | Clamp de `select` | `Crossword.create` | Limita a `[1,20]`; fora disso → 20 | Não |
394
+ | Recorte do tabuleiro | `ajustarTabuleiro()` | Remove bordas vazias, normaliza `row`/`column` à origem | Não |
395
+ | Marcação `isPrincipal` | `posicionarPalavraInicial` | Primeira palavra marcada como principal (não exportado) | Não |
396
+ | Logs em stdout | `Crossword.create` e `buildCrossword` | Imprime palavras, tabuleiro, modelo e uso de tokens no stdout | Não (apenas log de processo) |
397
+
398
+ ---
399
+
400
+ ## 7. Suportado vs NÃO Suportado
401
+
402
+ ### ✅ Suportado
403
+
404
+ - Geração de crossword por **tema em linguagem natural** (`content`).
405
+ - **Refinamento** de um crossword existente (`current` injetado como contexto da IA).
406
+ - Layout automático com cruzamentos perpendiculares e validação de adjacência.
407
+ - Clamp de `select` para `[1,20]`.
408
+ - Saída JSON pronta para o frontend: `_id`, `title`, `description`, `words[]` com `number`, `direction`, `row`, `column`, `hint`, `word`.
409
+
410
+ ### ❌ NÃO Suportado
411
+
412
+ - **CRUD `/v3/crossword`** (GET / POST / PUT / DELETE) — **não existe**. Verificado por grep em `funifier-service`. O único endpoint é `POST /v3/ai/build/crossword`.
413
+ - **Tela `/studio/crossword`** — não existe módulo Studio dedicado.
414
+ - **Coleção MongoDB `crossword`** — o objeto **não é persistido**. Não há listagem, edição nem remoção pelo backend.
415
+ - **Determinismo / reprodutibilidade** — saída varia entre chamadas (uso de `Random` sem seed).
416
+ - **Garantia de posicionar todas as `select` palavras** — pode posicionar menos.
417
+ - **`PUT`/atualização** de um crossword — inexistente (não há recurso para atualizar).
418
+ - `gerarTabuleiro()` + `Utility.carregarPalavras` (carga de `src/com/diego/cruzadas/resources/palavras.txt`) — código **legado** que lê palavras de arquivo local; **não** é alcançado pelo fluxo de IA (que usa `gerarPalavrasAdicionadas`).
419
+ - `posicionarPalavras___OLD()` — algoritmo antigo de posicionamento; **nunca chamado**.
420
+ - Geração de DOCX (`Utility.gerarTabelaComum`, `salvarArquivoNormal`, `salvarArquivoRespondido`) — **comentada** no fonte.
421
+ - `Cell.isInativa` — campo **declarado e lido** (`Checks.verifPalavra`) mas **nunca atribuído** → sempre `false` → o ramo que o consulta é morto.
422
+ - Bloco inline comentado em `AIRest.java:1949-1991` — duplica `Crossword.create` com clamp antigo `> 10 ? 10` (limite depois elevado para 20 em `Crossword.create`).
423
+ - `WordList`, `Utility.mostrarPalavras`, `Utility.separarPalavrasTamanho`, `Utility.setTableAlign` — auxiliares **somente** da geração DOCX legada.
424
+
425
+ ---
426
+
427
+ ## 8. Segurança e Permissões
428
+
429
+ - **Autenticação:** Bearer token via `@BeanParam AuthBean`. Sem autorização granular específica para crossword.
430
+ - **🔴 Token OpenAI hardcoded no fonte — `AIRest.java:79`:**
431
+ ```java
432
+ public static String CHATGPT_TOKEN = "Bearer sk-...<chave exposta>...";
433
+ ```
434
+ Segredo de API exposto em código versionado. Qualquer pessoa com acesso ao repositório pode consumir a chave (custo financeiro direto + risco de abuso). **Recomendação:** revogar a chave, movê-la para variável de ambiente / secret manager e remover do histórico do Git.
435
+ - **Prompt injection:** `content` e `current` são repassados **diretamente** ao modelo como mensagens. Um usuário pode tentar manipular/escapar o prompt (jailbreak). O impacto é limitado — a saída é apenas um JSON de crossword montado por `Crossword.create` — mas conteúdo arbitrário pode aparecer em `title`/`description`/`hint`.
436
+ - **Isolamento de tenant:** o prompt `intro_build_crossword` é **global** ao tenant (coleção `AI_PROMPT`). O crossword gerado não é escopado a nada por não ser persistido.
437
+ - **Sem rate limiting** visível no método → cada chamada é uma requisição paga ao OpenAI; não há proteção contra abuso/loop de requisições.
438
+
439
+ ---
440
+
441
+ ## 9. Observabilidade e Troubleshooting
442
+
443
+ **Diagnóstico:**
444
+
445
+ - Não há logger estruturado. O método imprime no **stdout** do servidor:
446
+ - `ChatGPT Build : Crossword`, `ChatGPT Model : gpt-3.5-turbo-1106`, `ChatGPT Usage : {...}` (tokens), o JSON cru retornado, cada palavra adicionada e o tabuleiro ASCII (`imprimirTabuleiro`).
447
+ - Para verificar se o módulo funciona: chamar o endpoint com um `content` simples e inspecionar a resposta (status 200 + `words[]` não vazio).
448
+
449
+ **Erros comuns e causas:**
450
+
451
+ | Sintoma | Causa provável |
452
+ |---|---|
453
+ | `500` / `NullPointerException` | `words` não parseado (`map == null`) → `Crossword.create` faz `wordsRepository.keySet()` sobre `null`. Ocorre quando a IA não retorna o campo `words` ou retorna formato inesperado |
454
+ | `500` no parse do retorno | OpenAI não retornou `function_call` (ex.: token inválido/expirado, indisponibilidade) → `message.getFunctionCall()` é `null` |
455
+ | `title`/`description` valendo a string `"null"` | A IA omitiu o campo; `String.valueOf(p.get("title"))` produz `"null"` literal |
456
+ | `words` com menos itens que o pedido | Normal — palavras sem cruzamento válido foram puladas, ou `tentativas >= faltam.size()*2` (ver §5.3) |
457
+ | Resposta sem nenhuma palavra | Pool com 1 palavra e ela maior que 60 chars, ou nenhuma interseção encontrada após a inicial |
458
+
459
+ **Comandos úteis:**
460
+
461
+ ```
462
+ POST /v3/ai/build/crossword { "content": "crossword sobre capitais do Brasil, 8 palavras" }
463
+ POST /v3/ai/build/crossword { "content": "...", "current": { ...crossword existente... } }
464
+ ```
465
+
466
+ Inspeção do prompt global (via API de IA/prompts do tenant), caso o comportamento da geração precise ser ajustado: procurar o documento `_id = "intro_build_crossword"` na coleção `AI_PROMPT`.
467
+
468
+ ---
469
+
470
+ ## 10. Exemplos Práticos
471
+
472
+ ### Exemplo mínimo
473
+
474
+ ```json
475
+ POST /v3/ai/build/crossword
476
+ { "content": "Crie um crossword sobre animais da savana com 5 palavras" }
477
+ ```
478
+
479
+ Devolve um `Crossword` com `_id`, `title`, `description` e até 5 palavras posicionadas.
480
+
481
+ ### Exemplo avançado — refinar um crossword existente
482
+
483
+ ```json
484
+ POST /v3/ai/build/crossword
485
+ {
486
+ "content": "Troque as palavras muito difíceis por outras mais fáceis e mantenha o tema",
487
+ "current": {
488
+ "title": "Desafio de Frutas Brasileiras",
489
+ "description": "Encontre as frutas típicas do Brasil.",
490
+ "words": [
491
+ { "number": 1, "direction": "vertical", "row": 12, "column": 8, "hint": "Fruta roxa amazônica", "word": "AÇAÍ" }
492
+ ]
493
+ }
494
+ }
495
+ ```
496
+
497
+ O `current` é injetado como contexto `system`; o modelo gera um novo conjunto de palavras/dicas e o motor remonta o tabuleiro.
498
+
499
+ ### Anti-pattern (o que NÃO fazer)
500
+
501
+ - ❌ Chamar `GET /v3/crossword` ou `DELETE /v3/crossword/:id` — **não existem**. Não há CRUD.
502
+ - ❌ Esperar que o backend **salve** o crossword. Ele apenas devolve; persistência é responsabilidade do chamador.
503
+ - ❌ Depender da **mesma saída** para a mesma entrada — o layout é aleatório.
504
+ - ❌ Pedir `select` muito alto achando que vem mais palavras — acima de 20 vira 20, e o tabuleiro ainda pode posicionar menos.
505
+ - ❌ Confiar no token OpenAI hardcoded em produção — deve ser externalizado (ver §8).
506
+
507
+ ---
508
+
509
+ ## Checklist de Configuração
510
+
511
+ - [ ] Token OpenAI **válido** disponível (hoje hardcoded em `AIRest.java:79` — externalizar e revogar a chave exposta).
512
+ - [ ] `content` descreve **tema + quantidade** (e idioma/formato, se necessário) de forma clara.
513
+ - [ ] O chamador **persiste** o crossword retornado (o backend não persiste).
514
+ - [ ] Não depender de **determinismo** — tratar cada resposta como nova.
515
+ - [ ] `select` entre **1 e 20** (fora disso o backend força 20).
516
+ - [ ] Tratar o caso de **`words` vir vazio ou menor** que o solicitado.
517
+ - [ ] Armadilha silenciosa: se a IA **omitir `words`**, a geração pode lançar `NullPointerException` (500) — validar o retorno.