Add own-work tracking, Claude hand-off check, and weekly recall
- Long pastes into Observe/Interpret/Apply are tagged as pasted; notes show Mine / N pasted and an own-words share. Pastes follow text through merges. - The "-> Claude" buttons on the Script stage ask first when a chunk lacks enough of the user's own (non-pasted) words. - Weekly recall card on Home: write a passage from memory, compare with notes, self-rate; recalls are saved on the chunk and synced. - Recall history on Home with an eight-week strip and the full list. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
edc5a65d2b
commit
0c8a632cff
11 files changed
+429
-5
No files matched your search
@@ -0,0 +1,58 @@
|
||||
import { test, expect } from 'vitest';
|
||||
import { pickRecall, WEEK_MS } from './recall.js';
|
||||
|
||||
const NOW = 100 * WEEK_MS;
|
||||
const text = (n) => Array.from({ length: n }, (_, i) => `w${i}`).join(' ');
|
||||
const chunk = (id, extra = {}) => ({ id, startVerse: 1, endVerse: 5, observation: text(40), editedAt: NOW - 2 * WEEK_MS, ...extra });
|
||||
const study = (chunks, id = 'p1') => ({ id, title: 'Romans', translation: 'BSB', chapters: [{ book: 'Romans', bookAbbrev: 'Rom', chapter: 8, chunks }] });
|
||||
|
||||
test('picks a studied chunk that has enough of your own writing', () => {
|
||||
const pick = pickRecall([study([chunk('a')])], NOW);
|
||||
expect(pick).toMatchObject({ projectId: 'p1', projectTitle: 'Romans' });
|
||||
expect(pick.chunk.id).toBe('a');
|
||||
});
|
||||
|
||||
test('skips thin notes, pasted notes and chunks edited this week', () => {
|
||||
const pasted = text(40);
|
||||
const projects = [study([
|
||||
chunk('thin', { observation: text(10) }),
|
||||
chunk('pasted', { observation: pasted, aiPastes: { observation: [pasted] } }),
|
||||
chunk('fresh', { editedAt: NOW - 1000 }),
|
||||
])];
|
||||
expect(pickRecall(projects, NOW)).toBeNull();
|
||||
});
|
||||
|
||||
test('never-recalled chunks come before ones recalled long ago', () => {
|
||||
const old = chunk('old', { recalls: [{ at: NOW - 3 * WEEK_MS, text: 'x', rating: 'solid' }] });
|
||||
expect(pickRecall([study([old, chunk('new')])], NOW).chunk.id).toBe('new');
|
||||
});
|
||||
|
||||
test('one recall a week: nothing is offered after you have done one', () => {
|
||||
const done = chunk('done', { recalls: [{ at: NOW - 1000, text: 'x', rating: 'partly' }] });
|
||||
expect(pickRecall([study([done, chunk('other')])], NOW)).toBeNull();
|
||||
expect(pickRecall([study([done, chunk('other')])], NOW + WEEK_MS)).not.toBeNull();
|
||||
});
|
||||
|
||||
import { recallHistory, recallSummary } from './recall.js';
|
||||
|
||||
test('history lists every recall newest first, with where it came from', () => {
|
||||
const a = chunk('a', { recalls: [{ at: NOW - 3 * WEEK_MS, text: 'old', rating: 'forgot' }, { at: NOW - 1000, text: 'new', rating: 'solid' }] });
|
||||
const h = recallHistory([study([a])]);
|
||||
expect(h.map((x) => x.text)).toEqual(['new', 'old']);
|
||||
expect(h[0]).toMatchObject({ projectTitle: 'Romans', chunkId: 'a' });
|
||||
expect(h[0].reference).toContain('Romans');
|
||||
});
|
||||
|
||||
test('the summary counts ratings and marks each of the last eight weeks by its weakest recall', () => {
|
||||
const history = [
|
||||
{ at: NOW - 1000, rating: 'solid' }, { at: NOW - 2000, rating: 'partly' },
|
||||
{ at: NOW - 2 * WEEK_MS + 1000, rating: 'forgot' },
|
||||
];
|
||||
const { counts, total, recent } = recallSummary(history, NOW);
|
||||
expect(counts).toEqual({ solid: 1, partly: 1, forgot: 1 });
|
||||
expect(total).toBe(3);
|
||||
expect(recent).toHaveLength(8);
|
||||
expect(recent[7].rating).toBe('partly'); // this week: the weaker of solid and partly
|
||||
expect(recent[6].rating).toBe('forgot'); // last week
|
||||
expect(recent[0].rating).toBeNull();
|
||||
});
|
||||
Reference in new issue
Block a user