[GH-ISSUE #4212] [Bug] Source-map edges target known directory containers #1654

Closed
opened 2026-10-16 21:08:32 +00:00 by bsakaguchi · 2 comments
Owner

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

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

inspect-artifact assigns a real source-map file a maps-source edge to a sibling directory whose name matches the map filename without .map. The inventory correctly identifies the sibling as a directory container, but the derivation edge treats it as the mapped file. This reproduced with ordinary ZIP directory entries and a regular version 3 source map; the correct occurrence tree remains available.

All five public CLI calls exited 0. The reduced failing case printed:

{"case": "directory-target", "exit": 0, "main_kind": "directory", "map_kind": "file", "relation": "maps-source", "edge_parent": "main.js", "occurrence_parent": ".", "coverage": {"scope": "container-entries", "status": "complete"}, "map_limitations": []}

The nonempty-directory case returned the same erroneous mapping parent. Fuller inspection confirmed that main.js retained artifact_kind: "container", artifact_format: "directory", a node with format directory, and its correct contains edge to the ZIP root. The map retained artifact_kind: "source-map", artifact_format: "source-map", and its actual root parent occurrence. The child remained contained by the directory.

The file-target control returned maps-source to the regular main.js file. Both the directory-map control and the missing-target control returned contains to the archive root. Every case reported complete container-entry traversal. The nested inventory Evidence was marked confidence: "observed" and authority: "shipped-artifact"; its only inventory limitation described central-directory coverage, with no qualification of the directory mapping.

The fixture map was also accepted as regular by REA's own parser and tracer, which returned the original input.ts position at line 1, column 0 without diagnostics. That parser check supports input validity; the failure observations above come from the public CLI.

Reproduction

Use the source revision and pinned toolchain stated below. Prepare the checkout with npm ci and npm run build:cached, then run the following from its root with node on PATH. The script creates every input inline, uses an isolated home/config/cache, checks ZIP integrity and explicit directory metadata, and invokes only static inspection. These reproduction steps and all five cases were executed after rebuilding.

python3 - <<'PYCODE'
import json, os, stat, subprocess, tempfile, zipfile
from pathlib import Path

repo = Path.cwd()
map_text = json.dumps({
    'version': 3, 'file': 'main.js', 'sources': ['input.ts'],
    'sourcesContent': ['export const answer = 42;\n'],
    'names': [], 'mappings': 'AAAA',
})
script = 'export const answer = 42;\n//# sourceMappingURL=main.js.map\n'
cases = {
    'directory-target': [('main.js/', ''), ('main.js.map', map_text)],
    'directory-with-child': [('main.js/', ''), ('main.js/child.txt', 'child'),
                             ('main.js.map', map_text)],
    'file-target': [('main.js', script), ('main.js.map', map_text)],
    'directory-map': [('main.js', script), ('main.js.map/', '')],
    'missing-target': [('main.js.map', map_text)],
}
with tempfile.TemporaryDirectory(prefix='artifact-map-review-') as folder:
    root = Path(folder)
    home = root / 'home'
    home.mkdir()
    env = dict(os.environ, HOME=str(home), XDG_CONFIG_HOME=str(home / 'config'),
               XDG_CACHE_HOME=str(home / 'cache'))
    for name, entries in cases.items():
        archive = root / (name + '.zip')
        with zipfile.ZipFile(archive, 'w', compression=zipfile.ZIP_STORED) as output:
            for path, content in entries:
                entry = zipfile.ZipInfo(path)
                entry.create_system = 3
                entry.external_attr = (((stat.S_IFDIR | 0o755) << 16) | 0x10
                                       if path.endswith('/') else
                                       (stat.S_IFREG | 0o644) << 16)
                output.writestr(entry, content)
        with zipfile.ZipFile(archive) as source:
            assert source.testzip() is None
            if name.startswith('directory-') and name != 'directory-map':
                assert source.getinfo('main.js/').is_dir()
        result = subprocess.run(
            ['node', str(repo / 'scripts/rea.mjs'), 'inspect-artifact',
             str(archive), '--json'], cwd=repo, env=env,
            text=True, capture_output=True, check=True,
        )
        inventory = json.loads(result.stdout)['normalized_result']['substeps'][0]['evidence']['normalized_result']
        occurrences = inventory['occurrences']
        by_id = {item['occurrence_id']: item for item in occurrences}
        main = next((item for item in occurrences if item['logical_path'] == 'main.js'), None)
        source_map = next(item for item in occurrences if item['logical_path'] == 'main.js.map')
        edge = next(item for item in inventory['edges'] if item['occurrence_id'] == source_map['occurrence_id'])
        edge_parent = next(item for item in occurrences if item['artifact_id'] == edge['parent_artifact_id'])
        print(json.dumps({
            'case': name, 'exit': result.returncode,
            'main_kind': main['entry_kind'] if main else None,
            'map_kind': source_map['entry_kind'], 'relation': edge['relation'],
            'edge_parent': edge_parent['logical_path'],
            'occurrence_parent': by_id[source_map['parent_occurrence_id']]['logical_path'],
            'coverage': inventory['coverage'],
            'map_limitations': source_map['limitations'],
        }))
PYCODE

The directory-target case is the reduced two-entry ZIP. directory-with-child retains a regular child file to check that directory containment is unaffected. file-target, directory-map, and missing-target are controls.

Expected behavior

A source-map relation inferred from a sibling filename must not identify a known directory container as the mapped file. Retain the map and directory as occurrences contained by the archive when there is no eligible file at the matching path. Preserve the child file's containment within its directory and keep the normal mapping relation when main.js is a regular file.

The artifact graph describes derivation relationships, while inventory already distinguishes directory containers from files. This expectation concerns an incompatible known endpoint type. It does not require decoding every source map during inventory, validating source-byte correspondence, or resolving the map's original sources entries. Complete container-entry traversal can remain complete.

Impact and scope

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A bounded class of valid archives returns an incorrect derivation relationship for an already identified directory. The CLI preserves the correct occurrence tree, and no downstream production failure, data loss, or execution effect was observed.
  • Workaround status: available
  • Workaround basis: A consumer can detect the mapping parent's retained directory kind, disregard that inapplicable relation, and recover the map's containing archive through its retained parent_occurrence_id. This lookup was demonstrated against the failing CLI inventory and recovered the manifest root without altering the archive or product source.

REA version

Source checkout at 62bf09809ef7bb224028916424f8f07652a20f4f, freshly rebuilt with locked dependencies. Current upstream main 08a897b2cd7b359fcb0674c82baec3f1a74af632 was inspected separately. The cited artifact implementation and contract files are unchanged. Current-main execution was not performed. Published-package applicability is NOT_ENOUGH_INFO because no installed release reproduction or complete release-applicability proof was run; the source package version string is not release proof.

Runtime and platform

Node v24.18.0 and npm 11.16.0 on macOS arm64, using the repository-pinned toolchain. Local ZIP inventory and a supporting parser/trace check need no analysis engine or network provider.

Target and artifact context

Ordinary stored ZIPs containing explicit directory entries, regular JavaScript files and a regular version 3 source map. All fixture bytes and ZIP attributes are inline above.

Interface

CLI

Earliest failure stage

Artifact or evidence registration

Relevant output or evidence

The reproduction block was replayed after a fresh publication-checkout build and returned the same five outputs. All 25 existing artifact relationship tests passed. The passing tests preserve existing controls and do not cover the new directory-target case.

docs/mcp-contracts.md:20-44 keeps traversal coverage separate from content certainty. The reported complete container-entry traversal is not a claim of certain map correspondence.

Typed outcome or recovery expected

Rerun the inline script. Neither directory-target case should emit maps-source with the main.js directory as its parent artifact. The map should retain archive containment, the directory should retain its own root containment, and the child should remain under the directory. Keep all entries and their directory/file classifications visible with complete traversal coverage for these fully read inputs.

The real-file control must still map to main.js, the directory named main.js.map/ must remain contained by its archive, and the missing-target map must remain contained by the archive. Dropping maps, rejecting these ordinary archives, removing directory entries, or disabling every mapping relation would not restore the same behavior.

Analysis or suspected cause

Source-derived reasoning: the map is correctly classified as a source-map file, but sibling lookup admits the unique known directory because it has a non-null artifact ID. The contradictory target kind remains available to consumers. This identifies the failed contract without requiring a particular repair.

Additional context

Closed issue #3909 concerns a directory at the map path main.js.map/, which incorrectly became the child of a mapping edge. Merged PR #4131 fixes that child-classification case and explicitly leaves matching sibling lookup unchanged. Its directory-map control passes in this reproduction. This report concerns a regular map whose target at main.js/ is a directory; that failing signal remains after the earlier restoration.

Related #195 describes filename inference to a source file; it does not promise byte-level source correspondence. ASAR, MCP transport, other hosts, installed-package behavior, and downstream production effects were not tested. No inspected-target execution or implementation restoration run was performed. The last known good revision is NOT_ENOUGH_INFO because no regression boundary was established.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4212 ## 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 `inspect-artifact` assigns a real source-map file a `maps-source` edge to a sibling directory whose name matches the map filename without `.map`. The inventory correctly identifies the sibling as a directory container, but the derivation edge treats it as the mapped file. This reproduced with ordinary ZIP directory entries and a regular version 3 source map; the correct occurrence tree remains available. All five public CLI calls exited 0. The reduced failing case printed: ```json {"case": "directory-target", "exit": 0, "main_kind": "directory", "map_kind": "file", "relation": "maps-source", "edge_parent": "main.js", "occurrence_parent": ".", "coverage": {"scope": "container-entries", "status": "complete"}, "map_limitations": []} ``` The nonempty-directory case returned the same erroneous mapping parent. Fuller inspection confirmed that `main.js` retained `artifact_kind: "container"`, `artifact_format: "directory"`, a node with format `directory`, and its correct `contains` edge to the ZIP root. The map retained `artifact_kind: "source-map"`, `artifact_format: "source-map"`, and its actual root parent occurrence. The child remained contained by the directory. The file-target control returned `maps-source` to the regular `main.js` file. Both the directory-map control and the missing-target control returned `contains` to the archive root. Every case reported complete container-entry traversal. The nested inventory Evidence was marked `confidence: "observed"` and `authority: "shipped-artifact"`; its only inventory limitation described central-directory coverage, with no qualification of the directory mapping. The fixture map was also accepted as regular by REA's own parser and tracer, which returned the original `input.ts` position at line 1, column 0 without diagnostics. That parser check supports input validity; the failure observations above come from the public CLI. ## Reproduction Use the source revision and pinned toolchain stated below. Prepare the checkout with `npm ci` and `npm run build:cached`, then run the following from its root with `node` on PATH. The script creates every input inline, uses an isolated home/config/cache, checks ZIP integrity and explicit directory metadata, and invokes only static inspection. These reproduction steps and all five cases were executed after rebuilding. ```sh python3 - <<'PYCODE' import json, os, stat, subprocess, tempfile, zipfile from pathlib import Path repo = Path.cwd() map_text = json.dumps({ 'version': 3, 'file': 'main.js', 'sources': ['input.ts'], 'sourcesContent': ['export const answer = 42;\n'], 'names': [], 'mappings': 'AAAA', }) script = 'export const answer = 42;\n//# sourceMappingURL=main.js.map\n' cases = { 'directory-target': [('main.js/', ''), ('main.js.map', map_text)], 'directory-with-child': [('main.js/', ''), ('main.js/child.txt', 'child'), ('main.js.map', map_text)], 'file-target': [('main.js', script), ('main.js.map', map_text)], 'directory-map': [('main.js', script), ('main.js.map/', '')], 'missing-target': [('main.js.map', map_text)], } with tempfile.TemporaryDirectory(prefix='artifact-map-review-') as folder: root = Path(folder) home = root / 'home' home.mkdir() env = dict(os.environ, HOME=str(home), XDG_CONFIG_HOME=str(home / 'config'), XDG_CACHE_HOME=str(home / 'cache')) for name, entries in cases.items(): archive = root / (name + '.zip') with zipfile.ZipFile(archive, 'w', compression=zipfile.ZIP_STORED) as output: for path, content in entries: entry = zipfile.ZipInfo(path) entry.create_system = 3 entry.external_attr = (((stat.S_IFDIR | 0o755) << 16) | 0x10 if path.endswith('/') else (stat.S_IFREG | 0o644) << 16) output.writestr(entry, content) with zipfile.ZipFile(archive) as source: assert source.testzip() is None if name.startswith('directory-') and name != 'directory-map': assert source.getinfo('main.js/').is_dir() result = subprocess.run( ['node', str(repo / 'scripts/rea.mjs'), 'inspect-artifact', str(archive), '--json'], cwd=repo, env=env, text=True, capture_output=True, check=True, ) inventory = json.loads(result.stdout)['normalized_result']['substeps'][0]['evidence']['normalized_result'] occurrences = inventory['occurrences'] by_id = {item['occurrence_id']: item for item in occurrences} main = next((item for item in occurrences if item['logical_path'] == 'main.js'), None) source_map = next(item for item in occurrences if item['logical_path'] == 'main.js.map') edge = next(item for item in inventory['edges'] if item['occurrence_id'] == source_map['occurrence_id']) edge_parent = next(item for item in occurrences if item['artifact_id'] == edge['parent_artifact_id']) print(json.dumps({ 'case': name, 'exit': result.returncode, 'main_kind': main['entry_kind'] if main else None, 'map_kind': source_map['entry_kind'], 'relation': edge['relation'], 'edge_parent': edge_parent['logical_path'], 'occurrence_parent': by_id[source_map['parent_occurrence_id']]['logical_path'], 'coverage': inventory['coverage'], 'map_limitations': source_map['limitations'], })) PYCODE ``` The `directory-target` case is the reduced two-entry ZIP. `directory-with-child` retains a regular child file to check that directory containment is unaffected. `file-target`, `directory-map`, and `missing-target` are controls. ## Expected behavior A source-map relation inferred from a sibling filename must not identify a known directory container as the mapped file. Retain the map and directory as occurrences contained by the archive when there is no eligible file at the matching path. Preserve the child file's containment within its directory and keep the normal mapping relation when `main.js` is a regular file. The artifact graph describes derivation relationships, while inventory already distinguishes directory containers from files. This expectation concerns an incompatible known endpoint type. It does not require decoding every source map during inventory, validating source-byte correspondence, or resolving the map's original `sources` entries. Complete container-entry traversal can remain complete. ## Impact and scope - Impact level: P2 - Assessment status: supported - Impact basis: A bounded class of valid archives returns an incorrect derivation relationship for an already identified directory. The CLI preserves the correct occurrence tree, and no downstream production failure, data loss, or execution effect was observed. - Workaround status: available - Workaround basis: A consumer can detect the mapping parent's retained directory kind, disregard that inapplicable relation, and recover the map's containing archive through its retained parent_occurrence_id. This lookup was demonstrated against the failing CLI inventory and recovered the manifest root without altering the archive or product source. ## REA version Source checkout at [62bf09809ef7bb224028916424f8f07652a20f4f](https://github.com/morluto/rea/tree/62bf09809ef7bb224028916424f8f07652a20f4f), freshly rebuilt with locked dependencies. Current upstream main [08a897b2cd7b359fcb0674c82baec3f1a74af632](https://github.com/morluto/rea/tree/08a897b2cd7b359fcb0674c82baec3f1a74af632) was inspected separately. The cited artifact implementation and contract files are unchanged. Current-main execution was not performed. Published-package applicability is NOT_ENOUGH_INFO because no installed release reproduction or complete release-applicability proof was run; the source package version string is not release proof. ## Runtime and platform Node v24.18.0 and npm 11.16.0 on macOS arm64, using the repository-pinned toolchain. Local ZIP inventory and a supporting parser/trace check need no analysis engine or network provider. ## Target and artifact context Ordinary stored ZIPs containing explicit directory entries, regular JavaScript files and a regular version 3 source map. All fixture bytes and ZIP attributes are inline above. ## Interface CLI ## Earliest failure stage Artifact or evidence registration ## Relevant output or evidence - Expected source: [src/domain/artifactGraph.ts:157-194](https://github.com/morluto/rea/blob/62bf09809ef7bb224028916424f8f07652a20f4f/src/domain/artifactGraph.ts#L157-L194) defines derivation edges and producer-owned traversal coverage. [src/artifacts/inventory/ArtifactGraphConstruction.ts:76-103](https://github.com/morluto/rea/blob/62bf09809ef7bb224028916424f8f07652a20f4f/src/artifacts/inventory/ArtifactGraphConstruction.ts#L76-L103) preserves directory/container identity. [src/artifacts/inventory/ArtifactGraphConstruction.test.ts:68-161](https://github.com/morluto/rea/blob/62bf09809ef7bb224028916424f8f07652a20f4f/src/artifacts/inventory/ArtifactGraphConstruction.test.ts#L68-L161) distinguishes regular mapped files from directory containment. - Failure source: executed `inspect-artifact --json` returns `maps-source` to the known directory. [src/artifacts/inventory/scanCanonical.ts:208-248](https://github.com/morluto/rea/blob/62bf09809ef7bb224028916424f8f07652a20f4f/src/artifacts/inventory/scanCanonical.ts#L208-L248) materializes directory identity before edge construction. [Edge construction at lines 304-345](https://github.com/morluto/rea/blob/62bf09809ef7bb224028916424f8f07652a20f4f/src/artifacts/inventory/ArtifactGraphConstruction.ts#L304-L345) selects a unique matching target with content identity without excluding directories. - Public entry: [src/cli/artifactCommands.ts:219-250](https://github.com/morluto/rea/blob/62bf09809ef7bb224028916424f8f07652a20f4f/src/cli/artifactCommands.ts#L219-L250) admits the static inspection request with the default integrity policy. [src/artifacts/ArtifactProvider.ts:330-403](https://github.com/morluto/rea/blob/62bf09809ef7bb224028916424f8f07652a20f4f/src/artifacts/ArtifactProvider.ts#L330-L403) wraps the observed inventory in inspection Evidence. - Supporting map validity: [src/javascript/sourceMaps/SourceMapFormat.ts:284-310](https://github.com/morluto/rea/blob/62bf09809ef7bb224028916424f8f07652a20f4f/src/javascript/sourceMaps/SourceMapFormat.ts#L284-L310) is the REA parser used by the separate verifier check. It accepted the exact fixture map as regular; the tracer returned original `input.ts` at line 1, column 0 without diagnostics. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete The reproduction block was replayed after a fresh publication-checkout build and returned the same five outputs. All 25 existing artifact relationship tests passed. The passing tests preserve existing controls and do not cover the new directory-target case. [docs/mcp-contracts.md:20-44](https://github.com/morluto/rea/blob/62bf09809ef7bb224028916424f8f07652a20f4f/docs/mcp-contracts.md#L20-L44) keeps traversal coverage separate from content certainty. The reported complete container-entry traversal is not a claim of certain map correspondence. ## Typed outcome or recovery expected Rerun the inline script. Neither directory-target case should emit `maps-source` with the `main.js` directory as its parent artifact. The map should retain archive containment, the directory should retain its own root containment, and the child should remain under the directory. Keep all entries and their directory/file classifications visible with complete traversal coverage for these fully read inputs. The real-file control must still map to `main.js`, the directory named `main.js.map/` must remain contained by its archive, and the missing-target map must remain contained by the archive. Dropping maps, rejecting these ordinary archives, removing directory entries, or disabling every mapping relation would not restore the same behavior. ## Analysis or suspected cause Source-derived reasoning: the map is correctly classified as a source-map file, but sibling lookup admits the unique known directory because it has a non-null artifact ID. The contradictory target kind remains available to consumers. This identifies the failed contract without requiring a particular repair. ## Additional context Closed [issue #3909](https://github.com/morluto/rea/issues/3909) concerns a directory at the map path `main.js.map/`, which incorrectly became the child of a mapping edge. Merged [PR #4131](https://github.com/morluto/rea/pull/4131) fixes that child-classification case and explicitly leaves matching sibling lookup unchanged. Its directory-map control passes in this reproduction. This report concerns a regular map whose target at `main.js/` is a directory; that failing signal remains after the earlier restoration. Related [#195](https://github.com/morluto/rea/issues/195) describes filename inference to a source file; it does not promise byte-level source correspondence. ASAR, MCP transport, other hosts, installed-package behavior, and downstream production effects were not tested. No inspected-target execution or implementation restoration run was performed. The last known good revision is NOT_ENOUGH_INFO because no regression boundary was established.
Author
Owner

@x1mm4nu3l commented on GitHub (Oct 20, 2026):

Reproduced on main 2538214e2, Linux/Node 24.18.0, through both the CLI and stdio MCP (open_binary, then inspect_artifact with empty arguments). The two-entry ZIP with main.js/ and a regular main.js.map still creates a maps-source edge to the directory. The regular-file target maps correctly; directory-map and missing-target controls retain contains edges. No browser or native analysis engine is needed.

<!-- gh-comment-id:6286504057 --> @x1mm4nu3l commented on GitHub (Oct 20, 2026): Reproduced on main 2538214e2, Linux/Node 24.18.0, through both the CLI and stdio MCP (`open_binary`, then `inspect_artifact` with empty arguments). The two-entry ZIP with main.js/ and a regular main.js.map still creates a maps-source edge to the directory. The regular-file target maps correctly; directory-map and missing-target controls retain contains edges. No browser or native analysis engine is needed.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

Edge construction now requires exactly one matching occurrence with entry_kind=file and non-null content identity before choosing maps-source; a known directory falls back to the map's containing occurrence and contains relation. Source-map child classification still uses artifact_kind, preserving the prior directory-map fix. The regression constructs all five original ZIP cases and asserts relation/parent identity, complete traversal, unchanged map occurrence containment, directory classification/root containment, and child containment.

Implementation: ArtifactGraphConstruction.ts, ArtifactGraphConstruction.ts.

The relevant regression checks in artifactSourceMapTargets.test.ts, ArtifactGraphConstruction.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380700906 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Edge construction now requires exactly one matching occurrence with entry_kind=file and non-null content identity before choosing maps-source; a known directory falls back to the map's containing occurrence and contains relation. Source-map child classification still uses artifact_kind, preserving the prior directory-map fix. The regression constructs all five original ZIP cases and asserts relation/parent identity, complete traversal, unchanged map occurrence containment, directory classification/root containment, and child containment. Implementation: [ArtifactGraphConstruction.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/artifacts/inventory/ArtifactGraphConstruction.ts#L328), [ArtifactGraphConstruction.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/artifacts/inventory/ArtifactGraphConstruction.ts#L367). The relevant regression checks in [artifactSourceMapTargets.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/filesystem/artifactSourceMapTargets.test.ts), [ArtifactGraphConstruction.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/artifacts/inventory/ArtifactGraphConstruction.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
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#1654
No description provided.