funifier-mcp 0.2.26 → 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 (170) 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 +4 -1
  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 +935 -280
  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/mcp/bundle.js +119 -93
  66. package/dist/mcp/index.js +2 -2
  67. package/dist/mcp/index.js.map +1 -1
  68. package/dist/mcp/resources/documentation.d.ts +1 -1
  69. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  70. package/dist/mcp/resources/documentation.js +39 -3
  71. package/dist/mcp/resources/documentation.js.map +1 -1
  72. package/dist/mcp/tools/connect.d.ts.map +1 -1
  73. package/dist/mcp/tools/connect.js +18 -8
  74. package/dist/mcp/tools/connect.js.map +1 -1
  75. package/dist/mcp/tools/database.d.ts.map +1 -1
  76. package/dist/mcp/tools/database.js +59 -47
  77. package/dist/mcp/tools/database.js.map +1 -1
  78. package/dist/mcp/tools/database.test.js +2 -2
  79. package/dist/mcp/tools/database.test.js.map +1 -1
  80. package/dist/mcp/tools/delete.d.ts.map +1 -1
  81. package/dist/mcp/tools/delete.js +13 -3
  82. package/dist/mcp/tools/delete.js.map +1 -1
  83. package/dist/mcp/tools/execute.d.ts.map +1 -1
  84. package/dist/mcp/tools/execute.js +20 -9
  85. package/dist/mcp/tools/execute.js.map +1 -1
  86. package/dist/mcp/tools/folder.d.ts.map +1 -1
  87. package/dist/mcp/tools/folder.js +22 -12
  88. package/dist/mcp/tools/folder.js.map +1 -1
  89. package/dist/mcp/tools/get.d.ts.map +1 -1
  90. package/dist/mcp/tools/get.js +16 -6
  91. package/dist/mcp/tools/get.js.map +1 -1
  92. package/dist/mcp/tools/index.d.ts +1 -1
  93. package/dist/mcp/tools/index.d.ts.map +1 -1
  94. package/dist/mcp/tools/index.js +3 -1
  95. package/dist/mcp/tools/index.js.map +1 -1
  96. package/dist/mcp/tools/list.d.ts.map +1 -1
  97. package/dist/mcp/tools/list.js +38 -14
  98. package/dist/mcp/tools/list.js.map +1 -1
  99. package/dist/mcp/tools/logs.d.ts.map +1 -1
  100. package/dist/mcp/tools/logs.js +15 -5
  101. package/dist/mcp/tools/logs.js.map +1 -1
  102. package/dist/mcp/tools/save.d.ts.map +1 -1
  103. package/dist/mcp/tools/save.js +14 -4
  104. package/dist/mcp/tools/save.js.map +1 -1
  105. package/dist/mcp/tools/save.test.js +3 -3
  106. package/dist/mcp/tools/save.test.js.map +1 -1
  107. package/dist/mcp/tools/search-docs.d.ts +3 -0
  108. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  109. package/dist/mcp/tools/search-docs.js +102 -0
  110. package/dist/mcp/tools/search-docs.js.map +1 -0
  111. package/package.json +6 -2
  112. package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
  113. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  114. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  115. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  116. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  117. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  118. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  119. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
  120. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  121. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  122. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  123. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  124. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  125. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  126. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  127. package/skills/funifier/SKILL.md +88 -0
  128. package/skills/funifier/references/configure-security.md +96 -0
  129. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  130. package/skills/funifier/references/create-aggregate.md +144 -0
  131. package/skills/funifier/references/create-challenge.md +116 -0
  132. package/skills/funifier/references/create-competition.md +98 -0
  133. package/skills/funifier/references/create-crossword.md +574 -0
  134. package/skills/funifier/references/create-custom-object.md +91 -0
  135. package/skills/funifier/references/create-custom-page.md +135 -0
  136. package/skills/funifier/references/create-folder.md +104 -0
  137. package/skills/funifier/references/create-lastmile.md +643 -0
  138. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  139. package/skills/funifier/references/create-level.md +94 -0
  140. package/skills/funifier/references/create-lottery.md +913 -0
  141. package/skills/funifier/references/create-mystery.md +769 -0
  142. package/skills/funifier/references/create-notification.md +75 -0
  143. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  144. package/skills/funifier/references/create-quiz.md +98 -0
  145. package/skills/funifier/references/create-scheduler.md +141 -0
  146. package/skills/funifier/references/create-story.md +636 -0
  147. package/skills/funifier/references/create-swap.md +95 -0
  148. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  149. package/skills/funifier/references/create-virtual-good.md +96 -0
  150. package/skills/funifier/references/create-webhook.md +72 -0
  151. package/skills/funifier/references/create-websocket.md +71 -0
  152. package/skills/funifier/references/create-widget.md +76 -0
  153. package/skills/funifier/references/debug.md +87 -0
  154. package/skills/funifier/references/help.md +81 -0
  155. package/skills/funifier/references/implement-frontend.md +106 -0
  156. package/skills/funifier/references/import-csv.md +75 -0
  157. package/skills/funifier/references/manage-player.md +82 -0
  158. package/skills/funifier/references/manage-team.md +76 -0
  159. package/skills/funifier/references/upload-file.md +91 -0
  160. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  161. package/skills/funifier-create-challenge/SKILL.md +0 -88
  162. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  163. package/skills/funifier-create-level/SKILL.md +0 -87
  164. package/skills/funifier-create-quiz/SKILL.md +0 -87
  165. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  166. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  167. package/skills/funifier-debug/SKILL.md +0 -92
  168. package/skills/funifier-help/SKILL.md +0 -86
  169. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  170. package/skills/funifier-index/SKILL.md +0 -58
