Skip to content

fix(core): close isCompatible() correctness holes for cross-app duck typing#80

Merged
cuibonobo merged 3 commits into
mainfrom
claude/design-review-54-5ur0ig
Jul 10, 2026
Merged

fix(core): close isCompatible() correctness holes for cross-app duck typing#80
cuibonobo merged 3 commits into
mainfrom
claude/design-review-54-5ur0ig

Conversation

@cuibonobo

Copy link
Copy Markdown
Member

Summary

Fixes #54. isCompatible() had two correctness holes and a spec example that contradicted the implementation, which matters because #47 promotes it to the official cross-app duck-typing mechanism.

  • Optional-field hole: the predicate checked whether the candidate schema declared a field, not whether it declared it required. A candidate with an optional title passed a required-title check, so a consumer trusting isCompatible() could read undefined from a field it was told was guaranteed. Fixed by requiring field.required === true on the candidate.
  • text vs string: these are distinct ScalarFieldKinds compared with strict equality, but the spec's own motivating example ({ text: string }) doesn't work under the shipped predicate since long-form text is typically declared kind: 'text'. Added a READ_COMPATIBLE table making stringtext mutually acceptable (identical value sets) while every other kind pair stays exact — notably date is not compatible with string, since date carries a parse/validity guarantee a plain string doesn't.
  • Shallow array/object matching: only top-level kind was checked, so a candidate's nested object.properties or array.items could silently drop a required sub-field. Now recurses using the same (strengthened) predicate.

Also scopes isCompatible() explicitly as read-compatibility (licenses consuming records, not writing them) in both the code doc comment and docs/spec.md §Type compatibility, whose inlined copy of the function is updated to match.

Test plan

  • Added tests: optional-field-on-candidate now fails a required check; text/string accepted both directions; date/string still rejected both directions; nested object required-subfield mismatch; nested array item-kind mismatch (both a true mismatch and a text/string-equivalent pair); the spec's own example as a fixture.
  • pnpm --filter @haverstack/core test — all 256 tests pass
  • pnpm --filter @haverstack/core typecheck and lint — clean
  • Confirmed via git stash that the pre-existing wire-types typecheck failure is unrelated to this change (missing built @haverstack/core output, not a type error)

Generated by Claude Code

claude added 3 commits July 10, 2026 02:19
…typing

Require field.required === true on the candidate instead of mere presence
(a candidate declaring a required field optional no longer passes),
add a text/string read-compatibility table so the spec's own motivating
example works, and recurse into object properties and array items so
nested required-field mismatches are caught.

Scope isCompatible() explicitly as read-compatibility in both the code
and the spec, matching #54.

Refs #54
…table

The spec previously duplicated the runtime implementation in a code
block, which is exactly what let it drift from the real function (the
bug this PR fixes). Describe the rule in prose, present the kind
compatibility relation as a table, and point to the source/tests as
the single authoritative reference instead.
…emas

A candidate schema can originate from another app's Type definition —
the untrusted side of duck-typed consumption — so deeply nested
array/object fields could recurse unboundedly and overflow the call
stack. Cap recursion at 32 levels (matching MAX_VALIDATION_DEPTH in
validate.ts) and fail closed: past the limit, treat as incompatible
rather than risk a crash.
@cuibonobo cuibonobo merged commit 845650f into main Jul 10, 2026
5 checks passed
@cuibonobo cuibonobo deleted the claude/design-review-54-5ur0ig branch July 10, 2026 02:36
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.

isCompatible() is too weak to be the official cross-app mechanism #47 makes it (optional-field hole, text vs string)

2 participants