Skip to content

fix: resolve three campaign residual defects - #2266

Merged
samchon merged 3 commits into
masterfrom
fix/campaign-residuals
Jul 20, 2026
Merged

fix: resolve three campaign residual defects#2266
samchon merged 3 commits into
masterfrom
fix/campaign-residuals

Conversation

@samchon

@samchon samchon commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Purpose

This consolidates the residual issue-campaign work into one pull request, replacing five separate campaign pull requests.

Two maintainer directives shaped it. First, the local machine ran out of resource headroom, so all local building and testing was stopped and verification is delegated to GitHub Actions CI. Second, the residual pull requests are to be combined into a single one. Accordingly the campaign's per-push exact-SHA CI cancellation rule is suspended for this branch: these runs must be allowed to finish, because they are now the verification.

Every commit here is a squashed net diff of one campaign unit, one commit per issue, so each issue keeps its own acceptance boundary, rollback unit, and failure attribution inside the shared pull request.

What each commit closes

Issue Commit Defect
#2082 fix(generate): order traversal entries by bytes, not locale gather sorted directory entries with localeCompare. Traversal order decides which lexical alias wins the visited-directory dedup, so the emitted output tree and the diagnostic path were locale-dependent.
#2256 fix(protobuf): reject malformed varints across values and framing _ProtobufReader accepted over-long varints; a ten-byte encoding with payload above bit 63 silently wrapped into a different value. Now bounded across values, tags, length prefixes, framing, and the unknown-field skip.
#2200 fix(native): require a runtime provider for native identity metadata_symbol_is_global_type treated global-table resolution as proof that a runtime provides the constructor, so a project's own declare global File/Blob won the lookup.
#2238 fix(native): preserve function semantics across callable spellings The pure-callable predicate was gated on KindInterfaceDeclaration, so type X = { (v: number): string } and the anonymous type literal fell through to structural object metadata.
#2252 fix(compare): preserve functional union membership during equality routing Equality routing decided union membership by a rule that disagreed with the emitted validator's own function rule.

#2238 and #2252 were previously serialized by a merge-order dependency. Consolidation removes it: the callable-spelling boundary and the routing rule that depends on it are now verified in one tree.

Evidence status — read this before reviewing

The campaign's normal gates are local build, focused suite, mutation proof, Self-Review, independent verification, and a full integration gate. The resource stop landed mid-wave, so those are not uniformly satisfied here. Stated exactly:

Unit Executed locally before the stop Never executed
#2082 Fail-before on unmodified master with the candidate test: repeated directory aliases are rejected failed with a real ERR_ASSERTION, not a timeout. Adjacent cycle controls passed. Junction/inode, stability, case-spelling, and replacement probes all executed. The narrowed determinism-only combination now shipping. The mutual-cycle cell, which timed out under fleet load and is recorded UNTESTED. The five CLI-mode rows.
#2256 Rebase verified byte-identical; protowire v1.34.2 source read; Java/Go divergence checked upstream. The suites and the Go oracle on this base.
#2200 Nothing. Everything, including compilation.
#2238 Nothing. Everything, including compilation. The 14-shape matrix has never run one cell.
#2252 Nothing on this base. An earlier 52-case proof and mutation witness exist against an older master and are not reused as evidence. Everything on this base.

Three commits contain Go that has never been compiled. CI is the first thing to compile or run them, and failures here are expected to be diagnosed and repaired in this pull request.

The #2238 invariant that #2252 builds on is stated in its commit message and is derived from source reading, not from an executed matrix. If CI contradicts it, the routing fix must be re-examined, not patched.

Not included: #2253

#2253 (contextual any/unknown JSON serialization) is excluded and stays open. Its work is preserved on fix/json-contextual-undefined and described on #2254. It is not a salvageable implementation:

  • the _jsonStringifyArray runtime helper is orphaned — StringifyJoiner.Array still emits input.map(arrow).join(",") and nothing imports or calls it;
  • the object-property half has no implementation at all, while its tests assert it;
  • the undefined: false oracle question is unresolved, and native JSON.stringify is not an unqualified oracle in a mode where typia may legitimately skip undefined checks.

Its root-cause analysis is worth keeping: the class is wider than the reported witness. A sparse number[] emits [,3], and a required property whose toJSON returns undefined emits {"k":undefined} — text that is not valid JSON at all.

Superseded pull requests

