Skip to content
52 changes: 21 additions & 31 deletions src/cli/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
* Interactive learning dashboard (Phase 1: text summary only)
*/

import fs from 'fs';
import path from 'path';
import { walkVault } from '../storage/vault-walker';
import { parseFrontmatter } from '../storage/frontmatter';
import { loadTopics } from '../storage/loader';
import { MASTERY_THRESHOLD } from '../engine/mastery';

import { loadConfig } from './config';
import { isJsonOutput, printEmptyVaultOnboarding, validateVaultPath } from './onboarding';
import { Difficulty, DashboardOptions, normalizeDifficulty } from '../types';
import { Difficulty, DashboardOptions } from '../types';


interface DashboardTopic {
id: string;
Expand All @@ -29,34 +28,25 @@ async function dashboardCommand(options: DashboardOptions = {}): Promise<void> {
const vaultPath = validateVaultPath(config.vaultPath, { json: jsonMode });
if (!vaultPath) return;

const files = walkVault(vaultPath);
const loaded = loadTopics(vaultPath);
const now = new Date();

// Load all topics sequentially
const topics: DashboardTopic[] = [];
for (const filePath of files) {
try {
const content = fs.readFileSync(filePath, 'utf8');
const { frontmatter } = parseFrontmatter(content);

if (frontmatter && frontmatter.palee_id) {
let dueAt = frontmatter.due_at ? new Date(frontmatter.due_at as string) : null;
if (dueAt && Number.isNaN(dueAt.getTime())) {
dueAt = null;
}

topics.push({
id: frontmatter.palee_id as string,
title: (frontmatter.title as string) || path.basename(filePath, '.md'),
mastery: (frontmatter.topic_mastery as number) || 0,
repetition: (frontmatter.repetition as number) || 0,
lapses: (frontmatter.lapses as number) || 0,
difficulty: normalizeDifficulty(frontmatter.difficulty),
due_at: dueAt,
});
}
} catch {}
}
const topics: DashboardTopic[] = loaded.map((t) => {
let dueAt = t.due_at ? new Date(t.due_at) : null;
if (dueAt && Number.isNaN(dueAt.getTime())) {
dueAt = null;
}
return {
id: t.palee_id,
title: t.title,
mastery: t.topic_mastery,
repetition: t.repetition ?? 0,
lapses: t.lapses ?? 0,
difficulty: t.difficulty ?? 'intermediate',
due_at: dueAt,
};
});


if (topics.length === 0) {
if (jsonMode) {
Expand Down
25 changes: 7 additions & 18 deletions src/cli/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { loadConfig } from './config';
/**
* Migrate Command Handler
* Schema migration (Phase 1: stub only)
*/

import fs from 'fs';
import { walkVault } from '../storage/vault-walker';
import { parseFrontmatter } from '../storage/frontmatter';
import { loadTopics } from '../storage/loader';

async function migrateCommand(): Promise<void> {
try {
Expand All @@ -19,31 +12,27 @@ async function migrateCommand(): Promise<void> {
}

const vaultPath = config.vaultPath;
const files = walkVault(vaultPath);
const loaded = loadTopics(vaultPath);

console.log('Scanning vault for PALEE schema versions...');
console.log();

let schemaV1 = 0;
const unrecognized: string[] = [];

for (const filePath of files) {
const content = fs.readFileSync(filePath, 'utf8');
const { frontmatter } = parseFrontmatter(content);

if (!frontmatter || !frontmatter.palee_id) continue;

const schema = frontmatter.palee_schema;
for (const t of loaded) {
const schema = t.frontmatter.palee_schema;

if (schema === 1) {
schemaV1++;
} else if (schema === undefined) {
unrecognized.push(filePath);
unrecognized.push(t.filePath);
} else {
unrecognized.push(`${filePath} (schema: ${schema})`);
unrecognized.push(`${t.filePath} (schema: ${schema})`);
}
}


console.log(`Schema v1: ${schemaV1} notes`);
if (unrecognized.length > 0) {
console.log(`Unrecognized schema: ${unrecognized.length} notes`);
Expand Down
35 changes: 15 additions & 20 deletions src/cli/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ import { isJsonOutput, printEmptyVaultOnboarding, validateVaultPath } from './on
* Shows next topic(s) due for review
*/

import fs from 'fs';
import path from 'path';
import { walkVault } from '../storage/vault-walker';
import { parseFrontmatter } from '../storage/frontmatter';
import { NextOptions } from '../types';
import { loadTopics } from '../storage/loader';
import { Difficulty, NextOptions } from '../types';


interface DueTopic {
id: string;
Expand All @@ -18,6 +16,7 @@ interface DueTopic {
dueAt: Date | null;
mastery: number;
repetition: number;
difficulty?: Difficulty;
}

async function nextCommand(options: NextOptions = {}): Promise<void> {
Expand All @@ -27,36 +26,32 @@ async function nextCommand(options: NextOptions = {}): Promise<void> {
const vaultPath = validateVaultPath(config.vaultPath, { json: jsonMode });
if (!vaultPath) return;

const files = walkVault(vaultPath);
const topics = loadTopics(vaultPath);
const totalTopics = topics.length;
const dueTopics: DueTopic[] = [];
let totalTopics = 0;
const now = new Date();

for (const filePath of files) {
const content = fs.readFileSync(filePath, 'utf8');
const { frontmatter } = parseFrontmatter(content);

if (!frontmatter || !frontmatter.palee_id) continue;
totalTopics++;

let dueAt = frontmatter.due_at ? new Date(frontmatter.due_at as string) : null;
for (const t of topics) {
let dueAt = t.due_at ? new Date(t.due_at) : null;
if (dueAt && Number.isNaN(dueAt.getTime())) {
dueAt = null; // Treat invalid dates as null (due immediately)
}

// Topics without due_at (or with invalid dates) are new and always ready
if (!dueAt || dueAt <= now) {
dueTopics.push({
id: frontmatter.palee_id as string,
title: (frontmatter.title as string) || path.basename(filePath, '.md'),
path: path.relative(vaultPath, filePath),
id: t.palee_id,
title: t.title,
path: t.path,
dueAt: dueAt,
mastery: (frontmatter.topic_mastery as number) || 0,
repetition: (frontmatter.repetition as number) || 0,
mastery: t.topic_mastery,
repetition: t.repetition ?? 0,
difficulty: t.difficulty,
});
}
}


if (totalTopics === 0) {
if (jsonMode) {
if (options.all) {
Expand Down
37 changes: 15 additions & 22 deletions src/cli/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ import { isJsonOutput, printEmptyVaultOnboarding, validateVaultPath } from './on
* Shows learning plan for the day
*/

import fs from 'fs';
import path from 'path';
import { walkVault } from '../storage/vault-walker';
import { parseFrontmatter } from '../storage/frontmatter';
import { loadTopics } from '../storage/loader';
import { getReadyTopics } from '../engine/dependency';
import { MASTERY_THRESHOLD } from '../engine/mastery';
import { Difficulty, normalizeDifficulty, PlanOptions, TopicNode } from '../types';
import { Difficulty, PlanOptions, TopicNode } from '../types';




Expand All @@ -31,34 +29,29 @@ async function planCommand(options: PlanOptions = {}): Promise<void> {
const vaultPath = validateVaultPath(config.vaultPath, { json: jsonMode });
if (!vaultPath) return;

const files = walkVault(vaultPath);
const loaded = loadTopics(vaultPath);
const topics = new Map<string, PlanTopic>();
const now = new Date();

for (const filePath of files) {
const content = fs.readFileSync(filePath, 'utf8');
const { frontmatter } = parseFrontmatter(content);

if (!frontmatter || !frontmatter.palee_id) continue;

const id = frontmatter.palee_id as string;
let dueAt = frontmatter.due_at ? new Date(frontmatter.due_at as string) : null;
for (const t of loaded) {
let dueAt = t.due_at ? new Date(t.due_at) : null;
if (dueAt && Number.isNaN(dueAt.getTime())) {
dueAt = null;
}

topics.set(id, {
palee_id: id,
title: (frontmatter.title as string) || path.basename(filePath, '.md'),
path: path.relative(vaultPath, filePath),
topic_mastery: (frontmatter.topic_mastery as number) || 0,
depends_on: (frontmatter.depends_on as string[]) || [],
topics.set(t.palee_id, {
palee_id: t.palee_id,
title: t.title,
path: t.path,
topic_mastery: t.topic_mastery,
depends_on: t.depends_on,
due_at: dueAt,
repetition: (frontmatter.repetition as number) || 0,
difficulty: normalizeDifficulty(frontmatter.difficulty),
repetition: t.repetition ?? 0,
difficulty: t.difficulty ?? 'intermediate',
});
}


const dueTopics: PlanTopic[] = [];
for (const topic of topics.values()) {
if (!topic.due_at || topic.due_at <= now) {
Expand Down
45 changes: 18 additions & 27 deletions src/cli/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ import { isJsonOutput, printEmptyVaultOnboarding, validateVaultPath } from './on
* Shows learning progress summary
*/

import fs from 'fs';
import path from 'path';
import { walkVault } from '../storage/vault-walker';
import { parseFrontmatter } from '../storage/frontmatter';
import { loadTopics } from '../storage/loader';
import { MASTERY_THRESHOLD } from '../engine/mastery';
import { Difficulty, normalizeDifficulty, ProgressOptions } from '../types';
import { Difficulty, ProgressOptions } from '../types';



interface ProgressTopic {
Expand All @@ -33,28 +31,21 @@ async function progressCommand(options: ProgressOptions = {}): Promise<void> {
const vaultPath = validateVaultPath(config.vaultPath, { json: jsonMode });
if (!vaultPath) return;

const files = walkVault(vaultPath);
const topics: ProgressTopic[] = [];

for (const filePath of files) {
const content = fs.readFileSync(filePath, 'utf8');
const { frontmatter } = parseFrontmatter(content);

if (!frontmatter || !frontmatter.palee_id) continue;

topics.push({
id: frontmatter.palee_id as string,
title: (frontmatter.title as string) || path.basename(filePath, '.md'),
path: path.relative(vaultPath, filePath),
status: typeof frontmatter.status === 'string' ? frontmatter.status.trim().toLowerCase() : 'not_started',
mastery: (frontmatter.topic_mastery as number) || 0,
repetition: (frontmatter.repetition as number) || 0,
lapses: (frontmatter.lapses as number) || 0,
assessed_at: (frontmatter.assessed_at as string) || null,
last_reviewed_at: (frontmatter.last_reviewed_at as string) || null,
difficulty: normalizeDifficulty(frontmatter.difficulty),
});
}
const loaded = loadTopics(vaultPath);
const topics: ProgressTopic[] = loaded.map((t) => ({
id: t.palee_id,
title: t.title,
path: t.path,
status: t.status ?? 'not_started',
mastery: t.topic_mastery,
repetition: t.repetition ?? 0,
lapses: t.lapses ?? 0,
assessed_at: t.assessed_at ?? null,
last_reviewed_at: t.last_reviewed_at ?? null,
difficulty: t.difficulty ?? 'intermediate',
}));



if (topics.length === 0 && !options.topic) {
if (jsonMode) {
Expand Down
Loading
Loading