opencode-leetcode-realworld 0.0.0-stage → 0.2.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/README.md +209 -2
- package/package.json +33 -4
- package/src/guard.ts +61 -0
- package/src/index.ts +173 -0
- package/src/languages.ts +358 -0
- package/src/leetcode.ts +257 -0
- package/src/patterns.ts +141 -0
- package/src/prompt.ts +111 -0
- package/src/scaffold.ts +310 -0
- package/src/types.ts +92 -0
package/src/patterns.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { ScaffoldSpec } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deterministic fallback inspiration. Maps a normalized topic tag to plausible
|
|
5
|
+
* real-world domains that exercise the same underlying principle. The agent is
|
|
6
|
+
* free to invent better scenarios; this exists so a sensible seed is always
|
|
7
|
+
* available even with a weak model.
|
|
8
|
+
*/
|
|
9
|
+
export const DOMAIN_SEEDS: Record<string, string[]> = {
|
|
10
|
+
array: ["batch audit-log processor", "telemetry windowing service"],
|
|
11
|
+
string: ["log sanitizer", "template renderer"],
|
|
12
|
+
"hash-table": ["event deduplication pipeline", "feature-flag lookup cache"],
|
|
13
|
+
"two-pointers": ["order-book matching engine", "sorted log-stream merger"],
|
|
14
|
+
"sliding-window": ["API rate limiter", "rolling p95 latency tracker"],
|
|
15
|
+
"binary-search": ["price-threshold alert engine", "pagination cursor resolver"],
|
|
16
|
+
"binary-search-tree": ["in-memory index range scanner", "time-series retention policy"],
|
|
17
|
+
"heap-priority-queue": ["background job scheduler", "incident severity triage"],
|
|
18
|
+
"priority-queue": ["background job scheduler", "incident severity triage"],
|
|
19
|
+
"dynamic-programming": ["delivery route cost optimizer", "budget allocation planner"],
|
|
20
|
+
greedy: ["resource bin-packing scheduler", "ad-slot allocator"],
|
|
21
|
+
graph: ["microservice dependency resolver", "network topology mapper"],
|
|
22
|
+
"depth-first-search": ["permission inheritance resolver", "nested category walker"],
|
|
23
|
+
"breadth-first-search": ["service-mesh shortest-path router", "org-chart traversal"],
|
|
24
|
+
"union-find": ["account clustering for fraud rings", "network segment merger"],
|
|
25
|
+
trie: ["autocomplete search index", "routing prefix matcher"],
|
|
26
|
+
stack: ["config-rule expression evaluator", "undo/redo command stack"],
|
|
27
|
+
queue: ["async task pipeline", "event buffer"],
|
|
28
|
+
"monotonic-stack": ["stock-span style sidebar calculator", "histogram-based capacity planner"],
|
|
29
|
+
sorting: ["leaderboard ranker", "changelog assembly job"],
|
|
30
|
+
backtracking: ["deployment permutation explorer", "seat-assignment planner"],
|
|
31
|
+
"bit-manipulation": ["feature-flag bitmask service", "compact permission encoder"],
|
|
32
|
+
math: ["billing proration engine", "metrics aggregation service"],
|
|
33
|
+
design: ["in-memory key-value store", "rate-limited API gateway"],
|
|
34
|
+
"linked-list": ["LRU eviction chain", "streaming ring buffer"],
|
|
35
|
+
tree: ["filesystem index builder", "org hierarchy aggregator"],
|
|
36
|
+
"prefix-sum": ["rolling revenue aggregator", "subnet traffic accounting"],
|
|
37
|
+
"segment-tree": ["range-quota enforcement service", "metrics rollup store"],
|
|
38
|
+
database: ["analytics reconciliation job", "query result differ"],
|
|
39
|
+
simulation: ["tick-based match engine", "capacity planning simulator"],
|
|
40
|
+
"ordered-set": ["leaderboard with rank lookup", "scheduler keyed by priority"],
|
|
41
|
+
"topological-sort": ["build-order planner", "migration dependency sequencer"],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Tags that are too generic to drive a scenario on their own. */
|
|
45
|
+
const WEAK_TAGS = new Set(["algorithms", "database", "math"]);
|
|
46
|
+
|
|
47
|
+
function normalizeTag(tag: string): string {
|
|
48
|
+
return tag.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function domainSeedsFor(topics: string[]): string[] {
|
|
52
|
+
const seeds: string[] = [];
|
|
53
|
+
for (const topic of topics) {
|
|
54
|
+
const key = normalizeTag(topic);
|
|
55
|
+
const matches = DOMAIN_SEEDS[key];
|
|
56
|
+
if (matches && !WEAK_TAGS.has(key)) seeds.push(...matches);
|
|
57
|
+
}
|
|
58
|
+
if (seeds.length === 0) {
|
|
59
|
+
seeds.push("internal developer-tooling service", "operations workflow automation");
|
|
60
|
+
}
|
|
61
|
+
return Array.from(new Set(seeds));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function pickDomainSeed(topics: string[], random: () => number = Math.random): string {
|
|
65
|
+
const seeds = domainSeedsFor(topics);
|
|
66
|
+
return seeds[Math.floor(random() * seeds.length)] ?? seeds[0]!;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const GENERIC_BANNED_TERMS = [
|
|
70
|
+
"leetcode",
|
|
71
|
+
"leet code",
|
|
72
|
+
"geeksforgeeks",
|
|
73
|
+
"hackerrank",
|
|
74
|
+
"codewars",
|
|
75
|
+
"neetcode",
|
|
76
|
+
"blind 75",
|
|
77
|
+
"grind 75",
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
export interface LeakReport {
|
|
81
|
+
term: string;
|
|
82
|
+
field: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function textsFromSpec(spec: ScaffoldSpec): Array<[string, string]> {
|
|
86
|
+
const entries: Array<[string, string]> = [];
|
|
87
|
+
const push = (field: string, value: unknown) => {
|
|
88
|
+
if (typeof value === "string" && value.length > 0) entries.push([field, value]);
|
|
89
|
+
};
|
|
90
|
+
push("title", spec.title);
|
|
91
|
+
push("projectName", spec.projectName);
|
|
92
|
+
push("scenario", spec.scenario);
|
|
93
|
+
push("pattern", spec.pattern);
|
|
94
|
+
push("functionName", spec.functionName);
|
|
95
|
+
push("starterCode", spec.starterCode);
|
|
96
|
+
(spec.requirements ?? []).forEach((req, i) => push(`requirements[${i}]`, req));
|
|
97
|
+
(spec.edgeCases ?? []).forEach((edge, i) => push(`edgeCases[${i}]`, edge));
|
|
98
|
+
(spec.publicTests ?? []).forEach((test, i) => push(`publicTests[${i}].name`, test.name));
|
|
99
|
+
(spec.hiddenTests ?? []).forEach((test, i) => push(`hiddenTests[${i}].name`, test.name));
|
|
100
|
+
return entries;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Guards the core promise of the plugin: the assignment must not be the LeetCode
|
|
105
|
+
* problem in disguise. Scans every learner-visible string for the source problem
|
|
106
|
+
* and generic problem-site branding.
|
|
107
|
+
*/
|
|
108
|
+
export function findLeaks(spec: ScaffoldSpec): LeakReport[] {
|
|
109
|
+
const lowered = spec.sourceTitle?.toLowerCase().trim();
|
|
110
|
+
const sourceSlug = spec.sourceSlug?.toLowerCase().trim();
|
|
111
|
+
const reports: LeakReport[] = [];
|
|
112
|
+
|
|
113
|
+
for (const [field, value] of textsFromSpec(spec)) {
|
|
114
|
+
const haystack = value.toLowerCase();
|
|
115
|
+
for (const term of GENERIC_BANNED_TERMS) {
|
|
116
|
+
if (haystack.includes(term)) reports.push({ term, field });
|
|
117
|
+
}
|
|
118
|
+
if (lowered && lowered.length > 3 && haystack.includes(lowered)) {
|
|
119
|
+
reports.push({ term: `source title "${spec.sourceTitle}"`, field });
|
|
120
|
+
}
|
|
121
|
+
if (sourceSlug && sourceSlug.length > 2 && haystack.includes(sourceSlug)) {
|
|
122
|
+
reports.push({ term: `source slug "${spec.sourceSlug}"`, field });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const unique = new Map<string, LeakReport>();
|
|
127
|
+
for (const report of reports) unique.set(`${report.term}::${report.field}`, report);
|
|
128
|
+
return Array.from(unique.values());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function assertNoLeak(spec: ScaffoldSpec): void {
|
|
132
|
+
const leaks = findLeaks(spec);
|
|
133
|
+
if (leaks.length === 0) return;
|
|
134
|
+
const details = leaks.map((l) => ` - "${l.term}" in ${l.field}`).join("\n");
|
|
135
|
+
throw new Error(
|
|
136
|
+
"The scaffold spec leaks the original problem, which breaks the whole point of this plugin.\n" +
|
|
137
|
+
"Rewrite the flagged fields so the assignment stands on its own as real software work:\n" +
|
|
138
|
+
`${details}\n` +
|
|
139
|
+
"Never mention the problem title, its slug, or any coding-practice site.",
|
|
140
|
+
);
|
|
141
|
+
}
|
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export const PRACTICE_DESCRIPTION =
|
|
2
|
+
"Turn a LeetCode problem into a realistic software-engineering assignment (scenario, starter, tests) so you practice applying the underlying algorithm, not solving the puzzle.";
|
|
3
|
+
|
|
4
|
+
export const SUPPORTED_LANGUAGES = [
|
|
5
|
+
"TypeScript",
|
|
6
|
+
"JavaScript",
|
|
7
|
+
"Python",
|
|
8
|
+
"Ruby",
|
|
9
|
+
"PHP",
|
|
10
|
+
"Go",
|
|
11
|
+
"Rust",
|
|
12
|
+
"C#",
|
|
13
|
+
"Swift",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The `/practice` command prompt. It orchestrates the two plugin tools and is
|
|
18
|
+
* deliberately opinionated: the whole value of this plugin is that the learner
|
|
19
|
+
* never sees the original puzzle.
|
|
20
|
+
*/
|
|
21
|
+
export const PRACTICE_TEMPLATE = `You are running the "LeetCode -> Real World" practice generator.
|
|
22
|
+
|
|
23
|
+
Goal: fetch a LeetCode problem, extract the *algorithmic principle* behind it, and
|
|
24
|
+
produce a realistic software-engineering assignment that requires the same
|
|
25
|
+
principle. The learner must solve a genuine engineering task, never the original
|
|
26
|
+
puzzle.
|
|
27
|
+
|
|
28
|
+
## 1. Parse arguments
|
|
29
|
+
Arguments: "$ARGUMENTS"
|
|
30
|
+
Recognise any of these (all optional):
|
|
31
|
+
- difficulty: easy | medium | hard
|
|
32
|
+
- tags: comma-separated topic tags, e.g. "dynamic-programming,graph"
|
|
33
|
+
- a specific problem id or slug, e.g. "two-sum" or "1"
|
|
34
|
+
- language: a supported language name or alias (below)
|
|
35
|
+
|
|
36
|
+
## 2. Ask when unclear (use the \`question\` tool)
|
|
37
|
+
Supported languages: ${SUPPORTED_LANGUAGES.join(", ")}.
|
|
38
|
+
|
|
39
|
+
- If the user did NOT specify a language, call the \`question\` tool and ask which
|
|
40
|
+
language they want to practice, offering these as options:
|
|
41
|
+
${SUPPORTED_LANGUAGES.map((name) => `"${name}"`).join(", ")}.
|
|
42
|
+
- If the user did NOT specify a difficulty, include a second question in the SAME
|
|
43
|
+
\`question\` call offering "Easy", "Medium" (recommended), and "Hard".
|
|
44
|
+
- Wait for the answers. Do not guess and do not default silently.
|
|
45
|
+
|
|
46
|
+
If the user did specify values, skip the corresponding question. Only ask at all
|
|
47
|
+
when something is missing.
|
|
48
|
+
|
|
49
|
+
## 3. Fetch the source problem (private)
|
|
50
|
+
Call the \`leetcode_fetch\` tool with the difficulty/tags/slug. The fetched
|
|
51
|
+
statement is for YOUR reasoning only. Never paste it, paraphrase it closely, or
|
|
52
|
+
name its source anywhere in the generated project.
|
|
53
|
+
|
|
54
|
+
## 4. Derive the principle
|
|
55
|
+
Identify the underlying algorithmic idea (e.g. sliding window, heap-based
|
|
56
|
+
scheduling, union-find, DP over intervals). Do not carry over the problem's
|
|
57
|
+
fiction (arrays of "nums", "target", etc.).
|
|
58
|
+
|
|
59
|
+
## 5. Invent a real engineering scenario
|
|
60
|
+
Design a plausible product/business task that genuinely needs that principle. Good
|
|
61
|
+
domains: observability pipelines, payments/ledgering, logistics, rate limiting,
|
|
62
|
+
access control, search, feature flags, data reconciliation, scheduling.
|
|
63
|
+
The scenario must read like a ticket from a real team: who needs it, why, and what
|
|
64
|
+
"correct" means.
|
|
65
|
+
|
|
66
|
+
The assignment has ONE entry point with a JSON-in / JSON-out contract:
|
|
67
|
+
- It receives a single JSON value (usually an object describing a request).
|
|
68
|
+
- It returns a single JSON value (a response or result).
|
|
69
|
+
This is how real services, CLIs and jobs exchange data, and it lets the same test
|
|
70
|
+
harness verify any language.
|
|
71
|
+
|
|
72
|
+
Call the tool \`leetcode_scaffold\` with a spec containing:
|
|
73
|
+
- title: real-world project title (no coding-practice words)
|
|
74
|
+
- scenario: 1-3 paragraphs of business context
|
|
75
|
+
- pattern: the underlying principle (internal only)
|
|
76
|
+
- language: the language the user chose (use the canonical id, e.g. "typescript",
|
|
77
|
+
"javascript", "python", "ruby", "php", "go", "rust", "csharp", "swift")
|
|
78
|
+
- requirements: concrete, testable bullet points
|
|
79
|
+
- edgeCases: tricky situations the solution must handle
|
|
80
|
+
- starterCode: optional; otherwise a TODO stub is generated
|
|
81
|
+
- publicTests: 5-8 visible cases that illustrate the contract
|
|
82
|
+
- hiddenTests: 6-12 acceptance cases, including edge cases, boundary values, an
|
|
83
|
+
empty/degenerate input, and at least one larger input
|
|
84
|
+
- sourceSlug / sourceTitle: for internal provenance ONLY (never surfaced)
|
|
85
|
+
|
|
86
|
+
Test cases must be JSON-serialisable: \`{ "name": string, "input": any, "expected": any }\`.
|
|
87
|
+
Make sure every \`expected\` value is actually correct for the described rules, and
|
|
88
|
+
that public and hidden cases do not overlap.
|
|
89
|
+
|
|
90
|
+
## 6. Rules (non-negotiable)
|
|
91
|
+
- The generated title, scenario, requirements, comments, and test names MUST NOT
|
|
92
|
+
mention LeetCode, its title, its slug, or any coding-practice site. The
|
|
93
|
+
\`leetcode_scaffold\` tool will reject the spec if it does.
|
|
94
|
+
- DO NOT implement the solution. Your job ends when the scaffold is written. The
|
|
95
|
+
learner implements the solution file themselves.
|
|
96
|
+
- Do not reveal the fetched problem or the original examples.
|
|
97
|
+
- If the tool reports a leak, rewrite the offending fields and retry.
|
|
98
|
+
|
|
99
|
+
## 7. After scaffolding
|
|
100
|
+
Report back with, in order:
|
|
101
|
+
1. One short paragraph describing the assignment as a real task (no source spoilers).
|
|
102
|
+
2. The project path and chosen language.
|
|
103
|
+
3. The exact command to run the tests (\`node tests/runner.mjs\`).
|
|
104
|
+
4. Note that \`tests/hidden/cases.json\` holds extra acceptance cases they should not edit.
|
|
105
|
+
Then stop. If the learner asks for help, give guiding hints and ask questions
|
|
106
|
+
rather than writing the algorithm for them.
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
export function practiceTemplate(): string {
|
|
110
|
+
return PRACTICE_TEMPLATE;
|
|
111
|
+
}
|
package/src/scaffold.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getLanguage, normalizeLanguage } from "./languages";
|
|
4
|
+
import { assertNoLeak } from "./patterns";
|
|
5
|
+
import type { FileEntry, LanguageId, ScaffoldResult, ScaffoldSpec, TestCase } from "./types";
|
|
6
|
+
|
|
7
|
+
function kebab(input: string): string {
|
|
8
|
+
return input
|
|
9
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
10
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
11
|
+
.replace(/^-+|-+$/g, "")
|
|
12
|
+
.toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function camel(input: string): string {
|
|
16
|
+
const parts = kebab(input).split("-").filter(Boolean);
|
|
17
|
+
return parts
|
|
18
|
+
.map((part, index) => (index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
|
|
19
|
+
.join("");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ResolvedNames {
|
|
23
|
+
projectName: string;
|
|
24
|
+
functionName: string;
|
|
25
|
+
title: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveNames(spec: ScaffoldSpec): ResolvedNames {
|
|
29
|
+
const projectName = kebab(spec.projectName ?? spec.title) || "practice-project";
|
|
30
|
+
const functionName = spec.functionName?.trim() || "solve";
|
|
31
|
+
return { projectName, functionName, title: spec.title };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function json(value: unknown, indent = 2): string {
|
|
35
|
+
return JSON.stringify(value, null, indent);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const RUNNER = String.raw`#!/usr/bin/env node
|
|
39
|
+
import { spawnSync } from "node:child_process";
|
|
40
|
+
import { readFileSync } from "node:fs";
|
|
41
|
+
import path from "node:path";
|
|
42
|
+
import { fileURLToPath } from "node:url";
|
|
43
|
+
|
|
44
|
+
const testsDir = path.dirname(fileURLToPath(import.meta.url));
|
|
45
|
+
const root = path.resolve(testsDir, "..");
|
|
46
|
+
const harness = JSON.parse(readFileSync(path.join(root, "harness.json"), "utf8"));
|
|
47
|
+
|
|
48
|
+
function canonical(value) {
|
|
49
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
50
|
+
if (value && typeof value === "object") {
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const key of Object.keys(value).sort()) out[key] = canonical(value[key]);
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function same(a, b) {
|
|
59
|
+
return JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function runCase(input) {
|
|
63
|
+
const result = spawnSync(harness.run, {
|
|
64
|
+
cwd: root,
|
|
65
|
+
shell: true,
|
|
66
|
+
encoding: "utf8",
|
|
67
|
+
input: JSON.stringify(input),
|
|
68
|
+
timeout: 30000,
|
|
69
|
+
});
|
|
70
|
+
if (result.error) throw result.error;
|
|
71
|
+
if (result.status !== 0) {
|
|
72
|
+
const detail = (result.stderr || result.stdout || "").trim().split("\n").slice(-6).join("\n");
|
|
73
|
+
throw new Error("process exited with code " + result.status + (detail ? "\n" + detail : ""));
|
|
74
|
+
}
|
|
75
|
+
const text = (result.stdout || "").trim();
|
|
76
|
+
return text === "" ? null : JSON.parse(text);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (harness.build) {
|
|
80
|
+
const build = spawnSync(harness.build, { cwd: root, shell: true, stdio: "inherit" });
|
|
81
|
+
if (build.status !== 0) {
|
|
82
|
+
console.error("\nBuild failed.");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const onlyPublic = process.argv.includes("--public");
|
|
88
|
+
const suites = onlyPublic ? ["public"] : ["public", "hidden"];
|
|
89
|
+
let passed = 0;
|
|
90
|
+
let failed = 0;
|
|
91
|
+
|
|
92
|
+
for (const suite of suites) {
|
|
93
|
+
const cases = JSON.parse(readFileSync(path.join(root, "tests", suite, "cases.json"), "utf8"));
|
|
94
|
+
console.log("\n" + suite + " (" + cases.length + " cases)");
|
|
95
|
+
for (const testCase of cases) {
|
|
96
|
+
try {
|
|
97
|
+
const received = runCase(testCase.input);
|
|
98
|
+
if (same(received, testCase.expected)) {
|
|
99
|
+
passed += 1;
|
|
100
|
+
console.log(" PASS " + testCase.name);
|
|
101
|
+
} else {
|
|
102
|
+
failed += 1;
|
|
103
|
+
console.log(" FAIL " + testCase.name);
|
|
104
|
+
console.log(" expected: " + JSON.stringify(testCase.expected));
|
|
105
|
+
console.log(" received: " + JSON.stringify(received));
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
failed += 1;
|
|
109
|
+
console.log(" ERROR " + testCase.name);
|
|
110
|
+
for (const line of String(error.message).split("\n")) {
|
|
111
|
+
console.log(" " + line);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log("\n" + passed + " passed, " + failed + " failed");
|
|
118
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
function requirementsSection(spec: ScaffoldSpec): string {
|
|
122
|
+
const requirements = spec.requirements ?? [];
|
|
123
|
+
if (requirements.length === 0) return "";
|
|
124
|
+
return `## Requirements\n\n${requirements.map((req, i) => `${i + 1}. ${req}`).join("\n")}\n`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function edgeCaseSection(spec: ScaffoldSpec): string {
|
|
128
|
+
const edgeCases = spec.edgeCases ?? [];
|
|
129
|
+
if (edgeCases.length === 0) return "";
|
|
130
|
+
return `## Edge cases to handle\n\n${edgeCases.map((edge) => `- ${edge}`).join("\n")}\n`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readmeFor(names: ResolvedNames, spec: ScaffoldSpec, language: LanguageId): string {
|
|
134
|
+
const adapter = getLanguage(language);
|
|
135
|
+
const source = `src/solution.${adapter.extension}`;
|
|
136
|
+
const publicCount = (spec.publicTests ?? []).length;
|
|
137
|
+
const hiddenCount = (spec.hiddenTests ?? []).length;
|
|
138
|
+
return `# ${names.title}
|
|
139
|
+
|
|
140
|
+
${names.title} is a self-contained engineering exercise. Read \`PROJECT.md\` for the
|
|
141
|
+
brief, implement the entry point in \`${source}\`, and make the test suite pass.
|
|
142
|
+
|
|
143
|
+
## Run the tests
|
|
144
|
+
|
|
145
|
+
\`\`\`bash
|
|
146
|
+
node tests/runner.mjs
|
|
147
|
+
\`\`\`
|
|
148
|
+
|
|
149
|
+
Requires ${adapter.runtime}.
|
|
150
|
+
|
|
151
|
+
- \`node tests/runner.mjs --public\` runs only the visible cases.
|
|
152
|
+
- Your program reads **one JSON value on stdin** and writes **one JSON value on
|
|
153
|
+
stdout**. The starter file shows the exact shape.
|
|
154
|
+
|
|
155
|
+
## Layout
|
|
156
|
+
|
|
157
|
+
\`\`\`
|
|
158
|
+
${source} # implement your solution here
|
|
159
|
+
tests/public/cases.json # ${publicCount} visible cases
|
|
160
|
+
tests/hidden/cases.json # ${hiddenCount} acceptance cases (do not edit)
|
|
161
|
+
tests/runner.mjs # test harness
|
|
162
|
+
harness.json # build/run commands
|
|
163
|
+
PROJECT.md # scenario, requirements, definition of done
|
|
164
|
+
\`\`\`
|
|
165
|
+
|
|
166
|
+
Prefer clarity over cleverness: the goal is production-quality code, not the
|
|
167
|
+
shortest possible snippet.
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function projectDoc(spec: ScaffoldSpec, names: ResolvedNames, language: LanguageId): string {
|
|
172
|
+
const adapter = getLanguage(language);
|
|
173
|
+
return `# ${names.title}
|
|
174
|
+
|
|
175
|
+
## Scenario
|
|
176
|
+
|
|
177
|
+
${spec.scenario.trim()}
|
|
178
|
+
|
|
179
|
+
${requirementsSection(spec)}
|
|
180
|
+
${edgeCaseSection(spec)}
|
|
181
|
+
## Interface
|
|
182
|
+
|
|
183
|
+
Implement \`src/solution.${adapter.extension}\`. Your program is invoked once per test
|
|
184
|
+
case: it reads **one JSON value from stdin** and must write **one JSON value to
|
|
185
|
+
stdout**. Nothing else may be printed to stdout (use stderr for debugging).
|
|
186
|
+
|
|
187
|
+
## Definition of done
|
|
188
|
+
|
|
189
|
+
- The full test suite passes: \`node tests/runner.mjs\`.
|
|
190
|
+
- Edge cases above are handled, not just the happy path.
|
|
191
|
+
- No network or external services are required.
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const PROJECT_GITIGNORE = `# build artifacts
|
|
196
|
+
out/
|
|
197
|
+
target/
|
|
198
|
+
bin/
|
|
199
|
+
obj/
|
|
200
|
+
node_modules/
|
|
201
|
+
__pycache__/
|
|
202
|
+
*.pyc
|
|
203
|
+
.pytest_cache/
|
|
204
|
+
|
|
205
|
+
# acceptance tests + provenance stay local
|
|
206
|
+
tests/hidden/
|
|
207
|
+
.practice-meta.json
|
|
208
|
+
`;
|
|
209
|
+
|
|
210
|
+
function metaFor(spec: ScaffoldSpec, names: ResolvedNames, language: LanguageId): string {
|
|
211
|
+
return (
|
|
212
|
+
json({
|
|
213
|
+
title: names.title,
|
|
214
|
+
functionName: names.functionName,
|
|
215
|
+
language,
|
|
216
|
+
pattern: spec.pattern,
|
|
217
|
+
difficulty: spec.difficulty ?? null,
|
|
218
|
+
source: { slug: spec.sourceSlug ?? null, title: spec.sourceTitle ?? null },
|
|
219
|
+
generatedAt: new Date().toISOString(),
|
|
220
|
+
}) + "\n"
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function casesFile(cases: TestCase[]): string {
|
|
225
|
+
return json(cases) + "\n";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function buildFiles(spec: ScaffoldSpec, names: ResolvedNames, language: LanguageId): FileEntry[] {
|
|
229
|
+
const adapter = getLanguage(language);
|
|
230
|
+
const ctx = { projectName: names.projectName, functionName: names.functionName, title: names.title };
|
|
231
|
+
|
|
232
|
+
const files: FileEntry[] = [
|
|
233
|
+
{ relative: "README.md", contents: readmeFor(names, spec, language) },
|
|
234
|
+
{ relative: "PROJECT.md", contents: projectDoc(spec, names, language) },
|
|
235
|
+
{ relative: ".gitignore", contents: PROJECT_GITIGNORE },
|
|
236
|
+
{ relative: ".practice-meta.json", contents: metaFor(spec, names, language) },
|
|
237
|
+
{
|
|
238
|
+
relative: "harness.json",
|
|
239
|
+
contents:
|
|
240
|
+
json({ build: adapter.build(ctx), run: adapter.run(ctx) }) + "\n",
|
|
241
|
+
},
|
|
242
|
+
{ relative: "tests/runner.mjs", contents: RUNNER },
|
|
243
|
+
{ relative: "tests/public/cases.json", contents: casesFile(spec.publicTests ?? []) },
|
|
244
|
+
{ relative: "tests/hidden/cases.json", contents: casesFile(spec.hiddenTests ?? []) },
|
|
245
|
+
{
|
|
246
|
+
relative: `src/solution.${adapter.extension}`,
|
|
247
|
+
contents: spec.starterCode?.trimEnd()
|
|
248
|
+
? spec.starterCode.trimEnd() + "\n"
|
|
249
|
+
: adapter.starter(ctx),
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
files.push(...adapter.files(ctx));
|
|
254
|
+
return files;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface ScaffoldOptions {
|
|
258
|
+
/** Base directory to resolve relative paths against (the session directory). */
|
|
259
|
+
directory: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function scaffold(
|
|
263
|
+
spec: ScaffoldSpec,
|
|
264
|
+
options: ScaffoldOptions,
|
|
265
|
+
): Promise<ScaffoldResult> {
|
|
266
|
+
const language = normalizeLanguage(spec.language) ?? (spec.language ? undefined : "typescript");
|
|
267
|
+
if (!language) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`Unsupported language "${spec.language}". Supported: typescript, javascript, python, ruby, php, go, rust, csharp, swift.`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const normalized: ScaffoldSpec = {
|
|
274
|
+
...spec,
|
|
275
|
+
language,
|
|
276
|
+
requirements: spec.requirements ?? [],
|
|
277
|
+
edgeCases: spec.edgeCases ?? [],
|
|
278
|
+
publicTests: spec.publicTests ?? [],
|
|
279
|
+
hiddenTests: spec.hiddenTests ?? [],
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
assertNoLeak(normalized);
|
|
283
|
+
|
|
284
|
+
const adapter = getLanguage(language);
|
|
285
|
+
const names = resolveNames(normalized);
|
|
286
|
+
const target = normalized.outDir
|
|
287
|
+
? path.resolve(options.directory, normalized.outDir)
|
|
288
|
+
: path.resolve(options.directory, ".practice", names.projectName);
|
|
289
|
+
|
|
290
|
+
const files = buildFiles(normalized, names, language);
|
|
291
|
+
for (const file of files) {
|
|
292
|
+
const destination = path.join(target, file.relative);
|
|
293
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
294
|
+
await writeFile(destination, file.contents, "utf8");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const check = "node tests/runner.mjs";
|
|
298
|
+
return {
|
|
299
|
+
outDir: target,
|
|
300
|
+
title: names.title,
|
|
301
|
+
language,
|
|
302
|
+
languageName: adapter.name,
|
|
303
|
+
runtime: adapter.runtime,
|
|
304
|
+
files: files.map((file) => file.relative).sort(),
|
|
305
|
+
runCommand: check,
|
|
306
|
+
checkCommand: check,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export { resolveNames, kebab, camel };
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export type LanguageId =
|
|
2
|
+
| "typescript"
|
|
3
|
+
| "javascript"
|
|
4
|
+
| "python"
|
|
5
|
+
| "ruby"
|
|
6
|
+
| "php"
|
|
7
|
+
| "go"
|
|
8
|
+
| "rust"
|
|
9
|
+
| "csharp"
|
|
10
|
+
| "swift";
|
|
11
|
+
|
|
12
|
+
export interface CodeSnippet {
|
|
13
|
+
lang: string;
|
|
14
|
+
langSlug: string;
|
|
15
|
+
code: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A LeetCode problem reduced to a stable, API-shape-agnostic structure.
|
|
20
|
+
* The upstream API is not perfectly consistent between endpoints, so everything
|
|
21
|
+
* is funneled through `normalizeProblem` before it reaches the rest of the plugin.
|
|
22
|
+
*/
|
|
23
|
+
export interface NormalizedProblem {
|
|
24
|
+
id: string;
|
|
25
|
+
frontendId: string;
|
|
26
|
+
slug: string;
|
|
27
|
+
title: string;
|
|
28
|
+
difficulty: string;
|
|
29
|
+
url: string;
|
|
30
|
+
topics: string[];
|
|
31
|
+
contentHtml: string;
|
|
32
|
+
contentText: string;
|
|
33
|
+
hints: string[];
|
|
34
|
+
snippets: CodeSnippet[];
|
|
35
|
+
acRate?: number;
|
|
36
|
+
paidOnly: boolean;
|
|
37
|
+
/** Only present for the `/daily` endpoint. */
|
|
38
|
+
date?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TestCase {
|
|
42
|
+
name: string;
|
|
43
|
+
input: unknown;
|
|
44
|
+
expected: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface FileEntry {
|
|
48
|
+
relative: string;
|
|
49
|
+
contents: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The contract the agent fills in after it has decided on a real-world scenario.
|
|
54
|
+
* None of these fields may reference LeetCode: `scaffold()` calls `assertNoLeak`
|
|
55
|
+
* before writing anything to disk.
|
|
56
|
+
*/
|
|
57
|
+
export interface ScaffoldSpec {
|
|
58
|
+
/** kebab-case directory name. Derived from `title` when omitted. */
|
|
59
|
+
projectName?: string;
|
|
60
|
+
/** Human title of the *real-world* project, e.g. "Realtime Dedup Pipeline". */
|
|
61
|
+
title: string;
|
|
62
|
+
/** Business context. Explains why the project matters and what it must do. */
|
|
63
|
+
scenario: string;
|
|
64
|
+
/** Underlying algorithmic principle, e.g. "sliding window". Internal only. */
|
|
65
|
+
pattern: string;
|
|
66
|
+
difficulty?: string;
|
|
67
|
+
language?: LanguageId;
|
|
68
|
+
/** Name of the entry function inside the solution file. Defaults to `solve`. */
|
|
69
|
+
functionName?: string;
|
|
70
|
+
requirements?: string[];
|
|
71
|
+
edgeCases?: string[];
|
|
72
|
+
/** Optional starter implementation. A TODO stub is generated when omitted. */
|
|
73
|
+
starterCode?: string;
|
|
74
|
+
publicTests?: TestCase[];
|
|
75
|
+
hiddenTests?: TestCase[];
|
|
76
|
+
/** Directory to create the project in, relative to the session directory. */
|
|
77
|
+
outDir?: string;
|
|
78
|
+
/** Internal provenance, stored in `.practice-meta.json` only. Never leaked. */
|
|
79
|
+
sourceSlug?: string;
|
|
80
|
+
sourceTitle?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ScaffoldResult {
|
|
84
|
+
outDir: string;
|
|
85
|
+
title: string;
|
|
86
|
+
language: LanguageId;
|
|
87
|
+
languageName: string;
|
|
88
|
+
runtime: string;
|
|
89
|
+
files: string[];
|
|
90
|
+
runCommand: string;
|
|
91
|
+
checkCommand: string;
|
|
92
|
+
}
|