Problem — server/services/meatspacePostStats.js already imports three helpers from server/services/meatspacePost.js, and its own header says "Shared session and legacy-task helpers intentionally stay there" — yet two of those helpers, trainingEntryTask and summarizeSkillEvidence, are pasted into it character for character instead of imported. They are the normalizers that turn a training-log entry into a scored task and roll drill evidence into the accuracy/completion means the POST stats surface. A change to how a legacy training entry maps to accuracy (the exact area the #2094 n-back rescoring touched) now has to be made twice, and if it isn't, the stats endpoint and the session view report different accuracy for the same drill.
Evidence — server/services/meatspacePostStats.js:7 already reaches into the sibling:
import { getPostSessions, deriveTaskAccuracy, deriveTaskCompletion } from './meatspacePost.js';
and then re-declares, at :13 and :40, bodies that are byte-identical to server/services/meatspacePost.js:949 and :990 (verified by extracting both function bodies and diffing — the only difference is the export keyword, which neither copy carries). The shared body:
function summarizeSkillEvidence(sessions, training) {
const accuracyLists = {}; const completionLists = {}; const counts = {};
const add = (task) => {
if (!task?.module || !task?.type) return;
const key = `${task.module}:${task.type}`;
counts[key] = (counts[key] || 0) + 1;
const accuracy = deriveTaskAccuracy(task);
if (accuracy != null) (accuracyLists[key] ||= []).push(accuracy);
...
Consumers: meatspacePost.js:985 and :1173 use trainingEntryTask; meatspacePostStats.js:54 and :93 use the copies.
Plan
- In
server/services/meatspacePost.js, add export to function trainingEntryTask (:949) and function summarizeSkillEvidence (:990).
- In
server/services/meatspacePostStats.js, delete lines 13-38 (trainingEntryTask) and 40-65 (summarizeSkillEvidence) and add both names to the existing import on line 7.
- Leave
skillEvidenceSessions (meatspacePost.js:975) private — it has one caller.
- The circular edge already exists and is safe:
meatspacePost.js:58 imports getPostStats from meatspacePostStats.js while meatspacePostStats.js:7 imports back the other way, and ESM live bindings resolve it. Adding two more names to the existing back-import changes nothing about the cycle — but run cd server && npm test for the meatspace suites specifically to confirm no TDZ error at module init.
Tests — extend server/services/meatspacePostStats.test.js (no such file today, so create it) with one boundary case: a training-log entry whose accuracy is absent but whose questionCount/correctCount are present appears in getPostStats()'s evidenceByDrillAccuracy with the derived ratio. That uniquely catches the copies drifting on the derive rule, which is the regression this change prevents.
Acceptance criteria
Out of scope — the client mirror of nBackBalancedAccuracy in client/src/components/meatspace/post/constants.js; any change to the scoring rules themselves.
Filed by a /do:better --scan-only --issues audit (2026-09-01). Category: dry · Severity: low · Files: server/services/meatspacePostStats.js:13, server/services/meatspacePostStats.js:40, server/services/meatspacePost.js:949, server/services/meatspacePost.js:990
All labels already exist in the repo; do NOT create labels. Never add planner:* labels.
Problem —
server/services/meatspacePostStats.jsalready imports three helpers fromserver/services/meatspacePost.js, and its own header says "Shared session and legacy-task helpers intentionally stay there" — yet two of those helpers,trainingEntryTaskandsummarizeSkillEvidence, are pasted into it character for character instead of imported. They are the normalizers that turn a training-log entry into a scored task and roll drill evidence into the accuracy/completion means the POST stats surface. A change to how a legacy training entry maps toaccuracy(the exact area the #2094 n-back rescoring touched) now has to be made twice, and if it isn't, the stats endpoint and the session view report different accuracy for the same drill.Evidence —
server/services/meatspacePostStats.js:7already reaches into the sibling:and then re-declares, at
:13and:40, bodies that are byte-identical toserver/services/meatspacePost.js:949and:990(verified by extracting both function bodies and diffing — the only difference is theexportkeyword, which neither copy carries). The shared body:Consumers:
meatspacePost.js:985and:1173usetrainingEntryTask;meatspacePostStats.js:54and:93use the copies.Plan
server/services/meatspacePost.js, addexporttofunction trainingEntryTask(:949) andfunction summarizeSkillEvidence(:990).server/services/meatspacePostStats.js, delete lines 13-38 (trainingEntryTask) and 40-65 (summarizeSkillEvidence) and add both names to the existing import on line 7.skillEvidenceSessions(meatspacePost.js:975) private — it has one caller.meatspacePost.js:58importsgetPostStatsfrommeatspacePostStats.jswhilemeatspacePostStats.js:7imports back the other way, and ESM live bindings resolve it. Adding two more names to the existing back-import changes nothing about the cycle — but runcd server && npm testfor the meatspace suites specifically to confirm no TDZ error at module init.Tests — extend
server/services/meatspacePostStats.test.js(no such file today, so create it) with one boundary case: a training-log entry whoseaccuracyis absent but whosequestionCount/correctCountare present appears ingetPostStats()'sevidenceByDrillAccuracywith the derived ratio. That uniquely catches the copies drifting on the derive rule, which is the regression this change prevents.Acceptance criteria
grep -c "function trainingEntryTask\|function summarizeSkillEvidence" server/services/meatspacePostStats.jsreturns 0.getPostStats()returns identical output before and after the change for a fixture with both session and training evidence.cd server && npm testpasses.Out of scope — the client mirror of
nBackBalancedAccuracyinclient/src/components/meatspace/post/constants.js; any change to the scoring rules themselves.Filed by a
/do:better --scan-only --issuesaudit (2026-09-01). Category:dry· Severity:low· Files:server/services/meatspacePostStats.js:13, server/services/meatspacePostStats.js:40, server/services/meatspacePost.js:949, server/services/meatspacePost.js:990All labels already exist in the repo; do NOT create labels. Never add
planner:*labels.