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.
Files changed (107) hide show
  1. package/install.js +688 -68
  2. package/package.json +15 -3
  3. package/src/dashboard/build.js +1503 -0
  4. package/src/dashboard/dashboard.js +276 -0
  5. package/src/dashboard/index.js +319 -0
  6. package/src/dashboard/lib/activation-log.js +297 -0
  7. package/src/dashboard/lib/active-context-parser.js +177 -0
  8. package/src/dashboard/lib/agent-command.js +93 -0
  9. package/src/dashboard/lib/agent-detect.js +255 -0
  10. package/src/dashboard/lib/agent-detector.js +92 -0
  11. package/src/dashboard/lib/agent-session.js +462 -0
  12. package/src/dashboard/lib/anti-identity-detector.js +116 -0
  13. package/src/dashboard/lib/auto-learnings-parser.js +183 -0
  14. package/src/dashboard/lib/cli-usage-parser.js +92 -0
  15. package/src/dashboard/lib/config-parser.js +109 -0
  16. package/src/dashboard/lib/connect-recommender.js +131 -0
  17. package/src/dashboard/lib/correlations-parser.js +231 -0
  18. package/src/dashboard/lib/daily-note-resolver.js +211 -0
  19. package/src/dashboard/lib/date-utils.js +35 -0
  20. package/src/dashboard/lib/distribution-parser.js +78 -0
  21. package/src/dashboard/lib/dossier-parser.js +64 -0
  22. package/src/dashboard/lib/drift-history.js +83 -0
  23. package/src/dashboard/lib/drift-score.js +119 -0
  24. package/src/dashboard/lib/engine-health.js +170 -0
  25. package/src/dashboard/lib/engine-writer.js +1685 -0
  26. package/src/dashboard/lib/execution-plan.js +125 -0
  27. package/src/dashboard/lib/experiments-parser.js +429 -0
  28. package/src/dashboard/lib/feed-parser.js +294 -0
  29. package/src/dashboard/lib/forked-future.js +60 -0
  30. package/src/dashboard/lib/goal-forecast.js +304 -0
  31. package/src/dashboard/lib/goals-parser.js +67 -0
  32. package/src/dashboard/lib/health-protocols-parser.js +133 -0
  33. package/src/dashboard/lib/hook-activity.js +48 -0
  34. package/src/dashboard/lib/hook-indexer.js +169 -0
  35. package/src/dashboard/lib/hourly-activity-parser.js +143 -0
  36. package/src/dashboard/lib/identity-parser.js +85 -0
  37. package/src/dashboard/lib/ingestion.js +418 -0
  38. package/src/dashboard/lib/library-indexer-v2.js +212 -0
  39. package/src/dashboard/lib/library-indexer.js +105 -0
  40. package/src/dashboard/lib/log-activation.sh +61 -0
  41. package/src/dashboard/lib/memory-curator.js +97 -0
  42. package/src/dashboard/lib/memory-gardener.js +177 -0
  43. package/src/dashboard/lib/memory-gepa.js +102 -0
  44. package/src/dashboard/lib/memory-index.js +470 -0
  45. package/src/dashboard/lib/memory-rerank.js +72 -0
  46. package/src/dashboard/lib/memory-text.js +136 -0
  47. package/src/dashboard/lib/metrics-log-parser.js +76 -0
  48. package/src/dashboard/lib/moves-usage-parser.js +184 -0
  49. package/src/dashboard/lib/onboarding-forge.js +70 -0
  50. package/src/dashboard/lib/override-outcome-parser.js +68 -0
  51. package/src/dashboard/lib/override-summary.js +73 -0
  52. package/src/dashboard/lib/paths.js +192 -0
  53. package/src/dashboard/lib/pattern-manifest-loader.js +29 -0
  54. package/src/dashboard/lib/phantom-strategy.js +129 -0
  55. package/src/dashboard/lib/project-scanner.js +121 -0
  56. package/src/dashboard/lib/promise-wall.js +88 -0
  57. package/src/dashboard/lib/record-score.js +173 -0
  58. package/src/dashboard/lib/redaction.js +140 -0
  59. package/src/dashboard/lib/refusal-parser.js +44 -0
  60. package/src/dashboard/lib/regenerate-manifest.js +206 -0
  61. package/src/dashboard/lib/rewind-snapshots.js +689 -0
  62. package/src/dashboard/lib/roast-wall-parser.js +200 -0
  63. package/src/dashboard/lib/run-registry.js +226 -0
  64. package/src/dashboard/lib/safe-write.js +63 -0
  65. package/src/dashboard/lib/session-log-parser.js +145 -0
  66. package/src/dashboard/lib/session-time-parser.js +158 -0
  67. package/src/dashboard/lib/skill-index.js +171 -0
  68. package/src/dashboard/lib/skill-indexer.js +118 -0
  69. package/src/dashboard/lib/skill-recommender.js +689 -0
  70. package/src/dashboard/lib/strategy-parser.js +245 -0
  71. package/src/dashboard/lib/strategy-protocol-parser.js +135 -0
  72. package/src/dashboard/lib/streak-parser.js +95 -0
  73. package/src/dashboard/lib/suggested-now.js +254 -0
  74. package/src/dashboard/lib/tool-awareness.js +125 -0
  75. package/src/dashboard/lib/transcript-parser.js +331 -0
  76. package/src/dashboard/lib/vault-graph-parser.js +205 -0
  77. package/src/dashboard/lib/view-generator.js +163 -0
  78. package/src/dashboard/lib/voice-dna-parser.js +57 -0
  79. package/src/dashboard/lib/walkthrough-script.js +140 -0
  80. package/src/dashboard/lib/workflow-graph-parser.js +170 -0
  81. package/src/dashboard/lib/workflow-library-parser.js +146 -0
  82. package/src/dashboard/server.js +2024 -0
  83. package/src/dashboard/shortcut.js +0 -0
  84. package/src/dashboard/static/setup-poc.html +306 -0
  85. package/src/dashboard/static/walkthrough-poc.html +580 -0
  86. package/src/dashboard/styles.css +1201 -0
  87. package/src/dashboard/templates/index.html +278 -0
  88. package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2 +0 -0
  89. package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2 +0 -0
  90. package/src/dashboard/ui/dist/assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2 +0 -0
  91. package/src/dashboard/ui/dist/assets/hero.png +0 -0
  92. package/src/dashboard/ui/dist/assets/index-598CSGOZ.js +157 -0
  93. package/src/dashboard/ui/dist/assets/index-BP--M69H.css +1 -0
  94. package/src/dashboard/ui/dist/assets/index-CidzmwSW.js +34 -0
  95. package/src/dashboard/ui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  96. package/src/dashboard/ui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  97. package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  98. package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  99. package/src/dashboard/ui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  100. package/src/dashboard/ui/dist/assets/mover-system.css +62 -0
  101. package/src/dashboard/ui/dist/assets/xterm-CqkleIqs.js +1 -0
  102. package/src/dashboard/ui/dist/icon.svg +4 -0
  103. package/src/dashboard/ui/dist/index.html +18 -0
  104. package/src/dashboard/ui/dist/manifest.webmanifest +1 -0
  105. package/src/dashboard/ui/dist/registerSW.js +1 -0
  106. package/src/dashboard/ui/dist/sw.js +1 -0
  107. package/src/dashboard/ui/dist/workbox-39fa566e.js +1 -0
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+
3
+ const { strategyPath, safeRead, safeStat } = require("./paths");
4
+ const { localDateKey } = require("./date-utils");
5
+
6
+ // Parse Strategy.md. Extracts hypothesis, single test, sacrifice, validation criteria, fail condition,
7
+ // LAB REPORT changelog, current phase + version.
8
+
9
+ function parseStrategy(vault) {
10
+ const filePath = strategyPath(vault);
11
+ const text = safeRead(filePath);
12
+ const stat = safeStat(filePath);
13
+ if (!text) return { available: false, filePath };
14
+
15
+ const out = {
16
+ available: true,
17
+ filePath,
18
+ lastUpdated: stat ? stat.mtime : null,
19
+ raw: text,
20
+ fields: {}
21
+ };
22
+
23
+ const re = /\*\*([^*]+):\*\*\s*([^\n]+)/g;
24
+ let m;
25
+ while ((m = re.exec(text)) !== null) {
26
+ const k = m[1].trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_|_$/g, "");
27
+ out.fields[k] = m[2].trim();
28
+ }
29
+
30
+ // Header-level fields
31
+ out.version = out.fields.version || null;
32
+ out.lastUpdatedField = out.fields.updated || out.fields.last_updated || null;
33
+ // Target Date — normalize to ISO YYYY-MM-DD so downstream T-MINUS math works.
34
+ // Field can be "June 30th, 2026" / "2026-05-15" / "by May 15" — all coerced.
35
+ const rawTarget = out.fields.target_date || null;
36
+ out.targetDate = rawTarget ? (extractDateFromText(rawTarget) || rawTarget) : null;
37
+ out.primeObjective = out.fields.prime_objective || null;
38
+ out.stretchTarget = out.fields.stretch_target || null;
39
+ out.currentState = out.fields.current_state || null;
40
+
41
+ // Single Test extraction — V4 uses **Mid-term Milestone:**, older versions
42
+ // used **Single Test:** / **The Bet:**. Try V4 first, fall back to legacy
43
+ // so the live dashboard parser keeps working across Strategy.md versions.
44
+ const milestoneMatch = text.match(/\*\*Mid-term Milestone:\*\*\s+(.+?)(?=\n\*\*|\n##|$)/is);
45
+ if (milestoneMatch) {
46
+ out.singleTest = milestoneMatch[1].trim().replace(/\s+/g, " ");
47
+ } else {
48
+ const stMatch = text.match(/\*\*(?:Single Test|The Bet):?\*\*\s*"?([^"\n]+)"?/i);
49
+ out.singleTest = stMatch ? stMatch[1].trim().replace(/^"|"$/g, "") : null;
50
+ }
51
+
52
+ // Pull deadline date out of the Single Test prose if Target Date is absent.
53
+ // V4 text says "by June 30, 2026" → ISO 2026-06-30. Older V3 said "by May 15".
54
+ if (!out.targetDate && out.singleTest) {
55
+ out.targetDate = extractDateFromText(out.singleTest);
56
+ }
57
+
58
+ // Sacrifice extraction — V4 uses "**The Sacrifice (V4 — hardened past V3.0):**"
59
+ // followed by a bulleted list. Old format was inline text after "**Sacrifice:**".
60
+ const sacrV4 = text.match(/\*\*The Sacrifice[^*]*\*\*\s*\n([\s\S]+?)(?=\n\*\*\w[^*]*\*\*|\n##\s|$)/i);
61
+ if (sacrV4) {
62
+ out.sacrificeBlock = sacrV4[1].trim();
63
+ out.sacrificeItems = sacrV4[1].split("\n")
64
+ .map(l => l.replace(/^\s*[*-]\s+/, "").trim())
65
+ .filter(l => l && !l.startsWith("**"))
66
+ .slice(0, 4);
67
+ } else {
68
+ const sacMatch = text.match(/\*\*The Sacrifice:?\*\*([\s\S]*?)(?=\n##|\n###|\n\*\*|$)/i);
69
+ out.sacrificeBlock = sacMatch ? sacMatch[1].trim() : null;
70
+ out.sacrificeItems = out.sacrificeBlock
71
+ ? out.sacrificeBlock.split("\n")
72
+ .map(l => l.match(/^\s*[-*]\s+(.+)$/))
73
+ .filter(Boolean).map(m => m[1].trim())
74
+ : [];
75
+ }
76
+
77
+ // Validation + Fail
78
+ const valMatch = text.match(/\*\*✅ Validation Criteria:?\*\*\s*([^\n]+)/i);
79
+ out.validationCriteria = valMatch ? valMatch[1].trim() : null;
80
+ const failMatch = text.match(/\*\*❌ Fail Condition:?\*\*\s*([^\n]+)/i);
81
+ out.failCondition = failMatch ? failMatch[1].trim() : null;
82
+
83
+ // Hypothesis section body. NOTE the `m` flag — without it `^` matches
84
+ // start of STRING not start of LINE, so the section headers below the
85
+ // first line never match. Found via dashboard CURRENT STRATEGIES empty.
86
+ // T222: Strategy V4+ leads Section 1 with a `**Context:**` block
87
+ // summarizing why the PRIOR version failed (literally "V3.0 Single Test
88
+ // FAILED (4/10 by May 15)..."). The dashboard was rendering this as if
89
+ // it were the current hypothesis, leaking the V3 May 15 date into the UI.
90
+ // Skip past the retrospective preamble — start from **Status:**, or
91
+ // **The Positioning** if Status is absent, falling back to the full
92
+ // section for older Strategy.md files that don't follow V4 layout.
93
+ let body = extractSection(text, /^##\s+1\.\s+BUSINESS HYPOTHESIS/im);
94
+ if (body) {
95
+ const statusIdx = body.search(/^\*\*Status:\*\*/m);
96
+ const positioningIdx = body.search(/^\*\*The Positioning/m);
97
+ const skipTo = statusIdx >= 0 ? statusIdx
98
+ : positioningIdx >= 0 ? positioningIdx
99
+ : -1;
100
+ if (skipTo > 0) {
101
+ body = body.slice(skipTo).trim();
102
+ }
103
+ }
104
+ out.hypothesisBody = body;
105
+
106
+ // Pivot Itch Defense
107
+ out.pivotItchDefense = extractSection(text, /\bPivot Itch\b/im);
108
+
109
+ // LAB REPORT changelog entries
110
+ out.labReport = extractLabReport(text);
111
+
112
+ // Bio-algorithm + Mindset hypotheses (for vitality/mindset domains)
113
+ out.bioAlgorithmBody = extractSection(text, /^##\s+2\.\s+THE BIO-ALGORITHM/im);
114
+ out.mindsetBody = extractSection(text, /^##\s+3\.\s+MINDSET HYPOTHESIS/im);
115
+
116
+ // Combined: per-domain strategies. Used by Briefing CURRENT STRATEGIES
117
+ // cards instead of Goals.md horizons (user feedback: should come from
118
+ // Strategy.md, not Goals.md).
119
+ out.hypotheses = [
120
+ { domain: 'WORK', body: out.hypothesisBody || '' },
121
+ { domain: 'VITALITY', body: out.bioAlgorithmBody || '' },
122
+ { domain: 'FAITH / PURPOSE', body: out.mindsetBody || '' },
123
+ ].filter(h => h.body && h.body.trim().length > 20);
124
+
125
+ // Per-domain MEASUREMENT declaration (single | chain | scalar | streak), written
126
+ // by the onboarding Scribe inside each domain section so the dashboard dispatches
127
+ // on the shape. Header NAMES differ between the hand-authored V4 file (BUSINESS
128
+ // HYPOTHESIS / THE BIO-ALGORITHM) and the fresh Scribe file (WORK / VITALITY
129
+ // HYPOTHESIS), so match on the section NUMBER, not the name.
130
+ out.measurement = {
131
+ work: readMeasurementDecl(extractSection(text, /^##\s+1\.\s+/im)),
132
+ vitality: readMeasurementDecl(extractSection(text, /^##\s+2\.\s+/im)),
133
+ faith: readMeasurementDecl(extractSection(text, /^##\s+3\.\s+/im)),
134
+ };
135
+
136
+ // Archived hypotheses (graveyard)
137
+ out.archived = extractGraveyardEntries(text);
138
+
139
+ return out;
140
+ }
141
+
142
+ // Read a "**Measurement:** <type>" declaration (plus a scalar baseline/target or a
143
+ // chain's reverse-engineering math) from a domain section body. Returns
144
+ // { type, baseline?, target?, chain? } or null. The honest placeholder
145
+ // "_(single | chain | scalar | streak, set once ...)_" contains all four words, so
146
+ // it is rejected: only a real, LEADING bare type counts as a declaration.
147
+ function readMeasurementDecl(body) {
148
+ if (!body) return null;
149
+ const mm = body.match(/\*\*Measurement:\*\*\s*([^\n]+)/i);
150
+ if (!mm) return null;
151
+ const tm = mm[1].trim().match(/^(single|chain|scalar|streak)\b/i); // anchored: the value IS the type
152
+ if (!tm) return null; // a "_(...)_" placeholder or no real declaration
153
+ const decl = { type: tm[1].toLowerCase() };
154
+ const real = (s) => (s && !/^_?\(/.test(s.trim()) ? s.trim() : null); // skip "_(...)_" placeholders
155
+ const base = body.match(/\*\*Baseline:\*\*\s*([^\n]+)/i);
156
+ const targ = body.match(/\*\*Target:\*\*\s*([^\n]+)/i);
157
+ const chain = body.match(/\*\*Chain:\*\*\s*([^\n]+)/i);
158
+ if (base && real(base[1])) decl.baseline = real(base[1]);
159
+ if (targ && real(targ[1])) decl.target = real(targ[1]);
160
+ if (chain && real(chain[1])) decl.chain = real(chain[1]);
161
+ return decl;
162
+ }
163
+
164
+ // Extract a date from arbitrary prose. Handles ISO YYYY-MM-DD, "by Month Day",
165
+ // "Month Day", "Month Day, YYYY". Returns ISO string or null. Assumes the
166
+ // current year if not specified.
167
+ function extractDateFromText(text) {
168
+ if (!text) return null;
169
+ // ISO date first — most specific
170
+ const iso = text.match(/\b(20\d{2}-[01]\d-[0-3]\d)\b/);
171
+ if (iso) return iso[1];
172
+
173
+ // "by Month Day(,? Year)?"
174
+ //
175
+ // T242 bug hunt fix (B#7): regex alternation is left-to-right. Previous
176
+ // order put short prefixes first ("jan", "feb", ...) so "january 5" only
177
+ // captured "jan" — then the day-capture group started at "uary 5", which
178
+ // luckily still found "5" via the lazy `\d{1,2}` advance. The bug surfaces
179
+ // when the input is "sept 5": short "sep" matched, capture continues at
180
+ // "t 5" — `\d` finds nothing, then engine backtracks. Fragile. Reorder
181
+ // to longest-first so "september", "sept" match before "sep".
182
+ const MONTHS = [
183
+ "january","february","march","april","may","june","july","august","september","october","november","december",
184
+ "sept","jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"
185
+ ];
186
+ const monthRe = MONTHS.join("|");
187
+ const m = text.match(new RegExp(`\\b(?:by\\s+)?(${monthRe})\\s+(\\d{1,2})(?:,?\\s*(20\\d{2}))?`, "i"));
188
+ if (m) {
189
+ const mIdx = monthIndex(m[1]);
190
+ if (mIdx < 0) return null;
191
+ const day = parseInt(m[2], 10);
192
+ if (!Number.isFinite(day) || day < 1 || day > 31) return null;
193
+ const year = m[3] ? parseInt(m[3], 10) : new Date().getFullYear();
194
+ const d = new Date(year, mIdx, day);
195
+ if (isNaN(d.getTime())) return null;
196
+ return localDateKey(d);
197
+ }
198
+ return null;
199
+ }
200
+
201
+ function monthIndex(name) {
202
+ const n = name.toLowerCase().slice(0, 3);
203
+ const map = { jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5, jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11 };
204
+ return map[n] != null ? map[n] : -1;
205
+ }
206
+
207
+ function extractSection(text, headerRe) {
208
+ const m = text.match(headerRe);
209
+ if (!m) return null;
210
+ const startIdx = m.index;
211
+ const startLineEnd = text.indexOf("\n", startIdx);
212
+ const after = text.slice(startLineEnd + 1);
213
+ const nextHeader = after.search(/^##\s+/m);
214
+ return nextHeader === -1 ? after.trim() : after.slice(0, nextHeader).trim();
215
+ }
216
+
217
+ function extractLabReport(text) {
218
+ const idx = text.search(/^##\s*🔄?\s*LAB REPORT/im);
219
+ if (idx === -1) return [];
220
+ const after = text.slice(idx);
221
+ const next = after.slice(after.indexOf("\n") + 1).search(/^##\s+/m);
222
+ const block = next === -1 ? after : after.slice(0, after.indexOf("\n") + 1 + next);
223
+ const lines = block.split("\n");
224
+ const entries = [];
225
+ for (const line of lines) {
226
+ const m = line.match(/^[-*]\s+\*\*\[([\d\-]+)\]:?\*\*\s*(.+)$/);
227
+ if (m) entries.push({ date: m[1], summary: m[2].trim() });
228
+ }
229
+ return entries;
230
+ }
231
+
232
+ function extractGraveyardEntries(text) {
233
+ const idx = text.search(/^##\s*⚰️?\s*ARCHIVED HYPOTHESES|^##\s*The Graveyard/im);
234
+ if (idx === -1) return [];
235
+ const after = text.slice(idx);
236
+ const lines = after.split("\n");
237
+ const out = [];
238
+ for (const line of lines) {
239
+ const m = line.match(/^###\s+(.+?)\s*$/);
240
+ if (m) out.push({ name: m[1].trim() });
241
+ }
242
+ return out;
243
+ }
244
+
245
+ module.exports = { parseStrategy, extractDateFromText };
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+
3
+ const { strategyPath, safeRead } = require("./paths");
4
+
5
+ // strategy-protocol-parser — extracts the "Daily floor" / "Daily minimum
6
+ // output" items per domain from Strategy.md. This is the SOURCE OF TRUTH
7
+ // for the Execution Protocol module on the Briefing route.
8
+ //
9
+ // Strategy.md is organized in sections:
10
+ // ## 1. BUSINESS HYPOTHESIS (V3.x) → Work domain
11
+ // ## 2. THE BIO-ALGORITHM (Vitality) → Vitality domain
12
+ // ## 3. MINDSET HYPOTHESIS (V2.x — The Anchor Reset) → Faith domain
13
+ //
14
+ // Per section, we extract:
15
+ // - Daily minimum output (if present) — bullet of required actions
16
+ // - Daily floor (bad days) — single-line bullet of bare minimum
17
+ // Both lines are split into individual checkboxes (sentences / commas).
18
+
19
+ function parseStrategyProtocol(vault) {
20
+ const text = safeRead(strategyPath(vault));
21
+ if (!text) return { available: false };
22
+
23
+ const sections = splitByDomain(text);
24
+
25
+ return {
26
+ available: true,
27
+ work: extractDomainItems(sections.work, "Work"),
28
+ vitality: extractDomainItems(sections.vitality, "Vitality"),
29
+ faith: extractDomainItems(sections.faith, "Faith"),
30
+ };
31
+ }
32
+
33
+ // Slice Strategy.md into 3 buckets by the major section headings.
34
+ // Uses string indexOf instead of regex with emoji classes (which are
35
+ // fragile across multi-codepoint glyphs like ⚰️).
36
+ function splitByDomain(text) {
37
+ const out = { work: "", vitality: "", faith: "" };
38
+
39
+ const businessIdx = text.search(/##\s*1\.\s*BUSINESS HYPOTHESIS/i);
40
+ const bioIdx = text.search(/##\s*2\.\s*THE BIO-ALGORITHM/i);
41
+ const mindsetIdx = text.search(/##\s*3\.\s*MINDSET HYPOTHESIS/i);
42
+
43
+ // Next-section heuristic: any `\n##` after the mindset start, excluding
44
+ // the mindset header itself. We split on `\n##` and pick the slice
45
+ // starting with "MINDSET HYPOTHESIS" then stop at the next `\n##`.
46
+ let mindsetEnd = text.length;
47
+ if (mindsetIdx >= 0) {
48
+ const tail = text.slice(mindsetIdx + 5); // skip "## 3."
49
+ const next = tail.search(/\n##\s/);
50
+ if (next > 0) mindsetEnd = mindsetIdx + 5 + next;
51
+ }
52
+
53
+ if (businessIdx >= 0 && bioIdx > businessIdx) {
54
+ out.work = text.slice(businessIdx, bioIdx);
55
+ }
56
+ if (bioIdx >= 0 && mindsetIdx > bioIdx) {
57
+ out.vitality = text.slice(bioIdx, mindsetIdx);
58
+ }
59
+ if (mindsetIdx >= 0) {
60
+ out.faith = text.slice(mindsetIdx, mindsetEnd);
61
+ }
62
+
63
+ return out;
64
+ }
65
+
66
+ // Within a domain section, find "Daily floor" and "Daily minimum output" lines
67
+ // and produce checklist items.
68
+ function extractDomainItems(section, domainLabel) {
69
+ if (!section) return { available: false, items: [] };
70
+ const items = [];
71
+
72
+ // Daily minimum output — multi-item, often a few comma- or slash-separated.
73
+ const minMatch = section.match(/\*\*Daily minimum output:\*\*\s*([^\n]+)/i);
74
+ if (minMatch) {
75
+ splitIntoBullets(minMatch[1]).forEach(text => {
76
+ items.push({ text, source: "Daily minimum output", emphasis: "primary" });
77
+ });
78
+ }
79
+
80
+ // Daily floor (bad days) — single floor item, sometimes multi-clause.
81
+ const floorMatch = section.match(/\*\*Daily floor[^:]*:\*\*\s*([^\n]+)/i);
82
+ if (floorMatch) {
83
+ splitIntoBullets(floorMatch[1]).forEach(text => {
84
+ items.push({ text, source: "Daily floor", emphasis: "floor" });
85
+ });
86
+ }
87
+
88
+ return {
89
+ available: items.length > 0,
90
+ domain: domainLabel,
91
+ items,
92
+ };
93
+ }
94
+
95
+ // Split a content line like "3 tweets/day + 1 thread/week" or
96
+ // "Mg glycinate + Vit D3 taken. 3L water. No melatonin." into separate
97
+ // human-readable bullet items.
98
+ const QUALIFIER_BLACKLIST = new Set([
99
+ "non-negotiable", "non negotiable",
100
+ "no excuses", "always", "never",
101
+ "tbd", "etc", "etc.",
102
+ ]);
103
+
104
+ function splitIntoBullets(line) {
105
+ const cleaned = line
106
+ .replace(/\.$/, "")
107
+ .trim()
108
+ .replace(/\*\*/g, "");
109
+
110
+ // Split on:
111
+ // - periods between sentences (not inside numbers)
112
+ // - " + " when followed by an uppercase letter (separator vs equation)
113
+ // - " · " bullets
114
+ // - " | " pipes
115
+ const parts = cleaned
116
+ .split(/(?:\s*\.(?=\s+[A-Z0-9])|\s*[·|]\s*|\s+\+\s+(?=[A-Z]))/)
117
+ .map(p => p.trim().replace(/\.$/, ""))
118
+ .filter(p => p.length >= 3 && p.length <= 140)
119
+ // Reject pure qualifier strings — these aren't actionable items
120
+ .filter(p => !QUALIFIER_BLACKLIST.has(p.toLowerCase()));
121
+
122
+ // De-dupe and cap at 5 per domain
123
+ const seen = new Set();
124
+ const out = [];
125
+ for (const p of parts) {
126
+ const k = p.toLowerCase();
127
+ if (seen.has(k)) continue;
128
+ seen.add(k);
129
+ out.push(p);
130
+ if (out.length >= 5) break;
131
+ }
132
+ return out;
133
+ }
134
+
135
+ module.exports = { parseStrategyProtocol };
@@ -0,0 +1,95 @@
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
+ // streak-parser.js — F34. 365-day Daily Note presence map. Each day is one
9
+ // cell. Used by StreakHeatmap.tsx for GitHub-style grid visualization.
10
+
11
+ function compute({ vault, daysLookback = 365 } = {}) {
12
+ if (!vault) return { available: false, days: [], currentStreak: 0, longestStreak: 0 };
13
+ const dDir = dailiesDir(vault);
14
+ if (!fs.existsSync(dDir)) return { available: false, days: [], currentStreak: 0, longestStreak: 0 };
15
+
16
+ const today = localMidnight();
17
+ const days = [];
18
+ let currentStreak = 0;
19
+ let streakThroughYesterday = 0;
20
+ let longestStreak = 0;
21
+ let runningStreak = 0;
22
+ let counted = 0;
23
+
24
+ for (let i = 0; i < daysLookback; i++) {
25
+ const d = new Date(today.getTime() - i * 24 * 3600 * 1000);
26
+ const key = localDateKey(d);
27
+ const monthDir = key.slice(0, 7);
28
+ const fpath = path.join(dDir, monthDir, `Daily - ${key}.md`);
29
+ let exists = false;
30
+ let size = 0;
31
+ try {
32
+ const stat = fs.statSync(fpath);
33
+ exists = true;
34
+ size = stat.size;
35
+ counted++;
36
+ } catch (_) { /* missing */ }
37
+
38
+ days.push({ date: key, exists, size });
39
+
40
+ // longestStreak is any run's max; runningStreak resets on each gap.
41
+ if (exists) {
42
+ runningStreak++;
43
+ if (runningStreak > longestStreak) longestStreak = runningStreak;
44
+ } else {
45
+ runningStreak = 0;
46
+ }
47
+ }
48
+
49
+ // currentStreak is the consecutive run ENDING TODAY, so it is computed in one
50
+ // dedicated forward pass that stops at the first gap — never mutated inside
51
+ // the scan above. The old interleaved update overwrote it with a LATER (older)
52
+ // run's length: a map like [T,T,T, gap, T,T,T,T,T,T] reported 6 instead of 3,
53
+ // because once the scan re-entered a run after a gap it re-assigned
54
+ // currentStreak while its `> 0` guard still held from the today-run.
55
+ if (days[0] && days[0].exists) {
56
+ for (let i = 0; i < days.length; i++) {
57
+ if (!days[i].exists) break;
58
+ currentStreak++;
59
+ }
60
+ }
61
+
62
+ // If today's Daily Note is missing, keep the preserved streak separately.
63
+ // `currentStreak` remains strict (today must exist); `streakThroughYesterday`
64
+ // lets the UI say "21-day streak waiting on today's note" instead of
65
+ // implying the history vanished at 00:01.
66
+ if (days[0] && !days[0].exists) {
67
+ for (let i = 1; i < days.length; i++) {
68
+ if (!days[i].exists) break;
69
+ streakThroughYesterday++;
70
+ }
71
+ } else {
72
+ streakThroughYesterday = currentStreak;
73
+ }
74
+
75
+ // T242 bug hunt fix (C#17): capture todayExists BEFORE reversing. JS
76
+ // object literal property evaluation is left-to-right, so `days: days.reverse()`
77
+ // mutates the array in place before `todayExists: days[0]` reads it —
78
+ // `days[0]` post-reverse is the OLDEST day, not today. Previous code
79
+ // reported `todayExists: false` (oldest day's existence) regardless
80
+ // of today's actual state — disagreeing with daily.today.exists for
81
+ // months.
82
+ const todayExists = !!(days[0] && days[0].exists);
83
+ return {
84
+ available: true,
85
+ days: days.reverse(),
86
+ currentStreak,
87
+ streakThroughYesterday,
88
+ todayExists,
89
+ longestStreak,
90
+ totalDays: counted,
91
+ coverage: counted / daysLookback
92
+ };
93
+ }
94
+
95
+ module.exports = { compute };