@memoring/cli 0.12.0 → 0.13.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
@@ -87,15 +87,17 @@ Cada item do lote (1 a 50, all-or-nothing):
87
87
  "front": "Quais são os quatro objetivos fundamentais da República (art. 3º da CF/88)?",
88
88
  "back": "I — construir uma sociedade livre, justa e solidária; II — garantir o desenvolvimento nacional; III — erradicar a pobreza e reduzir as desigualdades; IV — promover o bem de todos.",
89
89
  "subjectName": "Direito Constitucional",
90
+ "conceptKey": "const-cf88-art-3",
90
91
  "tags": ["cf88", "art-3"],
91
92
  "source": "Apostila constitucional.pdf"
92
93
  }
93
94
  ```
94
95
 
95
96
  - `type`: `NOTE` (nota, exige `content` em markdown; `NORMAL` legado é aceito e mapeado para `NOTE`), `FLASHCARD` ou `ONE_SHOT` (exigem `front` + `back`), `QUESTION` (exige `question`: `kind` `MULTIPLA_ESCOLHA`/`CERTO_ERRADO`, `statementHtml`, `correctAlternative` 1-based — 1=Certo/2=Errado —, `alternatives` 2–5 só em múltipla escolha, `commentHtml`).
97
+ - `conceptKey`: conceito do item em slug minúsculo (letras, números e hífens, até 80 chars). Opcional por compatibilidade, mas **fortemente recomendado em FLASHCARD/ONE_SHOT/QUESTION** — cartões sobre o MESMO conceito compartilham a MESMA chave e conceitos DIFERENTES usam chaves diferentes (nunca reuse a chave). Ex.: um flashcard e uma questão sobre o art. 4º do CTN usam ambos `"trib-art-4"`. No app, o whiteboard do Rascunho agrupa por conceito: ângulos do mesmo conceito contam como uma frente só nas rodadas guiadas; sem chave, cada cartão é uma frente individual. No `inbox update <id>`, o `conceptKey` atual é preservado quando o campo não é informado.
96
98
  - `deckName`: **obrigatório** (ou `--deck` no create) — cartão não fica sem baralho; o nome de um baralho existente (`memoring deck list`) é reutilizado, nome novo vira baralho criado no envio.
97
99
  - `source`: origem do material — default `"Importação assistida (CLI)"`.
98
- - 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.
100
+ - Limites por tipo são **tetos, nunca cotas**: `memoring stage caps` mostra/grava sua preferência (`livre`/`-1` = sem teto preferencial — o padrão, não um incentivo a inflar o lote; `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. O 50 é apenas o limite técnico por requisição, não a meta: sem quantidade explícita, o agente deve selecionar apenas os pontos realmente cobráveis e manter uma carga de revisão sustentável.
99
101
  - Sem sessão, o `stage schema --json` sai com `authenticated: false`, `userCaps`/`qualityRules`/`customInstructions` nulos e um `warning` — agentes devem logar (`auth login`) e reler o schema ANTES de gerar.
100
102
 
101
103
  ## Para agentes de IA
@@ -60,6 +60,7 @@ async function updateInboxItem(parsed, id) {
60
60
  subtopicName: overrides.subtopicName ?? stringFlag(flags, 'subtopic') ?? current.subtopicName ?? null,
61
61
  tags: overrides.tags ?? (tagsFlag !== undefined ? parseTagsFlag(tagsFlag) : current.tags ?? []),
62
62
  source: overrides.source ?? stringFlag(flags, 'source') ?? current.source ?? null,
63
+ conceptKey: overrides.conceptKey ?? stringFlag(flags, 'concept') ?? current.conceptKey ?? null,
63
64
  deckId: deckId ?? overrides.deckId ?? current.deckId ?? null,
64
65
  question: overrides.question ?? current.question ?? null,
65
66
  };
@@ -166,7 +167,8 @@ export async function inboxCommand(parsed) {
166
167
  ' discard-all descarta todos os itens pendentes\n' +
167
168
  ' update <id> edita um item PENDENTE (merge: --file parcial.json e/ou flags\n' +
168
169
  ' --title --content --front --back --subject --subtopic\n' +
169
- ' --tags a,b,c --source --type)\n');
170
+ ' --tags a,b,c --source --concept trib-art-4 --type)\n' +
171
+ ' (--concept preserva/define o conceptKey; sem ele, o valor atual é mantido)\n');
170
172
  if (sub) {
171
173
  process.exitCode = 1;
172
174
  }
