fix: reject output shapes that redeclare reserved envelope fields - #52
Merged
Conversation
Contributor
|
🎉 This PR is included in version 0.3.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Output shapes passed to the type helpers could redeclare reserved envelope fields (
ok,message,instructions). Because the author shape was spread after the composed envelope fields, an author declaration silently overrode the envelope contract —procedureTypeOutput({ instructions: z.string().optional() })quietly madeinstructionsoptional, defeating the whole point of the Procedure type.This change makes that collision loud at both layers:
output/fieldsnow reject reserved keys at compile time viaNoReserved<R>, applied to the three standalone helpers and to the three per-type subcommand spec interfaces.assertNoReservedCollisionthrows aRangeErrornaming every colliding key in author declaration order, as the first statement of each helper. Since all three per-type subcommand methods funnel through the standalone helpers, one guard covers the class path, the legacytool.subcommandpath, and the@deprecatedl1Output/l2Output/l3Outputaliases with no extra wiring.Reserved sets, declared once in
src/output-helpers.tsand consumed by both layers:dataTypeOutputok,messageclassificationTypeOutputok,messageprocedureTypeOutputok,message,instructionsclassificationstays a legal author-declared key — it is a required key inside the author's own shape by design, structurally different from a fixed spread-in field.Implementation notes
RESERVED_SUBCOMMANDSprecedent (src/args.ts:18): declared in the file that owns the concept, exported at module level, absent from thesrc/index.tsbarrel.RangeErrorstyle insrc/tool-class.ts:279-284(fact + em-dash + causal clause).types.ts → output-helpers.tsis type-only; no cycle.--schemaJSON Schema (ok,message,count→count,ok,message), breaking an invariantsrc/tool-class.ts:199-201deliberately maintains.Breaking-change assessment
Throwing where the helpers previously accepted silently is technically breaking, but it only breaks code that was already silently broken. A search across
src/,tests/,examples/changeset.tsandREADME.mdfor colliding declarations returns zero hits — nothing in this repo collides. Ships asfix:.Testing
tests/output-helpers.test.ts(new, 295 lines) — helper-level unit tests with noTooland noinvoke. The 9 schema-level tests moved out ofindex.test.ts, plus rejection tests per helper (reserved key present, weakened with.optional(), multiple simultaneous collisions) and pass-through tests (empty shape, no-arg overload, near-miss casing, prototype-inherited key,classificationalone and with extras).tests/index.test.ts— registration-path parity: a collidingoutputpassed to each ofdataSubcommand,classificationSubcommand,procedureSubcommand.@ts-expect-errordirectives double as compile-time assertions: if the type guard regresses,bunx tsc --noEmitfails withUnused '@ts-expect-error' directive.Verification:
bun test→ 180 pass / 0 fail.bunx tsc --noEmitclean.bunx biome check .clean.Files changed
src/output-helpers.tssrc/types.tstests/output-helpers.test.tstests/index.test.tsREADME.mddocs/specs/issue-50-*.mdPlan level:
full/ depthmedium.Closes #50