mover-os 4.7.7 → 4.7.9
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/README.md +34 -24
- package/install.js +2868 -251
- package/package.json +15 -3
- package/src/dashboard/build.js +1541 -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 +189 -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 +483 -0
- package/src/dashboard/lib/anti-identity-detector.js +116 -0
- package/src/dashboard/lib/approval-registry.js +170 -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 +228 -0
- package/src/dashboard/lib/date-utils.js +43 -0
- package/src/dashboard/lib/distribution-parser.js +137 -0
- package/src/dashboard/lib/dossier-parser.js +64 -0
- package/src/dashboard/lib/drift-history.js +88 -0
- package/src/dashboard/lib/drift-score.js +119 -0
- package/src/dashboard/lib/engine-default-fingerprints.json +53 -0
- package/src/dashboard/lib/engine-health.js +173 -0
- package/src/dashboard/lib/engine-writer.js +1831 -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 +427 -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 +226 -0
- package/src/dashboard/lib/library-indexer.js +105 -0
- package/src/dashboard/lib/library-search.js +290 -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 +519 -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/pid-markers.js +80 -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 +286 -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/state-core/backfill.js +298 -0
- package/src/dashboard/lib/state-core/dryrun.js +188 -0
- package/src/dashboard/lib/state-core/event-log.js +615 -0
- package/src/dashboard/lib/state-core/events.js +265 -0
- package/src/dashboard/lib/state-core/projections.js +376 -0
- package/src/dashboard/lib/state-core/start-close.js +162 -0
- package/src/dashboard/lib/state-core/trial.js +96 -0
- package/src/dashboard/lib/strategy-parser.js +248 -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 +371 -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 +2402 -0
- package/src/dashboard/shortcut.js +284 -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-ByVKPvLf.js +34 -0
- package/src/dashboard/ui/dist/assets/index-CCoKjUcC.js +161 -0
- package/src/dashboard/ui/dist/assets/index-CZWNQDt5.css +1 -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,294 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// feed-parser.js — the live work feed. A plain sentence per real work event.
|
|
4
|
+
//
|
|
5
|
+
// SOURCE (live, local, never telemetry):
|
|
6
|
+
// 1. ~/.claude/history.jsonl — one line per real prompt the user typed into a
|
|
7
|
+
// Claude Code session: { display, timestamp(ms), project, sessionId }.
|
|
8
|
+
// Each line IS a real work event (a thing that actually happened in a real
|
|
9
|
+
// session under this vault). We tail the last N and turn each into a plain
|
|
10
|
+
// sentence.
|
|
11
|
+
// 2. ~/.claude/sessions/*.json — live session descriptors:
|
|
12
|
+
// { pid, sessionId, cwd, startedAt(ms), updatedAt(ms), status, version,
|
|
13
|
+
// name, ... }. These tell us which sessions are open right now.
|
|
14
|
+
//
|
|
15
|
+
// SCOPING (Codex must-fix / v6 amendment fix 8 nice-to-have): resolve the
|
|
16
|
+
// project slug AND each session cwd to a REALPATH and test containment against
|
|
17
|
+
// THIS vault's realpath — NOT a loose `project` string match. A symlinked or
|
|
18
|
+
// case-differing vault path still scopes correctly; an unrelated project that
|
|
19
|
+
// happens to share a path prefix string does not leak in.
|
|
20
|
+
//
|
|
21
|
+
// HARD HONESTY (v6-PLAN-AMENDMENTS fix 8 + Fable Finding C — binding):
|
|
22
|
+
// - NO `cost` field. There is no per-step token source. We do not invent one.
|
|
23
|
+
// - NO `diff` field. There is no per-step diff source. The only file signal we
|
|
24
|
+
// emit is, at most, a real path that already appears in the user's own text.
|
|
25
|
+
// Each event is a plain sentence + ids. Nothing more.
|
|
26
|
+
//
|
|
27
|
+
// FALSE-ZERO (T261 discipline): if nothing real is happening under this vault —
|
|
28
|
+
// no in-vault history events AND no in-vault sessions — return
|
|
29
|
+
// { available:false, events:[], activeSessions:[], caption:'' }.
|
|
30
|
+
// We never fabricate scripted demo steps to fill an empty feed.
|
|
31
|
+
|
|
32
|
+
const fs = require("fs");
|
|
33
|
+
const os = require("os");
|
|
34
|
+
const path = require("path");
|
|
35
|
+
const { redactText } = require("./redaction");
|
|
36
|
+
|
|
37
|
+
const HISTORY_FILE = path.join(os.homedir(), ".claude", "history.jsonl");
|
|
38
|
+
const SESSIONS_DIR = path.join(os.homedir(), ".claude", "sessions");
|
|
39
|
+
|
|
40
|
+
// A session counts as "active" only if its descriptor was touched recently.
|
|
41
|
+
// Claude writes `updatedAt` on every turn; a stale file is a dead process whose
|
|
42
|
+
// descriptor was never cleaned up. 30 min is generous for "this is live".
|
|
43
|
+
const ACTIVE_WINDOW_MS = 30 * 60 * 1000;
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// realpath-based vault scoping
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
// Resolve a path to its realpath when it exists on disk; fall back to a
|
|
50
|
+
// lexical normalize (resolves `..`/`.` without touching disk) so a recorded
|
|
51
|
+
// path for a since-deleted dir still scopes sanely. Never throws.
|
|
52
|
+
function resolveReal(p) {
|
|
53
|
+
if (!p || typeof p !== "string") return null;
|
|
54
|
+
try {
|
|
55
|
+
return fs.realpathSync.native(p);
|
|
56
|
+
} catch (_) {
|
|
57
|
+
try {
|
|
58
|
+
return path.resolve(p);
|
|
59
|
+
} catch (_) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// True when `child` is the same path as, or nested under, `root`. Compares on a
|
|
66
|
+
// path-segment boundary (root + sep) so `/a/Mover OS` does NOT match
|
|
67
|
+
// `/a/Mover OS Bundle 2`. Case-insensitive on darwin/win32 where the FS is.
|
|
68
|
+
function isUnder(child, root) {
|
|
69
|
+
if (!child || !root) return false;
|
|
70
|
+
const ci = process.platform === "darwin" || process.platform === "win32";
|
|
71
|
+
let c = ci ? child.toLowerCase() : child;
|
|
72
|
+
let r = ci ? root.toLowerCase() : root;
|
|
73
|
+
if (c === r) return true;
|
|
74
|
+
const withSep = r.endsWith(path.sep) ? r : r + path.sep;
|
|
75
|
+
return c.startsWith(withSep);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// event classification — plain, honest, source-backed
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
// A leading slash means the user invoked a workflow / command — a deliberate
|
|
83
|
+
// "move". Everything else is a typed prompt to the agent terminal.
|
|
84
|
+
function classify(display) {
|
|
85
|
+
const text = (display || "").trim();
|
|
86
|
+
if (text.startsWith("/")) {
|
|
87
|
+
// /plan-tomorrow, /log, /compact ... → kind is the bare command token.
|
|
88
|
+
const cmd = text.slice(1).split(/[\s\n]/, 1)[0].toLowerCase();
|
|
89
|
+
return { lane: "move", kind: cmd ? `/${cmd}` : "command" };
|
|
90
|
+
}
|
|
91
|
+
return { lane: "term", kind: "prompt" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Turn a raw multi-line prompt into one clean redacted sentence. We keep the
|
|
95
|
+
// user's own words (that's the honest signal) — only collapse whitespace, trim
|
|
96
|
+
// to a readable length, and run the privacy scrubber over the result.
|
|
97
|
+
function toSentence(display) {
|
|
98
|
+
let t = (display || "").replace(/\s+/g, " ").trim();
|
|
99
|
+
if (!t) return "";
|
|
100
|
+
// Trim to a feed-readable length on a word boundary; mark truncation with an
|
|
101
|
+
// ellipsis so it never reads as a fabricated complete thought.
|
|
102
|
+
const MAX = 180;
|
|
103
|
+
if (t.length > MAX) {
|
|
104
|
+
const cut = t.slice(0, MAX);
|
|
105
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
106
|
+
t = (lastSpace > 80 ? cut.slice(0, lastSpace) : cut).trim() + "…";
|
|
107
|
+
}
|
|
108
|
+
return redactText(t).text;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// session descriptors
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
function readSessions(vaultReal) {
|
|
116
|
+
let names;
|
|
117
|
+
try {
|
|
118
|
+
names = fs.readdirSync(SESSIONS_DIR);
|
|
119
|
+
} catch (_) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
const sessions = [];
|
|
124
|
+
for (const name of names) {
|
|
125
|
+
if (!name.endsWith(".json")) continue;
|
|
126
|
+
let desc;
|
|
127
|
+
try {
|
|
128
|
+
desc = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, name), "utf8"));
|
|
129
|
+
} catch (_) {
|
|
130
|
+
continue; // skip mid-write / corrupt descriptor
|
|
131
|
+
}
|
|
132
|
+
if (!desc || typeof desc !== "object") continue;
|
|
133
|
+
const cwdReal = resolveReal(desc.cwd);
|
|
134
|
+
const isThisVault = isUnder(cwdReal, vaultReal);
|
|
135
|
+
const updatedAt = Number(desc.updatedAt) || Number(desc.startedAt) || 0;
|
|
136
|
+
// status: trust the descriptor when present; otherwise infer from recency.
|
|
137
|
+
// A descriptor with no `status`/`updatedAt` that hasn't been touched in the
|
|
138
|
+
// active window is a leftover from a closed process — mark it "idle".
|
|
139
|
+
let status = typeof desc.status === "string" && desc.status ? desc.status : null;
|
|
140
|
+
const fresh = updatedAt > 0 && now - updatedAt <= ACTIVE_WINDOW_MS;
|
|
141
|
+
if (!status) status = fresh ? "active" : "idle";
|
|
142
|
+
sessions.push({
|
|
143
|
+
name: desc.name || "",
|
|
144
|
+
cwd: desc.cwd || "",
|
|
145
|
+
status,
|
|
146
|
+
version: desc.version || "",
|
|
147
|
+
startedAt: Number(desc.startedAt) ? new Date(Number(desc.startedAt)).toISOString() : null,
|
|
148
|
+
updatedAt: updatedAt ? new Date(updatedAt).toISOString() : null,
|
|
149
|
+
isThisVault: Boolean(isThisVault),
|
|
150
|
+
_fresh: fresh,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// Most-recently-active first.
|
|
154
|
+
sessions.sort((a, b) => (b.updatedAt || "").localeCompare(a.updatedAt || ""));
|
|
155
|
+
return sessions;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Map a sessionId → a friendly name from the live descriptors, so a feed row
|
|
159
|
+
// can show "landing-polar-overhaul" instead of a bare uuid.
|
|
160
|
+
function buildSessionNameMap() {
|
|
161
|
+
const map = new Map();
|
|
162
|
+
let names;
|
|
163
|
+
try {
|
|
164
|
+
names = fs.readdirSync(SESSIONS_DIR);
|
|
165
|
+
} catch (_) {
|
|
166
|
+
return map;
|
|
167
|
+
}
|
|
168
|
+
for (const name of names) {
|
|
169
|
+
if (!name.endsWith(".json")) continue;
|
|
170
|
+
try {
|
|
171
|
+
const desc = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, name), "utf8"));
|
|
172
|
+
if (desc && desc.sessionId && desc.name) map.set(desc.sessionId, desc.name);
|
|
173
|
+
} catch (_) {
|
|
174
|
+
/* skip */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return map;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// history events (tailed, scoped, redacted)
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
function readEvents(vaultReal, limit, sessionNames) {
|
|
185
|
+
let raw;
|
|
186
|
+
try {
|
|
187
|
+
raw = fs.readFileSync(HISTORY_FILE, "utf8");
|
|
188
|
+
} catch (_) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const lines = raw.split("\n");
|
|
192
|
+
// Memoize realpath per distinct `project` string — history has thousands of
|
|
193
|
+
// lines but only a handful of distinct projects, and realpath hits disk.
|
|
194
|
+
const projectScopeCache = new Map();
|
|
195
|
+
const inVault = (project) => {
|
|
196
|
+
if (projectScopeCache.has(project)) return projectScopeCache.get(project);
|
|
197
|
+
const real = resolveReal(project);
|
|
198
|
+
const ok = isUnder(real, vaultReal);
|
|
199
|
+
projectScopeCache.set(project, ok);
|
|
200
|
+
return ok;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const events = [];
|
|
204
|
+
// Walk newest → oldest, collect up to `limit` in-vault events, then flip to
|
|
205
|
+
// chronological. Tailing from the end keeps this cheap on a large history.
|
|
206
|
+
for (let i = lines.length - 1; i >= 0 && events.length < limit; i--) {
|
|
207
|
+
const line = lines[i];
|
|
208
|
+
if (!line) continue;
|
|
209
|
+
let obj;
|
|
210
|
+
try {
|
|
211
|
+
obj = JSON.parse(line);
|
|
212
|
+
} catch (_) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const project = obj.project;
|
|
216
|
+
if (!project || !inVault(project)) continue;
|
|
217
|
+
|
|
218
|
+
const display = obj.display;
|
|
219
|
+
const text = toSentence(display);
|
|
220
|
+
if (!text) continue; // nothing real to say
|
|
221
|
+
|
|
222
|
+
const tsMs = Number(obj.timestamp) || 0;
|
|
223
|
+
const { lane, kind } = classify(display);
|
|
224
|
+
const sessionId = obj.sessionId || "";
|
|
225
|
+
events.push({
|
|
226
|
+
ts: tsMs ? new Date(tsMs).toISOString() : null,
|
|
227
|
+
sessionId,
|
|
228
|
+
sessionName: (sessionId && sessionNames.get(sessionId)) || "",
|
|
229
|
+
lane, // 'move' | 'term'
|
|
230
|
+
kind,
|
|
231
|
+
text,
|
|
232
|
+
project,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
events.reverse(); // chronological (oldest of the tail → newest)
|
|
236
|
+
return events;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// public API
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
// compute({ vault, limit }) → live work-feed payload.
|
|
244
|
+
// available false when nothing real is happening under this vault
|
|
245
|
+
// caption one honest line summarizing the feed (or '')
|
|
246
|
+
// events [{ ts, sessionId, sessionName, lane, kind, text, project }]
|
|
247
|
+
// activeSessions [{ name, cwd, status, version, startedAt, updatedAt, isThisVault }]
|
|
248
|
+
//
|
|
249
|
+
// No `cost`, no `diff` — there is no per-step source for either (binding).
|
|
250
|
+
function compute({ vault, limit = 40 } = {}) {
|
|
251
|
+
const EMPTY = { available: false, caption: "", events: [], activeSessions: [] };
|
|
252
|
+
if (!vault || typeof vault !== "string") return EMPTY;
|
|
253
|
+
|
|
254
|
+
const vaultReal = resolveReal(vault);
|
|
255
|
+
if (!vaultReal) return EMPTY;
|
|
256
|
+
|
|
257
|
+
const lim = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 40;
|
|
258
|
+
|
|
259
|
+
const sessions = readSessions(vaultReal);
|
|
260
|
+
const sessionNames = buildSessionNameMap();
|
|
261
|
+
const events = readEvents(vaultReal, lim, sessionNames);
|
|
262
|
+
|
|
263
|
+
// Active sessions surfaced to the UI = sessions running under THIS vault.
|
|
264
|
+
const activeSessions = sessions
|
|
265
|
+
.filter((s) => s.isThisVault)
|
|
266
|
+
.map(({ _fresh, ...s }) => s); // drop internal field
|
|
267
|
+
|
|
268
|
+
// False-zero discipline: a feed with no in-vault events AND no in-vault
|
|
269
|
+
// sessions is genuinely "nothing running" → unavailable, never a demo.
|
|
270
|
+
if (events.length === 0 && activeSessions.length === 0) return EMPTY;
|
|
271
|
+
|
|
272
|
+
// Honest caption from real counts only. "live" = a descriptor this vault owns
|
|
273
|
+
// that is currently active/busy (recently touched).
|
|
274
|
+
const liveNow = sessions.filter((s) => s.isThisVault && (s.status === "active" || s.status === "busy")).length;
|
|
275
|
+
let caption;
|
|
276
|
+
if (events.length === 0) {
|
|
277
|
+
caption = liveNow > 0 ? `${liveNow} session${liveNow === 1 ? "" : "s"} open — no events yet` : "";
|
|
278
|
+
} else {
|
|
279
|
+
const moves = events.filter((e) => e.lane === "move").length;
|
|
280
|
+
const parts = [`${events.length} recent event${events.length === 1 ? "" : "s"}`];
|
|
281
|
+
if (moves > 0) parts.push(`${moves} move${moves === 1 ? "" : "s"}`);
|
|
282
|
+
if (liveNow > 0) parts.push(`${liveNow} live`);
|
|
283
|
+
caption = parts.join(" · ");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
available: true,
|
|
288
|
+
caption,
|
|
289
|
+
events,
|
|
290
|
+
activeSessions,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = { compute };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// forked-future.js — Two-path drift projection for the Briefing module.
|
|
4
|
+
// Each scenario is a confidence-coned sparkline:
|
|
5
|
+
// path A: "if you hold the pattern" — extrapolates current drift trajectory
|
|
6
|
+
// path B: "if you break the pattern" — drift falls toward baseline (15-20)
|
|
7
|
+
//
|
|
8
|
+
// Pure function: takes current drift score + recent trajectory + active
|
|
9
|
+
// patterns, returns two arrays of {value, lo, hi} points across N days.
|
|
10
|
+
|
|
11
|
+
function compute({ driftScore = null, recent7d = [], activePatterns = [], horizonDays = 14 } = {}) {
|
|
12
|
+
if (!driftScore) return null;
|
|
13
|
+
|
|
14
|
+
const current = driftScore.value;
|
|
15
|
+
const baseline = 15; // healthy floor target
|
|
16
|
+
const ceiling = 100;
|
|
17
|
+
const trendDelta = computeTrend(recent7d);
|
|
18
|
+
const patternWeight = Math.min(1, (activePatterns || []).filter(p => p.confidence >= 3).length * 0.15);
|
|
19
|
+
|
|
20
|
+
// Path A: hold the pattern. Trajectory continues at observed slope, with
|
|
21
|
+
// pattern weight pulling toward ceiling (active Conf 3+ patterns add drift).
|
|
22
|
+
const slopeA = trendDelta + 0.6 + patternWeight * 0.8;
|
|
23
|
+
const holdPath = [];
|
|
24
|
+
for (let i = 0; i <= horizonDays; i++) {
|
|
25
|
+
const v = Math.min(ceiling, Math.max(0, current + slopeA * i));
|
|
26
|
+
const spread = 3 + i * 0.8; // widening confidence cone
|
|
27
|
+
holdPath.push({ day: i, value: round(v), lo: round(v - spread), hi: round(Math.min(ceiling, v + spread)) });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Path B: break the pattern. Drift decays toward baseline at -2/day (faster
|
|
31
|
+
// initially), with smaller confidence cone (the action is causal).
|
|
32
|
+
const breakPath = [];
|
|
33
|
+
for (let i = 0; i <= horizonDays; i++) {
|
|
34
|
+
const v = baseline + (current - baseline) * Math.pow(0.85, i);
|
|
35
|
+
const spread = 2 + i * 0.4;
|
|
36
|
+
breakPath.push({ day: i, value: round(v), lo: round(Math.max(0, v - spread)), hi: round(v + spread) });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
horizonDays,
|
|
41
|
+
current,
|
|
42
|
+
baseline,
|
|
43
|
+
holdPath,
|
|
44
|
+
breakPath,
|
|
45
|
+
interpretation: {
|
|
46
|
+
hold: `${slopeA >= 0.6 ? 'Drift climbs' : 'Drift drifts'} toward ${holdPath[holdPath.length - 1].value}/100 in ${horizonDays} days if pattern continues.`,
|
|
47
|
+
break: `Drift falls toward ${breakPath[breakPath.length - 1].value}/100 if the next 3 sessions break the pattern.`
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function computeTrend(values) {
|
|
53
|
+
if (!values || values.length < 2) return 0;
|
|
54
|
+
// Simple slope: last - first / count
|
|
55
|
+
return (values[values.length - 1] - values[0]) / Math.max(1, values.length - 1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function round(v) { return Math.round(v * 10) / 10; }
|
|
59
|
+
|
|
60
|
+
module.exports = { compute };
|