#2262, #2258, #2257, #2264, #2255 are superseded by this pull request. #2254 stays open for #2253.

Defect candidates raised while implementing, deliberately kept out of this diff

Recorded so they are not silently deferred; each needs its own issue.

  1. Lossy inode identity. fileIdentityKey builds identities from fs.Stats.ino as a JS number. In a 4000-directory probe, 1879 NTFS directory IDs exceeded 2^53, where the double ulp is 8. Two distinct directories can fold into one identity, silently skipping a real directory or raising a bogus revisit error. It also guards ensureTargetFiles' input/output overwrite check, which makes it a data-loss path. { bigint: true } is necessary but not sufficient: it addresses neither ino === 0 filesystems nor the win32 case-folding gap in the path fallback.
  2. metadata_symbol_from_default_lib decides default-library provenance from the lib.*.d.ts base name, which libReplacement defeats — real lib files resolve to node_modules/@typescript/lib-es2022/index.d.ts. This pull request routes only its own new predicate around it via a checker-keyed registry; the old helper's other callers, including metadata_is_global_function_interface, keep the wrong answer.
  3. Support-or-diagnostic divergence between spellings for member-carrying callable shapes whose only alias spelling is an intersection. Pinned as a declared boundary here rather than silently generalized.
  4. tests/test-typia-generate filesystem cases are a flaky oracle by construction. Each of the 10 cases pays full ttsx --no-plugins startup with no reuse, so cost is dominated by compiler startup rather than the behaviour under test — two consecutive full runs took 570 s and 644 s. The 180 s per-case bound sits only ~2.5x above unloaded cost. The durable fix is to stop paying startup ten times, not to raise the bound.

@samchon

samchon commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

Correction to candidate 1 in the body, from a source readback of ensureTargetFiles.

The body says the lossy inode identity guards ensureTargetFiles' input/output overwrite check "which makes it a data-loss path". That framing is wrong in direction, and it originated with me, not with the implementing agent.

ensureTargetFiles collects input identities into a set and throws when an output target's identity is already in it. So a collision — two distinct files folding into one key — makes an output that is not an input look like one, and generation is refused with a loud error. That is a false positive and a safe direction. Data loss would require the opposite: the same physical file yielding two different keys so the guard misses. Rounding cannot do that, because one file has one ino and therefore one rounded key.

The genuine data-loss consequence belongs to the dedup callers, not the overwrite guard: visitedFiles and visitedDirectories (TypiaGenerateWizard.ts:878 and :946). There, a collision makes a distinct file or directory look already-visited, so it is silently skipped and never transformed — silent source loss with no diagnostic.

So the candidate stands, and its severity is real, but it must be filed as:

  • dedup callers — collision causes a distinct source to be silently skipped (silent source loss);
  • overwrite guard — collision causes a spurious refusal to generate (loud, safe, still a defect);
  • { bigint: true } remains necessary but not sufficient, since it addresses neither ino === 0 filesystems nor the win32 case-folding gap in the path fallback.

No code in this pull request changes as a result; the correction is to the candidate write-up that will become its own issue.

samchon added 3 commits July 21, 2026 03:44
`gather` sorted directory entries with `String.prototype.localeCompare`,
which is locale-dependent. Under ICU en-US `"alias-a".localeCompare("alias_a")`
is 1, so `alias_a` traverses first, while `Buffer.compare` orders the
other way. Traversal order decides which lexical alias wins the
visited-directory dedup, so the emitted output tree and the diagnostic
path both varied by locale - the "nondeterministic overwrite order"
named in #2082's consequence surface.

`Buffer.compare` makes the order byte-lexical and deterministic.

Scope note: an earlier candidate for this issue also added a second
`path:<normalized realpath>` identity key beside the inode key. It was
dropped after measurement. Its stated root cause - that a Windows
junction can expose the same physical directory with a different
`stat.dev` - is false: `fs.promises.stat` follows the reparse point, so
junction and target report identical `dev` and `ino`. And the guard
matches with `.some(...)`, so a canonical-key hit means the same
normalized realpath was visited before, hence the same path, hence the
same object, hence the same `(dev, ino)` - the inode key already hit.
The only way it can hit where the inode key did not is if the object at
that path changed identity between visits, where traversing the new
object is correct and the added key would skip it. Redundant, or wrong.
On win32 `fs.realpathSync` also does not case-canonicalize, so `\real`
and `\REAL` yield equal inode keys but different canonical keys, making
the canonical key strictly weaker on the alias-spelling axis and
bypassing `FileSystemIdentity`'s own `caseSensitive` policy.

