types/rtti/todo: close counts a trailing undefined as a present member - #1716
Conversation
…ember RTTI states one rule for absence — an absent member reads as `undefined`, so a member is required exactly when its set excludes `undefined` — and `close` does not extend it past the declared positions. `fits` is `value.length <= declared` in both schema-form readers, and the data form says the same as a `rest` of `never`, so `close([number])` rejects `[1, undefined]` and a hole alike while the open form and a declared `option` position accept both. The hole is the sharper witness: it has no entry for `undeclaredEntries` to find, so only the length check rejects it — `length` being the one attribute the absence rule says stops being observable after the last required position. It is load-bearing rather than academic. `or(close(short), close(long))` is the only optional-tail spelling that rejects a present-but-`undefined` slot, and `fjs/edag` states its uniqueness claim as literal on the strength of it: of the three values that follow the canonical `['.', a, 'b', null]`, only the `'extra'` one is rejected for the reason its README gives, and the two spelling absence are rejected by `fits`. Records the two coherent answers, what each costs, and who has to hear the result. No behavior change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
…property-lambda-7fho1u
close trailing undefined behavior decisionclose counts a trailing undefined as a present member
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
functionalscript | 96dacce | Commit Preview URL Branch Preview URL |
Aug 26 2026, 08:36 PM |
o2alexanderfedin
left a comment
There was a problem hiding this comment.
The problem is real. The four-row table reproduces exactly — I ran all 32 cells (4 schemas × 8 values) through validate, parse and the data form at head and at base; identical at both, all three readers agree everywhere, every claimed verdict confirmed. The README genuinely does not carve close out of the absence rule, fits really is value.length <= declared in both schema-form readers with the struct one () => true, the data form really does say the same via containerUnion(state, c, never), and edag really is the sole consumer (grep outside fjs/types/rtti/ finds only fjs/edag/module.f.mjs; the effects/node hits are fh.close()). I also checked the thing #1712 could have disturbed: undeclaredEntries still walks the value with Object.entries — only schemas moved to Array.from.
But three of the load-bearing claims don't hold.
The mechanism attribution is false, and it's the half that got promoted into the headline table. closeContainerValidate is extra.length === 0 && fits(...) — && short-circuits, so fits is never reached when there are undeclared entries. An explicit trailing undefined has an own enumerable entry; a hole doesn't:
[1, undefined] entries=[["0",1],["1",undefined]] extra=[["1",undefined]] -> undeclaredEntries
[1, ,] hole entries=[["0",1]] extra=[] -> fits
Patching both tuple fits to () => true settles it: [1, undefined] still errors, the hole flips to ok. So line 75's "fits, on length alone" for ['.', a, 'b', null, undefined] is wrong, and the PR body repeats it. At the real edag schema, two of the three neighbour values are rejected for exactly the reason the README sentence gives — an undeclared member held to an absent rest. Only the hole rests on the length check. The load-bearing claim is about 2× overstated.
The file already knows this, incidentally — lines 40-45 correctly say the hole is the case with "no member to hold to a rest", which implies the explicit undefined has one.
The task list for answer A wouldn't implement A. Lines 120-122 name fits in the two readers plus the closed-tuple conversion. Per the above, changing fits alone leaves the file's own headline example (line 97, close([number]) accepting [1, undefined]) still failing — it would only admit the hole. The real knob is the undeclaredEntries filter, which is shared with the struct kind, so A also has to decide close({a: number}) against {a: 1, b: undefined} (currently an error, with no fits involved). The struct side isn't mentioned.
The option(propertyLambda) implication doesn't follow. Position 3 of close(['.', exp, index, option(propertyLambda)]) is declared, so neither fits nor the extra check governs it — an A/B question about undeclared trailing members can't decide that spelling. Measured, under both answers: ['.',a,'b',null], ['.',a,'b'], ['.',a,'b',undefined] and ['.',a,'b', ,] all validate. Your own row 3 corroborates it — close([number, option(string)]) already accepts [1] and [1, undefined], and the file endorses that row. So lines 82-87 need to go or be re-derived.
Two smaller things:
A third answer is missing, and it may be the right one. Given the above: a hole is absence, an explicit undefined is a present member. That's coherent with the absence rule, it's the smallest change that removes the one case genuinely resting on length, and it's exactly what "change fits alone" would produce. Your title — "close counts a trailing undefined as a present member" — is literally true and arguably describes correct behaviour under it.
#1708 is still open, but line 84 and the body use landed tense ("Ts<> now renders"). The Related section's "if it lands first" shows you know; the tense should match. Also line 122-123 says the acceptance table "carries no closed trailing-undefined row today" — validate/proof.f.mjs:95 already pins the hole row [close([number]), [42, ,]]; only the explicit-undefined row is missing.
Gates: npm test 3396/0, tsc --noEmit exit 0. Body's fjs t 3393/3393 is stale by exactly the three cbase32 proofs #1710 added. Changelog: none is correctly placed with a single trailer.
…`Ts<>` Review on #1716 found three claims that do not hold, all verified here. `closeContainerValidate` ends in `extra.length === 0 && fits(...)`, and `&&` short-circuits: an explicit trailing `undefined` is an own enumerable entry, so the extra check rejects it and `fits` is never reached; only a hole, which is no entry, reaches `fits`. Neutralizing both tuple `fits` settles it — the explicit `undefined` still errors, the hole flips to ok. So the file's headline attribution was wrong, and at the edag schema two of the three neighbour values are rejected for exactly the reason its README gives, not one. The task list for answer A therefore named the wrong knob: it is the `undeclaredEntries` filter, which is shared with the struct kind, so A has to answer `close({ a: number })` against `{ a: 1, b: undefined }` too. The `option(propertyLambda)` implication does not follow — position 3 there is declared, so neither half governs it, and all four spellings validate under every answer. Re-derived around `or(close, close)`, which the extra check does govern, and the paragraph now says why the `option` spelling is out of scope. Adds answer C, which the review proposed, and the argument against moving either half: `arraySetValidate` already records that the length check is what keeps a closed tuple's set equal to what `Ts<>` and JSON Schema render it as. That reframes the issue as a choice between two correspondences, and on the evidence B — the incumbent, documented — is the answer. Its own last task said C would leave edag's "exactly one" claim true; C admits a hole-padded second spelling, so it would not. Also: the acceptance table already pins the hole row, only the explicit `undefined` one is missing; #1708 is open, so the conditional tense. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
…/github.com/functionalscript/functionalscript into claude/edag-optional-property-lambda-7fho1u
|
All three hold. Verified each independently before changing anything; Mechanism. Confirmed by your method — neutralizing both tuple Task list for A. Corrected to the
Answer C added as you proposed. One thing your framing led me to that changes the recommendation: Smaller ones: proof-table row corrected to "the hole row is already at Thank you — the mechanism error was in the one sentence the whole file was built on. Generated by Claude Code |
o2alexanderfedin
left a comment
There was a problem hiding this comment.
The mechanism correction is right, and it didn't over-correct — the file doesn't claim the hole is an undeclaredEntries case or that fits is dead. I reproduced the whole entries/extra split, and the fits → () => true patch (both tuple readers plus the data form's value.length <= pn) does exactly what lines 51-53 say: [1, undefined] still errors, the hole flips to ok. The struct claim at line 135 checks out too. The option(propertyLambda) section is now correctly ruled out of scope, and I confirmed it by measurement rather than by reading — all four spellings validate against close(['.', exp, index, option(propertyLambda)]) both patched and unpatched, while the landed propertyLambda version rejects three of the four.
The edag dependency genuinely holds, which I'd wondered about: under the fits patch the hole validates at the real exp schema, so #1711's structural minimality does not independently exclude it. The 2-vs-1 split at lines 105-108 is exact.
One material finding, and it's the mirror image of last time:
Answer A's knob is now wrong in the other direction, and A-as-tasked is a no-op for the tuple kind. Line 133-134 says "the knob is the undeclaredEntries filter, not fits", and the A task names only that filter plus its data-form counterpart. But [1, undefined] trips both halves of extra.length === 0 && fits(...) — it is an undeclared entry and the array is length 2. Measured, with undeclaredEntries patched to drop undefined-valued extras and fits left alone:
close([number]): [1] ok [1, undefined] error [1, ,] error
Both knobs patched:
close([number]): [1] ok [1, undefined] ok [1, ,] ok
So A as specified changes nothing at all in the tuple kind. Its own headline example at line 128 doesn't happen, the hole stays rejected, and that contradicts line 127 ("both absence") and lines 196-198 ("Both admit a hole-padded array"). The truth is both knobs: A = C plus the extra-check change. The previous version said "fits alone"; this one says "not fits"; neither is right.
Same finding, one level down: line 178's "decide the struct kind, which shares that filter" holds for undeclaredEntries in validate and parse, but not for the data form. toData(close({a: number})) encodes closedness as rest = never and goes through objectSetValidate, which has no extra-length check at all, while toData(close([number])) is a prefix with no rest. So a "yes" on the struct question has a fourth site the task list never names.
Everything else verified: third answer C is present with knobs that match what the fits experiment actually flips, edag is still the sole close( consumer, all ten links resolve, the arraySetValidate and README quotes are verbatim, Ts<close([number])> is readonly [number] (checked with an invariant pin plus a negative control), #1708's tense is fixed, and the validate/proof.f.mjs:95 hole-row claim is corrected. The branch touched exactly one file — the flakes came in via the main merge. The body was updated and no longer carries the false mechanism claim, so this finding is file-only.
Gates: npm test 3396/0 matching the body, tsc --noEmit exit 0, purge matched nothing.
Second review on #1716: the previous fix corrected the mechanism attribution and then drew the wrong conclusion from it. `[1, undefined]` trips both halves of `extra.length === 0 && fits(...)` independently — it is an undeclared entry *and* the array is one longer than declared — so the short-circuit says which half fires first, not which one to change. Measured both ways: filtering `undefined`-valued extras out of `undeclaredEntries` with `fits` left alone changes nothing in the tuple kind, and patching both is what makes `close([number])` accept `[1, undefined]` and the hole. So A is C plus the extra-check change, and the file's own headline example for A did not follow from the task list it gave. "`fits` alone" was wrong one way; "not `fits`" was wrong the other. The struct question has a fourth site the task list never named: the data form encodes a closed struct as `rest: never` and reads it with `objectSetValidate`, which carries no length analogue, so `{ a: 1, b: undefined }` is rejected there by the `rest` alone. Tasks reordered to C-then-A, since A is now stated as a superset of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
|
Both hold, and the first one is squarely my error. A needs both knobs. Reproduced exactly: patching The fourth site. Confirmed — Nothing else changed, and the body needed no edit — as you say, it never carried the knob claim. Two rounds, two errors on the same sentence, in opposite directions. The thing I'd missed both times is that "which check rejects this value" and "which check to change to stop rejecting it" are different questions when the two are independent and one short-circuits. Generated by Claude Code |
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Approving. The correction is right and doesn't over-correct.
I re-ran both experiments. Patching only the extra check (undeclaredEntries plus arraySetValidate's entry filter) leaves close([number]) rejecting both [1, undefined] and the hole — a genuine no-op for the tuple kind, exactly as the file now says. Patching only fits flips the hole to ok while [1, undefined] still errors. Both knobs: all three accepted. So "either knob alone leaves [1, undefined] rejected" holds in both directions, and A = C plus the extra-check change is the right statement.
C's stated knobs still match what the fits-only patch actually flips, including leaving the struct kind untouched — so C is correctly not given the extra check. Nothing stale elsewhere: no surviving "not fits", and line 51's "only fits rejects it" is about the hole and is correct.
The fourth site is confirmed, and there's a sharp piece of evidence for it: under the extra-check-only patch, close({a: number}) against {a: 1, b: undefined} flips to ok in validate and parse but stays error in the data form. That's the site the old task list never named — a closed struct encodes rest: never, and objectSetValidate has no length or extra-count check at all, so the extra entry dies on never instead.
One note, not blocking: the body is silent on the conclusion this commit added. It never carried the wrong claim, so nothing stale ships — but the squash commit won't mention that A costs both knobs or that the struct question has a fourth site.
Gates: npm test 3396/0, tsc --noEmit exit 0, purge matched nothing.
|
Thanks — and taken on the non-blocking note: the body now carries both, so the squash commit will. It states that either knob alone leaves No file or code change in this round; body only. Generated by Claude Code |
|
Heads up on a red check that isn't this PR's, and that I can't clear myself.
Not attributable to this PR: the diff is one markdown file under The fix is one re-run, and Nothing else is outstanding: green elsewhere, approved, level with Generated by Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b2019272b9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
`parse-omits-undefined-members.md` is in the tree now, so the references to it as a pending PR are stale: answer A cites it directly, the `option` note says the type-level spelling exists rather than that it would, and the Related entry names the file instead of the PR. An earlier draft of this file said to fold into that one if it landed first. Reading it in place, that is wrong, and the entry now says so: it asks what `parse` *builds* at a **declared** position it found absent, this asks whether an **undeclared** trailing `undefined` or hole is a member at all. Either can be answered without the other, and answer A here is the only thing that would make them meet. Re-verified at this head that #1708 changed no runtime acceptance: the four schemas by three spellings, the `or(close, close)` rows, and the four edag neighbours all answer exactly as before across `validate`, `parse` and the data form. `arraySetValidate`'s comment, which this file quotes, is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ce5feaf739
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A review bot reports that B's documentation task is already satisfied: "Closed containers" says `close(c)` admits the declared members and no others, lists `close([number])` as "arrays of exactly one number", and requires a wrapped const rest for undeclared members that must be `undefined`. Half right, and worth taking. The explicit-`undefined` half *is* inferable — the wrapped-const rest would be pointless if a bare `close(c)` admitted one — so asking for it again risks a second telling that contradicts the first. The **length** half is not: a hole is not an enumerable entry, and no passage says a closed container bounds `value.length`, which is the only thing that rejects `[1, ,]` and the case the `Ts<>`/JSON Schema defence is actually about. So B's task now asks for that one sentence and says explicitly not to restate the other half. The bot's other finding — #1708 called open, with a conditional folding instruction — was already fixed in `ce5feaf`; it reviewed the parent commit.
…hree ways A review bot found the case this issue was missing, and it is a defect rather than a decision. Both length checks sit in the no-`rest` branch, so supplying a `rest` skips them; a hole is no entry, so it meets nothing on the way through. The data form reads a normalized set rather than a spelling and still applies its own. Measured: `close([number], never)` accepts `[1, ,]` in `validate` and `parse` and rejects it in the data form, while `close([number])` rejects it in all three — and `cmp` reports the two `toData` results equal, so these are one schema. `never` is the public spelling of the exact-members set, and the README says `close(c)` and `close(c, undefined)` are one spelling, so no answer here can leave them disagreeing. Recorded as its own subsection, listed first in the tasks as independent of the A/B/C choice, with the narrow fix — consult `fits` on both branches, since the `rest` loop constrains the entries a value has and `fits` constrains how long it may be. B is no longer documentation-only, and the priority is now P2 for the defect with the decision it sits inside still P3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
…/github.com/functionalscript/functionalscript into claude/edag-optional-property-lambda-7fho1u
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 028d913b26
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The bot caught it in the proposal rather than the diagnosis. `fits` is `value.length <= declared`, so consulting it wherever a `rest` is present would reject exactly the values a `rest` exists to admit: measured, `close([number], string)` accepts `[1, 'x']` in all three readers today, and the proposed fix would have made all three reject it. The measurement also shows the right fix. `toData(close([number], never))` and `toData(close([number]))` are the same data — no `rest` at all — so the data form has dropped the empty rest before it validates and takes the length bound, while the schema-form readers still see `rest !== undefined` and skip it. Dropping an empty `rest` before the branch in `closeContainerValidate` and `closeContainerParse` makes `close(c, never)` *be* `close(c)`, which is what the README already says it is, and leaves every non-empty `rest` alone. `arraySetValidate` needs no change — its half is the one that is right. Records the wider alternative too (validate a trailing hole against the `rest`) and what it would cost: `close([number], string)` against `[1, ,]` would move from ok to error in all three readers, where they agree today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e379d2b6c4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…G label Three findings from the fifth review round, all verified and all valid. Emptiness has to be detected semantically, not as the exported `never`. Measured: `close([number], never)`, `close([number], or())` and `close([number], close([never]))` all accept `[1, ,]` in `validate` and `parse` while their canonical data equals `close([number])` and rejects it — so an implementation keying on the singleton would pass a `never`-only proof with the disagreement intact. The task now says so and asks for one independently constructed empty rest in the proof. A row in the shared acceptance table is not enough for either task that adds one: it pins that the three readers agree, so it passes whenever all three move together. Both bullets now ask for an outright verdict beside the row, as `optionalPositions` does. And the changelog label was wrong. Narrowing `close(c, never)` on a trailing hole is observable for callers using the explicit-rest spelling, whatever the canonical semantics already said, so it is **BREAKING** — as #1712 labelled its analogous reader-alignment change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
The canonicalization table listed `close([number, cu])` and
`close({a:number, b:cu})` among the spellings A would make equal in
membership. It would not. A closed container reads a declared member
through `getItem` — `value[k]`, which walks the prototype chain — and
enumerates undeclared ones with `Object.entries`, which is own-only, so an
inherited member is invisible to the shorter schema and fatal to the
longer one. Measured on `Object.setPrototypeOf([1], proto)` with
`proto[1] = 'x'`: `close([number])` accepts it, `close([number, cu])`
rejects it, all three readers agreeing, today and under a simulated A —
the declared-member walk runs before and independently of `extra`, `rest`
and `fits`, so no patch confined to the undeclared branch reaches it.
Collapsing those two nodes would hand one `cas` hash to sets that differ.
Drops them from the table, leaving four rows, and names them as the two
that look like they belong and do not.
That is the value-side half of `schema-walk-own-indices.md`, whose closing
note calls the value side settled because the readers agree. They do; A is
what would make it matter anyway. Cross-referenced both ways.
Reported by the Codex review bot on #1716.
Changelog: none
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7a3f746baa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…cross The cross-reference added one commit earlier said answer A "asks the canonical form to equate `close([number])` with `close([number, cu])`". That is the pair the same commit removed from the table, so the note contradicted the file it pointed at, and read as making the schema-walk decision a prerequisite. The relationship is the other way round: the own-versus-inherited split is what keeps those two spellings apart, bounding what A may merge rather than being merged across. The pair A does merge is untouched by it — measured on eight values, `[1]`, an explicit trailing `undefined`, a hole, an own `'x'`, and prototypes carrying `1: 'x'`, `1: undefined`, `2: 'x'` and `1: 9` over `[1, undefined]`: `close([number])` and `close([number], cu)` answer alike on every one under a full simulated A, with all three readers agreeing. Both files now say so, and say neither decision is a prerequisite for the other. Reported by the Codex review bot on #1716. Changelog: none Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Approving. All three claims are true, and the first is the sharpest thing in the file.
The prototype asymmetry is real and I reproduced it both ways. getItem reads value[k] through the prototype chain while undeclaredEntries is own-only, so with an inherited index at 1:
[1] against close([number]) ok/ok/ok
[1] against close([number, cu]) error/error/error
{a:1} against close({a:number}) ok/ok/ok
{a:1} against close({a:number,b:cu}) error/error/error
All three readers agree in both directions. The "independently of extra, rest and fits" part holds structurally too — validate/module.f.mjs:216 returns on the declared-member loop's error before undeclaredEntries runs at 220. Dropping those two rows and leaving four is right, and the two toData values quoted for the dropped pair are exactly what the code produces.
The three named dropped rows are all genuinely empty-rest spellings under the equality criterion, all three genuinely disagree today on [42, ,] (ok/ok/error against the base's error/error/error), and they really are non-interchangeable — identity-keyed, empty-union-recognising, and close-cycle-reaching each pass a different subset. The struct-in gate is correct: objectSetValidate has no length or extra-count check and a closed struct encodes rest: {}, so with the struct kind out the readers do still reject {a:1,b:undefined} — collapsing those rows would hand one cas hash to two memberships.
The canonicalization invariant reproduces on all six original rows verbatim, two of which you then correctly retracted two commits later.
I also reviewed e44dda3d4, which landed while this was in flight, and it checks out: under a full simulated A — both knobs in validate and parse, plus the matching extra filter and length relaxation in arraySetValidate — all eight named values come out alike per schema with all three readers agreeing. The reframing ("bounds A rather than blocking it", neither decision a prerequisite for the other) does fix the contradiction 7a3f746ba had introduced.
Nothing I verified before broke: the three knob results and the fourth-site struct result are unchanged, and the unknown separation is intact.
Two things worth a line. The delta is docs-only but it is now two files — schema-walk-own-indices.md gained a cross-reference paragraph after my e0b77df3b approval, so that approval didn't cover it; the body correctly declares it. And the body is stale against the file it describes: it still says "the array and object rest spellings collapse", where 77b55e88a gated the object rows — the file now says "The array rows go with A; the object rows go with struct-in and not otherwise."
Cosmetic: close-counts-trailing-undefined.md:244 is 103 columns where the doc otherwise wraps at ~80.
Gates: npm test 3402/0 — the body's count is current — tsc purge matched nothing.
`close-counts-trailing-undefined.md:244` ran to 103 columns where the file otherwise wraps near 80. Reflowed that paragraph and one 81-column line in the prototype-asymmetry note. No wording changes. Reported by o2alexanderfedin on #1716. Changelog: none Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
|
Thanks — both items fixed, and the first was a real staleness. Body updated. You're right that it still described the object rows as collapsing with the array ones; Reflow in On the scope point — noted, and the body declares it because you're right that your Your structural check at
Generated by Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 673998f46b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The criterion said to drop a `rest` when `toData(close(c, rest))` equals
`toData(close(c))`. Taken with the exported `equal`, that is wrong: rule
names come from the defining function's name with a collision counter, and
the allocation is stateful across one conversion, so a discarded rule can
still reserve a name.
Measured witness — two separately constructed
`const r = () => ['or', undefined, array(r)]`, one in `c` and one inside
the rest:
toData(close([r2, never])) [{},{}] (never)
toData(close([r1], close([r2, never]))) rule named r0
toData(close([r1])) rule named r
`equal` compares recursive definitions by rule name, as its own doc
comment says, so it answers false. The rest is kept and `[undefined, ,]`
stays ok / ok / error — the disagreement the criterion exists to remove.
For contrast, `never` and `close([never])` both satisfy the equality and
are dropped correctly.
States the comparison as up to rule renaming, adds the case as the fifth,
and pins it as the seventh row — on the dropped side, since the rest is
empty and dropping it is the fix.
Reported by the Codex review bot on #1716.
Changelog: none
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Approving. "Up to rule renaming" is necessary, not decoration, and the witness reproduces exactly.
With const r = () => ['or', undefined, array(r)] built twice:
toData(close([r2, never])) = [{},{}] equal to toData(never): true
toData(close([r1], close([r2,never]))) = [{"r0":…},…]
toData(close([r1])) = [{"r": …},…]
equal? false
Structural equality fails while the two schemas denote the same set — the rest is empty by the criterion's intent, but converting it reserves the name r, pushing c's rule to r0. And it's minimal: with a single shared thunk the same pair compares equal, so the witness genuinely needs two separately constructed ones, exactly as the file says.
equal really doesn't compare up to renaming — its own comment at data/module.f.mjs:220-221 says recursive definitions "compare by their rule names, which are derived from the defining functions' names" — so the qualifier describes something the code doesn't do, and the new bullet says so.
No previously verified witness breaks. The qualifier is coarser than structural equality so it can only add drops, and it adds none of the kept rows: the three empty-rest spellings and the new collision witness all drop; r, the a/b cycle and close([], unknown) all stay, each still carrying its rest or its distinct acceptance. All seven proposed rows behave as claimed, the four dropped ones being live reader disagreements today that collapse onto their no-rest baselines. The three earlier claims still hold at this head, prototype asymmetry included. Body is no longer stale — it now records the per-kind split rather than "the array and object spellings collapse".
One suggestion worth a line, since the doc rules out equal without naming a replacement: subset applied both ways is exactly that comparison, and it already gives the right verdict on all seven rows — true for the four dropped, false for the three kept. data/module.f.mjs:546-551 says explicitly that it sees through α-equivalent rules under different names. Naming it makes the criterion implementable with an existing export instead of leaving the implementer to invent an α-equivalence check.
Gates: npm test 3402/0, purge matched nothing, scope still exactly the two .md files.
The criterion ruled out the exported `equal` without naming what to use
instead, leaving an implementer to invent an α-equivalence check. `subset`
applied both ways is that comparison — `dropSubsumed`'s note says it sees
through α-equivalent rules under different names.
Measured on all seven rows, it gives the intended verdict on every one:
row equal subset both ways verdict
never true true drop
or() true true drop
close([never]) true true drop
name collision false true drop
recursive r false false keep
a/b cycle false false keep
unknown false false keep
— including the name collision, the row `equal` gets wrong.
Also records the incompleteness and its direction: `subset` is never true
for a non-inclusion, so a false answer keeps a rest that could have been
dropped, leaving the disagreement in place rather than collapsing two
memberships onto one canonical form.
Suggested by o2alexanderfedin on #1716.
Changelog: none
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
|
Good suggestion — taken in Reproduced your verdict table on all seven rows before adopting it:
Right on every one, including the row I added one line you didn't ask for, because the replacement brings a property the equality didn't have: Your minimality check is the part I hadn't done — that a single shared thunk makes the same pair compare equal, so the witness genuinely needs two separately constructed ones. That's what makes it a real requirement on the comparison rather than an artifact.
Generated by Claude Code |
…iblings Human review, four items, all verified. The migration is not a rename. `option(t)` is `or(t, undefined)`, so it accepts a present `undefined` — measured — and rewriting it to `or(option, t)` narrows every migrated schema; the faithful form is `or(option, t, undefined)`. This issue takes the narrowing deliberately, per site rather than swept, and the changelog now says the schemas got stricter rather than that a spelling changed. The `close` counts were per-line, not per-occurrence: `data/proof.f.mjs` is 50 rather than 39, `parse/proof.f.mjs` (24) was missing, and the two `ts` proofs were uncounted — 124 in all. Stage 2's proof task now names the pin it abolishes, `validate/proof.f.mjs:290`, whose `[undefined, 5]` is commented as the same value as the hole and stops being so. Related now cites both siblings: #1716, whose lever is the `close(c, rest?)` overload stage 1 deletes and which already documents the empty-rest length-check defect this file's `array(or())` row belongs to, and #1719, whose worked examples are written in the eDSL this proposal respells. Changelog: none Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MQwnUe7SYoUFXchmAfx8Yf
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ee4d17876c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…p empty ones
The canonicalization invariant said to collapse the places the canonical
form records a member that "admits only `undefined`". That is too narrow.
A's filter sits on the shared `extra`, before the branch, so a `rest` is
never asked about `undefined` at all — its `undefined` component stops
meaning anything whether or not the rest admits other values too.
Measured under a simulated A with the filter on the shared `extra` in all
three readers, `close([number], option(string))` and
`close([number], string)` answer alike on `[1]`, `[1, undefined]`,
`[1, 'x']`, `[1, ,]` and `[1, 7]` — identical membership, where today only
the first accepts `[1, undefined]`. Their data stay
`rest: {unit:2, string:true}` and `rest: {string:true}`, and both `equal`
and mutual `subset` answer false, so the empty-rest criterion leaves them
apart: one membership, two `cas` hashes.
Restates the invariant as strip-then-drop, with the four-row table as the
special case where stripping empties the rest, adds the mixed-rest
equality to pin, and gates the strip half on the struct decision the same
way the drop half is. Also notes that neither half touches a declared
position, which the prototype witness keeps distinct.
Reported by the Codex review bot on #1716.
Changelog: none
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 611e96b8a6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…rrower than stated Two problems with B's task. The home was wrong. `fjs/AGENTS.md` puts API invariants in the JSDoc on `module.f.*` exports and reserves the README for why-this-not-that, but the task named only the README — leaving the exported `close`, and so the emitted declarations and editor hovers, without the fact. Its doc comment today calls `close([number])` "exactly one number", which is true and still leaves a reader unable to predict that a trailing hole is rejected. The claim was over-broad. "A closed container bounds `length`" is not true: measured, `close([number])` rejects `[1, ,]` while `close([number], string)` accepts it, because a `rest` skips the branch both length checks sit in, and the struct kind has no length to bound — `fits` is `() => true` there. It is a **no-rest closed tuple** that bounds `length`. Reported by the Codex review bot on #1716. Changelog: none Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 53f5fe2172
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A thunk can be both a declared member and the rest, and the data form
points both at one rule: `const r = () => ['or', undefined, array(r)]`
with `close([r], r)` converts to `prefix: ["r"]` and `rest: "r"`. So a
strip implemented as a rewrite of rule `r` strips the declared position
too. Measured against A's data form:
value original naive strip derived rest
[] ok error ok
[undefined] ok error ok
[[]] ok ok ok
[[], undefined] ok ok ok
[[], []] ok ok ok
`[]` and `[undefined]` are values the rest never sees, and the naive
rewrite rejects them. A context-local derived rest — `array(r)` in place
of the reference — leaves `r` alone and agrees with the original on all
five.
Adds the case and its proof obligation, with a note to measure it against
A's data form: under today's `extra` a stripped rest rejects
`[[], undefined]` for an unrelated reason, which is a way to mistake the
sound rewrite for a broken one.
Reported by the Codex review bot on #1716.
Changelog: none
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Approving. 611e96b8a is the strongest claim in the branch and it is empirically correct — I built the case rather than reasoning about it.
Baseline: close([number], option(string)) and close([number], string) differ on exactly one value, [1, undefined] — ok/ok/ok against error/error/error. Under simulated A, with undefined-valued entries filtered off the shared extra in all three readers plus both tuple fits and the length check neutralised, they answer identically on all five values with all three readers agreeing — while their data stay distinct (rest:{unit:2,string:true} vs rest:{string:true}), equal false and mutual subset false. So two canonically distinct forms would denote one set, and the empty-rest drop criterion can't catch it: one membership, two CAS hashes. The strip half is genuinely needed, not belt-and-braces.
The mutual-subset comparison gives the right verdict on all seven rows, and the name-collision row is exactly where equal fails while mutual subset succeeds — which is the point of naming it. dropSubsumed's note really does describe the α-equivalence property, and subset's own doc matches the sound-but-incomplete sentence verbatim.
Moving B's invariant onto the close export is right by the repo's own table — fjs/AGENTS.md:215 puts API invariants on JSDoc and architectural reasons in the README — and the narrowing to "no-rest closed tuple" is forced by the code: both length checks sit inside the rest === undefined branch, and the struct fits is literally () => true. Measured: close([number], string) accepts [1, ,] where close([number]) rejects it.
Nothing I verified before broke — the three knobs, the fourth-site struct result and the unknown separation all re-measure the same.
Gates: npm test 3402/0, docs-only (two .md), Changelog: none correct.
Measured at 53f5fe217; one further commit ("the undefined strip must be context-local") has landed since and is unreviewed.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d0125c3cb5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… lands in The entry said flatly that the empty-rest spellings stop accepting a trailing hole. That holds only while the length bound is still in place. A and C both remove it, and the fix normalizes those spellings onto the no-rest branch, so afterwards they accept the hole like the base does — measured under a simulated C, `close([number])` and `close([number], never)` both answer ok / ok / ok on `[42, ,]`. Taken first, as the task list has it, the entry is right and the narrowing is real. Taken after A or C, the same change is a widening and the entry would be backwards. `AGENTS.md` gives a PR one improvement, so the fix and the decision are separate entries and the order is a real choice rather than a hypothetical. Records both wordings and the condition that picks between them, and notes that the fix is independent of the decision in what it does, not in how it reads to a caller. Reported by the Codex review bot on #1716. Changelog: none Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 96dacceb3b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| reader-alignment change. Taken **after A or C**, the same change is a | ||
| *widening* — both remove the bound, so `close([number])` and | ||
| `close([number], never)` alike answer `ok / ok / ok` on `[42, ,]`, | ||
| measured under a simulated C — and the entry has to say so instead. The |
There was a problem hiding this comment.
Remove the post-A/C widening changelog instruction
After A or C lands, close([number]) already accepts [42, ,] because those options remove its no-rest length bound, while close([number], never) already accepts the same value because an explicit rest skips that branch. Normalizing the empty rest afterward therefore changes no accepted values, so describing that normalization as a widening would add a breaking changelog entry for a non-behavioral refactor. Fresh evidence beyond the prior comment is the revised text now explicitly—but incorrectly—classifying this ordering as a widening.
AGENTS.md reference: AGENTS.md:L89-L95
Useful? React with 👍 / 👎.
o2alexanderfedin
left a comment
There was a problem hiding this comment.
One finding on the reworded entry: "Taken after A or C, the same change is a widening" isn't supported by its own measurement — under both A and C the fix is unobservable to a caller, not a widening.
The text supports "widening" by measuring that close([number]) and close([number], never) both answer ok/ok/ok on [42, ,] under a simulated C. That reproduces exactly — but it shows the opposite of what it's cited for. Comparing the pre-fix explicit-rest spelling against the post-fix behaviour across all three readers:
B (as bound today) — the fix IS a narrowing:
[42, ,] pre=ok/ok/error post=error/error/error DIFF
[42, , ,] pre=ok/ok/error post=error/error/error DIFF
simulated C: [42] [42,,] [42,undefined] [] [42,43] [42,,,] → every row pre === post
simulated A: same — every row pre === post
The reason is structural, not incidental: under C the no-rest branch is extra.length === 0 and the empty-rest branch validates each extra against never — the same predicate — and arraySet(prefix, never) already normalizes to {array:[{prefix}]}, which is what dropping the rest produces. So the ok/ok/ok in the measurement is C's widening, credited to this change.
That matters for the gate the entry names. changelog/README.md:89-91 requires the **BREAKING CHANGES:** prefix only "when a change breaks the public API". Taken first, the prefix is right — the narrowing is measured. Taken after A or C, the entry owes not a reversed wording but no entry at all, so the bullet's flat opener ("worded for the order it lands in") is wrong in that branch too.
Everything else checks out: the baseline ok/ok/error disagreement reproduces, A really is "C plus the extra-check change" so grouping A with C on "both remove the bound" is correct, and the one-improvement-per-PR premise is right.
Nit: the body still carries Changelog: none, which #1724 removed the need for — CONTRIBUTING.md:201-202 now says a non-behaviour PR "needs no entry and omits the section entirely."
Gates: npm test 3402/0, docs-only.
Two review findings and two nits. The trimming rule must not fire when the stripped position and the rest are both empty. A bare `[option]` is closed, so both are `never`, and the rule would normalize it to `[]` — but `[option]` accepts `new Array(1)` while `[]` rejects that length, so the trim would split the data form from the thunk readers and have `equal`/`cmp` identify two different array sets. The referenced-rest task still said to mask the absent bit in `subset` while the prose two hundred lines above had already established that masking is unsound. The task now says resolve, and states the expected one-way inclusion for the absence-only case rather than the mutual one. `close-counts-trailing-undefined` has landed as #1716, so Related cites the file rather than the open PR, and this file is the one that restates. Added the rtti-move proposal, which re-anchors every relative path here if it lands first. Rewrapped the four lines that overran `deno.json`'s 80 columns. Changelog: none Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MQwnUe7SYoUFXchmAfx8Yf
RTTI states one rule for absence, in
fjs/types/rtti/README.mdand for bothcontainer kinds: an absent member reads as
undefined, so a member is requiredexactly when its set excludes
undefined.closedoes not extend it past thedeclared positions, so three spellings of one value part company there.
Measured at
be345a7and re-verified at this head after #1708,validate,parseand the data form agreeing on every cell:[1][1, undefined][1, ,](a hole)[number]close([number])close([number, option(string)])close([number], () => ['const', undefined])Rows 1 and 3 apply the rule, and row 3 applies it inside a closed container,
so this is not "closing is stricter": a declared position admitting
undefinedmay be absent, present-as-undefined, or a hole, all one value.The two rejections in row 2 are not the same rejection.
closeContainerValidateends inextra.length === 0 && fits(...), and&&short-circuits. An explicit trailing
undefinedis an own enumerable entry, sothe extra check rejects it — which is exactly what the README says
closedoes— and
fitsis never reached. A hole is no entry, so onlyfits(
value.length <= declared) rejects it. Neutralizing both tuplefitssettles which is which:
[1, undefined]still errors, the hole flips took.The data form mirrors both halves in
arraySetValidate.So exactly one case rests on
length— and even that has a stated defence,written down in that same function: the length check is what keeps a closed
tuple's value set equal to the set
Tsrenders it as (a tuple of exactlypnpositions) and JSON Schema's
items: false. The issue is therefore a choicebetween two correspondences the module cannot currently both keep: absence is
undefined, which the length check breaks, and the set is what it rendersas, which dropping it breaks.
A defect falls out, and it is independent of that choice. Both length
checks sit in the no-
restbranch, so supplying arestskips them, and ahole is no entry so it meets nothing on the way through.
neveris the publicspelling of the exact-members set, and
toDatamaps both spellings toidentical data, so these are one schema:
[1][1, undefined][1, ,](a hole)close([number])close([number], never)(
validate/parse/ data form.) That is a reader disagreement of the kind#1712 fixed, not a design choice, so it is listed first in the tasks and B is
not documentation-only.
The fix is to drop an empty
restbefore the branch incloseContainerValidateandcloseContainerParse, leavingarraySetValidatealone — not to consult
fitswherever arestis present, which would rejectthe values a
restexists to admit (close([number], string)would stopaccepting
[1, 'x']). "Empty" is defined as an equality, up to rulerenaming: drop the
restexactly whentoData(close(c, rest))equalstoData(close(c)), so therestmakes no difference to the canonical form andagreement holds by construction. That comparison already exists —
subsetapplied both ways, which seesthrough α-equivalent rules under different names, and which gives the intended
verdict on every row. Five cases fix the criterion, and the file says which
wrong test each one catches —
never,or()andclose([never])drop; arecursive
rwith no finite inhabitant does not; a mutually recursivea/bwhose own
toDataisneverdoes not, ruling out therest's standalonedata as the test;
close([], unknown), whose conversion is{ array: true }with no
restkey at all, does not, ruling out the absence of that key as thetest; and two separately constructed recursive
rs, one incand one insidethe
rest, rule out the exportedequalas the comparison — converting thediscarded rest reserves the name
r, soc's rule is namedr0, and aname-sensitive equality keeps a
restthat is empty.Who depends on the answer.
fjs/edagis the only consumer ofcloseoutside
fjs/types/rtti/, and its README states its uniqueness claim asliteral. Of the three values following the canonical
['.', a, 'b', null],'extra'andundefinedare both rejected by the extra check — the reasonthat sentence gives — and only the hole rests on
fits. Under any answer thatreads a hole as absence, that spelling stops being held, and one node gets two
hashes.
The file records three answers — apply the rule to both (A), keep the current
behaviour and document what is missing (B), or treat a hole as absence and a
present
undefinedas a member (C) — what each costs, and who has to hear theresult. On the evidence it favours B, which makes this issue one defect to
fix, one invariant to write down, and two rejected alternatives recorded. What
B owes is narrower than "document the carve-out", and belongs on the
closeexport rather than only in the README, per
fjs/AGENTS.md's table: theexplicit-
undefinedhalf is already inferable from "Closed containers"requiring a wrapped-const rest, and only the length half — that a no-rest
closed tuple bounds
length, so a trailing hole is a non-member — isunreachable from any current passage. Not "a closed container": a
restskipsthe branch both length checks sit in, so
close([number], string)accepts[1, ,], and the struct kind has no length to bound.What A would actually cost, since the mechanism above is easy to misread.
[1, undefined]trips both halves independently, so patching either knob aloneleaves it rejected — filtering
undefined-valued extras withfitsuntouchedis a no-op for the tuple kind. A is therefore C plus the extra-check change.
And the struct kind that the extra check carries along has a fourth site the
obvious three miss: the data form encodes a closed struct as
rest: neverandreads it with
objectSetValidate, which has no length or extra-count check atall, so under an extra-check-only patch
close({ a: number })against{ a: 1, b: undefined }flips to ok invalidateandparsewhile staying anerror in the data form.
A's canonicalization cannot follow the schema's shape. A's filter sits on
the shared
extra, before the branch, so arestis never asked aboutundefinedat all — itsundefinedcomponent stops meaning anything whetheror not it admits other values too. The canonical form therefore has to strip
the
undefinedcomponent from every undeclaredrestand drop arestthatstrips to empty:
close([number], option(string))andclose([number], string)answer alike on every measured value under a simulated A while both
equalandmutual
subsetkeep their data apart. But neither half may touch a declaredposition. A closed container reads a declared member through
getItem, whichwalks the prototype chain, and enumerates undeclared ones with
Object.entries,which is own-only — so with
proto[1] = 'x'behind[1],close([number])accepts the value and
close([number, () => ['const', undefined]])rejects it,all three readers agreeing, today and under a simulated A. The object kind
splits the same way, and the object rows go with struct-in and not otherwise —
so the file records a split per kind rather than one rule.
Relationship to
parse-omits-undefined-members.md, now that #1708 haslanded. An earlier draft said to fold this into that file if it landed first.
Reading it in place, that is wrong, and the Related entry now says why: that
one asks what
parsebuilds at a declared position it found absent; thisone asks whether an undeclared trailing
undefinedor hole is a member atall. Either can be answered without the other, and answer A here is the only
thing that would make them meet. #1708 also changed no runtime acceptance —
re-verified at this head, every cell above and every edag row answers as
before, and
arraySetValidate's comment that this file quotes is untouched.This PR adds the issue file, plus one paragraph and a cross-link in
fjs/types/rtti/todo/schema-walk-own-indices.md, whose closing note called thevalue-side prototype read settled; no behaviour changes here.
Related:
#1712 settled
the same "a hole is
undefined" reading on the schema side; this is thevalue side.
npx tscclean,fjs t3402/3402.Changelog: none
https://claude.ai/code/session_017xp3Axb2CF3pSjedM9sqmM