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,29 @@
1
+ "use strict";
2
+
3
+ const { moverPatternManifestPath, safeReadJson, safeStat } = require("./paths");
4
+
5
+ // Fast-path read of ~/.mover/pattern-manifest.json. Returns {available, manifest, lastUpdated}.
6
+
7
+ function loadPatternManifest() {
8
+ const filePath = moverPatternManifestPath();
9
+ const stat = safeStat(filePath);
10
+ const data = safeReadJson(filePath);
11
+ if (!data) return { available: false, filePath, error: "pattern-manifest.json not found" };
12
+
13
+ return {
14
+ available: true,
15
+ filePath,
16
+ lastUpdated: stat ? stat.mtime : null,
17
+ manifest: data,
18
+ singleTest: data.single_test || null,
19
+ sacrifice: data.sacrifice || null,
20
+ frictionLevel: data.friction_level || "L0",
21
+ zombieCount: data.zombie_count || 0,
22
+ zombieTasks: Array.isArray(data.zombie_tasks) ? data.zombie_tasks : [],
23
+ activePatterns: Array.isArray(data.active_patterns) ? data.active_patterns : [],
24
+ banKeywords: Array.isArray(data.ban_keywords) ? data.ban_keywords : [],
25
+ overrideHistory: Array.isArray(data.override_history) ? data.override_history : []
26
+ };
27
+ }
28
+
29
+ module.exports = { loadPatternManifest };
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+
3
+ // phantom-strategy.js — Comparator between STATED strategy (Strategy.md
4
+ // sacrifice + Single Test) and DEMONSTRATED behavior (manifest overrides
5
+ // + Auto_Learnings patterns + recent session deviations).
6
+ //
7
+ // Returns ranked PhantomRow[] with severity-encoded mismatches and source
8
+ // citations. Pure function: takes parsed inputs, returns array.
9
+
10
+ function compute({ strategy = null, autoLearnings = null, manifest = null, dailyHistory = null } = {}) {
11
+ const rows = [];
12
+ const now = Date.now();
13
+ const weekMs = 7 * 24 * 3600 * 1000;
14
+ const monthMs = 30 * 24 * 3600 * 1000;
15
+
16
+ // Helper: count recent override entries that touch a particular text fragment.
17
+ const overrides = (manifest && (manifest.overrideHistory || manifest.override_history)) || [];
18
+ function recentOverridesMatching(stated, sinceMs) {
19
+ const fragment = stated.slice(0, 24).toLowerCase();
20
+ return overrides.filter((o) => {
21
+ const t = Date.parse(o.date || o.timestamp || "");
22
+ if (!Number.isFinite(t) || now - t > sinceMs) return false;
23
+ const haystack = ((o.reason || "") + " " + (o.what || "") + " " + (o.plan_item || "")).toLowerCase();
24
+ return haystack.includes(fragment);
25
+ });
26
+ }
27
+
28
+ function getAutoLearningPattern(...keywords) {
29
+ if (!autoLearnings || !autoLearnings.activePatternsConf3Plus) return null;
30
+ const lower = keywords.map((k) => k.toLowerCase());
31
+ return autoLearnings.activePatternsConf3Plus.find((p) => {
32
+ const desc = ((p.description || p.text || "") + " " + (p.id || "")).toLowerCase();
33
+ return lower.some((k) => desc.includes(k));
34
+ });
35
+ }
36
+
37
+ function pushRow(id, stated, demonstrated, severity, sources, evidence = {}) {
38
+ rows.push({
39
+ id,
40
+ stated: String(stated).slice(0, 200),
41
+ demonstrated: String(demonstrated).slice(0, 200),
42
+ severity,
43
+ evidence: {
44
+ sessions: evidence.sessions || 0,
45
+ distGap: evidence.distGap || 0,
46
+ sources: sources || []
47
+ }
48
+ });
49
+ }
50
+
51
+ // 1) Single Test mismatch
52
+ const singleTest = (manifest && manifest.singleTest) || (strategy && strategy.singleTest);
53
+ if (singleTest) {
54
+ const distPattern = getAutoLearningPattern("distribution", "polish", "system architecture", "spree");
55
+ const matches = recentOverridesMatching(singleTest, monthMs);
56
+ const severity = distPattern ? "high" : matches.length > 0 ? "med" : "low";
57
+ pushRow(
58
+ "single-test",
59
+ singleTest,
60
+ distPattern
61
+ ? `Pattern detected: ${distPattern.description || distPattern.text || "behavioral drift"}`
62
+ : matches.length > 0
63
+ ? `Overridden ${matches.length}× in last 30 days`
64
+ : "Holding (no logged override).",
65
+ severity,
66
+ [
67
+ ...(distPattern
68
+ ? [{ file: "Auto_Learnings.md", line: "active_patterns", quote: distPattern.description || "", age: "live" }]
69
+ : []),
70
+ ...matches.slice(0, 3).map((m) => ({
71
+ file: "pattern-manifest.json",
72
+ line: "override_history",
73
+ quote: m.reason || m.what || "—",
74
+ age: m.date || "recent"
75
+ }))
76
+ ],
77
+ { sessions: matches.length }
78
+ );
79
+ }
80
+
81
+ // 2) Sacrifice rules
82
+ const sacrifices = manifest && manifest.sacrifice
83
+ ? (Array.isArray(manifest.sacrifice) ? manifest.sacrifice : [manifest.sacrifice])
84
+ : strategy && strategy.sacrifice
85
+ ? (Array.isArray(strategy.sacrifice) ? strategy.sacrifice : [strategy.sacrifice])
86
+ : [];
87
+
88
+ sacrifices.slice(0, 3).forEach((s, i) => {
89
+ const matches = recentOverridesMatching(s, weekMs);
90
+ const severity = matches.length >= 2 ? "high" : matches.length === 1 ? "med" : "low";
91
+ pushRow(
92
+ `sacrifice-${i}`,
93
+ s,
94
+ matches.length > 0
95
+ ? `Broken ${matches.length}× in last 7 days`
96
+ : "Holding (no override this week).",
97
+ severity,
98
+ matches.slice(0, 2).map((m) => ({
99
+ file: "pattern-manifest.json",
100
+ line: "override_history",
101
+ quote: m.reason || m.what || "—",
102
+ age: m.date || "recent"
103
+ })),
104
+ { sessions: matches.length }
105
+ );
106
+ });
107
+
108
+ // 3) Weekly Review hygiene (if Strategy says weekly but it's been >7d)
109
+ if (strategy && strategy.fields && /weekly|friday|review/i.test(JSON.stringify(strategy.fields))) {
110
+ // If we had analyseDay overdue in inputs, we'd surface that too. For now,
111
+ // skip unless we can wire engineHealth in.
112
+ }
113
+
114
+ // 4) Distribution-related patterns from Auto_Learnings (catch what Strategy doesn't name)
115
+ const distPattern = getAutoLearningPattern("system architecture spree");
116
+ if (distPattern && !rows.some((r) => r.id === "single-test" && r.severity === "high")) {
117
+ pushRow(
118
+ "system-spree",
119
+ "Distribution work over system polish.",
120
+ distPattern.description || distPattern.text || "System Architecture Spree pattern active.",
121
+ distPattern.confidence >= 4 ? "high" : "med",
122
+ [{ file: "Auto_Learnings.md", line: `pattern:${distPattern.id || "spree"}`, quote: distPattern.description || "", age: `conf ${distPattern.confidence || "?"}` }]
123
+ );
124
+ }
125
+
126
+ return rows.slice(0, 6); // cap at 6 for UI density
127
+ }
128
+
129
+ module.exports = { compute };
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+
3
+ const path = require("path");
4
+ const { projectsDir, safeListDir, safeRead, safeStat, exists } = require("./paths");
5
+
6
+ // Scan 01_Projects/* for active projects. Returns {available, projects: [...]}.
7
+ // Each project has plan task counts, recent project_state.md excerpt, lastModified.
8
+
9
+ function scanProjects(vault, options = {}) {
10
+ const dir = projectsDir(vault);
11
+ const entries = safeListDir(dir, { dirsOnly: true, skipUnderscore: true });
12
+ const limit = options.limit || 12;
13
+
14
+ const projects = [];
15
+ for (const entry of entries) {
16
+ const projectPath = entry.path;
17
+ const planMd = exists(path.join(projectPath, "dev", "plan.md"))
18
+ ? path.join(projectPath, "dev", "plan.md")
19
+ : (exists(path.join(projectPath, "plan.md")) ? path.join(projectPath, "plan.md") : null);
20
+ const stateMd = exists(path.join(projectPath, "dev", "project_state.md"))
21
+ ? path.join(projectPath, "dev", "project_state.md")
22
+ : (exists(path.join(projectPath, "project_state.md")) ? path.join(projectPath, "project_state.md") : null);
23
+ const briefMd = exists(path.join(projectPath, "dev", "project_brief.md"))
24
+ ? path.join(projectPath, "dev", "project_brief.md")
25
+ : (exists(path.join(projectPath, "project_brief.md")) ? path.join(projectPath, "project_brief.md") : null);
26
+
27
+ const project = {
28
+ name: entry.name,
29
+ path: projectPath,
30
+ planPath: planMd,
31
+ statePath: stateMd,
32
+ briefPath: briefMd,
33
+ hasPlanMd: !!planMd,
34
+ hasStateMd: !!stateMd
35
+ };
36
+
37
+ const stat = safeStat(projectPath);
38
+ project.lastModified = stat ? stat.mtime : null;
39
+
40
+ if (planMd) {
41
+ const planText = safeRead(planMd, "");
42
+ const tasks = parseTasks(planText);
43
+ project.totalTasks = tasks.total;
44
+ project.doneTasks = tasks.done;
45
+ project.unverifiedTasks = tasks.unverified;
46
+ project.openTasks = tasks.open;
47
+ project.zombieTasks = tasks.zombieCount;
48
+ project.recentExecutionLog = extractRecentExecutionLog(planText, 3);
49
+ }
50
+
51
+ if (stateMd) {
52
+ const stateText = safeRead(stateMd, "");
53
+ project.stateExcerpt = extractFirstParagraph(stateText);
54
+ }
55
+
56
+ projects.push(project);
57
+ }
58
+
59
+ projects.sort((a, b) => {
60
+ const aT = a.lastModified ? a.lastModified.getTime() : 0;
61
+ const bT = b.lastModified ? b.lastModified.getTime() : 0;
62
+ return bT - aT;
63
+ });
64
+
65
+ // activeCount = folders touched inside the dashboard's 30-day evidence
66
+ // window. totalCount counts every folder ever (incl. one-off errands), which
67
+ // reads as a lie next to "tests that earned real time".
68
+ const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
69
+ const activeCount = projects.filter((p) => p.lastModified && p.lastModified.getTime() >= cutoff).length;
70
+ return { available: true, projects: projects.slice(0, limit), totalCount: projects.length, activeCount };
71
+ }
72
+
73
+ function parseTasks(text) {
74
+ const lines = text.split("\n");
75
+ let total = 0, done = 0, unverified = 0, open = 0, zombieCount = 0;
76
+ for (const line of lines) {
77
+ const m = line.match(/^\s*[-*]\s*\[([ xX~])\]/);
78
+ if (!m) continue;
79
+ total++;
80
+ if (m[1] === "x" || m[1] === "X") done++;
81
+ else if (m[1] === "~") unverified++;
82
+ else open++;
83
+ if (/\[ZOMBIE\]/i.test(line)) zombieCount++;
84
+ }
85
+ return { total, done, unverified, open, zombieCount };
86
+ }
87
+
88
+ function extractFirstParagraph(text) {
89
+ if (!text) return null;
90
+ const lines = text.split("\n");
91
+ let collecting = false;
92
+ let buf = [];
93
+ for (const line of lines) {
94
+ if (/^#\s+/.test(line)) continue;
95
+ if (line.trim() === "") {
96
+ if (collecting) break;
97
+ continue;
98
+ }
99
+ if (/^[-*]\s+/.test(line)) continue;
100
+ collecting = true;
101
+ buf.push(line.trim());
102
+ if (buf.join(" ").length > 280) break;
103
+ }
104
+ return buf.join(" ").slice(0, 280);
105
+ }
106
+
107
+ function extractRecentExecutionLog(text, count) {
108
+ const lines = text.split("\n");
109
+ const idx = lines.findIndex(l => /^##\s*EXECUTION LOG/i.test(l));
110
+ if (idx === -1) return [];
111
+ const after = lines.slice(idx + 1);
112
+ const items = [];
113
+ for (const line of after) {
114
+ if (/^##\s+/.test(line)) break;
115
+ const m = line.match(/^[-*]\s+\[?(\d{4}-\d{2}-\d{2})\]?\s*(.+)$/);
116
+ if (m) items.push({ date: m[1], text: m[2].trim() });
117
+ }
118
+ return items.slice(-count).reverse();
119
+ }
120
+
121
+ module.exports = { scanProjects };
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+
3
+ // promise-wall.js — Aggregator for Promise Wall module.
4
+ // Pulls from Active_Context COMMITMENTS (S8.5) + WAITING FOR (S3.5) +
5
+ // Daily Notes commitments. Classifies by recipient + age. Pure function:
6
+ // takes already-parsed inputs and returns structured promise data.
7
+
8
+ function compute({ activeContext = null, dailyNoteHistory = [] }) {
9
+ const out = {
10
+ commitments: [],
11
+ waitingFor: [],
12
+ byRecipient: {},
13
+ overdue: [],
14
+ upcoming: [],
15
+ fulfilled: [],
16
+ stats: { total: 0, overdue: 0, dueSoon: 0, openWeekPlus: 0 }
17
+ };
18
+
19
+ if (!activeContext || !activeContext.available) return out;
20
+
21
+ const now = Date.now();
22
+ const dayMs = 24 * 3600 * 1000;
23
+
24
+ // Outbound commitments (things YOU promised to others)
25
+ (activeContext.commitments || []).forEach((c) => {
26
+ const entry = {
27
+ kind: "commitment",
28
+ text: c.text || c.what || "",
29
+ to: c.to || c.recipient || "—",
30
+ due: c.due || c.dueBy || null,
31
+ added: c.added || c.created || null,
32
+ ageDays: c.added ? Math.max(0, Math.floor((now - Date.parse(c.added)) / dayMs)) : 0,
33
+ };
34
+ entry.status = classifyStatus(entry, now);
35
+ out.commitments.push(entry);
36
+ bucketByRecipient(out.byRecipient, entry);
37
+ if (entry.status === "overdue") out.overdue.push(entry);
38
+ if (entry.status === "due-soon") out.upcoming.push(entry);
39
+ });
40
+
41
+ // Inbound waiting-for (things you're owed)
42
+ (activeContext.waitingFor || []).forEach((w) => {
43
+ const entry = {
44
+ kind: "waiting-for",
45
+ text: w.text || w.what || "",
46
+ from: w.from || w.who || "—",
47
+ expected: w.expected || w.expectedBy || null,
48
+ added: w.added || w.created || null,
49
+ ageDays: w.added ? Math.max(0, Math.floor((now - Date.parse(w.added)) / dayMs)) : 0,
50
+ };
51
+ entry.status = classifyStatus(entry, now);
52
+ out.waitingFor.push(entry);
53
+ bucketByRecipient(out.byRecipient, entry);
54
+ if (entry.status === "overdue") out.overdue.push(entry);
55
+ if (entry.status === "due-soon") out.upcoming.push(entry);
56
+ });
57
+
58
+ out.stats.total = out.commitments.length + out.waitingFor.length;
59
+ out.stats.overdue = out.overdue.length;
60
+ out.stats.dueSoon = out.upcoming.length;
61
+ out.stats.openWeekPlus = [...out.commitments, ...out.waitingFor].filter(e => e.ageDays >= 7).length;
62
+
63
+ return out;
64
+ }
65
+
66
+ function classifyStatus(entry, now) {
67
+ const dueRef = entry.due || entry.expected;
68
+ if (!dueRef) {
69
+ // Untimed — judge by age only.
70
+ if (entry.ageDays >= 14) return "overdue";
71
+ if (entry.ageDays >= 7) return "due-soon";
72
+ return "open";
73
+ }
74
+ const dueT = Date.parse(dueRef);
75
+ if (!Number.isFinite(dueT)) return "open";
76
+ const daysOut = Math.floor((dueT - now) / (24 * 3600 * 1000));
77
+ if (daysOut < 0) return "overdue";
78
+ if (daysOut <= 2) return "due-soon";
79
+ return "open";
80
+ }
81
+
82
+ function bucketByRecipient(bucket, entry) {
83
+ const key = entry.to || entry.from || "—";
84
+ if (!bucket[key]) bucket[key] = [];
85
+ bucket[key].push(entry);
86
+ }
87
+
88
+ module.exports = { compute };
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+
3
+ // record-score.js — the journal-calibration gap. What the RECORD says your
4
+ // day was (derived purely from logged signal), vs the self-score you'd give
5
+ // it. The dashboard surfaces the delta so a "felt like a 7" can meet a
6
+ // "logged like a 3" — or the reverse.
7
+ //
8
+ // computeRecordScore({ daily, distribution, strategyProtocol }) reads two
9
+ // honest signals and nothing else:
10
+ // 1. plan-done ratio — daily.today.tasksDone / daily.today.tasksTotal
11
+ // 2. ship/distribution today — distribution.todayCount
12
+ //
13
+ // FALSE-ZERO (binding, T261 discipline): "no tasks logged AND no
14
+ // distribution today" is INSUFFICIENT DATA, not a bad day. It returns
15
+ // { score: null, why: null } — never a fabricated 4. A real 0/N plan with
16
+ // 0 ships is a measured low day and DOES score (low), because the tasks
17
+ // were logged. The distinction is absence-of-record vs record-of-nothing.
18
+ //
19
+ // strategyProtocol is accepted for signature parity with the other parsers
20
+ // build.js wires (and to let the basis string note whether a protocol
21
+ // exists), but it is NOT load-bearing for the number — the score must stay
22
+ // defensible from the two hard signals above.
23
+
24
+ // ── The 1-10 mapping ──────────────────────────────────────────────────────
25
+ //
26
+ // Two components, then a blend that lets either one carry a day.
27
+ //
28
+ // PLAN component (0..10), only when tasksTotal > 0:
29
+ // ratio = tasksDone / tasksTotal, clamped to [0,1].
30
+ // plan = round(ratio * 9) + 1 → 0% maps to 1 (a logged dead day is a 1,
31
+ // not a 0; 0 is reserved for "no record"),
32
+ // 100% maps to 10. Linear between.
33
+ //
34
+ // SHIP component (0..10), from distribution.todayCount (a ship/outreach
35
+ // count — see distribution-parser.js KEYWORDS):
36
+ // 0 ships → 0 (no lift; it cannot, alone, drag a completed plan down —
37
+ // see blend)
38
+ // 1 ship → 6 (you put something into the world; floor of a "real" day)
39
+ // 2 ships → 8
40
+ // 3+ ships→ 10 (capped)
41
+ // This is deliberately generous: distribution is the scarce, high-leverage
42
+ // act in this system, so each unit of it is worth a lot.
43
+ //
44
+ // BLEND:
45
+ // - Both signals present → max(plan, ship). The day is as good as its best
46
+ // real axis; shipping rescues a thin plan, a full plan rescues a quiet
47
+ // ship-day. Taking the max (not the mean) avoids punishing a 10/10 plan
48
+ // for a no-outreach day, and avoids punishing a 3-ship day for a sparse
49
+ // task list.
50
+ // - Plan only (ships unknown/absent) → plan.
51
+ // - Ships only (no tasks logged, but ships happened) → ship, floored at 1
52
+ // so a logged ship is never a "1-or-below" by rounding.
53
+ // - Neither → null (false-zero).
54
+ //
55
+ // Final score is clamped to [1,10] and rounded to a whole number — a journal
56
+ // score is a gut integer, not 6.4.
57
+
58
+ function computeRecordScore({ daily, distribution, strategyProtocol } = {}) {
59
+ // ── Read the two hard signals, distinguishing absent from zero ──────────
60
+
61
+ // Plan: only "logged" when today's note exists AND has a task list.
62
+ // daily / daily.today may be null (build.js daily mapper, lines ~681-704).
63
+ const today = daily && daily.today ? daily.today : null;
64
+ const tasksTotal = today && Number.isFinite(today.tasksTotal) ? today.tasksTotal : 0;
65
+ const tasksDone = today && Number.isFinite(today.tasksDone) ? today.tasksDone : 0;
66
+ // A plan only counts as "logged" when there are tasks to grade against.
67
+ // 0/0 is an empty note — no record — not a 0% day.
68
+ const planLogged = tasksTotal > 0;
69
+
70
+ // Ships: distribution.todayCount is number|null. null = parser couldn't
71
+ // measure (no Dailies dir / unavailable), which is NOT zero. Only a real
72
+ // finite number is a measurement. [false-zero — mirrors goal-forecast.js:38]
73
+ const distAvailable = !!(distribution && distribution.available !== false);
74
+ const shipsToday = distAvailable && Number.isFinite(distribution.todayCount)
75
+ ? distribution.todayCount
76
+ : null;
77
+ const shipsKnown = shipsToday != null;
78
+
79
+ // ── FALSE-ZERO GATE (binding) ───────────────────────────────────────────
80
+ // No tasks logged AND no distribution measured today → not enough to score.
81
+ // Never a fabricated middle-of-the-road 4.
82
+ if (!planLogged && (!shipsKnown || shipsToday === 0)) {
83
+ // One more split: if ships were *measured* as 0 we still bail (no plan +
84
+ // 0 ships = nothing recorded happened). If ships are simply unknown, same
85
+ // outcome. Both are "nothing to grade."
86
+ return {
87
+ score: null,
88
+ why: null,
89
+ basis: "not enough logged to score today",
90
+ };
91
+ }
92
+
93
+ // ── Component scores ────────────────────────────────────────────────────
94
+ let planScore = null;
95
+ if (planLogged) {
96
+ const ratio = clamp(tasksDone / tasksTotal, 0, 1);
97
+ planScore = Math.round(ratio * 9) + 1; // 0%→1, 100%→10
98
+ }
99
+
100
+ let shipScore = null;
101
+ if (shipsKnown) {
102
+ shipScore = shipsToday <= 0 ? 0
103
+ : shipsToday === 1 ? 6
104
+ : shipsToday === 2 ? 8
105
+ : 10; // 3+
106
+ }
107
+
108
+ // ── Blend ───────────────────────────────────────────────────────────────
109
+ let raw;
110
+ if (planScore != null && shipScore != null) {
111
+ raw = Math.max(planScore, shipScore);
112
+ } else if (planScore != null) {
113
+ raw = planScore;
114
+ } else {
115
+ // ships-only path: a logged ship with no task list. Floor at 1 so a
116
+ // single ship can't round below the "1 = a real but minimal day" baseline
117
+ // (shipScore for 1 ship is already 6, so this floor only matters
118
+ // defensively).
119
+ raw = Math.max(1, shipScore);
120
+ }
121
+
122
+ const score = clamp(Math.round(raw), 1, 10);
123
+
124
+ // ── Explain the number in plain language ────────────────────────────────
125
+ const why = buildWhy({ planLogged, tasksDone, tasksTotal, shipsKnown, shipsToday, planScore, shipScore, score });
126
+
127
+ // basis = the provenance string, names which signals were actually used
128
+ // (and whether a protocol was available for context — not used in math).
129
+ const parts = [];
130
+ if (planLogged) parts.push("plan-done ratio");
131
+ if (shipsKnown) parts.push("distribution today");
132
+ const protoNote = strategyProtocol && strategyProtocol.available ? " (protocol present)" : "";
133
+ const basis = parts.length
134
+ ? `derived from ${parts.join(" + ")}${protoNote}`
135
+ : "derived from logged signal";
136
+
137
+ return { score, why, basis };
138
+ }
139
+
140
+ // ── helpers ─────────────────────────────────────────────────────────────────
141
+
142
+ function clamp(n, min, max) {
143
+ if (!Number.isFinite(n)) return min;
144
+ return Math.min(max, Math.max(min, n));
145
+ }
146
+
147
+ function buildWhy({ planLogged, tasksDone, tasksTotal, shipsKnown, shipsToday, planScore, shipScore, score }) {
148
+ const bits = [];
149
+ if (planLogged) {
150
+ const pct = Math.round((clamp(tasksDone / tasksTotal, 0, 1)) * 100);
151
+ bits.push(`${tasksDone}/${tasksTotal} plan tasks done (${pct}%)`);
152
+ }
153
+ if (shipsKnown) {
154
+ const n = shipsToday;
155
+ bits.push(n === 0 ? "no distribution logged today" : `${n} ship${n === 1 ? "" : "s"} logged today`);
156
+ }
157
+
158
+ // Name the carrier when blending, so the user sees WHY it landed where it did.
159
+ // Only claim something "carried" the day when it actually beat the other axis
160
+ // by enough to matter; ties (incl. a low/low dead day) just say they agree.
161
+ let carrier = "";
162
+ if (planScore != null && shipScore != null) {
163
+ if (shipScore > planScore) carrier = ". Shipping carried the day over a thin plan";
164
+ else if (planScore > shipScore) carrier = ". Plan completion carried it over a quiet ship-day";
165
+ else carrier = ". Plan and shipping agree";
166
+ }
167
+
168
+ // "an 8/10" — article matches the score's spoken vowel sound (8/11/18), else "a".
169
+ const art = /^(8|11|18)/.test(String(score)) ? "an" : "a";
170
+ return `Record scores today ${art} ${score}/10: ${bits.join(", ")}${carrier}. Compare against how the day felt.`;
171
+ }
172
+
173
+ module.exports = { computeRecordScore };
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+
3
+ // redaction.js — Privacy-first scrubber for Public Proof share cards.
4
+ // Allowlist-based: anything NOT explicitly allowed is redacted.
5
+ // Default redaction set covers personal goals, faith, weight, money,
6
+ // named people, project names, dates. Per ui-ux-audit: "redaction must
7
+ // feel trustworthy, not paranoid." Pure function — takes content +
8
+ // settings, returns redacted content + per-field provenance.
9
+
10
+ const DEFAULT_REDACTIONS = {
11
+ names: true,
12
+ faith: true,
13
+ weight: true,
14
+ money: true,
15
+ projectNames: true,
16
+ exactDates: false,
17
+ fileNames: true,
18
+ };
19
+
20
+ // Patterns that match commonly sensitive content. Each one returns a
21
+ // redacted replacement keeping the structure of the original visible.
22
+ const PATTERNS = {
23
+ // Personal names — capitalized two-word sequences. Best-effort heuristic.
24
+ names: {
25
+ re: /\b([A-Z][a-z]+) ([A-Z][a-z]+)\b/g,
26
+ replace: () => "[NAME]",
27
+ },
28
+ // Common faith markers
29
+ faith: {
30
+ re: /\b(Fajr|Dhuhr|Asr|Maghrib|Isha|Tahajjud|Quran|Hadith|Allah|Iman|Salah|Wudu)\b/gi,
31
+ replace: () => "[FAITH]",
32
+ },
33
+ // Weight references (kg/lb with numbers)
34
+ weight: {
35
+ re: /\b(\d{2,3})\s*(kg|kilos?|lbs?|pounds?)\b/gi,
36
+ replace: () => "[WEIGHT]",
37
+ },
38
+ // Money — $, £, €, ¥ with numbers, or "X dollars/pounds"
39
+ money: {
40
+ re: /(?:[$£€¥]\s?\d[\d,.]*\b|\b\d[\d,.]*\s*(?:dollars?|pounds?|euros?|GBP|USD|EUR)\b)/gi,
41
+ replace: () => "[MONEY]",
42
+ },
43
+ // Project names — anything in `01_Projects/Foo/`
44
+ projectNames: {
45
+ re: /01_Projects\/([^/\s)]+)/g,
46
+ replace: () => "01_Projects/[PROJECT]",
47
+ },
48
+ // Exact dates YYYY-MM-DD
49
+ exactDates: {
50
+ re: /\b20\d{2}-[01]\d-[0-3]\d\b/g,
51
+ replace: () => "[DATE]",
52
+ },
53
+ // File names with extensions
54
+ fileNames: {
55
+ re: /\b([A-Za-z0-9_-]{2,40})\.(md|txt|js|ts|tsx|jsx|json|yaml|toml|sh)\b/g,
56
+ replace: (_m, base) => `[FILE].${_m.split('.').pop()}`,
57
+ },
58
+ };
59
+
60
+ // Apply redactions to a string. Returns { text, replacements: { kind: count } }.
61
+ function redactText(text, settings = {}) {
62
+ const cfg = Object.assign({}, DEFAULT_REDACTIONS, settings);
63
+ if (!text || typeof text !== "string") return { text: text || "", replacements: {} };
64
+ let out = text;
65
+ const replacements = {};
66
+ for (const [key, pat] of Object.entries(PATTERNS)) {
67
+ if (!cfg[key]) continue;
68
+ let count = 0;
69
+ out = out.replace(pat.re, (...args) => {
70
+ count++;
71
+ return pat.replace(...args);
72
+ });
73
+ if (count > 0) replacements[key] = count;
74
+ }
75
+ return { text: out, replacements };
76
+ }
77
+
78
+ // Redact a Phantom share card payload — strips names from stated/demonstrated
79
+ // while preserving severity + source counts.
80
+ function redactPhantomCard(card, settings = {}) {
81
+ const stated = redactText(card.stated || "", settings);
82
+ const demonstrated = redactText(card.demonstrated || "", settings);
83
+ return {
84
+ ...card,
85
+ stated: stated.text,
86
+ demonstrated: demonstrated.text,
87
+ evidence: card.evidence
88
+ ? {
89
+ ...card.evidence,
90
+ sources: (card.evidence.sources || []).map((s) => ({
91
+ ...s,
92
+ file: redactText(s.file || "", settings).text,
93
+ quote: redactText(s.quote || "", settings).text,
94
+ })),
95
+ }
96
+ : undefined,
97
+ redactionReplacements: {
98
+ stated: stated.replacements,
99
+ demonstrated: demonstrated.replacements,
100
+ },
101
+ };
102
+ }
103
+
104
+ // Redact a full Wrapped/share data payload for export.
105
+ function redactWrappedPayload(payload, settings = {}) {
106
+ if (!payload || typeof payload !== "object") return payload;
107
+ const out = JSON.parse(JSON.stringify(payload));
108
+ walk(out, settings);
109
+ return out;
110
+ }
111
+
112
+ function walk(node, settings) {
113
+ if (!node) return;
114
+ if (Array.isArray(node)) {
115
+ for (let i = 0; i < node.length; i++) {
116
+ if (typeof node[i] === "string") {
117
+ node[i] = redactText(node[i], settings).text;
118
+ } else {
119
+ walk(node[i], settings);
120
+ }
121
+ }
122
+ return;
123
+ }
124
+ if (typeof node === "object") {
125
+ for (const k of Object.keys(node)) {
126
+ if (typeof node[k] === "string") {
127
+ node[k] = redactText(node[k], settings).text;
128
+ } else {
129
+ walk(node[k], settings);
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ module.exports = {
136
+ DEFAULT_REDACTIONS,
137
+ redactText,
138
+ redactPhantomCard,
139
+ redactWrappedPayload,
140
+ };