@kenkaiiii/ggcoder 5.6.3 → 5.7.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/dist/app-sidecar.js +124 -2
- package/dist/app-sidecar.js.map +1 -1
- package/dist/core/progress/engine.d.ts +23 -0
- package/dist/core/progress/engine.d.ts.map +1 -0
- package/dist/core/progress/engine.js +131 -0
- package/dist/core/progress/engine.js.map +1 -0
- package/dist/core/progress/engine.test.d.ts +2 -0
- package/dist/core/progress/engine.test.d.ts.map +1 -0
- package/dist/core/progress/engine.test.js +136 -0
- package/dist/core/progress/engine.test.js.map +1 -0
- package/dist/core/progress/git-xp.d.ts +16 -0
- package/dist/core/progress/git-xp.d.ts.map +1 -0
- package/dist/core/progress/git-xp.js +106 -0
- package/dist/core/progress/git-xp.js.map +1 -0
- package/dist/core/progress/git-xp.test.d.ts +2 -0
- package/dist/core/progress/git-xp.test.d.ts.map +1 -0
- package/dist/core/progress/git-xp.test.js +88 -0
- package/dist/core/progress/git-xp.test.js.map +1 -0
- package/dist/core/progress/ranks.d.ts +21 -0
- package/dist/core/progress/ranks.d.ts.map +1 -0
- package/dist/core/progress/ranks.js +141 -0
- package/dist/core/progress/ranks.js.map +1 -0
- package/dist/core/progress/ranks.test.d.ts +2 -0
- package/dist/core/progress/ranks.test.d.ts.map +1 -0
- package/dist/core/progress/ranks.test.js +59 -0
- package/dist/core/progress/ranks.test.js.map +1 -0
- package/dist/core/progress/rebuild.d.ts +7 -0
- package/dist/core/progress/rebuild.d.ts.map +1 -0
- package/dist/core/progress/rebuild.js +106 -0
- package/dist/core/progress/rebuild.js.map +1 -0
- package/dist/core/progress/rebuild.test.d.ts +2 -0
- package/dist/core/progress/rebuild.test.d.ts.map +1 -0
- package/dist/core/progress/rebuild.test.js +72 -0
- package/dist/core/progress/rebuild.test.js.map +1 -0
- package/dist/core/progress/store.d.ts +35 -0
- package/dist/core/progress/store.d.ts.map +1 -0
- package/dist/core/progress/store.js +200 -0
- package/dist/core/progress/store.js.map +1 -0
- package/dist/core/progress/store.test.d.ts +2 -0
- package/dist/core/progress/store.test.d.ts.map +1 -0
- package/dist/core/progress/store.test.js +108 -0
- package/dist/core/progress/store.test.js.map +1 -0
- package/dist/core/progress/types.d.ts +108 -0
- package/dist/core/progress/types.d.ts.map +1 -0
- package/dist/core/progress/types.js +3 -0
- package/dist/core/progress/types.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.test.d.ts","sourceRoot":"","sources":["../../../src/core/progress/engine.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { awardCommits, awardPrompt, applyStreak, commitXp, streakMultiplier } from "./engine.js";
|
|
3
|
+
import { createEmptyProgress, dayKey } from "./store.js";
|
|
4
|
+
const T0 = Date.parse("2026-07-01T12:00:00Z");
|
|
5
|
+
const HOUR = 60 * 60 * 1000;
|
|
6
|
+
const DAY = 24 * HOUR;
|
|
7
|
+
function fresh() {
|
|
8
|
+
return createEmptyProgress(new Date(T0));
|
|
9
|
+
}
|
|
10
|
+
function commit(sha, lines) {
|
|
11
|
+
return { sha, patchId: `pid-${sha}`, linesChanged: lines };
|
|
12
|
+
}
|
|
13
|
+
describe("awardPrompt", () => {
|
|
14
|
+
it("gives +10 for a normal prompt", () => {
|
|
15
|
+
const { file, gained } = awardPrompt(fresh(), T0);
|
|
16
|
+
expect(gained).toBe(10);
|
|
17
|
+
expect(file.xp).toBe(10);
|
|
18
|
+
expect(file.totals.prompts).toBe(1);
|
|
19
|
+
expect(file.xpBySource.prompts).toBe(10);
|
|
20
|
+
});
|
|
21
|
+
it("diminishes to +2 after 15 prompts in a rolling hour", () => {
|
|
22
|
+
let file = fresh();
|
|
23
|
+
for (let i = 0; i < 15; i++) {
|
|
24
|
+
file = awardPrompt(file, T0 + i * 1000).file;
|
|
25
|
+
}
|
|
26
|
+
const { gained } = awardPrompt(file, T0 + 16_000);
|
|
27
|
+
expect(gained).toBe(2);
|
|
28
|
+
});
|
|
29
|
+
it("recovers full XP once old prompts age out of the hour", () => {
|
|
30
|
+
let file = fresh();
|
|
31
|
+
for (let i = 0; i < 15; i++) {
|
|
32
|
+
file = awardPrompt(file, T0 + i * 1000).file;
|
|
33
|
+
}
|
|
34
|
+
const { gained } = awardPrompt(file, T0 + HOUR + 20_000);
|
|
35
|
+
expect(gained).toBe(10);
|
|
36
|
+
});
|
|
37
|
+
it("tracks distinct projects by hash", () => {
|
|
38
|
+
let file = fresh();
|
|
39
|
+
file = awardPrompt(file, T0, "/a").file;
|
|
40
|
+
file = awardPrompt(file, T0 + 1, "/b").file;
|
|
41
|
+
file = awardPrompt(file, T0 + 2, "/a").file;
|
|
42
|
+
expect(file.totals.projects).toHaveLength(2);
|
|
43
|
+
});
|
|
44
|
+
it("sets a fresh lastEvent nonce on every award", () => {
|
|
45
|
+
const r1 = awardPrompt(fresh(), T0);
|
|
46
|
+
const nonce1 = r1.file.lastEvent?.nonce;
|
|
47
|
+
const r2 = awardPrompt(r1.file, T0 + 1000);
|
|
48
|
+
expect(nonce1).toBeTruthy();
|
|
49
|
+
expect(r2.file.lastEvent?.nonce).not.toBe(nonce1);
|
|
50
|
+
});
|
|
51
|
+
it("reports levelUp when crossing a boundary", () => {
|
|
52
|
+
const file = fresh();
|
|
53
|
+
file.xp = 300; // level 2 needs 303
|
|
54
|
+
const { levelUp } = awardPrompt(file, T0);
|
|
55
|
+
expect(levelUp).toEqual({ from: 1, to: 2, rankName: "Tinkerer" });
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe("commitXp", () => {
|
|
59
|
+
it("scales sub-linearly and caps at 100", () => {
|
|
60
|
+
expect(commitXp(0)).toBe(20);
|
|
61
|
+
expect(commitXp(25)).toBe(40); // 20 + √25×4
|
|
62
|
+
expect(commitXp(400)).toBe(100); // capped
|
|
63
|
+
expect(commitXp(1_000_000)).toBe(100);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe("awardCommits", () => {
|
|
67
|
+
it("scores a commit by size", () => {
|
|
68
|
+
const { file, gained } = awardCommits(fresh(), [commit("a", 25)], T0);
|
|
69
|
+
expect(gained).toBe(40);
|
|
70
|
+
expect(file.totals.commits).toBe(1);
|
|
71
|
+
expect(file.totals.linesShipped).toBe(25);
|
|
72
|
+
expect(file.xpBySource.commits).toBe(40);
|
|
73
|
+
});
|
|
74
|
+
it("dedupes by patch-id — revert/recommit earns 0", () => {
|
|
75
|
+
const first = awardCommits(fresh(), [commit("a", 25)], T0);
|
|
76
|
+
const second = awardCommits(first.file, [{ ...commit("b", 25), patchId: "pid-a" }], T0 + 1000);
|
|
77
|
+
expect(second.gained).toBe(0);
|
|
78
|
+
expect(second.file.totals.commits).toBe(1); // not even counted
|
|
79
|
+
});
|
|
80
|
+
it("caps XP-earning commits at 6 per rolling hour", () => {
|
|
81
|
+
const commits = Array.from({ length: 8 }, (_, i) => commit(`c${i}`, 4));
|
|
82
|
+
const { file, gained } = awardCommits(fresh(), commits, T0);
|
|
83
|
+
expect(gained).toBe(6 * 28); // 20 + √4×4 = 28 each
|
|
84
|
+
expect(file.totals.commits).toBe(8); // totals still count all
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("streaks", () => {
|
|
88
|
+
it("multiplier tiers", () => {
|
|
89
|
+
expect(streakMultiplier(1)).toBe(1);
|
|
90
|
+
expect(streakMultiplier(3)).toBe(1.1);
|
|
91
|
+
expect(streakMultiplier(7)).toBe(1.25);
|
|
92
|
+
expect(streakMultiplier(14)).toBe(1.4);
|
|
93
|
+
expect(streakMultiplier(30)).toBe(1.5);
|
|
94
|
+
});
|
|
95
|
+
it("increments on consecutive days and resets after a gap", () => {
|
|
96
|
+
const file = fresh();
|
|
97
|
+
applyStreak(file, T0);
|
|
98
|
+
expect(file.streak.current).toBe(1);
|
|
99
|
+
applyStreak(file, T0 + DAY);
|
|
100
|
+
expect(file.streak.current).toBe(2);
|
|
101
|
+
applyStreak(file, T0 + DAY + 1000); // same day — no change
|
|
102
|
+
expect(file.streak.current).toBe(2);
|
|
103
|
+
applyStreak(file, T0 + 4 * DAY); // gap
|
|
104
|
+
expect(file.streak.current).toBe(1);
|
|
105
|
+
expect(file.streak.best).toBe(2);
|
|
106
|
+
});
|
|
107
|
+
it("applies the multiplier to prompt XP", () => {
|
|
108
|
+
const file = fresh();
|
|
109
|
+
file.streak = { current: 6, best: 6, lastActiveDay: dayKey(new Date(T0 - DAY)) };
|
|
110
|
+
const { gained } = awardPrompt(file, T0); // day 7 → ×1.25
|
|
111
|
+
expect(gained).toBe(Math.round(10 * 1.25));
|
|
112
|
+
expect(file.xpBySource.prompts).toBe(10);
|
|
113
|
+
expect(file.xpBySource.streakBonus).toBe(gained - 10);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe("daily soft cap", () => {
|
|
117
|
+
it("earns at 25% beyond 500 XP/day", () => {
|
|
118
|
+
const file = fresh();
|
|
119
|
+
file.rolling.dayXp = 500;
|
|
120
|
+
const { gained } = awardPrompt(file, T0);
|
|
121
|
+
expect(gained).toBe(Math.round(10 * 0.25));
|
|
122
|
+
});
|
|
123
|
+
it("splits an award straddling the cap", () => {
|
|
124
|
+
const file = fresh();
|
|
125
|
+
file.rolling.dayXp = 495;
|
|
126
|
+
const { gained } = awardPrompt(file, T0); // 5 full + 5×0.25
|
|
127
|
+
expect(gained).toBe(6);
|
|
128
|
+
});
|
|
129
|
+
it("resets on day rollover", () => {
|
|
130
|
+
const file = fresh();
|
|
131
|
+
file.rolling.dayXp = 500;
|
|
132
|
+
const { gained } = awardPrompt(file, T0 + DAY);
|
|
133
|
+
expect(gained).toBe(10);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
//# sourceMappingURL=engine.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.test.js","sourceRoot":"","sources":["../../../src/core/progress/engine.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ScoredCommit } from "./types.js";
|
|
2
|
+
/** Stable key for a repo root, used in ProgressFile.repos. */
|
|
3
|
+
export declare function repoKey(repoRoot: string): string;
|
|
4
|
+
export interface DetectResult {
|
|
5
|
+
repoRoot: string;
|
|
6
|
+
head: string;
|
|
7
|
+
commits: ScoredCommit[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Detect new, scoreable commits since `lastHead`.
|
|
11
|
+
* - `lastHead` undefined (first time seeing this repo) → record HEAD, score nothing.
|
|
12
|
+
* - Merge commits excluded; commits authored before the run window excluded.
|
|
13
|
+
* - Any git failure → null (silent).
|
|
14
|
+
*/
|
|
15
|
+
export declare function detectNewCommits(cwd: string, lastHead: string | undefined, runStartedAt: number): Promise<DetectResult | null>;
|
|
16
|
+
//# sourceMappingURL=git-xp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-xp.d.ts","sourceRoot":"","sources":["../../../src/core/progress/git-xp.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAqB/C,8DAA8D;AAC9D,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAsC9B"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Detect and score new commits made during an agent run. All failures are silent —
|
|
2
|
+
// progress must never break a run.
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
const GIT_TIMEOUT_MS = 5_000;
|
|
6
|
+
const MAX_COMMITS_PER_DETECT = 50;
|
|
7
|
+
/** Commits authored earlier than runStart - 5min are treated as imports (pull), not work. */
|
|
8
|
+
const AUTHOR_WINDOW_SLACK_MS = 5 * 60 * 1000;
|
|
9
|
+
function git(cwd, args, input) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const opts = { cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024 };
|
|
12
|
+
const child = execFile("git", args, opts, (err, stdout) => {
|
|
13
|
+
if (err)
|
|
14
|
+
reject(err);
|
|
15
|
+
else
|
|
16
|
+
resolve(String(stdout));
|
|
17
|
+
});
|
|
18
|
+
if (input !== undefined && child.stdin) {
|
|
19
|
+
child.stdin.write(input);
|
|
20
|
+
child.stdin.end();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Stable key for a repo root, used in ProgressFile.repos. */
|
|
25
|
+
export function repoKey(repoRoot) {
|
|
26
|
+
return crypto.createHash("sha256").update(repoRoot).digest("hex").slice(0, 16);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Detect new, scoreable commits since `lastHead`.
|
|
30
|
+
* - `lastHead` undefined (first time seeing this repo) → record HEAD, score nothing.
|
|
31
|
+
* - Merge commits excluded; commits authored before the run window excluded.
|
|
32
|
+
* - Any git failure → null (silent).
|
|
33
|
+
*/
|
|
34
|
+
export async function detectNewCommits(cwd, lastHead, runStartedAt) {
|
|
35
|
+
try {
|
|
36
|
+
const repoRoot = (await git(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
37
|
+
const head = (await git(cwd, ["rev-parse", "HEAD"])).trim();
|
|
38
|
+
if (!repoRoot || !head)
|
|
39
|
+
return null;
|
|
40
|
+
if (!lastHead || lastHead === head) {
|
|
41
|
+
return { repoRoot, head, commits: [] };
|
|
42
|
+
}
|
|
43
|
+
const sinceIso = new Date(runStartedAt - AUTHOR_WINDOW_SLACK_MS).toISOString();
|
|
44
|
+
let shas;
|
|
45
|
+
try {
|
|
46
|
+
const out = await git(cwd, [
|
|
47
|
+
"rev-list",
|
|
48
|
+
`${lastHead}..HEAD`,
|
|
49
|
+
"--no-merges",
|
|
50
|
+
`--since=${sinceIso}`,
|
|
51
|
+
`--max-count=${MAX_COMMITS_PER_DETECT}`,
|
|
52
|
+
]);
|
|
53
|
+
shas = out
|
|
54
|
+
.split("\n")
|
|
55
|
+
.map((s) => s.trim())
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// lastHead may be gone (rebase/gc) — reset the baseline without scoring.
|
|
60
|
+
return { repoRoot, head, commits: [] };
|
|
61
|
+
}
|
|
62
|
+
const commits = [];
|
|
63
|
+
for (const sha of shas.reverse()) {
|
|
64
|
+
const scored = await scoreCommit(cwd, sha);
|
|
65
|
+
if (scored)
|
|
66
|
+
commits.push(scored);
|
|
67
|
+
}
|
|
68
|
+
return { repoRoot, head, commits };
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function scoreCommit(cwd, sha) {
|
|
75
|
+
try {
|
|
76
|
+
// Lines changed: numstat sums added+deleted; binary files show "-" and count 0.
|
|
77
|
+
const numstat = await git(cwd, ["show", "--numstat", "--format=", sha]);
|
|
78
|
+
let linesChanged = 0;
|
|
79
|
+
for (const line of numstat.split("\n")) {
|
|
80
|
+
const parts = line.split("\t");
|
|
81
|
+
if (parts.length < 3)
|
|
82
|
+
continue;
|
|
83
|
+
const added = parseInt(parts[0], 10);
|
|
84
|
+
const deleted = parseInt(parts[1], 10);
|
|
85
|
+
if (Number.isFinite(added))
|
|
86
|
+
linesChanged += added;
|
|
87
|
+
if (Number.isFinite(deleted))
|
|
88
|
+
linesChanged += deleted;
|
|
89
|
+
}
|
|
90
|
+
// Patch-id dedupe: revert/recommit of the same diff scores 0.
|
|
91
|
+
let patchId = "";
|
|
92
|
+
try {
|
|
93
|
+
const diff = await git(cwd, ["show", sha]);
|
|
94
|
+
const out = await git(cwd, ["patch-id", "--stable"], diff);
|
|
95
|
+
patchId = out.trim().split(" ")[0] ?? "";
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
patchId = "";
|
|
99
|
+
}
|
|
100
|
+
return { sha, patchId, linesChanged };
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=git-xp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-xp.js","sourceRoot":"","sources":["../../../src/core/progress/git-xp.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-xp.test.d.ts","sourceRoot":"","sources":["../../../src/core/progress/git-xp.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { detectNewCommits } from "./git-xp.js";
|
|
7
|
+
function hasGit() {
|
|
8
|
+
try {
|
|
9
|
+
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const gitAvailable = hasGit();
|
|
17
|
+
const d = gitAvailable ? describe : describe.skip;
|
|
18
|
+
let repo;
|
|
19
|
+
function git(...args) {
|
|
20
|
+
return execFileSync("git", args, {
|
|
21
|
+
cwd: repo,
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
env: {
|
|
24
|
+
...process.env,
|
|
25
|
+
GIT_AUTHOR_NAME: "Test",
|
|
26
|
+
GIT_AUTHOR_EMAIL: "t@t.t",
|
|
27
|
+
GIT_COMMITTER_NAME: "Test",
|
|
28
|
+
GIT_COMMITTER_EMAIL: "t@t.t",
|
|
29
|
+
},
|
|
30
|
+
}).trim();
|
|
31
|
+
}
|
|
32
|
+
async function commitFile(name, lines, message) {
|
|
33
|
+
const content = Array.from({ length: lines }, (_, i) => `line ${i} of ${name}`).join("\n");
|
|
34
|
+
await fs.writeFile(path.join(repo, name), content + "\n");
|
|
35
|
+
git("add", name);
|
|
36
|
+
git("commit", "-m", message);
|
|
37
|
+
return git("rev-parse", "HEAD");
|
|
38
|
+
}
|
|
39
|
+
d("detectNewCommits", () => {
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
repo = await fs.mkdtemp(path.join(os.tmpdir(), "gg-gitxp-"));
|
|
42
|
+
git("init", "-q");
|
|
43
|
+
});
|
|
44
|
+
afterEach(async () => {
|
|
45
|
+
await fs.rm(repo, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
it("first sight of a repo records HEAD without scoring", async () => {
|
|
48
|
+
await commitFile("a.txt", 10, "initial");
|
|
49
|
+
const result = await detectNewCommits(repo, undefined, Date.now());
|
|
50
|
+
expect(result).not.toBeNull();
|
|
51
|
+
expect(result.commits).toHaveLength(0);
|
|
52
|
+
expect(result.head).toBe(git("rev-parse", "HEAD"));
|
|
53
|
+
});
|
|
54
|
+
it("scores new commits with line counts and patch ids", async () => {
|
|
55
|
+
const base = await commitFile("a.txt", 5, "initial");
|
|
56
|
+
const runStart = Date.now();
|
|
57
|
+
await commitFile("b.txt", 30, "feature");
|
|
58
|
+
const result = await detectNewCommits(repo, base, runStart);
|
|
59
|
+
expect(result.commits).toHaveLength(1);
|
|
60
|
+
expect(result.commits[0].linesChanged).toBe(30);
|
|
61
|
+
expect(result.commits[0].patchId).toMatch(/^[0-9a-f]{40}/);
|
|
62
|
+
});
|
|
63
|
+
it("returns no commits when HEAD is unchanged", async () => {
|
|
64
|
+
const head = await commitFile("a.txt", 5, "initial");
|
|
65
|
+
const result = await detectNewCommits(repo, head, Date.now());
|
|
66
|
+
expect(result.commits).toHaveLength(0);
|
|
67
|
+
});
|
|
68
|
+
it("excludes commits authored before the run window", async () => {
|
|
69
|
+
const base = await commitFile("a.txt", 5, "initial");
|
|
70
|
+
await commitFile("b.txt", 10, "old work");
|
|
71
|
+
// Run "started" an hour from now — the commit above predates the window.
|
|
72
|
+
const result = await detectNewCommits(repo, base, Date.now() + 60 * 60 * 1000);
|
|
73
|
+
expect(result.commits).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
it("resets the baseline silently when lastHead is unknown", async () => {
|
|
76
|
+
await commitFile("a.txt", 5, "initial");
|
|
77
|
+
const result = await detectNewCommits(repo, "0".repeat(40), Date.now());
|
|
78
|
+
expect(result).not.toBeNull();
|
|
79
|
+
expect(result.commits).toHaveLength(0);
|
|
80
|
+
});
|
|
81
|
+
it("returns null for a non-repo directory", async () => {
|
|
82
|
+
const plain = await fs.mkdtemp(path.join(os.tmpdir(), "gg-plain-"));
|
|
83
|
+
const result = await detectNewCommits(plain, undefined, Date.now());
|
|
84
|
+
expect(result).toBeNull();
|
|
85
|
+
await fs.rm(plain, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
//# sourceMappingURL=git-xp.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-xp.test.js","sourceRoot":"","sources":["../../../src/core/progress/git-xp.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ProgressFile, ProgressSnapshot, RankLadderEntry } from "./types.js";
|
|
2
|
+
export declare const MAX_LEVEL = 50;
|
|
3
|
+
/** Cumulative XP required to reach level n (level 1 = 0). */
|
|
4
|
+
export declare function xpForLevel(n: number): number;
|
|
5
|
+
/** Level for a cumulative XP total (1..MAX_LEVEL). */
|
|
6
|
+
export declare function levelForXp(xp: number): number;
|
|
7
|
+
export interface RankInfo {
|
|
8
|
+
level: number;
|
|
9
|
+
name: string;
|
|
10
|
+
tier: number;
|
|
11
|
+
tierName: string;
|
|
12
|
+
tierGlyph: string;
|
|
13
|
+
effectId: string;
|
|
14
|
+
}
|
|
15
|
+
/** Rank metadata for a level (clamped to 1..MAX_LEVEL). */
|
|
16
|
+
export declare function rankForLevel(level: number): RankInfo;
|
|
17
|
+
/** Build the broadcast snapshot the webview renders verbatim. */
|
|
18
|
+
export declare function buildSnapshot(file: ProgressFile): ProgressSnapshot;
|
|
19
|
+
/** Full 50-entry ladder for the scorecard. */
|
|
20
|
+
export declare function rankLadder(): RankLadderEntry[];
|
|
21
|
+
//# sourceMappingURL=ranks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ranks.d.ts","sourceRoot":"","sources":["../../../src/core/progress/ranks.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElF,eAAO,MAAM,SAAS,KAAK,CAAC;AAwE5B,6DAA6D;AAC7D,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAG5C;AAED,sDAAsD;AACtD,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAI7C;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAYpD;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,gBAAgB,CA+BlE;AAED,8CAA8C;AAC9C,wBAAgB,UAAU,IAAI,eAAe,EAAE,CAc9C"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// The 50-rank ladder: 10 tiers × 5 levels, with CSS effect ids and tier glyphs.
|
|
2
|
+
export const MAX_LEVEL = 50;
|
|
3
|
+
const TIERS = [
|
|
4
|
+
{
|
|
5
|
+
name: "Boot",
|
|
6
|
+
glyph: "○",
|
|
7
|
+
effectId: "dim",
|
|
8
|
+
ranks: ["Lurker", "Tinkerer", "Prompter", "Looper", "Scripter"],
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: "Ship",
|
|
12
|
+
glyph: "◇",
|
|
13
|
+
effectId: "plain",
|
|
14
|
+
ranks: ["Patcher", "Forker", "Merger", "Shipper", "Builder"],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "Flow",
|
|
18
|
+
glyph: "◆",
|
|
19
|
+
effectId: "blue",
|
|
20
|
+
ranks: ["Hacker", "Stacker", "Debugger", "Compiler", "Operator"],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "Craft",
|
|
24
|
+
glyph: "⬖",
|
|
25
|
+
effectId: "green",
|
|
26
|
+
ranks: ["Toolsmith", "Machinist", "Optimizer", "Artificer", "Architect"],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "Vibe",
|
|
30
|
+
glyph: "✦",
|
|
31
|
+
effectId: "gradient",
|
|
32
|
+
ranks: ["Vibesmith", "Codeslinger", "Bytebender", "Overclocker", "Netrunner"],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "Deep",
|
|
36
|
+
glyph: "✧",
|
|
37
|
+
effectId: "gradient-glow",
|
|
38
|
+
ranks: ["Cipher", "Daemon", "Phantom", "Glitch", "Specter"],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "Arcane",
|
|
42
|
+
glyph: "❖",
|
|
43
|
+
effectId: "animated",
|
|
44
|
+
ranks: ["Warlock", "Technomancer", "Codeweaver", "Archmage", "Oracle"],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "Root",
|
|
48
|
+
glyph: "⬢",
|
|
49
|
+
effectId: "gold",
|
|
50
|
+
ranks: ["Shellmaster", "Kernelghost", "Gitlord", "Mainframe", "Root"],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "Myth",
|
|
54
|
+
glyph: "★",
|
|
55
|
+
effectId: "gold-shimmer",
|
|
56
|
+
ranks: ["Basilisk", "Ascendant", "Sentinel", "Harbinger", "Titan"],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "Beyond",
|
|
60
|
+
glyph: "✹",
|
|
61
|
+
effectId: "iridescent",
|
|
62
|
+
ranks: ["Anomaly", "Entity", "Overmind", "Deity", "Singularity"],
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
/** Cumulative XP required to reach level n (level 1 = 0). */
|
|
66
|
+
export function xpForLevel(n) {
|
|
67
|
+
if (n <= 1)
|
|
68
|
+
return 0;
|
|
69
|
+
return Math.round(100 * Math.pow(n, 1.6));
|
|
70
|
+
}
|
|
71
|
+
/** Level for a cumulative XP total (1..MAX_LEVEL). */
|
|
72
|
+
export function levelForXp(xp) {
|
|
73
|
+
let level = 1;
|
|
74
|
+
while (level < MAX_LEVEL && xp >= xpForLevel(level + 1))
|
|
75
|
+
level++;
|
|
76
|
+
return level;
|
|
77
|
+
}
|
|
78
|
+
/** Rank metadata for a level (clamped to 1..MAX_LEVEL). */
|
|
79
|
+
export function rankForLevel(level) {
|
|
80
|
+
const l = Math.min(Math.max(1, Math.floor(level)), MAX_LEVEL);
|
|
81
|
+
const tierIndex = Math.floor((l - 1) / 5);
|
|
82
|
+
const tier = TIERS[tierIndex];
|
|
83
|
+
return {
|
|
84
|
+
level: l,
|
|
85
|
+
name: tier.ranks[(l - 1) % 5],
|
|
86
|
+
tier: tierIndex + 1,
|
|
87
|
+
tierName: tier.name,
|
|
88
|
+
tierGlyph: tier.glyph,
|
|
89
|
+
effectId: tier.effectId,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Build the broadcast snapshot the webview renders verbatim. */
|
|
93
|
+
export function buildSnapshot(file) {
|
|
94
|
+
const level = levelForXp(file.xp);
|
|
95
|
+
const rank = rankForLevel(level);
|
|
96
|
+
const floor = xpForLevel(level);
|
|
97
|
+
const ceil = level >= MAX_LEVEL ? floor : xpForLevel(level + 1);
|
|
98
|
+
const span = Math.max(1, ceil - floor);
|
|
99
|
+
const into = Math.max(0, file.xp - floor);
|
|
100
|
+
return {
|
|
101
|
+
level,
|
|
102
|
+
rankName: rank.name,
|
|
103
|
+
tier: rank.tier,
|
|
104
|
+
tierName: rank.tierName,
|
|
105
|
+
tierGlyph: rank.tierGlyph,
|
|
106
|
+
effectId: rank.effectId,
|
|
107
|
+
xp: file.xp,
|
|
108
|
+
xpIntoLevel: into,
|
|
109
|
+
xpForLevel: span,
|
|
110
|
+
percent: level >= MAX_LEVEL ? 100 : Math.min(100, Math.floor((into / span) * 100)),
|
|
111
|
+
streak: { current: file.streak.current, best: file.streak.best },
|
|
112
|
+
totals: {
|
|
113
|
+
prompts: file.totals.prompts,
|
|
114
|
+
commits: file.totals.commits,
|
|
115
|
+
linesShipped: file.totals.linesShipped,
|
|
116
|
+
projects: file.totals.projects.length,
|
|
117
|
+
},
|
|
118
|
+
xpBySource: { ...file.xpBySource },
|
|
119
|
+
memberSince: file.createdAt,
|
|
120
|
+
ladder: rankLadder(),
|
|
121
|
+
levelUp: file.lastEvent?.levelUp ?? null,
|
|
122
|
+
eventNonce: file.lastEvent?.nonce ?? null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Full 50-entry ladder for the scorecard. */
|
|
126
|
+
export function rankLadder() {
|
|
127
|
+
const ladder = [];
|
|
128
|
+
for (let level = 1; level <= MAX_LEVEL; level++) {
|
|
129
|
+
const rank = rankForLevel(level);
|
|
130
|
+
ladder.push({
|
|
131
|
+
level,
|
|
132
|
+
name: rank.name,
|
|
133
|
+
tier: rank.tier,
|
|
134
|
+
tierName: rank.tierName,
|
|
135
|
+
effectId: rank.effectId,
|
|
136
|
+
xpRequired: xpForLevel(level),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return ladder;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=ranks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ranks.js","sourceRoot":"","sources":["../../../src/core/progress/ranks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ranks.test.d.ts","sourceRoot":"","sources":["../../../src/core/progress/ranks.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { MAX_LEVEL, levelForXp, rankForLevel, rankLadder, xpForLevel } from "./ranks.js";
|
|
3
|
+
describe("xpForLevel", () => {
|
|
4
|
+
it("matches the 100 × N^1.6 curve", () => {
|
|
5
|
+
expect(xpForLevel(1)).toBe(0);
|
|
6
|
+
expect(xpForLevel(2)).toBe(303);
|
|
7
|
+
expect(xpForLevel(10)).toBe(3981);
|
|
8
|
+
expect(xpForLevel(50)).toBe(52282);
|
|
9
|
+
});
|
|
10
|
+
it("is strictly increasing", () => {
|
|
11
|
+
for (let n = 2; n <= MAX_LEVEL; n++) {
|
|
12
|
+
expect(xpForLevel(n)).toBeGreaterThan(xpForLevel(n - 1));
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
describe("levelForXp", () => {
|
|
17
|
+
it("returns 1 at 0 XP", () => {
|
|
18
|
+
expect(levelForXp(0)).toBe(1);
|
|
19
|
+
});
|
|
20
|
+
it("crosses level boundaries exactly", () => {
|
|
21
|
+
expect(levelForXp(302)).toBe(1);
|
|
22
|
+
expect(levelForXp(303)).toBe(2);
|
|
23
|
+
expect(levelForXp(3981)).toBe(10);
|
|
24
|
+
});
|
|
25
|
+
it("caps at MAX_LEVEL", () => {
|
|
26
|
+
expect(levelForXp(10_000_000)).toBe(MAX_LEVEL);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe("rankForLevel", () => {
|
|
30
|
+
it("names the tier boundaries correctly", () => {
|
|
31
|
+
expect(rankForLevel(1).name).toBe("Lurker");
|
|
32
|
+
expect(rankForLevel(5).name).toBe("Scripter");
|
|
33
|
+
expect(rankForLevel(6).name).toBe("Patcher");
|
|
34
|
+
expect(rankForLevel(20).name).toBe("Architect");
|
|
35
|
+
expect(rankForLevel(25).name).toBe("Netrunner");
|
|
36
|
+
expect(rankForLevel(50).name).toBe("Singularity");
|
|
37
|
+
});
|
|
38
|
+
it("assigns tiers 1–10", () => {
|
|
39
|
+
expect(rankForLevel(1).tier).toBe(1);
|
|
40
|
+
expect(rankForLevel(5).tier).toBe(1);
|
|
41
|
+
expect(rankForLevel(6).tier).toBe(2);
|
|
42
|
+
expect(rankForLevel(50).tier).toBe(10);
|
|
43
|
+
});
|
|
44
|
+
it("clamps out-of-range levels", () => {
|
|
45
|
+
expect(rankForLevel(0).level).toBe(1);
|
|
46
|
+
expect(rankForLevel(99).level).toBe(50);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe("rankLadder", () => {
|
|
50
|
+
it("has 50 unique names with increasing xpRequired", () => {
|
|
51
|
+
const ladder = rankLadder();
|
|
52
|
+
expect(ladder).toHaveLength(50);
|
|
53
|
+
expect(new Set(ladder.map((r) => r.name)).size).toBe(50);
|
|
54
|
+
for (let i = 1; i < ladder.length; i++) {
|
|
55
|
+
expect(ladder[i].xpRequired).toBeGreaterThan(ladder[i - 1].xpRequired);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
//# sourceMappingURL=ranks.test.js.map
|