mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-27 16:49:51 -07:00
fix(stats): parse v3 reading-aware known-word cache in stats server (#149)
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: stats
|
||||||
|
|
||||||
|
- Session known-word counts no longer show 0 everywhere. The stats server's known-word cache parser only understood the v1/v2 cache formats, so after the reading-aware v3 cache upgrade it silently treated the cache as missing; it now flattens v3 note entries into the headword set.
|
||||||
@@ -521,6 +521,44 @@ describe('stats server API routes', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('GET /api/stats/sessions enriches known-word metrics from a v3 reading-aware cache', async () => {
|
||||||
|
await withTempDir(async (dir) => {
|
||||||
|
const cachePath = path.join(dir, 'known-words.json');
|
||||||
|
fs.writeFileSync(
|
||||||
|
cachePath,
|
||||||
|
JSON.stringify({
|
||||||
|
version: 3,
|
||||||
|
refreshedAtMs: 1,
|
||||||
|
scope: 'deck:test',
|
||||||
|
notes: {
|
||||||
|
'101': [{ word: 'する', reading: 'する' }],
|
||||||
|
'102': [{ word: '猫', reading: null }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const app = createStatsApp(
|
||||||
|
createMockTracker({
|
||||||
|
getSessionWordsByLine: async (sessionId: number) =>
|
||||||
|
sessionId === 1
|
||||||
|
? [
|
||||||
|
{ lineIndex: 1, headword: 'する', occurrenceCount: 2 },
|
||||||
|
{ lineIndex: 2, headword: '未知', occurrenceCount: 1 },
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
}),
|
||||||
|
{ knownWordCachePath: cachePath },
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = await app.request('/api/stats/sessions?limit=5');
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
const body = await res.json();
|
||||||
|
const first = body[0];
|
||||||
|
assert.equal(first.knownWordsSeen, 2);
|
||||||
|
assert.equal(first.knownWordRate, 66.7);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('GET /api/stats/sessions/:id/events forwards event type filters to the tracker', async () => {
|
it('GET /api/stats/sessions/:id/events forwards event type filters to the tracker', async () => {
|
||||||
let seenSessionId = 0;
|
let seenSessionId = 0;
|
||||||
let seenLimit = 0;
|
let seenLimit = 0;
|
||||||
|
|||||||
@@ -261,10 +261,25 @@ function loadKnownWordsSet(cachePath: string | undefined): Set<string> | null {
|
|||||||
const raw = JSON.parse(readFileSync(cachePath, 'utf-8')) as {
|
const raw = JSON.parse(readFileSync(cachePath, 'utf-8')) as {
|
||||||
version?: number;
|
version?: number;
|
||||||
words?: string[];
|
words?: string[];
|
||||||
|
notes?: Record<string, Array<{ word?: unknown; reading?: unknown }>>;
|
||||||
};
|
};
|
||||||
if ((raw.version === 1 || raw.version === 2) && Array.isArray(raw.words)) {
|
if ((raw.version === 1 || raw.version === 2) && Array.isArray(raw.words)) {
|
||||||
return new Set(raw.words);
|
return new Set(raw.words);
|
||||||
}
|
}
|
||||||
|
// v3 stores reading-aware entries per note; stats rows only carry
|
||||||
|
// headwords, so flatten to a word set (reading-agnostic, fail-open).
|
||||||
|
if (raw.version === 3 && raw.notes && typeof raw.notes === 'object') {
|
||||||
|
const words = new Set<string>();
|
||||||
|
for (const entries of Object.values(raw.notes)) {
|
||||||
|
if (!Array.isArray(entries)) continue;
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry && typeof entry.word === 'string' && entry.word) {
|
||||||
|
words.add(entry.word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return words;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user