@kenkaiiii/ggcoder 5.6.3 → 5.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/dist/app-sidecar.js +291 -7
- package/dist/app-sidecar.js.map +1 -1
- package/dist/core/autopilot-cycle.d.ts +52 -17
- package/dist/core/autopilot-cycle.d.ts.map +1 -1
- package/dist/core/autopilot-cycle.js +53 -13
- package/dist/core/autopilot-cycle.js.map +1 -1
- package/dist/core/autopilot-cycle.test.js +195 -10
- package/dist/core/autopilot-cycle.test.js.map +1 -1
- package/dist/core/autopilot-gate.d.ts +12 -0
- package/dist/core/autopilot-gate.d.ts.map +1 -1
- package/dist/core/autopilot-gate.js +10 -1
- package/dist/core/autopilot-gate.js.map +1 -1
- package/dist/core/autopilot-gate.test.js +27 -2
- package/dist/core/autopilot-gate.test.js.map +1 -1
- package/dist/core/ken-context.d.ts +20 -0
- package/dist/core/ken-context.d.ts.map +1 -1
- package/dist/core/ken-context.js +46 -2
- package/dist/core/ken-context.js.map +1 -1
- package/dist/core/ken-context.test.js +52 -5
- package/dist/core/ken-context.test.js.map +1 -1
- package/dist/core/ken-prompt.js +14 -4
- package/dist/core/ken-prompt.js.map +1 -1
- package/dist/core/ken-prompt.test.js +20 -3
- package/dist/core/ken-prompt.test.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/dist/core/session-manager.d.ts +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +5 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +9 -0
- package/dist/core/session-manager.test.js.map +1 -1
- package/package.json +4 -4
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ranks.test.js","sourceRoot":"","sources":["../../../src/core/progress/ranks.test.ts"],"names":[],"mappings":"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"}
|