@agjs/tsforge 0.1.14 → 0.1.16
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/package.json +2 -1
- package/scripts/analyze-malformed.ts +264 -0
- package/scripts/analyze-runs.ts +279 -0
- package/scripts/benchmark-catalog.ts +387 -0
- package/scripts/browser-check.ts +87 -0
- package/scripts/build-rule-docs.ts +122 -0
- package/scripts/build-rules-md.ts +129 -0
- package/scripts/cli-metrics.ts +203 -0
- package/scripts/coverage-check.ts +33 -0
- package/scripts/edit-benchmark.ts +314 -0
- package/scripts/eval-create.ts +48 -0
- package/scripts/eval-spec.ts +47 -0
- package/scripts/eval-sum.ts +79 -0
- package/scripts/gen-tests.ts +140 -0
- package/scripts/headless-build.ts +292 -0
- package/scripts/interactive-eval.ts +172 -0
- package/scripts/rejudge.ts +135 -0
- package/scripts/run-eval-todo.ts +59 -0
- package/scripts/smoke.ts +18 -0
- package/scripts/stub-check.ts +44 -0
- package/scripts/sweep-report.ts +76 -0
- package/scripts/sweep.ts +389 -0
- package/src/cli.ts +39 -1
- package/src/inference/inference.types.ts +20 -0
- package/src/inference/openai-compatible.ts +11 -34
- package/src/inference/request.ts +148 -0
- package/src/models-config.ts +13 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// INTERACTIVE-PATH eval: drives Session.send() exactly the way the REPL does —
|
|
3
|
+
// agent-decides scaffold_web, plan mode, the malformed-call retry — the paths
|
|
4
|
+
// headless-build (which pre-scaffolds and calls buildStaged) never exercises.
|
|
5
|
+
// This net exists because a missing `scaffoldWeb: true` config flag killed the
|
|
6
|
+
// agent-decides path for weeks and no eval noticed.
|
|
7
|
+
//
|
|
8
|
+
// bun packages/core/scripts/interactive-eval.ts # default todo-app prompt
|
|
9
|
+
// bun packages/core/scripts/interactive-eval.ts "build a notes app" # custom prompt
|
|
10
|
+
// ... --plan exercise plan mode (plan → approve → implement)
|
|
11
|
+
// ... --force forced-tools arm (tool_choice required + yield_status)
|
|
12
|
+
//
|
|
13
|
+
// Each run gets evals/runs/<timestamp>-interactive[-flags]/ with agent.log,
|
|
14
|
+
// the JSONL event log, and a verdict.json for the analyzer.
|
|
15
|
+
import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
17
|
+
import {
|
|
18
|
+
Session,
|
|
19
|
+
PLAN_APPROVED_NOTE,
|
|
20
|
+
LOOP_LIMITS,
|
|
21
|
+
type ISendResult,
|
|
22
|
+
} from "../src/loop";
|
|
23
|
+
import { renderEvent, type Reporter } from "../src";
|
|
24
|
+
import {
|
|
25
|
+
buildGate,
|
|
26
|
+
buildWebGate,
|
|
27
|
+
buildWebFix,
|
|
28
|
+
buildWebTscCheck,
|
|
29
|
+
scaffoldWeb,
|
|
30
|
+
installWebDeps,
|
|
31
|
+
webGuidance,
|
|
32
|
+
} from "../src/detect-gate";
|
|
33
|
+
import { resolveActiveModel } from "../src/models-config";
|
|
34
|
+
import { OpenAICompatibleProvider } from "../src/inference";
|
|
35
|
+
import { providerConfig } from "../src/cli";
|
|
36
|
+
|
|
37
|
+
interface IVerdict {
|
|
38
|
+
status: string;
|
|
39
|
+
turns: number;
|
|
40
|
+
scaffolded: boolean;
|
|
41
|
+
planUsed: boolean;
|
|
42
|
+
forceTools: boolean;
|
|
43
|
+
malformedNudges: number;
|
|
44
|
+
salvaged: number;
|
|
45
|
+
toolRejections: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeReporter(
|
|
49
|
+
logFile: string,
|
|
50
|
+
agentLog: string,
|
|
51
|
+
verdict: IVerdict
|
|
52
|
+
): Reporter {
|
|
53
|
+
return (event) => {
|
|
54
|
+
process.stdout.write(renderEvent(event, { color: true }));
|
|
55
|
+
appendFileSync(agentLog, renderEvent(event, { color: false }));
|
|
56
|
+
appendFileSync(logFile, `${JSON.stringify({ t: Date.now(), ...event })}\n`);
|
|
57
|
+
|
|
58
|
+
// Live fingerprints for the verdict (same markers analyze-malformed reads).
|
|
59
|
+
if (event.kind === "tool") {
|
|
60
|
+
if (event.message.includes("malformed tool-call text")) {
|
|
61
|
+
verdict.malformedNudges += 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (event.message.startsWith("tool_rejected:")) {
|
|
65
|
+
verdict.toolRejections += 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
event.message.includes("recovered") &&
|
|
70
|
+
event.message.includes("malformed")
|
|
71
|
+
) {
|
|
72
|
+
verdict.salvaged += 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function main(): Promise<number> {
|
|
79
|
+
const argv = process.argv.slice(2);
|
|
80
|
+
const plan = argv.includes("--plan");
|
|
81
|
+
const force = argv.includes("--force");
|
|
82
|
+
const prompt =
|
|
83
|
+
argv.find((a) => !a.startsWith("--")) ?? "build a small todo web app";
|
|
84
|
+
|
|
85
|
+
const stamp = new Date()
|
|
86
|
+
.toISOString()
|
|
87
|
+
.replace(/[:T]/g, "-")
|
|
88
|
+
.replace(/\..+$/, "");
|
|
89
|
+
const label = `interactive${plan ? "-plan" : ""}${force ? "-force" : ""}`;
|
|
90
|
+
const dir = resolve(join("evals", "runs", `${stamp}-${label}`));
|
|
91
|
+
|
|
92
|
+
mkdirSync(dir, { recursive: true });
|
|
93
|
+
|
|
94
|
+
const verdict: IVerdict = {
|
|
95
|
+
status: "unknown",
|
|
96
|
+
turns: 0,
|
|
97
|
+
scaffolded: false,
|
|
98
|
+
planUsed: plan,
|
|
99
|
+
forceTools: force,
|
|
100
|
+
malformedNudges: 0,
|
|
101
|
+
salvaged: 0,
|
|
102
|
+
toolRejections: 0,
|
|
103
|
+
};
|
|
104
|
+
const report = makeReporter(
|
|
105
|
+
join(dir, "events.jsonl"),
|
|
106
|
+
join(dir, "agent.log"),
|
|
107
|
+
verdict
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const active = await resolveActiveModel();
|
|
111
|
+
const provider = new OpenAICompatibleProvider(providerConfig(active.entry));
|
|
112
|
+
const gate = await buildGate(dir);
|
|
113
|
+
|
|
114
|
+
process.stdout.write(
|
|
115
|
+
`interactive eval → ${dir}\n model ${active.name} · gate ${gate.label} · ${plan ? "plan-mode" : "direct"}${force ? " · forced-tools" : ""}\n\n`
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// The REPL's exact config shape — scaffoldWeb:true is the agent-decides flag.
|
|
119
|
+
const session = await Session.create({
|
|
120
|
+
provider,
|
|
121
|
+
cwd: dir,
|
|
122
|
+
files: ["**/*"],
|
|
123
|
+
accept: gate.command,
|
|
124
|
+
report,
|
|
125
|
+
scaffoldWeb: true,
|
|
126
|
+
enableThinking: false,
|
|
127
|
+
...(force ? { forceTools: true } : {}),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// The REPL's configureWeb, inlined: scaffold + deps + switch to the web gate.
|
|
131
|
+
session.setSetupWeb(async (framework) => {
|
|
132
|
+
const fw = framework === "vanilla" ? "vanilla" : "react";
|
|
133
|
+
|
|
134
|
+
await scaffoldWeb(dir, fw);
|
|
135
|
+
await installWebDeps(dir);
|
|
136
|
+
session.setGate(buildWebGate(fw).command);
|
|
137
|
+
session.setFix(buildWebFix(fw));
|
|
138
|
+
session.setIncrementalCheck(buildWebTscCheck());
|
|
139
|
+
session.guide(webGuidance(fw));
|
|
140
|
+
session.setMaxTurns(LOOP_LIMITS.webMaxTurns);
|
|
141
|
+
verdict.scaffolded = true;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
let result: ISendResult;
|
|
145
|
+
|
|
146
|
+
if (plan) {
|
|
147
|
+
session.setPlanMode(true);
|
|
148
|
+
result = await session.send(prompt);
|
|
149
|
+
|
|
150
|
+
const planned = session.messages.at(-1)?.content ?? "";
|
|
151
|
+
|
|
152
|
+
process.stdout.write(
|
|
153
|
+
`\n— plan turn: ${result.status}; ## Plan present: ${String(/^##\s*plan\b/im.test(planned))} —\n`
|
|
154
|
+
);
|
|
155
|
+
session.setPlanMode(false);
|
|
156
|
+
result = await session.send(PLAN_APPROVED_NOTE);
|
|
157
|
+
} else {
|
|
158
|
+
result = await session.send(prompt);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
verdict.status = result.status;
|
|
162
|
+
verdict.turns = result.turns;
|
|
163
|
+
writeFileSync(join(dir, "verdict.json"), JSON.stringify(verdict, null, 2));
|
|
164
|
+
|
|
165
|
+
process.stdout.write(
|
|
166
|
+
`\nverdict: ${JSON.stringify(verdict)}\n run dir: ${dir}\n`
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return result.status === "done" && verdict.scaffolded ? 0 : 1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
process.exit(await main());
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Re-score EXISTING run outputs with a judge — no re-implementation. Its purpose
|
|
2
|
+
// is to resolve the OKR crux: is a local self-judged Q4 the code's true quality,
|
|
3
|
+
// or just the local model lowballing itself? Point it at a FLAGSHIP judge to find
|
|
4
|
+
// out (offline MEASURE only — never a runtime dependency):
|
|
5
|
+
//
|
|
6
|
+
// TSFORGE_JUDGE_URL=https://… TSFORGE_JUDGE_MODEL=deepseek-… TSFORGE_JUDGE_KEY=… \
|
|
7
|
+
// bun run packages/core/scripts/rejudge.ts money 5
|
|
8
|
+
//
|
|
9
|
+
// Without the JUDGE_* env it falls back to the local model (self-judge) and just
|
|
10
|
+
// reproduces the existing scores — so it warns when no flagship judge is set.
|
|
11
|
+
import { readdir } from "node:fs/promises";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { parseSpec } from "../src/spec";
|
|
14
|
+
import { judge } from "../src/eval";
|
|
15
|
+
import { OpenAICompatibleProvider, PROVIDER_DEFAULTS } from "../src/inference";
|
|
16
|
+
import { isRecord } from "../src/lib/guards";
|
|
17
|
+
|
|
18
|
+
const evalsRoot = join(import.meta.dir, "..", "..", "..", "evals");
|
|
19
|
+
|
|
20
|
+
const flagshipSet =
|
|
21
|
+
process.env.TSFORGE_JUDGE_URL !== undefined ||
|
|
22
|
+
process.env.TSFORGE_JUDGE_MODEL !== undefined;
|
|
23
|
+
|
|
24
|
+
const provider = new OpenAICompatibleProvider({
|
|
25
|
+
baseUrl:
|
|
26
|
+
process.env.TSFORGE_JUDGE_URL ??
|
|
27
|
+
process.env.TSFORGE_BASE_URL ??
|
|
28
|
+
PROVIDER_DEFAULTS.baseUrl,
|
|
29
|
+
model:
|
|
30
|
+
process.env.TSFORGE_JUDGE_MODEL ??
|
|
31
|
+
process.env.TSFORGE_MODEL ??
|
|
32
|
+
PROVIDER_DEFAULTS.model,
|
|
33
|
+
apiKey: process.env.TSFORGE_JUDGE_KEY ?? process.env.TSFORGE_API_KEY,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
async function resolveDirs(): Promise<string[]> {
|
|
37
|
+
const args = process.argv.slice(2);
|
|
38
|
+
|
|
39
|
+
if (args.length === 2 && /^\d+$/.test(args[1] ?? "")) {
|
|
40
|
+
const prefix = args[0] ?? "";
|
|
41
|
+
const count = Number(args[1]);
|
|
42
|
+
const all = await readdir(evalsRoot, { withFileTypes: true });
|
|
43
|
+
const dirs = all
|
|
44
|
+
.filter((d) => d.isDirectory() && d.name.startsWith(prefix))
|
|
45
|
+
.map((d) => d.name)
|
|
46
|
+
.sort();
|
|
47
|
+
|
|
48
|
+
return dirs.slice(-count).map((name) => join(evalsRoot, name));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return args.map((a) => (a.startsWith("/") ? a : join(evalsRoot, a)));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The local (self-judge) overall score recorded at run time, if any. */
|
|
55
|
+
async function localScore(dir: string): Promise<number | undefined> {
|
|
56
|
+
const file = Bun.file(join(dir, "result.json"));
|
|
57
|
+
|
|
58
|
+
if (!(await file.exists())) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const data: unknown = JSON.parse(await file.text());
|
|
63
|
+
|
|
64
|
+
return isRecord(data) && typeof data.quality === "number"
|
|
65
|
+
? data.quality
|
|
66
|
+
: undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function specIn(dir: string): Promise<string | undefined> {
|
|
70
|
+
const entries = await readdir(dir);
|
|
71
|
+
|
|
72
|
+
return entries.find((e) => e.endsWith(".spec.md"));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const dirs = await resolveDirs();
|
|
76
|
+
|
|
77
|
+
if (!flagshipSet) {
|
|
78
|
+
process.stdout.write(
|
|
79
|
+
"⚠ No TSFORGE_JUDGE_URL/MODEL set — judging with the LOCAL model (self-judge). " +
|
|
80
|
+
"Set a flagship judge to measure true quality.\n"
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
process.stdout.write(
|
|
85
|
+
`\n=== re-judge (${dirs.length} runs, judge=${flagshipSet ? "flagship" : "LOCAL self-judge"}) ===\n\n`
|
|
86
|
+
);
|
|
87
|
+
process.stdout.write("localQ judgeOverall corr design read run\n");
|
|
88
|
+
|
|
89
|
+
for (const dir of dirs) {
|
|
90
|
+
const specName = await specIn(dir);
|
|
91
|
+
|
|
92
|
+
if (specName === undefined) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const spec = parseSpec(await Bun.file(join(dir, specName)).text());
|
|
97
|
+
const task = spec.tasks[0];
|
|
98
|
+
|
|
99
|
+
if (task === undefined) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const parts: string[] = [];
|
|
104
|
+
|
|
105
|
+
for (const f of task.files) {
|
|
106
|
+
const file = Bun.file(join(dir, f));
|
|
107
|
+
|
|
108
|
+
if (await file.exists()) {
|
|
109
|
+
parts.push(`// ${f}\n${await file.text()}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (parts.length === 0) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const score = await judge(provider, {
|
|
118
|
+
goal: spec.title,
|
|
119
|
+
criteria: await Bun.file(join(dir, specName)).text(),
|
|
120
|
+
code: parts.join("\n\n"),
|
|
121
|
+
});
|
|
122
|
+
const local = await localScore(dir);
|
|
123
|
+
const runId = dir.split("/").slice(-1)[0] ?? dir;
|
|
124
|
+
|
|
125
|
+
process.stdout.write(
|
|
126
|
+
[
|
|
127
|
+
(local === undefined ? "-" : String(local)).padStart(6),
|
|
128
|
+
String(score.overall).padStart(13),
|
|
129
|
+
String(score.correctness).padStart(6),
|
|
130
|
+
String(score.design).padStart(8),
|
|
131
|
+
String(score.readability).padStart(6),
|
|
132
|
+
` ${runId}`,
|
|
133
|
+
].join("") + "\n"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Drive the live model through the Todo spec into a fresh, uuid'd run folder
|
|
2
|
+
// under /evals (gitignored). Streams to your terminal AND to run.log.
|
|
3
|
+
// Run: bun run packages/core/scripts/run-eval-todo.ts
|
|
4
|
+
import { mkdir } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { parseSpec } from "../src/spec";
|
|
7
|
+
import { runSpec } from "../src/loop";
|
|
8
|
+
import { OpenAICompatibleProvider, PROVIDER_DEFAULTS } from "../src/inference";
|
|
9
|
+
import { renderEvent } from "../src/render";
|
|
10
|
+
|
|
11
|
+
const evalsRoot = join(import.meta.dir, "..", "..", "..", "evals");
|
|
12
|
+
const seedDir = join(evalsRoot, "todo");
|
|
13
|
+
|
|
14
|
+
// One isolated folder per run. Kept at evals/<id> depth so the spec's
|
|
15
|
+
// ../../node_modules paths still resolve.
|
|
16
|
+
const runId = `todo-${crypto.randomUUID().slice(0, 8)}`;
|
|
17
|
+
const runDir = join(evalsRoot, runId);
|
|
18
|
+
|
|
19
|
+
await mkdir(runDir, { recursive: true });
|
|
20
|
+
|
|
21
|
+
// Copy the seed (spec, tests, constitution) into the run folder.
|
|
22
|
+
for (const file of [
|
|
23
|
+
"todo.spec.md",
|
|
24
|
+
"todo.test.ts",
|
|
25
|
+
"tsconfig.json",
|
|
26
|
+
"eslint.config.js",
|
|
27
|
+
]) {
|
|
28
|
+
await Bun.write(join(runDir, file), Bun.file(join(seedDir, file)));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const spec = parseSpec(await Bun.file(join(runDir, "todo.spec.md")).text());
|
|
32
|
+
|
|
33
|
+
const provider = new OpenAICompatibleProvider({
|
|
34
|
+
baseUrl: process.env.TSFORGE_BASE_URL ?? PROVIDER_DEFAULTS.baseUrl,
|
|
35
|
+
model: process.env.TSFORGE_MODEL ?? PROVIDER_DEFAULTS.model,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Tee to the terminal (colored) AND run.log (plain).
|
|
39
|
+
const log = Bun.file(join(runDir, "run.log")).writer();
|
|
40
|
+
|
|
41
|
+
const out = (colored: string, plain: string): void => {
|
|
42
|
+
process.stdout.write(colored);
|
|
43
|
+
void log.write(plain);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
out(`run ${runId}\n`, `run ${runId}\n`);
|
|
47
|
+
|
|
48
|
+
const result = await runSpec(spec, runDir, provider, {
|
|
49
|
+
onEvent: (e) => {
|
|
50
|
+
out(renderEvent(e, { color: true }), renderEvent(e, { color: false }));
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const summary = `\n\nspec "${spec.id}" -> ${result.status}\ntasks: ${JSON.stringify(result.results)}\n`;
|
|
55
|
+
|
|
56
|
+
out(summary, summary);
|
|
57
|
+
await log.end();
|
|
58
|
+
|
|
59
|
+
console.log(`\nFull log + output in: ${runDir}`);
|
package/scripts/smoke.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Manual smoke check against a live local model. Not part of the test suite.
|
|
2
|
+
// Run: bun run packages/core/scripts/smoke.ts
|
|
3
|
+
import { OpenAICompatibleProvider, PROVIDER_DEFAULTS } from "../src/inference";
|
|
4
|
+
|
|
5
|
+
const p = new OpenAICompatibleProvider({
|
|
6
|
+
baseUrl: process.env.TSFORGE_BASE_URL ?? PROVIDER_DEFAULTS.baseUrl,
|
|
7
|
+
model: process.env.TSFORGE_MODEL ?? PROVIDER_DEFAULTS.model,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const r = await p.complete(
|
|
11
|
+
[{ role: "user", content: "Reply with exactly: pong" }],
|
|
12
|
+
{
|
|
13
|
+
temperature: 0,
|
|
14
|
+
}
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
console.log("content:", JSON.stringify(r.content));
|
|
18
|
+
console.log("toolCalls:", JSON.stringify(r.toolCalls));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Gate step: fail if any route is STILL an unfilled scaffold_routes stub. The
|
|
3
|
+
// scaffold lays down placeholder route files (marked `data-tsforge-stub`); the
|
|
4
|
+
// model must REPLACE each with the real page. An unfilled stub renders an empty
|
|
5
|
+
// placeholder — which the coverage gate (file exists) and the render smoke (root
|
|
6
|
+
// not blank) both miss — so without this an app of empty routes goes green.
|
|
7
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
const MARKER = "data-tsforge-stub";
|
|
11
|
+
const dir = process.argv[2] ?? ".";
|
|
12
|
+
const routesDir = join(dir, "src", "routes");
|
|
13
|
+
|
|
14
|
+
let files: string[];
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
files = (await readdir(routesDir)).filter((f) => f.endsWith(".tsx"));
|
|
18
|
+
} catch {
|
|
19
|
+
// No routes dir (non-web build) → nothing to enforce.
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const stubs: string[] = [];
|
|
24
|
+
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const content = await readFile(join(routesDir, file), "utf8");
|
|
27
|
+
|
|
28
|
+
if (content.includes(MARKER)) {
|
|
29
|
+
stubs.push(file);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (stubs.length > 0) {
|
|
34
|
+
process.stdout.write(
|
|
35
|
+
`stub-check: ${String(stubs.length)} route(s) are still empty scaffold STUBS — ` +
|
|
36
|
+
`replace each placeholder component with the REAL page (its list/detail/form, ` +
|
|
37
|
+
`using the SDK + your components). The app is NOT done while these render a ` +
|
|
38
|
+
`placeholder. Unfilled: ${stubs.join(", ")}\n`
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
process.stdout.write("stub-check: no unfilled route stubs\n");
|
|
44
|
+
process.exit(0);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Turn a sweep's `sweep-<seed>-<ts>.json` into a statistical Markdown report:
|
|
2
|
+
// per-variant pass rate with a 95% Wilson interval and, when TSFORGE_BASELINE
|
|
3
|
+
// names a variant, a two-proportion significance test against it.
|
|
4
|
+
//
|
|
5
|
+
// Run: bun run packages/core/scripts/sweep-report.ts [sweep.json]
|
|
6
|
+
// (no arg → the newest sweep-*.json under evals/runs)
|
|
7
|
+
// TSFORGE_BASELINE="ttsr=off,hashline=off temp=0" # optional baseline label
|
|
8
|
+
import { readdir } from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { isRecord } from "../src/lib/guards";
|
|
11
|
+
import {
|
|
12
|
+
buildSweepReport,
|
|
13
|
+
renderSweepReportMarkdown,
|
|
14
|
+
type IRunRecord,
|
|
15
|
+
} from "../src/eval";
|
|
16
|
+
|
|
17
|
+
const RUNS_DIR = "evals/runs";
|
|
18
|
+
|
|
19
|
+
async function newestSweep(): Promise<string> {
|
|
20
|
+
const names = (await readdir(RUNS_DIR))
|
|
21
|
+
.filter((n) => n.startsWith("sweep-") && n.endsWith(".json"))
|
|
22
|
+
.sort();
|
|
23
|
+
const latest = names.at(-1);
|
|
24
|
+
|
|
25
|
+
if (latest === undefined) {
|
|
26
|
+
throw new Error(`no sweep-*.json found in ${RUNS_DIR}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return join(RUNS_DIR, latest);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toRecords(value: unknown): IRunRecord[] {
|
|
33
|
+
if (!isRecord(value) || !Array.isArray(value.records)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const out: IRunRecord[] = [];
|
|
38
|
+
|
|
39
|
+
for (const r of value.records) {
|
|
40
|
+
if (
|
|
41
|
+
isRecord(r) &&
|
|
42
|
+
typeof r.label === "string" &&
|
|
43
|
+
typeof r.passed === "boolean" &&
|
|
44
|
+
typeof r.cycles === "number" &&
|
|
45
|
+
typeof r.ms === "number"
|
|
46
|
+
) {
|
|
47
|
+
out.push({
|
|
48
|
+
label: r.label,
|
|
49
|
+
passed: r.passed,
|
|
50
|
+
cycles: r.cycles,
|
|
51
|
+
ms: r.ms,
|
|
52
|
+
...(typeof r.quality === "number" ? { quality: r.quality } : {}),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const fileArg = process.argv[2];
|
|
61
|
+
const baseline = process.env.TSFORGE_BASELINE;
|
|
62
|
+
const path = fileArg ?? (await newestSweep());
|
|
63
|
+
const parsed: unknown = JSON.parse(await Bun.file(path).text());
|
|
64
|
+
const records = toRecords(parsed);
|
|
65
|
+
|
|
66
|
+
if (records.length === 0) {
|
|
67
|
+
process.stderr.write(`no run records in ${path}\n`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const report = buildSweepReport(records, baseline);
|
|
72
|
+
const markdown = renderSweepReportMarkdown(report);
|
|
73
|
+
const outPath = path.replace(/\.json$/, ".report.md");
|
|
74
|
+
|
|
75
|
+
await Bun.write(outPath, `${markdown}\n`);
|
|
76
|
+
process.stdout.write(`${markdown}\n\nwrote ${outPath}\n`);
|