Repository navigation
Code Analysis Conditional Null
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.
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.
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).
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.
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)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.