Skip to content

Code Analysis Conditional Null

Tu Shaokun edited this page Nov 6, 2026 · 1 revision

Code Analysis: conditional null package targets

Boundary

For a JavaScript runtime import, a matched null package condition blocks that target. For example, with {"imports":{"#feature":{"import":null,"default":"./fallback.cjs"}}}, Node blocks import "#feature"; the default sibling is not selected. Inside an array, however, Node can continue past a null or other eligible alternative: [{"import":null,"default":"./fallback.cjs"},"./array-next.cjs"] selects array-next.cjs when captured and valid. An explicitly enabled custom condition such as browser changes which branch is active.

Node 22.23.2's package-target implementation distinguishes an unmatched condition from a blocked target, and treats array alternatives separately. The TypeScript 5.9.3 module resolver source represents null as an unsuccessful search result in this path, allowing a later object condition to be considered. Archify's prior normal CLI could therefore report a captured fallback as a resolved JavaScript edge when Node blocks it. The separate declaration-to-implementation behavior and its original real-project evidence are documented in Code Analysis: Node Conditions.

Supported correction and limits

For non-type-only .js, .mjs, and .cjs references in Node16/NodeNext contexts, the correction handles affected exact-key, in-root package imports and self exports using captured package metadata and per-reference conditions. Matched null blocks an object sibling; arrays can continue after null, no match, or eligible invalid targets. A selected file that is missing or excluded does not make the selector advance to another alternative. Existing TypeScript, type-only, declaration and unaffected-route policies remain in place.

Affected patterns, package redirects, URL/escaped or legacy endpoint forms, non-JavaScript endpoints, numeric condition keys and relevant node-addons/module-sync feature policies are unsupported rather than guessed. This is a bounded correction, not a general Node loader. No target module or application is executed during analysis.

Evidence

A frozen 16-site authored CLI matrix compared the prior revision with Node 22.23.2's resolution-only oracle: nine prior targets were wrong. The candidate matches all 16, including the next array target; an active custom-browser case blocks correctly, and an unchanged upstream Node fallback fixture still selects asdf.js. This does not establish incidence in applications.

Seven fixed real scopes preserved full facts after normalizing only adapter revision and snapshot ID. The 19 unique dependency-cruiser and Chalk references previously checked with Node under both declaration settings were reused from the prior revision, not rerun. Candidate source and unpacked archive outputs matched byte for byte for all tested pairs. Local results: 68 focused and 236 module tests passed. No target applications were executed, and these scopes do not establish whole-resolver accuracy.

Candidate lower commit: 4bb002cc4b6062389db4c95355ae760708095134; extractor revision 15; archive SHA-256 b977652978835aa6570ccd6dc2cf3a8e530a38b2d9f3250d802884c5b8d8cd5f (two identical builds).

Published Drafts and integration verification

The correction is published in Draft #1220 and propagated through the existing native stack #1221. #1219 and Labs remain unchanged. Every listed PR is an unmerged Draft; AI validation is not human approval.

PR Final head TS revision Call model Archive SHA-256
#1220 4bb002cc4b6062389db4c95355ae760708095134 15 not collected b977652978835aa6570ccd6dc2cf3a8e530a38b2d9f3250d802884c5b8d8cd5f
#1225 b10e044c5f93d66a6fdb32cf235f76405e605cc6 17 js-ts-lexical-v1 6b5d66b66141c6f7a1d290a6de08da7c4cfbca0da8b198d0bacfd87431fe4bdf
#1226 5c944a64b47a953c7a3a3300b06b1f322f92cea6 17 js-ts-lexical-v1 886d5c4fd7c63dd04bef31dd52fc4ec5444432f4187085b6da6cd371c142f691
#1227 af2221c971725ccbda452f0e850e25542b150905 18 js-ts-lexical-v2 bc225db877aa839c23a7a8879da5b4396fc9c008b77ce05a4068793398aa1aca
#1229 919bf8c137ef9acd9c95ef39a1d2fb6106160ced 18 js-ts-lexical-v2 730ae0a2f4b87933d0146a90001c1f4059f787e0f260997cd44a963e137f5e9f
#1234 f0cd8dd4e0289a5fcc4548690e2b1b0157f5e228 18 js-ts-lexical-v2 bf0775272e202a18fde7a337a86d733d7882e72aec82c2eaa42cc56a414b1a62

