opencode-leetcode-realworld 0.1.0 → 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 +100 -6
- package/package.json +1 -1
- package/src/index.ts +14 -5
- package/src/languages.ts +358 -0
- package/src/patterns.ts +0 -1
- package/src/prompt.ts +53 -22
- package/src/scaffold.ts +173 -280
- package/src/types.ts +20 -6
package/README.md
CHANGED
|
@@ -15,6 +15,91 @@ and a test suite. The original problem is never shown to you.
|
|
|
15
15
|
/practice medium dynamic-programming python
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
/practice [options]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Options can be given in any order; the command figures out which is which.
|
|
25
|
+
|
|
26
|
+
### Options
|
|
27
|
+
|
|
28
|
+
| Option | Accepted values | Default | Description |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `difficulty` | `easy` \| `medium` \| `hard` | **asks you** (Medium suggested) | Difficulty of the source problem. |
|
|
31
|
+
| `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. |
|
|
32
|
+
| `<id-or-slug>` | A problem number or slug (e.g. `1`, `two-sum`) | random | Derive the assignment from one specific source problem. |
|
|
33
|
+
| `language` | A supported language or alias (see below) | **asks you** | Language of the generated project and tests. |
|
|
34
|
+
|
|
35
|
+
If you leave the language (or difficulty) out, opencode **asks you** with a
|
|
36
|
+
multiple-choice question before generating anything.
|
|
37
|
+
|
|
38
|
+
### Examples
|
|
39
|
+
|
|
40
|
+
| Command | What you get |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `/practice` | Asks for language + difficulty, then builds a random Medium problem. |
|
|
43
|
+
| `/practice easy` | Asks for the language, then an Easy problem. |
|
|
44
|
+
| `/practice hard graph` | Hard problem tagged Graph. |
|
|
45
|
+
| `/practice two-sum` | Asks for the language, then builds from the Two Sum problem. |
|
|
46
|
+
| `/practice medium dynamic-programming python` | Medium DP problem, Python project. |
|
|
47
|
+
| `/practice sliding-window js` | Random sliding-window problem in JavaScript. |
|
|
48
|
+
|
|
49
|
+
### Supported languages
|
|
50
|
+
|
|
51
|
+
| Language | Aliases | Runtime needed |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| TypeScript | `ts` | Node.js 22.6+ |
|
|
54
|
+
| JavaScript | `js`, `node` | Node.js 18+ |
|
|
55
|
+
| Python | `py`, `python3` | Python 3.8+ |
|
|
56
|
+
| Ruby | `rb` | Ruby 2.6+ |
|
|
57
|
+
| PHP | — | PHP 7.4+ |
|
|
58
|
+
| Go | `golang` | Go 1.18+ |
|
|
59
|
+
| Rust | `rs`, `cargo` | Rust 1.70+ (fetches `serde_json`) |
|
|
60
|
+
| C# | `cs`, `dotnet` | .NET 8 SDK |
|
|
61
|
+
| Swift | — | Swift 5.5+ |
|
|
62
|
+
|
|
63
|
+
Every language uses the same contract: implement `src/solution.<ext>`, read one
|
|
64
|
+
JSON value from stdin, write one JSON value to stdout. The shared runner verifies
|
|
65
|
+
any language identically.
|
|
66
|
+
|
|
67
|
+
### Topic tags you can practice
|
|
68
|
+
|
|
69
|
+
Any LeetCode topic tag works. Common ones and the kind of real-world scenario
|
|
70
|
+
they tend to produce:
|
|
71
|
+
|
|
72
|
+
| Tag | Typical generated domain |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `array` | batch audit-log processor |
|
|
75
|
+
| `string` | log sanitizer |
|
|
76
|
+
| `hash-table` | event deduplication pipeline |
|
|
77
|
+
| `two-pointers` | order-book matching engine |
|
|
78
|
+
| `sliding-window` | API rate limiter |
|
|
79
|
+
| `prefix-sum` | rolling revenue aggregator |
|
|
80
|
+
| `binary-search` | price-threshold alert engine |
|
|
81
|
+
| `sorting` | leaderboard ranker |
|
|
82
|
+
| `heap-priority-queue` | background job scheduler |
|
|
83
|
+
| `monotonic-stack` | histogram-based capacity planner |
|
|
84
|
+
| `stack` | config-rule expression evaluator |
|
|
85
|
+
| `queue` | async task pipeline |
|
|
86
|
+
| `greedy` | resource bin-packing scheduler |
|
|
87
|
+
| `dynamic-programming` | delivery route cost optimizer |
|
|
88
|
+
| `graph` | microservice dependency resolver |
|
|
89
|
+
| `depth-first-search` | permission inheritance resolver |
|
|
90
|
+
| `breadth-first-search` | service-mesh shortest-path router |
|
|
91
|
+
| `union-find` | account clustering for fraud rings |
|
|
92
|
+
| `topological-sort` | build-order planner |
|
|
93
|
+
| `trie` | autocomplete search index |
|
|
94
|
+
| `tree` | filesystem index builder |
|
|
95
|
+
| `linked-list` | LRU eviction chain |
|
|
96
|
+
| `backtracking` | deployment permutation explorer |
|
|
97
|
+
| `bit-manipulation` | feature-flag bitmask service |
|
|
98
|
+
| `segment-tree` | range-quota enforcement service |
|
|
99
|
+
| `design` | in-memory key-value store |
|
|
100
|
+
| `database` | analytics reconciliation job |
|
|
101
|
+
| `simulation` | tick-based match engine |
|
|
102
|
+
|
|
18
103
|
## How it works
|
|
19
104
|
|
|
20
105
|
```
|
|
@@ -35,13 +120,21 @@ The generated project looks like this:
|
|
|
35
120
|
.practice/<project>/
|
|
36
121
|
├── README.md # quickstart + how to run
|
|
37
122
|
├── PROJECT.md # scenario, requirements, definition of done
|
|
38
|
-
├──
|
|
39
|
-
├── src
|
|
40
|
-
├── tests/
|
|
41
|
-
├── tests/
|
|
123
|
+
├── harness.json # build + run commands for the chosen language
|
|
124
|
+
├── src/solution.<ext> # implement this (one JSON in, one JSON out)
|
|
125
|
+
├── tests/runner.mjs # language-agnostic test harness (Node)
|
|
126
|
+
├── tests/public/cases.json # visible test cases
|
|
127
|
+
├── tests/hidden/cases.json # extra acceptance cases (gitignored, guarded)
|
|
42
128
|
└── .practice-meta.json # provenance only (gitignored, guarded)
|
|
43
129
|
```
|
|
44
130
|
|
|
131
|
+
Run the checks with:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
node tests/runner.mjs # all cases
|
|
135
|
+
node tests/runner.mjs --public # visible cases only
|
|
136
|
+
```
|
|
137
|
+
|
|
45
138
|
## Install
|
|
46
139
|
|
|
47
140
|
### Local (development)
|
|
@@ -66,7 +159,7 @@ The plugin registers the `/practice` command itself, so no extra config is neede
|
|
|
66
159
|
| Tool | Purpose |
|
|
67
160
|
| --- | --- |
|
|
68
161
|
| `leetcode_fetch` | Fetch a problem by `mode` (`random` \| `specific` \| `daily` \| `filter`), `difficulty`, `tags`, `idOrSlug`. Skips premium problems. |
|
|
69
|
-
| `leetcode_scaffold` | Create the real-world project from a spec. Rejects specs that mention LeetCode, the source title, or its slug. |
|
|
162
|
+
| `leetcode_scaffold` | Create the real-world project from a spec in any supported language. Rejects specs that mention LeetCode, the source title, or its slug. |
|
|
70
163
|
|
|
71
164
|
## The honest-practice guard
|
|
72
165
|
|
|
@@ -106,8 +199,9 @@ command are resolved.
|
|
|
106
199
|
src/
|
|
107
200
|
index.ts plugin entry: tools, guard hooks, command registration
|
|
108
201
|
leetcode.ts API client + normalizer (handles endpoint shape differences)
|
|
202
|
+
languages.ts per-language adapters: starter, build, run commands
|
|
109
203
|
patterns.ts topic -> domain seeds, leak detection
|
|
110
|
-
scaffold.ts deterministic project writer (
|
|
204
|
+
scaffold.ts deterministic project writer (any supported language)
|
|
111
205
|
guard.ts protected-path detection
|
|
112
206
|
prompt.ts the /practice command template
|
|
113
207
|
types.ts shared types
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-leetcode-realworld",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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
|
@@ -75,11 +75,20 @@ export const LeetCodeRealWorld: Plugin = async ({ client, directory }) => {
|
|
|
75
75
|
projectName: tool.schema.string().optional().describe("kebab-case directory name"),
|
|
76
76
|
difficulty: tool.schema.string().optional(),
|
|
77
77
|
language: tool.schema
|
|
78
|
-
.enum([
|
|
78
|
+
.enum([
|
|
79
|
+
"typescript",
|
|
80
|
+
"javascript",
|
|
81
|
+
"python",
|
|
82
|
+
"ruby",
|
|
83
|
+
"php",
|
|
84
|
+
"go",
|
|
85
|
+
"rust",
|
|
86
|
+
"csharp",
|
|
87
|
+
"swift",
|
|
88
|
+
])
|
|
79
89
|
.default("typescript")
|
|
80
|
-
.describe("Target language"),
|
|
81
|
-
|
|
82
|
-
functionName: tool.schema.string().optional().describe("Exported entry function name"),
|
|
90
|
+
.describe("Target language for the practice project"),
|
|
91
|
+
functionName: tool.schema.string().optional().describe("Entry function name (default: solve)"),
|
|
83
92
|
requirements: tool.schema.array(tool.schema.string()).default([]).describe("Testable requirements"),
|
|
84
93
|
edgeCases: tool.schema.array(tool.schema.string()).default([]),
|
|
85
94
|
starterCode: tool.schema.string().optional().describe("Optional starter implementation"),
|
|
@@ -117,7 +126,7 @@ export const LeetCodeRealWorld: Plugin = async ({ client, directory }) => {
|
|
|
117
126
|
`Created a real-world practice project at ${relative}`,
|
|
118
127
|
"",
|
|
119
128
|
`Title: ${result.title}`,
|
|
120
|
-
`Language: ${result.
|
|
129
|
+
`Language: ${result.languageName} (${result.runtime})`,
|
|
121
130
|
`Files (${result.files.length}):`,
|
|
122
131
|
...result.files.map((file) => ` - ${file}`),
|
|
123
132
|
"",
|
package/src/languages.ts
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import type { FileEntry, LanguageId } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface LanguageContext {
|
|
4
|
+
projectName: string;
|
|
5
|
+
functionName: string;
|
|
6
|
+
title: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface LanguageAdapter {
|
|
10
|
+
id: LanguageId;
|
|
11
|
+
name: string;
|
|
12
|
+
aliases: string[];
|
|
13
|
+
extension: string;
|
|
14
|
+
/** Human hint shown in the generated README. */
|
|
15
|
+
runtime: string;
|
|
16
|
+
starter(ctx: LanguageContext): string;
|
|
17
|
+
/** Extra project files (build manifests etc). */
|
|
18
|
+
files(ctx: LanguageContext): FileEntry[];
|
|
19
|
+
/** Command run once before the cases, or null when not needed. */
|
|
20
|
+
build(ctx: LanguageContext): string | null;
|
|
21
|
+
/** Command that runs the solution once, reading one JSON value on stdin. */
|
|
22
|
+
run(ctx: LanguageContext): string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function packageJson(projectName: string, script: string): FileEntry {
|
|
26
|
+
return {
|
|
27
|
+
relative: "package.json",
|
|
28
|
+
contents:
|
|
29
|
+
JSON.stringify(
|
|
30
|
+
{
|
|
31
|
+
name: projectName,
|
|
32
|
+
version: "0.1.0",
|
|
33
|
+
private: true,
|
|
34
|
+
type: "module",
|
|
35
|
+
scripts: { test: script, check: script },
|
|
36
|
+
},
|
|
37
|
+
null,
|
|
38
|
+
2,
|
|
39
|
+
) + "\n",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const adapters: Record<LanguageId, LanguageAdapter> = {
|
|
44
|
+
typescript: {
|
|
45
|
+
id: "typescript",
|
|
46
|
+
name: "TypeScript",
|
|
47
|
+
aliases: ["ts"],
|
|
48
|
+
extension: "ts",
|
|
49
|
+
runtime: "Node.js 22.6+ (uses `--experimental-strip-types`)",
|
|
50
|
+
starter: ({ functionName }) => `import { readFileSync } from "node:fs";
|
|
51
|
+
|
|
52
|
+
function ${functionName}(payload: unknown): unknown {
|
|
53
|
+
throw new Error("Not implemented: ${functionName}");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const input = readFileSync(0, "utf8");
|
|
57
|
+
const payload = JSON.parse(input.trim() === "" ? "null" : input);
|
|
58
|
+
process.stdout.write(JSON.stringify(${functionName}(payload) ?? null));
|
|
59
|
+
`,
|
|
60
|
+
files: (ctx) => [
|
|
61
|
+
packageJson(ctx.projectName, "node tests/runner.mjs"),
|
|
62
|
+
{
|
|
63
|
+
relative: "tsconfig.json",
|
|
64
|
+
contents:
|
|
65
|
+
JSON.stringify(
|
|
66
|
+
{
|
|
67
|
+
compilerOptions: {
|
|
68
|
+
target: "ES2022",
|
|
69
|
+
module: "ESNext",
|
|
70
|
+
moduleResolution: "bundler",
|
|
71
|
+
strict: true,
|
|
72
|
+
skipLibCheck: true,
|
|
73
|
+
types: ["node"],
|
|
74
|
+
noEmit: true,
|
|
75
|
+
},
|
|
76
|
+
include: ["src"],
|
|
77
|
+
},
|
|
78
|
+
null,
|
|
79
|
+
2,
|
|
80
|
+
) + "\n",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
build: () => null,
|
|
84
|
+
run: ({ functionName }) => `node --experimental-strip-types src/solution.ts`,
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
javascript: {
|
|
88
|
+
id: "javascript",
|
|
89
|
+
name: "JavaScript",
|
|
90
|
+
aliases: ["js", "node"],
|
|
91
|
+
extension: "mjs",
|
|
92
|
+
runtime: "Node.js 18+",
|
|
93
|
+
starter: ({ functionName }) => `function ${functionName}(payload) {
|
|
94
|
+
throw new Error("Not implemented: ${functionName}");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const chunks = [];
|
|
98
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
99
|
+
const input = Buffer.concat(chunks).toString("utf8");
|
|
100
|
+
const payload = JSON.parse(input.trim() === "" ? "null" : input);
|
|
101
|
+
process.stdout.write(JSON.stringify(${functionName}(payload) ?? null));
|
|
102
|
+
`,
|
|
103
|
+
files: (ctx) => [packageJson(ctx.projectName, "node tests/runner.mjs")],
|
|
104
|
+
build: () => null,
|
|
105
|
+
run: () => "node src/solution.mjs",
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
python: {
|
|
109
|
+
id: "python",
|
|
110
|
+
name: "Python",
|
|
111
|
+
aliases: ["py", "python3"],
|
|
112
|
+
extension: "py",
|
|
113
|
+
runtime: "Python 3.8+",
|
|
114
|
+
starter: ({ functionName }) => `import json
|
|
115
|
+
import sys
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def ${functionName}(payload):
|
|
119
|
+
raise NotImplementedError("Not implemented: ${functionName}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def main():
|
|
123
|
+
raw = sys.stdin.read().strip()
|
|
124
|
+
payload = json.loads(raw) if raw else None
|
|
125
|
+
print(json.dumps(${functionName}(payload)))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
main()
|
|
130
|
+
`,
|
|
131
|
+
files: () => [],
|
|
132
|
+
build: () => null,
|
|
133
|
+
run: () => "python3 src/solution.py",
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
ruby: {
|
|
137
|
+
id: "ruby",
|
|
138
|
+
name: "Ruby",
|
|
139
|
+
aliases: ["rb"],
|
|
140
|
+
extension: "rb",
|
|
141
|
+
runtime: "Ruby 2.6+ (json is in the standard library)",
|
|
142
|
+
starter: ({ functionName }) => `require "json"
|
|
143
|
+
|
|
144
|
+
def ${functionName}(payload)
|
|
145
|
+
raise NotImplementedError, "Not implemented: ${functionName}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
raw = $stdin.read.strip
|
|
149
|
+
payload = raw.empty? ? nil : JSON.parse(raw)
|
|
150
|
+
puts JSON.generate(${functionName}(payload))
|
|
151
|
+
`,
|
|
152
|
+
files: () => [],
|
|
153
|
+
build: () => null,
|
|
154
|
+
run: () => "ruby src/solution.rb",
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
php: {
|
|
158
|
+
id: "php",
|
|
159
|
+
name: "PHP",
|
|
160
|
+
aliases: ["php8"],
|
|
161
|
+
extension: "php",
|
|
162
|
+
runtime: "PHP 7.4+",
|
|
163
|
+
starter: ({ functionName }) => `<?php
|
|
164
|
+
|
|
165
|
+
function ${functionName}($payload) {
|
|
166
|
+
throw new Exception("Not implemented: ${functionName}");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
$raw = trim(stream_get_contents(STDIN));
|
|
170
|
+
$payload = $raw === "" ? null : json_decode($raw, true);
|
|
171
|
+
echo json_encode(${functionName}($payload));
|
|
172
|
+
`,
|
|
173
|
+
files: () => [],
|
|
174
|
+
build: () => null,
|
|
175
|
+
run: () => "php src/solution.php",
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
go: {
|
|
179
|
+
id: "go",
|
|
180
|
+
name: "Go",
|
|
181
|
+
aliases: ["golang"],
|
|
182
|
+
extension: "go",
|
|
183
|
+
runtime: "Go 1.18+ (uses `any`)",
|
|
184
|
+
starter: ({ functionName }) => `package main
|
|
185
|
+
|
|
186
|
+
import (
|
|
187
|
+
"encoding/json"
|
|
188
|
+
"fmt"
|
|
189
|
+
"io"
|
|
190
|
+
"os"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
func ${functionName}(payload any) any {
|
|
194
|
+
panic("Not implemented: ${functionName}")
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func main() {
|
|
198
|
+
raw, _ := io.ReadAll(os.Stdin)
|
|
199
|
+
var payload any
|
|
200
|
+
if len(raw) > 0 {
|
|
201
|
+
if err := json.Unmarshal(raw, &payload); err != nil {
|
|
202
|
+
panic(err)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
out, err := json.Marshal(${functionName}(payload))
|
|
206
|
+
if err != nil {
|
|
207
|
+
panic(err)
|
|
208
|
+
}
|
|
209
|
+
fmt.Print(string(out))
|
|
210
|
+
}
|
|
211
|
+
`,
|
|
212
|
+
files: () => [
|
|
213
|
+
{ relative: "go.mod", contents: "module practice\n\ngo 1.21\n" },
|
|
214
|
+
],
|
|
215
|
+
build: () => "mkdir -p out && go build -o out/solution src/solution.go",
|
|
216
|
+
run: () => "./out/solution",
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
rust: {
|
|
220
|
+
id: "rust",
|
|
221
|
+
name: "Rust",
|
|
222
|
+
aliases: ["rs", "cargo"],
|
|
223
|
+
extension: "rs",
|
|
224
|
+
runtime: "Rust 1.70+ (uses serde_json)",
|
|
225
|
+
starter: ({ functionName }) => `use serde_json::Value;
|
|
226
|
+
use std::io::Read;
|
|
227
|
+
|
|
228
|
+
fn ${functionName}(payload: Value) -> Value {
|
|
229
|
+
let _ = payload;
|
|
230
|
+
panic!("Not implemented: ${functionName}");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
fn main() {
|
|
234
|
+
let mut raw = String::new();
|
|
235
|
+
std::io::stdin().read_to_string(&mut raw).expect("read stdin");
|
|
236
|
+
let trimmed = raw.trim();
|
|
237
|
+
let payload: Value = if trimmed.is_empty() {
|
|
238
|
+
Value::Null
|
|
239
|
+
} else {
|
|
240
|
+
serde_json::from_str(trimmed).expect("parse json")
|
|
241
|
+
};
|
|
242
|
+
let result = ${functionName}(payload);
|
|
243
|
+
println!("{}", serde_json::to_string(&result).expect("serialize json"));
|
|
244
|
+
}
|
|
245
|
+
`,
|
|
246
|
+
files: (ctx) => [
|
|
247
|
+
{
|
|
248
|
+
relative: "Cargo.toml",
|
|
249
|
+
contents: `[package]
|
|
250
|
+
name = "${ctx.projectName}"
|
|
251
|
+
version = "0.1.0"
|
|
252
|
+
edition = "2021"
|
|
253
|
+
|
|
254
|
+
[dependencies]
|
|
255
|
+
serde_json = "1"
|
|
256
|
+
|
|
257
|
+
[[bin]]
|
|
258
|
+
name = "solution"
|
|
259
|
+
path = "src/solution.rs"
|
|
260
|
+
`,
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
build: () => "cargo build --quiet",
|
|
264
|
+
run: () => "./target/debug/solution",
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
csharp: {
|
|
268
|
+
id: "csharp",
|
|
269
|
+
name: "C#",
|
|
270
|
+
aliases: ["cs", "dotnet"],
|
|
271
|
+
extension: "cs",
|
|
272
|
+
runtime: ".NET 8 SDK",
|
|
273
|
+
starter: ({ functionName }) => `using System;
|
|
274
|
+
using System.Text.Json.Nodes;
|
|
275
|
+
|
|
276
|
+
class Program
|
|
277
|
+
{
|
|
278
|
+
static JsonNode? ${functionName}(JsonNode? payload)
|
|
279
|
+
{
|
|
280
|
+
throw new NotImplementedException("Not implemented: ${functionName}");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
static void Main()
|
|
284
|
+
{
|
|
285
|
+
var raw = Console.In.ReadToEnd().Trim();
|
|
286
|
+
var payload = raw.Length == 0 ? null : JsonNode.Parse(raw);
|
|
287
|
+
Console.Write(${functionName}(payload)?.ToJsonString() ?? "null");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
`,
|
|
291
|
+
files: (ctx) => [
|
|
292
|
+
{
|
|
293
|
+
relative: `${ctx.projectName}.csproj`,
|
|
294
|
+
contents: `<Project Sdk="Microsoft.NET.Sdk">
|
|
295
|
+
<PropertyGroup>
|
|
296
|
+
<OutputType>Exe</OutputType>
|
|
297
|
+
<TargetFramework>net8.0</TargetFramework>
|
|
298
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
299
|
+
<Nullable>enable</Nullable>
|
|
300
|
+
<AssemblyName>solution</AssemblyName>
|
|
301
|
+
</PropertyGroup>
|
|
302
|
+
</Project>
|
|
303
|
+
`,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
build: () => "dotnet build -c Release -o out --nologo -v quiet",
|
|
307
|
+
run: () => "dotnet out/solution.dll",
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
swift: {
|
|
311
|
+
id: "swift",
|
|
312
|
+
name: "Swift",
|
|
313
|
+
aliases: [],
|
|
314
|
+
extension: "swift",
|
|
315
|
+
runtime: "Swift 5.5+ (Foundation JSONSerialization)",
|
|
316
|
+
starter: ({ functionName }) => `import Foundation
|
|
317
|
+
|
|
318
|
+
func ${functionName}(_ payload: Any) -> Any {
|
|
319
|
+
fatalError("Not implemented: ${functionName}")
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let raw = String(data: FileHandle.standardInput.readDataToEndOfFile(), encoding: .utf8) ?? ""
|
|
323
|
+
let data = raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
324
|
+
? Data("null".utf8)
|
|
325
|
+
: Data(raw.utf8)
|
|
326
|
+
let payload = try! JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed])
|
|
327
|
+
let result = ${functionName}(payload)
|
|
328
|
+
let out = try! JSONSerialization.data(withJSONObject: result, options: [.fragmentsAllowed])
|
|
329
|
+
FileHandle.standardOutput.write(out)
|
|
330
|
+
`,
|
|
331
|
+
files: () => [],
|
|
332
|
+
build: () => "mkdir -p out && swiftc -O src/solution.swift -o out/solution",
|
|
333
|
+
run: () => "./out/solution",
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
export function getLanguage(id: LanguageId): LanguageAdapter {
|
|
338
|
+
return adapters[id];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function languageIds(): LanguageId[] {
|
|
342
|
+
return Object.keys(adapters) as LanguageId[];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function languageNames(): string {
|
|
346
|
+
return languageIds()
|
|
347
|
+
.map((id) => adapters[id].name)
|
|
348
|
+
.join(", ");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function normalizeLanguage(input: string | undefined): LanguageId | undefined {
|
|
352
|
+
if (!input) return undefined;
|
|
353
|
+
const key = input.trim().toLowerCase();
|
|
354
|
+
for (const adapter of Object.values(adapters)) {
|
|
355
|
+
if (adapter.id === key || adapter.aliases.includes(key)) return adapter.id;
|
|
356
|
+
}
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
package/src/patterns.ts
CHANGED
|
@@ -91,7 +91,6 @@ function textsFromSpec(spec: ScaffoldSpec): Array<[string, string]> {
|
|
|
91
91
|
push("projectName", spec.projectName);
|
|
92
92
|
push("scenario", spec.scenario);
|
|
93
93
|
push("pattern", spec.pattern);
|
|
94
|
-
push("module", spec.module);
|
|
95
94
|
push("functionName", spec.functionName);
|
|
96
95
|
push("starterCode", spec.starterCode);
|
|
97
96
|
(spec.requirements ?? []).forEach((req, i) => push(`requirements[${i}]`, req));
|
package/src/prompt.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
export const PRACTICE_DESCRIPTION =
|
|
2
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
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
|
+
|
|
4
16
|
/**
|
|
5
17
|
* The `/practice` command prompt. It orchestrates the two plugin tools and is
|
|
6
18
|
* deliberately opinionated: the whole value of this plugin is that the learner
|
|
@@ -19,58 +31,77 @@ Recognise any of these (all optional):
|
|
|
19
31
|
- difficulty: easy | medium | hard
|
|
20
32
|
- tags: comma-separated topic tags, e.g. "dynamic-programming,graph"
|
|
21
33
|
- a specific problem id or slug, e.g. "two-sum" or "1"
|
|
22
|
-
- language:
|
|
23
|
-
|
|
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.
|
|
24
45
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
for YOUR reasoning only. Never paste it, paraphrase it closely, or name its source
|
|
28
|
-
anywhere in the generated project.
|
|
46
|
+
If the user did specify values, skip the corresponding question. Only ask at all
|
|
47
|
+
when something is missing.
|
|
29
48
|
|
|
30
|
-
## 3.
|
|
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
|
|
31
55
|
Identify the underlying algorithmic idea (e.g. sliding window, heap-based
|
|
32
56
|
scheduling, union-find, DP over intervals). Do not carry over the problem's
|
|
33
57
|
fiction (arrays of "nums", "target", etc.).
|
|
34
58
|
|
|
35
|
-
##
|
|
59
|
+
## 5. Invent a real engineering scenario
|
|
36
60
|
Design a plausible product/business task that genuinely needs that principle. Good
|
|
37
61
|
domains: observability pipelines, payments/ledgering, logistics, rate limiting,
|
|
38
62
|
access control, search, feature flags, data reconciliation, scheduling.
|
|
39
63
|
The scenario must read like a ticket from a real team: who needs it, why, and what
|
|
40
|
-
"correct" means.
|
|
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.
|
|
41
71
|
|
|
42
|
-
Design the assignment so a single exported function is the contract:
|
|
43
|
-
- It receives one JSON-shaped \`payload\` value.
|
|
44
|
-
- It returns one JSON-shaped result.
|
|
45
72
|
Call the tool \`leetcode_scaffold\` with a spec containing:
|
|
46
73
|
- title: real-world project title (no coding-practice words)
|
|
47
74
|
- scenario: 1-3 paragraphs of business context
|
|
48
75
|
- pattern: the underlying principle (internal only)
|
|
49
|
-
- language:
|
|
76
|
+
- language: the language the user chose (use the canonical id, e.g. "typescript",
|
|
77
|
+
"javascript", "python", "ruby", "php", "go", "rust", "csharp", "swift")
|
|
50
78
|
- requirements: concrete, testable bullet points
|
|
51
79
|
- edgeCases: tricky situations the solution must handle
|
|
52
80
|
- starterCode: optional; otherwise a TODO stub is generated
|
|
53
|
-
- publicTests:
|
|
54
|
-
- hiddenTests:
|
|
55
|
-
|
|
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)
|
|
56
85
|
|
|
57
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.
|
|
58
89
|
|
|
59
|
-
##
|
|
90
|
+
## 6. Rules (non-negotiable)
|
|
60
91
|
- The generated title, scenario, requirements, comments, and test names MUST NOT
|
|
61
92
|
mention LeetCode, its title, its slug, or any coding-practice site. The
|
|
62
93
|
\`leetcode_scaffold\` tool will reject the spec if it does.
|
|
63
94
|
- DO NOT implement the solution. Your job ends when the scaffold is written. The
|
|
64
|
-
learner implements
|
|
95
|
+
learner implements the solution file themselves.
|
|
65
96
|
- Do not reveal the fetched problem or the original examples.
|
|
66
97
|
- If the tool reports a leak, rewrite the offending fields and retry.
|
|
67
98
|
|
|
68
|
-
##
|
|
99
|
+
## 7. After scaffolding
|
|
69
100
|
Report back with, in order:
|
|
70
101
|
1. One short paragraph describing the assignment as a real task (no source spoilers).
|
|
71
|
-
2. The project path
|
|
72
|
-
3. The exact
|
|
73
|
-
4. Note that \`tests/hidden\` holds extra acceptance
|
|
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.
|
|
74
105
|
Then stop. If the learner asks for help, give guiding hints and ask questions
|
|
75
106
|
rather than writing the algorithm for them.
|
|
76
107
|
`;
|
package/src/scaffold.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { getLanguage, normalizeLanguage } from "./languages";
|
|
3
4
|
import { assertNoLeak } from "./patterns";
|
|
4
|
-
import type {
|
|
5
|
+
import type { FileEntry, LanguageId, ScaffoldResult, ScaffoldSpec, TestCase } from "./types";
|
|
5
6
|
|
|
6
7
|
function kebab(input: string): string {
|
|
7
8
|
return input
|
|
@@ -18,101 +19,157 @@ function camel(input: string): string {
|
|
|
18
19
|
.join("");
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
function snake(input: string): string {
|
|
22
|
-
return kebab(input).replace(/-/g, "_");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function pascal(input: string): string {
|
|
26
|
-
const value = camel(input);
|
|
27
|
-
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
22
|
interface ResolvedNames {
|
|
31
23
|
projectName: string;
|
|
32
|
-
module: string;
|
|
33
24
|
functionName: string;
|
|
34
25
|
title: string;
|
|
35
26
|
}
|
|
36
27
|
|
|
37
28
|
function resolveNames(spec: ScaffoldSpec): ResolvedNames {
|
|
38
29
|
const projectName = kebab(spec.projectName ?? spec.title) || "practice-project";
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
const rawFunction = spec.functionName ?? (camel(module) || "solve");
|
|
42
|
-
const functionName = language === "python" ? snake(rawFunction) : camel(rawFunction);
|
|
43
|
-
return { projectName, module, functionName, title: spec.title };
|
|
30
|
+
const functionName = spec.functionName?.trim() || "solve";
|
|
31
|
+
return { projectName, functionName, title: spec.title };
|
|
44
32
|
}
|
|
45
33
|
|
|
46
34
|
function json(value: unknown, indent = 2): string {
|
|
47
35
|
return JSON.stringify(value, null, indent);
|
|
48
36
|
}
|
|
49
37
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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;
|
|
61
54
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 : ""));
|
|
69
74
|
}
|
|
70
|
-
|
|
75
|
+
const text = (result.stdout || "").trim();
|
|
76
|
+
return text === "" ? null : JSON.parse(text);
|
|
71
77
|
}
|
|
72
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
|
+
|
|
73
121
|
function requirementsSection(spec: ScaffoldSpec): string {
|
|
74
122
|
const requirements = spec.requirements ?? [];
|
|
75
123
|
if (requirements.length === 0) return "";
|
|
76
|
-
|
|
77
|
-
return `## Requirements\n\n${lines.join("\n")}\n`;
|
|
124
|
+
return `## Requirements\n\n${requirements.map((req, i) => `${i + 1}. ${req}`).join("\n")}\n`;
|
|
78
125
|
}
|
|
79
126
|
|
|
80
127
|
function edgeCaseSection(spec: ScaffoldSpec): string {
|
|
81
128
|
const edgeCases = spec.edgeCases ?? [];
|
|
82
129
|
if (edgeCases.length === 0) return "";
|
|
83
|
-
|
|
84
|
-
return `## Edge cases to handle\n\n${lines.join("\n")}\n`;
|
|
130
|
+
return `## Edge cases to handle\n\n${edgeCases.map((edge) => `- ${edge}`).join("\n")}\n`;
|
|
85
131
|
}
|
|
86
132
|
|
|
87
|
-
function readmeFor(names: ResolvedNames, language:
|
|
88
|
-
const
|
|
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;
|
|
89
138
|
return `# ${names.title}
|
|
90
139
|
|
|
91
140
|
${names.title} is a self-contained engineering exercise. Read \`PROJECT.md\` for the
|
|
92
|
-
|
|
93
|
-
and make the test suite pass.
|
|
141
|
+
brief, implement the entry point in \`${source}\`, and make the test suite pass.
|
|
94
142
|
|
|
95
|
-
##
|
|
143
|
+
## Run the tests
|
|
96
144
|
|
|
97
145
|
\`\`\`bash
|
|
98
|
-
|
|
146
|
+
node tests/runner.mjs
|
|
99
147
|
\`\`\`
|
|
100
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
|
+
|
|
101
155
|
## Layout
|
|
102
156
|
|
|
103
157
|
\`\`\`
|
|
104
|
-
${
|
|
105
|
-
tests/public/
|
|
106
|
-
tests/hidden/
|
|
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
|
|
107
163
|
PROJECT.md # scenario, requirements, definition of done
|
|
108
164
|
\`\`\`
|
|
109
165
|
|
|
110
|
-
|
|
111
|
-
|
|
166
|
+
Prefer clarity over cleverness: the goal is production-quality code, not the
|
|
167
|
+
shortest possible snippet.
|
|
112
168
|
`;
|
|
113
169
|
}
|
|
114
170
|
|
|
115
|
-
function projectDoc(spec: ScaffoldSpec, names: ResolvedNames): string {
|
|
171
|
+
function projectDoc(spec: ScaffoldSpec, names: ResolvedNames, language: LanguageId): string {
|
|
172
|
+
const adapter = getLanguage(language);
|
|
116
173
|
return `# ${names.title}
|
|
117
174
|
|
|
118
175
|
## Scenario
|
|
@@ -121,252 +178,79 @@ ${spec.scenario.trim()}
|
|
|
121
178
|
|
|
122
179
|
${requirementsSection(spec)}
|
|
123
180
|
${edgeCaseSection(spec)}
|
|
124
|
-
##
|
|
125
|
-
|
|
126
|
-
- The entry point is implemented and exported as documented.
|
|
127
|
-
- The full test suite passes: \`check\` command in the README.
|
|
128
|
-
- No external service calls are required; input arrives as a plain value and the result is returned.
|
|
129
|
-
- Handle the edge cases above rather than only the happy path.
|
|
130
|
-
`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function starterFor(names: ResolvedNames, spec: ScaffoldSpec, language: PracticeLanguage): string {
|
|
134
|
-
if (spec.starterCode && spec.starterCode.trim().length > 0) return spec.starterCode.trimEnd() + "\n";
|
|
135
|
-
const summary = spec.scenario.trim().split(/\n/)[0] ?? "";
|
|
136
|
-
if (language === "python") {
|
|
137
|
-
return `"""${names.title}.
|
|
138
|
-
|
|
139
|
-
${summary}
|
|
140
|
-
"""
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def ${names.functionName}(payload):
|
|
144
|
-
"""Implement the behaviour described in PROJECT.md."""
|
|
145
|
-
raise NotImplementedError("${names.functionName} is not implemented yet")
|
|
146
|
-
`;
|
|
147
|
-
}
|
|
148
|
-
const doc = `/**\n * ${names.title}\n *\n * ${summary}\n */`;
|
|
149
|
-
if (language === "javascript") {
|
|
150
|
-
return `${doc}\nexport function ${names.functionName}(payload) {\n throw new Error("${names.functionName} is not implemented yet");\n}\n`;
|
|
151
|
-
}
|
|
152
|
-
const signature = `export function ${names.functionName}(payload: unknown): unknown {`;
|
|
153
|
-
return `${doc}\n${signature}\n throw new Error("${names.functionName} is not implemented yet");\n}\n`;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function testCasesArray(cases: TestCase[]): string {
|
|
157
|
-
return json(cases, 2);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function publicTestFor(
|
|
161
|
-
names: ResolvedNames,
|
|
162
|
-
spec: ScaffoldSpec,
|
|
163
|
-
language: PracticeLanguage,
|
|
164
|
-
): string {
|
|
165
|
-
const cases = spec.publicTests ?? [];
|
|
166
|
-
return testFile(names, cases, language, "public");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function hiddenTestFor(
|
|
170
|
-
names: ResolvedNames,
|
|
171
|
-
spec: ScaffoldSpec,
|
|
172
|
-
language: PracticeLanguage,
|
|
173
|
-
): string {
|
|
174
|
-
const cases = spec.hiddenTests ?? [];
|
|
175
|
-
return testFile(names, cases, language, "hidden");
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function testFile(
|
|
179
|
-
names: ResolvedNames,
|
|
180
|
-
cases: TestCase[],
|
|
181
|
-
language: PracticeLanguage,
|
|
182
|
-
kind: "public" | "hidden",
|
|
183
|
-
): string {
|
|
184
|
-
const title = `${names.title} (${kind} acceptance)`;
|
|
185
|
-
if (language === "python") {
|
|
186
|
-
return `import pytest
|
|
181
|
+
## Interface
|
|
187
182
|
|
|
188
|
-
|
|
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).
|
|
189
186
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
@pytest.mark.parametrize("case", CASES, ids=lambda case: case["name"])
|
|
194
|
-
def test_${names.functionName}(case):
|
|
195
|
-
assert ${names.functionName}(case["input"]) == case["expected"]
|
|
196
|
-
`;
|
|
197
|
-
}
|
|
198
|
-
const casesLiteral = testCasesArray(cases);
|
|
199
|
-
if (language === "javascript") {
|
|
200
|
-
return `import test from "node:test";
|
|
201
|
-
import assert from "node:assert/strict";
|
|
202
|
-
import { ${names.functionName} } from "../../src/${names.module}.js";
|
|
203
|
-
|
|
204
|
-
// ${title}
|
|
205
|
-
const cases = ${casesLiteral};
|
|
206
|
-
|
|
207
|
-
for (const testCase of cases) {
|
|
208
|
-
test(testCase.name, () => {
|
|
209
|
-
assert.deepStrictEqual(${names.functionName}(testCase.input), testCase.expected);
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
`;
|
|
213
|
-
}
|
|
214
|
-
return `import { describe, expect, it } from "vitest";
|
|
215
|
-
import { ${names.functionName} } from "../../src/${names.module}";
|
|
216
|
-
|
|
217
|
-
// ${title}
|
|
218
|
-
const cases: Array<{ name: string; input: unknown; expected: unknown }> = ${casesLiteral};
|
|
187
|
+
## Definition of done
|
|
219
188
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
expect(${names.functionName}(testCase.input)).toEqual(testCase.expected);
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
});
|
|
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.
|
|
227
192
|
`;
|
|
228
193
|
}
|
|
229
194
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
install: string;
|
|
237
|
-
check: string;
|
|
238
|
-
run: string;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function runCommands(language: PracticeLanguage): RunCommands {
|
|
242
|
-
if (language === "python") {
|
|
243
|
-
return {
|
|
244
|
-
install: "python -m pip install -r requirements.txt",
|
|
245
|
-
check: "python -m pytest -q",
|
|
246
|
-
run: "python -m pytest -q",
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
if (language === "javascript") {
|
|
250
|
-
return { install: "npm install", check: "node --test", run: "node --test" };
|
|
251
|
-
}
|
|
252
|
-
return { install: "npm install", check: "npm test", run: "npm test" };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function packageJsonFor(
|
|
256
|
-
names: ResolvedNames,
|
|
257
|
-
language: PracticeLanguage,
|
|
258
|
-
): string {
|
|
259
|
-
if (language === "javascript") {
|
|
260
|
-
return (
|
|
261
|
-
json(
|
|
262
|
-
{
|
|
263
|
-
name: names.projectName,
|
|
264
|
-
version: "0.1.0",
|
|
265
|
-
private: true,
|
|
266
|
-
type: "module",
|
|
267
|
-
scripts: { test: "node --test", check: "node --test" },
|
|
268
|
-
},
|
|
269
|
-
2,
|
|
270
|
-
) + "\n"
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
return (
|
|
274
|
-
json(
|
|
275
|
-
{
|
|
276
|
-
name: names.projectName,
|
|
277
|
-
version: "0.1.0",
|
|
278
|
-
private: true,
|
|
279
|
-
type: "module",
|
|
280
|
-
scripts: { test: "vitest run", check: "vitest run" },
|
|
281
|
-
devDependencies: { "@types/node": "^22.13.9", typescript: "^5.8.2", vitest: "^3.0.0" },
|
|
282
|
-
},
|
|
283
|
-
2,
|
|
284
|
-
) + "\n"
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function tsconfigFor(): string {
|
|
289
|
-
return (
|
|
290
|
-
json(
|
|
291
|
-
{
|
|
292
|
-
compilerOptions: {
|
|
293
|
-
target: "ES2022",
|
|
294
|
-
module: "ESNext",
|
|
295
|
-
moduleResolution: "bundler",
|
|
296
|
-
strict: true,
|
|
297
|
-
skipLibCheck: true,
|
|
298
|
-
types: ["node"],
|
|
299
|
-
noEmit: true,
|
|
300
|
-
},
|
|
301
|
-
include: ["src", "tests"],
|
|
302
|
-
},
|
|
303
|
-
2,
|
|
304
|
-
) + "\n"
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const PROJECT_GITIGNORE = `node_modules/
|
|
195
|
+
const PROJECT_GITIGNORE = `# build artifacts
|
|
196
|
+
out/
|
|
197
|
+
target/
|
|
198
|
+
bin/
|
|
199
|
+
obj/
|
|
200
|
+
node_modules/
|
|
309
201
|
__pycache__/
|
|
202
|
+
*.pyc
|
|
310
203
|
.pytest_cache/
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
# Acceptance tests are meant to stay local; don't commit the oracle.
|
|
204
|
+
|
|
205
|
+
# acceptance tests + provenance stay local
|
|
314
206
|
tests/hidden/
|
|
315
207
|
.practice-meta.json
|
|
316
208
|
`;
|
|
317
209
|
|
|
318
|
-
function metaFor(spec: ScaffoldSpec, names: ResolvedNames, language:
|
|
210
|
+
function metaFor(spec: ScaffoldSpec, names: ResolvedNames, language: LanguageId): string {
|
|
319
211
|
return (
|
|
320
|
-
json(
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
slug: spec.sourceSlug ?? null,
|
|
330
|
-
title: spec.sourceTitle ?? null,
|
|
331
|
-
},
|
|
332
|
-
generatedAt: new Date().toISOString(),
|
|
333
|
-
},
|
|
334
|
-
2,
|
|
335
|
-
) + "\n"
|
|
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"
|
|
336
221
|
);
|
|
337
222
|
}
|
|
338
223
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
contents: string;
|
|
224
|
+
function casesFile(cases: TestCase[]): string {
|
|
225
|
+
return json(cases) + "\n";
|
|
342
226
|
}
|
|
343
227
|
|
|
344
|
-
function buildFiles(spec: ScaffoldSpec, names: ResolvedNames): FileEntry[] {
|
|
345
|
-
const
|
|
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
|
+
|
|
346
232
|
const files: FileEntry[] = [
|
|
347
|
-
{ relative: "README.md", contents: readmeFor(names, language) },
|
|
348
|
-
{ relative: "PROJECT.md", contents: projectDoc(spec, names) },
|
|
233
|
+
{ relative: "README.md", contents: readmeFor(names, spec, language) },
|
|
234
|
+
{ relative: "PROJECT.md", contents: projectDoc(spec, names, language) },
|
|
349
235
|
{ relative: ".gitignore", contents: PROJECT_GITIGNORE },
|
|
350
236
|
{ relative: ".practice-meta.json", contents: metaFor(spec, names, language) },
|
|
351
|
-
{
|
|
352
|
-
|
|
353
|
-
|
|
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
|
+
},
|
|
354
251
|
];
|
|
355
252
|
|
|
356
|
-
|
|
357
|
-
files.push(
|
|
358
|
-
{ relative: "requirements.txt", contents: "pytest>=8.0\n" },
|
|
359
|
-
{ relative: "conftest.py", contents: "import sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent))\n" },
|
|
360
|
-
{ relative: "src/__init__.py", contents: "" },
|
|
361
|
-
{ relative: "tests/__init__.py", contents: "" },
|
|
362
|
-
{ relative: "tests/public/__init__.py", contents: "" },
|
|
363
|
-
{ relative: "tests/hidden/__init__.py", contents: "" },
|
|
364
|
-
);
|
|
365
|
-
} else {
|
|
366
|
-
files.push({ relative: "package.json", contents: packageJsonFor(names, language) });
|
|
367
|
-
if (language === "typescript") files.push({ relative: "tsconfig.json", contents: tsconfigFor() });
|
|
368
|
-
}
|
|
369
|
-
|
|
253
|
+
files.push(...adapter.files(ctx));
|
|
370
254
|
return files;
|
|
371
255
|
}
|
|
372
256
|
|
|
@@ -379,7 +263,13 @@ export async function scaffold(
|
|
|
379
263
|
spec: ScaffoldSpec,
|
|
380
264
|
options: ScaffoldOptions,
|
|
381
265
|
): Promise<ScaffoldResult> {
|
|
382
|
-
const language = spec.language ?? "typescript";
|
|
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
|
+
|
|
383
273
|
const normalized: ScaffoldSpec = {
|
|
384
274
|
...spec,
|
|
385
275
|
language,
|
|
@@ -391,27 +281,30 @@ export async function scaffold(
|
|
|
391
281
|
|
|
392
282
|
assertNoLeak(normalized);
|
|
393
283
|
|
|
284
|
+
const adapter = getLanguage(language);
|
|
394
285
|
const names = resolveNames(normalized);
|
|
395
286
|
const target = normalized.outDir
|
|
396
287
|
? path.resolve(options.directory, normalized.outDir)
|
|
397
288
|
: path.resolve(options.directory, ".practice", names.projectName);
|
|
398
289
|
|
|
399
|
-
const files = buildFiles(normalized, names);
|
|
290
|
+
const files = buildFiles(normalized, names, language);
|
|
400
291
|
for (const file of files) {
|
|
401
292
|
const destination = path.join(target, file.relative);
|
|
402
293
|
await mkdir(path.dirname(destination), { recursive: true });
|
|
403
294
|
await writeFile(destination, file.contents, "utf8");
|
|
404
295
|
}
|
|
405
296
|
|
|
406
|
-
const
|
|
297
|
+
const check = "node tests/runner.mjs";
|
|
407
298
|
return {
|
|
408
299
|
outDir: target,
|
|
409
300
|
title: names.title,
|
|
410
301
|
language,
|
|
302
|
+
languageName: adapter.name,
|
|
303
|
+
runtime: adapter.runtime,
|
|
411
304
|
files: files.map((file) => file.relative).sort(),
|
|
412
|
-
runCommand:
|
|
413
|
-
checkCommand:
|
|
305
|
+
runCommand: check,
|
|
306
|
+
checkCommand: check,
|
|
414
307
|
};
|
|
415
308
|
}
|
|
416
309
|
|
|
417
|
-
export { resolveNames, kebab, camel
|
|
310
|
+
export { resolveNames, kebab, camel };
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type LanguageId =
|
|
2
|
+
| "typescript"
|
|
3
|
+
| "javascript"
|
|
4
|
+
| "python"
|
|
5
|
+
| "ruby"
|
|
6
|
+
| "php"
|
|
7
|
+
| "go"
|
|
8
|
+
| "rust"
|
|
9
|
+
| "csharp"
|
|
10
|
+
| "swift";
|
|
2
11
|
|
|
3
12
|
export interface CodeSnippet {
|
|
4
13
|
lang: string;
|
|
@@ -35,6 +44,11 @@ export interface TestCase {
|
|
|
35
44
|
expected: unknown;
|
|
36
45
|
}
|
|
37
46
|
|
|
47
|
+
export interface FileEntry {
|
|
48
|
+
relative: string;
|
|
49
|
+
contents: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
/**
|
|
39
53
|
* The contract the agent fills in after it has decided on a real-world scenario.
|
|
40
54
|
* None of these fields may reference LeetCode: `scaffold()` calls `assertNoLeak`
|
|
@@ -50,10 +64,8 @@ export interface ScaffoldSpec {
|
|
|
50
64
|
/** Underlying algorithmic principle, e.g. "sliding window". Internal only. */
|
|
51
65
|
pattern: string;
|
|
52
66
|
difficulty?: string;
|
|
53
|
-
language?:
|
|
54
|
-
/**
|
|
55
|
-
module?: string;
|
|
56
|
-
/** Exported entry function, e.g. "dedupe". Derived from module when omitted. */
|
|
67
|
+
language?: LanguageId;
|
|
68
|
+
/** Name of the entry function inside the solution file. Defaults to `solve`. */
|
|
57
69
|
functionName?: string;
|
|
58
70
|
requirements?: string[];
|
|
59
71
|
edgeCases?: string[];
|
|
@@ -71,7 +83,9 @@ export interface ScaffoldSpec {
|
|
|
71
83
|
export interface ScaffoldResult {
|
|
72
84
|
outDir: string;
|
|
73
85
|
title: string;
|
|
74
|
-
language:
|
|
86
|
+
language: LanguageId;
|
|
87
|
+
languageName: string;
|
|
88
|
+
runtime: string;
|
|
75
89
|
files: string[];
|
|
76
90
|
runCommand: string;
|
|
77
91
|
checkCommand: string;
|