Validate every configuration field against one schema - #64
Conversation
There was a problem hiding this comment.
🟡 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.
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>
0e6fea0 to
d5906b8
Compare
There was a problem hiding this comment.
🟡 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
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d5906b8 to
c10dc6b
Compare
There was a problem hiding this comment.
🔵 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
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
isPartialContextandisBindingsonly checked "is it a mapping" and "is it an array of objects with a stringkey". Everything below that was unchecked, and the failures fell into three kinds:colors.prompt: {}, atype: bindingswithoutdesc, a group whosebindings:is a scalar,buffer: 42— all reached the drawing code and exited 1 with a stack trace, withouttui.close(), leaving the menu on screen. Same class as (config, bindings): an empty config file crashes instead of falling back #58.outputDelimiter: 42produced4242ls -la, so the widget took4as the delimiter and put stray bytes intoBUFFER. A multi-characteroutputDelimiteror an empty bindingdelimiterdid the same, and atype: commandwithoutbuffersilently inserted an empty line.timeout: "5s"disabled the timeout without saying so. A nestedkey: 1fell 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.tsbecomes the single source of truth. The types, the defaults, the runtime checks and the distributable JSON Schemas are all derived from one@zod/minischema, so a field cannot be enforced in one place and forgotten in another.src/types/Binding.tsandsrc/types/Context.tsare gone (z.infer), andmergeContextis gone (z._default/z.prefault).A violation stops wk with exit 7 and one line naming the path that failed:
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 foldloadYamlapplies to a whole null document.keynow takes an unquoted digit. YAML readskey: 1as 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, sincesymbols.keysalready accepted unquoted digits because JS object keys are always strings.schemas/*.jsonis no longer committed.mise run generate:schemaswrites it intodist/schemas/, andmise run dist <target>packs the binary, the schemas and the license intowk-<target>.tar.gz, following the layout 844196/chroma uses. TheCheckworkflow runsmise run distfor one target so broken packaging is caught before a tag is pushed.Breaking changes
Release artefacts are now tarballs. Extract and put
wkinto$PATH; the README's installation steps are updated. The archive holdswk,LICENSEandschemas/.A field that violates the schema stops wk. Anything that used to be ignored is now rejected: a non-boolean
evaloraccept, a non-scalar extra field on a command, a colour outside-1–255or not matching^#[0-9a-fA-F]{6}$, an unknown field on atype: bindingsgroup, a non-integertimeout, a non-stringdesc/icon/symbols.*.Migration is to run wk once and follow the path in the error. My own
~/.config/wk/{config,bindings}.yamlneeded no changes.Verification
mise run checkandmise run gha:lintclean.09_validation.batsis new (20 cases covering each class above); the threeinvalid formatassertions in04_config.batsare updated, and01_protocol.bats's "non-scalar fields are dropped" is removed because it encoded the old silent-drop behaviour.parseContext/parseBindingschecking that no message falls back to zod's bareInvalid input—@zod/miniships no locale, so every expectation is spelled out.ContextSchema.parse({})is byte-identical to the deleteddefaultContext, and that a partialcolors:orsymbols.keys:still merges with the defaults rather than replacing them.wk initunchanged at 17 ms (the schema is loaded lazily, sincewk inithas nothing to validate),wk run17 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.