@memoring/cli 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,9 +54,10 @@ Os itens enviados nascem **PENDING_REVIEW** — nada entra nas suas listagens ne
54
54
  | Command | Purpose |
55
55
  | --- | --- |
56
56
  | `memoring auth` | login / logout / status |
57
- | `memoring stage` | schema (o contrato + seus limites por tipo) / caps (mostra/grava os limites — valem para o app e para os agentes) / create (envia o lote, `--file` ou stdin, `--replace` esvazia a caixa antes; `--max-*` vence a preferência por lote) |
57
+ | `memoring stage` | schema (o contrato + seus limites por tipo + as regras de qualidade e suas instruções custom — tudo que o agente deve seguir) / caps (mostra/grava os limites — valem para o app e para os agentes) / create (envia o lote, `--file` ou stdin, `--replace` esvazia a caixa antes; `--max-*` vence a preferência por lote) |
58
58
  | `memoring inbox` | list / accept `<id>` / accept-all / discard `<id>` |
59
59
  | `memoring update` | atualiza a CLI para a última versão (via npm) |
60
+ | `memoring skill` | imprime o guia completo da CLI para agentes de IA (o SKILL.md empacotado) |
60
61
  | `memoring version` | versão da CLI |
61
62
 
62
63
  ## Flags
@@ -92,11 +93,11 @@ Cada item do lote (1 a 50, all-or-nothing):
92
93
 
93
94
  - `type`: `NOTE` (nota, exige `content` em markdown; `NORMAL` legado é aceito e mapeado para `NOTE`), `FLASHCARD` ou `ONE_SHOT` (exigem `front` + `back`).
94
95
  - `source`: origem do material — default `"Importação assistida (CLI)"`.
95
- - Limites por tipo: `memoring stage caps` mostra/grava sua preferência (0 = não gerar o tipo) — o `stage schema` a expõe ao agente e o `create` a impõe; flags `--max-*` no `create` vencem por lote.
96
+ - Limites por tipo: `memoring stage caps` mostra/grava sua preferência (`livre`/`-1` = à vontade — o padrão; `0` = não gerar o tipo) — o `stage schema` a expõe ao agente (`userCaps`) e o `create` a impõe; flags `--max-*` no `create` vencem por lote.
96
97
 
97
98
  ## Para agentes de IA
98
99
 
99
- Este pacote inclui uma skill em `skills/memoring-cli/SKILL.md` com o fluxo completo (auth → schema → gerar → stage → avisar o usuário). Aponte o agente para ela, ou simplesmente peça "use a cli do memoring" — os comandos são auto-descritivos (`--help` em todos).
100
+ Este pacote inclui uma skill em `skills/memoring-cli/SKILL.md` com o fluxo completo (auth → schema → gerar → stage → avisar o usuário). O jeito mais fácil de um agente aprender a CLI de cabo a rabo é rodar **`memoring skill`** — o comando imprime o guia completo empacotado. Ou simplesmente peça "use a cli do memoring" — os comandos são auto-descritivos (`--help` em todos).
100
101
 
101
102
  ## Dev (backend local)
102
103
 
@@ -0,0 +1,20 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fail } from './shared.js';
3
+ /**
4
+ * `memoring skill` — imprime o SKILL.md empacotado no npm (guia completo da CLI para
5
+ * agentes de IA: fluxo, contrato, limites por tipo, armadilhas). É como um agente
6
+ * aprende a CLI de cabo a rabo sem precisar caçar o arquivo no node_modules.
7
+ */
8
+ export function skillCommand() {
9
+ let content;
10
+ try {
11
+ content = readFileSync(new URL('../../skills/memoring-cli/SKILL.md', import.meta.url), 'utf8');
12
+ }
13
+ catch {
14
+ fail('não encontrei o SKILL.md empacotado — reinstale com `npm i -g @memoring/cli@latest`.');
15
+ }
16
+ process.stdout.write(content);
17
+ if (!content.endsWith('\n')) {
18
+ process.stdout.write('\n');
19
+ }
20
+ }
@@ -8,6 +8,43 @@ const TYPES = ['NOTE', 'FLASHCARD', 'ONE_SHOT'];
8
8
  const LEGACY_TYPE_ALIASES = { NORMAL: 'NOTE' };
