@memoring/cli 0.12.1 → 0.15.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 +163 -8
- package/dist/api.js +90 -4
- package/dist/args.js +14 -1
- package/dist/auth.js +1 -1
- package/dist/commands/batch.js +327 -0
- package/dist/commands/inbox.js +3 -1
- package/dist/commands/materialReferences.js +359 -0
- package/dist/commands/plan-analyze.js +256 -0
- package/dist/commands/plan-diagnostics.js +65 -0
- package/dist/commands/plan-exams.js +122 -0
- package/dist/commands/plan-goal.js +131 -0
- package/dist/commands/plan-shared.js +133 -0
- package/dist/commands/plan.js +219 -0
- package/dist/commands/planDrafts.js +162 -0
- package/dist/commands/scratch.js +632 -0
- package/dist/commands/stage.js +274 -73
- package/dist/config.js +2 -0
- package/dist/main.js +43 -2
- package/dist/pdf/batchValidation.js +72 -0
- package/dist/pdf/documentCache.js +68 -0
- package/dist/pdf/quoteLocate.js +181 -0
- package/package.json +8 -2
- package/skills/memoring-cli/SKILL.md +58 -7
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { apiFetch } from '../api.js';
|
|
2
|
+
import { fail, green, printJson, printTable, stringFlag } from './shared.js';
|
|
3
|
+
import { formatPercent, integerFlag, object, positiveId, readJson } from './plan-shared.js';
|
|
4
|
+
function examBody(parsed) {
|
|
5
|
+
const file = stringFlag(parsed.flags, 'file');
|
|
6
|
+
if (file) {
|
|
7
|
+
return object(readJson(file), 'a prova deve ser um objeto JSON.');
|
|
8
|
+
}
|
|
9
|
+
const name = stringFlag(parsed.flags, 'name');
|
|
10
|
+
const examDate = stringFlag(parsed.flags, 'date');
|
|
11
|
+
if (!name || !examDate) {
|
|
12
|
+
fail('informe --file prova.json ou --name, --date, --total e --correct.');
|
|
13
|
+
}
|
|
14
|
+
const planId = integerFlag(parsed.flags, 'plan-id', { min: 1 });
|
|
15
|
+
const totalQuestions = integerFlag(parsed.flags, 'total', { required: true, min: 1 });
|
|
16
|
+
const correctAnswers = integerFlag(parsed.flags, 'correct', { required: true, min: 0 });
|
|
17
|
+
const subjectsRaw = stringFlag(parsed.flags, 'subjects');
|
|
18
|
+
let subjectResults;
|
|
19
|
+
if (subjectsRaw !== undefined) {
|
|
20
|
+
try {
|
|
21
|
+
subjectResults = JSON.parse(subjectsRaw);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
fail(`--subjects deve ser um array JSON válido: ${error instanceof Error ? error.message : String(error)}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
...(planId !== undefined ? { planId } : {}),
|
|
29
|
+
name,
|
|
30
|
+
examDate,
|
|
31
|
+
totalQuestions,
|
|
32
|
+
correctAnswers,
|
|
33
|
+
...(subjectResults !== undefined ? { subjectResults } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function validateExam(value) {
|
|
37
|
+
if (typeof value.name !== 'string' || value.name.trim() === '') {
|
|
38
|
+
fail('a prova precisa de name não vazio.');
|
|
39
|
+
}
|
|
40
|
+
if (typeof value.examDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value.examDate) || Number.isNaN(Date.parse(`${value.examDate}T00:00:00Z`))) {
|
|
41
|
+
fail('examDate deve estar no formato YYYY-MM-DD.');
|
|
42
|
+
}
|
|
43
|
+
for (const [field, min] of [['totalQuestions', 1], ['correctAnswers', 0]]) {
|
|
44
|
+
const fieldValue = value[field];
|
|
45
|
+
if (typeof fieldValue !== 'number' || !Number.isInteger(fieldValue) || fieldValue < min) {
|
|
46
|
+
fail(`${field} deve ser um inteiro >= ${min}.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (value.correctAnswers > value.totalQuestions) {
|
|
50
|
+
fail('correctAnswers não pode superar totalQuestions.');
|
|
51
|
+
}
|
|
52
|
+
if (value.planId !== undefined && value.planId !== null &&
|
|
53
|
+
(typeof value.planId !== 'number' || !Number.isInteger(value.planId) || value.planId < 1)) {
|
|
54
|
+
fail('planId deve ser um inteiro positivo.');
|
|
55
|
+
}
|
|
56
|
+
if (value.subjectResults !== undefined && !Array.isArray(value.subjectResults)) {
|
|
57
|
+
fail('subjectResults deve ser um array.');
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
export async function examsCommand(parsed, rest) {
|
|
62
|
+
const [sub, ...args] = rest;
|
|
63
|
+
switch (sub) {
|
|
64
|
+
case 'list': {
|
|
65
|
+
const exams = (await apiFetch('/planning/exams'));
|
|
66
|
+
if (parsed.flags.json) {
|
|
67
|
+
printJson(exams);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (exams.length === 0) {
|
|
71
|
+
process.stdout.write('Nenhuma prova registrada.\n');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
printTable(exams.map((exam) => ({
|
|
75
|
+
id: `#${String(exam.id)}`,
|
|
76
|
+
name: exam.name,
|
|
77
|
+
date: exam.examDate,
|
|
78
|
+
result: `${String(exam.correctAnswers)}/${String(exam.totalQuestions)}`,
|
|
79
|
+
accuracy: formatPercent(exam.accuracyPercent),
|
|
80
|
+
plan: exam.planId ? `#${String(exam.planId)}` : '—',
|
|
81
|
+
})), [
|
|
82
|
+
{ key: 'id', label: 'ID' },
|
|
83
|
+
{ key: 'name', label: 'PROVA' },
|
|
84
|
+
{ key: 'date', label: 'DATA' },
|
|
85
|
+
{ key: 'result', label: 'ACERTOS' },
|
|
86
|
+
{ key: 'accuracy', label: 'DESEMPENHO' },
|
|
87
|
+
{ key: 'plan', label: 'PLANO' },
|
|
88
|
+
]);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
case 'create': {
|
|
92
|
+
const body = validateExam(examBody(parsed));
|
|
93
|
+
const exam = await apiFetch('/planning/exams', { method: 'POST', body });
|
|
94
|
+
if (parsed.flags.json) {
|
|
95
|
+
printJson(exam);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const created = object(exam, 'resposta inválida ao criar prova.');
|
|
99
|
+
process.stdout.write(green(`Prova "${String(created.name)}" registrada (#${String(created.id)}).\n`));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
case 'delete': {
|
|
103
|
+
const examId = positiveId(args[0], 'examId');
|
|
104
|
+
await apiFetch(`/planning/exams/${examId}`, { method: 'DELETE' });
|
|
105
|
+
if (parsed.flags.json) {
|
|
106
|
+
printJson({ deleted: true, examId });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
process.stdout.write(green(`Prova #${examId} excluída.\n`));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
default:
|
|
113
|
+
process.stdout.write('Uso: memoring plan exams <list|create|delete>\n' +
|
|
114
|
+
' list\n' +
|
|
115
|
+
' create --file prova.json\n' +
|
|
116
|
+
' create --name "Simulado 1" --date YYYY-MM-DD --total N --correct N [--plan-id N] [--subjects JSON]\n' +
|
|
117
|
+
' delete <examId>\n');
|
|
118
|
+
if (sub) {
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { apiFetch } from '../api.js';
|
|
2
|
+
import { createDraftHandoff, deleteDraftAfterCreation, readPlanningDraft, requireReviewed, } from './planDrafts.js';
|
|
3
|
+
import { fail, printJson, stringFlag } from './shared.js';
|
|
4
|
+
import { formatPercent, integerFlag, normalized, printActivity, printGoal, } from './plan-shared.js';
|
|
5
|
+
import { ERROR_REASONS } from './plan-analyze.js';
|
|
6
|
+
export async function goalCommand(sub, planId, parsed) {
|
|
7
|
+
let details;
|
|
8
|
+
if (sub === 'current') {
|
|
9
|
+
details = (await apiFetch(`/planning/plans/${planId}/goals/current`));
|
|
10
|
+
}
|
|
11
|
+
else if (parsed.flags.web) {
|
|
12
|
+
if (parsed.flags.draft !== undefined || parsed.flags.activities !== undefined) {
|
|
13
|
+
fail('next --web não aceita --draft nem --activities; a Meta será definida na revisão web.');
|
|
14
|
+
}
|
|
15
|
+
await createDraftHandoff('GOAL', planId, {}, parsed);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
let reviewedDraft = null;
|
|
20
|
+
let activityTarget;
|
|
21
|
+
if (parsed.flags.draft !== undefined) {
|
|
22
|
+
if (parsed.flags.activities !== undefined) {
|
|
23
|
+
fail('use --draft ou --activities, não os dois.');
|
|
24
|
+
}
|
|
25
|
+
reviewedDraft = await readPlanningDraft(stringFlag(parsed.flags, 'draft'));
|
|
26
|
+
requireReviewed(reviewedDraft, 'GOAL', planId);
|
|
27
|
+
const value = reviewedDraft.payload.activityTarget;
|
|
28
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1 || value > 200) {
|
|
29
|
+
fail('o payload revisado precisa de activityTarget inteiro entre 1 e 200.');
|
|
30
|
+
}
|
|
31
|
+
activityTarget = value;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
activityTarget = integerFlag(parsed.flags, 'activities', { required: true, min: 1, max: 200 });
|
|
35
|
+
}
|
|
36
|
+
details = (await apiFetch(`/planning/plans/${planId}/goals`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
body: { activityTarget },
|
|
39
|
+
}));
|
|
40
|
+
if (reviewedDraft) {
|
|
41
|
+
await deleteDraftAfterCreation(reviewedDraft);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (parsed.flags.json) {
|
|
45
|
+
printJson(details);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!details) {
|
|
49
|
+
process.stdout.write(`Nenhuma Meta ativa no planejamento #${planId}. Gere uma com \`memoring plan next ${planId} --activities N\`.\n`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
printGoal(details, sub === 'next' ? 'Meta gerada' : 'Meta atual');
|
|
53
|
+
}
|
|
54
|
+
function errorReason(raw) {
|
|
55
|
+
const key = normalized(raw);
|
|
56
|
+
const aliases = {
|
|
57
|
+
UNKNOWN: 'UNKNOWN',
|
|
58
|
+
DESCONHECIDO: 'UNKNOWN',
|
|
59
|
+
NAO_IDENTIFICADO: 'UNKNOWN',
|
|
60
|
+
FORGETTING: 'FORGETTING',
|
|
61
|
+
ESQUECIMENTO: 'FORGETTING',
|
|
62
|
+
CONCEPTUAL_CONFUSION: 'CONCEPTUAL_CONFUSION',
|
|
63
|
+
CONFUSAO_CONCEITUAL: 'CONCEPTUAL_CONFUSION',
|
|
64
|
+
INATTENTION: 'INATTENTION',
|
|
65
|
+
DESATENCAO: 'INATTENTION',
|
|
66
|
+
INTERPRETATION: 'INTERPRETATION',
|
|
67
|
+
INTERPRETACAO: 'INTERPRETATION',
|
|
68
|
+
TIME_SHORTAGE: 'TIME_SHORTAGE',
|
|
69
|
+
FALTA_DE_TEMPO: 'TIME_SHORTAGE',
|
|
70
|
+
};
|
|
71
|
+
const reason = aliases[key];
|
|
72
|
+
if (!reason) {
|
|
73
|
+
fail(`motivo de erro desconhecido: ${raw}. Use ${ERROR_REASONS.join(', ')}.`);
|
|
74
|
+
}
|
|
75
|
+
return reason;
|
|
76
|
+
}
|
|
77
|
+
function parseErrors(raw, incorrectCount) {
|
|
78
|
+
if (raw === undefined) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const entries = raw.split(/[,;]/).map((entry) => entry.trim()).filter(Boolean);
|
|
82
|
+
if (entries.length === 0) {
|
|
83
|
+
fail('--errors vazio; use motivo:N separado por vírgulas.');
|
|
84
|
+
}
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
const errors = entries.map((entry) => {
|
|
87
|
+
const match = /^(.+):(\d+)$/.exec(entry);
|
|
88
|
+
if (!match) {
|
|
89
|
+
fail(`--errors inválido em ${JSON.stringify(entry)}; use motivo:N (ex.: FORGETTING:2).`);
|
|
90
|
+
}
|
|
91
|
+
const reason = errorReason(match[1]);
|
|
92
|
+
const errorCount = Number(match[2]);
|
|
93
|
+
if (errorCount < 1) {
|
|
94
|
+
fail(`a contagem de ${reason} deve ser pelo menos 1.`);
|
|
95
|
+
}
|
|
96
|
+
if (seen.has(reason)) {
|
|
97
|
+
fail(`o motivo ${reason} aparece mais de uma vez em --errors.`);
|
|
98
|
+
}
|
|
99
|
+
seen.add(reason);
|
|
100
|
+
return { reason, errorCount };
|
|
101
|
+
});
|
|
102
|
+
const classified = errors.reduce((sum, error) => sum + error.errorCount, 0);
|
|
103
|
+
if (classified > incorrectCount) {
|
|
104
|
+
fail(`a soma de --errors (${classified}) não pode superar os ${incorrectCount} erros da atividade.`);
|
|
105
|
+
}
|
|
106
|
+
return errors;
|
|
107
|
+
}
|
|
108
|
+
export async function recordResult(activityId, parsed) {
|
|
109
|
+
const questionCount = integerFlag(parsed.flags, 'total', { required: true, min: 1 });
|
|
110
|
+
const correctCount = integerFlag(parsed.flags, 'correct', { required: true, min: 0 });
|
|
111
|
+
const actualSeconds = integerFlag(parsed.flags, 'seconds', { min: 0 });
|
|
112
|
+
if (correctCount > questionCount) {
|
|
113
|
+
fail('--correct não pode superar --total.');
|
|
114
|
+
}
|
|
115
|
+
const errors = parseErrors(stringFlag(parsed.flags, 'errors'), questionCount - correctCount);
|
|
116
|
+
const activity = (await apiFetch(`/planning/activities/${activityId}/result`, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
body: {
|
|
119
|
+
questionCount,
|
|
120
|
+
correctCount,
|
|
121
|
+
...(actualSeconds !== undefined ? { actualSeconds } : {}),
|
|
122
|
+
...(errors !== undefined ? { errors } : {}),
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
if (parsed.flags.json) {
|
|
126
|
+
printJson(activity);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
printActivity(activity, 'Resultado registrado');
|
|
130
|
+
process.stdout.write(` ${correctCount}/${questionCount} corretas (${formatPercent((100 * correctCount) / questionCount)}).\n`);
|
|
131
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { dim, fail, green, printTable, stringFlag } from './shared.js';
|
|
4
|
+
export const STATUS_LABELS = {
|
|
5
|
+
ACTIVE: 'Ativo',
|
|
6
|
+
ARCHIVED: 'Arquivado',
|
|
7
|
+
COMPLETED: 'Concluída',
|
|
8
|
+
PENDING: 'Pendente',
|
|
9
|
+
IN_PROGRESS: 'Em andamento',
|
|
10
|
+
POSTPONED: 'Adiada',
|
|
11
|
+
DISMISSED: 'Dispensada',
|
|
12
|
+
CANCELLED: 'Cancelada',
|
|
13
|
+
};
|
|
14
|
+
// PlanStatus da web usa COMPLETED no masculino ("planejamento concluído");
|
|
15
|
+
// STATUS_LABELS acima atende atividades/Metas ("concluída").
|
|
16
|
+
export const PLAN_STATUS_LABELS = {
|
|
17
|
+
...STATUS_LABELS,
|
|
18
|
+
COMPLETED: 'Concluído',
|
|
19
|
+
};
|
|
20
|
+
export const TYPE_LABELS = {
|
|
21
|
+
THEORY: 'Teoria',
|
|
22
|
+
MAPPING: 'Mapeamento',
|
|
23
|
+
REVIEW: 'Revisão',
|
|
24
|
+
QUESTIONS: 'Questões',
|
|
25
|
+
TEST: 'Teste',
|
|
26
|
+
REINFORCEMENT: 'Reforço',
|
|
27
|
+
};
|
|
28
|
+
export const MATURITY_LABELS = {
|
|
29
|
+
BEGINNER: 'Iniciante',
|
|
30
|
+
INTERMEDIATE: 'Intermediário',
|
|
31
|
+
ADVANCED: 'Avançado',
|
|
32
|
+
};
|
|
33
|
+
export const ERROR_LABELS = {
|
|
34
|
+
UNKNOWN: 'Não identificado',
|
|
35
|
+
FORGETTING: 'Esquecimento',
|
|
36
|
+
CONCEPTUAL_CONFUSION: 'Confusão conceitual',
|
|
37
|
+
INATTENTION: 'Desatenção',
|
|
38
|
+
INTERPRETATION: 'Interpretação',
|
|
39
|
+
TIME_SHORTAGE: 'Falta de tempo',
|
|
40
|
+
};
|
|
41
|
+
export function object(value, message) {
|
|
42
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
43
|
+
fail(message);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
export function readJson(file) {
|
|
48
|
+
try {
|
|
49
|
+
const raw = file === '-' ? readFileSync(0, 'utf8') : readFileSync(resolve(file), 'utf8');
|
|
50
|
+
return JSON.parse(raw);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
fail(`não foi possível ler JSON de ${file === '-' ? 'stdin' : file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function positiveId(raw, label) {
|
|
57
|
+
if (raw === undefined || !/^\d+$/.test(raw) || Number(raw) < 1 || !Number.isSafeInteger(Number(raw))) {
|
|
58
|
+
fail(`${label} deve ser um inteiro positivo.`);
|
|
59
|
+
}
|
|
60
|
+
return Number(raw);
|
|
61
|
+
}
|
|
62
|
+
export function integerFlag(flags, name, options) {
|
|
63
|
+
const raw = stringFlag(flags, name);
|
|
64
|
+
if (raw === undefined) {
|
|
65
|
+
if (options.required) {
|
|
66
|
+
fail(`--${name} é obrigatório.`);
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
if (!/^-?\d+$/.test(raw)) {
|
|
71
|
+
fail(`--${name} deve ser um inteiro.`);
|
|
72
|
+
}
|
|
73
|
+
const value = Number(raw);
|
|
74
|
+
if (!Number.isSafeInteger(value) || value < options.min || (options.max !== undefined && value > options.max)) {
|
|
75
|
+
fail(`--${name} deve estar entre ${options.min} e ${options.max ?? '∞'}.`);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
export function normalized(value) {
|
|
80
|
+
return value
|
|
81
|
+
.normalize('NFD')
|
|
82
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
83
|
+
.trim()
|
|
84
|
+
.replace(/[\s-]+/g, '_')
|
|
85
|
+
.toUpperCase();
|
|
86
|
+
}
|
|
87
|
+
export function formatPercent(value) {
|
|
88
|
+
return typeof value === 'number' ? `${value.toFixed(2).replace('.', ',')}%` : '—';
|
|
89
|
+
}
|
|
90
|
+
export function formatSeconds(value) {
|
|
91
|
+
if (typeof value !== 'number') {
|
|
92
|
+
return '—';
|
|
93
|
+
}
|
|
94
|
+
if (value < 60) {
|
|
95
|
+
return `${Math.round(value)}s`;
|
|
96
|
+
}
|
|
97
|
+
const minutes = Math.floor(value / 60);
|
|
98
|
+
const seconds = Math.round(value % 60);
|
|
99
|
+
return minutes < 60 ? `${minutes}min${seconds ? ` ${seconds}s` : ''}` : `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}`;
|
|
100
|
+
}
|
|
101
|
+
export function printActivity(activity, heading = 'Atividade atualizada') {
|
|
102
|
+
process.stdout.write(green(`${heading}: #${activity.id} — ${activity.title}\n`));
|
|
103
|
+
process.stdout.write(` tipo: ${TYPE_LABELS[activity.type] ?? activity.type} status: ${STATUS_LABELS[activity.status] ?? activity.status} estimativa: ${activity.estimatedMinutes}min\n`);
|
|
104
|
+
if (activity.command) {
|
|
105
|
+
process.stdout.write(` ${activity.command}\n`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function printGoal(details, heading) {
|
|
109
|
+
process.stdout.write(green(`${heading}: Meta #${details.goal.number} (id ${details.goal.id}) — alvo de ${details.goal.activityTarget} atividades.\n`));
|
|
110
|
+
if (details.activities.length === 0) {
|
|
111
|
+
process.stdout.write('Nenhuma atividade nesta Meta.\n');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
printTable(details.activities.map((activity) => ({
|
|
115
|
+
id: `#${activity.id}`,
|
|
116
|
+
type: TYPE_LABELS[activity.type] ?? activity.type,
|
|
117
|
+
title: activity.title,
|
|
118
|
+
status: STATUS_LABELS[activity.status] ?? activity.status,
|
|
119
|
+
minutes: `${activity.estimatedMinutes}min`,
|
|
120
|
+
questions: activity.questionTarget ?? activity.questionCount ?? '—',
|
|
121
|
+
})), [
|
|
122
|
+
{ key: 'id', label: 'ID' },
|
|
123
|
+
{ key: 'type', label: 'TIPO' },
|
|
124
|
+
{ key: 'title', label: 'ATIVIDADE' },
|
|
125
|
+
{ key: 'status', label: 'STATUS' },
|
|
126
|
+
{ key: 'minutes', label: 'ESTIMATIVA' },
|
|
127
|
+
{ key: 'questions', label: 'QUESTÕES' },
|
|
128
|
+
]);
|
|
129
|
+
const first = details.activities.find((activity) => activity.status === 'PENDING' || activity.status === 'IN_PROGRESS');
|
|
130
|
+
if (first) {
|
|
131
|
+
process.stdout.write(dim(`\nPróxima: memoring plan start ${first.id}\n`));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { apiFetch } from '../api.js';
|
|
2
|
+
import { deleteDraftAfterCreation, readPlanningDraft, requireReviewed, reviewDraft, } from './planDrafts.js';
|
|
3
|
+
import { PLAN_SCHEMA, analyze, planBody, planPayload } from './plan-analyze.js';
|
|
4
|
+
import { diagnostics } from './plan-diagnostics.js';
|
|
5
|
+
import { examsCommand } from './plan-exams.js';
|
|
6
|
+
import { goalCommand, recordResult } from './plan-goal.js';
|
|
7
|
+
import { MATURITY_LABELS, PLAN_STATUS_LABELS, STATUS_LABELS, formatPercent, object, positiveId, printActivity, } from './plan-shared.js';
|
|
8
|
+
import { bold, fail, green, printJson, printTable, stringFlag } from './shared.js';
|
|
9
|
+
/**
|
|
10
|
+
* `memoring plan` — dispatcher dos subcomandos do Planejamento. O módulo
|
|
11
|
+
* ficou grande demais como arquivo único; cada família coesa vive no seu
|
|
12
|
+
* próprio módulo: `plan-analyze` (schema/analyze/create payload), `plan-goal`
|
|
13
|
+
* (current/next/start/complete/postpone/dismiss/cancel/result), `plan-exams`,
|
|
14
|
+
* `plan-diagnostics` e `plan-shared` (rótulos, parsers e impressão).
|
|
15
|
+
*/
|
|
16
|
+
function printHelp() {
|
|
17
|
+
process.stdout.write(`Uso: memoring plan <subcomando> [argumentos] [flags]
|
|
18
|
+
|
|
19
|
+
schema contrato JSON editável do planejamento e das provas
|
|
20
|
+
analyze <pdfs...> analisa PDFs no backend e aguarda a proposta
|
|
21
|
+
[--out proposta.json] salva o rascunho (com --json, imprime só o rascunho)
|
|
22
|
+
[--web] [--no-open] envia a proposta para revisão web, sem criar o plano
|
|
23
|
+
review <draftId> [--wait] lê o rascunho; --wait aguarda REVIEWED
|
|
24
|
+
[--timeout N] [--out f] timeout em segundos; exporta o payload já REVIEWED
|
|
25
|
+
create --file proposta.json cria o planejamento após sua revisão no arquivo
|
|
26
|
+
create --draft <uuid> cria somente de um rascunho PLAN REVIEWED e o exclui
|
|
27
|
+
list lista planejamentos
|
|
28
|
+
show <planId> detalha disciplinas e assuntos
|
|
29
|
+
current <planId> mostra a Meta ativa
|
|
30
|
+
next <planId> --activities N gera a próxima Meta (activityTarget obrigatório)
|
|
31
|
+
next <planId> --web cria handoff de Meta para revisão web, sem gerar a Meta
|
|
32
|
+
next <planId> --draft <uuid> gera de um rascunho GOAL REVIEWED e o exclui
|
|
33
|
+
start|complete|postpone|dismiss|cancel <activityId>
|
|
34
|
+
transições de uma atividade
|
|
35
|
+
(cancel = redução definitiva de escopo:
|
|
36
|
+
encerra na Meta e nunca é recomposta)
|
|
37
|
+
result <activityId> --total N --correct N [--seconds N]
|
|
38
|
+
[--errors motivo:N,...] registra desempenho e motivos dos erros
|
|
39
|
+
tec-result <activityId> importa resultado do caderno TEC já disponível
|
|
40
|
+
performance|error-patterns|comparison|journey <planId>
|
|
41
|
+
diagnósticos do planejamento
|
|
42
|
+
coordinates coordenadas do método de planejamento
|
|
43
|
+
exams list lista provas
|
|
44
|
+
exams create --file prova.json registra prova (ou use --name/--date/--total/--correct)
|
|
45
|
+
exams delete <examId> exclui prova
|
|
46
|
+
|
|
47
|
+
Maturidades: BEGINNER (Iniciante), INTERMEDIATE (Intermediário), ADVANCED (Avançado).
|
|
48
|
+
Motivos de erro: UNKNOWN, FORGETTING, CONCEPTUAL_CONFUSION, INATTENTION, INTERPRETATION, TIME_SHORTAGE (aliases em PT-BR também são aceitos).
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
export async function planCommand(parsed) {
|
|
52
|
+
const [sub, ...rest] = parsed._;
|
|
53
|
+
switch (sub) {
|
|
54
|
+
case 'schema':
|
|
55
|
+
printJson(PLAN_SCHEMA);
|
|
56
|
+
return;
|
|
57
|
+
case 'analyze':
|
|
58
|
+
await analyze(parsed, rest);
|
|
59
|
+
return;
|
|
60
|
+
case 'review':
|
|
61
|
+
case 'draft':
|
|
62
|
+
await reviewDraft(parsed, rest[0]);
|
|
63
|
+
return;
|
|
64
|
+
case 'create': {
|
|
65
|
+
const file = stringFlag(parsed.flags, 'file');
|
|
66
|
+
if (file && parsed.flags.draft !== undefined) {
|
|
67
|
+
fail('use --file ou --draft, não os dois.');
|
|
68
|
+
}
|
|
69
|
+
let body;
|
|
70
|
+
let reviewedDraft = null;
|
|
71
|
+
if (parsed.flags.draft !== undefined) {
|
|
72
|
+
reviewedDraft = await readPlanningDraft(stringFlag(parsed.flags, 'draft'));
|
|
73
|
+
requireReviewed(reviewedDraft, 'PLAN');
|
|
74
|
+
body = planPayload(reviewedDraft.payload);
|
|
75
|
+
}
|
|
76
|
+
else if (file) {
|
|
77
|
+
body = planBody(file);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
fail('informe a proposta revisada: memoring plan create --file proposta.json ou --draft <uuid>.');
|
|
81
|
+
}
|
|
82
|
+
const details = (await apiFetch('/planning/plans', { method: 'POST', body }));
|
|
83
|
+
if (reviewedDraft) {
|
|
84
|
+
await deleteDraftAfterCreation(reviewedDraft);
|
|
85
|
+
}
|
|
86
|
+
if (parsed.flags.json) {
|
|
87
|
+
printJson(details);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const plan = object(details.plan, 'resposta inválida ao criar planejamento.');
|
|
91
|
+
process.stdout.write(green(`Planejamento "${String(plan.name)}" criado (#${String(plan.id)}).\n`));
|
|
92
|
+
process.stdout.write(`Gere a primeira Meta: memoring plan next ${String(plan.id)} --activities N\n`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
case 'list': {
|
|
96
|
+
const plans = (await apiFetch('/planning/plans'));
|
|
97
|
+
if (parsed.flags.json) {
|
|
98
|
+
printJson(plans);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (plans.length === 0) {
|
|
102
|
+
process.stdout.write('Nenhum planejamento. Analise PDFs com `memoring plan analyze <pdfs...>`.\n');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
printTable(plans.map((plan) => ({
|
|
106
|
+
id: `#${String(plan.id)}`,
|
|
107
|
+
name: plan.name,
|
|
108
|
+
status: PLAN_STATUS_LABELS[String(plan.status)] ?? plan.status,
|
|
109
|
+
subjects: plan.activeSubjectLimit,
|
|
110
|
+
weekly: plan.weeklyMinutes ? `${String(plan.weeklyMinutes)}min` : '—',
|
|
111
|
+
})), [
|
|
112
|
+
{ key: 'id', label: 'ID' },
|
|
113
|
+
{ key: 'name', label: 'PLANEJAMENTO' },
|
|
114
|
+
{ key: 'status', label: 'STATUS' },
|
|
115
|
+
{ key: 'subjects', label: 'DISCIPLINAS ATIVAS' },
|
|
116
|
+
{ key: 'weekly', label: 'TEMPO SEMANAL' },
|
|
117
|
+
]);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
case 'show': {
|
|
121
|
+
const planId = positiveId(rest[0], 'planId');
|
|
122
|
+
const details = (await apiFetch(`/planning/plans/${planId}`));
|
|
123
|
+
if (parsed.flags.json) {
|
|
124
|
+
printJson(details);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const plan = object(details.plan, 'resposta inválida ao consultar planejamento.');
|
|
128
|
+
process.stdout.write(bold(`${String(plan.name)} (#${planId}) — ${PLAN_STATUS_LABELS[String(plan.status)] ?? String(plan.status)}\n`));
|
|
129
|
+
if (plan.objective) {
|
|
130
|
+
process.stdout.write(`${String(plan.objective)}\n`);
|
|
131
|
+
}
|
|
132
|
+
const programs = Array.isArray(details.programs) ? details.programs : [];
|
|
133
|
+
const topics = Array.isArray(details.topics) ? details.topics : [];
|
|
134
|
+
for (const program of programs) {
|
|
135
|
+
process.stdout.write(`\n${String(program.subjectName)} — ${MATURITY_LABELS[String(program.maturity)] ?? String(program.maturity)}\n`);
|
|
136
|
+
const programTopics = topics.filter((topic) => topic.programId === program.id);
|
|
137
|
+
printTable(programTopics.map((topic) => ({
|
|
138
|
+
id: `#${String(topic.id)}`,
|
|
139
|
+
title: topic.title,
|
|
140
|
+
pages: topic.pageFrom ? `${String(topic.pageFrom)}${topic.pageTo !== topic.pageFrom ? `–${String(topic.pageTo)}` : ''}` : '—',
|
|
141
|
+
minutes: `${String(topic.estimatedMinutes)}min`,
|
|
142
|
+
status: STATUS_LABELS[String(topic.status)] ?? topic.status,
|
|
143
|
+
})), [
|
|
144
|
+
{ key: 'id', label: 'ID' },
|
|
145
|
+
{ key: 'title', label: 'ASSUNTO' },
|
|
146
|
+
{ key: 'pages', label: 'PÁGINAS' },
|
|
147
|
+
{ key: 'minutes', label: 'ESTIMATIVA' },
|
|
148
|
+
{ key: 'status', label: 'STATUS' },
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
case 'current':
|
|
154
|
+
case 'next':
|
|
155
|
+
await goalCommand(sub, positiveId(rest[0], 'planId'), parsed);
|
|
156
|
+
return;
|
|
157
|
+
case 'start':
|
|
158
|
+
case 'complete':
|
|
159
|
+
case 'postpone':
|
|
160
|
+
case 'dismiss':
|
|
161
|
+
case 'cancel': {
|
|
162
|
+
const activityId = positiveId(rest[0], 'activityId');
|
|
163
|
+
const activity = (await apiFetch(`/planning/activities/${activityId}/${sub}`, { method: 'POST' }));
|
|
164
|
+
if (parsed.flags.json) {
|
|
165
|
+
printJson(activity);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
printActivity(activity);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
case 'result':
|
|
173
|
+
await recordResult(positiveId(rest[0], 'activityId'), parsed);
|
|
174
|
+
return;
|
|
175
|
+
case 'tec-result': {
|
|
176
|
+
const activityId = positiveId(rest[0], 'activityId');
|
|
177
|
+
const result = (await apiFetch(`/planning/activities/${activityId}/tec-result`, { method: 'POST' }));
|
|
178
|
+
if (parsed.flags.json) {
|
|
179
|
+
printJson(result);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
process.stdout.write(green(`Resultado TEC importado para a atividade #${activityId}.\n`));
|
|
183
|
+
process.stdout.write(` ${String(result.correctCount)}/${String(result.questionCount)} corretas (${formatPercent(result.accuracyPercent)}); ` +
|
|
184
|
+
`${String(result.errorCount)} erros.\n`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
case 'performance':
|
|
188
|
+
case 'error-patterns':
|
|
189
|
+
case 'comparison':
|
|
190
|
+
case 'journey':
|
|
191
|
+
await diagnostics(sub, positiveId(rest[0], 'planId'), parsed.flags.json === true);
|
|
192
|
+
return;
|
|
193
|
+
case 'coordinates': {
|
|
194
|
+
const guides = (await apiFetch('/planning/coordinates'));
|
|
195
|
+
if (parsed.flags.json) {
|
|
196
|
+
printJson(guides);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
for (const guide of guides) {
|
|
200
|
+
process.stdout.write(bold(`${String(guide.title)}\n`));
|
|
201
|
+
process.stdout.write(`${String(guide.description)}\n`);
|
|
202
|
+
const actions = Array.isArray(guide.actions) ? guide.actions : [];
|
|
203
|
+
for (const action of actions) {
|
|
204
|
+
process.stdout.write(` • ${String(action)}\n`);
|
|
205
|
+
}
|
|
206
|
+
process.stdout.write('\n');
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
case 'exams':
|
|
211
|
+
await examsCommand(parsed, rest);
|
|
212
|
+
return;
|
|
213
|
+
default:
|
|
214
|
+
printHelp();
|
|
215
|
+
if (sub) {
|
|
216
|
+
process.exitCode = 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|