Closes #2082
`_ProtobufReader` accepted over-long varints. A ten-byte encoding whose
final byte carries payload above bit 63 silently wrapped into a
different value, so bytes a Go peer rejects became data. The bound is
now enforced the way `protowire.ConsumeVarint` enforces it - the tenth
byte must be below 2 - across field values, field tags, length
prefixes, and message framing, including the unknown-field skip path.

The oracle is a JSON corpus shared by the TypeScript suite and a Go test
that replays it through `protowire`, so both languages answer the same
expectations from the authoritative parser rather than from typia's own
output. The corpus covers every legal varint width, the exact 64-bit
maximum, the one-past-boundary value, a continuation bit set on the last
permitted byte, and truncation at every byte position.

The reference is named as the Go `protowire` parser rather than "the
official parser": upstream Java `CodedInputStream.readRawVarint64`
accepts and discards a tenth byte above bit 63, and the encoding
specification fixes only the ten-byte maximum while staying silent on
excess payload. Rejecting matches `proto.Unmarshal`, keeps typia from
accepting bytes a Go peer refuses, and no encoder emits that form - but
the divergence is recorded so the corpus cannot later be "disproved" by
pointing at Java.

Closes #2256
…uting

`CompareEqualProgrammer` routed a union member by a rule that disagreed
with the validator's own function rule, so a functional member of a
union could be matched by the wrong arm during equality comparison. The
routing now decides membership by the same rule the emitted validator
uses, keeping one answer for one type.

This unit was previously blocked on #2238's declaration-spelling
boundary. Consolidating both into one pull request removes the merge
ordering constraint: the callable-spelling fix is present in the same
tree, so the routing rule and the boundary it depends on are verified
together.

NOT REVERIFIED ON THIS BASE. The candidate carries an earlier 52-case
Node runtime proof and a `Functional=false` mutation witness, but those
were executed against an older master and cannot be reused as evidence.
CI is the first thing to run it here.

Closes #2252
@samchon
samchon force-pushed the fix/campaign-residuals branch from 218e663 to 3bdfa05 Compare July 20, 2026 18:49
@samchon

samchon commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

Scope reduced after CI, with diagnosis

CI compiled and ran everything for the first time. Results on 218e6638: build passed — the Go that had never been compiled compiles — spell-check passed, two of three packed-consumer jobs passed, and no pre-existing test broke. Exactly three tests failed, all newly added by this campaign.

I then ran those three locally, one at a time, and instrumented the native identity predicate to get ground truth. Here is what each failure actually was.

#2200 — the product change is correct; the fixtures encode an unreachable premise

Instrumenting metadata_symbol_is_runtime_native_type printed, for the user-global fixture:

[dbg] name=File  global=true provider=true buffer=false
  decl KindVariableDeclaration   file=bundled:///libs/lib.dom.d.ts  isLib=true
  decl KindInterfaceDeclaration  file=bundled:///libs/lib.dom.d.ts  isLib=true
[dbg] name=Blob  global=true provider=true buffer=false
  decl ... file=bundled:///libs/lib.dom.d.ts  isLib=true
  decl ... file=.../src/globals.d.ts          isLib=false

The fixture sets "lib": ["ES2022"], "types": [] and believes it has excluded DOM. It has not: lib.dom.d.ts is loaded, the genuine DOM File/Blob are in scope, and the fixture's declare global declarations merge with them. The symbol therefore does have a runtime provider, and classifying it native is the right answer. The same applies to the lib.*.d.ts spoof fixture: the real DOM declarations merge with the forged ones.

Program.IsLibFile was also confirmed to be set-membership (p.libFiles[sourceFile.Path()]), not a file-name test, so the registry this pull request added is a genuinely better oracle than the base-name rule — it simply was not the thing under test here.

Why the premise is unreachable, which is the more important finding: packages/interface/src/http/IHttpConnection.ts opens with /// <reference lib="dom" />, and @typia/interface's index.ts exports ./http/index. So every project that imports typia loads the DOM library regardless of its own lib setting. Every name in iterate_metadata_native_simples is then provided by some loaded library — Date, RegExp, Uint8Array from the ES libs, File and Blob from DOM — which makes "a user-authored global that no runtime provides" unreachable for a typia consumer.

