[GH-ISSUE #3882] [Bug] HTML script URLs ending in / or /. resolve to a same-named regular file #1502

Closed
opened 2026-10-16 21:06:15 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 15, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3882

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

rea analyze-javascript-application resolves the HTML script references ./target.js/ and ./target.js/. to the regular file target.js, creating loads edges with resolution_status: "resolved" and complete graph coverage. Both URLs resolve to the directory-form path /target.js/, which cannot load a regular file, so they should stay unresolved.

All three references produce loads edges to target.js with resolution_status: "resolved", and graph coverage is complete with no limits. The URL check shows that the two directory-form references end in /target.js/ and that the filesystem reports ENOTDIR for them. A fixture containing only the trailing-slash reference produced the same false edge.

Reproduction

  1. Create a fixture directory with two files.
mkdir fixture
printf '%s\n' '<script src="./target.js"></script>' '<script src="./target.js/"></script>' '<script src="./target.js/."></script>' > fixture/index.html
printf '%s\n' 'globalThis.fixtureValue = 42;' > fixture/target.js
  1. Analyze it and list the HTML loads edges and graph coverage, then compute each reference's URL path and local file status with Node's URL rules (metadata only).
rea analyze-javascript-application "$PWD/fixture" --json > evidence.json
node --input-type=module - evidence.json "$PWD/fixture" <<'JS'
import { readFileSync, statSync } from 'node:fs';
import { pathToFileURL, fileURLToPath } from 'node:url';
const result = JSON.parse(readFileSync(process.argv[2], 'utf8')).normalized_result;
for (const edge of result.graph.edges
  .filter(x => x.relation === 'loads' && x.properties.script_path)
  .sort((a, b) => a.properties.script_path.localeCompare(b.properties.script_path))) {
  console.log(JSON.stringify({script_path: edge.properties.script_path, resolved_path: edge.properties.resolved_path, resolution_status: edge.properties.resolution_status}));
}
console.log('coverage:', JSON.stringify(result.graph.coverage));
const base = pathToFileURL(process.argv[3] + '/index.html');
for (const specifier of ['./target.js', './target.js/', './target.js/.']) {
  const url = new URL(specifier, base);
  let kind;
  try { kind = statSync(fileURLToPath(url)).isFile() ? 'regular-file' : 'not-file'; } catch (e) { kind = e.code; }
  console.log(JSON.stringify({specifier, url_path_suffix: url.pathname.slice(url.pathname.lastIndexOf('/target.js')), file_status: kind}));
}
JS

Expected behavior

./target.js resolves to target.js. ./target.js/ and ./target.js/. remain unresolved with a retained reason (for example, that the reference resolves to a directory, not an exact application file), and coverage does not present those references as resolved. docs/javascript-artifact-reconstruction.md requires HTML script references to resolve to exact inventoried files after URL rules, and the resolver already rejects directory-form URL paths for other reference kinds.

Impact and scope

HTML references ending in a slash or terminal dot attach a renderer to a regular file that the declared URL cannot load, and downstream traversal follows the false edge despite complete coverage; it requires an inventoried regular file matching the slash-stripped path.

Workaround: available. Resolving the retained script declarations with new URL and checking the local file kind, as in step 2, identifies these false edges without executing the target, though it does not repair the REA graph.

Reporter's impact estimate: P2 (supported).

REA version

6.5.0, built from source at main commit 034342ba28 (latest npm release is 6.5.0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1 arm64

Target and artifact context

Local static web application directory (HTML plus JavaScript)

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: docs/javascript-artifact-reconstruction.md:629-640 (HTML script references resolve to exact inventoried files after document-base and URL rules; unresolved references retain their reason); src/application/javascript/JavaScriptArtifactPathResolution.ts:1178-1196 (exactUrlPathCandidate and urlPathIsDirectory reject directory-form URL paths)
  • Failure source: observed loads edges and coverage from rea analyze-javascript-application on the fixture above, with a Node URL and file-status check as an independent control
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete
{"script_path":"./target.js","resolved_path":"target.js","resolution_status":"resolved"}
{"script_path":"./target.js/","resolved_path":"target.js","resolution_status":"resolved"}
{"script_path":"./target.js/.","resolved_path":"target.js","resolution_status":"resolved"}
coverage: {"status":"complete","truncated":false,"omitted_count":0,"limits":[]}
{"specifier":"./target.js","url_path_suffix":"/target.js","file_status":"regular-file"}
{"specifier":"./target.js/","url_path_suffix":"/target.js/","file_status":"ENOTDIR"}
{"specifier":"./target.js/.","url_path_suffix":"/target.js/","file_status":"ENOTDIR"}

Source-derived reasoning: src/application/javascript/JavaScriptArtifactGraphDocuments.ts:55-64 passes each script through the html-reference resolver. For relative references without a base, htmlCandidate returns urlRelativeCandidate directly (src/application/javascript/JavaScriptArtifactPathResolution.ts:1145-1148), and urlPathCandidate drops the final empty URL segment (lines 650-660). The resulting target.js passes exact file lookup (lines 298-307) and becomes a loads edge (src/application/javascript/JavaScriptArtifactGraphDocuments.ts:92-108). This HTML branch skips the directory-form guard at lines 1178-1196.

Typed outcome or recovery expected

Directory-form HTML script references should stay unresolved with a retained not-found or directory reason instead of producing resolved loads edges to a regular file.

Restoration check:

On the fixture above, ./target.js/ and ./target.js/. currently print "resolution_status":"resolved" with "resolved_path":"target.js"; after a fix they are unresolved with a retained reason, and the graph no longer has loads edges from them to target.js. Control: ./target.js continues to print "resolved_path":"target.js","resolution_status":"resolved".

Analysis or suspected cause

Source-derived: htmlCandidate returns urlRelativeCandidate directly at src/application/javascript/JavaScriptArtifactPathResolution.ts:1145-1148, urlPathCandidate drops the final empty segment at lines 650-660, and this HTML branch skips the exactUrlPathCandidate directory-form guard at lines 1178-1196.

Additional context

Related closed #3307 fixed empty path segments followed by a parent segment selecting the wrong file under the same exact-HTML-resource contract; this report covers a different directory-form input.

This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3882 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior `rea analyze-javascript-application` resolves the HTML script references `./target.js/` and `./target.js/.` to the regular file `target.js`, creating `loads` edges with `resolution_status: "resolved"` and complete graph coverage. Both URLs resolve to the directory-form path `/target.js/`, which cannot load a regular file, so they should stay unresolved. All three references produce `loads` edges to `target.js` with `resolution_status: "resolved"`, and graph coverage is `complete` with no limits. The URL check shows that the two directory-form references end in `/target.js/` and that the filesystem reports `ENOTDIR` for them. A fixture containing only the trailing-slash reference produced the same false edge. ### Reproduction 1. Create a fixture directory with two files. ```sh mkdir fixture printf '%s\n' '<script src="./target.js"></script>' '<script src="./target.js/"></script>' '<script src="./target.js/."></script>' > fixture/index.html printf '%s\n' 'globalThis.fixtureValue = 42;' > fixture/target.js ``` 2. Analyze it and list the HTML `loads` edges and graph coverage, then compute each reference's URL path and local file status with Node's URL rules (metadata only). ```sh rea analyze-javascript-application "$PWD/fixture" --json > evidence.json node --input-type=module - evidence.json "$PWD/fixture" <<'JS' import { readFileSync, statSync } from 'node:fs'; import { pathToFileURL, fileURLToPath } from 'node:url'; const result = JSON.parse(readFileSync(process.argv[2], 'utf8')).normalized_result; for (const edge of result.graph.edges .filter(x => x.relation === 'loads' && x.properties.script_path) .sort((a, b) => a.properties.script_path.localeCompare(b.properties.script_path))) { console.log(JSON.stringify({script_path: edge.properties.script_path, resolved_path: edge.properties.resolved_path, resolution_status: edge.properties.resolution_status})); } console.log('coverage:', JSON.stringify(result.graph.coverage)); const base = pathToFileURL(process.argv[3] + '/index.html'); for (const specifier of ['./target.js', './target.js/', './target.js/.']) { const url = new URL(specifier, base); let kind; try { kind = statSync(fileURLToPath(url)).isFile() ? 'regular-file' : 'not-file'; } catch (e) { kind = e.code; } console.log(JSON.stringify({specifier, url_path_suffix: url.pathname.slice(url.pathname.lastIndexOf('/target.js')), file_status: kind})); } JS ``` ### Expected behavior `./target.js` resolves to `target.js`. `./target.js/` and `./target.js/.` remain unresolved with a retained reason (for example, that the reference resolves to a directory, not an exact application file), and coverage does not present those references as resolved. `docs/javascript-artifact-reconstruction.md` requires HTML script references to resolve to exact inventoried files after URL rules, and the resolver already rejects directory-form URL paths for other reference kinds. ### Impact and scope HTML references ending in a slash or terminal dot attach a renderer to a regular file that the declared URL cannot load, and downstream traversal follows the false edge despite complete coverage; it requires an inventoried regular file matching the slash-stripped path. Workaround: available. Resolving the retained script declarations with `new URL` and checking the local file kind, as in step 2, identifies these false edges without executing the target, though it does not repair the REA graph. Reporter's impact estimate: P2 (supported). ### REA version 6.5.0, built from source at main commit 034342ba28c8e56c3746be92b78aa7299afa02bd (latest npm release is 6.5.0) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1 arm64 ### Target and artifact context Local static web application directory (HTML plus JavaScript) ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence - Expected source: docs/javascript-artifact-reconstruction.md:629-640 (HTML script references resolve to exact inventoried files after document-base and URL rules; unresolved references retain their reason); src/application/javascript/JavaScriptArtifactPathResolution.ts:1178-1196 (`exactUrlPathCandidate` and `urlPathIsDirectory` reject directory-form URL paths) - Failure source: observed `loads` edges and coverage from `rea analyze-javascript-application` on the fixture above, with a Node URL and file-status check as an independent control - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete ```text {"script_path":"./target.js","resolved_path":"target.js","resolution_status":"resolved"} {"script_path":"./target.js/","resolved_path":"target.js","resolution_status":"resolved"} {"script_path":"./target.js/.","resolved_path":"target.js","resolution_status":"resolved"} coverage: {"status":"complete","truncated":false,"omitted_count":0,"limits":[]} {"specifier":"./target.js","url_path_suffix":"/target.js","file_status":"regular-file"} {"specifier":"./target.js/","url_path_suffix":"/target.js/","file_status":"ENOTDIR"} {"specifier":"./target.js/.","url_path_suffix":"/target.js/","file_status":"ENOTDIR"} ``` Source-derived reasoning: `src/application/javascript/JavaScriptArtifactGraphDocuments.ts:55-64` passes each script through the `html-reference` resolver. For relative references without a base, `htmlCandidate` returns `urlRelativeCandidate` directly (`src/application/javascript/JavaScriptArtifactPathResolution.ts:1145-1148`), and `urlPathCandidate` drops the final empty URL segment (lines 650-660). The resulting `target.js` passes exact file lookup (lines 298-307) and becomes a `loads` edge (`src/application/javascript/JavaScriptArtifactGraphDocuments.ts:92-108`). This HTML branch skips the directory-form guard at lines 1178-1196. ### Typed outcome or recovery expected Directory-form HTML script references should stay unresolved with a retained not-found or directory reason instead of producing resolved loads edges to a regular file. Restoration check: On the fixture above, `./target.js/` and `./target.js/.` currently print `"resolution_status":"resolved"` with `"resolved_path":"target.js"`; after a fix they are unresolved with a retained reason, and the graph no longer has `loads` edges from them to `target.js`. Control: `./target.js` continues to print `"resolved_path":"target.js","resolution_status":"resolved"`. ### Analysis or suspected cause Source-derived: htmlCandidate returns urlRelativeCandidate directly at src/application/javascript/JavaScriptArtifactPathResolution.ts:1145-1148, urlPathCandidate drops the final empty segment at lines 650-660, and this HTML branch skips the exactUrlPathCandidate directory-form guard at lines 1178-1196. ### Additional context Related closed #3307 fixed empty path segments followed by a parent segment selecting the wrong file under the same exact-HTML-resource contract; this report covers a different directory-form input. This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.
Author
Owner

@yankikucuk commented on GitHub (Oct 15, 2026):

Working on this. Reproduced with your fixture: all three references produced resolved loads edges. The HTML resolver now applies the same directory-form guard the URL and module URL contexts use on every branch (absolute, document-relative, base-href relative), so ./target.js/ and ./target.js/. stay unresolved with the retained reason "resolves to directory target.js, not an exact selected application file" while ./target.js still resolves. Draft PR follows shortly.

<!-- gh-comment-id:6189820960 --> @yankikucuk commented on GitHub (Oct 15, 2026): Working on this. Reproduced with your fixture: all three references produced resolved `loads` edges. The HTML resolver now applies the same directory-form guard the URL and module URL contexts use on every branch (absolute, document-relative, base-href relative), so `./target.js/` and `./target.js/.` stay unresolved with the retained reason "resolves to directory target.js, not an exact selected application file" while `./target.js` still resolves. Draft PR follows shortly.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1502
No description provided.