opencode-leetcode-realworld 0.3.0 → 0.4.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 +20 -2
- package/package.json +1 -1
- package/src/index.ts +24 -1
- package/src/patterns.ts +2 -1
- package/src/prompt.ts +73 -54
- package/src/scaffold.ts +144 -2
- package/src/types.ts +22 -1
package/README.md
CHANGED
|
@@ -76,6 +76,7 @@ Options can be given in any order; the command figures out which is which.
|
|
|
76
76
|
| `tags` | One or more topic tags, comma-separated (e.g. `graph` or `dynamic-programming,sliding-window`) | any | Nudges which underlying algorithm and real-world domain you get. |
|
|
77
77
|
| `<id-or-slug>` | A problem number or slug (e.g. `1`, `two-sum`) | random | Derive the assignment from one specific source problem. |
|
|
78
78
|
| `language` | A supported language or alias (see below) | **asks you** | Language of the generated project and tests. |
|
|
79
|
+
| `scope` | `single` \| `mini` \| `full` | **asks you** | How big the assignment is (see Two modes). |
|
|
79
80
|
|
|
80
81
|
If you leave the language (or difficulty) out, opencode **asks you** with a
|
|
81
82
|
multiple-choice question before generating anything.
|
|
@@ -150,17 +151,34 @@ they tend to produce:
|
|
|
150
151
|
```
|
|
151
152
|
/practice
|
|
152
153
|
│
|
|
153
|
-
├─ (agent)
|
|
154
|
+
├─ (agent) ask you for language/difficulty, then how you want to start
|
|
154
155
|
│
|
|
155
156
|
├─ leetcode_fetch fetch a problem from the LeetCode API (private reasoning input)
|
|
156
157
|
│
|
|
157
158
|
├─ (agent) derive the principle -> invent a real-world scenario
|
|
158
159
|
│
|
|
159
|
-
├─ leetcode_scaffold write
|
|
160
|
+
├─ leetcode_scaffold write the exercises(s): one function, or a multi-file app
|
|
160
161
|
│
|
|
161
162
|
└─ guard hooks block reading hidden tests + provenance, keep the exercise honest
|
|
162
163
|
```
|
|
163
164
|
|
|
165
|
+
## Two modes
|
|
166
|
+
|
|
167
|
+
`/practice` asks **how you want to start**, and the answer changes what gets built:
|
|
168
|
+
|
|
169
|
+
| Scope | What you get |
|
|
170
|
+
| --- | --- |
|
|
171
|
+
| **Quick exercise** | One entry point with a JSON-in / JSON-out contract (any language). |
|
|
172
|
+
| **Mini project** | A small multi-file app (~3-6 files) with the algorithm behind a single `TODO` (TypeScript, Python, Rust). |
|
|
173
|
+
| **Fuller project** | A multi-file app with more realistic layers (~6-15 files) and the same single `TODO`. |
|
|
174
|
+
|
|
175
|
+
In project mode you get a real little codebase — CLI entry, domain types, service
|
|
176
|
+
layer, wiring — where **everything works except one `TODO`**. You implement that
|
|
177
|
+
piece (the algorithm) and `node tests/runner.mjs` verifies the app end-to-end.
|
|
178
|
+
|
|
179
|
+
Project mode is available for **TypeScript, Python, Rust**; the other languages use
|
|
180
|
+
the quick exercise.
|
|
181
|
+
|
|
164
182
|
The generated project looks like this:
|
|
165
183
|
|
|
166
184
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-leetcode-realworld",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "OpenCode plugin that turns a LeetCode problem into a realistic software-engineering assignment, so you practice applying the underlying algorithm instead of solving the puzzle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -71,9 +71,15 @@ export const LeetCodeRealWorld: Plugin = async ({ client, directory }) => {
|
|
|
71
71
|
|
|
72
72
|
const scaffoldTool = tool({
|
|
73
73
|
description:
|
|
74
|
-
"Create a real-world practice project from a spec
|
|
74
|
+
"Create a real-world practice project from a spec. " +
|
|
75
|
+
"Use mode \"single\" for one JSON-in/out function (any language) or mode \"project\" " +
|
|
76
|
+
"(typescript, python, rust) for a multi-file app where one TODO must be implemented. " +
|
|
75
77
|
"Rejects any spec that leaks the original problem, its title, slug, or a coding-practice site name.",
|
|
76
78
|
args: {
|
|
79
|
+
mode: tool.schema
|
|
80
|
+
.enum(["single", "project"])
|
|
81
|
+
.default("single")
|
|
82
|
+
.describe("single = one function; project = multi-file app (ts/python/rust)"),
|
|
77
83
|
title: tool.schema.string().describe("Real-world project title (no coding-practice words)"),
|
|
78
84
|
scenario: tool.schema.string().describe("Business context, written like a real ticket"),
|
|
79
85
|
pattern: tool.schema.string().describe("Underlying algorithmic principle (internal only)"),
|
|
@@ -102,6 +108,22 @@ export const LeetCodeRealWorld: Plugin = async ({ client, directory }) => {
|
|
|
102
108
|
outDir: tool.schema.string().optional().describe("Target directory, relative to the session directory"),
|
|
103
109
|
sourceSlug: tool.schema.string().optional().describe("Provenance only; never surfaced"),
|
|
104
110
|
sourceTitle: tool.schema.string().optional().describe("Provenance only; never surfaced"),
|
|
111
|
+
task: tool.schema
|
|
112
|
+
.string()
|
|
113
|
+
.optional()
|
|
114
|
+
.describe("Project mode: markdown brief telling the learner what to implement and where"),
|
|
115
|
+
files: tool.schema
|
|
116
|
+
.array(tool.schema.object({ path: tool.schema.string(), content: tool.schema.string() }))
|
|
117
|
+
.optional()
|
|
118
|
+
.describe("Project mode: the full file tree, leaving one TODO to implement"),
|
|
119
|
+
runCommand: tool.schema
|
|
120
|
+
.string()
|
|
121
|
+
.optional()
|
|
122
|
+
.describe("Project mode: command that runs one case (JSON on stdin, JSON on stdout)"),
|
|
123
|
+
buildCommand: tool.schema
|
|
124
|
+
.string()
|
|
125
|
+
.optional()
|
|
126
|
+
.describe("Project mode: optional one-time build command (compiled languages)"),
|
|
105
127
|
},
|
|
106
128
|
async execute(rawArgs, context) {
|
|
107
129
|
const spec = rawArgs as ScaffoldSpec;
|
|
@@ -131,6 +153,7 @@ export const LeetCodeRealWorld: Plugin = async ({ client, directory }) => {
|
|
|
131
153
|
`Created a real-world practice project at ${relative}`,
|
|
132
154
|
"",
|
|
133
155
|
`Title: ${result.title}`,
|
|
156
|
+
`Mode: ${result.mode}${result.mode === "project" ? " (implement the marked TODO)" : ""}`,
|
|
134
157
|
`Language: ${result.languageName} (${result.runtime})`,
|
|
135
158
|
`Files (${result.files.length}):`,
|
|
136
159
|
...result.files.map((file) => ` - ${file}`),
|
package/src/patterns.ts
CHANGED
|
@@ -92,11 +92,12 @@ function textsFromSpec(spec: ScaffoldSpec): Array<[string, string]> {
|
|
|
92
92
|
push("scenario", spec.scenario);
|
|
93
93
|
push("pattern", spec.pattern);
|
|
94
94
|
push("functionName", spec.functionName);
|
|
95
|
-
push("
|
|
95
|
+
push("task", spec.task);
|
|
96
96
|
(spec.requirements ?? []).forEach((req, i) => push(`requirements[${i}]`, req));
|
|
97
97
|
(spec.edgeCases ?? []).forEach((edge, i) => push(`edgeCases[${i}]`, edge));
|
|
98
98
|
(spec.publicTests ?? []).forEach((test, i) => push(`publicTests[${i}].name`, test.name));
|
|
99
99
|
(spec.hiddenTests ?? []).forEach((test, i) => push(`hiddenTests[${i}].name`, test.name));
|
|
100
|
+
(spec.files ?? []).forEach((file, i) => push(`files[${i}] (${file.path})`, file.content));
|
|
100
101
|
return entries;
|
|
101
102
|
}
|
|
102
103
|
|
package/src/prompt.ts
CHANGED
|
@@ -13,6 +13,9 @@ export const SUPPORTED_LANGUAGES = [
|
|
|
13
13
|
"Swift",
|
|
14
14
|
];
|
|
15
15
|
|
|
16
|
+
/** Languages that can receive a full multi-file project. */
|
|
17
|
+
export const PROJECT_LANGUAGES = ["TypeScript", "Python", "Rust"];
|
|
18
|
+
|
|
16
19
|
/**
|
|
17
20
|
* The `/practice` command prompt. It orchestrates the two plugin tools and is
|
|
18
21
|
* deliberately opinionated: the whole value of this plugin is that the learner
|
|
@@ -32,6 +35,7 @@ Recognise any of these (all optional):
|
|
|
32
35
|
- tags: topic names or slugs, e.g. "graph" or "dynamic-programming,sliding-window"
|
|
33
36
|
- a specific problem id or slug, e.g. "two-sum" or "1"
|
|
34
37
|
- language: a supported language name or alias (below)
|
|
38
|
+
- scope: "single" | "mini" | "full" (how they want to start)
|
|
35
39
|
|
|
36
40
|
Mapping rules:
|
|
37
41
|
- A word that names a topic (array, string, graph, tree, dp/dynamic-programming,
|
|
@@ -42,82 +46,97 @@ Mapping rules:
|
|
|
42
46
|
- When the user gives a tag, it MUST be respected. After fetching, if the problem's
|
|
43
47
|
topics do not include the requested tag, the fetch is wrong: retry with the tag.
|
|
44
48
|
|
|
45
|
-
## 2. Ask
|
|
46
|
-
|
|
49
|
+
## 2. Ask how they want to start (use the \`question\` tool)
|
|
50
|
+
|
|
51
|
+
Step A — if the language and/or difficulty are missing, ask for them in one
|
|
52
|
+
\`question\` call. Supported languages: ${SUPPORTED_LANGUAGES.join(", ")}.
|
|
53
|
+
Offer difficulty options "Easy", "Medium" (recommended), "Hard".
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
-
|
|
55
|
+
Step B — decide the *scope*. Full multi-file projects are only available for
|
|
56
|
+
${PROJECT_LANGUAGES.join(", ")}.
|
|
57
|
+
- If the chosen language is one of those AND scope was not given, make a SECOND
|
|
58
|
+
\`question\` call titled "How do you want to start?" with these options:
|
|
59
|
+
- "Quick exercise" — one focused function with a JSON-in/JSON-out contract.
|
|
60
|
+
- "Mini project" — a small multi-file app (~3-6 files) with the algorithm at its core.
|
|
61
|
+
- "Fuller project" — a multi-file app with more realistic layers (~6-15 files).
|
|
62
|
+
- Otherwise (other language, or scope already given) skip this question.
|
|
54
63
|
|
|
55
|
-
|
|
56
|
-
when something is missing.
|
|
64
|
+
Wait for the answers. Do not guess.
|
|
57
65
|
|
|
58
66
|
## 3. Fetch the source problem (private)
|
|
59
67
|
Call the \`leetcode_fetch\` tool with the difficulty, and pass any requested topic
|
|
60
|
-
as \`tags\` (array of slugs)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
in the generated project.
|
|
68
|
+
as \`tags\` (array of slugs). Verify the returned topics include every requested
|
|
69
|
+
tag; if not, retry. The fetched statement is for YOUR reasoning only. Never paste
|
|
70
|
+
it, paraphrase it closely, or name its source anywhere in the generated project.
|
|
64
71
|
|
|
65
72
|
## 4. Derive the principle
|
|
66
73
|
Identify the underlying algorithmic idea (e.g. sliding window, heap-based
|
|
67
74
|
scheduling, union-find, DP over intervals). Do not carry over the problem's
|
|
68
75
|
fiction (arrays of "nums", "target", etc.).
|
|
69
76
|
|
|
70
|
-
## 5.
|
|
77
|
+
## 5. Build the assignment
|
|
71
78
|
Design a plausible product/business task that genuinely needs that principle. Good
|
|
72
|
-
domains: observability
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
domains: observability, payments/ledgering, logistics, rate limiting, access
|
|
80
|
+
control, search, feature flags, data reconciliation, scheduling.
|
|
81
|
+
|
|
82
|
+
Then call \`leetcode_scaffold\`. There are two shapes:
|
|
83
|
+
|
|
84
|
+
### A) mode = "single" (Quick exercise)
|
|
85
|
+
- One entry point with a JSON-in / JSON-out contract.
|
|
86
|
+
- Do NOT provide \`starterCode\`: the plugin generates a typed starter (type-safe
|
|
87
|
+
languages) or a documented one (dynamic languages) inferred from your test cases,
|
|
88
|
+
so make the first few cases representative (vary fields to expose optionals).
|
|
89
|
+
- Spec: mode, title, scenario, pattern, difficulty, language, requirements,
|
|
90
|
+
edgeCases, publicTests (5-8), hiddenTests (6-12 incl. empty + large), task optional,
|
|
91
|
+
sourceSlug, sourceTitle.
|
|
92
|
+
|
|
93
|
+
### B) mode = "project" (only ${PROJECT_LANGUAGES.join(", ")})
|
|
94
|
+
Build a small but real application where the algorithm is the piece the learner
|
|
95
|
+
implements. You author the whole tree and pass it as \`files\`.
|
|
96
|
+
- Provide a working CLI entry (e.g. \`src/cli.ts\`, \`src/cli.py\`, or \`src/main.rs\`)
|
|
97
|
+
that reads ONE JSON value on stdin and writes ONE JSON value on stdout. Tests
|
|
98
|
+
drive this entry end-to-end.
|
|
99
|
+
- Put the algorithm behind a single, clearly marked \`TODO\` in a domain/service file
|
|
100
|
+
(e.g. \`// TODO: implement ...\`). Everything else — argument/stdin parsing, domain
|
|
101
|
+
models/types, wiring, config, a repository or helper — must be complete and runnable
|
|
102
|
+
so that the ONLY thing missing is that TODO.
|
|
103
|
+
- Keep the surrounding code real but not algorithm-heavy: parsing, types, plumbing.
|
|
104
|
+
- Respect the requested size: mini ~3-6 files, fuller ~6-15 files.
|
|
105
|
+
- Provide \`runCommand\` (runs one case) and, for compiled languages, \`buildCommand\`
|
|
106
|
+
(e.g. Rust: \`cargo build\`; then \`runCommand: ./target/debug/<bin>\`).
|
|
107
|
+
- Provide \`task\`: a short markdown brief naming the file and function to implement.
|
|
108
|
+
- Include everything needed to build/run (package.json/tsconfig, Cargo.toml, etc.).
|
|
109
|
+
- Import/runtime correctness (the tests run your \`runCommand\`):
|
|
110
|
+
- TypeScript: relative imports MUST include the \`.ts\` extension
|
|
111
|
+
(\`import { x } from "./service.ts"\`) because the app runs under Node ESM type
|
|
112
|
+
stripping, which does not resolve extensionless specifiers.
|
|
113
|
+
- Python: make imports work when the entry runs directly (\`python3 src/cli.py\`);
|
|
114
|
+
same-directory modules or a small \`sys.path\` adjustment are both fine.
|
|
115
|
+
- Rust: declare internal modules with \`mod ...;\` and keep them under \`src/\`.
|
|
116
|
+
- Do NOT implement the TODO and do not reveal the algorithm in comments.
|
|
117
|
+
|
|
118
|
+
Both shapes:
|
|
89
119
|
- requirements: concrete, testable bullet points
|
|
90
120
|
- edgeCases: tricky situations the solution must handle
|
|
91
|
-
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
The generated types are inferred from your publicTests/hiddenTests, so make the
|
|
95
|
-
first few cases representative of the full input shape (and vary fields so
|
|
96
|
-
optional ones are detected).
|
|
97
|
-
- publicTests: 5-8 visible cases that illustrate the contract
|
|
98
|
-
- hiddenTests: 6-12 acceptance cases, including edge cases, boundary values, an
|
|
99
|
-
empty/degenerate input, and at least one larger input
|
|
100
|
-
- sourceSlug / sourceTitle: for internal provenance ONLY (never surfaced)
|
|
101
|
-
|
|
102
|
-
Test cases must be JSON-serialisable: \`{ "name": string, "input": any, "expected": any }\`.
|
|
103
|
-
Make sure every \`expected\` value is actually correct for the described rules, and
|
|
104
|
-
that public and hidden cases do not overlap.
|
|
121
|
+
- publicTests / hiddenTests: JSON-serialisable \`{ "name": string, "input": any, "expected": any }\`.
|
|
122
|
+
Every \`expected\` must be correct for the described rules; public and hidden must not overlap.
|
|
123
|
+
- sourceSlug / sourceTitle: provenance ONLY, never surfaced.
|
|
105
124
|
|
|
106
125
|
## 6. Rules (non-negotiable)
|
|
107
|
-
-
|
|
108
|
-
mention LeetCode, its title, its slug, or any coding-practice site. The
|
|
109
|
-
|
|
110
|
-
-
|
|
111
|
-
|
|
126
|
+
- Titles, scenarios, requirements, comments, file contents and test names MUST NOT
|
|
127
|
+
mention LeetCode, its title, its slug, or any coding-practice site. The tool
|
|
128
|
+
rejects leaks (including inside \`files\`).
|
|
129
|
+
- NEVER implement the algorithm (single mode: leave the entry unimplemented;
|
|
130
|
+
project mode: leave the TODO unimplemented). The learner does that.
|
|
112
131
|
- Do not reveal the fetched problem or the original examples.
|
|
113
|
-
- If the tool reports a leak, rewrite the offending fields and retry.
|
|
114
132
|
|
|
115
133
|
## 7. After scaffolding
|
|
116
134
|
Report back with, in order:
|
|
117
135
|
1. One short paragraph describing the assignment as a real task (no source spoilers).
|
|
118
|
-
2. The project path and
|
|
136
|
+
2. The project path, mode, and language.
|
|
119
137
|
3. The exact command to run the tests (\`node tests/runner.mjs\`).
|
|
120
|
-
4.
|
|
138
|
+
4. For project mode: which file/function holds the TODO.
|
|
139
|
+
5. Note that \`tests/hidden/cases.json\` holds extra acceptance cases they should not edit.
|
|
121
140
|
Then stop. If the learner asks for help, give guiding hints and ask questions
|
|
122
141
|
rather than writing the algorithm for them.
|
|
123
142
|
`;
|
package/src/scaffold.ts
CHANGED
|
@@ -232,9 +232,15 @@ tests/hidden/
|
|
|
232
232
|
.practice-meta.json
|
|
233
233
|
`;
|
|
234
234
|
|
|
235
|
-
function metaFor(
|
|
235
|
+
function metaFor(
|
|
236
|
+
spec: ScaffoldSpec,
|
|
237
|
+
names: ResolvedNames,
|
|
238
|
+
language: LanguageId,
|
|
239
|
+
mode: "single" | "project" = "single",
|
|
240
|
+
): string {
|
|
236
241
|
return (
|
|
237
242
|
json({
|
|
243
|
+
mode,
|
|
238
244
|
title: names.title,
|
|
239
245
|
functionName: names.functionName,
|
|
240
246
|
language,
|
|
@@ -289,6 +295,119 @@ function buildFiles(spec: ScaffoldSpec, names: ResolvedNames, language: Language
|
|
|
289
295
|
return files;
|
|
290
296
|
}
|
|
291
297
|
|
|
298
|
+
/** Languages that support full multi-file project mode. */
|
|
299
|
+
export const PROJECT_LANGUAGES = new Set<LanguageId>(["typescript", "python", "rust"]);
|
|
300
|
+
|
|
301
|
+
function sanitizeRelative(value: string): string {
|
|
302
|
+
const normalized = value.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
303
|
+
if (normalized.includes("..") || /^[a-zA-Z]:/.test(normalized)) {
|
|
304
|
+
throw new Error(`Refusing to write outside the project directory: "${value}"`);
|
|
305
|
+
}
|
|
306
|
+
return normalized;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function projectReadme(
|
|
310
|
+
names: ResolvedNames,
|
|
311
|
+
spec: ScaffoldSpec,
|
|
312
|
+
language: LanguageId,
|
|
313
|
+
): string {
|
|
314
|
+
const adapter = getLanguage(language);
|
|
315
|
+
const fileList = (spec.files ?? [])
|
|
316
|
+
.map((file) => ` ${sanitizeRelative(file.path)}`)
|
|
317
|
+
.join("\n");
|
|
318
|
+
return `# ${names.title}
|
|
319
|
+
|
|
320
|
+
This is a small application, not a single function. Read \`PROJECT.md\` for the
|
|
321
|
+
brief, find the marked \`TODO\`, and implement the behaviour it needs.
|
|
322
|
+
|
|
323
|
+
## Run the checks
|
|
324
|
+
|
|
325
|
+
\`\`\`bash
|
|
326
|
+
node tests/runner.mjs # all cases
|
|
327
|
+
node tests/runner.mjs --public # visible cases only
|
|
328
|
+
\`\`\`
|
|
329
|
+
|
|
330
|
+
Requires ${adapter.runtime}.
|
|
331
|
+
|
|
332
|
+
The app is invoked once per case as \`${spec.runCommand}\`: it reads **one JSON value
|
|
333
|
+
on stdin** and must write **one JSON value on stdout**. Debug output goes to stderr.
|
|
334
|
+
|
|
335
|
+
## Files
|
|
336
|
+
|
|
337
|
+
\`\`\`
|
|
338
|
+
${fileList}
|
|
339
|
+
\`\`\`
|
|
340
|
+
`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function projectModeDoc(spec: ScaffoldSpec, names: ResolvedNames, language: LanguageId): string {
|
|
344
|
+
const task = spec.task?.trim() || "Find the `TODO` in the source and implement it.";
|
|
345
|
+
return `# ${names.title}
|
|
346
|
+
|
|
347
|
+
## Scenario
|
|
348
|
+
|
|
349
|
+
${spec.scenario.trim()}
|
|
350
|
+
|
|
351
|
+
${exampleSection(spec)}
|
|
352
|
+
${requirementsSection(spec)}
|
|
353
|
+
${edgeCaseSection(spec)}
|
|
354
|
+
## What to build
|
|
355
|
+
|
|
356
|
+
${task}
|
|
357
|
+
|
|
358
|
+
## Interface
|
|
359
|
+
|
|
360
|
+
The application is invoked as \`${spec.runCommand}\`. It reads **one JSON value from
|
|
361
|
+
stdin** and must write **one JSON value to stdout**. Only stdout is compared; use
|
|
362
|
+
stderr for logs.
|
|
363
|
+
|
|
364
|
+
## Definition of done
|
|
365
|
+
|
|
366
|
+
- The full test suite passes: \`node tests/runner.mjs\`.
|
|
367
|
+
- The provided code is understood and reused, not bypassed.
|
|
368
|
+
- Edge cases above are handled, not just the happy path.
|
|
369
|
+
`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function buildProjectFiles(
|
|
373
|
+
spec: ScaffoldSpec,
|
|
374
|
+
names: ResolvedNames,
|
|
375
|
+
language: LanguageId,
|
|
376
|
+
): FileEntry[] {
|
|
377
|
+
const adapter = getLanguage(language);
|
|
378
|
+
const cases = [...(spec.publicTests ?? []), ...(spec.hiddenTests ?? [])];
|
|
379
|
+
const first = cases[0];
|
|
380
|
+
const ctx: LanguageContext = {
|
|
381
|
+
projectName: names.projectName,
|
|
382
|
+
functionName: names.functionName,
|
|
383
|
+
title: names.title,
|
|
384
|
+
input: schemaFromSamples(cases.map((test) => test.input)),
|
|
385
|
+
output: schemaFromSamples(cases.map((test) => test.expected)),
|
|
386
|
+
inputExample: first?.input ?? null,
|
|
387
|
+
outputExample: first?.expected ?? null,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const files: FileEntry[] = [
|
|
391
|
+
{ relative: "README.md", contents: projectReadme(names, spec, language) },
|
|
392
|
+
{ relative: "PROJECT.md", contents: projectModeDoc(spec, names, language) },
|
|
393
|
+
{ relative: ".gitignore", contents: PROJECT_GITIGNORE },
|
|
394
|
+
{ relative: ".practice-meta.json", contents: metaFor(spec, names, language, "project") },
|
|
395
|
+
{
|
|
396
|
+
relative: "harness.json",
|
|
397
|
+
contents:
|
|
398
|
+
json({ build: spec.buildCommand ?? adapter.build(ctx), run: spec.runCommand }) + "\n",
|
|
399
|
+
},
|
|
400
|
+
{ relative: "tests/runner.mjs", contents: RUNNER },
|
|
401
|
+
{ relative: "tests/public/cases.json", contents: casesFile(spec.publicTests ?? []) },
|
|
402
|
+
{ relative: "tests/hidden/cases.json", contents: casesFile(spec.hiddenTests ?? []) },
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
for (const file of spec.files ?? []) {
|
|
406
|
+
files.push({ relative: sanitizeRelative(file.path), contents: file.content });
|
|
407
|
+
}
|
|
408
|
+
return files;
|
|
409
|
+
}
|
|
410
|
+
|
|
292
411
|
export interface ScaffoldOptions {
|
|
293
412
|
/** Base directory to resolve relative paths against (the session directory). */
|
|
294
413
|
directory: string;
|
|
@@ -316,13 +435,35 @@ export async function scaffold(
|
|
|
316
435
|
|
|
317
436
|
assertNoLeak(normalized);
|
|
318
437
|
|
|
438
|
+
const mode = normalized.mode === "project" ? "project" : "single";
|
|
439
|
+
|
|
319
440
|
const adapter = getLanguage(language);
|
|
320
441
|
const names = resolveNames(normalized);
|
|
442
|
+
|
|
443
|
+
if (mode === "project") {
|
|
444
|
+
if (!PROJECT_LANGUAGES.has(language)) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
`Project mode is only supported for: ${[...PROJECT_LANGUAGES].join(", ")}. ` +
|
|
447
|
+
`Use mode "single" for ${adapter.name}.`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
if (!normalized.files || normalized.files.length === 0) {
|
|
451
|
+
throw new Error("Project mode requires a `files` list (the full file tree).");
|
|
452
|
+
}
|
|
453
|
+
if (!normalized.runCommand) {
|
|
454
|
+
throw new Error("Project mode requires a `runCommand` (run one case, JSON on stdin/stdout).");
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
321
458
|
const target = normalized.outDir
|
|
322
459
|
? path.resolve(options.directory, normalized.outDir)
|
|
323
460
|
: path.resolve(options.directory, ".practice", names.projectName);
|
|
324
461
|
|
|
325
|
-
const files =
|
|
462
|
+
const files =
|
|
463
|
+
mode === "project"
|
|
464
|
+
? buildProjectFiles(normalized, names, language)
|
|
465
|
+
: buildFiles(normalized, names, language);
|
|
466
|
+
|
|
326
467
|
for (const file of files) {
|
|
327
468
|
const destination = path.join(target, file.relative);
|
|
328
469
|
await mkdir(path.dirname(destination), { recursive: true });
|
|
@@ -336,6 +477,7 @@ export async function scaffold(
|
|
|
336
477
|
language,
|
|
337
478
|
languageName: adapter.name,
|
|
338
479
|
runtime: adapter.runtime,
|
|
480
|
+
mode,
|
|
339
481
|
files: files.map((file) => file.relative).sort(),
|
|
340
482
|
runCommand: check,
|
|
341
483
|
checkCommand: check,
|
package/src/types.ts
CHANGED
|
@@ -49,12 +49,22 @@ export interface FileEntry {
|
|
|
49
49
|
contents: string;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** A file the agent authored for a multi-file project. */
|
|
53
|
+
export interface ProjectFile {
|
|
54
|
+
path: string;
|
|
55
|
+
content: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type PracticeMode = "single" | "project";
|
|
59
|
+
|
|
52
60
|
/**
|
|
53
61
|
* The contract the agent fills in after it has decided on a real-world scenario.
|
|
54
62
|
* None of these fields may reference LeetCode: `scaffold()` calls `assertNoLeak`
|
|
55
63
|
* before writing anything to disk.
|
|
56
64
|
*/
|
|
57
65
|
export interface ScaffoldSpec {
|
|
66
|
+
/** `single` = one JSON-in/out function. `project` = a multi-file app. */
|
|
67
|
+
mode?: PracticeMode;
|
|
58
68
|
/** kebab-case directory name. Derived from `title` when omitted. */
|
|
59
69
|
projectName?: string;
|
|
60
70
|
/** Human title of the *real-world* project, e.g. "Realtime Dedup Pipeline". */
|
|
@@ -69,7 +79,7 @@ export interface ScaffoldSpec {
|
|
|
69
79
|
functionName?: string;
|
|
70
80
|
requirements?: string[];
|
|
71
81
|
edgeCases?: string[];
|
|
72
|
-
/** Optional starter implementation. A TODO stub is generated when omitted. */
|
|
82
|
+
/** Optional starter implementation (single mode). A TODO stub is generated when omitted. */
|
|
73
83
|
starterCode?: string;
|
|
74
84
|
publicTests?: TestCase[];
|
|
75
85
|
hiddenTests?: TestCase[];
|
|
@@ -78,6 +88,16 @@ export interface ScaffoldSpec {
|
|
|
78
88
|
/** Internal provenance, stored in `.practice-meta.json` only. Never leaked. */
|
|
79
89
|
sourceSlug?: string;
|
|
80
90
|
sourceTitle?: string;
|
|
91
|
+
|
|
92
|
+
// --- project mode -------------------------------------------------------
|
|
93
|
+
/** The full file tree for a project-mode assignment. */
|
|
94
|
+
files?: ProjectFile[];
|
|
95
|
+
/** Markdown brief for the learner: what to implement and where. */
|
|
96
|
+
task?: string;
|
|
97
|
+
/** Command that runs one test case (JSON on stdin, JSON on stdout). */
|
|
98
|
+
runCommand?: string;
|
|
99
|
+
/** Optional one-time build command (compiled languages). */
|
|
100
|
+
buildCommand?: string;
|
|
81
101
|
}
|
|
82
102
|
|
|
83
103
|
export interface ScaffoldResult {
|
|
@@ -86,6 +106,7 @@ export interface ScaffoldResult {
|
|
|
86
106
|
language: LanguageId;
|
|
87
107
|
languageName: string;
|
|
88
108
|
runtime: string;
|
|
109
|
+
mode: PracticeMode;
|
|
89
110
|
files: string[];
|
|
90
111
|
runCommand: string;
|
|
91
112
|
checkCommand: string;
|