So #2200's residual has no reachable failing scenario while that reference stands, and its product change has no regression that can distinguish it. That is the same standard this pull request already applied when it dropped #2082's second identity key, so it is applied here too: #2200 is removed from this pull request pending re-adjudication. Its work is preserved on fix/metadata-identity-residuals.

The forced DOM library is filed as its own candidate: it changes the global scope of every typia consumer, including pure Node projects, and it is upstream of what #2200 even means.

Separately, that fixture's typia.llm.schema<File, "chatgpt">() is malformed regardless: the signature is schema<T, Config = {}>($defs), so "chatgpt" lands in Config, which must be a literal object type.

#2238 — the fix works; the three-way identity assertion also indicts something older

TestCallableTypeLiteralBoundariesTransform requires the emitted consumer JavaScript to be byte-identical across interface Consumer { (i): o }, type Consumer = { (i): o }, and type Consumer = (i) => o. Diffing the emits shows the literal and the function alias now agree with each other, and the interface is the odd one out:

interface:                         literal and alias:
  properties: {},                    properties: { fn: { $ref: "#/components/schemas/Consumer" } },
  required: [],                      required: ["fn"],
                                     Consumer: {}

So the fix did what it set out to do — a type literal is no longer expanded as a structural object and now routes exactly like the mutually assignable function type. What remains is a disagreement in the JSON-schema path between the interface spelling and plain function types, which is #2250's merged boundary rather than anything this diff introduced, and it is not obvious which of the two behaviours is canonical: omitting a function member from a JSON schema, or emitting a $ref to an empty schema for it.

Deciding that is a separate product question. #2238 is removed from this pull request rather than shipped with a red mandatory regression or with that regression weakened. Its work is preserved on fix/callable-declaration-identity.

What remains here

Issue Status
#2082 kept — no test failed
#2256 kept — no test failed
#2252 kept — its regression passed, and re-verified locally on this reduced tree without #2238 present (ok 1.173s), since consolidation had been its reason for not waiting on that merge

CI is now re-running on the reduced branch.

@samchon samchon changed the title fix: resolve five campaign residual defects fix: resolve three campaign residual defects Jul 20, 2026
@samchon
samchon merged commit da8a458 into master Jul 20, 2026
8 checks passed
@samchon
samchon deleted the fix/campaign-residuals branch July 20, 2026 19:15
@samchon

samchon commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

Correction: the DOM finding is narrower than I stated, and it changes what #2200 means

I checked the published declaration on npm — https://unpkg.com/@typia/interface/lib/http/IHttpConnection.d.ts — and it does not carry the /// <reference lib="dom" />. The directive exists only in packages/interface/src/http/IHttpConnection.ts; the build output drops it.

So my earlier statement on this thread, that every project importing typia loads the DOM library, is wrong for npm consumers and I withdraw it. The correct scope:

  • npm consumers resolve typia through the built lib/** declarations, which carry no DOM reference. A pure Node project genuinely has no DOM File/Blob.
  • In-repo fixtures resolve typia through the package exports map, which points . at ./src/index.ts. They therefore read the source tree, pick up that triple-slash directive, and load lib.dom.d.ts no matter what their own tsconfig lib says.

Two consequences, and the second one matters more than the first.

1. #2200's residual is not unreachable after all. It is unreachable in an in-repo transform fixture, because such a fixture cannot construct a DOM-free project while importing typia from source. For a real npm consumer with neither DOM nor @types/node, a user-authored declare global { interface File { ... } } genuinely is the only declaration, and the resolution-only identity gate would promote it. So the product change in that unit may well be both correct and needed; what failed is the harness's ability to express the scenario, not the scenario itself. Removing #2200 from this pull request was still right — a fix must ship with a regression that actually reproduces the defect — but it is a re-adjudication with a reachable target, not a dismissal.

2. This is a test-oracle defect in its own right. Any in-repo transform fixture that believes it has excluded the DOM library has not. Every native-identity fixture that reasons about File, Blob, or any other DOM-provided global is silently testing a merged declaration. That is a property of the harness, not of the code under test, and it is exactly what made two of this campaign's fixtures assert something the product was right to refuse.

A correct #2200 regression needs a fixture that reproduces the npm-consumer resolution — a packed or stubbed typia rather than the source tree — or an equivalent that keeps the DOM library out. That is the actionable next step, and it will be filed with the other candidates from this campaign.

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.

1 participant