@@ -10,6 +10,13 @@ const LEGACY_TYPE_ALIASES = { NORMAL: 'NOTE' };
10
10
  const MAX_ITEMS = 50;
11
11
  const DEFAULT_SOURCE = 'Importação assistida (CLI)';
12
12
  const QUESTION_KINDS = ['MULTIPLA_ESCOLHA', 'CERTO_ERRADO'];
13
+ /**
14
+ * Slug minúsculo (letras, números, hífens, até 80 chars) que identifica o conceito
15
+ * do item. Segmentos alfanuméricos separados por hífen único — sem hífen no
16
+ * início/fim nem duplo, espelhando a validação do backend (`assignConceptKey`).
17
+ */
18
+ const CONCEPT_KEY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
19
+ const CONCEPT_KEY_MAX = 80;
13
20
  const UNLIMITED = -1;
14
21
  /**
15
22
  * GET /notes/import/agent-guidance, fail-open como o fetchStoredCaps: sem sessão/rede
@@ -147,6 +154,7 @@ const SCHEMA_JSON = {
147
154
  subjectName: 'string? — matéria (ex.: "Direito Constitucional")',
148
155
  subtopicName: 'string? — subtópico',
149
156
  tags: 'string[]?',
157
+ conceptKey: `string? — conceito do item em slug minúsculo (letras/números/hífens, até ${CONCEPT_KEY_MAX} chars). Opcional por compatibilidade, mas FORTEMENTE RECOMENDADO em FLASHCARD/ONE_SHOT/QUESTION: cartões sobre o MESMO conceito compartilham a chave (ex.: "trib-art-4" num flashcard E numa questão do art. 4º do CTN — o whiteboard do app agrupa por conceito) e conceitos DIFERENTES usam chaves diferentes — nunca reuse a chave para outro conceito`,
150
158
  deckName: 'string (OBRIGATÓRIO — cartão não fica sem baralho: consulte `memoring deck list`, reutilize um existente ou crie com `memoring deck create`; nome inexistente é criado no envio como PENDING — oculto no app até o 1º item aceite, que ativa o baralho e os ancestrais pendentes. A flag --deck do create vale como padrão do lote)',
151
159
  question: 'objeto? — snapshot da questão (obrigatório quando type=QUESTION; contrato em questionContract)',
152
160
  source: `string? — origem do material (default: "${DEFAULT_SOURCE}")`,
@@ -161,6 +169,7 @@ const SCHEMA_JSON = {
161
169
  subjectName: 'Direito Constitucional',
162
170
  deckName: 'Direito Constitucional',
163
171
  tags: ['cf88', 'art-3'],
172
+ conceptKey: 'const-cf88-art-3',
164
173
  source: 'Apostila constitucional.pdf',
165
174
  },
166
175
  {
@@ -169,6 +178,7 @@ const SCHEMA_JSON = {
169
178
  subjectName: 'Direito Tributário',
170
179
  deckName: 'Direito Tributário',
171
180
  tags: ['ctn', 'art-3'],
181
+ conceptKey: 'trib-ctn-art-3',
172
182
  source: 'aula-00.pdf',
173
183
  question: {
174
184
  kind: 'CERTO_ERRADO',
@@ -177,8 +187,34 @@ const SCHEMA_JSON = {
177
187
  commentHtml: 'É a definição do art. 3º do CTN (Lei 5.172/1966).',
178
188
  },
179
189
  },
190
+ {
191
+ type: 'FLASHCARD',
192
+ title: 'CTN art. 4º — territorialidade',
193
+ front: 'Qual a regra de incidência dos tributos quanto à aplicação da lei (art. 4º, caput, do CTN)?',
194
+ back: 'Regra geral: a lei que institui o tributo aplica-se a todos os fatos geradores ocorridos no território nacional.',
195
+ subjectName: 'Direito Tributário',
196
+ deckName: 'Direito Tributário',
197
+ tags: ['ctn', 'art-4'],
198
+ conceptKey: 'trib-art-4',
199
+ source: 'aula-00.pdf',
200
+ },
201
+ {
202
+ type: 'QUESTION',
203
+ title: 'CTN art. 4º — exceção à territorialidade',
204
+ subjectName: 'Direito Tributário',
205
+ deckName: 'Direito Tributário',
206
+ tags: ['ctn', 'art-4'],
207
+ conceptKey: 'trib-art-4',
208
+ source: 'aula-00.pdf',
209
+ question: {
210
+ kind: 'CERTO_ERRADO',
211
+ statementHtml: 'Nos termos do art. 4º do CTN, a lei tributária brasileira NUNCA se aplica a fatos geradores ocorridos no exterior, ainda que o fato tenha produzido efeitos no Brasil.',
212
+ correctAlternative: 2,
213
+ commentHtml: 'Parágrafo único do art. 4º: a lei brasileira pode reger fatos ocorridos no exterior quando a eles se refiram dispositivos expressos da Constituição.',
214
+ },
215
+ },
180
216
  ],
181
- 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 | --max-questions N, ou "livre") vencem a preferência por lote.',
217
+ typeCaps: 'preferência por usuário (`memoring stage caps`, vale para app e CLI) — limites são TETOS, nunca cotas: 50 é só o limite técnico por requisição, não a meta do lote; userCaps -1 = sem teto preferencial (não é incentivo a inflar), 0 = não gerar o tipo. Sem quantidade explícita no pedido, selecione só pontos realmente cobráveis, com cobertura seletiva e carga de revisão sustentável. Flags do `stage create` (--max-notes N | --max-flashcards N | --max-one-shots N | --max-questions N, ou "livre") vencem a preferência por lote.',
182
218
  };
183
219
  const SCHEMA_TEXT = `Contrato dos itens da caixa de revisão (POST /api/notes/review-inbox):
184
220
 
@@ -199,6 +235,14 @@ const SCHEMA_TEXT = `Contrato dos itens da caixa de revisão (POST /api/notes/re
199
235
  subjectName? string — matéria (ex.: "Direito Constitucional")
200
236
  subtopicName? string — subtópico
201
237
  tags? string[]
238
+ conceptKey? string — conceito do item em slug minúsculo (letras, números
239
+ e hífens, até 80 chars). Opcional por compatibilidade, mas
240
+ FORTEMENTE RECOMENDADO em FLASHCARD/ONE_SHOT/QUESTION: cartões
241
+ sobre o MESMO conceito compartilham a MESMA chave — ex.:
242
+ "trib-art-4" num flashcard E numa questão do art. 4º do CTN
243
+ (o whiteboard do app agrupa por conceito, não por cartão) —
244
+ e conceitos DIFERENTES usam chaves diferentes: nunca reuse
245
+ uma chave para outro conceito
202
246
  source? string — origem do material (default: "${DEFAULT_SOURCE}")
203
247
 
204
248
  Snapshot de QUESTION (questões manuais — sem identidade TEC):
@@ -294,6 +338,7 @@ async function assignDecks(items, created, deckIds) {
294
338
  subtopicName: item.subtopicName ?? null,
295
339
  tags: item.tags ?? [],
296
340
  source: item.source ?? null,
341
+ conceptKey: item.conceptKey ?? null,
297
342
  deckId,
298
343
  question: item.question ?? null,
299
344
  },
@@ -410,7 +455,25 @@ function validateItems(raw) {
410
455
  if (it.tags !== undefined && (!Array.isArray(it.tags) || it.tags.some((t) => typeof t !== 'string'))) {
411
456
  fail(`${where} (${it.title}): tags deve ser um array de strings.`);
412
457
  }
413
- return { ...it, source: typeof it.source === 'string' && it.source.trim() !== '' ? it.source : DEFAULT_SOURCE };
458
+ let conceptKey = null;
459
+ if (it.conceptKey !== undefined && it.conceptKey !== null) {
460
+ if (typeof it.conceptKey !== 'string') {
461
+ fail(`${where} (${it.title}): conceptKey deve ser uma string (slug minúsculo) ou null.`);
462
+ }
463
+ const key = it.conceptKey.trim();
464
+ if (key !== '') {
465
+ if (key.length > CONCEPT_KEY_MAX || !CONCEPT_KEY_PATTERN.test(key)) {
466
+ fail(`${where} (${it.title}): conceptKey "${key}" inválido — use slug minúsculo com letras, ` +
467
+ `números e hífens, até ${CONCEPT_KEY_MAX} chars (ex.: "trib-art-4"). Mesmo conceito, mesma chave; conceito diferente, chave diferente.`);
468
+ }
469
+ conceptKey = key;
470
+ }
471
+ }
472
+ return {
473
+ ...it,
474
+ conceptKey,
475
+ source: typeof it.source === 'string' && it.source.trim() !== '' ? it.source : DEFAULT_SOURCE,
476
+ };
414
477
  });
415
478
  }
416
479
  /**
@@ -466,7 +529,7 @@ export async function stageCommand(parsed) {
466
529
  process.stdout.write(`\nLimites por tipo configurados por você — o agente DEVE respeitá-los ao gerar o lote: ` +
467
530
  `notas: ${capLabel(caps.maxNotes)}, flashcards: ${capLabel(caps.maxFlashcards)}, ` +
468
531
  `1-shots: ${capLabel(caps.maxOneShots)}, questões: ${capLabel(caps.maxQuestions)}.\n` +
469
- dim('("à vontade" = crie quantos o material justificar; altere com `memoring stage caps`)\n'));
532
+ dim('("à vontade" = sem teto preferencial, não uma cota: selecione só o que for realmente cobrável; 50 é só o limite técnico por envio, nunca a meta. Altere com `memoring stage caps`)\n'));
470
533
  }
471
534
  else {
472
535
  process.stdout.write(dim('\n(limites por tipo indisponíveis — faça `memoring auth login` ou verifique a conexão)\n'));
package/dist/main.js CHANGED
@@ -9,7 +9,7 @@ import { updateCommand } from './commands/update.js';
9
9
  import { skillCommand } from './commands/skill.js';
10
10
  import { parseArgs as parse } from './commands/shared.js';
11
11
  import { newerVersionAvailable, scheduleUpdateCheck } from './update.js';
12
- const VERSION = '0.12.0';
12
+ const VERSION = '0.13.0';
13
13
  const HELP = `Memoring CLI — flashcards e notas por IA, direto do terminal.
14
14
 
15
15
  Uso: memoring <comando> [subcomando] [flags]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memoring/cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.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",
@@ -27,7 +27,7 @@ O Memoring é um app de estudos com revisão espaçada (FSRS). A CLI `memoring`
27
27
  | `memoring inbox list` | itens pendentes de revisão |
28
28
  | `memoring inbox accept <id>\|accept-all` | aceita itens (entram nas listagens e na fila FSRS) — **decisão do usuário: só rode se ELE pedir explicitamente** |
29
29
  | `memoring inbox discard <id>\|discard-all` | descarta item(ns) pendente(s) |
30
- | `memoring inbox update <id> [--file parcial.json] [--title ...] [--front ...] [--back ...] [--tags a,b] ...` | edita um item AINDA PENDENTE (merge com o estado atual) |
30
+ | `memoring inbox update <id> [--file parcial.json] [--title ...] [--front ...] [--back ...] [--tags a,b] [--concept trib-art-4] ...` | edita um item AINDA PENDENTE (merge com o estado atual — `conceptKey` é preservado se não for informado) |
31
31
  | `memoring skill` | imprime este guia |
32
32
  | `memoring update` | atualiza a CLI (decisão do usuário — nunca rode por conta própria) |
33
33
 
@@ -51,16 +51,19 @@ Flags globais: `--json` (saída legível por máquina), `--no-color`, `--api-bas
51
51
  - `deckName` **OBRIGATÓRIO** (ou `--deck` no create como padrão do lote): cartão não fica sem baralho. Use o nome de um baralho de `memoring deck list`; nome inexistente é criado no envio como **PENDING** (oculto no app até o 1º item aceite). Espelhe a hierarquia do material: matéria = baralho raiz; tema da aula/capítulo = sub-baralho criado ANTES com `deck create "<tema>" --parent "<matéria>" --pending` (o `stage create` só cria raiz pelo nome). O primeiro item aceito ativa o baralho e os ancestrais pendentes.
52
52
  - `ONE_SHOT` = pergunta de resposta única (UMA palavra ou termo curto, texto puro). Ative o tipo sempre que o cobrável for recall de termo puro — nome de princípio/instituto, termo em latim, sigla, número de lei/artigo, data, prazo. Resposta cabe num termo → `ONE_SHOT`; exige frase ou explicação → `FLASHCARD`.
53
53
  - `QUESTION` = questão objetiva para verificação: `question.kind` (`MULTIPLA_ESCOLHA` | `CERTO_ERRADO`), `question.statementHtml` (enunciado), `question.correctAlternative` (gabarito, int 1-based; em CERTO_ERRADO, 1 = Certo, 2 = Errado), `question.alternatives` (2 a 5 textos — só em MULTIPLA_ESCOLHA), `question.commentHtml` (explicação do gabarito). **Se o material contiver questões prontas (provas, simulados, listas C/E), APROVEITE-AS como QUESTION** preservando enunciado e gabarito — não as achate em flashcards.
54
+ - `conceptKey` = slug minúsculo (letras, números e hífens, até 80 chars) que identifica o CONCEITO do item. Opcional por compatibilidade, mas **fortemente recomendado em FLASHCARD/ONE_SHOT/QUESTION**. A regra é simples: cartões sobre o MESMO conceito compartilham a MESMA chave — ex.: um flashcard e uma questão sobre o art. 4º do CTN usam ambos `"trib-art-4"` — e conceitos DIFERENTES usam chaves diferentes, nunca reuse uma chave para outro conceito. No app, o whiteboard do Rascunho usa a chave para AGRUPAR por conceito: ângulos do mesmo conceito (flashcard, questão, 1-shot) contam como uma frente só nas rodadas guiadas — sem chave, cada cartão vira uma frente individual. Chaves curtas e estáveis, com contexto da matéria (ex.: `trib-ctn-art-3`, `const-cf88-art-3`), evitando chaves genéricas que fundam conceitos distintos.
54
55
  - Preencha `source` com a origem real do material (nome do arquivo/pasta) — não use o default quando souber a origem.
55
56
  - `subjectName`/`subtopicName` quando o material tiver matéria/subtópico claros; `tags` curtas e em minúsculas.
56
57
  - NUNCA invente campos fora do contrato — o servidor rejeita o lote inteiro.
57
58
 
58
- ## Quantidades (limites por tipo)
59
+ ## Quantidades (limites são tetos, NUNCA cotas)
59
60
 
60
- - 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.
61
- - Se `userCaps` trouxer tetos numéricos, distribua a geração para caber neles; `0` proíbe o tipo.
62
- - 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`.
63
- - Lotes grandes: divida em vários arquivos de até 50 itens e envie sequencialmente.
61
+ - **50 é apenas o limite técnico de itens por requisição, NÃO a meta do lote.** Nunca mire nesse número, não proponha faixas como “46–50” sem o usuário pedir e não acrescente itens para “fechar” um lote.
62
+ - **Sem quantidade explícita no pedido, não estabeleça um alvo numérico antes de analisar o material.** A quantidade é consequência da seleção: inclua conceitos centrais, exceções e confusões realmente cobráveis, além das questões prontas. Não transforme cada parágrafo, detalhe secundário, exemplo ou sigla em cartão — a cobertura é SELETIVA, não exaustiva. Em dúvida, prefira menos itens.
63
+ - **Considere a carga de revisão acumulada do usuário.** Milhares de páginas de estudo não podem virar dezenas de milhares de revisões mecânicas. Não use uma taxa de cartões por página (ex.: “50 a cada 10–15 páginas”): densidade e importância do assunto importam mais que o volume do PDF.
64
+ - `userCaps` com `-1` significa apenas **sem teto preferencial**, não “cresça à vontade”. Tetos numéricos são limites máximos, nunca cotas a preencher; `0` proíbe o tipo.
65
+ - 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`.
66
+ - Se a seleção de pontos realmente úteis ultrapassar 50 itens, divida em vários arquivos de até 50 e envie sequencialmente — nunca infle um lote para usar a capacidade disponível.
64
67
 
65
68
  ## Qualidade dos flashcards (o padrão que o app espera)
66
69