9
9
  const MAX_ITEMS = 50;
10
10
  const DEFAULT_SOURCE = 'Importação assistida (CLI)';
11
+ const UNLIMITED = -1;
12
+ /**
13
+ * GET /notes/import/agent-guidance, fail-open como o fetchStoredCaps: sem sessão/rede
14
+ * devolve null e o schema sai só com o contrato.
15
+ */
16
+ async function fetchAgentGuidance() {
17
+ if (!hasSession()) {
18
+ return null;
19
+ }
20
+ try {
21
+ return (await apiFetch('/notes/import/agent-guidance'));
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ /** Rótulo de exibição de um teto: -1 → "à vontade". */
28
+ function capLabel(max) {
29
+ return max === UNLIMITED ? 'à vontade' : `até ${max}`;
30
+ }
31
+ /**
32
+ * Teto vindo de flag ou preferência: inteiro -1..50, ou a palavra "livre" (= -1, à vontade).
33
+ * `null` quando a flag está ausente.
34
+ */
35
+ function parseCapFlag(raw, flagName) {
36
+ if (raw === undefined) {
37
+ return null;
38
+ }
39
+ if (raw.trim().toLowerCase() === 'livre') {
40
+ return UNLIMITED;
41
+ }
42
+ const value = Number(raw);
43
+ if (!Number.isInteger(value) || value < UNLIMITED || value > 50) {
44
+ fail(`--${flagName} inválido: use "livre" ou um inteiro entre -1 (à vontade) e 50 (veio ${JSON.stringify(raw)}).`);
45
+ }
46
+ return value;
47
+ }
11
48
  /**
12
49
  * GET /notes/import/caps, fail-open: sem sessão ou sem rede devolve null (o schema
13
50
  * continua útil deslogado e o create só perde a validação extra). Só chama a API
@@ -44,23 +81,19 @@ const CAPS_FLAG_BY_FIELD = {
44
81
  };
45
82
  /**
46
83
  * Teto por tipo no lote (validação local): a flag --max-<tipo> vence; sem flag, vale a
47
- * preferência gravada do usuário (`memoring stage caps` / app). Tipo sem flag nem
48
- * preferência fica livre.
84
+ * preferência gravada do usuário (`memoring stage caps` / app). "livre"/-1 (à vontade)
85
+ * não limita; tipo sem flag nem preferência fica livre.
49
86
  */
50
87
  function enforceTypeCaps(items, flags, stored) {
51
88
  const storedByType = stored
52
89
  ? { NOTE: stored.maxNotes, FLASHCARD: stored.maxFlashcards, ONE_SHOT: stored.maxOneShots }
53
90
  : {};
54
91
  for (const [type, flag] of Object.entries(MAX_FLAG_BY_TYPE)) {
55
- const raw = stringFlag(flags, flag);
92
+ const fromFlag = parseCapFlag(stringFlag(flags, flag), flag);
56
93
  let max;
57
94
  let source;
58
- if (raw !== undefined) {
59
- const parsed = Number(raw);
60
- if (!Number.isInteger(parsed) || parsed < 0) {
61
- fail(`--${flag} inválido: use um inteiro >= 0 (veio ${JSON.stringify(raw)}).`);
62
- }
63
- max = parsed;
95
+ if (fromFlag !== null) {
96
+ max = fromFlag;
64
97
  source = `--${flag}`;
65
98
  }
66
99
  else if (storedByType[type] !== undefined) {
@@ -70,9 +103,12 @@ function enforceTypeCaps(items, flags, stored) {
70
103
  else {
71
104
  continue;
72
105
  }
106
+ if (max === UNLIMITED) {
107
+ continue;
108
+ }
73
109
  const count = items.filter((item) => item.type === type).length;
74
110
  if (count > max) {
75
- fail(`lote tem ${count} ${TYPE_LABEL[type]} e o limite é ${max} (${source}). Ajuste o lote ou o limite. ` +
111
+ fail(`lote tem ${count} ${TYPE_LABEL[type]} e o limite é ${max} (${source}). Ajuste o lote ou o limite ("livre" = à vontade). ` +
76
112
  'Se um agente gerou este lote, peça a ele para respeitar os limites por tipo.');
77
113
  }
78
114
  }
@@ -101,7 +137,7 @@ const SCHEMA_JSON = {
101
137
  source: 'Apostila constitucional.pdf',
102
138
  },
103
139
  ],
104
- typeCaps: 'preferência por usuário (`memoring stage caps`, vale para app e CLI) — o `stage schema` mostra os limites ao agente; flags do `stage create` (--max-notes N | --max-flashcards N | --max-one-shots N) vencem a preferência por lote.',
140
+ typeCaps: 'preferência por usuário (`memoring stage caps`, vale para app e CLI) — o `stage schema` mostra os limites ao agente (userCaps: -1 = à vontade, 0 = não gerar o tipo); flags do `stage create` (--max-notes N | --max-flashcards N | --max-one-shots N, ou "livre") vencem a preferência por lote.',
105
141
  };
106
142
  const SCHEMA_TEXT = `Contrato dos itens da caixa de revisão (POST /api/notes/review-inbox):
107
143
 
@@ -118,7 +154,7 @@ Lote: 1 a ${MAX_ITEMS} itens, all-or-nothing — qualquer item inválido rejeita
118
154
  Os itens nascem PENDING_REVIEW: o usuário aceita, edita ou recusa em /cards/inbox.
119
155
  Dica: FLASHCARD entra na fila de revisão espaçada (FSRS); ONE_SHOT é pergunta de resposta única.
120
156
  Limites por tipo: a preferência do usuário (abaixo, quando logado) vale por padrão;
121
- --max-notes N | --max-flashcards N | --max-one-shots N no create vencem por lote.`;
157
+ --max-notes N | --max-flashcards N | --max-one-shots N no create vencem por lote ("livre" = à vontade).`;
122
158
  async function readStdin() {
123
159
  const chunks = [];
124
160
  for await (const chunk of process.stdin) {
@@ -185,29 +221,42 @@ export async function stageCommand(parsed) {
185
221
  const [sub] = parsed._;
186
222
  switch (sub) {
187
223
  case 'schema': {
188
- const caps = await fetchStoredCaps();
224
+ const [caps, guidance] = await Promise.all([fetchStoredCaps(), fetchAgentGuidance()]);
189
225
  if (parsed.flags.json) {
190
- printJson(caps
191
- ? {
192
- ...SCHEMA_JSON,
193
- userCaps: {
194
- maxNotes: caps.maxNotes,
195
- maxFlashcards: caps.maxFlashcards,
196
- maxOneShots: caps.maxOneShots,
197
- },
198
- }
199
- : SCHEMA_JSON);
226
+ printJson({
227
+ ...SCHEMA_JSON,
228
+ ...(caps
229
+ ? {
230
+ userCaps: {
231
+ maxNotes: caps.maxNotes,
232
+ maxFlashcards: caps.maxFlashcards,
233
+ maxOneShots: caps.maxOneShots,
234
+ },
235
+ }
236
+ : {}),
237
+ ...(guidance ?? {}),
238
+ });
200
239
  }
201
240
  else {
202
241
  process.stdout.write(SCHEMA_TEXT + '\n');
203
242
  if (caps) {
204
243
  process.stdout.write(`\nLimites por tipo configurados por você — o agente DEVE respeitá-los ao gerar o lote: ` +
205
- `até ${caps.maxNotes} nota(s), até ${caps.maxFlashcards} flashcard(s), até ${caps.maxOneShots} 1-shot(s).\n` +
206
- dim('(altere com `memoring stage caps`; QUESTION não é estagiável pela CLI)\n'));
244
+ `notas: ${capLabel(caps.maxNotes)}, flashcards: ${capLabel(caps.maxFlashcards)}, 1-shots: ${capLabel(caps.maxOneShots)}.\n` +
245
+ dim('("à vontade" = crie quantos o material justificar; altere com `memoring stage caps`; QUESTION não é estagiável pela CLI)\n'));
207
246
  }
208
247
  else {
209
248
  process.stdout.write(dim('\n(limites por tipo indisponíveis — faça `memoring auth login` ou verifique a conexão)\n'));
210
249
  }
250
+ if (guidance) {
251
+ process.stdout.write('\nRegras de qualidade — SEMPRE aplicadas ao gerar (as mesmas que o app envia à LLM):\n' +
252
+ guidance.qualityRules.trim() +
253
+ '\n');
254
+ if (guidance.customInstructions) {
255
+ process.stdout.write('\nInstruções adicionais do usuário — também SEMPRE aplicadas:\n' +
256
+ guidance.customInstructions.trim() +
257
+ '\n');
258
+ }
259
+ }
211
260
  }
212
261
  return;
213
262
  }
@@ -216,14 +265,10 @@ export async function stageCommand(parsed) {
216
265
  const current = (await apiFetch('/notes/import/caps'));
217
266
  const updates = {};
218
267
  for (const [field, flag] of Object.entries(CAPS_FLAG_BY_FIELD)) {
219
- const raw = stringFlag(parsed.flags, flag);
220
- if (raw === undefined) {
268
+ const value = parseCapFlag(stringFlag(parsed.flags, flag), flag);
269
+ if (value === null) {
221
270
  continue;
222
271
  }
223
- const value = Number(raw);
224
- if (!Number.isInteger(value) || value < 0 || value > 50) {
225
- fail(`--${flag} inválido: use um inteiro entre 0 e 50 (veio ${JSON.stringify(raw)}).`);
226
- }
227
272
  updates[field] = value;
228
273
  }
229
274
  const hasUpdates = Object.keys(updates).length > 0;
@@ -236,10 +281,10 @@ export async function stageCommand(parsed) {
236
281
  }
237
282
  process.stdout.write((hasUpdates ? green('Limites gravados') : 'Limites por tipo') +
238
283
  ' — valem para a importação do app e para os agentes via CLI:\n');
239
- process.stdout.write(` notas: até ${effective.maxNotes} flashcards: até ${effective.maxFlashcards} ` +
240
- `1-shots: até ${effective.maxOneShots} questões: até ${effective.maxQuestions}\n`);
284
+ process.stdout.write(` notas: ${capLabel(effective.maxNotes)} flashcards: ${capLabel(effective.maxFlashcards)} ` +
285
+ `1-shots: ${capLabel(effective.maxOneShots)} questões: ${capLabel(effective.maxQuestions)}\n`);
241
286
  if (!hasUpdates) {
242
- process.stdout.write(dim('Altere com: memoring stage caps --max-flashcards 30 (0 = não gerar o tipo)\n'));
287
+ process.stdout.write(dim('Altere com: memoring stage caps --max-flashcards 30 (0 = não gerar o tipo; "livre" = à vontade)\n'));
243
288
  }
244
289
  return;
245
290
  }
package/dist/main.js CHANGED
@@ -5,9 +5,10 @@ import { authCommand } from './commands/auth.js';
5
5
  import { inboxCommand } from './commands/inbox.js';
6
6
  import { stageCommand } from './commands/stage.js';
7
7
  import { updateCommand } from './commands/update.js';
8
+ import { skillCommand } from './commands/skill.js';
8
9
  import { fail, parseArgs as parse } from './commands/shared.js';
9
10
  import { newerVersionAvailable, scheduleUpdateCheck } from './update.js';
10
- const VERSION = '0.4.0';
11
+ const VERSION = '0.6.0';
11
12
  const HELP = `Memoring CLI — flashcards e notas por IA, direto do terminal.
12
13
 
13
14
  Uso: memoring <comando> [subcomando] [flags]
@@ -22,6 +23,7 @@ Comandos:
22
23
  inbox accept <id>|accept-all aceita itens (entram nas listagens e na fila FSRS)
23
24
  inbox discard <id> descarta um item pendente
24
25
  update atualiza a CLI para a versão mais recente (via npm)
26
+ skill guia completo da CLI para agentes de IA (imprime o SKILL.md)
25
27
  version versão da CLI
26
28
 
27
29
  Flags globais:
@@ -85,6 +87,9 @@ async function main() {
85
87
  case 'update':
86
88
  await updateCommand(VERSION);
87
89
  return;
90
+ case 'skill':
91
+ skillCommand();
92
+ return;
88
93
  case 'version':
89
94
  process.stdout.write(`memoring-cli ${VERSION}\n`);
90
95
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memoring/cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "CLI do Memoring — envie flashcards/notas gerados por IA para a sua caixa de revisão, direto do terminal.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -3,38 +3,62 @@ name: memoring-cli
3
3
  description: Gere flashcards e notas a partir de materiais de estudo locais (PDFs, textos, anotações) e envie para a caixa de revisão do Memoring usando a CLI `memoring`. Use quando o usuário pedir para "usar a cli do memoring", criar flashcards/notas de uma pasta ou arquivo, ou enviar material de estudo para o Memoring.
4
4
  ---
5
5
 
6
- # Memoring CLI — geração de flashcards/notas por agente
6
+ # Memoring CLI — guia completo para agentes
7
7
 
8
- O Memoring é um app de estudos com revisão espaçada (FSRS). A CLI `memoring` é a ponte entre você (o agente, que lê o material e gera o conteúdo) e a conta do usuário no app. Você NÃO chama a API HTTP diretamente — use sempre a CLI.
8
+ O Memoring é um app de estudos com revisão espaçada (FSRS). A CLI `memoring` é a ponte entre você (o agente, que lê o material e gera o conteúdo) e a conta do usuário no app. Você NÃO chama a API HTTP diretamente — use sempre a CLI. Este guia está sempre disponível via `memoring skill`.
9
9
 
10
10
  ## Pré-requisitos
11
11
 
12
12
  1. CLI instalada: `memoring version` deve responder. Se não, peça ao usuário: `npm install -g @memoring/cli`.
13
13
  2. Sessão ativa: `memoring auth status --json` → `authenticated: true`. Se não, peça ao usuário para rodar `memoring auth login` (device flow — ele aprova no browser; você não consegue fazer isso por ele).
14
14
 
15
- ## Fluxo
15
+ ## Mapa dos comandos
16
16
 
17
- 1. **Entenda o pedido** — qual pasta/arquivo/material virar flashcards/notas.
18
- 2. **Leia o contrato**: `memoring stage schema --json`.
17
+ | Comando | O que faz |
18
+ | --- | --- |
19
+ | `memoring auth login\|logout\|status` | sessão (device flow — sem senha no terminal) |
20
+ | `memoring stage schema [--json]` | O CONTRATO dos itens + os limites por tipo do usuário — leia SEMPRE antes de gerar |
21
+ | `memoring stage caps [--max-* N\|livre]` | limites por tipo do usuário (preferência DELE — nunca escreva sem ele pedir) |
22
+ | `memoring stage create --file f [--replace] [--max-* N]` | envia o lote (1 a 50 itens) para a caixa de revisão |
23
+ | `memoring inbox list` | itens pendentes de revisão |
24
+ | `memoring inbox accept <id>\|accept-all` | aceita itens (entram nas listagens e na fila FSRS) |
25
+ | `memoring inbox discard <id>` | descarta um item pendente |
26
+ | `memoring skill` | imprime este guia |
27
+ | `memoring update` | atualiza a CLI (decisão do usuário — nunca rode por conta própria) |
28
+
29
+ Flags globais: `--json` (saída legível por máquina), `--no-color`, `--api-base`.
30
+
31
+ ## Fluxo padrão
32
+
33
+ 1. **Entenda o pedido** — qual pasta/arquivo/material vira flashcards/notas e se o usuário pediu quantidades.
34
+ 2. **Leia o contrato, os limites e as regras**: `memoring stage schema --json`. Além do contrato, a saída traz: `userCaps` — os limites por tipo DO USUÁRIO (`-1` = à vontade, `0` = não gerar o tipo, `N` = teto), respeite-os SEMPRE; `qualityRules` — as regras de qualidade INTEGRAIS (as mesmas que o app envia à LLM), aplique-as em TODOS os itens; `customInstructions` — instruções adicionais do usuário (quando não-nulas), também sempre aplicadas. Tudo isso vale mesmo sem pedido explícito no chat.
19
35
  3. **Leia os materiais** e gere os itens no contrato, num arquivo JSON (array ou `{ "items": [...] }`).
20
- 4. **Envie**: `memoring stage create --file <arquivo> --json`.
36
+ 4. **Envie**: `memoring stage create --file <arquivo> --json`. Se o lote for rejeitado por limite de tipo, ajuste o LOTE — não relaxe o limite do usuário.
21
37
  5. **Avise o usuário**: quantos itens foram para a caixa de revisão e onde aceitar — no app em `/cards/inbox` ou `memoring inbox list` / `memoring inbox accept-all`.
22
38
 
23
- ## Regras do contrato (resumo — a fonte da verdade é `stage schema`)
39
+ ## O contrato (a fonte da verdade é `stage schema`)
24
40
 
25
41
  - `type`: `NOTE` (nota, exige `content` markdown; `NORMAL` legado é aceito e mapeado para `NOTE`), `FLASHCARD` ou `ONE_SHOT` (exigem `front` + `back`).
26
- - `title` sempre obrigatório; lote de 1 a 50 itens, all-or-nothing.
27
- - Se o usuário pedir limites por tipo (ex.: "no máximo 20 flashcards e 5 notas"), respeite-os ao gerar e valide com `--max-notes N --max-flashcards N --max-one-shots N` no `stage create` — o lote é rejeitado se exceder.
28
- - O `stage schema` mostra os **limites por tipo do usuário** (`userCaps` no JSON; texto no fim da saída em modo humano) — respeite-os ao gerar, mesmo sem pedido explícito no chat. Sem limites visíveis, o create ainda pode rejeitar pela preferência gravada: ajuste o lote e reenvie. Para MUDAR a preferência permanente, o usuário roda `memoring stage caps` — não rode por conta própria.
29
- - `ONE_SHOT` = pergunta de resposta única; prefira `FLASHCARD` para memorização.
42
+ - `title` sempre obrigatório; lote de 1 a 50 itens, all-or-nothing — qualquer item inválido rejeita o lote inteiro.
43
+ - `ONE_SHOT` = pergunta de resposta única (uma palavra ou termo curto); prefira `FLASHCARD` para memorização.
30
44
  - Preencha `source` com a origem real do material (nome do arquivo/pasta) — não use o default quando souber a origem.
31
45
  - `subjectName`/`subtopicName` quando o material tiver matéria/subtópico claros; `tags` curtas e em minúsculas.
32
46
  - NUNCA invente campos fora do contrato — o servidor rejeita o lote inteiro.
33
47
 
48
+ ## Quantidades (limites por tipo)
49
+
50
+ - O default do usuário costuma ser **à vontade** (`userCaps` com `-1`): crie QUANTOS itens o material justificar — qualidade acima de quantidade, sem encher cota e sem repetir o mesmo fato em variações triviais.
51
+ - Se `userCaps` trouxer tetos numéricos, distribua a geração para caber neles; `0` proíbe o tipo.
52
+ - Se o usuário pedir quantidades no chat (ex.: "quero 30 flashcards e 5 notas"), o pedido vence o `userCaps`: gere e valide com `--max-flashcards 30 --max-notes 5` no `stage create`.
53
+ - Lotes grandes: divida em vários arquivos de até 50 itens e envie sequencialmente.
54
+
55
+ ## Qualidade dos flashcards (o padrão que o app espera)
56
+
57
+ Cartões atômicos: UMA ideia por cartão — se um conceito tem 3 partes cobráveis, crie 3 cartões, nunca um cartão com lista como resposta. A frente é uma pergunta direta que força LEMBRAR (não reconhecer). A resposta é auto-contida (faz sentido sem o material) e curta. Pares confundíveis (prazos, siglas, exceções) levam na frente o traço que os diferencia. Não invente conteúdo que não esteja no material.
58
+
34
59
  ## Armadilhas
35
60
 
36
61
  - **Nunca** use `--replace` sem o usuário pedir explicitamente — ele apaga os itens pendentes existentes da caixa.
37
62
  - Se aparecer um aviso de versão nova no stderr, ignore e siga o fluxo — **não** rode `memoring update` por conta própria; atualizar a CLI é decisão do usuário.
63
+ - **Não** altere os limites do usuário (`stage caps`) por conta própria — é preferência dele, compartilhada com o app.
38
64
  - Se `stage create` falhar com erro de validação do servidor, corrija os itens indicados e reenvie — não relaxe o contrato.
39
- - Lotes grandes: divida em vários arquivos de até 50 itens e envie sequencialmente.
40
- - Flashcards bons são atômicos: uma ideia por cartão, pergunta direta, resposta completa mas curta.