bun-sticky 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -120
- package/index.js +8 -0
- package/package.json +7 -27
- package/.claude/commands/score.md +0 -12
- package/.claude/commands/test.md +0 -15
- package/.github/FUNDING.yml +0 -5
- package/.github/workflows/ci.yml +0 -26
- package/.github/workflows/release.yml +0 -36
- package/CLAUDE.md +0 -98
- package/LICENSE +0 -21
- package/PUBLISH-PROTOCOL.md +0 -63
- package/index.ts +0 -349
- package/lib/parser.ts +0 -75
- package/lib/scorer.ts +0 -237
- package/lib/tier.ts +0 -46
- package/project.faf +0 -23
- package/tests/__snapshots__/sticky.test.ts.snap +0 -114
- package/tests/sticky.test.ts +0 -1321
- package/tests/wjttc.test.ts +0 -966
package/index.ts
DELETED
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* 🥐 Bun Sticky - Fastest bun under the sum.
|
|
4
|
-
*
|
|
5
|
-
* Built the Anthropic way:
|
|
6
|
-
* - First principles
|
|
7
|
-
* - Zero dependencies
|
|
8
|
-
* - Native Bun APIs
|
|
9
|
-
* - TypeScript native
|
|
10
|
-
*
|
|
11
|
-
* Wolfejam slot-based scoring (NOT Elon weights).
|
|
12
|
-
* For Claude Codesters.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { parseYaml, getNestedValue } from "./lib/parser.ts";
|
|
16
|
-
import { calculateScore, FafScore } from "./lib/scorer.ts";
|
|
17
|
-
import { getTier } from "./lib/tier.ts";
|
|
18
|
-
|
|
19
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
20
|
-
// CONSTANTS
|
|
21
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
22
|
-
|
|
23
|
-
const VERSION = "1.0.3";
|
|
24
|
-
|
|
25
|
-
// Standard colors only (B/W version - color reserved for ZIG poster child)
|
|
26
|
-
const GREEN = "\x1b[32m";
|
|
27
|
-
const YELLOW = "\x1b[33m";
|
|
28
|
-
const RED = "\x1b[31m";
|
|
29
|
-
const BOLD = "\x1b[1m";
|
|
30
|
-
const DIM = "\x1b[2m";
|
|
31
|
-
const RESET = "\x1b[0m";
|
|
32
|
-
|
|
33
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
34
|
-
// ASCII ART BANNER
|
|
35
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
36
|
-
|
|
37
|
-
const BANNER = `
|
|
38
|
-
────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
▄▄ ▄▀▀▀ ▀█▀ █ ▄▀▀ █▄▀ █ █
|
|
41
|
-
████ ▀▀█▄ █ █ █ █▀▄ █
|
|
42
|
-
██████ ▄▄▄▀ █ █ ▀▀▀ █ █ █
|
|
43
|
-
████████
|
|
44
|
-
████████ █▀▄ █ █ █▀▄
|
|
45
|
-
██████ ██▀ █ █ █ █
|
|
46
|
-
████ █▄▀ ▀▄▀ █ █
|
|
47
|
-
▀▀
|
|
48
|
-
|
|
49
|
-
🥐 Bun Sticky v${VERSION} .faf CLI
|
|
50
|
-
Fastest bun under the sum.
|
|
51
|
-
|
|
52
|
-
────────────────────────────────────────────────
|
|
53
|
-
`;
|
|
54
|
-
|
|
55
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
56
|
-
// COMMANDS
|
|
57
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
58
|
-
|
|
59
|
-
async function cmdScore(): Promise<void> {
|
|
60
|
-
const file = Bun.file("project.faf");
|
|
61
|
-
|
|
62
|
-
if (!(await file.exists())) {
|
|
63
|
-
console.log(`${RED}No project.faf found${RESET}`);
|
|
64
|
-
console.log(`${DIM}Run: bun-sticky init <name>${RESET}`);
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const content = await file.text();
|
|
69
|
-
const faf = parseYaml(content);
|
|
70
|
-
const result = calculateScore(faf);
|
|
71
|
-
const tier = getTier(result.score);
|
|
72
|
-
|
|
73
|
-
console.log(BANNER);
|
|
74
|
-
|
|
75
|
-
// Project name & type
|
|
76
|
-
const name = (getNestedValue(faf, "project.name") as string) || "Unknown";
|
|
77
|
-
console.log(` Project: ${BOLD}${name}${RESET}`);
|
|
78
|
-
console.log(` Type: ${DIM}${result.projectType}${RESET}`);
|
|
79
|
-
console.log();
|
|
80
|
-
|
|
81
|
-
// Section breakdown (only show applicable sections)
|
|
82
|
-
const { sections } = result;
|
|
83
|
-
|
|
84
|
-
if (sections.project.total > 0) {
|
|
85
|
-
console.log(` ${DIM}Project${RESET} ${formatBar(sections.project.percentage)} ${sections.project.filled}/${sections.project.total}`);
|
|
86
|
-
}
|
|
87
|
-
if (sections.frontend.total > 0) {
|
|
88
|
-
console.log(` ${DIM}Frontend${RESET} ${formatBar(sections.frontend.percentage)} ${sections.frontend.filled}/${sections.frontend.total}`);
|
|
89
|
-
}
|
|
90
|
-
if (sections.backend.total > 0) {
|
|
91
|
-
console.log(` ${DIM}Backend${RESET} ${formatBar(sections.backend.percentage)} ${sections.backend.filled}/${sections.backend.total}`);
|
|
92
|
-
}
|
|
93
|
-
if (sections.universal.total > 0) {
|
|
94
|
-
console.log(` ${DIM}Universal${RESET} ${formatBar(sections.universal.percentage)} ${sections.universal.filled}/${sections.universal.total}`);
|
|
95
|
-
}
|
|
96
|
-
if (sections.human.total > 0) {
|
|
97
|
-
console.log(` ${DIM}Human${RESET} ${formatBar(sections.human.percentage)} ${sections.human.filled}/${sections.human.total}`);
|
|
98
|
-
}
|
|
99
|
-
console.log();
|
|
100
|
-
|
|
101
|
-
// Total
|
|
102
|
-
console.log(` ${tier.color}${tier.emoji} ${BOLD}${result.score}%${RESET} ${tier.color}${tier.name}${RESET}`);
|
|
103
|
-
console.log(` ${DIM}Filled: ${result.filled}/${result.total} slots${RESET}`);
|
|
104
|
-
console.log();
|
|
105
|
-
|
|
106
|
-
// Show missing slots with copy-paste YAML
|
|
107
|
-
if (result.missing.length > 0) {
|
|
108
|
-
console.log(` ${YELLOW}Add to project.faf:${RESET}`);
|
|
109
|
-
console.log();
|
|
110
|
-
|
|
111
|
-
// Group by section
|
|
112
|
-
const projectMissing = result.missing.filter(s => s.startsWith("project."));
|
|
113
|
-
const humanMissing = result.missing.filter(s => s.startsWith("human_context."));
|
|
114
|
-
const stackMissing = result.missing.filter(s => s.startsWith("stack."));
|
|
115
|
-
|
|
116
|
-
if (projectMissing.length > 0) {
|
|
117
|
-
console.log(` ${DIM}project:${RESET}`);
|
|
118
|
-
for (const slot of projectMissing) {
|
|
119
|
-
const field = slot.replace("project.", "");
|
|
120
|
-
console.log(` ${DIM}${field}:${RESET} "${getHint(field)}"`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (stackMissing.length > 0) {
|
|
125
|
-
console.log(` ${DIM}stack:${RESET}`);
|
|
126
|
-
for (const slot of stackMissing) {
|
|
127
|
-
const field = slot.replace("stack.", "");
|
|
128
|
-
console.log(` ${DIM}${field}:${RESET} "${getHint(field)}"`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (humanMissing.length > 0) {
|
|
133
|
-
console.log(` ${DIM}human_context:${RESET}`);
|
|
134
|
-
for (const slot of humanMissing) {
|
|
135
|
-
const field = slot.replace("human_context.", "");
|
|
136
|
-
console.log(` ${DIM}${field}:${RESET} "${getHint(field)}"`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
console.log();
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function getHint(field: string): string {
|
|
144
|
-
const hints: Record<string, string> = {
|
|
145
|
-
// Project
|
|
146
|
-
name: "Project name",
|
|
147
|
-
goal: "What problem does this solve?",
|
|
148
|
-
main_language: "TypeScript",
|
|
149
|
-
// Human context - questions that make you think
|
|
150
|
-
who: "Who is it for?",
|
|
151
|
-
what: "What does it do?",
|
|
152
|
-
why: "Why does it exist?",
|
|
153
|
-
where: "Where is it deployed/used?",
|
|
154
|
-
when: "When is it due/released?",
|
|
155
|
-
how: "How is it built?",
|
|
156
|
-
// Stack
|
|
157
|
-
frontend: "React",
|
|
158
|
-
css_framework: "Tailwind",
|
|
159
|
-
ui_library: "shadcn",
|
|
160
|
-
state_management: "zustand",
|
|
161
|
-
backend: "Node.js",
|
|
162
|
-
api_type: "REST",
|
|
163
|
-
runtime: "Bun",
|
|
164
|
-
database: "PostgreSQL",
|
|
165
|
-
connection: "prisma",
|
|
166
|
-
hosting: "Vercel",
|
|
167
|
-
build: "vite",
|
|
168
|
-
cicd: "GitHub Actions",
|
|
169
|
-
};
|
|
170
|
-
return hints[field] || "";
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function formatBar(percent: number): string {
|
|
174
|
-
const width = 12;
|
|
175
|
-
const filled = Math.round((percent / 100) * width);
|
|
176
|
-
const empty = width - filled;
|
|
177
|
-
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
178
|
-
|
|
179
|
-
if (percent >= 85) return `${GREEN}${bar}${RESET}`;
|
|
180
|
-
if (percent >= 70) return `${GREEN}${bar}${RESET}`;
|
|
181
|
-
if (percent >= 55) return `${YELLOW}${bar}${RESET}`;
|
|
182
|
-
return `${RED}${bar}${RESET}`;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async function cmdInit(name: string): Promise<void> {
|
|
186
|
-
const file = Bun.file("project.faf");
|
|
187
|
-
|
|
188
|
-
if (await file.exists()) {
|
|
189
|
-
console.log(`${YELLOW}project.faf already exists${RESET}`);
|
|
190
|
-
process.exit(1);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const template = `# ${name} - Project DNA
|
|
194
|
-
# Generated by Bun Sticky
|
|
195
|
-
|
|
196
|
-
faf_version: 2.5.0
|
|
197
|
-
|
|
198
|
-
project:
|
|
199
|
-
name: ${name}
|
|
200
|
-
goal: Define your project goal here
|
|
201
|
-
main_language: TypeScript
|
|
202
|
-
type: cli
|
|
203
|
-
version: 0.1.0
|
|
204
|
-
|
|
205
|
-
human_context:
|
|
206
|
-
who: Your target users
|
|
207
|
-
what: What this project does
|
|
208
|
-
why: Why it exists
|
|
209
|
-
where: Where it runs
|
|
210
|
-
when: When to use it
|
|
211
|
-
how: How to get started
|
|
212
|
-
|
|
213
|
-
stack:
|
|
214
|
-
runtime: Bun
|
|
215
|
-
build: bun build
|
|
216
|
-
`;
|
|
217
|
-
|
|
218
|
-
await Bun.write("project.faf", template);
|
|
219
|
-
console.log(BANNER);
|
|
220
|
-
console.log(` ${GREEN}Created${RESET} project.faf`);
|
|
221
|
-
console.log(` ${DIM}Run: bun-sticky score${RESET}`);
|
|
222
|
-
console.log();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
async function cmdSync(): Promise<void> {
|
|
226
|
-
const fafFile = Bun.file("project.faf");
|
|
227
|
-
|
|
228
|
-
if (!(await fafFile.exists())) {
|
|
229
|
-
console.log(`${RED}No project.faf found${RESET}`);
|
|
230
|
-
process.exit(1);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const content = await fafFile.text();
|
|
234
|
-
const faf = parseYaml(content);
|
|
235
|
-
const name = (getNestedValue(faf, "project.name") as string) || "Project";
|
|
236
|
-
const goal = (getNestedValue(faf, "project.goal") as string) || "";
|
|
237
|
-
const result = calculateScore(faf);
|
|
238
|
-
const tier = getTier(result.score);
|
|
239
|
-
|
|
240
|
-
const scoreBadge = `**${tier.emoji} ${result.score}% ${tier.name}** - ${result.filled}/${result.total} slots filled`;
|
|
241
|
-
const claudeFile = Bun.file("CLAUDE.md");
|
|
242
|
-
|
|
243
|
-
if (await claudeFile.exists()) {
|
|
244
|
-
// Update existing CLAUDE.md - preserve content, update/insert score badge
|
|
245
|
-
let existing = await claudeFile.text();
|
|
246
|
-
const badgePattern = /^\*\*[🏆🥇🥈🥉🟢🟡🔴⚪🍊]\s*\d+%.*\*\*.*slots filled$/mu;
|
|
247
|
-
|
|
248
|
-
if (badgePattern.test(existing)) {
|
|
249
|
-
// Replace existing badge
|
|
250
|
-
existing = existing.replace(badgePattern, scoreBadge);
|
|
251
|
-
} else {
|
|
252
|
-
// Insert badge after first paragraph (after title + description)
|
|
253
|
-
const lines = existing.split("\n");
|
|
254
|
-
let insertIndex = 1;
|
|
255
|
-
|
|
256
|
-
// Find first empty line after title
|
|
257
|
-
for (let i = 1; i < lines.length; i++) {
|
|
258
|
-
if (lines[i].trim() === "") {
|
|
259
|
-
insertIndex = i + 1;
|
|
260
|
-
// Skip consecutive empty lines
|
|
261
|
-
while (insertIndex < lines.length && lines[insertIndex].trim() === "") {
|
|
262
|
-
insertIndex++;
|
|
263
|
-
}
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Check if next line is already a heading, insert before it
|
|
269
|
-
if (lines[insertIndex]?.startsWith("#")) {
|
|
270
|
-
lines.splice(insertIndex, 0, scoreBadge, "");
|
|
271
|
-
} else {
|
|
272
|
-
lines.splice(insertIndex, 0, "", scoreBadge);
|
|
273
|
-
}
|
|
274
|
-
existing = lines.join("\n");
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
await Bun.write("CLAUDE.md", existing);
|
|
278
|
-
} else {
|
|
279
|
-
// Create new minimal CLAUDE.md
|
|
280
|
-
const claudeMd = `# ${name}
|
|
281
|
-
|
|
282
|
-
${goal}
|
|
283
|
-
|
|
284
|
-
${scoreBadge}
|
|
285
|
-
|
|
286
|
-
---
|
|
287
|
-
*Synced by Bun Sticky*
|
|
288
|
-
`;
|
|
289
|
-
await Bun.write("CLAUDE.md", claudeMd);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
console.log(BANNER);
|
|
293
|
-
console.log(` ${GREEN}Synced${RESET} project.faf → CLAUDE.md`);
|
|
294
|
-
console.log();
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function cmdHelp(): void {
|
|
298
|
-
console.log(BANNER);
|
|
299
|
-
console.log(` ${BOLD}Commands${RESET}`);
|
|
300
|
-
console.log();
|
|
301
|
-
console.log(` score Show FAF score + tier`);
|
|
302
|
-
console.log(` init <n> Create project.faf`);
|
|
303
|
-
console.log(` sync Sync to CLAUDE.md`);
|
|
304
|
-
console.log(` version Show version`);
|
|
305
|
-
console.log(` help Show this help`);
|
|
306
|
-
console.log();
|
|
307
|
-
console.log(` ${DIM}Zero dependencies. Pure Bun.${RESET}`);
|
|
308
|
-
console.log(` ${DIM}Wolfejam slot-based scoring.${RESET}`);
|
|
309
|
-
console.log();
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function cmdVersion(): void {
|
|
313
|
-
console.log(`bun-sticky v${VERSION}`);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
317
|
-
// MAIN
|
|
318
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
319
|
-
|
|
320
|
-
const args = process.argv.slice(2);
|
|
321
|
-
const cmd = args[0] || "help";
|
|
322
|
-
|
|
323
|
-
switch (cmd) {
|
|
324
|
-
case "score":
|
|
325
|
-
await cmdScore();
|
|
326
|
-
break;
|
|
327
|
-
case "init":
|
|
328
|
-
const name = args[1];
|
|
329
|
-
if (!name) {
|
|
330
|
-
console.log(`${RED}Usage: bun-sticky init <name>${RESET}`);
|
|
331
|
-
process.exit(1);
|
|
332
|
-
}
|
|
333
|
-
await cmdInit(name);
|
|
334
|
-
break;
|
|
335
|
-
case "sync":
|
|
336
|
-
await cmdSync();
|
|
337
|
-
break;
|
|
338
|
-
case "version":
|
|
339
|
-
case "-v":
|
|
340
|
-
case "--version":
|
|
341
|
-
cmdVersion();
|
|
342
|
-
break;
|
|
343
|
-
case "help":
|
|
344
|
-
case "-h":
|
|
345
|
-
case "--help":
|
|
346
|
-
default:
|
|
347
|
-
cmdHelp();
|
|
348
|
-
break;
|
|
349
|
-
}
|
package/lib/parser.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal YAML Parser for .faf files
|
|
3
|
-
*
|
|
4
|
-
* Zero dependencies. Pure Bun.
|
|
5
|
-
* Handles: scalars, nested objects, arrays
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export function parseYaml(content: string): Record<string, unknown> {
|
|
9
|
-
const result: Record<string, unknown> = {};
|
|
10
|
-
const lines = content.split("\n");
|
|
11
|
-
const stack: { indent: number; obj: Record<string, unknown> }[] = [
|
|
12
|
-
{ indent: -1, obj: result },
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
for (const line of lines) {
|
|
16
|
-
// Skip comments and empty lines
|
|
17
|
-
if (line.trim().startsWith("#") || line.trim() === "") continue;
|
|
18
|
-
|
|
19
|
-
const indent = line.search(/\S/);
|
|
20
|
-
const trimmed = line.trim();
|
|
21
|
-
|
|
22
|
-
// Handle key: value pairs
|
|
23
|
-
const colonIndex = trimmed.indexOf(":");
|
|
24
|
-
if (colonIndex === -1) continue;
|
|
25
|
-
|
|
26
|
-
const key = trimmed.slice(0, colonIndex).trim();
|
|
27
|
-
const value = trimmed.slice(colonIndex + 1).trim();
|
|
28
|
-
|
|
29
|
-
// Pop stack to find parent
|
|
30
|
-
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
31
|
-
stack.pop();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const parent = stack[stack.length - 1].obj;
|
|
35
|
-
|
|
36
|
-
if (value === "" || value === "|" || value === ">") {
|
|
37
|
-
// Nested object or multiline
|
|
38
|
-
const newObj: Record<string, unknown> = {};
|
|
39
|
-
parent[key] = newObj;
|
|
40
|
-
stack.push({ indent, obj: newObj });
|
|
41
|
-
} else if (value.startsWith("[") && value.endsWith("]")) {
|
|
42
|
-
// Inline array
|
|
43
|
-
parent[key] = value
|
|
44
|
-
.slice(1, -1)
|
|
45
|
-
.split(",")
|
|
46
|
-
.map((s) => s.trim().replace(/^["']|["']$/g, ""));
|
|
47
|
-
} else if (value.startsWith("- ")) {
|
|
48
|
-
// Array item (simple case)
|
|
49
|
-
parent[key] = [value.slice(2).trim()];
|
|
50
|
-
} else {
|
|
51
|
-
// Scalar value
|
|
52
|
-
parent[key] = value.replace(/^["']|["']$/g, "");
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return result;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
60
|
-
const parts = path.split(".");
|
|
61
|
-
let current: unknown = obj;
|
|
62
|
-
for (const part of parts) {
|
|
63
|
-
if (current === null || current === undefined) return undefined;
|
|
64
|
-
if (typeof current !== "object") return undefined;
|
|
65
|
-
current = (current as Record<string, unknown>)[part];
|
|
66
|
-
}
|
|
67
|
-
return current;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function hasValue(obj: Record<string, unknown>, path: string): boolean {
|
|
71
|
-
const value = getNestedValue(obj, path);
|
|
72
|
-
if (value === undefined || value === null) return false;
|
|
73
|
-
if (typeof value === "string" && value.trim() === "") return false;
|
|
74
|
-
return true;
|
|
75
|
-
}
|
package/lib/scorer.ts
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 🥐 Bun Sticky Scorer - Wolfejam Slot-Based Scoring
|
|
3
|
-
*
|
|
4
|
-
* Score = (Filled slots / Applicable slots) × 100
|
|
5
|
-
*
|
|
6
|
-
* 21 total slots, type-aware scoring.
|
|
7
|
-
* Zero dependencies. Pure Bun.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { hasValue, getNestedValue } from "./parser.ts";
|
|
11
|
-
|
|
12
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
13
|
-
// SLOT DEFINITIONS - 21 Slots Total
|
|
14
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
15
|
-
|
|
16
|
-
export const SLOTS = {
|
|
17
|
-
// Project slots (3)
|
|
18
|
-
project: [
|
|
19
|
-
"project.name",
|
|
20
|
-
"project.goal",
|
|
21
|
-
"project.main_language",
|
|
22
|
-
],
|
|
23
|
-
// Frontend slots (4)
|
|
24
|
-
frontend: [
|
|
25
|
-
"stack.frontend",
|
|
26
|
-
"stack.css_framework",
|
|
27
|
-
"stack.ui_library",
|
|
28
|
-
"stack.state_management",
|
|
29
|
-
],
|
|
30
|
-
// Backend slots (5)
|
|
31
|
-
backend: [
|
|
32
|
-
"stack.backend",
|
|
33
|
-
"stack.api_type",
|
|
34
|
-
"stack.runtime",
|
|
35
|
-
"stack.database",
|
|
36
|
-
"stack.connection",
|
|
37
|
-
],
|
|
38
|
-
// Universal slots (3)
|
|
39
|
-
universal: [
|
|
40
|
-
"stack.hosting",
|
|
41
|
-
"stack.build",
|
|
42
|
-
"stack.cicd",
|
|
43
|
-
],
|
|
44
|
-
// Human context slots (6)
|
|
45
|
-
human: [
|
|
46
|
-
"human_context.who",
|
|
47
|
-
"human_context.what",
|
|
48
|
-
"human_context.why",
|
|
49
|
-
"human_context.where",
|
|
50
|
-
"human_context.when",
|
|
51
|
-
"human_context.how",
|
|
52
|
-
],
|
|
53
|
-
} as const;
|
|
54
|
-
|
|
55
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
56
|
-
// TYPE DEFINITIONS - Which slots apply to each type
|
|
57
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
58
|
-
|
|
59
|
-
export type ProjectType =
|
|
60
|
-
| "cli"
|
|
61
|
-
| "library"
|
|
62
|
-
| "api"
|
|
63
|
-
| "webapp"
|
|
64
|
-
| "fullstack"
|
|
65
|
-
| "mobile"
|
|
66
|
-
| "unknown";
|
|
67
|
-
|
|
68
|
-
export const TYPE_CATEGORIES: Record<ProjectType, (keyof typeof SLOTS)[]> = {
|
|
69
|
-
// CLI/Tool: 9 slots (project + human)
|
|
70
|
-
cli: ["project", "human"],
|
|
71
|
-
|
|
72
|
-
// Library/Package: 9 slots (project + human)
|
|
73
|
-
library: ["project", "human"],
|
|
74
|
-
|
|
75
|
-
// API/Backend: 17 slots (project + backend + universal + human)
|
|
76
|
-
api: ["project", "backend", "universal", "human"],
|
|
77
|
-
|
|
78
|
-
// Web App: 16 slots (project + frontend + universal + human)
|
|
79
|
-
webapp: ["project", "frontend", "universal", "human"],
|
|
80
|
-
|
|
81
|
-
// Fullstack: 21 slots (all)
|
|
82
|
-
fullstack: ["project", "frontend", "backend", "universal", "human"],
|
|
83
|
-
|
|
84
|
-
// Mobile: 9 slots (project + human) - simplified
|
|
85
|
-
mobile: ["project", "human"],
|
|
86
|
-
|
|
87
|
-
// Unknown: 9 slots (project + human) - safe default
|
|
88
|
-
unknown: ["project", "human"],
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
92
|
-
// SCORE INTERFACE
|
|
93
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
94
|
-
|
|
95
|
-
export interface SlotSection {
|
|
96
|
-
filled: number;
|
|
97
|
-
total: number;
|
|
98
|
-
percentage: number;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface FafScore {
|
|
102
|
-
projectType: ProjectType;
|
|
103
|
-
sections: {
|
|
104
|
-
project: SlotSection;
|
|
105
|
-
frontend: SlotSection;
|
|
106
|
-
backend: SlotSection;
|
|
107
|
-
universal: SlotSection;
|
|
108
|
-
human: SlotSection;
|
|
109
|
-
};
|
|
110
|
-
filled: number;
|
|
111
|
-
total: number;
|
|
112
|
-
score: number;
|
|
113
|
-
missing: string[];
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
117
|
-
// SCORING FUNCTIONS
|
|
118
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Detect project type from .faf content
|
|
122
|
-
*/
|
|
123
|
-
export function detectProjectType(faf: Record<string, unknown>): ProjectType {
|
|
124
|
-
const type = getNestedValue(faf, "project.type") as string;
|
|
125
|
-
|
|
126
|
-
if (type) {
|
|
127
|
-
const typeLower = type.toLowerCase();
|
|
128
|
-
if (typeLower.includes("cli")) return "cli";
|
|
129
|
-
if (typeLower.includes("lib") || typeLower.includes("package")) return "library";
|
|
130
|
-
if (typeLower.includes("api") || typeLower.includes("backend")) return "api";
|
|
131
|
-
if (typeLower.includes("web") || typeLower.includes("frontend")) return "webapp";
|
|
132
|
-
if (typeLower.includes("full")) return "fullstack";
|
|
133
|
-
if (typeLower.includes("mobile") || typeLower.includes("app")) return "mobile";
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Infer from stack
|
|
137
|
-
const hasFrontend = hasValue(faf, "stack.frontend");
|
|
138
|
-
const hasBackend = hasValue(faf, "stack.backend") || hasValue(faf, "stack.database");
|
|
139
|
-
|
|
140
|
-
if (hasFrontend && hasBackend) return "fullstack";
|
|
141
|
-
if (hasFrontend) return "webapp";
|
|
142
|
-
if (hasBackend) return "api";
|
|
143
|
-
|
|
144
|
-
return "unknown";
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Count filled slots in a section
|
|
149
|
-
*/
|
|
150
|
-
function countSection(
|
|
151
|
-
faf: Record<string, unknown>,
|
|
152
|
-
slots: readonly string[],
|
|
153
|
-
applies: boolean
|
|
154
|
-
): SlotSection {
|
|
155
|
-
if (!applies) {
|
|
156
|
-
return { filled: 0, total: 0, percentage: 0 };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
let filled = 0;
|
|
160
|
-
for (const slot of slots) {
|
|
161
|
-
if (hasValue(faf, slot)) filled++;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const total = slots.length;
|
|
165
|
-
const percentage = total > 0 ? Math.round((filled / total) * 100) : 0;
|
|
166
|
-
|
|
167
|
-
return { filled, total, percentage };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Calculate score using wolfejam slot-based system
|
|
172
|
-
* Score = (Filled slots / Applicable slots) × 100
|
|
173
|
-
*/
|
|
174
|
-
export function calculateScore(faf: Record<string, unknown>): FafScore {
|
|
175
|
-
const projectType = detectProjectType(faf);
|
|
176
|
-
const applicableCategories = TYPE_CATEGORIES[projectType];
|
|
177
|
-
|
|
178
|
-
// Count each section
|
|
179
|
-
const sections = {
|
|
180
|
-
project: countSection(
|
|
181
|
-
faf,
|
|
182
|
-
SLOTS.project,
|
|
183
|
-
applicableCategories.includes("project")
|
|
184
|
-
),
|
|
185
|
-
frontend: countSection(
|
|
186
|
-
faf,
|
|
187
|
-
SLOTS.frontend,
|
|
188
|
-
applicableCategories.includes("frontend")
|
|
189
|
-
),
|
|
190
|
-
backend: countSection(
|
|
191
|
-
faf,
|
|
192
|
-
SLOTS.backend,
|
|
193
|
-
applicableCategories.includes("backend")
|
|
194
|
-
),
|
|
195
|
-
universal: countSection(
|
|
196
|
-
faf,
|
|
197
|
-
SLOTS.universal,
|
|
198
|
-
applicableCategories.includes("universal")
|
|
199
|
-
),
|
|
200
|
-
human: countSection(
|
|
201
|
-
faf,
|
|
202
|
-
SLOTS.human,
|
|
203
|
-
applicableCategories.includes("human")
|
|
204
|
-
),
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
// Sum totals
|
|
208
|
-
let filled = 0;
|
|
209
|
-
let total = 0;
|
|
210
|
-
|
|
211
|
-
for (const section of Object.values(sections)) {
|
|
212
|
-
filled += section.filled;
|
|
213
|
-
total += section.total;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Calculate final score
|
|
217
|
-
const score = total > 0 ? Math.round((filled / total) * 100) : 0;
|
|
218
|
-
|
|
219
|
-
// Find missing slots
|
|
220
|
-
const missing: string[] = [];
|
|
221
|
-
for (const category of applicableCategories) {
|
|
222
|
-
for (const slot of SLOTS[category]) {
|
|
223
|
-
if (!hasValue(faf, slot)) {
|
|
224
|
-
missing.push(slot);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return {
|
|
230
|
-
projectType,
|
|
231
|
-
sections,
|
|
232
|
-
filled,
|
|
233
|
-
total,
|
|
234
|
-
score,
|
|
235
|
-
missing,
|
|
236
|
-
};
|
|
237
|
-
}
|
package/lib/tier.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tier System
|
|
3
|
-
*
|
|
4
|
-
* The medal hierarchy for FAF scores.
|
|
5
|
-
* Zero dependencies. Pure Bun.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface Tier {
|
|
9
|
-
emoji: string;
|
|
10
|
-
name: string;
|
|
11
|
-
color: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// ANSI colors
|
|
15
|
-
const YELLOW = "\x1b[33m";
|
|
16
|
-
const GREEN = "\x1b[32m";
|
|
17
|
-
const RED = "\x1b[31m";
|
|
18
|
-
const DIM = "\x1b[2m";
|
|
19
|
-
const ORANGE = "\x1b[38;5;208m";
|
|
20
|
-
const WHITE = "\x1b[37m";
|
|
21
|
-
|
|
22
|
-
export const TIERS: Tier[] = [
|
|
23
|
-
{ emoji: "🏆", name: "Trophy", color: YELLOW },
|
|
24
|
-
{ emoji: "🥇", name: "Gold", color: YELLOW },
|
|
25
|
-
{ emoji: "🥈", name: "Silver", color: WHITE },
|
|
26
|
-
{ emoji: "🥉", name: "Bronze", color: ORANGE },
|
|
27
|
-
{ emoji: "🟢", name: "Green", color: GREEN },
|
|
28
|
-
{ emoji: "🟡", name: "Yellow", color: YELLOW },
|
|
29
|
-
{ emoji: "🔴", name: "Red", color: RED },
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
export function getTier(score: number): Tier {
|
|
33
|
-
if (score >= 105) return { emoji: "🍊", name: "Big Orange", color: ORANGE };
|
|
34
|
-
if (score >= 100) return { emoji: "🏆", name: "Trophy", color: YELLOW };
|
|
35
|
-
if (score >= 99) return { emoji: "🥇", name: "Gold", color: YELLOW };
|
|
36
|
-
if (score >= 95) return { emoji: "🥈", name: "Silver", color: WHITE };
|
|
37
|
-
if (score >= 85) return { emoji: "🥉", name: "Bronze", color: ORANGE };
|
|
38
|
-
if (score >= 70) return { emoji: "🟢", name: "Green", color: GREEN };
|
|
39
|
-
if (score >= 55) return { emoji: "🟡", name: "Yellow", color: YELLOW };
|
|
40
|
-
if (score > 0) return { emoji: "🔴", name: "Red", color: RED };
|
|
41
|
-
return { emoji: "⚪", name: "Empty", color: DIM };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function isLaunchReady(score: number): boolean {
|
|
45
|
-
return score >= 85;
|
|
46
|
-
}
|