Skip to content

types/rtti: own unit-bit subtraction as withoutUnits - #1591

Merged
sergey-shandar merged 2 commits into
mainfrom
claude/rtti-without-units
Aug 15, 2026
Merged

types/rtti: own unit-bit subtraction as withoutUnits#1591
sergey-shandar merged 2 commits into
mainfrom
claude/rtti-without-units

Conversation

@sergey-shandar

Copy link
Copy Markdown
Contributor

Implements fjs/media/json/schema/todo/strip-undefined-set-op.md (deleted here).

Why

stripUndefined performed a set operation on the rtti data form — remove the undefined unit from a union — by enumerating every member of UnionSet to copy it:

const stripUndefined = n => {
    if (typeof n === 'string') { return n }
    const unit = (n.unit ?? 0) & ~undefinedBit
    return {
        ...(unit === 0 ? {} : { unit }),
        ...(n.number === undefined ? {} : { number: n.number }),
        ...(n.string === undefined ? {} : { string: n.string }),
        ...(n.bigint === undefined ? {} : { bigint: n.bigint }),
        ...(n.array === undefined ? {} : { array: n.array }),
        ...(n.object === undefined ? {} : { object: n.object }),
    }
}

The duplication is the smaller problem. The TODO's real point is the failure mode: if UnionSet ever gains a kind, unionSchema's per-kind eliminator visibly stops handling it, while this one silently drops it from every optional property's schema.

What

fjs/types/rtti/data owns UnionSet and its algebra, so it gains the missing operation:

/** @type {(bits: number) => (n: UnionSet) => UnionSet} */
export const withoutUnits = bits => n => {
    const unit = (n.unit ?? 0) & ~bits
    const { unit: _, ...rest } = n
    return unit === 0 ? rest : { unit, ...rest }
}

and stripUndefined becomes:

const stripUndefined = n =>
    typeof n === 'string' ? n : withoutUnits(undefinedBit)(n)

Spreading the other five kinds rather than copying them is what closes the failure mode — a kind added later cannot be dropped by code that never names it.

Two scoping decisions

  • withoutUnits takes UnionSet, not Node. The TODO sketched (n: Node) => Node. The reference (string) case is kept in the schema module because why a reference is passed through unchanged is a schema-level judgment — "its definition is shared, and the extra { "not": {} } member it may carry matches no JSON value anyway" — not part of the set algebra. fjs/types/rtti/data stays about sets.
  • merge's field-by-field enumeration is left alone. It looks like the same smell but is not: each kind needs its own cmpItem, so it is a genuine per-kind eliminator, exactly like unionSchema. Only the unit-only operation can avoid naming the kinds.

Verification

  • npx tsc clean; fjs test 2844 pass, 0 fail.
  • npm run cov: fjs/types/rtti/data/module.f.mjs and fjs/media/json/schema/module.f.mjs both 100% lines/branches/functions.
  • New proof covers the drop-empty-unit-key rule, a partial removal, a no-op removal, never, and — the point of the change — that the other five kinds survive untouched.
  • Differential check on generated schemas. The rewrite changes the key order of the returned UnionSet (unit first, then the node's own order, matching merge), so I compared toJsonSchema output across 13 shapes built around optional properties — optional string/number/bigint/boolean/array/record/unknown, optional unions, nested and deeply nested optionals, an all-optional struct, and an optional whose set is only undefined. Byte-identical, no case throwing.

Changelog:

  • types/rtti/data: new withoutUnits removes unit bits from a union set, dropping the unit key when it empties. media/json/schema uses it instead of rebuilding the union field by field

🤖 Generated with Claude Code

https://claude.ai/code/session_016HvbYkBMYWwQECL7myLhqs


Generated by Claude Code

Working notes; the PR title and description are the commit message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HvbYkBMYWwQECL7myLhqs
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HvbYkBMYWwQECL7myLhqs
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
functionalscript 55d5231 Commit Preview URL

Branch Preview URL
Aug 15 2026, 06:55 PM

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Equivalence verified exhaustively rather than by reading. stripUndefined is the only call site, and its result feeds only nodeSchema/unionSchema/cmp, all of which read kinds by name — so the two real differences from the old inline code (key order: unit first, then the node's own; and explicit-undefined kind keys surviving instead of being dropped) are unobservable.

Checked over all 16 unit bitsets × all 32 kind-presence combinations (512 nodes, incl. {}/never, unit-only, no-unit, top): identical key sets and values both ways. Also ran toJsonSchema from origin/main against this PR's over 20 shapes — optional string/number/bigint/boolean/array/record/unknown/union/null/tuple, { a: () => undefined }, all-optional, nested and 4-deep optionals, optionals inside array/record, top-minus-undefined — byte-identical JSON, nothing thrown.

No mask concern: withoutUnits hardcodes nothing, and the only mask is the caller's unitBit(undefined) = 2, matching unitList = ['null','undefined','false','true'] as the existing unitBits proof asserts. fjs/types/rtti/data/proof.f.mjs:122-144 covers both branches of n.unit ?? 0 and unit === 0 ? …, including empty-key drop, partial, no-op, never and top.

npx tsc exit 0; fjs t → 2844 pass / 0 fail; fjs ci leaves the tree clean. Changelog matches the 3-line form and the implemented todo strip-undefined-set-op.md is deleted here.

@sergey-shandar
sergey-shandar added this pull request to the merge queue Aug 15, 2026
Merged via the queue into main with commit ff0fae9 Aug 15, 2026
19 checks passed
@sergey-shandar
sergey-shandar deleted the claude/rtti-without-units branch August 15, 2026 20:09
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.

3 participants