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.
@@ -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
+ }
@@ -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
+ }