pi-mega-compact 0.7.8 → 0.8.0
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 +11 -12
- package/dist/extensions/dashboard-server/html.js +1023 -0
- package/dist/extensions/dashboard-server/html.test.js +41 -0
- package/dist/extensions/dashboard-server/index-reader.js +133 -0
- package/dist/extensions/dashboard-server/server.js +530 -0
- package/dist/extensions/dashboard-server/server.test.js +120 -0
- package/dist/extensions/dashboard-server/snapshot.js +43 -0
- package/dist/extensions/dashboard-server/state.js +30 -0
- package/dist/extensions/dashboard-server/types.js +5 -0
- package/dist/extensions/dashboard-server-s32.test.js +181 -0
- package/dist/extensions/dashboard-server.js +7 -1315
- package/dist/extensions/mega-commands.js +162 -134
- package/dist/extensions/mega-compact.js +3 -0
- package/dist/extensions/mega-compact.test.js +90 -21
- package/dist/extensions/mega-conflict-cmds.js +5 -1
- package/dist/extensions/mega-dashboard-cmds.js +29 -22
- package/dist/extensions/mega-db-cmds.js +11 -2
- package/dist/extensions/mega-events/agent-handlers.js +222 -0
- package/dist/extensions/mega-events/compact-handlers.js +162 -0
- package/dist/extensions/mega-events/context-handler.js +249 -0
- package/dist/extensions/mega-events/register.js +21 -0
- package/dist/extensions/mega-events/session-handlers.js +142 -0
- package/dist/extensions/mega-events.js +15 -699
- package/dist/extensions/mega-game-cmds.js +106 -0
- package/dist/extensions/mega-game-cmds.test.js +113 -0
- package/dist/extensions/mega-pipeline/compact.js +324 -0
- package/dist/extensions/mega-pipeline/memory-review.js +38 -0
- package/dist/extensions/mega-pipeline/recall.js +147 -0
- package/dist/extensions/mega-pipeline.js +9 -480
- package/dist/extensions/mega-runtime/helpers.js +40 -0
- package/dist/extensions/mega-runtime/query.js +29 -0
- package/dist/extensions/mega-runtime/state.js +877 -0
- package/dist/extensions/mega-runtime/state.test.js +171 -0
- package/dist/extensions/mega-runtime/widget.js +270 -0
- package/dist/extensions/mega-runtime/widget.test.js +160 -0
- package/dist/extensions/mega-runtime.js +15 -947
- package/dist/src/config/themes.js +84 -0
- package/dist/src/config/themes.test.js +94 -0
- package/dist/src/game/scoring.js +105 -0
- package/dist/src/game/scoring.test.js +98 -0
- package/dist/src/store/sqlite/checkpoints.js +145 -0
- package/dist/src/store/sqlite/dedup-mirror.js +64 -0
- package/dist/src/store/sqlite/foundation.js +38 -0
- package/dist/src/store/sqlite/game-achievements.js +111 -0
- package/dist/src/store/sqlite/game-achievements.test.js +67 -0
- package/dist/src/store/sqlite/game-scores.js +105 -0
- package/dist/src/store/sqlite/game-scores.test.js +106 -0
- package/dist/src/store/sqlite/game-state.js +54 -0
- package/dist/src/store/sqlite/game-state.test.js +76 -0
- package/dist/src/store/sqlite/global-index.js +224 -0
- package/dist/src/store/sqlite/maintenance.js +235 -0
- package/dist/src/store/sqlite/memories.js +164 -0
- package/dist/src/store/sqlite/meta.js +82 -0
- package/dist/src/store/sqlite/model-snapshots.js +47 -0
- package/dist/src/store/sqlite/raptor.js +57 -0
- package/dist/src/store/sqlite/raw-transcript.js +134 -0
- package/dist/src/store/sqlite/schema.js +294 -0
- package/dist/src/store/sqlite/session-state.js +28 -0
- package/dist/src/store/sqlite/stats.js +66 -0
- package/dist/src/store/sqlite/utils.js +120 -0
- package/dist/src/store/sqlite.js +23 -1607
- package/extensions/dashboard-server/html.test.ts +50 -0
- package/extensions/dashboard-server/html.ts +1026 -0
- package/extensions/dashboard-server/index-reader.ts +130 -0
- package/extensions/dashboard-server/server.test.ts +131 -0
- package/extensions/dashboard-server/server.ts +505 -0
- package/extensions/dashboard-server/snapshot.ts +44 -0
- package/extensions/dashboard-server/state.ts +33 -0
- package/extensions/dashboard-server/types.ts +134 -0
- package/extensions/dashboard-server-s32.test.ts +195 -0
- package/extensions/dashboard-server.ts +7 -1431
- package/extensions/mega-commands.ts +33 -10
- package/extensions/mega-compact.test.ts +198 -43
- package/extensions/mega-compact.ts +3 -0
- package/extensions/mega-conflict-cmds.ts +6 -2
- package/extensions/mega-dashboard-cmds.ts +30 -23
- package/extensions/mega-db-cmds.ts +11 -3
- package/extensions/mega-events/agent-handlers.ts +262 -0
- package/extensions/mega-events/compact-handlers.ts +192 -0
- package/extensions/mega-events/context-handler.ts +290 -0
- package/extensions/mega-events/register.ts +37 -0
- package/extensions/mega-events/session-handlers.ts +165 -0
- package/extensions/mega-events.ts +15 -780
- package/extensions/mega-game-cmds.test.ts +137 -0
- package/extensions/mega-game-cmds.ts +122 -0
- package/extensions/mega-pipeline/compact.ts +366 -0
- package/extensions/mega-pipeline/memory-review.ts +46 -0
- package/extensions/mega-pipeline/recall.ts +165 -0
- package/extensions/mega-pipeline.ts +9 -537
- package/extensions/mega-runtime/helpers.ts +68 -0
- package/extensions/mega-runtime/query.ts +29 -0
- package/extensions/mega-runtime/state.test.ts +171 -0
- package/extensions/mega-runtime/state.ts +967 -0
- package/extensions/mega-runtime/widget.test.ts +185 -0
- package/extensions/mega-runtime/widget.ts +359 -0
- package/extensions/mega-runtime.ts +15 -1093
- package/package.json +4 -3
- package/src/config/themes.test.ts +116 -0
- package/src/config/themes.ts +124 -0
- package/src/game/scoring.test.ts +103 -0
- package/src/game/scoring.ts +158 -0
- package/src/store/sqlite/checkpoints.ts +204 -0
- package/src/store/sqlite/dedup-mirror.ts +114 -0
- package/src/store/sqlite/foundation.ts +63 -0
- package/src/store/sqlite/game-achievements.test.ts +80 -0
- package/src/store/sqlite/game-achievements.ts +147 -0
- package/src/store/sqlite/game-scores.test.ts +132 -0
- package/src/store/sqlite/game-scores.ts +168 -0
- package/src/store/sqlite/game-state.test.ts +89 -0
- package/src/store/sqlite/game-state.ts +87 -0
- package/src/store/sqlite/global-index.ts +305 -0
- package/src/store/sqlite/maintenance.ts +294 -0
- package/src/store/sqlite/memories.ts +217 -0
- package/src/store/sqlite/meta.ts +108 -0
- package/src/store/sqlite/model-snapshots.ts +83 -0
- package/src/store/sqlite/raptor.ts +107 -0
- package/src/store/sqlite/raw-transcript.ts +221 -0
- package/src/store/sqlite/schema.ts +305 -0
- package/src/store/sqlite/session-state.ts +38 -0
- package/src/store/sqlite/stats.ts +127 -0
- package/src/store/sqlite/utils.ts +125 -0
- package/src/store/sqlite.ts +23 -2204
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* themes.ts — SINGLE SOURCE OF TRUTH for game-mode visual-effect themes (S30).
|
|
3
|
+
*
|
|
4
|
+
* Each theme defines BOTH an HTML/CSS palette (dashboard) AND an ANSI accent
|
|
5
|
+
* mapping (TUI widget), since the TUI is ANSI and the dashboard is HTML.
|
|
6
|
+
*
|
|
7
|
+
* Theme scope (QA1 correction): themes restyle the mega-compact statusbar
|
|
8
|
+
* widget + the whole dashboard HTML page. We do NOT theme pi's chrome.
|
|
9
|
+
*
|
|
10
|
+
* `transparent` (default) semantics: no background fill, colored accents only —
|
|
11
|
+
* so game-mode stays visible against any terminal background.
|
|
12
|
+
*
|
|
13
|
+
* PREVENT-PI-004: pure config, no network. Pi-agnostic: no pi runtime types.
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_THEME = "transparent";
|
|
16
|
+
export const THEMES = [
|
|
17
|
+
{
|
|
18
|
+
id: "transparent",
|
|
19
|
+
label: "Transparent (accents only)",
|
|
20
|
+
css: { bg: null, fg: "#c9d1d9", accent: "#3fb950", mega: "#f0883e" },
|
|
21
|
+
ansi: { bg: null, fg: "39", accent: "32", mega: "33" },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "retro",
|
|
25
|
+
label: "Retro 8-bit green",
|
|
26
|
+
css: { bg: "#003300", fg: "#33ff33", accent: "#00ff41", mega: "#39ff14" },
|
|
27
|
+
ansi: { bg: "40", fg: "92", accent: "92", mega: "1;92" },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "orange-bold",
|
|
31
|
+
label: "Orange bold",
|
|
32
|
+
css: { bg: "#1a0f00", fg: "#ff8c00", accent: "#ff6b00", mega: "#ff4500" },
|
|
33
|
+
ansi: { bg: "48;5;52", fg: "38;5;208", accent: "38;5;208", mega: "1;38;5;202" },
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "cyan-neon",
|
|
37
|
+
label: "Cyan neon",
|
|
38
|
+
css: { bg: "#001a1a", fg: "#00ffff", accent: "#22d3ee", mega: "#7df9ff" },
|
|
39
|
+
ansi: { bg: "48;5;23", fg: "38;5;51", accent: "38;5;51", mega: "1;38;5;51" },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "amber-mono",
|
|
43
|
+
label: "Amber phosphor",
|
|
44
|
+
css: { bg: "#1a1200", fg: "#ffb000", accent: "#ff8c00", mega: "#ff5500" },
|
|
45
|
+
ansi: { bg: "48;5;58", fg: "38;5;214", accent: "38;5;214", mega: "1;38;5;208" },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "grayscale",
|
|
49
|
+
label: "Grayscale minimal",
|
|
50
|
+
css: { bg: "#161616", fg: "#b0b0b0", accent: "#8b949e", mega: "#e6edf3" },
|
|
51
|
+
ansi: { bg: "48;5;235", fg: "38;5;249", accent: "38;5;249", mega: "1;38;5;255" },
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
/** All valid theme ids, in definition order (used for `theme next` cycling). */
|
|
55
|
+
export const THEME_IDS = THEMES.map((t) => t.id);
|
|
56
|
+
/** Look up a theme by id; returns `undefined` for an unknown id (caller
|
|
57
|
+
* validates + falls back to DEFAULT_THEME). */
|
|
58
|
+
export function getTheme(id) {
|
|
59
|
+
return THEMES.find((t) => t.id === id);
|
|
60
|
+
}
|
|
61
|
+
/** True if `id` is a known theme id. */
|
|
62
|
+
export function isValidTheme(id) {
|
|
63
|
+
return THEME_IDS.includes(id);
|
|
64
|
+
}
|
|
65
|
+
/** Cycle to the next theme id (wraps at the end). Falls back to DEFAULT_THEME
|
|
66
|
+
* if `current` is unknown (defensive — the stored id should always be valid). */
|
|
67
|
+
export function nextTheme(current) {
|
|
68
|
+
const i = THEME_IDS.indexOf(current);
|
|
69
|
+
if (i < 0)
|
|
70
|
+
return DEFAULT_THEME;
|
|
71
|
+
return THEME_IDS[(i + 1) % THEME_IDS.length];
|
|
72
|
+
}
|
|
73
|
+
/** CSS variable declarations for a theme's CSS palette. `bg` is `null` for the
|
|
74
|
+
* transparent theme → `--bg: transparent` (no page fill, accents only). Pure,
|
|
75
|
+
* no pi types. */
|
|
76
|
+
export function themeCssVars(theme) {
|
|
77
|
+
const bg = theme.css.bg ?? "transparent";
|
|
78
|
+
return `--bg: ${bg}; --fg: ${theme.css.fg}; --accent: ${theme.css.accent}; --mega: ${theme.css.mega};`;
|
|
79
|
+
}
|
|
80
|
+
/** A `:root[data-theme="<id>"]` override block carrying the theme's CSS vars,
|
|
81
|
+
* for client-side instant theme switching. Pure, no pi types. */
|
|
82
|
+
export function themeDataBlock(theme) {
|
|
83
|
+
return `:root[data-theme="${theme.id}"]{ ${themeCssVars(theme)} }`;
|
|
84
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* themes.test.ts — S30 theme palette source-of-truth tests.
|
|
3
|
+
* Pi-agnostic: no pi runtime imports.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { THEMES, THEME_IDS, DEFAULT_THEME, getTheme, isValidTheme, nextTheme, themeCssVars, themeDataBlock, } from "./themes.js";
|
|
8
|
+
describe("themes (S30)", () => {
|
|
9
|
+
it("defines exactly 6 themes", () => {
|
|
10
|
+
assert.equal(THEMES.length, 6);
|
|
11
|
+
assert.equal(THEME_IDS.length, 6);
|
|
12
|
+
});
|
|
13
|
+
it("has unique ids", () => {
|
|
14
|
+
assert.equal(new Set(THEME_IDS).size, 6);
|
|
15
|
+
});
|
|
16
|
+
it("default theme is transparent", () => {
|
|
17
|
+
assert.equal(DEFAULT_THEME, "transparent");
|
|
18
|
+
assert.ok(isValidTheme(DEFAULT_THEME));
|
|
19
|
+
});
|
|
20
|
+
it("includes retro, orange-bold, cyan-neon, amber-mono, grayscale", () => {
|
|
21
|
+
for (const id of ["retro", "orange-bold", "cyan-neon", "amber-mono", "grayscale"]) {
|
|
22
|
+
assert.ok(isValidTheme(id), `missing ${id}`);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
it("transparent theme has null bg (CSS + ANSI)", () => {
|
|
26
|
+
const t = getTheme("transparent");
|
|
27
|
+
assert.ok(t);
|
|
28
|
+
assert.equal(t.css.bg, null);
|
|
29
|
+
assert.equal(t.ansi.bg, null);
|
|
30
|
+
});
|
|
31
|
+
it("every non-transparent theme has a non-null bg", () => {
|
|
32
|
+
for (const t of THEMES) {
|
|
33
|
+
if (t.id === "transparent")
|
|
34
|
+
continue;
|
|
35
|
+
assert.ok(t.css.bg, `${t.id} css.bg`);
|
|
36
|
+
assert.ok(t.ansi.bg, `${t.id} ansi.bg`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
it("every theme has css + ansi {fg,accent,mega} strings", () => {
|
|
40
|
+
for (const t of THEMES) {
|
|
41
|
+
assert.ok(typeof t.css.fg === "string" && t.css.fg.startsWith("#"));
|
|
42
|
+
assert.ok(typeof t.css.accent === "string");
|
|
43
|
+
assert.ok(typeof t.css.mega === "string");
|
|
44
|
+
assert.ok(typeof t.ansi.fg === "string");
|
|
45
|
+
assert.ok(typeof t.ansi.accent === "string");
|
|
46
|
+
assert.ok(typeof t.ansi.mega === "string");
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
it("getTheme returns undefined for unknown id", () => {
|
|
50
|
+
assert.equal(getTheme("nope"), undefined);
|
|
51
|
+
});
|
|
52
|
+
it("isValidTheme is false for unknown", () => {
|
|
53
|
+
assert.equal(isValidTheme("nope"), false);
|
|
54
|
+
});
|
|
55
|
+
it("nextTheme cycles through all themes and wraps", () => {
|
|
56
|
+
const seen = [];
|
|
57
|
+
let cur = DEFAULT_THEME;
|
|
58
|
+
for (let i = 0; i < THEME_IDS.length; i++) {
|
|
59
|
+
cur = nextTheme(cur);
|
|
60
|
+
seen.push(cur);
|
|
61
|
+
}
|
|
62
|
+
// after N steps we've visited every theme once
|
|
63
|
+
assert.equal(new Set(seen).size, THEME_IDS.length);
|
|
64
|
+
// wraps: one more step from the last returns to the first
|
|
65
|
+
const first = nextTheme(THEME_IDS[THEME_IDS.length - 1]);
|
|
66
|
+
assert.equal(first, THEME_IDS[0]);
|
|
67
|
+
});
|
|
68
|
+
it("nextTheme falls back to DEFAULT_THEME for unknown current", () => {
|
|
69
|
+
assert.equal(nextTheme("garbage"), DEFAULT_THEME);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe("themeCssVars / themeDataBlock (S32)", () => {
|
|
73
|
+
it("themeCssVars emits --bg/--fg/--accent/--mega for a normal theme", () => {
|
|
74
|
+
const t = getTheme("retro");
|
|
75
|
+
const vars = themeCssVars(t);
|
|
76
|
+
assert.match(vars, /--bg: #003300;/);
|
|
77
|
+
assert.match(vars, /--fg: #33ff33;/);
|
|
78
|
+
assert.match(vars, /--accent: #00ff41;/);
|
|
79
|
+
assert.match(vars, /--mega: #39ff14;/);
|
|
80
|
+
});
|
|
81
|
+
it("themeCssVars maps null bg to 'transparent'", () => {
|
|
82
|
+
const t = getTheme("transparent");
|
|
83
|
+
const vars = themeCssVars(t);
|
|
84
|
+
assert.match(vars, /--bg: transparent;/);
|
|
85
|
+
assert.match(vars, /--fg: #c9d1d9;/);
|
|
86
|
+
});
|
|
87
|
+
it("themeDataBlock wraps vars in a :root[data-theme=\"<id\"] selector", () => {
|
|
88
|
+
const t = getTheme("cyan-neon");
|
|
89
|
+
const block = themeDataBlock(t);
|
|
90
|
+
assert.equal(block.startsWith(':root[data-theme="cyan-neon"]{ '), true);
|
|
91
|
+
assert.equal(block.endsWith(" }"), true);
|
|
92
|
+
assert.ok(block.includes("--accent: #22d3ee;"));
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** Canonical allow-list of game metrics (mirrors the schema CHECK constraint). */
|
|
2
|
+
export const METRICS = [
|
|
3
|
+
"cache",
|
|
4
|
+
"dedupe",
|
|
5
|
+
"turns",
|
|
6
|
+
"repos",
|
|
7
|
+
"mega_cache",
|
|
8
|
+
];
|
|
9
|
+
/**
|
|
10
|
+
* Player level as a function of completed turns.
|
|
11
|
+
* Sequence: n=0→1, 1→2, 2→2, 3→3, 4→3, 7→4, 15→5, ...
|
|
12
|
+
* i.e. floor(log2(n+1)) + 1 (one level per doubling of turns). Defensive: any
|
|
13
|
+
* non-finite / negative input collapses to level 1 (never throws, never NaN).
|
|
14
|
+
*/
|
|
15
|
+
export function turnLevel(n) {
|
|
16
|
+
if (!Number.isFinite(n) || n < 0)
|
|
17
|
+
return 1;
|
|
18
|
+
return Math.floor(Math.log2(n + 1)) + 1;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* MEGA CACHE trigger test: the cache hit-rate (real ratio, may exceed 100%)
|
|
22
|
+
* has crossed the 100% threshold. Used to award the mega_cache trophy + arm
|
|
23
|
+
* the widget flare (the oopsie gag).
|
|
24
|
+
*/
|
|
25
|
+
export function isMegaCache(pct) {
|
|
26
|
+
return Number.isFinite(pct) && pct >= 100;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Cache-hit percentage (0..100+). Returns 0 when there are no lookups so the
|
|
30
|
+
* ratio can never be NaN / Infinity (QA3). Safe against non-finite inputs.
|
|
31
|
+
*/
|
|
32
|
+
export function cacheScore(hits, lookups) {
|
|
33
|
+
if (!Number.isFinite(hits) || !Number.isFinite(lookups))
|
|
34
|
+
return 0;
|
|
35
|
+
return lookups > 0 ? (hits / lookups) * 100 : 0;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The 9 achievements (8 visible + 1 hidden easter egg = Opie's Wild Ride).
|
|
39
|
+
* Seeded idempotently into the `game_achievements` table on first open
|
|
40
|
+
* (schema.ts). Pure, pi-agnostic — no I/O.
|
|
41
|
+
*/
|
|
42
|
+
export const ACHIEVEMENT_DEFS = [
|
|
43
|
+
{ id: "first_compact", title: "First Compact", description: "Compact a conversation once.", hidden: false, icon: "\u{1F476}" },
|
|
44
|
+
{ id: "compact_streak", title: "Compact Streak", description: "Compact 5 times in one session.", hidden: false, icon: "\u{1F525}" },
|
|
45
|
+
{ id: "turn_veteran", title: "Turn Veteran", description: "Reach 25 turns in a repo.", hidden: false, icon: "\u{1F3C3}" },
|
|
46
|
+
{ id: "level_five", title: "Level 5", description: "Reach player level 5.", hidden: false, icon: "\u{2B50}" },
|
|
47
|
+
{ id: "dedupe_master", title: "Dedupe Master", description: "Collapse 100 chunks total.", hidden: false, icon: "\u{1F5DC}\u{FE0F}" },
|
|
48
|
+
{ id: "repo_explorer", title: "Repo Explorer", description: "Use game mode across 3 repos.", hidden: false, icon: "\u{1F30D}" },
|
|
49
|
+
{ id: "night_owl", title: "Night Owl", description: "Record a score after midnight (00:00–05:00 local).", hidden: false, icon: "\u{1F989}" },
|
|
50
|
+
{ id: "flawless", title: "Flawless", description: "Hit exactly 100% cache with no overshoot.", hidden: false, icon: "\u{1F4AF}" },
|
|
51
|
+
{ id: "opie_wild_ride", title: "Opie's Wild Ride", description: "Push the cache past 100%.", hidden: true, icon: "\u{1F3C6}" },
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
54
|
+
* Max `compactCount` carried in a dedupe row's `meta` (S33 records
|
|
55
|
+
* `{ compactCount }` on each session_compact). Returns 0 when absent.
|
|
56
|
+
*/
|
|
57
|
+
export function maxCompactCount(rows) {
|
|
58
|
+
return rows.reduce((m, r) => {
|
|
59
|
+
const meta = r.meta;
|
|
60
|
+
const cm = meta && typeof meta === "object" && "compactCount" in meta
|
|
61
|
+
? Number(meta.compactCount) || 0
|
|
62
|
+
: 0;
|
|
63
|
+
return Math.max(m, cm);
|
|
64
|
+
}, 0);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Pure evaluation of the 9 achievements against the leaderboard aggregates.
|
|
68
|
+
* `alreadyUnlocked` is the set of ids already persisted (so `newlyUnlocked`
|
|
69
|
+
* excludes them). No I/O, no pi types. Mirrors the S35.9 condition table.
|
|
70
|
+
*/
|
|
71
|
+
export function evaluateAchievements(ctx, alreadyUnlocked) {
|
|
72
|
+
const sumBy = (rows) => rows.reduce((s, r) => s + (Number(r.value) || 0), 0);
|
|
73
|
+
const maxBy = (rows) => rows.reduce((m, r) => Math.max(m, Number(r.value) || 0), 0);
|
|
74
|
+
const dedupeSum = sumBy(ctx.dedupe);
|
|
75
|
+
const maxTurns = maxBy(ctx.turns);
|
|
76
|
+
const maxCache = maxBy(ctx.cache);
|
|
77
|
+
const maxCompact = maxCompactCount(ctx.dedupe);
|
|
78
|
+
// Night owl: any row (across all metrics) recorded between 00:00–05:00 local.
|
|
79
|
+
const allTs = [
|
|
80
|
+
...ctx.dedupe,
|
|
81
|
+
...ctx.turns,
|
|
82
|
+
...ctx.cache,
|
|
83
|
+
...ctx.megaCache,
|
|
84
|
+
];
|
|
85
|
+
const nightOwl = allTs.some((r) => {
|
|
86
|
+
const h = new Date(Number(r.ts) || 0).getHours();
|
|
87
|
+
return h >= 0 && h < 5;
|
|
88
|
+
});
|
|
89
|
+
// Flawless: cache hit 100%+, but NO mega_cache overshoot (>100).
|
|
90
|
+
const megaOvershoot = ctx.megaCache.some((r) => Number(r.value) > 100);
|
|
91
|
+
const conditions = {
|
|
92
|
+
first_compact: ctx.dedupe.length >= 1,
|
|
93
|
+
compact_streak: maxCompact >= 5,
|
|
94
|
+
turn_veteran: maxTurns >= 25,
|
|
95
|
+
level_five: turnLevel(maxTurns) >= 5,
|
|
96
|
+
dedupe_master: dedupeSum >= 100,
|
|
97
|
+
repo_explorer: ctx.reposCount >= 3,
|
|
98
|
+
night_owl: nightOwl,
|
|
99
|
+
flawless: maxCache >= 100 && !megaOvershoot,
|
|
100
|
+
opie_wild_ride: ctx.megaCache.length >= 1,
|
|
101
|
+
};
|
|
102
|
+
const unlocked = Object.keys(conditions).filter((id) => conditions[id]);
|
|
103
|
+
const newlyUnlocked = unlocked.filter((id) => !alreadyUnlocked.includes(id));
|
|
104
|
+
return { unlocked, newlyUnlocked };
|
|
105
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scoring.test.ts — S33 pure scoring helpers. Pi-agnostic, no I/O.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { turnLevel, isMegaCache, cacheScore, evaluateAchievements, maxCompactCount, ACHIEVEMENT_DEFS } from "./scoring.js";
|
|
7
|
+
describe("scoring (S33)", () => {
|
|
8
|
+
it("turnLevel: floor(log2(n+1))+1 sequence", () => {
|
|
9
|
+
assert.equal(turnLevel(0), 1);
|
|
10
|
+
assert.equal(turnLevel(1), 2);
|
|
11
|
+
assert.equal(turnLevel(2), 2);
|
|
12
|
+
assert.equal(turnLevel(3), 3);
|
|
13
|
+
assert.equal(turnLevel(4), 3);
|
|
14
|
+
assert.equal(turnLevel(7), 4);
|
|
15
|
+
assert.equal(turnLevel(15), 5);
|
|
16
|
+
// defensive: non-finite / negative inputs collapse to level 1
|
|
17
|
+
assert.equal(turnLevel(-1), 1);
|
|
18
|
+
assert.equal(turnLevel(NaN), 1);
|
|
19
|
+
});
|
|
20
|
+
it("isMegaCache: 100% is the boundary", () => {
|
|
21
|
+
assert.equal(isMegaCache(99.9), false);
|
|
22
|
+
assert.equal(isMegaCache(100), true);
|
|
23
|
+
assert.equal(isMegaCache(150), true);
|
|
24
|
+
assert.equal(isMegaCache(0), false);
|
|
25
|
+
assert.equal(isMegaCache(NaN), false);
|
|
26
|
+
});
|
|
27
|
+
it("cacheScore: 0 lookups -> 0 (not NaN), else hits/lookups*100", () => {
|
|
28
|
+
assert.equal(cacheScore(0, 0), 0);
|
|
29
|
+
assert.equal(cacheScore(7, 10), 70);
|
|
30
|
+
assert.equal(cacheScore(0, 5), 0);
|
|
31
|
+
assert.equal(cacheScore(10, 10), 100);
|
|
32
|
+
// non-finite inputs are guarded (no NaN leaks)
|
|
33
|
+
assert.equal(cacheScore(1, NaN), 0);
|
|
34
|
+
assert.equal(cacheScore(NaN, 5), 0);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
function achRow(value, opts = {}) {
|
|
38
|
+
return { repo_root: "/repo/a", value, ts: opts.ts ?? 1_700_000_000_000, meta: opts.meta ?? null };
|
|
39
|
+
}
|
|
40
|
+
describe("evaluateAchievements (S35)", () => {
|
|
41
|
+
const empty = { dedupe: [], turns: [], cache: [], megaCache: [], reposCount: 0 };
|
|
42
|
+
it("ACHIEVEMENT_DEFS: 9 achievements, exactly 1 hidden", () => {
|
|
43
|
+
assert.equal(ACHIEVEMENT_DEFS.length, 9);
|
|
44
|
+
assert.equal(ACHIEVEMENT_DEFS.filter((d) => d.hidden).length, 1);
|
|
45
|
+
});
|
|
46
|
+
it("first_compact: >=1 dedupe row", () => {
|
|
47
|
+
const r = evaluateAchievements({ ...empty, dedupe: [achRow(1)] }, []);
|
|
48
|
+
assert.ok(r.unlocked.includes("first_compact"));
|
|
49
|
+
assert.ok(r.newlyUnlocked.includes("first_compact"));
|
|
50
|
+
});
|
|
51
|
+
it("compact_streak: max meta.compactCount >= 5", () => {
|
|
52
|
+
const r = evaluateAchievements({ ...empty, dedupe: [achRow(1, { meta: { compactCount: 5 } })] }, []);
|
|
53
|
+
assert.ok(r.unlocked.includes("compact_streak"));
|
|
54
|
+
});
|
|
55
|
+
it("turn_veteran: max turns >= 25", () => {
|
|
56
|
+
const r = evaluateAchievements({ ...empty, turns: [achRow(25)] }, []);
|
|
57
|
+
assert.ok(r.unlocked.includes("turn_veteran"));
|
|
58
|
+
});
|
|
59
|
+
it("level_five: turnLevel(maxTurns) >= 5 (15 turns)", () => {
|
|
60
|
+
const r = evaluateAchievements({ ...empty, turns: [achRow(15)] }, []);
|
|
61
|
+
assert.ok(r.unlocked.includes("level_five"));
|
|
62
|
+
});
|
|
63
|
+
it("dedupe_master: sum dedupe >= 100", () => {
|
|
64
|
+
const r = evaluateAchievements({ ...empty, dedupe: [achRow(60), achRow(40)] }, []);
|
|
65
|
+
assert.ok(r.unlocked.includes("dedupe_master"));
|
|
66
|
+
});
|
|
67
|
+
it("repo_explorer: reposCount >= 3", () => {
|
|
68
|
+
const r = evaluateAchievements({ ...empty, reposCount: 3 }, []);
|
|
69
|
+
assert.ok(r.unlocked.includes("repo_explorer"));
|
|
70
|
+
});
|
|
71
|
+
it("night_owl: a row whose local hour is in [0,5)", () => {
|
|
72
|
+
const d = new Date();
|
|
73
|
+
d.setHours(2, 0, 0, 0);
|
|
74
|
+
const r = evaluateAchievements({ ...empty, cache: [achRow(50, { ts: d.getTime() })] }, []);
|
|
75
|
+
assert.ok(r.unlocked.includes("night_owl"));
|
|
76
|
+
});
|
|
77
|
+
it("flawless: maxCache >= 100 AND no mega_cache overshoot", () => {
|
|
78
|
+
const r = evaluateAchievements({ ...empty, cache: [achRow(100)], megaCache: [] }, []);
|
|
79
|
+
assert.ok(r.unlocked.includes("flawless"));
|
|
80
|
+
});
|
|
81
|
+
it("flawless NOT when a mega_cache overshoot row (>100) exists", () => {
|
|
82
|
+
const r = evaluateAchievements({ ...empty, cache: [achRow(100)], megaCache: [achRow(101)] }, []);
|
|
83
|
+
assert.ok(!r.unlocked.includes("flawless"));
|
|
84
|
+
});
|
|
85
|
+
it("opie_wild_ride: any mega_cache trophy row exists", () => {
|
|
86
|
+
const r = evaluateAchievements({ ...empty, megaCache: [achRow(100)] }, []);
|
|
87
|
+
assert.ok(r.unlocked.includes("opie_wild_ride"));
|
|
88
|
+
});
|
|
89
|
+
it("newlyUnlocked excludes ids already unlocked", () => {
|
|
90
|
+
const r = evaluateAchievements({ ...empty, dedupe: [achRow(1)] }, ["first_compact"]);
|
|
91
|
+
assert.ok(r.unlocked.includes("first_compact"));
|
|
92
|
+
assert.ok(!r.newlyUnlocked.includes("first_compact"));
|
|
93
|
+
});
|
|
94
|
+
it("maxCompactCount: 0 when meta absent, else the max", () => {
|
|
95
|
+
assert.equal(maxCompactCount([achRow(1)]), 0);
|
|
96
|
+
assert.equal(maxCompactCount([achRow(1, { meta: { compactCount: 7 } })]), 7);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { getStateDir, normalizeSessionId } from "../../store.js";
|
|
2
|
+
import { openStore, withTx, jsonText, encodeEmbedding, rowToCheckpoint, } from "./utils.js";
|
|
3
|
+
/** Insert or replace a checkpoint (idempotent by id). */
|
|
4
|
+
export function upsertCheckpoint(cp, stateDir = getStateDir()) {
|
|
5
|
+
const db = openStore(stateDir);
|
|
6
|
+
const sid = normalizeSessionId(cp.sessionId);
|
|
7
|
+
withTx(db, () => {
|
|
8
|
+
db.prepare(`INSERT INTO context_chunks
|
|
9
|
+
(id, session_id, region_hash, content_hash, content_hash2, content_hash_version,
|
|
10
|
+
normalized_text, summary, topic_summary, summary_hash,
|
|
11
|
+
key_decisions, next_steps, files_modified, embedding_blob,
|
|
12
|
+
token_estimate, original_token_estimate, timestamp, dedup_status, compressed_original)
|
|
13
|
+
VALUES (@id, @sid, @region_hash, @content_hash, @content_hash2, @content_hash_version,
|
|
14
|
+
@normalized_text, @summary, @topic_summary, @summary_hash,
|
|
15
|
+
@key_decisions, @next_steps, @files_modified, @embedding_blob,
|
|
16
|
+
@token_estimate, @original_token_estimate, @timestamp, @dedup_status, @compressed_original)
|
|
17
|
+
ON CONFLICT(session_id, id) DO UPDATE SET
|
|
18
|
+
summary=excluded.summary,
|
|
19
|
+
topic_summary=excluded.topic_summary,
|
|
20
|
+
summary_hash=excluded.summary_hash,
|
|
21
|
+
key_decisions=excluded.key_decisions,
|
|
22
|
+
next_steps=excluded.next_steps,
|
|
23
|
+
files_modified=excluded.files_modified,
|
|
24
|
+
embedding_blob=excluded.embedding_blob,
|
|
25
|
+
token_estimate=excluded.token_estimate,
|
|
26
|
+
original_token_estimate=excluded.original_token_estimate,
|
|
27
|
+
timestamp=excluded.timestamp,
|
|
28
|
+
dedup_status=excluded.dedup_status,
|
|
29
|
+
compressed_original=excluded.compressed_original`).run({
|
|
30
|
+
"@id": cp.checkpointId,
|
|
31
|
+
"@sid": sid,
|
|
32
|
+
"@region_hash": cp.regionHash ?? null,
|
|
33
|
+
"@content_hash": cp.contentHash ?? null,
|
|
34
|
+
"@content_hash2": cp.contentHash2 ?? null,
|
|
35
|
+
"@content_hash_version": cp.contentHashVersion ?? null,
|
|
36
|
+
"@normalized_text": cp.normalizedText ?? null,
|
|
37
|
+
"@summary": cp.summary ?? "",
|
|
38
|
+
"@topic_summary": cp.topicSummary ?? null,
|
|
39
|
+
"@summary_hash": cp.summaryHash ?? null,
|
|
40
|
+
"@key_decisions": jsonText(cp.keyDecisions),
|
|
41
|
+
"@next_steps": jsonText(cp.nextSteps),
|
|
42
|
+
"@files_modified": jsonText(cp.filesModified),
|
|
43
|
+
"@embedding_blob": encodeEmbedding(cp.embedding ?? []),
|
|
44
|
+
"@token_estimate": cp.tokenEstimate ?? 0,
|
|
45
|
+
"@original_token_estimate": cp.originalTokenEstimate ?? null,
|
|
46
|
+
"@timestamp": cp.timestamp ?? 0,
|
|
47
|
+
"@dedup_status": "active",
|
|
48
|
+
"@compressed_original": cp.compressedOriginal ?? null,
|
|
49
|
+
});
|
|
50
|
+
// FTS5 virtual tables don't support UPSERT — delete any prior row, reinsert.
|
|
51
|
+
// Store normalized_text (the L1 verify key); fall back to summary for rows
|
|
52
|
+
// that predate normalized_text population.
|
|
53
|
+
db.prepare("DELETE FROM context_chunks_trgm WHERE id = ?").run(cp.checkpointId);
|
|
54
|
+
db.prepare("INSERT INTO context_chunks_trgm(id, normalized_text) VALUES(?, ?)").run(cp.checkpointId, cp.normalizedText ?? cp.summary ?? "");
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// --- Sprint 11: MinHash signatures + LSH buckets --------------------------
|
|
58
|
+
/** Persist a checkpoint's MinHash signature (idempotent by chunk_id + version). */
|
|
59
|
+
export function upsertMinhashSignature(chunkId, sessionId, signatureVersion, signatures, stateDir = getStateDir()) {
|
|
60
|
+
const db = openStore(stateDir);
|
|
61
|
+
const sid = normalizeSessionId(sessionId);
|
|
62
|
+
db.prepare(`INSERT INTO minhash_signatures(chunk_id, session_id, signature_version, signatures)
|
|
63
|
+
VALUES(?, ?, ?, ?)
|
|
64
|
+
ON CONFLICT(chunk_id, signature_version) DO UPDATE SET
|
|
65
|
+
session_id=excluded.session_id, signatures=excluded.signatures`).run(chunkId, sid, signatureVersion, JSON.stringify(signatures));
|
|
66
|
+
}
|
|
67
|
+
/** Persist LSH bucket memberships for a chunk (one row per bucket key). */
|
|
68
|
+
export function insertLshBuckets(chunkId, sessionId, signatureVersion, bucketKeys, stateDir = getStateDir()) {
|
|
69
|
+
const db = openStore(stateDir);
|
|
70
|
+
const sid = normalizeSessionId(sessionId);
|
|
71
|
+
const del = db.prepare("DELETE FROM dedup_lsh_buckets WHERE chunk_id = ?");
|
|
72
|
+
const ins = db.prepare("INSERT OR IGNORE INTO dedup_lsh_buckets(bucket_key, chunk_id, session_id, signature_version) VALUES(?, ?, ?, ?)");
|
|
73
|
+
withTx(db, () => {
|
|
74
|
+
del.run(chunkId);
|
|
75
|
+
for (const key of bucketKeys)
|
|
76
|
+
ins.run(key, chunkId, sid, signatureVersion);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Candidate chunk_ids sharing any LSH bucket with `bucketKeys`, scoped to the
|
|
81
|
+
* session, capped at `limit`. Single query (no N loops) — QA #15 amplification
|
|
82
|
+
* guard. Returns DISTINCT chunk_ids excluding `excludeChunkId` (the new row).
|
|
83
|
+
*/
|
|
84
|
+
export function lshCandidateChunks(bucketKeys, sessionId, excludeChunkId, stateDir = getStateDir(), limit = 100) {
|
|
85
|
+
if (bucketKeys.length === 0)
|
|
86
|
+
return [];
|
|
87
|
+
const db = openStore(stateDir);
|
|
88
|
+
const sid = normalizeSessionId(sessionId);
|
|
89
|
+
const placeholders = bucketKeys.map(() => "?").join(",");
|
|
90
|
+
const rows = db
|
|
91
|
+
.prepare(`SELECT DISTINCT chunk_id FROM dedup_lsh_buckets
|
|
92
|
+
WHERE bucket_key IN (${placeholders}) AND session_id = ? AND chunk_id != ?
|
|
93
|
+
LIMIT ?`)
|
|
94
|
+
.all(...bucketKeys, sid, excludeChunkId, limit);
|
|
95
|
+
return rows.map((r) => r.chunk_id);
|
|
96
|
+
}
|
|
97
|
+
/** All checkpoints for a session, sorted by id. */
|
|
98
|
+
export function listCheckpoints(sessionId, stateDir = getStateDir()) {
|
|
99
|
+
const db = openStore(stateDir);
|
|
100
|
+
const sid = normalizeSessionId(sessionId);
|
|
101
|
+
const rows = db
|
|
102
|
+
.prepare("SELECT * FROM context_chunks WHERE session_id = ? ORDER BY id ASC")
|
|
103
|
+
.all(sid);
|
|
104
|
+
return rows.map(rowToCheckpoint);
|
|
105
|
+
}
|
|
106
|
+
/** S25: the newest checkpoint timestamp for a session, or 0 when none. Used by
|
|
107
|
+
* the RAPTOR freshness guard to reject a tree older than the live checkpoints. */
|
|
108
|
+
export function maxCheckpointTimestamp(sessionId, stateDir = getStateDir()) {
|
|
109
|
+
const db = openStore(stateDir);
|
|
110
|
+
const row = db
|
|
111
|
+
.prepare("SELECT MAX(timestamp) AS mx FROM context_chunks WHERE session_id = ?")
|
|
112
|
+
.get(normalizeSessionId(sessionId));
|
|
113
|
+
return Number(row?.mx ?? 0);
|
|
114
|
+
}
|
|
115
|
+
/** Next sequential checkpoint id (chkpt_001 …) for a session. */
|
|
116
|
+
export function nextCheckpointId(sessionId, stateDir = getStateDir()) {
|
|
117
|
+
const db = openStore(stateDir);
|
|
118
|
+
const sid = normalizeSessionId(sessionId);
|
|
119
|
+
const row = db
|
|
120
|
+
.prepare("SELECT MAX(CAST(SUBSTR(id, 7) AS INTEGER)) AS n FROM context_chunks WHERE session_id = ?")
|
|
121
|
+
.get(sid);
|
|
122
|
+
const next = (row.n ?? 0) + 1;
|
|
123
|
+
return `chkpt_${String(next).padStart(3, "0")}`;
|
|
124
|
+
}
|
|
125
|
+
/** True if a checkpoint id already exists for a session. */
|
|
126
|
+
export function hasCheckpoint(sessionId, checkpointId, stateDir = getStateDir()) {
|
|
127
|
+
const db = openStore(stateDir);
|
|
128
|
+
const row = db
|
|
129
|
+
.prepare("SELECT 1 FROM context_chunks WHERE session_id = ? AND id = ? LIMIT 1")
|
|
130
|
+
.get(normalizeSessionId(sessionId), checkpointId);
|
|
131
|
+
return row !== undefined;
|
|
132
|
+
}
|
|
133
|
+
/** Fetch a single checkpoint by (session, id), or undefined if absent. */
|
|
134
|
+
export function getCheckpoint(sessionId, checkpointId, stateDir = getStateDir()) {
|
|
135
|
+
const db = openStore(stateDir);
|
|
136
|
+
const row = db
|
|
137
|
+
.prepare("SELECT * FROM context_chunks WHERE session_id = ? AND id = ? LIMIT 1")
|
|
138
|
+
.get(normalizeSessionId(sessionId), checkpointId);
|
|
139
|
+
return row ? rowToCheckpoint(row) : undefined;
|
|
140
|
+
}
|
|
141
|
+
/** Mark a checkpoint's dedup_status (e.g. 'removed' by SemDeDup). */
|
|
142
|
+
export function setDedupStatus(checkpointId, sessionId, status, stateDir = getStateDir()) {
|
|
143
|
+
const db = openStore(stateDir);
|
|
144
|
+
db.prepare("UPDATE context_chunks SET dedup_status = ? WHERE id = ? AND session_id = ?").run(status, checkpointId, normalizeSessionId(sessionId));
|
|
145
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upsert a row into dedup_mirror. If the hash already exists, increment ref_count.
|
|
3
|
+
* Returns true if this was a NEW unique content (first insert), false if it was a duplicate.
|
|
4
|
+
*/
|
|
5
|
+
export function upsertDedupMirror(db, contentHash, contentBytes, seq) {
|
|
6
|
+
const now = Date.now();
|
|
7
|
+
const existing = db
|
|
8
|
+
.prepare(`SELECT content_hash FROM dedup_mirror WHERE content_hash = @hash`)
|
|
9
|
+
.get({ "@hash": contentHash });
|
|
10
|
+
if (existing) {
|
|
11
|
+
db.prepare(`UPDATE dedup_mirror SET ref_count = ref_count + 1 WHERE content_hash = @hash`).run({
|
|
12
|
+
"@hash": contentHash,
|
|
13
|
+
});
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
db.prepare(`INSERT INTO dedup_mirror (content_hash, content_bytes, ref_count, first_seen_seq, created_at)
|
|
17
|
+
VALUES (@hash, @bytes, 1, @seq, @now)`).run({
|
|
18
|
+
"@hash": contentHash,
|
|
19
|
+
"@bytes": contentBytes,
|
|
20
|
+
"@seq": seq,
|
|
21
|
+
"@now": now,
|
|
22
|
+
});
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get dedup ratio for a session: total bytes vs unique bytes.
|
|
27
|
+
*/
|
|
28
|
+
export function getDedupRatio(db, sessionId) {
|
|
29
|
+
const totalRow = db
|
|
30
|
+
.prepare(`SELECT COALESCE(SUM(LENGTH(content_bytes)), 0) AS total
|
|
31
|
+
FROM raw_transcript
|
|
32
|
+
WHERE session_id = @session_id`)
|
|
33
|
+
.get({ "@session_id": sessionId });
|
|
34
|
+
const uniqueRow = db
|
|
35
|
+
.prepare(`SELECT COALESCE(SUM(LENGTH(content_bytes)), 0) AS unique_bytes
|
|
36
|
+
FROM dedup_mirror`)
|
|
37
|
+
.get();
|
|
38
|
+
const totalBytes = totalRow.total;
|
|
39
|
+
const uniqueBytes = uniqueRow.unique_bytes;
|
|
40
|
+
const ratio = uniqueBytes > 0 ? totalBytes / uniqueBytes : 1;
|
|
41
|
+
return { totalBytes, uniqueBytes, ratio };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get dedup mirror stats (diagnostic / test helper).
|
|
45
|
+
*/
|
|
46
|
+
export function getDedupMirrorStats(db) {
|
|
47
|
+
const row = db
|
|
48
|
+
.prepare(`SELECT COUNT(*) AS cnt,
|
|
49
|
+
COALESCE(SUM(LENGTH(content_bytes)), 0) AS total_bytes,
|
|
50
|
+
COALESCE(AVG(ref_count), 0) AS avg_ref
|
|
51
|
+
FROM dedup_mirror`)
|
|
52
|
+
.get();
|
|
53
|
+
return { rowCount: row.cnt, totalBytes: row.total_bytes, avgRefCount: row.avg_ref };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Update raw_transcript.content_ref to point to dedup_mirror.
|
|
57
|
+
*/
|
|
58
|
+
export function updateRawTranscriptRef(db, sessionId, seq, contentHash) {
|
|
59
|
+
db.prepare(`UPDATE raw_transcript SET content_ref = @ref WHERE session_id = @sid AND seq = @seq`).run({
|
|
60
|
+
"@ref": contentHash,
|
|
61
|
+
"@sid": sessionId,
|
|
62
|
+
"@seq": seq,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* foundation.ts — future-feature foundation (resume sessions / daily log / lessons).
|
|
3
|
+
*
|
|
4
|
+
* Scaffolded tables + minimal helpers so all store data lives in SQLite from
|
|
5
|
+
* day one. Full UI/recall for these lands in later sprints.
|
|
6
|
+
*/
|
|
7
|
+
import { getStateDir, normalizeSessionId } from "../../store.js";
|
|
8
|
+
import { openStore } from "./utils.js";
|
|
9
|
+
/** Upsert a `sessions` row (resume + per-repo session history). */
|
|
10
|
+
export function touchSession(sessionId, repo, stateDir = getStateDir()) {
|
|
11
|
+
const db = openStore(stateDir);
|
|
12
|
+
const sid = normalizeSessionId(sessionId);
|
|
13
|
+
const existing = db
|
|
14
|
+
.prepare("SELECT started_at FROM sessions WHERE session_id = ?")
|
|
15
|
+
.get(sid);
|
|
16
|
+
const now = Math.floor(Date.now() / 1000);
|
|
17
|
+
if (!existing) {
|
|
18
|
+
db.prepare(`INSERT INTO sessions(session_id, repo, started_at, last_compacted_at, status)
|
|
19
|
+
VALUES(?, ?, ?, ?, 'active')`).run(sid, repo ?? null, now, now);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
db.prepare("UPDATE sessions SET last_compacted_at = ?, repo = COALESCE(?, repo), status = 'active' WHERE session_id = ?").run(now, repo ?? null, sid);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Append a `daily_log` entry (day = YYYY-MM-DD, local-naive from Date). */
|
|
26
|
+
export function logDaily(sessionId, event, detail, tokensSaved, stateDir = getStateDir()) {
|
|
27
|
+
const db = openStore(stateDir);
|
|
28
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
29
|
+
const now = Math.floor(Date.now() / 1000);
|
|
30
|
+
db.prepare(`INSERT INTO daily_log(day, session_id, event, detail, tokens_saved, ts)
|
|
31
|
+
VALUES(?, ?, ?, ?, ?, ?)`).run(day, normalizeSessionId(sessionId), event, detail ?? null, tokensSaved, now);
|
|
32
|
+
}
|
|
33
|
+
/** Append a `lessons` entry (future lessons-learned browse/recall). */
|
|
34
|
+
export function addLesson(sessionId, repo, lesson, stateDir = getStateDir()) {
|
|
35
|
+
const db = openStore(stateDir);
|
|
36
|
+
const now = Math.floor(Date.now() / 1000);
|
|
37
|
+
db.prepare(`INSERT INTO lessons(session_id, repo, lesson, ts) VALUES(?, ?, ?, ?)`).run(normalizeSessionId(sessionId), repo ?? null, lesson, now);
|
|
38
|
+
}
|