Skip to content

Validate every configuration field against one schema - #64

Merged
844196 merged 2 commits into
mainfrom
feat/schema-driven-validation
Sep 3, 2026
Merged

Validate every configuration field against one schema#64
844196 merged 2 commits into
mainfrom
feat/schema-driven-validation

Conversation

@844196

@844196 844196 commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Follow-up to #63. That change stopped wk on a file it could not read or whose outermost shape was wrong; this one covers the fields inside.

Background

isPartialContext and isBindings only checked "is it a mapping" and "is it an array of objects with a string key". Everything below that was unchecked, and the failures fell into three kinds:

  • Crashes. colors.prompt: {}, a type: bindings without desc, a group whose bindings: is a scalar, buffer: 42 — all reached the drawing code and exited 1 with a stack trace, without tui.close(), leaving the menu on screen. Same class as (config, bindings): an empty config file crashes instead of falling back #58.
  • Protocol corruption. outputDelimiter: 42 produced 4242ls -la, so the widget took 4 as the delimiter and put stray bytes into BUFFER. A multi-character outputDelimiter or an empty binding delimiter did the same, and a type: command without buffer silently inserted an empty line.
  • Silent no-ops. timeout: "5s" disabled the timeout without saying so. A nested key: 1 fell through to exit 5, while the same typo at the top level exited 7 — the two disagreed.