@@ -0,0 +1,98 @@
1
+ # funifier-create-competition
2
+
3
+ Create a Funifier competition — timed ranking contests with player enrollment, position-based rewards, and execution; use when you need player sign-up, prizes by finish position, or a scheduled end date, not when an always-on leaderboard is enough
4
+
5
+ ---
6
+
7
+ ## Before starting — find relevant docs
8
+
9
+ Run lexical search to load only what you need:
10
+
11
+ ```bash
12
+ npx funifier-mcp search "competition contest ranking tournament enrollment rewards position gt26" --skill funifier-create-competition
13
+ ```
14
+
15
+ Read only files returned with score > 0.5 (this threshold is printed in the search output).
16
+
17
+ ## Primary docs for this skill
18
+
19
+ If search returns insufficient results, read directly:
20
+
21
+ - `datasource-funifier-docs/knowledge/modules/competition.md`
22
+
23
+ ## Steps
24
+
25
+ ### Rules — follow exactly, no exceptions
26
+
27
+ **Technique:** always `["GT26"]`.
28
+ **`operation.type`:**
29
+ - `1` = Count actions
30
+ - `3` = Sum achievements (points) — most common
31
+
32
+ **`period.expression`:** Funifier date syntax, e.g. `"-0M-;+1M+"` = current month.
33
+ **Rewards by position:** use `extra.position_start` and `extra.position_ends` on each reward.
34
+ **Execution:** automatic (`autoExecute: true`) or manual (`GET /v3/competition/:id/execute`).
35
+
36
+ ---
37
+
38
+ ### 1. Check dependencies
39
+
40
+ ```
41
+ funifier_list type=point search=<metric>
42
+ funifier_list type=action search=<metric>
43
+ ```
44
+
45
+ ### 2. Check if competition already exists
46
+
47
+ ```
48
+ funifier_list type=competition search=<title>
49
+ ```
50
+
51
+ ### 3. Build the payload
52
+
53
+ ```json
54
+ {
55
+ "title": "Sales Race",
56
+ "description": "Top sellers of the month",
57
+ "period": { "expression": "-0M-;+1M+" },
58
+ "maxWinners": 3,
59
+ "maxPlayers": 100,
60
+ "minScore": 0,
61
+ "operation": {
62
+ "type": 3,
63
+ "achievement_type": 0,
64
+ "item": "xp",
65
+ "sort": -1,
66
+ "sub": false,
67
+ "filters": []
68
+ },
69
+ "rewards": [
70
+ { "total": 200, "type": 0, "item": "xp", "operation": 0, "extra": { "position_start": 1, "position_ends": 1 }, "restrict": false, "perPlayer": false },
71
+ { "total": 100, "type": 0, "item": "xp", "operation": 0, "extra": { "position_start": 2, "position_ends": 3 }, "restrict": false, "perPlayer": false }
72
+ ],
73
+ "notifications": [],
74
+ "active": true,
75
+ "teamCompetition": false,
76
+ "autoExecute": true,
77
+ "techniques": ["GT26"]
78
+ }
79
+ ```
80
+
81
+ ### 4. Save
82
+
83
+ ```
84
+ funifier_save type=competition payload=<json>
85
+ ```
86
+
87
+ ### 5. Enroll players
88
+
89
+ ```
90
+ POST /v3/competition/join { "competition": "<_id>", "player": "<player_id>" }
91
+ ```
92
+
93
+ ### 6. Check leaderboard and execute
94
+
95
+ ```
96
+ POST /v3/competition/leader/aggregate?id=<_id>
97
+ GET /v3/competition/<_id>/execute (manual execution only)
98
+ ```
@@ -0,0 +1,574 @@
1
+ # funifier-create-crossword
2
+
3
+ Create a Funifier crossword puzzle game — themed word games with custom clues and grid positions for educational onboarding or marketing; use when building interactive word-based games, not when a multiple-choice quiz is enough
4
+
5
+ ---
6
+
7
+ ## Documentação do módulo (já incluída — não precisa buscar)
8
+
9
+ **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.
10
+ **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`).
11
+ **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`.
12
+
13
+ ---
14
+
15
+ ## 1. Visão Geral
16
+
17
+ > **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.
18
+
19
+ O `crossword` na plataforma Funifier é composto por **duas peças**, sem relação CRUD entre si:
20
+
21
+ 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.
22
+ 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.
23
+
24
+ Papel arquitetural:
25
+
26
+ - É **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.
27
+ - 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`).
28
+
29
+ Dependências em runtime:
30
+
31
+ - `com.funifier.engine.system.ai` — `Prompt`, `AIManager` (`findPrompt`/`insertPrompt`), `ChatCompletionRequest`/`ChatCompletionResult`.
32
+ - API externa **OpenAI** (`https://api.openai.com/v1/chat/completions`, modelo `gpt-3.5-turbo-1106`).
33
+ - `com.funifier.engine.guid.Guid` — geração do `_id`.
34
+
35
+ Não há, em runtime, acoplamento com `challenge`, `player`, `achievement` ou qualquer outro módulo de gamificação.
36
+
37
+ ---
38
+
39
+ ## 2. Arquitetura e Fluxos
40
+
41
+ ### 2.1 Classes envolvidas
42
+
43
+ | Classe | Papel |
44
+ |---|---|
45
+ | `rest.v3.rest.AIRest.buildCrossword` | Entrada REST (`POST /v3/ai/build/crossword`). Orquestra ChatGPT + motor de layout |
46
+ | `engine.crossword.Crossword` | POJO de saída (documento raiz) + factory estática `create(...)` |
47
+ | `engine.crossword.Word` | Sub-objeto de saída — uma palavra posicionada |
48
+ | `engine.crossword.CrossBoard` | Motor de layout. Tabuleiro `Cell[60][60]`, posicionamento e ajuste |
49
+ | `engine.crossword.Checks` | Validação de posições candidatas (cruzamentos, limites, adjacências) |
50
+ | `engine.crossword.Coordinate` | Posição candidata `{x, y, isVertical}` |
51
+ | `engine.crossword.AddedWord` | Palavra já posicionada `{posicaoX, posicaoY, palavra, isVertical, casas[], isPrincipal}` |
52
+ | `engine.crossword.Cell` | Célula do tabuleiro `{letra, isOcupada, isInativa, isPrincipal}` |
53
+ | `engine.crossword.WordComparator` | `Comparator<String>` — ordena por **comprimento decrescente** |
54
+ | `engine.crossword.Utility` | Utilitários **legados** (carregar palavras de arquivo, gerar DOCX). **Não usados** no fluxo de IA |
55
+ | `engine.crossword.WordList` | Agrupamento de palavras por tamanho — usado **apenas** pela geração DOCX legada |
56
+ | `engine.crossword.CrosswordTest` | Classe de teste/`main` standalone |
57
+
58
+ Não há `Resource` CRUD, `Service`, `Repository`/`Dao`, `Manager` nem `Scheduler`/`Job` para crossword. A única "porta" é o método `buildCrossword`.
59
+
60
+ ### 2.2 Pipeline principal — `buildCrossword` (`AIRest.java:1861`)
61
+
62
+ Fluxo **síncrono** (bloqueia na chamada HTTP ao OpenAI via Unirest). Sem transação, sem persistência do resultado.
63
+
64
+ ```
65
+ [1] Lê do body: content (String, prompt do usuário) e current (Object, opcional)
66
+ [2] intro = IAManager.findPrompt("intro_build_crossword")
67
+ SE intro == null:
68
+ intro = new Prompt("intro_build_crossword", ..., <instrução hardcoded>)
69
+ IAManager.insertPrompt(intro) // SIDE EFFECT: grava em AI_PROMPT (global)
70
+ [3] Define a function OpenAI "build_crossword" (param obrigatório: crossword: string)
71
+ Monta system message "formatacao" instruindo o modelo a chamar a function
72
+ [4] ChatCompletionRequest:
73
+ model = gpt-3.5-turbo-1106 (MODEL_GPT_3_5)
74
+ system = intro.content
75
+ system = "Este é o crossword atual do usuário: ..." (SOMENTE se current != null)
76
+ system = formatacao
77
+ user = content
78
+ function_call = "auto"
79
+ [5] POST https://api.openai.com/v1/chat/completions
80
+ header Authorization = CHATGPT_TOKEN (TOKEN HARDCODED — AIRest.java:79 — ver §8)
81
+ [6] result = parse(response); call = result.choices[0].message.functionCall
82
+ json = call.getArguments()
83
+ [7] p = fromJson(json) // { "crossword": ... }
84
+ SE p.crossword é String → p = parse(p.crossword) // JSON aninhado em string
85
+ SENÃO → p = parse(toJson(p.crossword))
86
+ [8] Extrai com try/catch silencioso (apenas printStackTrace em falha):
87
+ title = String(p.title)
88
+ description = String(p.description)
89
+ select = parseInt(p.select) // default local = 5 se ausente/inválido
90
+ map = fromJson(p.words) // HashMap<palavra, dica>
91
+ [9] crossword = Crossword.create(title, description, map, select)
92
+ [10] return Callback 200 com toJson(crossword)
93
+ ```
94
+
95
+ Observações:
96
+
97
+ - 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.
98
+ - 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.
99
+
100
+ #### Fluxo — `buildCrossword`
101
+
102
+ ```mermaid
103
+ flowchart TB
104
+ A[POST /v3/ai/build/crossword<br/>content, current] --> B{findPrompt<br/>intro_build_crossword}
105
+ B -- null --> C[insertPrompt em AI_PROMPT]
106
+ B -- existe --> D[Monta ChatCompletionRequest]
107
+ C --> D
108
+ D --> E[POST OpenAI chat/completions<br/>token hardcoded]
109
+ E --> F[functionCall.arguments]
110
+ F --> G[Extrai title, description,<br/>select, words map]
111
+ G --> H[Crossword.create]
112
+ H --> I[200 + JSON do crossword]
113
+ ```
114
+
115
+ #### Interação com OpenAI
116
+
117
+ ```mermaid
118
+ sequenceDiagram
119
+ participant Cli as Chamador
120
+ participant AI as AIRest.buildCrossword
121
+ participant Mgr as AIManager
122
+ participant GPT as OpenAI
123
+ participant Eng as Crossword.create / CrossBoard
124
+
125
+ Cli->>AI: POST /v3/ai/build/crossword {content, current?}
126
+ AI->>Mgr: findPrompt("intro_build_crossword")
127
+ alt prompt ausente
128
+ AI->>Mgr: insertPrompt(intro) (AI_PROMPT)
129
+ end
130
+ AI->>GPT: chat/completions (function build_crossword)
131
+ GPT-->>AI: function_call.arguments (JSON: title, description, words, select)
132
+ AI->>Eng: create(title, description, words, select)
133
+ Eng-->>AI: Crossword {_id, title, description, words[]}
134
+ AI-->>Cli: 200 + JSON
135
+ ```
136
+
137
+ ### 2.3 Pipeline de layout — `Crossword.create` (`Crossword.java:32`)
138
+
139
+ ```
140
+ [1] numTotalPalavras = (select < 1 || select > 20) ? 20 : select // clamp [1,20], default 20
141
+ [2] tabuleiro = new CrossBoard(60, numTotalPalavras, true) // grid 60x60
142
+ [3] adicionadas = tabuleiro.gerarPalavrasAdicionadas(words.keySet(), numTotalPalavras)
143
+ [4] System.out de cada palavra adicionada + total // stdout
144
+ [5] tabuleiro.ajustarTabuleiro() // recorta bordas vazias
145
+ [6] tabuleiro.imprimirTabuleiro() // stdout
146
+ [7] PARA i = 0..adicionadas.size()-1:
147
+ Word(
148
+ number = i + 1,
149
+ direction = isVertical ? "vertical" : "horizontal",
150
+ row = posicaoX + 1, // 1-indexed
151
+ column = posicaoY + 1, // 1-indexed
152
+ hint = words.get(palavra), // dica pela chave exata
153
+ word = palavra
154
+ )
155
+ [8] Crossword.builder()._id(Guid.newShortGuid()) // ObjectId hex, 24 chars
156
+ .title(title).description(description).words(words).build()
157
+ ```
158
+
159
+ ### 2.4 Posicionamento das palavras — `CrossBoard`
160
+
161
+ `gerarPalavrasAdicionadas` (`CrossBoard.java:26`):
162
+
163
+ ```
164
+ [1] gerarTabuleiroVazio() // 60x60 Cells vazias
165
+ [2] limpa faltam / adicionadas / puladas; palavrasJaTrocadas = false
166
+ [3] faltam = cópia das palavras; Collections.sort(faltam, WordComparator) // maior → menor
167
+ [4] posicionarPalavraInicial()
168
+ [5] posicionarPalavras()
169
+ [6] return adicionadas
170
+ ```
171
+
172
+ `posicionarPalavraInicial` (`CrossBoard.java:112`):
173
+ - `escolherUmaPalavra()` seleciona a primeira palavra.
174
+ - SE comprimento > `tamanho` (60) → **retorna sem posicionar** (caso de borda).
175
+ - Posiciona próximo ao **centro** com `Coordinate(x, y, isVertical=true)` — a palavra inicial é **vertical** (apesar do comentário "para ser horizontal...").
176
+ - Marca `adicionadas.get(0).isPrincipal = true` (palavra "principal"; sem restrição de cruzamento).
177
+
178
+ `escolherUmaPalavra` (`CrossBoard.java:128`):
179
+ - SE `faltam.size() > 4` → índice aleatório em `[0, faltam.size()/2)` (favorece a metade mais longa).
180
+ - SENÃO → `faltam.get(0)` (a mais longa restante).
181
+
182
+ `posicionarPalavras` (`CrossBoard.java:179` — versão **ativa**):
183
+
184
+ ```
185
+ isVertical = true
186
+ maxTentativas = faltam.size() * 2
187
+ ENQUANTO faltam não vazio E adicionadas.size() < numPalavras E tentativas < maxTentativas:
188
+ palavra = escolherUmaPalavra()
189
+ isVertical = !isVertical // ALTERNA direção a cada iteração
190
+ adicionou = tentarAdicionarPalavra(palavra, isVertical)
191
+ SE não adicionou:
192
+ atualizarListasFaltamParaPuladas(palavra) // move de faltam → puladas
193
+ tentativas++
194
+ SENÃO:
195
+ tentativas = 0
196
+ SE puladas não vazio:
197
+ tentarPosicionarPuladas(isVertical) // tenta reencaixar puladas
198
+ ```
199
+
200
+ `verifDisponibilidade` (`Checks.java:13`) decide onde uma palavra pode entrar:
201
+
202
+ ```
203
+ posicoes = contarPossiveisPosicoes(palavra, isVertical)
204
+ // para CADA palavra já adicionada, verifPalavra() acha cruzamentos
205
+ // SOMENTE perpendiculares (palavraAdicionada.isVertical != isVertical)
206
+ // onde uma letra coincide
207
+ SE posicoes vazio → null
208
+ posicoes = removerPosicoesInadequadas(posicoes, palavra)
209
+ // mantém só as que passam em:
210
+ // verifLimites() → cabe dentro do tabuleiro
211
+ // verifCasasNoTabuleiro() → sem adjacência indevida; letras coincidem no cruzamento
212
+ SE posicoes vazio → null
213
+ SENÃO → posicoes[ Random.nextInt(posicoes.size()) ] // escolha ALEATÓRIA
214
+ ```
215
+
216
+ 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.
217
+
218
+ #### Loop de posicionamento
219
+
220
+ ```mermaid
221
+ flowchart TB
222
+ S[faltam ordenado por tamanho] --> Ini[posicionarPalavraInicial<br/>vertical, centro, isPrincipal]
223
+ Ini --> L{"faltam vazio? OR<br/>adicionadas >= numPalavras? OR<br/>tentativas >= max?"}
224
+ L -- continuar --> P[escolherUmaPalavra]
225
+ P --> T[isVertical = !isVertical]
226
+ T --> V[verifDisponibilidade<br/>cruzamentos perpendiculares]
227
+ V -- posição válida --> Add[adicionarPalavraTabuleiro<br/>tentativas = 0]
228
+ V -- nenhuma --> Sk[move para puladas<br/>tentativas++]
229
+ Add --> R[tentarPosicionarPuladas]
230
+ Sk --> R
231
+ R --> L
232
+ L -- parar --> Fim[adicionadas]
233
+ ```
234
+
235
+ `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.
236
+
237
+ ---
238
+
239
+ ## 3. Estrutura dos Objetos
240
+
241
+ ### 3.1 `Crossword` — documento raiz (saída)
242
+
243
+ `com.funifier.engine.crossword.Crossword`. **Não persistido pelo backend** — é o corpo da resposta.
244
+
245
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
246
+ |---|---|---|---|---|
247
+ | `_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")` |
248
+ | `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 |
249
+ | `description` | String | `"null"` (string) se ausente | Não | Descrição vinda do JSON da IA. Mesmo efeito `"null"` literal se ausente |
250
+ | `words` | List<`Word`> | `[]` | Sim (significativo) | Palavras efetivamente posicionadas no tabuleiro. **Tamanho ≤ `select`** (pode ser menor) |
251
+
252
+ Anotações: `@Data @Builder(toBuilder=true) @NoArgsConstructor @AllArgsConstructor @JsonIgnoreProperties(ignoreUnknown=true)`. `words` é `@Singular`.
253
+
254
+ **Campos computados / não persistidos:** todo o objeto é transiente — nada vai ao MongoDB por este fluxo.
255
+
256
+ ### 3.2 `Word` — sub-entidade (saída)
257
+
258
+ `com.funifier.engine.crossword.Word`. Um item de `Crossword.words`.
259
+
260
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
261
+ |---|---|---|---|---|
262
+ | `number` | int | — | Sim | Sequencial `1..N` na ordem de adição ao tabuleiro (não é ordem de leitura/grade) |
263
+ | `direction` | String | — | Sim | `"vertical"` ou `"horizontal"`. Derivado de `AddedWord.isVertical` |
264
+ | `row` | int | — | Sim | `posicaoX + 1` — **1-indexed**. Eixo das linhas (cresce para baixo) |
265
+ | `column` | int | — | Sim | `posicaoY + 1` — **1-indexed**. Eixo das colunas |
266
+ | `hint` | String | — | Sim | Dica da palavra (`words.get(palavra)`, busca pela chave exata) |
267
+ | `word` | String | — | Sim | A própria palavra |
268
+
269
+ **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).
270
+
271
+ ### 3.3 Estruturas internas (não saem na resposta)
272
+
273
+ | Classe | Campos | Observação |
274
+ |---|---|---|
275
+ | `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) |
276
+ | `AddedWord` | `posicaoX`, `posicaoY`, `palavra`, `isVertical`, `casas[]`, `isPrincipal` | `isPrincipal` marca a primeira palavra; **não** é exportado para `Word` |
277
+ | `Coordinate` | `x`, `y`, `isVertical` | Posição candidata produzida por `Checks` |
278
+ | `CrossBoard` | `tamanho`, `numPalavras`, `faltam`, `adicionadas`, `puladas`, `tabuleiro[][]`, ... | Estado do motor; descartado ao fim de `create` |
279
+
280
+ ### 3.4 Estrutura do JSON gerado pela IA (entrada do motor)
281
+
282
+ O ChatGPT é instruído (prompt `intro_build_crossword`) a produzir um JSON com:
283
+
284
+ | Campo | Tipo | Descrição (conforme o prompt) |
285
+ |---|---|---|
286
+ | `title` | String | Título do jogo |
287
+ | `description` | String | Descrição |
288
+ | `words` | Map<palavra, dica> | Pares palavra→dica. O prompt manda gerar **o dobro** das palavras pedidas, limitado a **40** |
289
+ | `select` | int | Quantas palavras o usuário quer ver no jogo, limitado a **20** no prompt |
290
+
291
+ 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.
292
+
293
+ ---
294
+
295
+ ## 4. Endpoints
296
+
297
+ ### 4.1 `POST /v3/ai/build/crossword`
298
+
299
+ | Aspecto | Detalhe |
300
+ |---|---|
301
+ | Finalidade | Gerar um crossword (palavras + dicas + layout) a partir de um tema em linguagem natural, via IA |
302
+ | Autenticação | Bearer token (`@BeanParam AuthBean`) |
303
+ | Consumes | `application/json` |
304
+ | Produces | `application/json; charset=UTF-8` |
305
+ | Full replace ou patch | N/A — não é CRUD. Gera e devolve; **não persiste** |
306
+ | Idempotência | **Não** — saída varia entre chamadas idênticas (ver §5.2) |
307
+
308
+ **Body params:**
309
+
310
+ | Param | Tipo | Obrigatório | Descrição |
311
+ |---|---|---|---|
312
+ | `content` | String | Sim (de fato) | Pedido em linguagem natural (tema, quantidade, idioma…). Vira a mensagem `user` enviada ao modelo |
313
+ | `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 |
314
+
315
+ **Comportamento real:**
316
+
317
+ - O backend **não valida** `content`; se ausente, `content = null` e o modelo recebe uma mensagem `user` nula (comportamento indefinido do parse OpenAI).
318
+ - O resultado **não é salvo**. O chamador é responsável por persistir.
319
+ - `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.
320
+ - Logs vão para **stdout** (`System.out.println`), não para logger estruturado.
321
+
322
+ **Exemplo de request:**
323
+
324
+ ```json
325
+ POST /v3/ai/build/crossword
326
+ Authorization: Bearer eyJhbGciOi...
327
+ Content-Type: application/json
328
+
329
+ {
330
+ "content": "Crie um crossword sobre frutas típicas do Brasil com 5 palavras"
331
+ }
332
+ ```
333
+
334
+ **Exemplo de response (200):**
335
+
336
+ ```json
337
+ {
338
+ "_id": "665f0a91c8b0000000abcdef",
339
+ "title": "Desafio de Frutas Brasileiras",
340
+ "description": "Encontre as frutas típicas do Brasil.",
341
+ "words": [
342
+ { "number": 1, "direction": "vertical", "row": 12, "column": 8, "hint": "Fruta roxa amazônica usada em sucos", "word": "AÇAÍ" },
343
+ { "number": 2, "direction": "horizontal", "row": 13, "column": 6, "hint": "Fruta pequena e roxa, nativa do Brasil", "word": "JABUTICABA" },
344
+ { "number": 3, "direction": "vertical", "row": 10, "column": 11, "hint": "Fruta em forma de estrela quando cortada", "word": "CARAMBOLA" }
345
+ ]
346
+ }
347
+ ```
348
+
349
+ > O número, a posição e a quantidade de palavras variam a cada chamada — o exemplo é ilustrativo.
350
+
351
+ ---
352
+
353
+ ## 5. Regras de Negócio
354
+
355
+ Regras presentes no **código** e ausentes de qualquer schema:
356
+
357
+ ### 5.1 `select` — dois níveis de tratamento
358
+
359
+ - Em `AIRest.buildCrossword`: `int select = 5;` e só é sobrescrito se `parseInt(p.select)` tiver sucesso. Logo, **omissão da IA → `select = 5`**.
360
+ - 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`.
361
+
362
+ ### 5.2 Saída não-determinística
363
+
364
+ `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.
365
+
366
+ ### 5.3 Pode posicionar **menos** palavras que `select`
367
+
368
+ 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.
369
+
370
+ ### 5.4 Primeira palavra é "principal" e sem restrição
371
+
372
+ 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).
373
+
374
+ ### 5.5 Direção alternada
375
+
376
+ `isVertical` é invertido a cada iteração do laço (`isVertical = !isVertical`), tendendo a alternar horizontal/vertical entre tentativas consecutivas.
377
+
378
+ ### 5.6 Cruzamento obrigatório e perpendicular
379
+
380
+ `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.
381
+
382
+ ### 5.7 Multi-tenant
383
+
384
+ - Autenticação por `AuthBean` (Bearer). O crossword gerado **não é escopado** a nada (não persiste).
385
+ - 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.
386
+
387
+ ### 5.8 Falhas de parse são silenciosas
388
+
389
+ 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).
390
+
391
+ ---
392
+
393
+ ## 6. Comportamentos Automáticos
394
+
395
+ | Comportamento | Trigger | Impacto | Persistência |
396
+ |---|---|---|---|
397
+ | 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) |
398
+ | Geração de `_id` | `Crossword.builder()` em `create` | `ObjectId` hex de 24 chars | Não (só no objeto devolvido) |
399
+ | Clamp de `select` | `Crossword.create` | Limita a `[1,20]`; fora disso → 20 | Não |
400
+ | Recorte do tabuleiro | `ajustarTabuleiro()` | Remove bordas vazias, normaliza `row`/`column` à origem | Não |
401
+ | Marcação `isPrincipal` | `posicionarPalavraInicial` | Primeira palavra marcada como principal (não exportado) | Não |
402
+ | Logs em stdout | `Crossword.create` e `buildCrossword` | Imprime palavras, tabuleiro, modelo e uso de tokens no stdout | Não (apenas log de processo) |
403
+
404
+ ---
405
+
406
+ ## 7. Suportado vs NÃO Suportado
407
+
408
+ ### ✅ Suportado
409
+
410
+ - Geração de crossword por **tema em linguagem natural** (`content`).
411
+ - **Refinamento** de um crossword existente (`current` injetado como contexto da IA).
412
+ - Layout automático com cruzamentos perpendiculares e validação de adjacência.
413
+ - Clamp de `select` para `[1,20]`.
414
+ - Saída JSON pronta para o frontend: `_id`, `title`, `description`, `words[]` com `number`, `direction`, `row`, `column`, `hint`, `word`.
415
+
416
+ ### ❌ NÃO Suportado
417
+
418
+ - **CRUD `/v3/crossword`** (GET / POST / PUT / DELETE) — **não existe**. Verificado por grep em `funifier-service`. O único endpoint é `POST /v3/ai/build/crossword`.
419
+ - **Tela `/studio/crossword`** — não existe módulo Studio dedicado.
420
+ - **Coleção MongoDB `crossword`** — o objeto **não é persistido**. Não há listagem, edição nem remoção pelo backend.
421
+ - **Determinismo / reprodutibilidade** — saída varia entre chamadas (uso de `Random` sem seed).
422
+ - **Garantia de posicionar todas as `select` palavras** — pode posicionar menos.
423
+ - **`PUT`/atualização** de um crossword — inexistente (não há recurso para atualizar).
424
+ - `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`).
425
+ - `posicionarPalavras___OLD()` — algoritmo antigo de posicionamento; **nunca chamado**.
426
+ - Geração de DOCX (`Utility.gerarTabelaComum`, `salvarArquivoNormal`, `salvarArquivoRespondido`) — **comentada** no fonte.
427
+ - `Cell.isInativa` — campo **declarado e lido** (`Checks.verifPalavra`) mas **nunca atribuído** → sempre `false` → o ramo que o consulta é morto.
428
+ - Bloco inline comentado em `AIRest.java:1949-1991` — duplica `Crossword.create` com clamp antigo `> 10 ? 10` (limite depois elevado para 20 em `Crossword.create`).
429
+ - `WordList`, `Utility.mostrarPalavras`, `Utility.separarPalavrasTamanho`, `Utility.setTableAlign` — auxiliares **somente** da geração DOCX legada.
430
+
431
+ ---
432
+
433
+ ## 8. Segurança e Permissões
434
+
435
+ - **Autenticação:** Bearer token via `@BeanParam AuthBean`. Sem autorização granular específica para crossword.
436
+ - **🔴 Token OpenAI hardcoded no fonte — `AIRest.java:79`:**
437
+ ```java
438
+ public static String CHATGPT_TOKEN = "Bearer sk-...<chave exposta>...";
439
+ ```
440
+ 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.
441
+ - **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`.
442
+ - **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.
443
+ - **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.
444
+
445
+ ---
446
+
447
+ ## 9. Observabilidade e Troubleshooting
448
+
449
+ **Diagnóstico:**
450
+
451
+ - Não há logger estruturado. O método imprime no **stdout** do servidor:
452
+ - `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`).
453
+ - Para verificar se o módulo funciona: chamar o endpoint com um `content` simples e inspecionar a resposta (status 200 + `words[]` não vazio).
454
+
455
+ **Erros comuns e causas:**
456
+
457
+ | Sintoma | Causa provável |
458
+ |---|---|
459
+ | `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 |
460
+ | `500` no parse do retorno | OpenAI não retornou `function_call` (ex.: token inválido/expirado, indisponibilidade) → `message.getFunctionCall()` é `null` |
461
+ | `title`/`description` valendo a string `"null"` | A IA omitiu o campo; `String.valueOf(p.get("title"))` produz `"null"` literal |
462
+ | `words` com menos itens que o pedido | Normal — palavras sem cruzamento válido foram puladas, ou `tentativas >= faltam.size()*2` (ver §5.3) |
463
+ | Resposta sem nenhuma palavra | Pool com 1 palavra e ela maior que 60 chars, ou nenhuma interseção encontrada após a inicial |
464
+
465
+ **Comandos úteis:**
466
+
467
+ ```
468
+ POST /v3/ai/build/crossword { "content": "crossword sobre capitais do Brasil, 8 palavras" }
469
+ POST /v3/ai/build/crossword { "content": "...", "current": { ...crossword existente... } }
470
+ ```
471
+
472
+ 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`.
473
+
474
+ ---
475
+
476
+ ## 10. Exemplos Práticos
477
+
478
+ ### Exemplo mínimo
479
+
480
+ ```json
481
+ POST /v3/ai/build/crossword
482
+ { "content": "Crie um crossword sobre animais da savana com 5 palavras" }
483
+ ```
484
+
485
+ Devolve um `Crossword` com `_id`, `title`, `description` e até 5 palavras posicionadas.
486
+
487
+ ### Exemplo avançado — refinar um crossword existente
488
+
489
+ ```json
490
+ POST /v3/ai/build/crossword
491
+ {
492
+ "content": "Troque as palavras muito difíceis por outras mais fáceis e mantenha o tema",
493
+ "current": {
494
+ "title": "Desafio de Frutas Brasileiras",
495
+ "description": "Encontre as frutas típicas do Brasil.",
496
+ "words": [
497
+ { "number": 1, "direction": "vertical", "row": 12, "column": 8, "hint": "Fruta roxa amazônica", "word": "AÇAÍ" }
498
+ ]
499
+ }
500
+ }
501
+ ```
502
+
503
+ O `current` é injetado como contexto `system`; o modelo gera um novo conjunto de palavras/dicas e o motor remonta o tabuleiro.
504
+
505
+ ### Anti-pattern (o que NÃO fazer)
506
+
507
+ - ❌ Chamar `GET /v3/crossword` ou `DELETE /v3/crossword/:id` — **não existem**. Não há CRUD.
508
+ - ❌ Esperar que o backend **salve** o crossword. Ele apenas devolve; persistência é responsabilidade do chamador.
509
+ - ❌ Depender da **mesma saída** para a mesma entrada — o layout é aleatório.
510
+ - ❌ Pedir `select` muito alto achando que vem mais palavras — acima de 20 vira 20, e o tabuleiro ainda pode posicionar menos.
511
+ - ❌ Confiar no token OpenAI hardcoded em produção — deve ser externalizado (ver §8).
512
+
513
+ ---
514
+
515
+ ## Checklist de Configuração
516
+
517
+ - [ ] Token OpenAI **válido** disponível (hoje hardcoded em `AIRest.java:79` — externalizar e revogar a chave exposta).
518
+ - [ ] `content` descreve **tema + quantidade** (e idioma/formato, se necessário) de forma clara.
519
+ - [ ] O chamador **persiste** o crossword retornado (o backend não persiste).
520
+ - [ ] Não depender de **determinismo** — tratar cada resposta como nova.
521
+ - [ ] `select` entre **1 e 20** (fora disso o backend força 20).
522
+ - [ ] Tratar o caso de **`words` vir vazio ou menor** que o solicitado.
523
+ - [ ] Armadilha silenciosa: se a IA **omitir `words`**, a geração pode lançar `NullPointerException` (500) — validar o retorno.
524
+
525
+ ## Steps
526
+
527
+ ### Sobre Crosswords
528
+
529
+ Palavras cruzadas têm: **título** → **palavras** (com dicas e posições na grade) → **tema visual**.
530
+ O Studio (`/studio/crossword`) fornece editor visual para posicionar as palavras na grade.
531
+
532
+ ---
533
+
534
+ ## Steps
535
+
536
+ ### 1. Verificar se já existe
537
+
538
+ ```
539
+ funifier_list type=crossword search=<titulo>
540
+ ```
541
+
542
+ ### 2. Ler a documentação do módulo
543
+
544
+ ```
545
+ datasource-funifier-docs/knowledge/modules/crossword.md
546
+ ```
547
+
548
+ ### 3. Criar o crossword
549
+
550
+ ```json
551
+ {
552
+ "title": "Tech Vocabulary",
553
+ "description": "Test your technical vocabulary.",
554
+ "active": true
555
+ }
556
+ ```
557
+
558
+ ```
559
+ funifier_save type=crossword payload=<json>
560
+ ```
561
+
562
+ ### 4. Adicionar palavras e posições
563
+
564
+ Use o editor visual do Studio (`/studio/crossword`) para posicionar palavras na grade e definir dicas.
565
+
566
+ Para criação via API, consulte `GET /v3/crossword/:id` em um crossword existente para entender a estrutura de campos das palavras (`words`, `position`, `clue`, `direction`).
567
+
568
+ ### 5. Validar
569
+
570
+ ```
571
+ funifier_list type=crossword search=<titulo>
572
+ ```
573
+
574
+ Teste o jogo no Studio e verifique que as dicas e palavras estão corretas.