The final #1225 head passed 39 focused tests. Its preceding integration candidate passed the frozen CLI matrix and source/archive comparisons; final test-only cleanup changed only a shipped test file, so the runtime-byte proof is reused explicitly. The final #1234 head passed all 287 module tests, zero failures or skips. Its fresh Redux default-import question, six saved question controls, saved Python/Java questions and three selected paired questions agreed between source and archive. Question comparisons exclude only installation/output-location fields where applicable. Saved Python/Java facts were reused, not freshly extracted.

Independent scoped review compared every old-public-to-final source delta and all six archives: only the intended shared correction, revision metadata and two redundant exact-revision test assertions changed. Dedicated extractor-revision checks remain. All tracked module files match their corresponding archive bytes, and no unrelated archive member changed. A fresh top archive build matched the committed archive. These checks preserve selected behavior; they do not establish complete impact analysis or broad call accuracy.

Public reproduction

The script below fetches the exact public baseline and corrected lower commit, installs the locked analysis dependencies inside temporary module directories, and uses the normal CLI plus a freshly built and unpacked archive. The two upstream Node fixture files are downloaded unchanged from commit aa4c77582be995286fc6e00aaf530dc7ade102a9 and checked against frozen hashes. It retains Git identity, all outputs and verification.json.

Run with official Node 22.23.2 (bundled zlib 1.3.1-e00f703), Python 3 and Git. Save the block as replay.py, then run TMPDIR=/private/tmp python3 replay.py on macOS. No target fixture module is executed. The independent Node resolution observations described above are reused rather than rerun by this script.

The public replay completed with exactly nine expected baseline mismatches and zero candidate mismatches across 16 authored references. Type-only, active-browser, excluded-target and unchanged upstream-array controls passed; all four source/archive raw-facts pairs were byte-identical. This is an authored/upstream boundary matrix, not evidence of failure prevalence in real applications. The seven earlier real-scope controls remain separately identified above.

The first public execution printed PASS and wrote its receipt, but the outer shell failed while recording its exit status. That attempt was retained; a second full public execution completed with recorded exit 0. Both used the same script. Script SHA-256: bebccf354a76219e1e9cce3aa10e94616f83278bdc23b10186b2a925d144e5fb.

Standalone public replay
#!/usr/bin/env python3
"""Bounded, public conditional-null replay; fixtures and expectations embedded."""
import hashlib, json, os, pathlib, subprocess, sys, tempfile, urllib.request, zipfile

FILES = {'authored/entry.cjs': 'require("#conditional");\nrequire("#require-blocked");\nimport("#conditional");\nrequire("#array");\n', 'authored/entry.mjs': 'import "#conditional";\nimport "#require-blocked";\nimport "#nested";\nimport "#array";\nimport "#conditional-in-array";\nimport "#empty";\nimport "#inactive";\nimport "#typed-blocked";\nimport "conditional-null-self/blocked";\nimport "conditional-null-self/array";\n', 'authored/fallback.cjs': 'exports.value = "fallback";\n', 'authored/types.d.ts': 'export interface Value { value: number; }\n', 'authored/type-control.ts': 'import type { Value } from "#typed-blocked";\nexport type Kept = Value;\n', 'authored/array-next.cjs': 'exports.value = "array-next";\n', 'authored/package.json': '{\n  "name": "conditional-null-self",\n  "type": "module",\n  "imports": {\n    "#conditional": {\n      "import": null,\n      "default": "./fallback.cjs"\n    },\n    "#require-blocked": {\n      "require": null,\n      "default": "./fallback.cjs"\n    },\n    "#nested": {\n      "node": {\n        "import": null,\n        "default": "./fallback.cjs"\n      },\n      "default": "./fallback.cjs"\n    },\n    "#array": [\n      null,\n      "./fallback.cjs"\n    ],\n    "#conditional-in-array": [\n      {\n        "import": null,\n        "default": "./fallback.cjs"\n      },\n      "./array-next.cjs"\n    ],\n    "#empty": [],\n    "#inactive": {\n      "browser": null,\n      "default": "./fallback.cjs"\n    },\n    "#typed-blocked": {\n      "types": "./types.d.ts",\n      "import": null,\n      "default": "./fallback.cjs"\n    }\n  },\n  "exports": {\n    "./blocked": {\n      "import": null,\n      "default": "./fallback.cjs"\n    },\n    "./array": [\n      null,\n      "./fallback.cjs"\n    ]\n  }\n}\n', 'authored/entry.js': 'import "#conditional";\nimport "#typed-blocked";\n', 'custom-condition/entry.cjs': 'require("#conditional");\nrequire("#require-blocked");\nimport("#conditional");\nrequire("#array");\n', 'custom-condition/entry.mjs': 'import "#conditional";\nimport "#require-blocked";\nimport "#nested";\nimport "#array";\nimport "#conditional-in-array";\nimport "#empty";\nimport "#inactive";\nimport "#typed-blocked";\nimport "conditional-null-self/blocked";\nimport "conditional-null-self/array";\n', 'custom-condition/fallback.cjs': 'exports.value = "fallback";\n', 'custom-condition/types.d.ts': 'export interface Value { value: number; }\n', 'custom-condition/type-control.ts': 'import type { Value } from "#typed-blocked";\nexport type Kept = Value;\n', 'custom-condition/array-next.cjs': 'exports.value = "array-next";\n', 'custom-condition/package.json': '{\n  "name": "conditional-null-self",\n  "type": "module",\n  "imports": {\n    "#conditional": {\n      "import": null,\n      "default": "./fallback.cjs"\n    },\n    "#require-blocked": {\n      "require": null,\n      "default": "./fallback.cjs"\n    },\n    "#nested": {\n      "node": {\n        "import": null,\n        "default": "./fallback.cjs"\n      },\n      "default": "./fallback.cjs"\n    },\n    "#array": [\n      null,\n      "./fallback.cjs"\n    ],\n    "#conditional-in-array": [\n      {\n        "import": null,\n        "default": "./fallback.cjs"\n      },\n      "./array-next.cjs"\n    ],\n    "#empty": [],\n    "#inactive": {\n      "browser": null,\n      "default": "./fallback.cjs"\n    },\n    "#typed-blocked": {\n      "types": "./types.d.ts",\n      "import": null,\n      "default": "./fallback.cjs"\n    }\n  },\n  "exports": {\n    "./blocked": {\n      "import": null,\n      "default": "./fallback.cjs"\n    },\n    "./array": [\n      null,\n      "./fallback.cjs"\n    ]\n  }\n}\n', 'custom-condition/tsconfig.json': '{"compilerOptions":{"customConditions":["browser"]}}\n', 'custom-condition/entry.js': 'import "#conditional";\nimport "#typed-blocked";\n', 'upstream-array/entry.mjs': 'import "pkg-exports/fallbackfile";\n', 'expected.json': '{\n  "cases": [\n    {\n      "file": "entry.mjs",\n      "line": 1,\n      "specifier": "#conditional",\n      "mode": "import",\n      "error": "ERR_PACKAGE_IMPORT_NOT_DEFINED"\n    },\n    {\n      "file": "entry.mjs",\n      "line": 2,\n      "specifier": "#require-blocked",\n      "mode": "import",\n      "to": "fallback.cjs"\n    },\n    {\n      "file": "entry.mjs",\n      "line": 3,\n      "specifier": "#nested",\n      "mode": "import",\n      "error": "ERR_PACKAGE_IMPORT_NOT_DEFINED"\n    },\n    {\n      "file": "entry.mjs",\n      "line": 4,\n      "specifier": "#array",\n      "mode": "import",\n      "to": "fallback.cjs"\n    },\n    {\n      "file": "entry.mjs",\n      "line": 5,\n      "specifier": "#conditional-in-array",\n      "mode": "import",\n      "to": "array-next.cjs"\n    },\n    {\n      "file": "entry.mjs",\n      "line": 6,\n      "specifier": "#empty",\n      "mode": "import",\n      "error": "ERR_PACKAGE_IMPORT_NOT_DEFINED"\n    },\n    {\n      "file": "entry.mjs",\n      "line": 7,\n      "specifier": "#inactive",\n      "mode": "import",\n      "to": "fallback.cjs"\n    },\n    {\n      "file": "entry.mjs",\n      "line": 8,\n      "specifier": "#typed-blocked",\n      "mode": "import",\n      "error": "ERR_PACKAGE_IMPORT_NOT_DEFINED"\n    },\n    {\n      "file": "entry.mjs",\n      "line": 9,\n      "specifier": "conditional-null-self/blocked",\n      "mode": "import",\n      "error": "ERR_PACKAGE_PATH_NOT_EXPORTED"\n    },\n    {\n      "file": "entry.mjs",\n      "line": 10,\n      "specifier": "conditional-null-self/array",\n      "mode": "import",\n      "to": "fallback.cjs"\n    },\n    {\n      "file": "entry.js",\n      "line": 1,\n      "specifier": "#conditional",\n      "mode": "import",\n      "error": "ERR_PACKAGE_IMPORT_NOT_DEFINED"\n    },\n    {\n      "file": "entry.js",\n      "line": 2,\n      "specifier": "#typed-blocked",\n      "mode": "import",\n      "error": "ERR_PACKAGE_IMPORT_NOT_DEFINED"\n    },\n    {\n      "file": "entry.cjs",\n      "line": 1,\n      "specifier": "#conditional",\n      "mode": "require",\n      "to": "fallback.cjs"\n    },\n    {\n      "file": "entry.cjs",\n      "line": 2,\n      "specifier": "#require-blocked",\n      "mode": "require",\n      "error": "ERR_PACKAGE_IMPORT_NOT_DEFINED"\n    },\n    {\n      "file": "entry.cjs",\n      "line": 3,\n      "specifier": "#conditional",\n      "mode": "import",\n      "error": "ERR_PACKAGE_IMPORT_NOT_DEFINED"\n    },\n    {\n      "file": "entry.cjs",\n      "line": 4,\n      "specifier": "#array",\n      "mode": "require",\n      "to": "fallback.cjs"\n    }\n  ],\n  "typeControl": {\n    "file": "type-control.ts",\n    "specifier": "#typed-blocked",\n    "expectedTo": "types.d.ts",\n    "typeOnly": true\n  }\n}\n', 'include-declarations.json': '{"exclude":[]}\n', 'exclude-target.json': '{"exclude":["fallback.cjs"]}\n'}
BASE = "06dd7340c44fc2bebafbbb897b86f9f789d303a1"
FIX = "4bb002cc4b6062389db4c95355ae760708095134"
URL = "https://github.com/tt-a1i/archify.git"
EXPECTED_ARCHIVE = "b977652978835aa6570ccd6dc2cf3a8e530a38b2d9f3250d802884c5b8d8cd5f"
BASE_MISMATCHES = {("entry.mjs", 1), ("entry.mjs", 3), ("entry.mjs", 5), ("entry.mjs", 8), ("entry.mjs", 9), ("entry.js", 1), ("entry.js", 2), ("entry.cjs", 2), ("entry.cjs", 3)}

def run(args, cwd=None):
    print('+', ' '.join(map(str,args)), flush=True)
    subprocess.run(list(map(str,args)), cwd=cwd, check=True)

def output(args, cwd=None):
    return subprocess.check_output(list(map(str,args)), cwd=cwd, text=True).strip()

def capture(cli, fixture, config, dest):
    args=['node',cli,'code-analysis','check',fixture,'--language','ts','--fail-on','never','--json','--out',dest]
    if config: args += ['--config',config]
    run(args)
    facts=json.loads((dest/'raw-facts.json').read_text())
    assert not facts.get('parse_errors'), facts.get('parse_errors')
    return facts

def find(facts, case):
    found=[i for i in facts['imports'] if i['from']==case['file'] and i['line']==case['line'] and i['specifier']==case['specifier']]
    assert len(found)==1, (case,found)
    return found[0]

def mismatch(facts, cases):
    result=set()
    for c in cases:
        a=find(facts,c)
        correct=(not a['resolved'] and 'to' not in a) if 'error' in c else a.get('resolved') is True and a.get('to')==c['to']
        if not correct: result.add((c['file'],c['line']))
    return result

def checks(facts, expected, name):
    actual=mismatch(facts,expected['cases'])
    assert actual == (BASE_MISMATCHES if name=='base' else set()), (name,actual)
    t=expected['typeControl']
    rows=[i for i in facts['imports'] if i['from']==t['file'] and i['specifier']==t['specifier']]
    assert len(rows)==1 and rows[0].get('to')==t['expectedTo'] and rows[0].get('typeOnly') is True, rows

if output(['node','--version']) != 'v22.23.2': sys.exit('Requires official Node 22.23.2 on PATH')
if output(['node','-p','process.versions.zlib']) != '1.3.1-e00f703': sys.exit('Requires official Node 22 bundled zlib 1.3.1-e00f703')
root=pathlib.Path(tempfile.mkdtemp(prefix='archify-null-public-')).resolve()
print('Artifacts:',root,flush=True)
fixtures=root/'fixtures'
for name,content in FILES.items():
    f=fixtures/name; f.parent.mkdir(parents=True,exist_ok=True); f.write_text(content)
# Upstream Node fixture files are fetched at an immutable commit and checked
# against hashes frozen before analyzer runs. Node.js source is MIT licensed:
# https://github.com/nodejs/node/blob/aa4c77582be995286fc6e00aaf530dc7ade102a9/LICENSE
upstream='https://raw.githubusercontent.com/nodejs/node/aa4c77582be995286fc6e00aaf530dc7ade102a9/test/fixtures/node_modules/pkgexports/'
for filename,digest in [('package.json','b044319b49b13dbcfac83f6c06d0c45bcfad1e2690f26b5b14f99d4dbf45eb98'),('asdf.js','5686c39d281bf1d2d2776753b2dc1743112a5919aa4f6abd9294a9c8bf9eea73')]:
    with urllib.request.urlopen(upstream+filename,timeout=30) as response: data=response.read()
    assert hashlib.sha256(data).hexdigest()==digest, filename
    (fixtures/'upstream-array'/filename).write_bytes(data)
expected=json.loads((fixtures/'expected.json').read_text())
repo=root/'repo'
repo.mkdir()
run(['git','init',repo])
run(['git','remote','add','origin',URL],repo)
assert output(['git','remote','get-url','origin'],repo)==URL
for head in (BASE,FIX): run(['git','fetch','--no-tags','--depth=1','origin',head],repo)
run(['git','checkout','--detach',BASE],repo)
assert output(['git','rev-parse','HEAD'],repo)==BASE
run(['npm','ci'],repo/'archify/modules/code-analysis')
base=capture(repo/'archify/bin/archify.mjs',fixtures/'authored',fixtures/'include-declarations.json',root/'base')
assert base['repository']['adapter']=='typescript@5.9.3' and base['repository']['adapterRevision']==6
checks(base,expected,'base')
run(['git','checkout','--detach',FIX],repo)
assert output(['git','rev-parse','HEAD'],repo)==FIX
run(['npm','ci'],repo/'archify/modules/code-analysis')
archive=root/'candidate.zip'
run(['bash','scripts/build-zip.sh',archive],repo)
assert hashlib.sha256(archive.read_bytes()).hexdigest()==EXPECTED_ARCHIVE, 'candidate archive hash changed'
with zipfile.ZipFile(archive) as z: z.extractall(root/'unpacked')
# Archive layout is archify/ at its root.
arch=root/'unpacked/archify'
run(['npm','ci'],arch/'modules/code-analysis')
source=repo/'archify/bin/archify.mjs'; packaged=arch/'bin/archify.mjs'
pair_hashes={}
for label,fixture,config in [('authored','authored','include-declarations.json'),('custom','custom-condition','include-declarations.json'),('excluded','authored','exclude-target.json'),('upstream','upstream-array',None)]:
    cfg=fixtures/config if config else None
    left=capture(source,fixtures/fixture,cfg,root/('source-'+label))
    right=capture(packaged,fixtures/fixture,cfg,root/('archive-'+label))
    source_bytes=(root/('source-'+label)/'raw-facts.json').read_bytes()
    archive_bytes=(root/('archive-'+label)/'raw-facts.json').read_bytes()
    assert source_bytes==archive_bytes, label
    pair_hashes[label]=hashlib.sha256(source_bytes).hexdigest()
    assert left['repository']['adapter']=='typescript@5.9.3' and left['repository']['adapterRevision']==15
    if label=='authored': checks(left,expected,'candidate')
    if label=='custom':
        a=next(i for i in left['imports'] if i['from']=='entry.mjs' and i['specifier']=='#inactive')
        assert a['resolved'] is False and 'to' not in a
    if label=='excluded':
        a=next(i for i in left['imports'] if i['from']=='entry.mjs' and i['specifier']=='#array')
        assert a['resolved'] is False and 'to' not in a
    if label=='upstream': assert next(i for i in left['imports'] if i['specifier']=='pkg-exports/fallbackfile')['to']=='asdf.js'
verification={
    'base_commit':BASE, 'candidate_commit':FIX, 'origin':URL,
    'node':output(['node','--version']), 'zlib':output(['node','-p','process.versions.zlib']),
    'npm':output(['npm','--version']), 'python':sys.version.split()[0],
    'base_adapter':base['repository']['adapter'], 'base_revision':base['repository']['adapterRevision'],
    'candidate_adapter':'typescript@5.9.3', 'candidate_revision':15,
    'archive_sha256':hashlib.sha256(archive.read_bytes()).hexdigest(),
    'base_mismatches':[{'file':f,'line':n} for f,n in sorted(BASE_MISMATCHES)],
    'candidate_mismatches':[], 'frozen_reference_count':len(expected['cases']),
    'source_archive_byte_equal_sha256':pair_hashes,
    'controls':{'type_only':True,'browser_null_blocked':True,'excluded_selected_unresolved':True,'upstream_asdf_selected':True},
    'upstream_node_commit':'aa4c77582be995286fc6e00aaf530dc7ade102a9',
    'meaning':'Bounded authored and upstream fixture replay; no target module execution'
}
(root/'verification.json').write_text(json.dumps(verification,indent=2)+'\n')
print('PASS: frozen 16; 9 baseline mismatches; 0 candidate mismatches; type-only, custom, excluded, upstream, source/archive parity',flush=True)

CI status at publication

Snapshot: 6 November 2026, 15:36 UTC. Five final-head CI runs completed with failure. Each has 17 successful jobs, two failed jobs and two skipped jobs. In each run, the only failed regression test is the existing README Classic motion-proof digest mismatch; test (22) is the dependent failed aggregate gate. The expected and actual hashes match the failure independently reproduced on exact Labs. No CI-green claim is made.

PR Final-head run Status at this snapshot
#1220 Run 42241865434 Completed / failure; known README regression and aggregate gate
#1225 Run 42241864269 Completed / failure; known README regression and aggregate gate
#1226 Run 42241866102 Completed / failure; known README regression and aggregate gate
#1227 Run 42241863193 Completed / failure; known README regression and aggregate gate
#1229 Run 42241861978 Completed / failure; known README regression and aggregate gate
#1234 Run 42241863207 In progress; same known regression, WebM decoder installation unfinished

The #1234 run is not complete at this snapshot. Its WebM job started at 15:19:45 UTC and remains in the decoder-installation step; no successful outcome is inferred. Continuous work and CI monitoring were paused at the author’s request after this round. This pause does not cancel the GitHub job.

Clone this wiki locally