Underneath all of it, the schema lived in three places (src/types/*.ts, schemas/*.json, the README), kept in step by hand. #63's review found the gap; nothing structurally prevented the next one.

What this does

src/schema.ts becomes the single source of truth. The types, the defaults, the runtime checks and the distributable JSON Schemas are all derived from one @zod/mini schema, so a field cannot be enforced in one place and forgotten in another. src/types/Binding.ts and src/types/Context.ts are gone (z.infer), and mergeContext is gone (z._default / z.prefault).

A violation stops wk with exit 7 and one line naming the path that failed:

~/.config/wk/bindings.yaml: [0].bindings[0].key: expected a key name or a digit 0-9
~/.config/wk/bindings.yaml: [0]: unknown field "oops"
~/.config/wk/config.yaml: colors.prompt: expected a color

The rule is that the schema is the contract: every field the schema constrains is checked, and what it does not describe — an unknown top-level config field — is left alone. Empty documents still fall back, and colors: written with nothing under it counts as "not written", the same fold loadYaml applies to a whole null document.

key now takes an unquoted digit. YAML reads key: 1 as a number, which is a natural thing to bind and previously killed the whole file; wk reads it back as the digit that was typed. This also removes an inconsistency, since symbols.keys already accepted unquoted digits because JS object keys are always strings.

schemas/*.json is no longer committed. mise run generate:schemas writes it into dist/schemas/, and mise run dist <target> packs the binary, the schemas and the license into wk-<target>.tar.gz, following the layout 844196/chroma uses. The Check workflow runs mise run dist for one target so broken packaging is caught before a tag is pushed.

Breaking changes

Release artefacts are now tarballs. Extract and put wk into $PATH; the README's installation steps are updated. The archive holds wk, LICENSE and schemas/.

A field that violates the schema stops wk. Anything that used to be ignored is now rejected: a non-boolean eval or accept, a non-scalar extra field on a command, a colour outside -1255 or not matching ^#[0-9a-fA-F]{6}$, an unknown field on a type: bindings group, a non-integer timeout, a non-string desc / icon / symbols.*.

Migration is to run wk once and follow the path in the error. My own ~/.config/wk/{config,bindings}.yaml needed no changes.

Verification

  • mise run check and mise run gha:lint clean.
  • e2e 106/106. 09_validation.bats is new (20 cases covering each class above); the three invalid format assertions in 04_config.bats are updated, and 01_protocol.bats's "non-scalar fields are dropped" is removed because it encoded the old silent-drop behaviour.
  • Swept ~40 malformed inputs through parseContext / parseBindings checking that no message falls back to zod's bare Invalid input@zod/mini ships no locale, so every expectation is spelled out.
  • Confirmed ContextSchema.parse({}) is byte-identical to the deleted defaultContext, and that a partial colors: or symbols.keys: still merges with the defaults rather than replacing them.
  • Reproduced every crash and protocol corruption listed above against the built binary, before and after.
  • Measured: bundle 112.5 KB → 144.6 KB, wk init unchanged at 17 ms (the schema is loaded lazily, since wk init has nothing to validate), wk run 17 ms → 20 ms. z.compile() was measured and rejected: it costs 4.2 ms to compile in order to save 0.07 ms of parsing.

@844196 844196 self-assigned this Sep 2, 2026
@844196
844196 requested a balanced review from Copilot September 2, 2026 18:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Generated schemas misrepresent accepted null containers, and the new timeout limit is undocumented.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Centralizes configuration validation, defaults, inferred types, and generated JSON Schemas in src/schema.ts.

Changes:

  • Adds comprehensive Zod-based runtime validation and error paths.
  • Generates schemas and packages releases as tarballs.
  • Expands validation tests and installation guidance.
File summaries
File Description
src/schema.ts Defines schemas, defaults, types, and parsers.
src/run.ts Validates loaded YAML before execution.
src/main.ts Uses schema-derived types.
src/ui.ts Uses schema-derived context types.
src/types/Binding.ts Removes handwritten binding types.
src/types/Context.ts Removes handwritten context types/default merging.
scripts/generate-schemas.ts Generates distributable JSON Schemas.
schemas/config.json Removes committed generated schema.
schemas/bindings.json Removes committed generated schema.
README.md Documents tarball installation and schema use.
mise.toml Adds schema generation and packaging tasks.
e2e/tests/09_validation.bats Adds field-validation coverage.
e2e/tests/04_config.bats Updates configuration error assertions.
e2e/tests/01_protocol.bats Removes obsolete silent-drop behavior.
deno.jsonc Adds the Zod import.
deno.lock Locks the Zod dependency.
CLAUDE.md Documents the schema workflow.
.github/workflows/release.yaml Publishes release tarballs.
.github/workflows/check.yaml Verifies packaging in CI.
.claude/skills/run-wk/SKILL.md Updates validation guidance.
.claude/skills/run-wk/driver.sh Documents exit code 7.
Review details
  • Files reviewed: 20/21 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/schema.ts Outdated
Comment thread src/schema.ts
Comment thread README.md Outdated
Only the outermost shape of config.yaml and bindings.yaml was checked, so a
field could be wrong in a way nothing noticed. Some of those reached the
drawing code and crashed with a stack trace, leaving the menu on screen;
others were quietly ignored, and a bad outputDelimiter put stray bytes into
BUFFER.

src/schema.ts is now the single source of truth. The types, the defaults, the
runtime checks and the distributable JSON Schemas all derive from it, so a
field can no longer be enforced in one place and forgotten in another. A
violation stops wk with exit 7 and one line naming the path that failed
(`bindings.yaml: [0].bindings[0].key: expected a key name or a digit 0-9`).
What the schema does not describe — an unknown top-level config field — is
still left alone.

An unquoted digit is now accepted as a key: YAML reads it as a number, which
wk reads back as the digit that was typed.

The schemas are generated rather than committed, and ride in the release
archive, so they cannot drift from what wk enforces.

BREAKING CHANGE: releases ship a tarball holding the binary, the schemas and
the license, rather than a bare binary.

BREAKING CHANGE: a field that violates the schema stops wk. Values that used
to be ignored — a non-boolean eval, a non-scalar extra field on a command, a
colour outside the ANSI range, a timeout that is not a whole number of
milliseconds or that runs past five minutes — are now rejected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@844196
844196 force-pushed the feat/schema-driven-validation branch from 0e6fea0 to d5906b8 Compare September 2, 2026 18:22
@844196
844196 requested a balanced review from Copilot September 2, 2026 18:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Protocol ordering, Unicode delimiter validation, and the undocumented timeout limit need resolution.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

src/schema.ts:170

  • This introduces an undocumented breaking limit: integer timeouts above five minutes worked previously, while the PR's breaking-change section only says non-integer timeouts are rejected. The comment cites V8's 2^31−1 ms timer limit, which does not justify a 300,000 ms ceiling. Either use the actual timer-safe maximum or explicitly document the new five-minute restriction and migration impact.
      z.maximum(300_000, expected('300000 (5 minutes) or less')),
  • Files reviewed: 20/21 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread src/schema.ts
Comment thread src/schema.ts
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@844196
844196 force-pushed the feat/schema-driven-validation branch from d5906b8 to c10dc6b Compare September 2, 2026 18:46
@844196
844196 requested a balanced review from Copilot September 3, 2026 06:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The new five-minute timeout ceiling rejects valid existing configurations despite wraparound occurring only at a much higher timer limit.

Review details

Suppressed comments (1)

src/schema.ts:170

  • This 5-minute ceiling rejects previously valid, correctly functioning configurations such as timeout: 600000. The stated timer-wrap rationale only applies above 2,147,483,647 ms, and the PR's breaking-change section mentions non-integer timeouts but not this much lower new limit. Please either cap at the actual timer limit or explicitly make and document the additional five-minute API restriction.
      z.maximum(300_000, expected('300000 (5 minutes) or less')),
  • Files reviewed: 20/21 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@844196
844196 merged commit 1d77f36 into main Sep 3, 2026
4 checks passed
@844196
844196 deleted the feat/schema-driven-validation branch September 3, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants