mover-os 4.7.7 → 4.7.8
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/install.js +688 -68
- package/package.json +15 -3
- package/src/dashboard/build.js +1503 -0
- package/src/dashboard/dashboard.js +276 -0
- package/src/dashboard/index.js +319 -0
- package/src/dashboard/lib/activation-log.js +297 -0
- package/src/dashboard/lib/active-context-parser.js +177 -0
- package/src/dashboard/lib/agent-command.js +93 -0
- package/src/dashboard/lib/agent-detect.js +255 -0
- package/src/dashboard/lib/agent-detector.js +92 -0
- package/src/dashboard/lib/agent-session.js +462 -0
- package/src/dashboard/lib/anti-identity-detector.js +116 -0
- package/src/dashboard/lib/auto-learnings-parser.js +183 -0
- package/src/dashboard/lib/cli-usage-parser.js +92 -0
- package/src/dashboard/lib/config-parser.js +109 -0
- package/src/dashboard/lib/connect-recommender.js +131 -0
- package/src/dashboard/lib/correlations-parser.js +231 -0
- package/src/dashboard/lib/daily-note-resolver.js +211 -0
- package/src/dashboard/lib/date-utils.js +35 -0
- package/src/dashboard/lib/distribution-parser.js +78 -0
- package/src/dashboard/lib/dossier-parser.js +64 -0
- package/src/dashboard/lib/drift-history.js +83 -0
- package/src/dashboard/lib/drift-score.js +119 -0
- package/src/dashboard/lib/engine-health.js +170 -0
- package/src/dashboard/lib/engine-writer.js +1685 -0
- package/src/dashboard/lib/execution-plan.js +125 -0
- package/src/dashboard/lib/experiments-parser.js +429 -0
- package/src/dashboard/lib/feed-parser.js +294 -0
- package/src/dashboard/lib/forked-future.js +60 -0
- package/src/dashboard/lib/goal-forecast.js +304 -0
- package/src/dashboard/lib/goals-parser.js +67 -0
- package/src/dashboard/lib/health-protocols-parser.js +133 -0
- package/src/dashboard/lib/hook-activity.js +48 -0
- package/src/dashboard/lib/hook-indexer.js +169 -0
- package/src/dashboard/lib/hourly-activity-parser.js +143 -0
- package/src/dashboard/lib/identity-parser.js +85 -0
- package/src/dashboard/lib/ingestion.js +418 -0
- package/src/dashboard/lib/library-indexer-v2.js +212 -0
- package/src/dashboard/lib/library-indexer.js +105 -0
- package/src/dashboard/lib/log-activation.sh +61 -0
- package/src/dashboard/lib/memory-curator.js +97 -0
- package/src/dashboard/lib/memory-gardener.js +177 -0
- package/src/dashboard/lib/memory-gepa.js +102 -0
- package/src/dashboard/lib/memory-index.js +470 -0
- package/src/dashboard/lib/memory-rerank.js +72 -0
- package/src/dashboard/lib/memory-text.js +136 -0
- package/src/dashboard/lib/metrics-log-parser.js +76 -0
- package/src/dashboard/lib/moves-usage-parser.js +184 -0
- package/src/dashboard/lib/onboarding-forge.js +70 -0
- package/src/dashboard/lib/override-outcome-parser.js +68 -0
- package/src/dashboard/lib/override-summary.js +73 -0
- package/src/dashboard/lib/paths.js +192 -0
- package/src/dashboard/lib/pattern-manifest-loader.js +29 -0
- package/src/dashboard/lib/phantom-strategy.js +129 -0
- package/src/dashboard/lib/project-scanner.js +121 -0
- package/src/dashboard/lib/promise-wall.js +88 -0
- package/src/dashboard/lib/record-score.js +173 -0
- package/src/dashboard/lib/redaction.js +140 -0
- package/src/dashboard/lib/refusal-parser.js +44 -0
- package/src/dashboard/lib/regenerate-manifest.js +206 -0
- package/src/dashboard/lib/rewind-snapshots.js +689 -0
- package/src/dashboard/lib/roast-wall-parser.js +200 -0
- package/src/dashboard/lib/run-registry.js +226 -0
- package/src/dashboard/lib/safe-write.js +63 -0
- package/src/dashboard/lib/session-log-parser.js +145 -0
- package/src/dashboard/lib/session-time-parser.js +158 -0
- package/src/dashboard/lib/skill-index.js +171 -0
- package/src/dashboard/lib/skill-indexer.js +118 -0
- package/src/dashboard/lib/skill-recommender.js +689 -0
- package/src/dashboard/lib/strategy-parser.js +245 -0
- package/src/dashboard/lib/strategy-protocol-parser.js +135 -0
- package/src/dashboard/lib/streak-parser.js +95 -0
- package/src/dashboard/lib/suggested-now.js +254 -0
- package/src/dashboard/lib/tool-awareness.js +125 -0
- package/src/dashboard/lib/transcript-parser.js +331 -0
- package/src/dashboard/lib/vault-graph-parser.js +205 -0
- package/src/dashboard/lib/view-generator.js +163 -0
- package/src/dashboard/lib/voice-dna-parser.js +57 -0
- package/src/dashboard/lib/walkthrough-script.js +140 -0
- package/src/dashboard/lib/workflow-graph-parser.js +170 -0
- package/src/dashboard/lib/workflow-library-parser.js +146 -0
- package/src/dashboard/server.js +2024 -0
- package/src/dashboard/shortcut.js +0 -0
- package/src/dashboard/static/setup-poc.html +306 -0
- package/src/dashboard/static/walkthrough-poc.html +580 -0
- package/src/dashboard/styles.css +1201 -0
- package/src/dashboard/templates/index.html +278 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hero.png +0 -0
- package/src/dashboard/ui/dist/assets/index-598CSGOZ.js +157 -0
- package/src/dashboard/ui/dist/assets/index-BP--M69H.css +1 -0
- package/src/dashboard/ui/dist/assets/index-CidzmwSW.js +34 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/mover-system.css +62 -0
- package/src/dashboard/ui/dist/assets/xterm-CqkleIqs.js +1 -0
- package/src/dashboard/ui/dist/icon.svg +4 -0
- package/src/dashboard/ui/dist/index.html +18 -0
- package/src/dashboard/ui/dist/manifest.webmanifest +1 -0
- package/src/dashboard/ui/dist/registerSW.js +1 -0
- package/src/dashboard/ui/dist/sw.js +1 -0
- package/src/dashboard/ui/dist/workbox-39fa566e.js +1 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Black Box Correlations parser — V5 wishlist item (V5_CLOSED_LOOP:1039).
|
|
4
|
+
//
|
|
5
|
+
// Correlates daily-note Battery signals (sleep hours, protein, energy
|
|
6
|
+
// rating) against output signals (tasksDone, sessionCount). Computes
|
|
7
|
+
// Pearson correlation coefficient over the trailing N days.
|
|
8
|
+
//
|
|
9
|
+
// Designed to be HONEST under low-data conditions:
|
|
10
|
+
// - <14 days: returns { available: false, reason: 'needs_more_data', haveDays: N }
|
|
11
|
+
// - >=14 days but Battery section empty: returns { available: false, reason: 'no_battery_data' }
|
|
12
|
+
// - With data: returns array of correlation entries, each with
|
|
13
|
+
// { x_signal, y_signal, r, n, sample_window_days }
|
|
14
|
+
//
|
|
15
|
+
// No fake patterns. The UI module shows the underlying numbers, not just
|
|
16
|
+
// a verdict like "good sleep = better output." Codex ethics: surface the
|
|
17
|
+
// truth, not the convenient interpretation.
|
|
18
|
+
|
|
19
|
+
const fs = require("fs");
|
|
20
|
+
const path = require("path");
|
|
21
|
+
|
|
22
|
+
const MIN_DAYS = 14;
|
|
23
|
+
const DEFAULT_WINDOW = 30;
|
|
24
|
+
|
|
25
|
+
function dailiesDir(vault) {
|
|
26
|
+
return path.join(vault, "02_Areas", "Engine", "Dailies");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function safeRead(filePath) {
|
|
30
|
+
try { return fs.readFileSync(filePath, "utf8"); } catch { return ""; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Parse a Daily Note for the metrics we care about.
|
|
34
|
+
// Returns { date, sleepHours, proteinG, energy, tasksDone, sessionCount,
|
|
35
|
+
// hasBatterySection } or null.
|
|
36
|
+
//
|
|
37
|
+
// User-reported bug 2026-05-24: parser said "Daily Notes don't have Battery
|
|
38
|
+
// section data yet · coverage sleep=0 protein=0 energy=0" — but vault has
|
|
39
|
+
// ## Battery section in every May daily. The catch: section is a markdown
|
|
40
|
+
// TABLE, values are [TBD] placeholders, and metric names differ from the
|
|
41
|
+
// regex labels. Two fixes here:
|
|
42
|
+
// 1. Match both `Sleep: 7h 30m` AND markdown-table `| Sleep | 7h 30m |`
|
|
43
|
+
// 2. Reject placeholder values like `[TBD]`, `—`, `(none)`, `n/a`
|
|
44
|
+
// 3. Emit hasBatterySection so the UI can distinguish "no section" from
|
|
45
|
+
// "section exists but unfilled" — the user's real state.
|
|
46
|
+
function parseDaily(filePath, dateKey) {
|
|
47
|
+
const text = safeRead(filePath);
|
|
48
|
+
if (!text) return null;
|
|
49
|
+
const entry = { date: dateKey, sleepHours: null, proteinG: null, energy: null, tasksDone: null, sessionCount: null, hasBatterySection: false };
|
|
50
|
+
|
|
51
|
+
// Detect Battery section presence (independent of value extraction).
|
|
52
|
+
entry.hasBatterySection = /^##\s+Battery\b/im.test(text);
|
|
53
|
+
|
|
54
|
+
// T242 bug hunt fix (B#9): scope value matching to within the
|
|
55
|
+
// `## Battery` section only. Previous version did `text.match` against
|
|
56
|
+
// the whole file, so any Session Log line containing "Energy: high" or
|
|
57
|
+
// a quoted "Sleep: 8h" anywhere in the note would match and pollute
|
|
58
|
+
// the metric. Now: locate the Battery header, slice to the NEXT
|
|
59
|
+
// top-level header (or EOF), and match only within that slice.
|
|
60
|
+
let batterySlice = "";
|
|
61
|
+
if (entry.hasBatterySection) {
|
|
62
|
+
const startIdx = text.search(/^##\s+Battery\b/im);
|
|
63
|
+
if (startIdx >= 0) {
|
|
64
|
+
const after = text.slice(startIdx);
|
|
65
|
+
// Find the next `##` heading (any level 2+) that's NOT the Battery one.
|
|
66
|
+
const nextHeader = after.slice(8).search(/^##\s+\S/m);
|
|
67
|
+
batterySlice = nextHeader > 0 ? after.slice(0, 8 + nextHeader) : after;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const valueText = batterySlice || ""; // Only match inside Battery slice.
|
|
71
|
+
|
|
72
|
+
// Combined regex: capture value after `:`/`=` OR after a `|` pipe (table).
|
|
73
|
+
// Reject placeholder values like [TBD], —, (none), n/a, ?, TBD.
|
|
74
|
+
const isReal = (v) => v && !/^(?:\[?[Tt][Bb][Dd]\]?|[—–-]+|n\/?a|\(?none\)?|\?+|tbd|pending)$/i.test(v.trim());
|
|
75
|
+
|
|
76
|
+
// Sleep — "Sleep: 7h 30m" OR "| Sleep | 7h 30m |"
|
|
77
|
+
const sleepMatch = valueText.match(/(?:Sleep|Slept)\s*(?:[:=]|\|)\s*([^|\n]+?)(?=\s*\||\s*$)/im);
|
|
78
|
+
if (sleepMatch && isReal(sleepMatch[1])) {
|
|
79
|
+
const sub = sleepMatch[1].trim().match(/(\d+(?:\.\d+)?)\s*h?(?:\s*(\d+)\s*m)?/);
|
|
80
|
+
if (sub) {
|
|
81
|
+
const h = parseFloat(sub[1]);
|
|
82
|
+
const m = sub[2] ? parseFloat(sub[2]) : 0;
|
|
83
|
+
entry.sleepHours = Number.isFinite(h) ? h + (Number.isFinite(m) ? m / 60 : 0) : null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Protein — "Protein: 120g" OR "| Protein | 120g |"
|
|
88
|
+
const proteinMatch = valueText.match(/Protein\s*(?:[:=]|\|)\s*([^|\n]+?)(?=\s*\||\s*$)/im);
|
|
89
|
+
if (proteinMatch && isReal(proteinMatch[1])) {
|
|
90
|
+
const sub = proteinMatch[1].trim().match(/(\d+(?:\.\d+)?)\s*g?/);
|
|
91
|
+
if (sub) entry.proteinG = parseFloat(sub[1]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Energy — "Energy: 7/10" OR "| Energy | 7/10 |"
|
|
95
|
+
const energyMatch = valueText.match(/Energy\s*(?:[:=]|\|)\s*([^|\n]+?)(?=\s*\||\s*$)/im);
|
|
96
|
+
if (energyMatch && isReal(energyMatch[1])) {
|
|
97
|
+
const sub = energyMatch[1].trim().match(/(\d+(?:\.\d+)?)/);
|
|
98
|
+
if (sub) entry.energy = parseFloat(sub[1]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Tasks — count "- [x]" and "- [ ]"
|
|
102
|
+
const doneCount = (text.match(/^\s*[-*]\s*\[x\]/gmi) || []).length;
|
|
103
|
+
const totalCount = (text.match(/^\s*[-*]\s*\[[ x]\]/gmi) || []).length;
|
|
104
|
+
if (totalCount > 0) {
|
|
105
|
+
entry.tasksDone = doneCount;
|
|
106
|
+
entry.tasksTotal = totalCount;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Session count — count "## Session" or "Session 1, Session 2" headers
|
|
110
|
+
const sessionMatch = text.match(/##\s*Session/gi);
|
|
111
|
+
if (sessionMatch) entry.sessionCount = sessionMatch.length;
|
|
112
|
+
|
|
113
|
+
return entry;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Walk daily notes for the trailing N days.
|
|
117
|
+
function collectEntries(vault, windowDays) {
|
|
118
|
+
const dDir = dailiesDir(vault);
|
|
119
|
+
if (!fs.existsSync(dDir)) return [];
|
|
120
|
+
const entries = [];
|
|
121
|
+
const cutoff = Date.now() - windowDays * 86400000;
|
|
122
|
+
let months;
|
|
123
|
+
try {
|
|
124
|
+
months = fs.readdirSync(dDir).filter((d) => /^\d{4}-\d{2}$/.test(d)).sort().reverse();
|
|
125
|
+
} catch { return []; }
|
|
126
|
+
for (const mDir of months) {
|
|
127
|
+
if (entries.length >= windowDays * 2) break; // safety
|
|
128
|
+
let files;
|
|
129
|
+
try {
|
|
130
|
+
files = fs.readdirSync(path.join(dDir, mDir)).filter((f) => /^Daily - \d{4}-\d{2}-\d{2}\.md$/.test(f)).sort().reverse();
|
|
131
|
+
} catch { continue; }
|
|
132
|
+
for (const f of files) {
|
|
133
|
+
const m = f.match(/(\d{4})-(\d{2})-(\d{2})/);
|
|
134
|
+
if (!m) continue;
|
|
135
|
+
const dateKey = m[0];
|
|
136
|
+
const t = Date.parse(`${m[1]}-${m[2]}-${m[3]}`);
|
|
137
|
+
if (!Number.isFinite(t) || t < cutoff) continue;
|
|
138
|
+
const parsed = parseDaily(path.join(dDir, mDir, f), dateKey);
|
|
139
|
+
if (parsed) entries.push(parsed);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return entries;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Pearson correlation coefficient over paired (x, y) values.
|
|
146
|
+
// Returns null if N < 3 or zero variance.
|
|
147
|
+
function pearson(xs, ys) {
|
|
148
|
+
const n = Math.min(xs.length, ys.length);
|
|
149
|
+
if (n < 3) return null;
|
|
150
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0;
|
|
151
|
+
for (let i = 0; i < n; i++) {
|
|
152
|
+
const x = xs[i], y = ys[i];
|
|
153
|
+
sumX += x; sumY += y;
|
|
154
|
+
sumXY += x * y;
|
|
155
|
+
sumX2 += x * x;
|
|
156
|
+
sumY2 += y * y;
|
|
157
|
+
}
|
|
158
|
+
const num = n * sumXY - sumX * sumY;
|
|
159
|
+
const den = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
|
|
160
|
+
if (den === 0) return null;
|
|
161
|
+
return num / den;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function correlate(entries, xKey, yKey) {
|
|
165
|
+
const pairs = entries
|
|
166
|
+
.map(e => ({ x: e[xKey], y: e[yKey] }))
|
|
167
|
+
.filter(p => Number.isFinite(p.x) && Number.isFinite(p.y));
|
|
168
|
+
if (pairs.length < 3) return null;
|
|
169
|
+
const r = pearson(pairs.map(p => p.x), pairs.map(p => p.y));
|
|
170
|
+
if (r == null) return null;
|
|
171
|
+
return {
|
|
172
|
+
x_signal: xKey,
|
|
173
|
+
y_signal: yKey,
|
|
174
|
+
r: Math.round(r * 1000) / 1000,
|
|
175
|
+
n: pairs.length,
|
|
176
|
+
direction: r > 0.1 ? "positive" : r < -0.1 ? "negative" : "none",
|
|
177
|
+
strength: Math.abs(r) > 0.6 ? "strong" : Math.abs(r) > 0.3 ? "moderate" : "weak",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function compute({ vault, windowDays = DEFAULT_WINDOW } = {}) {
|
|
182
|
+
if (!vault) return { available: false, reason: "no_vault" };
|
|
183
|
+
const entries = collectEntries(vault, windowDays);
|
|
184
|
+
if (entries.length < MIN_DAYS) {
|
|
185
|
+
return {
|
|
186
|
+
available: false,
|
|
187
|
+
reason: "needs_more_data",
|
|
188
|
+
haveDays: entries.length,
|
|
189
|
+
neededDays: MIN_DAYS,
|
|
190
|
+
windowDays,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const haveSleep = entries.filter(e => Number.isFinite(e.sleepHours)).length;
|
|
194
|
+
const haveProtein = entries.filter(e => Number.isFinite(e.proteinG)).length;
|
|
195
|
+
const haveEnergy = entries.filter(e => Number.isFinite(e.energy)).length;
|
|
196
|
+
// Distinguish "no Battery section anywhere" from "section exists but
|
|
197
|
+
// values are placeholders ([TBD]/—/n/a)". The empty-state copy in the
|
|
198
|
+
// UI changes depending on which — telling someone with [TBD]s "you
|
|
199
|
+
// don't have a Battery section" is gaslighting, the truth is "fill in
|
|
200
|
+
// your placeholders."
|
|
201
|
+
const haveBatterySection = entries.filter(e => e.hasBatterySection).length;
|
|
202
|
+
if (haveSleep < MIN_DAYS && haveProtein < MIN_DAYS && haveEnergy < MIN_DAYS) {
|
|
203
|
+
return {
|
|
204
|
+
available: false,
|
|
205
|
+
reason: haveBatterySection > 0 ? "battery_section_unfilled" : "no_battery_section",
|
|
206
|
+
haveDays: entries.length,
|
|
207
|
+
windowDays,
|
|
208
|
+
batterySectionDays: haveBatterySection,
|
|
209
|
+
coverage: { sleep: haveSleep, protein: haveProtein, energy: haveEnergy },
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const pairs = [
|
|
213
|
+
["sleepHours", "tasksDone"],
|
|
214
|
+
["sleepHours", "sessionCount"],
|
|
215
|
+
["sleepHours", "energy"],
|
|
216
|
+
["proteinG", "energy"],
|
|
217
|
+
["proteinG", "tasksDone"],
|
|
218
|
+
["energy", "tasksDone"],
|
|
219
|
+
["energy", "sessionCount"],
|
|
220
|
+
];
|
|
221
|
+
const correlations = pairs.map(([x, y]) => correlate(entries, x, y)).filter(Boolean);
|
|
222
|
+
return {
|
|
223
|
+
available: true,
|
|
224
|
+
windowDays,
|
|
225
|
+
haveDays: entries.length,
|
|
226
|
+
coverage: { sleep: haveSleep, protein: haveProtein, energy: haveEnergy },
|
|
227
|
+
correlations,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = { compute, parseDaily, pearson };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const { dailiesDir, dailyNotePath, exists, safeRead, safeListDir } = require("./paths");
|
|
6
|
+
|
|
7
|
+
// Resolves Daily Notes — supports YYYY-MM/Daily - YYYY-MM-DD.md format.
|
|
8
|
+
// Returns streak, today's note, last-N notes, total count.
|
|
9
|
+
|
|
10
|
+
function fmtDate(d) {
|
|
11
|
+
const y = d.getFullYear();
|
|
12
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
13
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
14
|
+
return `${y}-${m}-${dd}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveDailyNotes(vault, options = {}) {
|
|
18
|
+
// T245 — apply the Mover OS session-day convention. Before 05:00, the
|
|
19
|
+
// session belongs to YESTERDAY (matches /log, /analyse-day, /plan-tomorrow
|
|
20
|
+
// workflows). Without this, at 03:00 the dashboard reports "no Daily
|
|
21
|
+
// Note for today" while the user has been actively editing yesterday's
|
|
22
|
+
// note all night — gives a ghost "missing daily" warning every late
|
|
23
|
+
// session.
|
|
24
|
+
let sessionDate = options.sessionDate;
|
|
25
|
+
if (!sessionDate) {
|
|
26
|
+
const now = new Date();
|
|
27
|
+
sessionDate = new Date(now);
|
|
28
|
+
if (now.getHours() < 5) sessionDate.setDate(sessionDate.getDate() - 1);
|
|
29
|
+
}
|
|
30
|
+
const lookback = options.lookback || 30;
|
|
31
|
+
|
|
32
|
+
const out = {
|
|
33
|
+
available: true,
|
|
34
|
+
today: null,
|
|
35
|
+
streak: 0,
|
|
36
|
+
recentNotes: [],
|
|
37
|
+
totalThisMonth: 0,
|
|
38
|
+
totalThisYear: 0
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Check today's note
|
|
42
|
+
const todayPath = dailyNotePath(vault, sessionDate);
|
|
43
|
+
if (exists(todayPath)) {
|
|
44
|
+
const text = safeRead(todayPath, "");
|
|
45
|
+
out.today = {
|
|
46
|
+
date: fmtDate(sessionDate),
|
|
47
|
+
path: todayPath,
|
|
48
|
+
exists: true,
|
|
49
|
+
...analyzeNoteText(text)
|
|
50
|
+
};
|
|
51
|
+
} else {
|
|
52
|
+
out.today = { date: fmtDate(sessionDate), path: todayPath, exists: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Streak — count consecutive days backwards including today
|
|
56
|
+
let streakCount = 0;
|
|
57
|
+
for (let i = 0; i < 365; i++) {
|
|
58
|
+
const d = new Date(sessionDate);
|
|
59
|
+
d.setDate(d.getDate() - i);
|
|
60
|
+
const p = dailyNotePath(vault, d);
|
|
61
|
+
if (exists(p)) {
|
|
62
|
+
streakCount++;
|
|
63
|
+
} else if (i === 0) {
|
|
64
|
+
// No daily note today — streak counts from yesterday backwards
|
|
65
|
+
continue;
|
|
66
|
+
} else {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
out.streak = streakCount;
|
|
71
|
+
|
|
72
|
+
// Recent notes (last N days)
|
|
73
|
+
const recent = [];
|
|
74
|
+
for (let i = 0; i < lookback; i++) {
|
|
75
|
+
const d = new Date(sessionDate);
|
|
76
|
+
d.setDate(d.getDate() - i);
|
|
77
|
+
const p = dailyNotePath(vault, d);
|
|
78
|
+
if (exists(p)) {
|
|
79
|
+
const text = safeRead(p, "");
|
|
80
|
+
recent.push({ date: fmtDate(d), path: p, exists: true, ...analyzeNoteText(text) });
|
|
81
|
+
} else {
|
|
82
|
+
recent.push({ date: fmtDate(d), path: p, exists: false });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
out.recentNotes = recent;
|
|
86
|
+
|
|
87
|
+
// Counts
|
|
88
|
+
const yyyy = sessionDate.getFullYear();
|
|
89
|
+
const yyyyMm = `${yyyy}-${String(sessionDate.getMonth() + 1).padStart(2, "0")}`;
|
|
90
|
+
const monthDir = path.join(dailiesDir(vault), yyyyMm);
|
|
91
|
+
out.totalThisMonth = safeListDir(monthDir, { extension: ".md" }).length;
|
|
92
|
+
|
|
93
|
+
// This year — sum across all month folders
|
|
94
|
+
const yearDirs = safeListDir(dailiesDir(vault), { dirsOnly: true })
|
|
95
|
+
.filter(e => e.name.startsWith(`${yyyy}-`));
|
|
96
|
+
let yearCount = 0;
|
|
97
|
+
for (const yd of yearDirs) {
|
|
98
|
+
yearCount += safeListDir(yd.path, { extension: ".md" }).length;
|
|
99
|
+
}
|
|
100
|
+
out.totalThisYear = yearCount;
|
|
101
|
+
|
|
102
|
+
// 365-day grid: array of {date, exists}
|
|
103
|
+
const heatmapDays = [];
|
|
104
|
+
for (let i = 364; i >= 0; i--) {
|
|
105
|
+
const d = new Date(sessionDate);
|
|
106
|
+
d.setDate(d.getDate() - i);
|
|
107
|
+
const p = dailyNotePath(vault, d);
|
|
108
|
+
heatmapDays.push({ date: fmtDate(d), exists: exists(p) });
|
|
109
|
+
}
|
|
110
|
+
out.heatmap = heatmapDays;
|
|
111
|
+
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Strip bold FIRST (else the bullet-strip eats one * of a **bold** line),
|
|
116
|
+
// then a leading bullet/checkbox marker. Returns null on empty.
|
|
117
|
+
function cleanLine(s) {
|
|
118
|
+
if (!s) return null;
|
|
119
|
+
return s.replace(/\*\*/g, "").replace(/^\s*[-*]\s+(\[.\]\s*)?/, "").trim() || null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const EMPTY_JOURNAL = { hasEvening: false, hasMorning: false, evening: null, morning: null };
|
|
123
|
+
|
|
124
|
+
// v6 SURFACE 2 — parse the Daily Note `## Journal` section into its
|
|
125
|
+
// `### Evening · …` / `### Morning · …` sub-anchors (written by the journal
|
|
126
|
+
// close endpoint, W3). Every field is null when absent — a guessed score is
|
|
127
|
+
// never ok (false-zero contract). The section ends at the next H1/H2.
|
|
128
|
+
function parseJournal(text) {
|
|
129
|
+
if (!text) return EMPTY_JOURNAL;
|
|
130
|
+
const jm = text.match(/^#{1,6}\s*journal\s*$/im);
|
|
131
|
+
if (!jm) return EMPTY_JOURNAL;
|
|
132
|
+
let sect = text.slice(jm.index + jm[0].length);
|
|
133
|
+
const nextTop = sect.search(/^#{1,2}\s/m);
|
|
134
|
+
if (nextTop > 0) sect = sect.slice(0, nextTop);
|
|
135
|
+
const under = (re) => { const m = sect.match(re); return m ? cleanLine(m[1]) : null; };
|
|
136
|
+
const num = (line) => { if (!line) return null; const m = line.match(/\b(?:10|[1-9])\b/); return m ? Number(m[0]) : null; };
|
|
137
|
+
|
|
138
|
+
const evScoreLine = under(/###[^\n]*evening[^\n]*score[^\n]*\n+([^\n#][^\n]*)/i);
|
|
139
|
+
const evening = {
|
|
140
|
+
score: num(evScoreLine),
|
|
141
|
+
scoreWhy: evScoreLine,
|
|
142
|
+
winAvoidance: under(/###[^\n]*evening[^\n]*(?:win|avoid)[^\n]*\n+([^\n#][^\n]*)/i),
|
|
143
|
+
workedDidnt: under(/###[^\n]*evening[^\n]*work[^\n]*\n+([^\n#][^\n]*)/i),
|
|
144
|
+
doDifferently: under(/###[^\n]*evening[^\n]*differ[^\n]*\n+([^\n#][^\n]*)/i),
|
|
145
|
+
// Codex W3 round 3: the alibi is an EVENING field — read it ONLY from an
|
|
146
|
+
// Evening header (matches what the journal endpoint writes: "### Evening ·
|
|
147
|
+
// alibi"). Requiring "evening" positively — rather than just excluding
|
|
148
|
+
// morning — means a user's unrelated "### Random prediction note" is NOT
|
|
149
|
+
// misread as the evening alibi. Consistent with every other evening field,
|
|
150
|
+
// which already requires "evening" in the header.
|
|
151
|
+
alibi: under(/###[^\n]*\bevening\b[^\n]*(?:alibi|prediction)[^\n]*\n+([^\n#][^\n]*)/i),
|
|
152
|
+
};
|
|
153
|
+
const mEnergyLine = under(/###[^\n]*morning[^\n]*energy[^\n]*\n+([^\n#][^\n]*)/i);
|
|
154
|
+
const morning = {
|
|
155
|
+
wantVsShould: under(/###[^\n]*morning[^\n]*(?:want|should)[^\n]*\n+([^\n#][^\n]*)/i),
|
|
156
|
+
energy: num(mEnergyLine),
|
|
157
|
+
grateful: under(/###[^\n]*morning[^\n]*grate[^\n]*\n+([^\n#][^\n]*)/i),
|
|
158
|
+
picture: under(/###[^\n]*morning[^\n]*(?:picture|visuali)[^\n]*\n+([^\n#][^\n]*)/i),
|
|
159
|
+
};
|
|
160
|
+
// Codex W3 fix 3: alibi counts toward an evening entry — the journal endpoint
|
|
161
|
+
// can write an alibi-only evening block, and it must read back (not be dropped).
|
|
162
|
+
const hasEvening = !!(evening.score || evening.winAvoidance || evening.workedDidnt || evening.doDifferently || evening.alibi);
|
|
163
|
+
const hasMorning = !!(morning.wantVsShould || morning.energy || morning.grateful || morning.picture);
|
|
164
|
+
return { hasEvening, hasMorning, evening: hasEvening ? evening : null, morning: hasMorning ? morning : null };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function analyzeNoteText(text) {
|
|
168
|
+
if (!text) return { tasksTotal: 0, tasksDone: 0, tasks: [], length: 0, focus: null, todaysCall: null, journal: EMPTY_JOURNAL, plan: null, got: null };
|
|
169
|
+
const lines = text.split("\n");
|
|
170
|
+
let total = 0, done = 0;
|
|
171
|
+
// W12: keep each checkbox line's text so the dashboard can write the tick
|
|
172
|
+
// back (POST /api/checkbox/toggle matches on exact trimmed item text). The
|
|
173
|
+
// counters above stay authoritative for the aggregate surfaces.
|
|
174
|
+
const tasks = [];
|
|
175
|
+
for (const line of lines) {
|
|
176
|
+
const m = line.match(/^\s*[-*]\s*\[([ xX])\]\s*(.*)$/);
|
|
177
|
+
if (m) {
|
|
178
|
+
total++;
|
|
179
|
+
const checked = m[1].toLowerCase() === "x";
|
|
180
|
+
if (checked) done++;
|
|
181
|
+
const t = (m[2] || "").trim();
|
|
182
|
+
if (t) tasks.push({ text: t, checked });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// v6 SURFACE 1 — today's stated focus, for the Say-it friction rule
|
|
186
|
+
// (off-plan new-project intent vs the day's plan). First content line under
|
|
187
|
+
// a "Focus" heading; null when there's no plan to argue against. Forgiving
|
|
188
|
+
// heading match because Daily templates vary; a guessed plan is never ok.
|
|
189
|
+
const fm = text.match(/^#{1,6}\s*[^\n]*focus[^\n]*\n+([^\n#][^\n]*)/im);
|
|
190
|
+
const focus = cleanLine(fm ? fm[1] : null);
|
|
191
|
+
// v6 — `## Today's Call` (the v6 hero label); falls back to focus when absent
|
|
192
|
+
// (the real template uses `## Focus`; Today's Call is the v6 demo label).
|
|
193
|
+
const tc = text.match(/^#{1,6}\s*[^\n]*today'?s call[^\n]*\n+([^\n#][^\n]*)/im);
|
|
194
|
+
const todaysCall = cleanLine(tc ? tc[1] : null);
|
|
195
|
+
// v6 SURFACE 2 — the `## Journal` sub-anchors (evening/morning).
|
|
196
|
+
const journal = parseJournal(text);
|
|
197
|
+
// v6 SURFACE 7 (Rewind) — per-date plan/got. plan = today's-call|focus;
|
|
198
|
+
// got = first line of `## Review`, else the first `## Key Insights` bullet;
|
|
199
|
+
// null when neither exists (Rewind shows an honest gap, never a guess).
|
|
200
|
+
let got = null;
|
|
201
|
+
const rev = text.match(/^#{1,6}\s*review[^\n]*\n+([^\n#][^\n]*)/im);
|
|
202
|
+
if (rev) got = cleanLine(rev[1]);
|
|
203
|
+
else {
|
|
204
|
+
const ki = text.match(/^#{1,6}\s*[^\n]*key insight[^\n]*\n+(?:[^\n]*\n)*?\s*[-*]\s+([^\n]+)/im);
|
|
205
|
+
if (ki) got = cleanLine(ki[1]);
|
|
206
|
+
}
|
|
207
|
+
const plan = todaysCall || focus;
|
|
208
|
+
return { tasksTotal: total, tasksDone: done, tasks, length: text.length, focus, todaysCall, journal, plan, got };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { resolveDailyNotes, fmtDate, analyzeNoteText, parseJournal };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function localDateKey(date = new Date()) {
|
|
4
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
5
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
6
|
+
const y = d.getFullYear();
|
|
7
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
8
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
9
|
+
return `${y}-${m}-${day}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function localMidnight(date = new Date()) {
|
|
13
|
+
const d = date instanceof Date ? new Date(date.getTime()) : new Date(date);
|
|
14
|
+
d.setHours(0, 0, 0, 0);
|
|
15
|
+
return d;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// T245 — Mover OS session-day convention. Workflows (/log, /analyse-day,
|
|
19
|
+
// /plan-tomorrow) treat any session before 05:00 as belonging to YESTERDAY
|
|
20
|
+
// — the user is still on "today's" plan until they wake up tomorrow.
|
|
21
|
+
// Parsers that ask "is there a Daily Note for today?" need the same rule
|
|
22
|
+
// or they report ghost gaps at 3 AM (the user wrote the note for May 24,
|
|
23
|
+
// it's now 03:00 on May 25 — `localDateKey()` returns 2026-05-25 and the
|
|
24
|
+
// May 24 note is invisible to the check).
|
|
25
|
+
function sessionDateKey(date = new Date()) {
|
|
26
|
+
const d = date instanceof Date ? new Date(date.getTime()) : new Date(date);
|
|
27
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
28
|
+
// Roll back to yesterday's date when hour < 5.
|
|
29
|
+
if (d.getHours() < 5) {
|
|
30
|
+
d.setDate(d.getDate() - 1);
|
|
31
|
+
}
|
|
32
|
+
return localDateKey(d);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { localDateKey, localMidnight, sessionDateKey };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { dailiesDir } = require("./paths");
|
|
6
|
+
const { localDateKey, localMidnight } = require("./date-utils");
|
|
7
|
+
|
|
8
|
+
// distribution-parser.js — F30. Heuristic scan of Daily Notes session logs
|
|
9
|
+
// for distribution-action keywords. Buckets by day. Returns 30-day rolling
|
|
10
|
+
// counts so the StatsStrip can show "DIST 7D · +3 vs prev week".
|
|
11
|
+
|
|
12
|
+
// HONESTY FIX (owner catch, 2026-07-07): bare verbs (shipped/published/posted/
|
|
13
|
+
// replied/commented) counted DEV work as distribution. Session logs are full of
|
|
14
|
+
// "R-A shipped" and "artifact published", which inflated the Home KPI, the day
|
|
15
|
+
// score's ship component, and Evidence's "distribution shipped" verdicts on days
|
|
16
|
+
// with zero real outreach. Every pattern below is CHANNEL-ANCHORED: the verb must
|
|
17
|
+
// sit near a real audience artifact or outreach act. We accept undercounting over
|
|
18
|
+
// inflating. A distribution count is a claim about reaching people, never about
|
|
19
|
+
// writing code.
|
|
20
|
+
const KEYWORDS = [
|
|
21
|
+
// publishing verbs anchored to an audience artifact or platform
|
|
22
|
+
/\b(?:posted|published|launched|shipped)\b[^.\n]{0,60}\b(?:reel|tweet|thread|article|video|newsletter|episode|waitlist|x\.com|twitter|instagram|\big\b|tiktok|linkedin|reddit|youtube|hacker ?news|product ?hunt)\b/i,
|
|
23
|
+
// reverse order: "reel posted", "article went live"
|
|
24
|
+
/\b(?:reel|tweet|thread|article|video|newsletter|episode)\b[^.\n]{0,40}\b(?:posted|published|launched|shipped|went live)\b/i,
|
|
25
|
+
// direct outreach: PERFORMED verbs only. Bare nouns (outreach, DMs) match
|
|
26
|
+
// planning talk ("cold-outreach template", "DM proof") and inflate.
|
|
27
|
+
/\bDM'?(?:d|ed)\b/i,
|
|
28
|
+
/\bsent (?:\d+ )?(?:DMs?|emails?|outreach|messages?|cold)\b/i,
|
|
29
|
+
/\breached out\b/i,
|
|
30
|
+
/\breply[- ]seed(?:ing|ed)?\b/i,
|
|
31
|
+
// replies count only when aimed at a person/audience surface, not code review
|
|
32
|
+
/\breplied?\b[^.\n]{0,50}\b(?:lead|prospect|customer|buyer|DM|thread|tweet|comment)\b/i,
|
|
33
|
+
/\btweeted\b/i,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function compute({ vault, daysLookback = 30 } = {}) {
|
|
37
|
+
if (!vault) return { available: false, days: [], todayCount: null, total7d: null, total30d: null, delta7d: null };
|
|
38
|
+
const dDir = dailiesDir(vault);
|
|
39
|
+
if (!fs.existsSync(dDir)) return { available: false, days: [], todayCount: null, total7d: null, total30d: null, delta7d: null };
|
|
40
|
+
|
|
41
|
+
const today = localMidnight();
|
|
42
|
+
const days = [];
|
|
43
|
+
for (let i = 0; i < daysLookback; i++) {
|
|
44
|
+
const d = new Date(today.getTime() - i * 24 * 3600 * 1000);
|
|
45
|
+
const key = localDateKey(d);
|
|
46
|
+
const monthDir = key.slice(0, 7);
|
|
47
|
+
const fpath = path.join(dDir, monthDir, `Daily - ${key}.md`);
|
|
48
|
+
let count = 0;
|
|
49
|
+
// R3-E1: a day with no note is NO-RECORD, not "zero receipts" — the UI
|
|
50
|
+
// needs the difference to clamp all-time views to the first real note.
|
|
51
|
+
let exists = false;
|
|
52
|
+
try {
|
|
53
|
+
const text = fs.readFileSync(fpath, "utf8");
|
|
54
|
+
exists = true;
|
|
55
|
+
const slIdx = text.indexOf("## Session Log");
|
|
56
|
+
const section = slIdx >= 0 ? text.slice(slIdx) : text;
|
|
57
|
+
for (const re of KEYWORDS) {
|
|
58
|
+
const matches = section.match(new RegExp(re.source, "g" + (re.flags.includes("i") ? "i" : "")));
|
|
59
|
+
if (matches) count += matches.length;
|
|
60
|
+
}
|
|
61
|
+
} catch (_) { /* missing daily — count stays 0 */ }
|
|
62
|
+
days.push({ date: key, count, exists });
|
|
63
|
+
}
|
|
64
|
+
const total7d = days.slice(0, 7).reduce((s, d) => s + d.count, 0);
|
|
65
|
+
const total30d = days.reduce((s, d) => s + d.count, 0);
|
|
66
|
+
const prior7d = days.slice(7, 14).reduce((s, d) => s + d.count, 0);
|
|
67
|
+
const todayCount = days[0] ? days[0].count : 0;
|
|
68
|
+
return {
|
|
69
|
+
available: true,
|
|
70
|
+
days: days.reverse(),
|
|
71
|
+
todayCount,
|
|
72
|
+
total7d,
|
|
73
|
+
total30d,
|
|
74
|
+
delta7d: total7d - prior7d
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { compute };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { moverDossierPath, safeRead, safeStat } = require("./paths");
|
|
4
|
+
|
|
5
|
+
// Parse Mover_Dossier.md — asset inventory (skills, capital, audience, network).
|
|
6
|
+
|
|
7
|
+
function parseDossier(vault) {
|
|
8
|
+
const filePath = moverDossierPath(vault);
|
|
9
|
+
const text = safeRead(filePath);
|
|
10
|
+
const stat = safeStat(filePath);
|
|
11
|
+
if (!text) return { available: false };
|
|
12
|
+
|
|
13
|
+
const out = {
|
|
14
|
+
available: true,
|
|
15
|
+
filePath,
|
|
16
|
+
lastUpdated: stat ? stat.mtime : null,
|
|
17
|
+
fields: {},
|
|
18
|
+
sections: {}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Bold-key fields
|
|
22
|
+
const re = /\*\*([^*]+):\*\*\s*([^\n]+)/g;
|
|
23
|
+
let m;
|
|
24
|
+
while ((m = re.exec(text)) !== null) {
|
|
25
|
+
const k = m[1].trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_|_$/g, "");
|
|
26
|
+
out.fields[k] = m[2].trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Section split
|
|
30
|
+
const lines = text.split("\n");
|
|
31
|
+
let cur = null, buf = [];
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const h = line.match(/^##+\s+(.+?)\s*$/);
|
|
34
|
+
if (h) {
|
|
35
|
+
if (cur) out.sections[cur.toLowerCase()] = buf.join("\n");
|
|
36
|
+
cur = h[1];
|
|
37
|
+
buf = [];
|
|
38
|
+
} else if (cur) buf.push(line);
|
|
39
|
+
}
|
|
40
|
+
if (cur) out.sections[cur.toLowerCase()] = buf.join("\n");
|
|
41
|
+
|
|
42
|
+
// Common keys
|
|
43
|
+
out.skills = extractList(out.sections, "skills");
|
|
44
|
+
out.capital = extractList(out.sections, "capital");
|
|
45
|
+
out.audience = extractList(out.sections, "audience");
|
|
46
|
+
out.network = extractList(out.sections, "network");
|
|
47
|
+
out.assets = extractList(out.sections, "assets") || extractList(out.sections, "asset inventory");
|
|
48
|
+
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractList(sections, needle) {
|
|
53
|
+
for (const [title, body] of Object.entries(sections)) {
|
|
54
|
+
if (title.includes(needle)) {
|
|
55
|
+
return body.split("\n")
|
|
56
|
+
.map(l => l.match(/^\s*[-*\d.]+\s+(.+)$/))
|
|
57
|
+
.filter(Boolean).map(m => m[1].trim())
|
|
58
|
+
.slice(0, 8);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { parseDossier };
|