@kevinrabun/judges 3.53.0 → 3.54.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/CHANGELOG.md +12 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +56 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/approve-chain.d.ts +8 -0
- package/dist/commands/approve-chain.d.ts.map +1 -0
- package/dist/commands/approve-chain.js +235 -0
- package/dist/commands/approve-chain.js.map +1 -0
- package/dist/commands/coach-mode.d.ts +8 -0
- package/dist/commands/coach-mode.d.ts.map +1 -0
- package/dist/commands/coach-mode.js +230 -0
- package/dist/commands/coach-mode.js.map +1 -0
- package/dist/commands/context-inject.d.ts +9 -0
- package/dist/commands/context-inject.d.ts.map +1 -0
- package/dist/commands/context-inject.js +212 -0
- package/dist/commands/context-inject.js.map +1 -0
- package/dist/commands/finding-contest.d.ts +8 -0
- package/dist/commands/finding-contest.d.ts.map +1 -0
- package/dist/commands/finding-contest.js +193 -0
- package/dist/commands/finding-contest.js.map +1 -0
- package/dist/commands/habit-tracker.d.ts +8 -0
- package/dist/commands/habit-tracker.d.ts.map +1 -0
- package/dist/commands/habit-tracker.js +195 -0
- package/dist/commands/habit-tracker.js.map +1 -0
- package/dist/commands/prompt-replay.d.ts +8 -0
- package/dist/commands/prompt-replay.d.ts.map +1 -0
- package/dist/commands/prompt-replay.js +177 -0
- package/dist/commands/prompt-replay.js.map +1 -0
- package/dist/commands/review-replay.d.ts +9 -0
- package/dist/commands/review-replay.d.ts.map +1 -0
- package/dist/commands/review-replay.js +265 -0
- package/dist/commands/review-replay.js.map +1 -0
- package/dist/commands/snippet-eval.d.ts +8 -0
- package/dist/commands/snippet-eval.d.ts.map +1 -0
- package/dist/commands/snippet-eval.js +224 -0
- package/dist/commands/snippet-eval.js.map +1 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context inject — feed project-specific context (architecture docs,
|
|
3
|
+
* API contracts, coding standards) into evaluation for higher-precision
|
|
4
|
+
* findings.
|
|
5
|
+
*
|
|
6
|
+
* Parses context files and maintains a local context cache for judges.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
|
|
9
|
+
import { join, basename } from "path";
|
|
10
|
+
// ─── Storage ────────────────────────────────────────────────────────────────
|
|
11
|
+
const CONTEXT_DIR = ".judges-context";
|
|
12
|
+
function ensureDir() {
|
|
13
|
+
if (!existsSync(CONTEXT_DIR))
|
|
14
|
+
mkdirSync(CONTEXT_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
function loadProfile() {
|
|
17
|
+
const file = join(CONTEXT_DIR, "profile.json");
|
|
18
|
+
if (!existsSync(file))
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(file, "utf-8"));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function saveProfile(profile) {
|
|
28
|
+
ensureDir();
|
|
29
|
+
writeFileSync(join(CONTEXT_DIR, "profile.json"), JSON.stringify(profile, null, 2));
|
|
30
|
+
}
|
|
31
|
+
// ─── Context Extraction ────────────────────────────────────────────────────
|
|
32
|
+
const RULE_PATTERNS = [
|
|
33
|
+
{ regex: /(?:must|should|always|never|require|mandatory)\s+(.{10,80})/i, category: "requirement" },
|
|
34
|
+
{ regex: /(?:do not|don't|avoid|prohibit|forbid)\s+(.{10,80})/i, category: "prohibition" },
|
|
35
|
+
{ regex: /(?:all|every)\s+(?:api|endpoint|route|handler)\s+(?:must|should)\s+(.{10,80})/i, category: "api-standard" },
|
|
36
|
+
{
|
|
37
|
+
regex: /(?:database|db|data)\s+(?:access|query|operation)\s+(?:must|should)\s+(.{10,80})/i,
|
|
38
|
+
category: "data-access",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
regex: /(?:error|exception)\s+(?:handling|management)\s+(?:must|should)\s+(.{10,80})/i,
|
|
42
|
+
category: "error-handling",
|
|
43
|
+
},
|
|
44
|
+
{ regex: /(?:auth|authentication|authorization)\s+(.{10,80})/i, category: "auth" },
|
|
45
|
+
{ regex: /(?:naming|convention|style)\s+(?:must|should|:)\s*(.{10,80})/i, category: "naming" },
|
|
46
|
+
{ regex: /(?:test|testing)\s+(?:must|should|require)\s+(.{10,80})/i, category: "testing" },
|
|
47
|
+
{ regex: /(?:log|logging)\s+(?:must|should|require)\s+(.{10,80})/i, category: "logging" },
|
|
48
|
+
{ regex: /(?:security|secure)\s+(?:must|should|require)\s+(.{10,80})/i, category: "security" },
|
|
49
|
+
];
|
|
50
|
+
function extractRules(content, source) {
|
|
51
|
+
const rules = [];
|
|
52
|
+
const lines = content.split("\n");
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
56
|
+
continue;
|
|
57
|
+
for (const pattern of RULE_PATTERNS) {
|
|
58
|
+
const match = pattern.regex.exec(trimmed);
|
|
59
|
+
if (match) {
|
|
60
|
+
rules.push({ source: basename(source), category: pattern.category, rule: trimmed.substring(0, 120) });
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return rules;
|
|
66
|
+
}
|
|
67
|
+
// ─── CLI ────────────────────────────────────────────────────────────────────
|
|
68
|
+
export function runContextInject(argv) {
|
|
69
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
70
|
+
console.log(`
|
|
71
|
+
judges context-inject — Feed project context into evaluation
|
|
72
|
+
|
|
73
|
+
Usage:
|
|
74
|
+
judges context-inject --add docs/architecture.md
|
|
75
|
+
judges context-inject --add docs/coding-standards.md
|
|
76
|
+
judges context-inject --show
|
|
77
|
+
judges context-inject --scan docs/
|
|
78
|
+
judges context-inject --clear
|
|
79
|
+
|
|
80
|
+
Options:
|
|
81
|
+
--add <file> Add a context file (Markdown/YAML/text)
|
|
82
|
+
--scan <dir> Scan directory for context documents
|
|
83
|
+
--show Show current context profile
|
|
84
|
+
--clear Clear all context
|
|
85
|
+
--format json JSON output
|
|
86
|
+
--help, -h Show this help
|
|
87
|
+
|
|
88
|
+
Context files are parsed for rules, standards, and conventions that
|
|
89
|
+
judges use to calibrate findings for your specific project.
|
|
90
|
+
`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const format = argv.find((_a, i) => argv[i - 1] === "--format") || "text";
|
|
94
|
+
const isAdd = argv.includes("--add");
|
|
95
|
+
const _isShow = argv.includes("--show");
|
|
96
|
+
const isScan = argv.includes("--scan");
|
|
97
|
+
const isClear = argv.includes("--clear");
|
|
98
|
+
if (isClear) {
|
|
99
|
+
saveProfile({
|
|
100
|
+
name: "default",
|
|
101
|
+
sources: [],
|
|
102
|
+
rules: [],
|
|
103
|
+
createdAt: new Date().toISOString(),
|
|
104
|
+
updatedAt: new Date().toISOString(),
|
|
105
|
+
});
|
|
106
|
+
console.log(" ✅ Context cleared");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (isAdd) {
|
|
110
|
+
const fileIdx = argv.indexOf("--add") + 1;
|
|
111
|
+
const file = argv[fileIdx] || "";
|
|
112
|
+
if (!file || !existsSync(file)) {
|
|
113
|
+
console.error(` File not found: ${file}`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const content = readFileSync(file, "utf-8");
|
|
117
|
+
const newRules = extractRules(content, file);
|
|
118
|
+
const profile = loadProfile() || {
|
|
119
|
+
name: "default",
|
|
120
|
+
sources: [],
|
|
121
|
+
rules: [],
|
|
122
|
+
createdAt: new Date().toISOString(),
|
|
123
|
+
updatedAt: "",
|
|
124
|
+
};
|
|
125
|
+
if (!profile.sources.includes(file))
|
|
126
|
+
profile.sources.push(file);
|
|
127
|
+
profile.rules.push(...newRules);
|
|
128
|
+
profile.updatedAt = new Date().toISOString();
|
|
129
|
+
saveProfile(profile);
|
|
130
|
+
console.log(` ✅ Added ${file} — extracted ${newRules.length} rule(s)`);
|
|
131
|
+
if (newRules.length > 0) {
|
|
132
|
+
for (const r of newRules.slice(0, 5)) {
|
|
133
|
+
console.log(` [${r.category}] ${r.rule}`);
|
|
134
|
+
}
|
|
135
|
+
if (newRules.length > 5)
|
|
136
|
+
console.log(` ... and ${newRules.length - 5} more`);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (isScan) {
|
|
141
|
+
const dirIdx = argv.indexOf("--scan") + 1;
|
|
142
|
+
const dir = argv[dirIdx] || "docs";
|
|
143
|
+
if (!existsSync(dir)) {
|
|
144
|
+
console.error(` Directory not found: ${dir}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const DOC_EXTS = new Set([".md", ".txt", ".yaml", ".yml", ".rst"]);
|
|
148
|
+
let entries;
|
|
149
|
+
try {
|
|
150
|
+
entries = readdirSync(dir);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
entries = [];
|
|
154
|
+
}
|
|
155
|
+
const docFiles = entries.filter((e) => DOC_EXTS.has(join(".", e).includes(".") ? "." + e.split(".").pop() : ""));
|
|
156
|
+
const profile = loadProfile() || {
|
|
157
|
+
name: "default",
|
|
158
|
+
sources: [],
|
|
159
|
+
rules: [],
|
|
160
|
+
createdAt: new Date().toISOString(),
|
|
161
|
+
updatedAt: "",
|
|
162
|
+
};
|
|
163
|
+
let totalNew = 0;
|
|
164
|
+
for (const f of docFiles) {
|
|
165
|
+
const fullPath = join(dir, f);
|
|
166
|
+
try {
|
|
167
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
168
|
+
const rules = extractRules(content, fullPath);
|
|
169
|
+
if (!profile.sources.includes(fullPath))
|
|
170
|
+
profile.sources.push(fullPath);
|
|
171
|
+
profile.rules.push(...rules);
|
|
172
|
+
totalNew += rules.length;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
/* skip */
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
profile.updatedAt = new Date().toISOString();
|
|
179
|
+
saveProfile(profile);
|
|
180
|
+
console.log(` ✅ Scanned ${docFiles.length} doc(s) in ${dir} — extracted ${totalNew} rule(s)`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Default: show profile
|
|
184
|
+
const profile = loadProfile();
|
|
185
|
+
if (!profile || profile.rules.length === 0) {
|
|
186
|
+
console.log(" No context loaded. Use --add <file> or --scan <dir> to inject context.");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (format === "json") {
|
|
190
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
console.log(`\n Context Profile — ${profile.rules.length} rule(s) from ${profile.sources.length} source(s)\n ──────────────────────────`);
|
|
194
|
+
const byCategory = new Map();
|
|
195
|
+
for (const r of profile.rules) {
|
|
196
|
+
const list = byCategory.get(r.category) || [];
|
|
197
|
+
list.push(r);
|
|
198
|
+
byCategory.set(r.category, list);
|
|
199
|
+
}
|
|
200
|
+
for (const [cat, rules] of byCategory) {
|
|
201
|
+
console.log(`\n 📋 ${cat} (${rules.length}):`);
|
|
202
|
+
for (const r of rules.slice(0, 5)) {
|
|
203
|
+
console.log(` ${r.rule}`);
|
|
204
|
+
}
|
|
205
|
+
if (rules.length > 5)
|
|
206
|
+
console.log(` ... and ${rules.length - 5} more`);
|
|
207
|
+
}
|
|
208
|
+
console.log(`\n Sources: ${profile.sources.join(", ")}`);
|
|
209
|
+
console.log(` Last updated: ${profile.updatedAt}\n`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=context-inject.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-inject.js","sourceRoot":"","sources":["../../src/commands/context-inject.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,IAAI,CAAC;AACrF,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAmBtC,+EAA+E;AAE/E,MAAM,WAAW,GAAG,iBAAiB,CAAC;AAEtC,SAAS,SAAS;IAChB,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED,SAAS,WAAW;IAClB,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IAC/C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,OAAuB;IAC1C,SAAS,EAAE,CAAC;IACZ,aAAa,CAAC,IAAI,CAAC,WAAW,EAAE,cAAc,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AACrF,CAAC;AAED,8EAA8E;AAE9E,MAAM,aAAa,GAA+C;IAChE,EAAE,KAAK,EAAE,8DAA8D,EAAE,QAAQ,EAAE,aAAa,EAAE;IAClG,EAAE,KAAK,EAAE,sDAAsD,EAAE,QAAQ,EAAE,aAAa,EAAE;IAC1F,EAAE,KAAK,EAAE,gFAAgF,EAAE,QAAQ,EAAE,cAAc,EAAE;IACrH;QACE,KAAK,EAAE,mFAAmF;QAC1F,QAAQ,EAAE,aAAa;KACxB;IACD;QACE,KAAK,EAAE,+EAA+E;QACtF,QAAQ,EAAE,gBAAgB;KAC3B;IACD,EAAE,KAAK,EAAE,qDAAqD,EAAE,QAAQ,EAAE,MAAM,EAAE;IAClF,EAAE,KAAK,EAAE,+DAA+D,EAAE,QAAQ,EAAE,QAAQ,EAAE;IAC9F,EAAE,KAAK,EAAE,0DAA0D,EAAE,QAAQ,EAAE,SAAS,EAAE;IAC1F,EAAE,KAAK,EAAE,yDAAyD,EAAE,QAAQ,EAAE,SAAS,EAAE;IACzF,EAAE,KAAK,EAAE,6DAA6D,EAAE,QAAQ,EAAE,UAAU,EAAE;CAC/F,CAAC;AAEF,SAAS,YAAY,CAAC,OAAe,EAAE,MAAc;IACnD,MAAM,KAAK,GAAkB,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAElC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAElD,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;YACpC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC1C,IAAI,KAAK,EAAE,CAAC;gBACV,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;gBACtG,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,+EAA+E;AAE/E,MAAM,UAAU,gBAAgB,CAAC,IAAc;IAC7C,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACnD,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;CAoBf,CAAC,CAAC;QACC,OAAO;IACT,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAU,EAAE,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,UAAU,CAAC,IAAI,MAAM,CAAC;IAC1F,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACrC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAEzC,IAAI,OAAO,EAAE,CAAC;QACZ,WAAW,CAAC;YACV,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,EAAE;YACX,KAAK,EAAE,EAAE;YACT,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;QACnC,OAAO;IACT,CAAC;IAED,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QACjC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAC/B,OAAO,CAAC,KAAK,CAAC,qBAAqB,IAAI,EAAE,CAAC,CAAC;YAC3C,OAAO;QACT,CAAC;QAED,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC5C,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAE7C,MAAM,OAAO,GAAG,WAAW,EAAE,IAAI;YAC/B,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,EAAE;YACX,KAAK,EAAE,EAAE;YACT,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,SAAS,EAAE,EAAE;SACd,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC;QAChC,OAAO,CAAC,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC7C,WAAW,CAAC,OAAO,CAAC,CAAC;QAErB,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,gBAAgB,QAAQ,CAAC,MAAM,UAAU,CAAC,CAAC;QACxE,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,KAAK,MAAM,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;gBACrC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YAC/C,CAAC;YACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;gBAAE,OAAO,CAAC,GAAG,CAAC,eAAe,QAAQ,CAAC,MAAM,GAAG,CAAC,OAAO,CAAC,CAAC;QAClF,CAAC;QACD,OAAO;IACT,CAAC;IAED,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC;QACnC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACrB,OAAO,CAAC,KAAK,CAAC,0BAA0B,GAAG,EAAE,CAAC,CAAC;YAC/C,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QACnE,IAAI,OAAiB,CAAC;QACtB,IAAI,CAAC;YACH,OAAO,GAAG,WAAW,CAAC,GAAG,CAAwB,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,GAAG,EAAE,CAAC;QACf,CAAC;QACD,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAEjH,MAAM,OAAO,GAAG,WAAW,EAAE,IAAI;YAC/B,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,EAAE;YACX,KAAK,EAAE,EAAE;YACT,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,SAAS,EAAE,EAAE;SACd,CAAC;QACF,IAAI,QAAQ,GAAG,CAAC,CAAC;QAEjB,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YAC9B,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;gBAChD,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;gBAC9C,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;oBAAE,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACxE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;gBAC7B,QAAQ,IAAI,KAAK,CAAC,MAAM,CAAC;YAC3B,CAAC;YAAC,MAAM,CAAC;gBACP,UAAU;YACZ,CAAC;QACH,CAAC;QAED,OAAO,CAAC,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC7C,WAAW,CAAC,OAAO,CAAC,CAAC;QACrB,OAAO,CAAC,GAAG,CAAC,eAAe,QAAQ,CAAC,MAAM,cAAc,GAAG,gBAAgB,QAAQ,UAAU,CAAC,CAAC;QAC/F,OAAO;IACT,CAAC;IAED,wBAAwB;IACxB,MAAM,OAAO,GAAG,WAAW,EAAE,CAAC;IAE9B,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3C,OAAO,CAAC,GAAG,CAAC,0EAA0E,CAAC,CAAC;QACxF,OAAO;IACT,CAAC;IAED,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAChD,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CACT,yBAAyB,OAAO,CAAC,KAAK,CAAC,MAAM,iBAAiB,OAAO,CAAC,OAAO,CAAC,MAAM,0CAA0C,CAC/H,CAAC;QAEF,MAAM,UAAU,GAAG,IAAI,GAAG,EAAyB,CAAC;QACpD,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YAC9C,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACb,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACnC,CAAC;QAED,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,UAAU,EAAE,CAAC;YACtC,OAAO,CAAC,GAAG,CAAC,YAAY,GAAG,KAAK,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;YAClD,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;gBAClC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YACnC,CAAC;YACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;gBAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB,KAAK,CAAC,MAAM,GAAG,CAAC,OAAO,CAAC,CAAC;QAChF,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,kBAAkB,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC5D,OAAO,CAAC,GAAG,CAAC,qBAAqB,OAAO,CAAC,SAAS,IAAI,CAAC,CAAC;IAC1D,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding contest — gamified challenge mode where developers compete
|
|
3
|
+
* to fix the most findings in a codebase within a time window.
|
|
4
|
+
*
|
|
5
|
+
* Results stored locally in `.judges-contests/`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function runFindingContest(argv: string[]): void;
|
|
8
|
+
//# sourceMappingURL=finding-contest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"finding-contest.d.ts","sourceRoot":"","sources":["../../src/commands/finding-contest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAyEH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAuKtD"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding contest — gamified challenge mode where developers compete
|
|
3
|
+
* to fix the most findings in a codebase within a time window.
|
|
4
|
+
*
|
|
5
|
+
* Results stored locally in `.judges-contests/`.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
// ─── Storage ────────────────────────────────────────────────────────────────
|
|
10
|
+
const DATA_DIR = ".judges-contests";
|
|
11
|
+
function ensureDir() {
|
|
12
|
+
if (!existsSync(DATA_DIR))
|
|
13
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
function loadContest(id) {
|
|
16
|
+
const file = join(DATA_DIR, `${id}.json`);
|
|
17
|
+
if (!existsSync(file))
|
|
18
|
+
return null;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(file, "utf-8"));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function saveContest(contest) {
|
|
27
|
+
ensureDir();
|
|
28
|
+
writeFileSync(join(DATA_DIR, `${contest.id}.json`), JSON.stringify(contest, null, 2));
|
|
29
|
+
}
|
|
30
|
+
function getActiveContest() {
|
|
31
|
+
ensureDir();
|
|
32
|
+
const indexFile = join(DATA_DIR, "active.json");
|
|
33
|
+
if (!existsSync(indexFile))
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
const data = JSON.parse(readFileSync(indexFile, "utf-8"));
|
|
37
|
+
return loadContest(data.id);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function setActiveContest(id) {
|
|
44
|
+
ensureDir();
|
|
45
|
+
const indexFile = join(DATA_DIR, "active.json");
|
|
46
|
+
if (id)
|
|
47
|
+
writeFileSync(indexFile, JSON.stringify({ id }));
|
|
48
|
+
else if (existsSync(indexFile))
|
|
49
|
+
writeFileSync(indexFile, "{}");
|
|
50
|
+
}
|
|
51
|
+
// ─── Scoring ────────────────────────────────────────────────────────────────
|
|
52
|
+
const SEVERITY_POINTS = { critical: 10, high: 5, medium: 3, low: 1 };
|
|
53
|
+
// ─── CLI ────────────────────────────────────────────────────────────────────
|
|
54
|
+
export function runFindingContest(argv) {
|
|
55
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
56
|
+
console.log(`
|
|
57
|
+
judges finding-contest — Gamified fix challenge
|
|
58
|
+
|
|
59
|
+
Usage:
|
|
60
|
+
judges finding-contest --start --duration 60 --findings 25
|
|
61
|
+
judges finding-contest --fix --participant "alice" --rule SEC-001 --severity high
|
|
62
|
+
judges finding-contest --leaderboard
|
|
63
|
+
judges finding-contest --end
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
--start Start a new contest
|
|
67
|
+
--duration <min> Contest duration in minutes (default: 60)
|
|
68
|
+
--findings <n> Initial finding count (default: 20)
|
|
69
|
+
--fix Record a fix
|
|
70
|
+
--participant <name> Participant name
|
|
71
|
+
--rule <id> Rule ID that was fixed
|
|
72
|
+
--severity <level> Severity of the fixed finding
|
|
73
|
+
--leaderboard Show current leaderboard
|
|
74
|
+
--end End active contest and show final results
|
|
75
|
+
--format json JSON output
|
|
76
|
+
--help, -h Show this help
|
|
77
|
+
`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const format = argv.find((_a, i) => argv[i - 1] === "--format") || "text";
|
|
81
|
+
const isStart = argv.includes("--start");
|
|
82
|
+
const isFix = argv.includes("--fix");
|
|
83
|
+
const isLeaderboard = argv.includes("--leaderboard");
|
|
84
|
+
const isEnd = argv.includes("--end");
|
|
85
|
+
if (isStart) {
|
|
86
|
+
const duration = parseInt(argv.find((_a, i) => argv[i - 1] === "--duration") || "60");
|
|
87
|
+
const findings = parseInt(argv.find((_a, i) => argv[i - 1] === "--findings") || "20");
|
|
88
|
+
const now = new Date();
|
|
89
|
+
const contest = {
|
|
90
|
+
id: `contest-${Date.now()}`,
|
|
91
|
+
durationMinutes: duration,
|
|
92
|
+
startedAt: now.toISOString(),
|
|
93
|
+
endsAt: new Date(now.getTime() + duration * 60000).toISOString(),
|
|
94
|
+
status: "active",
|
|
95
|
+
initialFindings: findings,
|
|
96
|
+
entries: [],
|
|
97
|
+
};
|
|
98
|
+
saveContest(contest);
|
|
99
|
+
setActiveContest(contest.id);
|
|
100
|
+
console.log(`\n 🏁 Contest Started!`);
|
|
101
|
+
console.log(` ──────────────────────────`);
|
|
102
|
+
console.log(` ID: ${contest.id}`);
|
|
103
|
+
console.log(` Duration: ${duration} minutes`);
|
|
104
|
+
console.log(` Findings to fix: ${findings}`);
|
|
105
|
+
console.log(` Ends at: ${contest.endsAt}`);
|
|
106
|
+
console.log(`\n Use: judges finding-contest --fix --participant "name" --rule RULE-ID --severity level\n`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (isFix) {
|
|
110
|
+
const contest = getActiveContest();
|
|
111
|
+
if (!contest || contest.status !== "active") {
|
|
112
|
+
console.error(" No active contest. Use --start to begin one.");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Check if contest has expired
|
|
116
|
+
if (new Date() > new Date(contest.endsAt)) {
|
|
117
|
+
contest.status = "completed";
|
|
118
|
+
saveContest(contest);
|
|
119
|
+
setActiveContest(null);
|
|
120
|
+
console.log(" ⏰ Contest has ended! Use --leaderboard to see results.");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const participant = argv.find((_a, i) => argv[i - 1] === "--participant") || "";
|
|
124
|
+
const ruleId = argv.find((_a, i) => argv[i - 1] === "--rule") || "unknown";
|
|
125
|
+
const severity = argv.find((_a, i) => argv[i - 1] === "--severity") || "medium";
|
|
126
|
+
if (!participant) {
|
|
127
|
+
console.error(" --participant is required");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
let entry = contest.entries.find((e) => e.participant === participant);
|
|
131
|
+
if (!entry) {
|
|
132
|
+
entry = { participant, fixCount: 0, sevPoints: 0, startedAt: new Date().toISOString(), fixes: [] };
|
|
133
|
+
contest.entries.push(entry);
|
|
134
|
+
}
|
|
135
|
+
const points = SEVERITY_POINTS[severity] || 3;
|
|
136
|
+
entry.fixCount++;
|
|
137
|
+
entry.sevPoints += points;
|
|
138
|
+
entry.fixes.push({ ruleId, severity, fixedAt: new Date().toISOString() });
|
|
139
|
+
saveContest(contest);
|
|
140
|
+
console.log(` ✅ ${participant} fixed ${ruleId} [${severity}] — +${points} points (total: ${entry.sevPoints})`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (isLeaderboard || isEnd) {
|
|
144
|
+
const contest = getActiveContest() ||
|
|
145
|
+
(() => {
|
|
146
|
+
// Try to find latest completed contest
|
|
147
|
+
ensureDir();
|
|
148
|
+
return null;
|
|
149
|
+
})();
|
|
150
|
+
if (!contest) {
|
|
151
|
+
console.error(" No contest found. Use --start to begin one.");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (isEnd && contest.status === "active") {
|
|
155
|
+
contest.status = "completed";
|
|
156
|
+
saveContest(contest);
|
|
157
|
+
setActiveContest(null);
|
|
158
|
+
}
|
|
159
|
+
const sorted = [...contest.entries].sort((a, b) => b.sevPoints - a.sevPoints);
|
|
160
|
+
const totalFixed = sorted.reduce((s, e) => s + e.fixCount, 0);
|
|
161
|
+
if (format === "json") {
|
|
162
|
+
console.log(JSON.stringify({
|
|
163
|
+
contest: { id: contest.id, status: contest.status, duration: contest.durationMinutes },
|
|
164
|
+
leaderboard: sorted,
|
|
165
|
+
totalFixed,
|
|
166
|
+
timestamp: new Date().toISOString(),
|
|
167
|
+
}, null, 2));
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const statusIcon = contest.status === "active" ? "🟢 ACTIVE" : "🏁 COMPLETED";
|
|
171
|
+
console.log(`\n Finding Contest ${statusIcon}\n ──────────────────────────`);
|
|
172
|
+
console.log(` ID: ${contest.id} | Duration: ${contest.durationMinutes}m | Findings: ${contest.initialFindings} → ${contest.initialFindings - totalFixed}\n`);
|
|
173
|
+
if (sorted.length === 0) {
|
|
174
|
+
console.log(" No fixes recorded yet!");
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
178
|
+
const e = sorted[i];
|
|
179
|
+
const medal = i === 0 ? "🥇" : i === 1 ? "🥈" : i === 2 ? "🥉" : " ";
|
|
180
|
+
console.log(` ${medal} ${e.participant.padEnd(20)} ${String(e.sevPoints).padStart(5)} pts | ${e.fixCount} fixes`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (contest.status === "active") {
|
|
184
|
+
const remaining = Math.max(0, Math.floor((new Date(contest.endsAt).getTime() - Date.now()) / 60000));
|
|
185
|
+
console.log(`\n ⏱️ ${remaining} minutes remaining`);
|
|
186
|
+
}
|
|
187
|
+
console.log("");
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
console.log(" Use --start, --fix, --leaderboard, or --end. See --help for details.");
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=finding-contest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"finding-contest.js","sourceRoot":"","sources":["../../src/commands/finding-contest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Habit tracker — track recurring finding patterns per developer or
|
|
3
|
+
* AI model and surface personalized improvement suggestions.
|
|
4
|
+
*
|
|
5
|
+
* Data stored locally in `.judges-habits/`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function runHabitTracker(argv: string[]): void;
|
|
8
|
+
//# sourceMappingURL=habit-tracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"habit-tracker.d.ts","sourceRoot":"","sources":["../../src/commands/habit-tracker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAsGH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAoIpD"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Habit tracker — track recurring finding patterns per developer or
|
|
3
|
+
* AI model and surface personalized improvement suggestions.
|
|
4
|
+
*
|
|
5
|
+
* Data stored locally in `.judges-habits/`.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
// ─── Storage ────────────────────────────────────────────────────────────────
|
|
10
|
+
const DATA_DIR = ".judges-habits";
|
|
11
|
+
function ensureDir() {
|
|
12
|
+
if (!existsSync(DATA_DIR))
|
|
13
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
function loadProfile(author) {
|
|
16
|
+
const file = join(DATA_DIR, `${author}.json`);
|
|
17
|
+
if (!existsSync(file))
|
|
18
|
+
return null;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(file, "utf-8"));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function saveProfile(profile) {
|
|
27
|
+
ensureDir();
|
|
28
|
+
writeFileSync(join(DATA_DIR, `${profile.author}.json`), JSON.stringify(profile, null, 2));
|
|
29
|
+
}
|
|
30
|
+
// ─── Learning Resources ────────────────────────────────────────────────────
|
|
31
|
+
const CATEGORY_RESOURCES = {
|
|
32
|
+
"sql-injection": [
|
|
33
|
+
"Use parameterized queries",
|
|
34
|
+
"Review OWASP SQL Injection Prevention Cheat Sheet",
|
|
35
|
+
"Enable SQL linting in CI",
|
|
36
|
+
],
|
|
37
|
+
xss: [
|
|
38
|
+
"Sanitize all user input before rendering",
|
|
39
|
+
"Use Content Security Policy headers",
|
|
40
|
+
"Review OWASP XSS Prevention Cheat Sheet",
|
|
41
|
+
],
|
|
42
|
+
"hardcoded-secret": [
|
|
43
|
+
"Use environment variables for secrets",
|
|
44
|
+
"Set up a secrets manager",
|
|
45
|
+
"Add secret scanning to pre-commit hooks",
|
|
46
|
+
],
|
|
47
|
+
"empty-catch": [
|
|
48
|
+
"Always log or re-throw caught exceptions",
|
|
49
|
+
"Use error monitoring (Sentry, etc.)",
|
|
50
|
+
"Define error handling strategy per module",
|
|
51
|
+
],
|
|
52
|
+
"missing-auth": [
|
|
53
|
+
"Apply auth middleware to all routes",
|
|
54
|
+
"Review OWASP Authentication Cheat Sheet",
|
|
55
|
+
"Implement role-based access control",
|
|
56
|
+
],
|
|
57
|
+
"insecure-crypto": [
|
|
58
|
+
"Use crypto.randomUUID() for tokens",
|
|
59
|
+
"Use bcrypt/scrypt for password hashing",
|
|
60
|
+
"Never use MD5 or SHA1 for security",
|
|
61
|
+
],
|
|
62
|
+
"error-handling": [
|
|
63
|
+
"Define a consistent error hierarchy",
|
|
64
|
+
"Use Result types or Either monads",
|
|
65
|
+
"Log errors with correlation IDs",
|
|
66
|
+
],
|
|
67
|
+
performance: ["Profile before optimizing", "Use pagination for list endpoints", "Cache frequently accessed data"],
|
|
68
|
+
"code-quality": [
|
|
69
|
+
"Follow single responsibility principle",
|
|
70
|
+
"Reduce cyclomatic complexity",
|
|
71
|
+
"Write meaningful test names",
|
|
72
|
+
],
|
|
73
|
+
documentation: ["Keep JSDoc in sync with code", "Document public API contracts", "Add examples to complex functions"],
|
|
74
|
+
};
|
|
75
|
+
function getResources(category) {
|
|
76
|
+
const key = Object.keys(CATEGORY_RESOURCES).find((k) => category.toLowerCase().includes(k));
|
|
77
|
+
return key
|
|
78
|
+
? CATEGORY_RESOURCES[key]
|
|
79
|
+
: ["Review team coding standards", "Add targeted linting rules", "Consider pair programming for this area"];
|
|
80
|
+
}
|
|
81
|
+
// ─── CLI ────────────────────────────────────────────────────────────────────
|
|
82
|
+
export function runHabitTracker(argv) {
|
|
83
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
84
|
+
console.log(`
|
|
85
|
+
judges habit-tracker — Track recurring finding patterns
|
|
86
|
+
|
|
87
|
+
Usage:
|
|
88
|
+
judges habit-tracker --record --author "alice" --category "sql-injection"
|
|
89
|
+
judges habit-tracker --record --author "copilot" --category "empty-catch" --count 3
|
|
90
|
+
judges habit-tracker --show --author "alice"
|
|
91
|
+
judges habit-tracker --digest --author "alice"
|
|
92
|
+
|
|
93
|
+
Options:
|
|
94
|
+
--record Record finding occurrence
|
|
95
|
+
--author <name> Developer or AI model name
|
|
96
|
+
--category <name> Finding category (e.g., sql-injection, xss, empty-catch)
|
|
97
|
+
--count <n> Number of occurrences (default: 1)
|
|
98
|
+
--show Show author's habit profile
|
|
99
|
+
--digest Show improvement digest with resources
|
|
100
|
+
--format json JSON output
|
|
101
|
+
--help, -h Show this help
|
|
102
|
+
`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const format = argv.find((_a, i) => argv[i - 1] === "--format") || "text";
|
|
106
|
+
const isRecord = argv.includes("--record");
|
|
107
|
+
const isDigest = argv.includes("--digest");
|
|
108
|
+
const authorName = argv.find((_a, i) => argv[i - 1] === "--author") || "";
|
|
109
|
+
const category = argv.find((_a, i) => argv[i - 1] === "--category") || "";
|
|
110
|
+
const count = parseInt(argv.find((_a, i) => argv[i - 1] === "--count") || "1");
|
|
111
|
+
if (!authorName) {
|
|
112
|
+
console.error(" --author is required");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (isRecord) {
|
|
116
|
+
if (!category) {
|
|
117
|
+
console.error(" --category is required for --record");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const profile = loadProfile(authorName) || {
|
|
121
|
+
author: authorName,
|
|
122
|
+
entries: [],
|
|
123
|
+
totalFindings: 0,
|
|
124
|
+
topCategory: "",
|
|
125
|
+
lastUpdated: "",
|
|
126
|
+
};
|
|
127
|
+
let entry = profile.entries.find((e) => e.category === category);
|
|
128
|
+
if (!entry) {
|
|
129
|
+
entry = { category, count: 0, lastSeen: "", trend: "stable", resources: getResources(category) };
|
|
130
|
+
profile.entries.push(entry);
|
|
131
|
+
}
|
|
132
|
+
const prevCount = entry.count;
|
|
133
|
+
entry.count += count;
|
|
134
|
+
entry.lastSeen = new Date().toISOString();
|
|
135
|
+
entry.trend = entry.count > prevCount * 1.5 ? "rising" : entry.count < prevCount * 0.7 ? "declining" : "stable";
|
|
136
|
+
profile.totalFindings += count;
|
|
137
|
+
profile.topCategory = profile.entries.sort((a, b) => b.count - a.count)[0]?.category || "";
|
|
138
|
+
profile.lastUpdated = new Date().toISOString();
|
|
139
|
+
saveProfile(profile);
|
|
140
|
+
console.log(` ✅ Recorded ${count}x ${category} for ${authorName} (total: ${entry.count})`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (isDigest) {
|
|
144
|
+
const profile = loadProfile(authorName);
|
|
145
|
+
if (!profile || profile.entries.length === 0) {
|
|
146
|
+
console.log(` No data for ${authorName}. Use --record to add findings.`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const sorted = [...profile.entries].sort((a, b) => b.count - a.count);
|
|
150
|
+
const top3 = sorted.slice(0, 3);
|
|
151
|
+
if (format === "json") {
|
|
152
|
+
console.log(JSON.stringify({
|
|
153
|
+
author: authorName,
|
|
154
|
+
digest: top3,
|
|
155
|
+
totalFindings: profile.totalFindings,
|
|
156
|
+
timestamp: new Date().toISOString(),
|
|
157
|
+
}, null, 2));
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
console.log(`\n Weekly Improvement Digest for ${authorName}\n ──────────────────────────`);
|
|
161
|
+
console.log(` Total findings: ${profile.totalFindings} across ${profile.entries.length} categories\n`);
|
|
162
|
+
for (const entry of top3) {
|
|
163
|
+
const trendIcon = entry.trend === "rising" ? "📈" : entry.trend === "declining" ? "📉" : "➡️";
|
|
164
|
+
console.log(` ${trendIcon} ${entry.category} — ${entry.count} occurrences (${entry.trend})`);
|
|
165
|
+
console.log(" Suggested improvements:");
|
|
166
|
+
for (const r of entry.resources) {
|
|
167
|
+
console.log(` • ${r}`);
|
|
168
|
+
}
|
|
169
|
+
console.log("");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Default: show profile
|
|
175
|
+
const profile = loadProfile(authorName);
|
|
176
|
+
if (!profile || profile.entries.length === 0) {
|
|
177
|
+
console.log(` No data for ${authorName}. Use --record to add findings.`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (format === "json") {
|
|
181
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.log(`\n Habit Profile: ${authorName}\n ──────────────────────────`);
|
|
185
|
+
console.log(` Total: ${profile.totalFindings} | Categories: ${profile.entries.length} | Top: ${profile.topCategory}\n`);
|
|
186
|
+
const sorted = [...profile.entries].sort((a, b) => b.count - a.count);
|
|
187
|
+
for (const entry of sorted) {
|
|
188
|
+
const bar = "█".repeat(Math.min(20, Math.floor((entry.count / Math.max(1, profile.totalFindings)) * 40)));
|
|
189
|
+
const trendIcon = entry.trend === "rising" ? "↑" : entry.trend === "declining" ? "↓" : "→";
|
|
190
|
+
console.log(` ${entry.category.padEnd(25)} ${String(entry.count).padStart(4)} ${bar} ${trendIcon}`);
|
|
191
|
+
}
|
|
192
|
+
console.log("");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=habit-tracker.js.map
|