-
Notifications
You must be signed in to change notification settings - Fork 0
refactor: re-inline thin helpers and extract saved-kata CRUD #390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import { join } from 'node:path'; | ||
| import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs'; | ||
| import type { StageCategory } from '@domain/types/stage.js'; | ||
| import { SavedKataSchema, type FlavorHint } from '@domain/types/saved-kata.js'; | ||
| import { isJsonFile } from '@shared/lib/file-filters.js'; | ||
| import { KATA_DIRS } from '@shared/constants/paths.js'; | ||
| import { assertValidKataName } from '@cli/commands/execute.helpers.js'; | ||
|
|
||
| function katasDir(kataDir: string): string { | ||
| return join(kataDir, KATA_DIRS.katas); | ||
| } | ||
|
|
||
| export function listSavedKatas(kataDir: string): Array<{ name: string; stages: StageCategory[]; description?: string }> { | ||
| const dir = katasDir(kataDir); | ||
| if (!existsSync(dir)) return []; | ||
| return readdirSync(dir) | ||
| .filter(isJsonFile) | ||
| .map((f) => { | ||
| try { | ||
| const raw = JSON.parse(readFileSync(join(dir, f), 'utf-8')); | ||
| return SavedKataSchema.parse(raw); | ||
| } catch (e) { | ||
| if (e instanceof SyntaxError || (e instanceof Error && e.constructor.name === 'ZodError')) { | ||
| console.error(`Warning: skipping invalid kata file "${f}": ${e.message}`); | ||
| return null; | ||
| } | ||
| throw e; | ||
| } | ||
| }) | ||
| .filter((k): k is NonNullable<typeof k> => k !== null); | ||
| } | ||
|
Comment on lines
+13
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Prefer This new store re-implements JSON persistence behavior with manual Based on learnings: "Store persistent data as JSON files in Also applies to: 33-64 |
||
|
|
||
| export function loadSavedKata(kataDir: string, name: string): { stages: StageCategory[]; flavorHints?: Record<string, FlavorHint> } { | ||
| assertValidKataName(name); | ||
| const filePath = join(katasDir(kataDir), `${name}.json`); | ||
| if (!existsSync(filePath)) { | ||
| throw new Error(`Kata "${name}" not found. Use --list-katas to see available katas.`); | ||
| } | ||
| let raw: unknown; | ||
| try { | ||
| raw = JSON.parse(readFileSync(filePath, 'utf-8')); | ||
| } catch (e) { | ||
| throw new Error( | ||
| `Kata "${name}" has invalid JSON: ${e instanceof Error ? e.message : String(e)}`, | ||
| { cause: e }, | ||
| ); | ||
| } | ||
| try { | ||
| return SavedKataSchema.parse(raw); | ||
| } catch (e) { | ||
| throw new Error( | ||
| `Kata "${name}" has invalid structure. Ensure it has "name" (string) and "stages" (array of categories).`, | ||
| { cause: e }, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| export function saveSavedKata(kataDir: string, name: string, stages: StageCategory[], flavorHints?: Record<string, FlavorHint>): void { | ||
| assertValidKataName(name); | ||
| const dir = katasDir(kataDir); | ||
| mkdirSync(dir, { recursive: true }); | ||
| const kata = SavedKataSchema.parse({ name, stages, flavorHints }); | ||
| writeFileSync(join(dir, `${name}.json`), JSON.stringify(kata, null, 2), 'utf-8'); | ||
| } | ||
|
|
||
| export function deleteSavedKata(kataDir: string, name: string): void { | ||
| assertValidKataName(name); | ||
| const filePath = join(katasDir(kataDir), `${name}.json`); | ||
| if (!existsSync(filePath)) { | ||
| throw new Error(`Kata "${name}" not found. Use --list-katas to see available katas.`); | ||
| } | ||
| try { | ||
| unlinkSync(filePath); | ||
| } catch (e) { | ||
| throw new Error( | ||
| `Could not delete kata "${name}": ${e instanceof Error ? e.message : String(e)}`, | ||
| { cause: e }, | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In Zod v4, isZodErrorexported from thezod/v4entrypoint and isinstanceof ZodErrorthe recommended runtime check?💡 Result:
Yes.
ZodErroris exported from thezod/v4entrypoint:zod/v4re-exports Zod 4 “classic” (./classic/index.js), which re-exports./errors.js, and that module exportsZodError(the constructor) as a public export. [1] [2] [3]err instanceof z.ZodErroras the way to detect validation errors at runtime. [4] Also, in v4 specifically, the core error type ($ZodError) does not extend the built-inError, soinstanceof Erroris not a reliable check for Zod validation errors. [5]Sources:
[1] https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/index.ts
[2] https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/external.ts
[3] https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/errors.ts
[4] https://github.com/colinhacks/zod (README “Handling errors” example)
[5] https://zod.dev/packages/core (Errors section)
Import
ZodErrorfromzod/v4and useinstanceoffor error type checking.At line 23, replace the brittle
constructor.name === 'ZodError'check withinstanceof ZodError. This is the recommended runtime pattern in Zod v4, whereZodErroris properly exported and type-safe, whereas the string comparison is fragile and can misclassify errors.Proposed fix
import { SavedKataSchema, type FlavorHint } from '@domain/types/saved-kata.js'; +import { ZodError } from 'zod/v4'; @@ - if (e instanceof SyntaxError || (e instanceof Error && e.constructor.name === 'ZodError')) { + if (e instanceof SyntaxError || e instanceof ZodError) {🤖 Prompt for AI Agents