opencode-leetcode-realworld 0.2.2 → 0.3.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 +38 -0
- package/package.json +1 -1
- package/src/index.ts +6 -1
- package/src/languages.ts +473 -119
- package/src/leetcode.ts +120 -33
- package/src/prompt.ts +21 -5
- package/src/scaffold.ts +13 -2
- package/src/schema.ts +226 -0
package/README.md
CHANGED
|
@@ -182,6 +182,43 @@ node tests/runner.mjs # all cases
|
|
|
182
182
|
node tests/runner.mjs --public # visible cases only
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
+
## Typed starters
|
|
186
|
+
|
|
187
|
+
The starter file is generated from your test cases, so the input/output contract
|
|
188
|
+
is explicit before you write any logic:
|
|
189
|
+
|
|
190
|
+
- **Type-safe languages** (`typescript`, `go`, `rust`, `csharp`, `swift`) get real
|
|
191
|
+
type declarations inferred from the sample inputs/outputs — an interface /
|
|
192
|
+
struct / class per object, with nested objects and arrays typed too — plus a
|
|
193
|
+
typed function signature. Your editor autocompletes `payload.<field>`.
|
|
194
|
+
- **Dynamic languages** (`javascript`, `python`, `ruby`, `php`) — and Swift when a
|
|
195
|
+
shape can't be expressed statically — get a commented input/output format with a
|
|
196
|
+
concrete example instead.
|
|
197
|
+
|
|
198
|
+
Example (TypeScript), generated from input `{ "values": [1,2,3] }` -> output `[3,2,1]`:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
interface Payload {
|
|
202
|
+
values: number[];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function solve(payload: Payload): number[] {
|
|
206
|
+
throw new Error("Not implemented: solve");
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Example (Python):
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
# Input format: { values: number[] }
|
|
214
|
+
# Input example: {"values":[1,2,3]}
|
|
215
|
+
# Output format: number[]
|
|
216
|
+
# Output example: [3,2,1]
|
|
217
|
+
|
|
218
|
+
def solve(payload):
|
|
219
|
+
raise NotImplementedError("Not implemented: solve")
|
|
220
|
+
```
|
|
221
|
+
|
|
185
222
|
## Running from source
|
|
186
223
|
|
|
187
224
|
For plugin development, clone the repo and open opencode inside it — files under
|
|
@@ -234,6 +271,7 @@ src/
|
|
|
234
271
|
index.ts plugin entry: tools, guard hooks, command registration
|
|
235
272
|
leetcode.ts API client + normalizer (handles endpoint shape differences)
|
|
236
273
|
languages.ts per-language adapters: starter, build, run commands
|
|
274
|
+
schema.ts input/output schema inference + type generation
|
|
237
275
|
patterns.ts topic -> domain seeds, leak detection
|
|
238
276
|
scaffold.ts deterministic project writer (any supported language)
|
|
239
277
|
guard.ts protected-path detection
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-leetcode-realworld",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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
|
@@ -38,7 +38,12 @@ export const LeetCodeRealWorld: Plugin = async ({ client, directory }) => {
|
|
|
38
38
|
.describe("How to choose the problem"),
|
|
39
39
|
idOrSlug: tool.schema.string().optional().describe("Problem id or slug (required for mode=specific)"),
|
|
40
40
|
difficulty: tool.schema.enum(["Easy", "Medium", "Hard"]).optional(),
|
|
41
|
-
tags: tool.schema
|
|
41
|
+
tags: tool.schema
|
|
42
|
+
.array(tool.schema.string())
|
|
43
|
+
.optional()
|
|
44
|
+
.describe(
|
|
45
|
+
"Topic tag slugs to constrain the problem, e.g. ['array'] or ['dynamic-programming']",
|
|
46
|
+
),
|
|
42
47
|
},
|
|
43
48
|
async execute(args, context) {
|
|
44
49
|
const problem = await fetchProblem({
|
package/src/languages.ts
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildTypes,
|
|
3
|
+
isFullyTyped,
|
|
4
|
+
isIdentifier,
|
|
5
|
+
isReserved,
|
|
6
|
+
pascal,
|
|
7
|
+
schemaSignature,
|
|
8
|
+
snake,
|
|
9
|
+
type SchemaNode,
|
|
10
|
+
type TypeDecl,
|
|
11
|
+
type TypeRef,
|
|
12
|
+
} from "./schema";
|
|
1
13
|
import type { FileEntry, LanguageId } from "./types";
|
|
2
14
|
|
|
3
15
|
export interface LanguageContext {
|
|
4
16
|
projectName: string;
|
|
5
17
|
functionName: string;
|
|
6
18
|
title: string;
|
|
19
|
+
input: SchemaNode;
|
|
20
|
+
output: SchemaNode;
|
|
21
|
+
inputExample: unknown;
|
|
22
|
+
outputExample: unknown;
|
|
7
23
|
}
|
|
8
24
|
|
|
9
25
|
export interface LanguageAdapter {
|
|
@@ -11,17 +27,32 @@ export interface LanguageAdapter {
|
|
|
11
27
|
name: string;
|
|
12
28
|
aliases: string[];
|
|
13
29
|
extension: string;
|
|
14
|
-
/** Human hint shown in the generated README. */
|
|
15
30
|
runtime: string;
|
|
31
|
+
/** True when the starter exposes static types for the payload/result. */
|
|
32
|
+
typed: boolean;
|
|
16
33
|
starter(ctx: LanguageContext): string;
|
|
17
|
-
/** Extra project files (build manifests etc). */
|
|
18
34
|
files(ctx: LanguageContext): FileEntry[];
|
|
19
|
-
/** Command run once before the cases, or null when not needed. */
|
|
20
35
|
build(ctx: LanguageContext): string | null;
|
|
21
|
-
/** Command that runs the solution once, reading one JSON value on stdin. */
|
|
22
36
|
run(ctx: LanguageContext): string;
|
|
23
37
|
}
|
|
24
38
|
|
|
39
|
+
const INPUT_ROOT = "Payload";
|
|
40
|
+
const OUTPUT_ROOT = "Output";
|
|
41
|
+
|
|
42
|
+
function example(value: unknown): string {
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Format comment for languages without static types (and as a fallback). */
|
|
47
|
+
function formatComment(prefix: string, ctx: LanguageContext): string {
|
|
48
|
+
return [
|
|
49
|
+
`${prefix} Input format: ${schemaSignature(ctx.input)}`,
|
|
50
|
+
`${prefix} Input example: ${example(ctx.inputExample)}`,
|
|
51
|
+
`${prefix} Output format: ${schemaSignature(ctx.output)}`,
|
|
52
|
+
`${prefix} Output example: ${example(ctx.outputExample)}`,
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
25
56
|
function packageJson(projectName: string, script: string): FileEntry {
|
|
26
57
|
return {
|
|
27
58
|
relative: "package.json",
|
|
@@ -40,6 +71,401 @@ function packageJson(projectName: string, script: string): FileEntry {
|
|
|
40
71
|
};
|
|
41
72
|
}
|
|
42
73
|
|
|
74
|
+
/* ------------------------------------------------------------------ */
|
|
75
|
+
/* TypeScript: named interfaces -> dot completion on `payload.` */
|
|
76
|
+
/* ------------------------------------------------------------------ */
|
|
77
|
+
|
|
78
|
+
function tsRef(ref: TypeRef): string {
|
|
79
|
+
switch (ref.t) {
|
|
80
|
+
case "string":
|
|
81
|
+
return "string";
|
|
82
|
+
case "int":
|
|
83
|
+
case "double":
|
|
84
|
+
return "number";
|
|
85
|
+
case "bool":
|
|
86
|
+
return "boolean";
|
|
87
|
+
case "any":
|
|
88
|
+
return "unknown";
|
|
89
|
+
case "null":
|
|
90
|
+
return "null";
|
|
91
|
+
case "array": {
|
|
92
|
+
const inner = tsRef(ref.of);
|
|
93
|
+
return inner.includes(" ") ? `(${inner})[]` : `${inner}[]`;
|
|
94
|
+
}
|
|
95
|
+
case "named":
|
|
96
|
+
return ref.name;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function tsProp(name: string): string {
|
|
101
|
+
return isIdentifier(name) && !isReserved(name) ? name : JSON.stringify(name);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function tsDecl(decl: TypeDecl): string {
|
|
105
|
+
const fields = decl.fields
|
|
106
|
+
.map((field) => ` ${tsProp(field.name)}${field.optional ? "?" : ""}: ${tsRef(field.ref)};`)
|
|
107
|
+
.join("\n");
|
|
108
|
+
return `interface ${decl.name} {\n${fields}\n}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function typescriptStarter(ctx: LanguageContext): string {
|
|
112
|
+
const input = buildTypes(INPUT_ROOT, ctx.input);
|
|
113
|
+
const output = buildTypes(OUTPUT_ROOT, ctx.output);
|
|
114
|
+
const decls = [...input.decls, ...output.decls].map(tsDecl).join("\n\n");
|
|
115
|
+
const inputType = tsRef(input.root);
|
|
116
|
+
const outputType = tsRef(output.root);
|
|
117
|
+
const header = decls ? `${decls}\n\n` : "";
|
|
118
|
+
return `import { readFileSync } from "node:fs";
|
|
119
|
+
|
|
120
|
+
${header}function ${ctx.functionName}(payload: ${inputType}): ${outputType} {
|
|
121
|
+
throw new Error("Not implemented: ${ctx.functionName}");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const input = readFileSync(0, "utf8");
|
|
125
|
+
const payload = JSON.parse(input.trim() === "" ? "null" : input) as ${inputType};
|
|
126
|
+
process.stdout.write(JSON.stringify(${ctx.functionName}(payload) ?? null));
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* ------------------------------------------------------------------ */
|
|
131
|
+
/* Go: structs with json tags */
|
|
132
|
+
/* ------------------------------------------------------------------ */
|
|
133
|
+
|
|
134
|
+
function goRef(ref: TypeRef): string {
|
|
135
|
+
switch (ref.t) {
|
|
136
|
+
case "string":
|
|
137
|
+
return "string";
|
|
138
|
+
case "int":
|
|
139
|
+
return "int";
|
|
140
|
+
case "double":
|
|
141
|
+
return "float64";
|
|
142
|
+
case "bool":
|
|
143
|
+
return "bool";
|
|
144
|
+
case "any":
|
|
145
|
+
case "null":
|
|
146
|
+
return "any";
|
|
147
|
+
case "array":
|
|
148
|
+
return "[]" + goRef(ref.of);
|
|
149
|
+
case "named":
|
|
150
|
+
return ref.name;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function goFieldName(name: string): string {
|
|
155
|
+
const value = pascal(name);
|
|
156
|
+
return value.length > 0 ? value : "Field";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function goDecl(decl: TypeDecl): string {
|
|
160
|
+
const fields = decl.fields
|
|
161
|
+
.map((field) => {
|
|
162
|
+
const tag = `json:"${field.name}${field.optional ? ",omitempty" : ""}"`;
|
|
163
|
+
return "\t" + goFieldName(field.name) + " " + goRef(field.ref) + " `" + tag + "`";
|
|
164
|
+
})
|
|
165
|
+
.join("\n");
|
|
166
|
+
return `type ${decl.name} struct {\n${fields}\n}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function goAliasOrDecls(
|
|
170
|
+
root: TypeRef,
|
|
171
|
+
decls: TypeDecl[],
|
|
172
|
+
rootName: string,
|
|
173
|
+
): string {
|
|
174
|
+
const pieces = decls.map(goDecl);
|
|
175
|
+
if (root.t !== "named") pieces.unshift(`type ${rootName} = ${goRef(root)}`);
|
|
176
|
+
return pieces.join("\n\n");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function goStarter(ctx: LanguageContext): string {
|
|
180
|
+
const input = buildTypes(INPUT_ROOT, ctx.input);
|
|
181
|
+
const output = buildTypes(OUTPUT_ROOT, ctx.output);
|
|
182
|
+
const types = [
|
|
183
|
+
goAliasOrDecls(input.root, input.decls, INPUT_ROOT),
|
|
184
|
+
goAliasOrDecls(output.root, output.decls, OUTPUT_ROOT),
|
|
185
|
+
].join("\n\n");
|
|
186
|
+
return `package main
|
|
187
|
+
|
|
188
|
+
import (
|
|
189
|
+
"encoding/json"
|
|
190
|
+
"fmt"
|
|
191
|
+
"io"
|
|
192
|
+
"os"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
${types}
|
|
196
|
+
|
|
197
|
+
func ${ctx.functionName}(payload ${INPUT_ROOT}) ${OUTPUT_ROOT} {
|
|
198
|
+
panic("Not implemented: ${ctx.functionName}")
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
func main() {
|
|
202
|
+
raw, _ := io.ReadAll(os.Stdin)
|
|
203
|
+
var payload ${INPUT_ROOT}
|
|
204
|
+
if len(raw) > 0 {
|
|
205
|
+
if err := json.Unmarshal(raw, &payload); err != nil {
|
|
206
|
+
panic(err)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
out, err := json.Marshal(${ctx.functionName}(payload))
|
|
210
|
+
if err != nil {
|
|
211
|
+
panic(err)
|
|
212
|
+
}
|
|
213
|
+
fmt.Print(string(out))
|
|
214
|
+
}
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* ------------------------------------------------------------------ */
|
|
219
|
+
/* Rust: serde structs */
|
|
220
|
+
/* ------------------------------------------------------------------ */
|
|
221
|
+
|
|
222
|
+
function rustRef(ref: TypeRef): string {
|
|
223
|
+
switch (ref.t) {
|
|
224
|
+
case "string":
|
|
225
|
+
return "String";
|
|
226
|
+
case "int":
|
|
227
|
+
return "i64";
|
|
228
|
+
case "double":
|
|
229
|
+
return "f64";
|
|
230
|
+
case "bool":
|
|
231
|
+
return "bool";
|
|
232
|
+
case "any":
|
|
233
|
+
case "null":
|
|
234
|
+
return "serde_json::Value";
|
|
235
|
+
case "array":
|
|
236
|
+
return `Vec<${rustRef(ref.of)}>`;
|
|
237
|
+
case "named":
|
|
238
|
+
return ref.name;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function rustField(name: string): string {
|
|
243
|
+
let value = snake(name);
|
|
244
|
+
if (isReserved(value)) value = `${value}_`;
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function rustDecl(decl: TypeDecl): string {
|
|
249
|
+
const fields = decl.fields
|
|
250
|
+
.map((field) => {
|
|
251
|
+
const ident = rustField(field.name);
|
|
252
|
+
const base = rustRef(field.ref);
|
|
253
|
+
const ty = field.optional ? `Option<${base}>` : base;
|
|
254
|
+
const attrs: string[] = [];
|
|
255
|
+
if (ident !== field.name) attrs.push(` #[serde(rename = "${field.name}")]`);
|
|
256
|
+
if (field.optional) {
|
|
257
|
+
attrs.push(' #[serde(default, skip_serializing_if = "Option::is_none")]');
|
|
258
|
+
}
|
|
259
|
+
const attrText = attrs.length > 0 ? attrs.join("\n") + "\n" : "";
|
|
260
|
+
return `${attrText} ${ident}: ${ty},`;
|
|
261
|
+
})
|
|
262
|
+
.join("\n");
|
|
263
|
+
return `#[derive(Serialize, Deserialize)]\nstruct ${decl.name} {\n${fields}\n}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function rustAliasOrDecls(root: TypeRef, decls: TypeDecl[], rootName: string): string {
|
|
267
|
+
const pieces = decls.map(rustDecl);
|
|
268
|
+
if (root.t !== "named") pieces.unshift(`type ${rootName} = ${rustRef(root)};`);
|
|
269
|
+
return pieces.join("\n\n");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function rustStarter(ctx: LanguageContext): string {
|
|
273
|
+
const input = buildTypes(INPUT_ROOT, ctx.input);
|
|
274
|
+
const output = buildTypes(OUTPUT_ROOT, ctx.output);
|
|
275
|
+
const types = [
|
|
276
|
+
rustAliasOrDecls(input.root, input.decls, INPUT_ROOT),
|
|
277
|
+
rustAliasOrDecls(output.root, output.decls, OUTPUT_ROOT),
|
|
278
|
+
].join("\n\n");
|
|
279
|
+
return `use serde::{Deserialize, Serialize};
|
|
280
|
+
use std::io::Read;
|
|
281
|
+
|
|
282
|
+
${types}
|
|
283
|
+
|
|
284
|
+
fn ${ctx.functionName}(payload: ${INPUT_ROOT}) -> ${OUTPUT_ROOT} {
|
|
285
|
+
let _ = &payload;
|
|
286
|
+
panic!("Not implemented: ${ctx.functionName}");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
fn main() {
|
|
290
|
+
let mut raw = String::new();
|
|
291
|
+
std::io::stdin().read_to_string(&mut raw).expect("read stdin");
|
|
292
|
+
let trimmed = raw.trim();
|
|
293
|
+
let source = if trimmed.is_empty() { "null" } else { trimmed };
|
|
294
|
+
let payload: ${INPUT_ROOT} = serde_json::from_str(source).expect("parse json");
|
|
295
|
+
let result = ${ctx.functionName}(payload);
|
|
296
|
+
println!("{}", serde_json::to_string(&result).expect("serialize json"));
|
|
297
|
+
}
|
|
298
|
+
`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* ------------------------------------------------------------------ */
|
|
302
|
+
/* C#: classes with JsonPropertyName */
|
|
303
|
+
/* ------------------------------------------------------------------ */
|
|
304
|
+
|
|
305
|
+
function csRef(ref: TypeRef): string {
|
|
306
|
+
switch (ref.t) {
|
|
307
|
+
case "string":
|
|
308
|
+
return "string";
|
|
309
|
+
case "int":
|
|
310
|
+
return "long";
|
|
311
|
+
case "double":
|
|
312
|
+
return "double";
|
|
313
|
+
case "bool":
|
|
314
|
+
return "bool";
|
|
315
|
+
case "any":
|
|
316
|
+
case "null":
|
|
317
|
+
return "JsonNode";
|
|
318
|
+
case "array":
|
|
319
|
+
return `List<${csRef(ref.of)}>`;
|
|
320
|
+
case "named":
|
|
321
|
+
return ref.name;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function csFieldName(name: string): string {
|
|
326
|
+
const value = pascal(name);
|
|
327
|
+
return value.length > 0 ? value : "Field";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function csDecl(decl: TypeDecl): string {
|
|
331
|
+
const props = decl.fields
|
|
332
|
+
.map((field) => {
|
|
333
|
+
return ` [JsonPropertyName("${field.name}")]\n public ${csRef(field.ref)} ${csFieldName(
|
|
334
|
+
field.name,
|
|
335
|
+
)} { get; set; }`;
|
|
336
|
+
})
|
|
337
|
+
.join("\n\n");
|
|
338
|
+
return `class ${decl.name}\n{\n${props}\n}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function csStarter(ctx: LanguageContext): string {
|
|
342
|
+
const input = buildTypes(INPUT_ROOT, ctx.input);
|
|
343
|
+
const output = buildTypes(OUTPUT_ROOT, ctx.output);
|
|
344
|
+
const decls = [...input.decls, ...output.decls].map(csDecl).join("\n\n");
|
|
345
|
+
const inputType = csRef(input.root);
|
|
346
|
+
const outputType = csRef(output.root);
|
|
347
|
+
return `using System;
|
|
348
|
+
using System.Collections.Generic;
|
|
349
|
+
using System.Text.Json;
|
|
350
|
+
using System.Text.Json.Nodes;
|
|
351
|
+
using System.Text.Json.Serialization;
|
|
352
|
+
|
|
353
|
+
${decls}
|
|
354
|
+
|
|
355
|
+
class Program
|
|
356
|
+
{
|
|
357
|
+
static ${outputType} ${ctx.functionName}(${inputType} payload)
|
|
358
|
+
{
|
|
359
|
+
throw new NotImplementedException("Not implemented: ${ctx.functionName}");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
static void Main()
|
|
363
|
+
{
|
|
364
|
+
var raw = Console.In.ReadToEnd().Trim();
|
|
365
|
+
var payload = JsonSerializer.Deserialize<${inputType}>(raw);
|
|
366
|
+
Console.Write(JsonSerializer.Serialize(${ctx.functionName}(payload)));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/* ------------------------------------------------------------------ */
|
|
373
|
+
/* Swift: Codable structs (only when the schema is fully typed) */
|
|
374
|
+
/* ------------------------------------------------------------------ */
|
|
375
|
+
|
|
376
|
+
function swiftRef(ref: TypeRef): string {
|
|
377
|
+
switch (ref.t) {
|
|
378
|
+
case "string":
|
|
379
|
+
return "String";
|
|
380
|
+
case "int":
|
|
381
|
+
return "Int";
|
|
382
|
+
case "double":
|
|
383
|
+
return "Double";
|
|
384
|
+
case "bool":
|
|
385
|
+
return "Bool";
|
|
386
|
+
case "array":
|
|
387
|
+
return `[${swiftRef(ref.of)}]`;
|
|
388
|
+
case "named":
|
|
389
|
+
return ref.name;
|
|
390
|
+
default:
|
|
391
|
+
return "Any";
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function swiftFieldName(name: string): string {
|
|
396
|
+
if (isIdentifier(name) && !isReserved(name)) return name;
|
|
397
|
+
let value = snake(name);
|
|
398
|
+
if (isReserved(value)) value = `${value}_`;
|
|
399
|
+
return isIdentifier(value) ? value : "field";
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function swiftDecl(decl: TypeDecl): string {
|
|
403
|
+
const fields: string[] = [];
|
|
404
|
+
const codingKeys: string[] = [];
|
|
405
|
+
let needsKeys = false;
|
|
406
|
+
for (const field of decl.fields) {
|
|
407
|
+
const prop = swiftFieldName(field.name);
|
|
408
|
+
const base = swiftRef(field.ref);
|
|
409
|
+
const ty = field.optional ? `${base}?` : base;
|
|
410
|
+
fields.push(` let ${prop}: ${ty}`);
|
|
411
|
+
if (prop !== field.name) {
|
|
412
|
+
needsKeys = true;
|
|
413
|
+
codingKeys.push(` case ${prop} = "${field.name}"`);
|
|
414
|
+
} else {
|
|
415
|
+
codingKeys.push(` case ${prop}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const keys = needsKeys ? `\n\n enum CodingKeys: String, CodingKey {\n${codingKeys.join("\n")}\n }` : "";
|
|
419
|
+
return `struct ${decl.name}: Codable {\n${fields.join("\n")}${keys}\n}`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function swiftAliasOrDecls(root: TypeRef, decls: TypeDecl[], rootName: string): string {
|
|
423
|
+
const pieces = decls.map(swiftDecl);
|
|
424
|
+
if (root.t !== "named") pieces.unshift(`typealias ${rootName} = ${swiftRef(root)}`);
|
|
425
|
+
return pieces.join("\n\n");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function swiftStarter(ctx: LanguageContext): string {
|
|
429
|
+
const input = buildTypes(INPUT_ROOT, ctx.input);
|
|
430
|
+
const output = buildTypes(OUTPUT_ROOT, ctx.output);
|
|
431
|
+
const types = [
|
|
432
|
+
swiftAliasOrDecls(input.root, input.decls, INPUT_ROOT),
|
|
433
|
+
swiftAliasOrDecls(output.root, output.decls, OUTPUT_ROOT),
|
|
434
|
+
].join("\n\n");
|
|
435
|
+
return `import Foundation
|
|
436
|
+
|
|
437
|
+
${types}
|
|
438
|
+
|
|
439
|
+
func ${ctx.functionName}(_ payload: ${INPUT_ROOT}) -> ${OUTPUT_ROOT} {
|
|
440
|
+
fatalError("Not implemented: ${ctx.functionName}")
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let raw = String(data: FileHandle.standardInput.readDataToEndOfFile(), encoding: .utf8) ?? ""
|
|
444
|
+
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
445
|
+
let data = trimmed.isEmpty ? Data("null".utf8) : Data(raw.utf8)
|
|
446
|
+
let payload = try! JSONDecoder().decode(${INPUT_ROOT}.self, from: data)
|
|
447
|
+
let out = try! JSONEncoder().encode(${ctx.functionName}(payload))
|
|
448
|
+
FileHandle.standardOutput.write(out)
|
|
449
|
+
`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function swiftFallbackStarter(ctx: LanguageContext): string {
|
|
453
|
+
return `import Foundation
|
|
454
|
+
|
|
455
|
+
${formatComment("//", ctx)}
|
|
456
|
+
|
|
457
|
+
func ${ctx.functionName}(_ payload: Any) -> Any {
|
|
458
|
+
fatalError("Not implemented: ${ctx.functionName}")
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let raw = String(data: FileHandle.standardInput.readDataToEndOfFile(), encoding: .utf8) ?? ""
|
|
462
|
+
let data = raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? Data("null".utf8) : Data(raw.utf8)
|
|
463
|
+
let payload = try! JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed])
|
|
464
|
+
let out = try! JSONSerialization.data(withJSONObject: ${ctx.functionName}(payload), options: [.fragmentsAllowed])
|
|
465
|
+
FileHandle.standardOutput.write(out)
|
|
466
|
+
`;
|
|
467
|
+
}
|
|
468
|
+
|
|
43
469
|
const adapters: Record<LanguageId, LanguageAdapter> = {
|
|
44
470
|
typescript: {
|
|
45
471
|
id: "typescript",
|
|
@@ -47,16 +473,8 @@ const adapters: Record<LanguageId, LanguageAdapter> = {
|
|
|
47
473
|
aliases: ["ts"],
|
|
48
474
|
extension: "ts",
|
|
49
475
|
runtime: "Node.js 22.6+ (uses `--experimental-strip-types`)",
|
|
50
|
-
|
|
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
|
-
`,
|
|
476
|
+
typed: true,
|
|
477
|
+
starter: typescriptStarter,
|
|
60
478
|
files: (ctx) => [
|
|
61
479
|
packageJson(ctx.projectName, "node tests/runner.mjs"),
|
|
62
480
|
{
|
|
@@ -81,7 +499,7 @@ process.stdout.write(JSON.stringify(${functionName}(payload) ?? null));
|
|
|
81
499
|
},
|
|
82
500
|
],
|
|
83
501
|
build: () => null,
|
|
84
|
-
run: (
|
|
502
|
+
run: () => "node --experimental-strip-types src/solution.ts",
|
|
85
503
|
},
|
|
86
504
|
|
|
87
505
|
javascript: {
|
|
@@ -90,15 +508,18 @@ process.stdout.write(JSON.stringify(${functionName}(payload) ?? null));
|
|
|
90
508
|
aliases: ["js", "node"],
|
|
91
509
|
extension: "mjs",
|
|
92
510
|
runtime: "Node.js 18+",
|
|
93
|
-
|
|
94
|
-
|
|
511
|
+
typed: false,
|
|
512
|
+
starter: (ctx) => `${formatComment("//", ctx)}
|
|
513
|
+
|
|
514
|
+
function ${ctx.functionName}(payload) {
|
|
515
|
+
throw new Error("Not implemented: ${ctx.functionName}");
|
|
95
516
|
}
|
|
96
517
|
|
|
97
518
|
const chunks = [];
|
|
98
519
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
99
520
|
const input = Buffer.concat(chunks).toString("utf8");
|
|
100
521
|
const payload = JSON.parse(input.trim() === "" ? "null" : input);
|
|
101
|
-
process.stdout.write(JSON.stringify(${functionName}(payload) ?? null));
|
|
522
|
+
process.stdout.write(JSON.stringify(${ctx.functionName}(payload) ?? null));
|
|
102
523
|
`,
|
|
103
524
|
files: (ctx) => [packageJson(ctx.projectName, "node tests/runner.mjs")],
|
|
104
525
|
build: () => null,
|
|
@@ -111,18 +532,21 @@ process.stdout.write(JSON.stringify(${functionName}(payload) ?? null));
|
|
|
111
532
|
aliases: ["py", "python3"],
|
|
112
533
|
extension: "py",
|
|
113
534
|
runtime: "Python 3.8+",
|
|
114
|
-
|
|
535
|
+
typed: false,
|
|
536
|
+
starter: (ctx) => `${formatComment("#", ctx)}
|
|
537
|
+
|
|
538
|
+
import json
|
|
115
539
|
import sys
|
|
116
540
|
|
|
117
541
|
|
|
118
|
-
def ${functionName}(payload):
|
|
119
|
-
raise NotImplementedError("Not implemented: ${functionName}")
|
|
542
|
+
def ${ctx.functionName}(payload):
|
|
543
|
+
raise NotImplementedError("Not implemented: ${ctx.functionName}")
|
|
120
544
|
|
|
121
545
|
|
|
122
546
|
def main():
|
|
123
547
|
raw = sys.stdin.read().strip()
|
|
124
548
|
payload = json.loads(raw) if raw else None
|
|
125
|
-
print(json.dumps(${functionName}(payload)))
|
|
549
|
+
print(json.dumps(${ctx.functionName}(payload)))
|
|
126
550
|
|
|
127
551
|
|
|
128
552
|
if __name__ == "__main__":
|
|
@@ -139,15 +563,18 @@ if __name__ == "__main__":
|
|
|
139
563
|
aliases: ["rb"],
|
|
140
564
|
extension: "rb",
|
|
141
565
|
runtime: "Ruby 2.6+ (json is in the standard library)",
|
|
142
|
-
|
|
566
|
+
typed: false,
|
|
567
|
+
starter: (ctx) => `${formatComment("#", ctx)}
|
|
568
|
+
|
|
569
|
+
require "json"
|
|
143
570
|
|
|
144
|
-
def ${functionName}(payload)
|
|
145
|
-
raise NotImplementedError, "Not implemented: ${functionName}"
|
|
571
|
+
def ${ctx.functionName}(payload)
|
|
572
|
+
raise NotImplementedError, "Not implemented: ${ctx.functionName}"
|
|
146
573
|
end
|
|
147
574
|
|
|
148
575
|
raw = $stdin.read.strip
|
|
149
576
|
payload = raw.empty? ? nil : JSON.parse(raw)
|
|
150
|
-
puts JSON.generate(${functionName}(payload))
|
|
577
|
+
puts JSON.generate(${ctx.functionName}(payload))
|
|
151
578
|
`,
|
|
152
579
|
files: () => [],
|
|
153
580
|
build: () => null,
|
|
@@ -160,15 +587,17 @@ puts JSON.generate(${functionName}(payload))
|
|
|
160
587
|
aliases: ["php8"],
|
|
161
588
|
extension: "php",
|
|
162
589
|
runtime: "PHP 7.4+",
|
|
163
|
-
|
|
590
|
+
typed: false,
|
|
591
|
+
starter: (ctx) => `<?php
|
|
592
|
+
${formatComment("//", ctx)}
|
|
164
593
|
|
|
165
|
-
function ${functionName}($payload) {
|
|
166
|
-
throw new Exception("Not implemented: ${functionName}");
|
|
594
|
+
function ${ctx.functionName}($payload) {
|
|
595
|
+
throw new Exception("Not implemented: ${ctx.functionName}");
|
|
167
596
|
}
|
|
168
597
|
|
|
169
598
|
$raw = trim(stream_get_contents(STDIN));
|
|
170
599
|
$payload = $raw === "" ? null : json_decode($raw, true);
|
|
171
|
-
echo json_encode(${functionName}($payload));
|
|
600
|
+
echo json_encode(${ctx.functionName}($payload));
|
|
172
601
|
`,
|
|
173
602
|
files: () => [],
|
|
174
603
|
build: () => null,
|
|
@@ -180,38 +609,10 @@ echo json_encode(${functionName}($payload));
|
|
|
180
609
|
name: "Go",
|
|
181
610
|
aliases: ["golang"],
|
|
182
611
|
extension: "go",
|
|
183
|
-
runtime: "Go 1.18+ (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
],
|
|
612
|
+
runtime: "Go 1.18+ (`any`)",
|
|
613
|
+
typed: true,
|
|
614
|
+
starter: goStarter,
|
|
615
|
+
files: () => [{ relative: "go.mod", contents: "module practice\n\ngo 1.21\n" }],
|
|
215
616
|
build: () => "mkdir -p out && go build -o out/solution src/solution.go",
|
|
216
617
|
run: () => "./out/solution",
|
|
217
618
|
},
|
|
@@ -221,28 +622,9 @@ func main() {
|
|
|
221
622
|
name: "Rust",
|
|
222
623
|
aliases: ["rs", "cargo"],
|
|
223
624
|
extension: "rs",
|
|
224
|
-
runtime: "Rust 1.70+ (
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
`,
|
|
625
|
+
runtime: "Rust 1.70+ (serde + serde_json)",
|
|
626
|
+
typed: true,
|
|
627
|
+
starter: rustStarter,
|
|
246
628
|
files: (ctx) => [
|
|
247
629
|
{
|
|
248
630
|
relative: "Cargo.toml",
|
|
@@ -252,6 +634,7 @@ version = "0.1.0"
|
|
|
252
634
|
edition = "2021"
|
|
253
635
|
|
|
254
636
|
[dependencies]
|
|
637
|
+
serde = { version = "1", features = ["derive"] }
|
|
255
638
|
serde_json = "1"
|
|
256
639
|
|
|
257
640
|
[[bin]]
|
|
@@ -270,24 +653,8 @@ path = "src/solution.rs"
|
|
|
270
653
|
aliases: ["cs", "dotnet"],
|
|
271
654
|
extension: "cs",
|
|
272
655
|
runtime: ".NET 8 SDK",
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
`,
|
|
656
|
+
typed: true,
|
|
657
|
+
starter: csStarter,
|
|
291
658
|
files: (ctx) => [
|
|
292
659
|
{
|
|
293
660
|
relative: `${ctx.projectName}.csproj`,
|
|
@@ -296,7 +663,7 @@ class Program
|
|
|
296
663
|
<OutputType>Exe</OutputType>
|
|
297
664
|
<TargetFramework>net8.0</TargetFramework>
|
|
298
665
|
<ImplicitUsings>enable</ImplicitUsings>
|
|
299
|
-
<Nullable>
|
|
666
|
+
<Nullable>disable</Nullable>
|
|
300
667
|
<AssemblyName>solution</AssemblyName>
|
|
301
668
|
</PropertyGroup>
|
|
302
669
|
</Project>
|
|
@@ -312,22 +679,9 @@ class Program
|
|
|
312
679
|
name: "Swift",
|
|
313
680
|
aliases: [],
|
|
314
681
|
extension: "swift",
|
|
315
|
-
runtime: "Swift 5.5+
|
|
316
|
-
|
|
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
|
-
`,
|
|
682
|
+
runtime: "Swift 5.5+",
|
|
683
|
+
typed: true,
|
|
684
|
+
starter: (ctx) => (isFullyTyped(ctx.input) && isFullyTyped(ctx.output) ? swiftStarter(ctx) : swiftFallbackStarter(ctx)),
|
|
331
685
|
files: () => [],
|
|
332
686
|
build: () => "mkdir -p out && swiftc -O src/solution.swift -o out/solution",
|
|
333
687
|
run: () => "./out/solution",
|
package/src/leetcode.ts
CHANGED
|
@@ -157,19 +157,29 @@ export async function getProblem(idOrSlug: string, signal?: AbortSignal): Promis
|
|
|
157
157
|
return normalizeProblem(raw, preferredSlug(idOrSlug));
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
interface
|
|
160
|
+
interface ProblemListEntry {
|
|
161
161
|
title_slug?: string;
|
|
162
162
|
titleSlug?: string;
|
|
163
|
+
difficulty?: string;
|
|
164
|
+
paid_only?: boolean;
|
|
165
|
+
isPaidOnly?: boolean;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function slugifyTag(input: string): string {
|
|
169
|
+
return input
|
|
170
|
+
.trim()
|
|
171
|
+
.toLowerCase()
|
|
172
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
173
|
+
.replace(/^-|-$/g, "");
|
|
163
174
|
}
|
|
164
175
|
|
|
165
176
|
export async function getRandom(
|
|
166
|
-
opts: { difficulty?: string;
|
|
177
|
+
opts: { difficulty?: string; signal?: AbortSignal } = {},
|
|
167
178
|
): Promise<NormalizedProblem> {
|
|
168
179
|
const params = new URLSearchParams();
|
|
169
180
|
if (opts.difficulty) params.set("difficulty", opts.difficulty);
|
|
170
|
-
if (opts.tags?.length) params.set("tags", opts.tags.join(","));
|
|
171
181
|
const query = params.toString();
|
|
172
|
-
const raw = await getJson<
|
|
182
|
+
const raw = await getJson<ProblemListEntry>(`/random${query ? `?${query}` : ""}`, opts.signal);
|
|
173
183
|
const slug = raw.title_slug ?? raw.titleSlug;
|
|
174
184
|
if (!slug) throw new Error("LeetCode API /random did not return a problem slug.");
|
|
175
185
|
return getProblem(slug, opts.signal);
|
|
@@ -181,20 +191,102 @@ export async function getDaily(signal?: AbortSignal): Promise<NormalizedProblem>
|
|
|
181
191
|
}
|
|
182
192
|
|
|
183
193
|
interface FilterResponse {
|
|
184
|
-
problems?:
|
|
194
|
+
problems?: ProblemListEntry[];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* IMPORTANT: `/problems/filter` and `/random` silently IGNORE the `tags` query
|
|
199
|
+
* parameter, so the only endpoint that actually filters by topic is
|
|
200
|
+
* `/problems/tag/{slug}`. Difficulty still has to be applied client-side there.
|
|
201
|
+
*/
|
|
202
|
+
interface TagResponse {
|
|
203
|
+
tag?: string;
|
|
204
|
+
problems?: ProblemListEntry[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function listByTag(
|
|
208
|
+
tag: string,
|
|
209
|
+
difficulty: string | undefined,
|
|
210
|
+
signal: AbortSignal | undefined,
|
|
211
|
+
pages = 4,
|
|
212
|
+
): Promise<ProblemListEntry[]> {
|
|
213
|
+
const results: ProblemListEntry[] = [];
|
|
214
|
+
const seen = new Set<string>();
|
|
215
|
+
for (let page = 0; page < pages; page += 1) {
|
|
216
|
+
const query = new URLSearchParams({ limit: "50", skip: String(page * 50) });
|
|
217
|
+
const raw = await getJson<TagResponse>(
|
|
218
|
+
`/problems/tag/${encodeURIComponent(tag)}?${query.toString()}`,
|
|
219
|
+
signal,
|
|
220
|
+
);
|
|
221
|
+
const problems = raw.problems ?? [];
|
|
222
|
+
if (problems.length === 0) break;
|
|
223
|
+
for (const problem of problems) {
|
|
224
|
+
const slug = String(problem.title_slug ?? problem.titleSlug ?? "");
|
|
225
|
+
if (!slug || seen.has(slug)) continue;
|
|
226
|
+
seen.add(slug);
|
|
227
|
+
if (problem.paid_only || problem.isPaidOnly) continue;
|
|
228
|
+
if (
|
|
229
|
+
difficulty &&
|
|
230
|
+
String(problem.difficulty ?? "").toLowerCase() !== difficulty.toLowerCase()
|
|
231
|
+
) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
results.push(problem);
|
|
235
|
+
}
|
|
236
|
+
if (problems.length < 50) break;
|
|
237
|
+
}
|
|
238
|
+
return results;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function matchesTags(problem: NormalizedProblem, tags: string[]): boolean {
|
|
242
|
+
const present = problem.topics.map(slugifyTag);
|
|
243
|
+
// Some LeetCode tag slugs differ from the topic label, e.g. tag "graph" ->
|
|
244
|
+
// topic "Graph Theory" (slug "graph-theory"). Allow prefix aliases both ways.
|
|
245
|
+
return tags.every((tag) =>
|
|
246
|
+
present.some(
|
|
247
|
+
(topic) => topic === tag || topic.startsWith(`${tag}-`) || tag.startsWith(`${topic}-`),
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function fetchByTags(
|
|
253
|
+
tags: string[],
|
|
254
|
+
difficulty: string | undefined,
|
|
255
|
+
signal?: AbortSignal,
|
|
256
|
+
): Promise<NormalizedProblem> {
|
|
257
|
+
const candidates = await listByTag(tags[0]!, difficulty, signal);
|
|
258
|
+
if (candidates.length === 0) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`No ${difficulty ?? "matching"} problems found for tag "${tags[0]}". ` +
|
|
261
|
+
"Check the tag slug (e.g. array, hash-table, dynamic-programming).",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const strict = tags.length > 1;
|
|
265
|
+
const shuffled = [...candidates].sort(() => Math.random() - 0.5);
|
|
266
|
+
const attempts = Math.min(shuffled.length, 12);
|
|
267
|
+
let fallback: NormalizedProblem | undefined;
|
|
268
|
+
for (let index = 0; index < attempts; index += 1) {
|
|
269
|
+
const slug = String(shuffled[index]!.title_slug ?? shuffled[index]!.titleSlug ?? "");
|
|
270
|
+
if (!slug) continue;
|
|
271
|
+
const problem = await getProblem(slug, signal);
|
|
272
|
+
if (!isUsableProblem(problem)) continue;
|
|
273
|
+
fallback = fallback ?? problem;
|
|
274
|
+
if (!strict || matchesTags(problem, tags)) return problem;
|
|
275
|
+
}
|
|
276
|
+
if (fallback) return fallback;
|
|
277
|
+
throw new Error(`Could not find a usable problem for tag "${tags[0]}".`);
|
|
185
278
|
}
|
|
186
279
|
|
|
187
280
|
export async function getFiltered(
|
|
188
|
-
opts: { difficulty?: string;
|
|
281
|
+
opts: { difficulty?: string; limit?: number; signal?: AbortSignal } = {},
|
|
189
282
|
): Promise<NormalizedProblem> {
|
|
190
283
|
const params = new URLSearchParams();
|
|
191
284
|
if (opts.difficulty) params.set("difficulty", opts.difficulty);
|
|
192
|
-
if (opts.tags?.length) params.set("tags", opts.tags.join(","));
|
|
193
285
|
params.set("limit", String(opts.limit ?? 20));
|
|
194
286
|
const raw = await getJson<FilterResponse>(`/problems/filter?${params.toString()}`, opts.signal);
|
|
195
287
|
const candidates = raw.problems ?? [];
|
|
196
288
|
if (candidates.length === 0) throw new Error("LeetCode API returned no problems for the given filter.");
|
|
197
|
-
const choice = candidates[Math.floor(Math.random() * candidates.length)]
|
|
289
|
+
const choice = candidates[Math.floor(Math.random() * candidates.length)]!;
|
|
198
290
|
const slug = String(choice.title_slug ?? choice.titleSlug ?? "");
|
|
199
291
|
if (!slug) throw new Error("Filtered problem is missing a slug.");
|
|
200
292
|
return getProblem(slug, opts.signal);
|
|
@@ -202,32 +294,27 @@ export async function getFiltered(
|
|
|
202
294
|
|
|
203
295
|
export async function fetchProblem(options: FetchOptions = {}): Promise<NormalizedProblem> {
|
|
204
296
|
const mode = options.mode ?? "random";
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
);
|
|
225
|
-
case "random":
|
|
226
|
-
default:
|
|
227
|
-
return pickUsable(() =>
|
|
228
|
-
getRandom({ difficulty: options.difficulty, tags: options.tags, signal: options.signal }),
|
|
229
|
-
);
|
|
297
|
+
const tags = (options.tags ?? []).map(slugifyTag).filter((tag) => tag.length > 0);
|
|
298
|
+
|
|
299
|
+
if (mode === "specific") {
|
|
300
|
+
if (!options.idOrSlug) throw new Error("`idOrSlug` is required when mode is `specific`.");
|
|
301
|
+
return getProblem(options.idOrSlug, options.signal);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (mode === "daily" && tags.length === 0) {
|
|
305
|
+
const daily = await getDaily(options.signal);
|
|
306
|
+
if (isUsableProblem(daily)) return daily;
|
|
307
|
+
return pickUsable(() => getRandom({ difficulty: options.difficulty, signal: options.signal }));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (tags.length > 0) {
|
|
311
|
+
return fetchByTags(tags, options.difficulty, options.signal);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (mode === "filter") {
|
|
315
|
+
return pickUsable(() => getFiltered({ difficulty: options.difficulty, signal: options.signal }));
|
|
230
316
|
}
|
|
317
|
+
return pickUsable(() => getRandom({ difficulty: options.difficulty, signal: options.signal }));
|
|
231
318
|
}
|
|
232
319
|
|
|
233
320
|
export interface TagInfo {
|
package/src/prompt.ts
CHANGED
|
@@ -29,10 +29,19 @@ puzzle.
|
|
|
29
29
|
Arguments: "$ARGUMENTS"
|
|
30
30
|
Recognise any of these (all optional):
|
|
31
31
|
- difficulty: easy | medium | hard
|
|
32
|
-
- tags:
|
|
32
|
+
- tags: topic names or slugs, e.g. "graph" or "dynamic-programming,sliding-window"
|
|
33
33
|
- a specific problem id or slug, e.g. "two-sum" or "1"
|
|
34
34
|
- language: a supported language name or alias (below)
|
|
35
35
|
|
|
36
|
+
Mapping rules:
|
|
37
|
+
- A word that names a topic (array, string, graph, tree, dp/dynamic-programming,
|
|
38
|
+
sliding-window, two-pointers, greedy, ...) is a TAG. Pass it to \`leetcode_fetch\`
|
|
39
|
+
as \`tags\` (an array of slugs, e.g. \`["array"]\`).
|
|
40
|
+
- Only pass \`idOrSlug\` when the argument is a number or a known problem slug
|
|
41
|
+
(e.g. "1", "two-sum"). Never pass a topic name as \`idOrSlug\`.
|
|
42
|
+
- When the user gives a tag, it MUST be respected. After fetching, if the problem's
|
|
43
|
+
topics do not include the requested tag, the fetch is wrong: retry with the tag.
|
|
44
|
+
|
|
36
45
|
## 2. Ask when unclear (use the \`question\` tool)
|
|
37
46
|
Supported languages: ${SUPPORTED_LANGUAGES.join(", ")}.
|
|
38
47
|
|
|
@@ -47,9 +56,11 @@ If the user did specify values, skip the corresponding question. Only ask at all
|
|
|
47
56
|
when something is missing.
|
|
48
57
|
|
|
49
58
|
## 3. Fetch the source problem (private)
|
|
50
|
-
Call the \`leetcode_fetch\` tool with the difficulty
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
Call the \`leetcode_fetch\` tool with the difficulty, and pass any requested topic
|
|
60
|
+
as \`tags\` (array of slugs) — not as \`idOrSlug\`. Verify the returned topics
|
|
61
|
+
include every requested tag; if not, retry. The fetched statement is for YOUR
|
|
62
|
+
reasoning only. Never paste it, paraphrase it closely, or name its source anywhere
|
|
63
|
+
in the generated project.
|
|
53
64
|
|
|
54
65
|
## 4. Derive the principle
|
|
55
66
|
Identify the underlying algorithmic idea (e.g. sliding window, heap-based
|
|
@@ -77,7 +88,12 @@ Call the tool \`leetcode_scaffold\` with a spec containing:
|
|
|
77
88
|
"javascript", "python", "ruby", "php", "go", "rust", "csharp", "swift")
|
|
78
89
|
- requirements: concrete, testable bullet points
|
|
79
90
|
- edgeCases: tricky situations the solution must handle
|
|
80
|
-
- starterCode
|
|
91
|
+
- Do NOT provide starterCode. Omit it so the plugin can generate a starter that
|
|
92
|
+
reflects the real types: type-safe languages get typed interfaces/structs and a
|
|
93
|
+
typed signature; dynamic languages get a documented input/output format comment.
|
|
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).
|
|
81
97
|
- publicTests: 5-8 visible cases that illustrate the contract
|
|
82
98
|
- hiddenTests: 6-12 acceptance cases, including edge cases, boundary values, an
|
|
83
99
|
empty/degenerate input, and at least one larger input
|
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
|
+
import { getLanguage, normalizeLanguage, type LanguageContext } from "./languages";
|
|
4
4
|
import { assertNoLeak } from "./patterns";
|
|
5
|
+
import { schemaFromSamples } from "./schema";
|
|
5
6
|
import type { FileEntry, LanguageId, ScaffoldResult, ScaffoldSpec, TestCase } from "./types";
|
|
6
7
|
|
|
7
8
|
function kebab(input: string): string {
|
|
@@ -251,7 +252,17 @@ function casesFile(cases: TestCase[]): string {
|
|
|
251
252
|
|
|
252
253
|
function buildFiles(spec: ScaffoldSpec, names: ResolvedNames, language: LanguageId): FileEntry[] {
|
|
253
254
|
const adapter = getLanguage(language);
|
|
254
|
-
const
|
|
255
|
+
const cases = [...(spec.publicTests ?? []), ...(spec.hiddenTests ?? [])];
|
|
256
|
+
const first = cases[0];
|
|
257
|
+
const ctx: LanguageContext = {
|
|
258
|
+
projectName: names.projectName,
|
|
259
|
+
functionName: names.functionName,
|
|
260
|
+
title: names.title,
|
|
261
|
+
input: schemaFromSamples(cases.map((test) => test.input)),
|
|
262
|
+
output: schemaFromSamples(cases.map((test) => test.expected)),
|
|
263
|
+
inputExample: first?.input ?? null,
|
|
264
|
+
outputExample: first?.expected ?? null,
|
|
265
|
+
};
|
|
255
266
|
|
|
256
267
|
const files: FileEntry[] = [
|
|
257
268
|
{ relative: "README.md", contents: readmeFor(names, spec, language) },
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infers a small structural schema from the sample test cases so the generated
|
|
3
|
+
* starter files can carry real types (type-safe languages) or a documented
|
|
4
|
+
* format (dynamic languages).
|
|
5
|
+
*
|
|
6
|
+
* This is intentionally schema-light: it only needs enough fidelity to describe
|
|
7
|
+
* JSON input/output shapes (objects, arrays, primitives, optionality).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type SchemaNode =
|
|
11
|
+
| { kind: "string" }
|
|
12
|
+
| { kind: "number"; integer: boolean }
|
|
13
|
+
| { kind: "boolean" }
|
|
14
|
+
| { kind: "null" }
|
|
15
|
+
| { kind: "unknown" }
|
|
16
|
+
| { kind: "array"; element: SchemaNode }
|
|
17
|
+
| { kind: "object"; fields: FieldSchema[] };
|
|
18
|
+
|
|
19
|
+
export interface FieldSchema {
|
|
20
|
+
name: string;
|
|
21
|
+
schema: SchemaNode;
|
|
22
|
+
optional: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function inferSchema(value: unknown): SchemaNode {
|
|
26
|
+
if (value === null || value === undefined) return { kind: "null" };
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return { kind: "array", element: mergeMany(value.map(inferSchema)) };
|
|
29
|
+
}
|
|
30
|
+
switch (typeof value) {
|
|
31
|
+
case "string":
|
|
32
|
+
return { kind: "string" };
|
|
33
|
+
case "boolean":
|
|
34
|
+
return { kind: "boolean" };
|
|
35
|
+
case "number":
|
|
36
|
+
return { kind: "number", integer: Number.isInteger(value) };
|
|
37
|
+
case "object": {
|
|
38
|
+
const fields = Object.entries(value as Record<string, unknown>).map(([name, entry]) => ({
|
|
39
|
+
name,
|
|
40
|
+
schema: inferSchema(entry),
|
|
41
|
+
optional: false,
|
|
42
|
+
}));
|
|
43
|
+
return { kind: "object", fields };
|
|
44
|
+
}
|
|
45
|
+
default:
|
|
46
|
+
return { kind: "unknown" };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function mergeSchemas(a: SchemaNode, b: SchemaNode): SchemaNode {
|
|
51
|
+
if (a.kind === "unknown") return b;
|
|
52
|
+
if (b.kind === "unknown") return a;
|
|
53
|
+
if (a.kind === "null") return b;
|
|
54
|
+
if (b.kind === "null") return a;
|
|
55
|
+
|
|
56
|
+
if (a.kind === "object" && b.kind === "object") {
|
|
57
|
+
const names = new Set([...a.fields.map((f) => f.name), ...b.fields.map((f) => f.name)]);
|
|
58
|
+
const fields: FieldSchema[] = [];
|
|
59
|
+
for (const name of names) {
|
|
60
|
+
const left = a.fields.find((f) => f.name === name);
|
|
61
|
+
const right = b.fields.find((f) => f.name === name);
|
|
62
|
+
if (left && right) {
|
|
63
|
+
fields.push({
|
|
64
|
+
name,
|
|
65
|
+
schema: mergeSchemas(left.schema, right.schema),
|
|
66
|
+
optional: left.optional && right.optional,
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
const only = (left ?? right)!;
|
|
70
|
+
fields.push({ name, schema: only.schema, optional: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { kind: "object", fields };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (a.kind === "array" && b.kind === "array") {
|
|
77
|
+
return { kind: "array", element: mergeSchemas(a.element, b.element) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (a.kind === "number" && b.kind === "number") {
|
|
81
|
+
return { kind: "number", integer: a.integer && b.integer };
|
|
82
|
+
}
|
|
83
|
+
if (a.kind === b.kind) return a;
|
|
84
|
+
return { kind: "unknown" };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mergeMany(nodes: SchemaNode[]): SchemaNode {
|
|
88
|
+
if (nodes.length === 0) return { kind: "unknown" };
|
|
89
|
+
return nodes.reduce((acc, node) => mergeSchemas(acc, node));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function schemaFromSamples(values: unknown[]): SchemaNode {
|
|
93
|
+
return mergeMany(values.map(inferSchema));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** True when the schema contains only types representable as static types. */
|
|
97
|
+
export function isFullyTyped(node: SchemaNode): boolean {
|
|
98
|
+
switch (node.kind) {
|
|
99
|
+
case "unknown":
|
|
100
|
+
case "null":
|
|
101
|
+
return false;
|
|
102
|
+
case "array":
|
|
103
|
+
return isFullyTyped(node.element);
|
|
104
|
+
case "object":
|
|
105
|
+
return node.fields.every((field) => isFullyTyped(field.schema));
|
|
106
|
+
default:
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Human-readable signature, e.g. `{ amounts: number[]; target: number }`. */
|
|
112
|
+
export function schemaSignature(node: SchemaNode): string {
|
|
113
|
+
switch (node.kind) {
|
|
114
|
+
case "string":
|
|
115
|
+
return "string";
|
|
116
|
+
case "number":
|
|
117
|
+
return "number";
|
|
118
|
+
case "boolean":
|
|
119
|
+
return "boolean";
|
|
120
|
+
case "null":
|
|
121
|
+
return "null";
|
|
122
|
+
case "unknown":
|
|
123
|
+
return "any";
|
|
124
|
+
case "array": {
|
|
125
|
+
const inner = schemaSignature(node.element);
|
|
126
|
+
return inner.includes(" ") ? `(${inner})[]` : `${inner}[]`;
|
|
127
|
+
}
|
|
128
|
+
case "object":
|
|
129
|
+
if (node.fields.length === 0) return "{}";
|
|
130
|
+
return `{ ${node.fields
|
|
131
|
+
.map((field) => `${field.name}${field.optional ? "?" : ""}: ${schemaSignature(field.schema)}`)
|
|
132
|
+
.join("; ")} }`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type TypeRef =
|
|
137
|
+
| { t: "string" }
|
|
138
|
+
| { t: "int" }
|
|
139
|
+
| { t: "double" }
|
|
140
|
+
| { t: "bool" }
|
|
141
|
+
| { t: "any" }
|
|
142
|
+
| { t: "null" }
|
|
143
|
+
| { t: "array"; of: TypeRef }
|
|
144
|
+
| { t: "named"; name: string };
|
|
145
|
+
|
|
146
|
+
export interface TypeDecl {
|
|
147
|
+
name: string;
|
|
148
|
+
fields: { name: string; ref: TypeRef; optional: boolean }[];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function buildTypes(
|
|
152
|
+
rootName: string,
|
|
153
|
+
node: SchemaNode,
|
|
154
|
+
): { root: TypeRef; decls: TypeDecl[] } {
|
|
155
|
+
const decls: TypeDecl[] = [];
|
|
156
|
+
const root = refFor(rootName, node, decls);
|
|
157
|
+
return { root, decls };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function refFor(name: string, node: SchemaNode, decls: TypeDecl[]): TypeRef {
|
|
161
|
+
switch (node.kind) {
|
|
162
|
+
case "object": {
|
|
163
|
+
const decl: TypeDecl = { name, fields: [] };
|
|
164
|
+
decls.push(decl);
|
|
165
|
+
for (const field of node.fields) {
|
|
166
|
+
decl.fields.push({
|
|
167
|
+
name: field.name,
|
|
168
|
+
optional: field.optional,
|
|
169
|
+
ref: refFor(`${name}${pascal(field.name)}`, field.schema, decls),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return { t: "named", name };
|
|
173
|
+
}
|
|
174
|
+
case "array":
|
|
175
|
+
return { t: "array", of: refFor(`${name}Item`, node.element, decls) };
|
|
176
|
+
case "string":
|
|
177
|
+
return { t: "string" };
|
|
178
|
+
case "boolean":
|
|
179
|
+
return { t: "bool" };
|
|
180
|
+
case "null":
|
|
181
|
+
return { t: "null" };
|
|
182
|
+
case "unknown":
|
|
183
|
+
return { t: "any" };
|
|
184
|
+
case "number":
|
|
185
|
+
return { t: node.integer ? "int" : "double" };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function pascal(input: string): string {
|
|
190
|
+
const parts = input
|
|
191
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
192
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
193
|
+
.filter(Boolean);
|
|
194
|
+
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function snake(input: string): string {
|
|
198
|
+
const value = input
|
|
199
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
200
|
+
.replace(/[^a-zA-Z0-9]+/g, "_")
|
|
201
|
+
.replace(/^_+|_+$/g, "")
|
|
202
|
+
.toLowerCase();
|
|
203
|
+
return /^[0-9]/.test(value) ? `_${value}` : value || "field";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const RESERVED = new Set([
|
|
207
|
+
"abstract", "as", "async", "await", "base", "bool", "break", "byte", "case", "catch",
|
|
208
|
+
"class", "const", "continue", "crate", "default", "delegate", "do", "double", "dyn",
|
|
209
|
+
"else", "enum", "event", "explicit", "extern", "false", "final", "finally", "fixed",
|
|
210
|
+
"float", "fn", "for", "foreach", "func", "goto", "if", "impl", "implements", "import",
|
|
211
|
+
"in", "int", "interface", "internal", "is", "let", "lock", "long", "loop", "match",
|
|
212
|
+
"mod", "move", "mut", "namespace", "new", "null", "object", "operator", "out",
|
|
213
|
+
"override", "package", "params", "private", "protected", "pub", "public", "readonly",
|
|
214
|
+
"ref", "return", "self", "short", "sizeof", "static", "string", "struct", "super",
|
|
215
|
+
"switch", "this", "throw", "trait", "true", "try", "type", "typeof", "uint", "ulong",
|
|
216
|
+
"unchecked", "unsafe", "use", "ushort", "var", "virtual", "void", "volatile", "where",
|
|
217
|
+
"while", "with", "yield",
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
export function isReserved(word: string): boolean {
|
|
221
|
+
return RESERVED.has(word.toLowerCase());
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function isIdentifier(word: string): boolean {
|
|
225
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(word);
|
|
226
|
+
}
|