[GH-ISSUE #6484] [Bug] stop counting changed PyInstaller member types as unchanged #3890

Closed
opened 2026-10-31 00:44:58 +00:00 by bsakaguchi · 0 comments
Owner

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

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

compare_artifacts omits a declared member-type change from its successful delta. Two one-member PyInstaller CArchive fixtures differ only in the TOC type-code byte for helper: m versus s. Both inventories preserve that difference in format_metadata.type_code, while reporting identical verified member content. Comparison returns only the root content change, with unchanged: 1 and changed: 1, counting helper unchanged. Reversing the inputs produces the same omission; the result carries no limitation qualifying metadata comparison.

Reproduction

Run this complete block from the repository root with its pinned Node.js toolchain, installed locked dependencies and prepared compiled CLI/MCP runtime. Checkout preparation was already complete during verification; no build or installation was run. This block constructs its own parser fixtures, invokes the supported CLI four times and the real stdio MCP tool five times, and keeps all child home/config/cache/data/state/temp directories within the disposable work directory. It inherits no client-path overrides and never executes target bytecode. The embedded payload is a CPython 3.14 marshalled code object for VALUE = 1; Python is not a prerequisite for replay.

WORK=$(mktemp -d)
node --input-type=module - "$WORK" <<'JS'
import assert from 'node:assert/strict';
import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
import { resolve, join } from 'node:path';
import { spawnSync } from 'node:child_process';
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';

const work = resolve(process.argv[2]);
const dispatcher = resolve('scripts/rea.mjs');
mkdirSync(work, { recursive: true });
const childEnv = { PATH: process.env.PATH, REA_LOG_LEVEL: 'silent', NODE_DISABLE_COMPILE_CACHE: '1' };
for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME', 'TMPDIR', 'DSH_HOME', 'DSH_AGENTS_HOME']) {
  const path = join(work, key.toLowerCase());
  mkdirSync(path, { recursive: true });
  childEnv[key] = path;
}
// CPython 3.14 marshal.dumps(compile('VALUE = 1\n', 'helper.py', 'exec')).
// REA reads these inert bytes; this probe never executes them.
const payload = Buffer.from('YwAAAAAAAAAAAAAAAAEAAAAAAAAA8woAAACAAF4BdABSASMAKQLpAQAAAE4pAdoFVkFMVUWpAPMAAAAA2gloZWxwZXIucHnaCDxtb2R1bGU+cgYAAAABAAAAcwoAAADwAwEBAdgICYIFcgQAAAA=', 'base64');
const changedPayload = Buffer.from(payload);
const integerOffset = changedPayload.indexOf(Buffer.from('e901000000', 'hex'));
assert(integerOffset >= 0);
changedPayload[integerOffset + 1] = 2;
const build = (typeCode, body = payload) => {
  const elf = Buffer.alloc(64);
  elf.writeUInt32LE(0x464c457f, 0);
  elf[4] = 2; elf[5] = 1; elf[6] = 1;
  elf.writeUInt16LE(2, 16); elf.writeUInt16LE(62, 18);
  elf.writeUInt32LE(1, 20); elf.writeUInt16LE(64, 52);
  const toc = Buffer.alloc(32);
  toc.writeUInt32BE(toc.length, 0);
  toc.writeUInt32BE(body.length, 8);
  toc.writeUInt32BE(body.length, 12);
  toc[17] = typeCode.charCodeAt(0);
  Buffer.from('helper\0').copy(toc, 18);
  const cookie = Buffer.alloc(88);
  Buffer.from('4d45490c0b0a0b0e', 'hex').copy(cookie);
  cookie.writeUInt32BE(body.length + toc.length + cookie.length, 8);
  cookie.writeUInt32BE(body.length, 12);
  cookie.writeUInt32BE(toc.length, 16);
  cookie.writeUInt32BE(314, 20);
  Buffer.from('libpython3.14.so').copy(cookie, 24);
  return Buffer.concat([elf, body, toc, cookie]);
};
const bytesM = build('m');
const bytesS = build('s');
const differingOffsets = [...bytesM.keys()].filter(i => bytesM[i] !== bytesS[i]);
assert.deepEqual(differingOffsets, [64 + payload.length + 17]);
const evidence = {};
for (const [name, bytes] of [['module', bytesM], ['script', bytesS], ['same-role', build('m')], ['changed-content', build('m', changedPayload)]]) {
  const file = join(work, name);
  writeFileSync(file, bytes);
  const run = spawnSync(process.execPath, [dispatcher, 'inspect-artifact', file, '--json'], { cwd: work, env: childEnv, encoding: 'utf8', timeout: 30000 });
  writeFileSync(join(work, `${name}.stderr`), run.stderr ?? '');
  assert.equal(run.status, 0, run.stderr);
  writeFileSync(join(work, `${name}.evidence.json`), run.stdout);
  evidence[name] = JSON.parse(run.stdout);
}
const inventoryEvidence = item => item.normalized_result.substeps.find(s => s.operation === 'inventory_artifact').evidence;
const inventory = item => inventoryEvidence(item).normalized_result;
const member = item => inventory(item).occurrences.find(o => o.logical_path === 'helper');
const leftMember = member(evidence.module);
const rightMember = member(evidence.script);
assert.equal(leftMember.artifact_id, rightMember.artifact_id);
for (const item of Object.values(evidence)) {
  const inv = inventory(item);
  assert.deepEqual(inv.coverage, { scope: 'container-entries', status: 'complete' });
  assert.equal(inv.nodes.length, inv.manifest.node_count);
  assert.equal(inv.occurrences.length, inv.manifest.occurrence_count);
  assert.equal(inv.edges.length, inv.manifest.edge_count);
  assert.equal(inv.occurrences.length, 2);
  assert.equal(member(item).hash_status, 'verified');
  assert.equal(member(item).entry_kind, 'file');
  assert(member(item).artifact_id);
}
const metadataDifferences = Object.keys(leftMember.format_metadata).filter(k => JSON.stringify(leftMember.format_metadata[k]) !== JSON.stringify(rightMember.format_metadata[k]));
assert.deepEqual(metadataDifferences, ['type_code']);
const transport = new StdioClientTransport({ command: process.execPath, args: [dispatcher, 'mcp'], cwd: work, env: childEnv, stderr: 'pipe' });
let diagnostics = '';
transport.stderr?.on('data', data => { diagnostics += data; });
const client = new Client({ name: 'member-type-probe', version: '1' });
const comparisons = {};
await client.connect(transport);
try {
  const catalog = await client.listTools();
  const contract = catalog.tools.find(t => t.name === 'compare_artifacts');
  assert(contract);
  writeFileSync(join(work, 'compare-artifacts.contract.json'), JSON.stringify(contract, null, 2));
  for (const [name, left, right] of [['role-change', 'module', 'script'], ['role-change-reversed', 'script', 'module'], ['same-role', 'module', 'same-role'], ['self', 'module', 'module'], ['content-change', 'module', 'changed-content']]) {
    const response = await client.callTool({ name: 'compare_artifacts', arguments: { left: inventoryEvidence(evidence[left]), right: inventoryEvidence(evidence[right]) } });
    assert(!response.isError, JSON.stringify(response));
    writeFileSync(join(work, `${name}.comparison.json`), JSON.stringify(response, null, 2));
    const result = response.structuredContent.normalized_result;
    comparisons[name] = { status: result.status, summary: result.summary, changes: result.changes.map(({ logical_path, classification, dimensions }) => ({ logical_path, classification, dimensions })), limitations: result.limitations };
  }
} finally {
  await client.close();
  writeFileSync(join(work, 'mcp.stderr'), diagnostics);
}
assert.equal(comparisons['same-role'].status, 'unchanged');
assert.equal(comparisons.self.status, 'unchanged');
assert.equal(comparisons['content-change'].changes.find(c => c.logical_path === 'helper')?.classification, 'changed');
const report = { runtime: { node: process.version, platform: process.platform, arch: process.arch }, fixture: { bytes: bytesM.length, payload_bytes: payload.length, differing_offsets: differingOffsets, metadata_differences: metadataDifferences }, member: { artifact_id: leftMember.artifact_id, left_type: leftMember.format_metadata.type_code, right_type: rightMember.format_metadata.type_code, left_hash_status: leftMember.hash_status, right_hash_status: rightMember.hash_status }, comparisons };
writeFileSync(join(work, 'observed.json'), JSON.stringify(report, null, 2));
console.log(JSON.stringify(report, null, 2));
JS

The two carriers are 294 bytes each and differ only at byte offset 191. Each inventory contains exactly the root and helper, with complete container-entries coverage, returned counts matching its manifest, a non-null member artifact ID, and hash_status: "verified". There are no runtime-option/dependency declarations, PYZ entries, duplicate paths, opaque subtrees or derived headers. The script also checks unchanged self-comparison, independently inspected identical-role input, and a valid member-content change produced by changing its marshalled integer constant to 2.

Expected behavior

The documented metadata comparison must expose the observed declared type difference at helper, independently of its unchanged content digest. Return a changed helper row with the metadata dimension and citations to both occurrences, and exclude it from summary.unchanged. The root content delta must remain visible. For this one-member fixture, the restored counts are changed: 2 and unchanged: 0. Preserving content-only equivalence for the member does not satisfy the advertised occurrence-metadata comparison.

Impact and scope

P2: a caller relying on the per-path delta or unchanged count misses an observed member declaration change. The overall result remains changed because the carrier digest changes; this report does not claim a false overall unchanged result. The inspected member metadata remains correct and available, so manually reading and comparing the two retained format_metadata.type_code values is a demonstrated workaround. The probe performs that read and observes m versus s before requesting comparison.

Observed scope is synthetic CArchive file members with verified identical bytes through the CLI-to-MCP workflow on the stated platform. Other metadata fields, producer formats, real PyInstaller bootloader builds, target execution and downstream decisions were not tested. No source mutation, data loss or security-boundary crossing was observed.

REA version

Source checkout 6dff37594c, package version 6.7.0, using its prepared compiled CLI/MCP runtime. No published-package execution or rebuild was performed.

Runtime and platform

Node.js v24.18.0; npm 11.16.0; macOS 27.0.1, build 26A434; arm64. CPython 3.14.8 generated and independently decoded the inert embedded code-object fixture during investigation; it is not used by the reproduction block. No native analysis engine or PyInstaller application was launched.

Target and artifact context

Synthetic 64-byte ELF prefixes followed by PyInstaller layout-2.1 CArchives, Python cookie version 314, one uncompressed helper member, a 32-byte directory record and an 88-byte cookie. Member payloads are 110 bytes. These are static parser fixtures, not runnable bootloader applications.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Projection of the observed forward and reverse comparisons, each from a successful MCP reply:

{
  "status": "changed",
  "summary": {"unchanged": 1, "added": 0, "removed": 0, "changed": 1, "unknown": 0, "contradiction": 0},
  "changes": [{"logical_path": ".", "classification": "changed", "dimensions": ["content"]}],
  "limitations": []
}

The two helper occurrences both have artifact ID art_f86bd5f17ffada6b56fa5e4b34ca2f892dab4ad79643b2e1ab5c2d279b3d05b6, verified hashes and no occurrence limitations. Their only differing format_metadata field is type_code. The self and equal-role controls each return status: "unchanged", unchanged: 2, changed: 0, and no change rows. The changed-content control returns status: "changed", unchanged: 0, changed: 2, a root content row and a helper row with content and relations dimensions. These are observed results, not a proposed test outcome.

  • Expected source: src/contracts/sessionToolContracts.ts:107-110 and the observed MCP tools/list description explicitly include metadata alongside content identity and graph relations; .agents/skills/reverse-engineer-anything/references/evidence-workflows.md:48-49 documents occurrence comparison; docs/python-packaged-applications.md:9-21 specifies retained member type metadata. The official PyInstaller type declarations identify m as a module and s as a script; that citation explains the declared roles, not a claim that these fixtures were executed by PyInstaller.
  • Failure source: The executed CLI/MCP block above returns only the root delta despite differing retained member types. Source-derived explanation is at src/domain/artifactComparison.ts:346-437: verified file occurrences reach changedDimensions, but metadataChanged does not read format_metadata. Its summary at :487-529 counts the unreported member path unchanged. src/artifacts/pyinstaller/PyInstallerMetadata.ts:94-114 preserves the original TOC type in the inventory.
  • Evidence provenance: observed. The primary violation is the successful real stdio MCP comparison of unmodified CLI-produced inventory Evidence; the source explanation is separately identified as derived reasoning.
  • Local verification: reproduced. The forward/reverse role-change pair and all three controls completed through the public interfaces; the exact inline reproduction was replayed before delivery.
  • Reproduction completeness: complete.

Restoration check: Replay this block and require a changed helper row containing metadata, unchanged: 0, and changed: 2 for both role-change directions, while retaining the root content delta and both source citations. Preserve unchanged: 2 with no changes for the self and equal-role controls. Preserve detection of the genuine member-content change. Do not obtain a pass by dropping the member, weakening complete coverage, or discarding its recorded type. This is a proposed restoration check; no repair was applied.

Analysis or suspected cause

Source-derived: both member occurrences have verified content and valid artifact nodes, so the availability guard allows the normal dimension comparison. That comparison examines selected node/occurrence fields but omits the producer's retained type metadata, producing no member change row. The successful wire reproduction and controls support the omission without prescribing a replacement representation or algorithm.

Additional context

Open and closed issue searches found no matching report. Related reports concern complete plist coverage (#2173), duplicate ZIP path accounting (#3956), ZIP-tail root deltas (#1485), partial root double-counting (#1844), inspection carriers/substeps (#3171, #3459, #3984), integrity attribution/correlation (#5866, #6217), ASAR summaries (#6436), and extraction notices (#6441). None restores a missing declared-type delta for a fully covered, uniquely named, verified file member.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The observed successful per-path comparison omits a meaningful declared-role change and counts the member unchanged, while its aggregate root change and original inventory facts remain available. No severe downstream incident or broader population frequency was established.
  • Workaround status: available
  • Workaround basis: The executed probe reads both original helper occurrences and directly observes their retained type_code values as m and s. Comparing those values manually recovers this specific missing fact without changing authenticated Evidence or target bytes.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6484 ## 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 `compare_artifacts` omits a declared member-type change from its successful delta. Two one-member PyInstaller CArchive fixtures differ only in the TOC type-code byte for `helper`: `m` versus `s`. Both inventories preserve that difference in `format_metadata.type_code`, while reporting identical verified member content. Comparison returns only the root content change, with `unchanged: 1` and `changed: 1`, counting `helper` unchanged. Reversing the inputs produces the same omission; the result carries no limitation qualifying metadata comparison. ## Reproduction Run this complete block from the repository root with its pinned Node.js toolchain, installed locked dependencies and prepared compiled CLI/MCP runtime. Checkout preparation was already complete during verification; no build or installation was run. This block constructs its own parser fixtures, invokes the supported CLI four times and the real stdio MCP tool five times, and keeps all child home/config/cache/data/state/temp directories within the disposable work directory. It inherits no client-path overrides and never executes target bytecode. The embedded payload is a CPython 3.14 marshalled code object for `VALUE = 1`; Python is not a prerequisite for replay. ```sh WORK=$(mktemp -d) node --input-type=module - "$WORK" <<'JS' import assert from 'node:assert/strict'; import { mkdirSync, writeFileSync, readFileSync } from 'node:fs'; import { resolve, join } from 'node:path'; import { spawnSync } from 'node:child_process'; import { Client } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; const work = resolve(process.argv[2]); const dispatcher = resolve('scripts/rea.mjs'); mkdirSync(work, { recursive: true }); const childEnv = { PATH: process.env.PATH, REA_LOG_LEVEL: 'silent', NODE_DISABLE_COMPILE_CACHE: '1' }; for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME', 'TMPDIR', 'DSH_HOME', 'DSH_AGENTS_HOME']) { const path = join(work, key.toLowerCase()); mkdirSync(path, { recursive: true }); childEnv[key] = path; } // CPython 3.14 marshal.dumps(compile('VALUE = 1\n', 'helper.py', 'exec')). // REA reads these inert bytes; this probe never executes them. const payload = Buffer.from('YwAAAAAAAAAAAAAAAAEAAAAAAAAA8woAAACAAF4BdABSASMAKQLpAQAAAE4pAdoFVkFMVUWpAPMAAAAA2gloZWxwZXIucHnaCDxtb2R1bGU+cgYAAAABAAAAcwoAAADwAwEBAdgICYIFcgQAAAA=', 'base64'); const changedPayload = Buffer.from(payload); const integerOffset = changedPayload.indexOf(Buffer.from('e901000000', 'hex')); assert(integerOffset >= 0); changedPayload[integerOffset + 1] = 2; const build = (typeCode, body = payload) => { const elf = Buffer.alloc(64); elf.writeUInt32LE(0x464c457f, 0); elf[4] = 2; elf[5] = 1; elf[6] = 1; elf.writeUInt16LE(2, 16); elf.writeUInt16LE(62, 18); elf.writeUInt32LE(1, 20); elf.writeUInt16LE(64, 52); const toc = Buffer.alloc(32); toc.writeUInt32BE(toc.length, 0); toc.writeUInt32BE(body.length, 8); toc.writeUInt32BE(body.length, 12); toc[17] = typeCode.charCodeAt(0); Buffer.from('helper\0').copy(toc, 18); const cookie = Buffer.alloc(88); Buffer.from('4d45490c0b0a0b0e', 'hex').copy(cookie); cookie.writeUInt32BE(body.length + toc.length + cookie.length, 8); cookie.writeUInt32BE(body.length, 12); cookie.writeUInt32BE(toc.length, 16); cookie.writeUInt32BE(314, 20); Buffer.from('libpython3.14.so').copy(cookie, 24); return Buffer.concat([elf, body, toc, cookie]); }; const bytesM = build('m'); const bytesS = build('s'); const differingOffsets = [...bytesM.keys()].filter(i => bytesM[i] !== bytesS[i]); assert.deepEqual(differingOffsets, [64 + payload.length + 17]); const evidence = {}; for (const [name, bytes] of [['module', bytesM], ['script', bytesS], ['same-role', build('m')], ['changed-content', build('m', changedPayload)]]) { const file = join(work, name); writeFileSync(file, bytes); const run = spawnSync(process.execPath, [dispatcher, 'inspect-artifact', file, '--json'], { cwd: work, env: childEnv, encoding: 'utf8', timeout: 30000 }); writeFileSync(join(work, `${name}.stderr`), run.stderr ?? ''); assert.equal(run.status, 0, run.stderr); writeFileSync(join(work, `${name}.evidence.json`), run.stdout); evidence[name] = JSON.parse(run.stdout); } const inventoryEvidence = item => item.normalized_result.substeps.find(s => s.operation === 'inventory_artifact').evidence; const inventory = item => inventoryEvidence(item).normalized_result; const member = item => inventory(item).occurrences.find(o => o.logical_path === 'helper'); const leftMember = member(evidence.module); const rightMember = member(evidence.script); assert.equal(leftMember.artifact_id, rightMember.artifact_id); for (const item of Object.values(evidence)) { const inv = inventory(item); assert.deepEqual(inv.coverage, { scope: 'container-entries', status: 'complete' }); assert.equal(inv.nodes.length, inv.manifest.node_count); assert.equal(inv.occurrences.length, inv.manifest.occurrence_count); assert.equal(inv.edges.length, inv.manifest.edge_count); assert.equal(inv.occurrences.length, 2); assert.equal(member(item).hash_status, 'verified'); assert.equal(member(item).entry_kind, 'file'); assert(member(item).artifact_id); } const metadataDifferences = Object.keys(leftMember.format_metadata).filter(k => JSON.stringify(leftMember.format_metadata[k]) !== JSON.stringify(rightMember.format_metadata[k])); assert.deepEqual(metadataDifferences, ['type_code']); const transport = new StdioClientTransport({ command: process.execPath, args: [dispatcher, 'mcp'], cwd: work, env: childEnv, stderr: 'pipe' }); let diagnostics = ''; transport.stderr?.on('data', data => { diagnostics += data; }); const client = new Client({ name: 'member-type-probe', version: '1' }); const comparisons = {}; await client.connect(transport); try { const catalog = await client.listTools(); const contract = catalog.tools.find(t => t.name === 'compare_artifacts'); assert(contract); writeFileSync(join(work, 'compare-artifacts.contract.json'), JSON.stringify(contract, null, 2)); for (const [name, left, right] of [['role-change', 'module', 'script'], ['role-change-reversed', 'script', 'module'], ['same-role', 'module', 'same-role'], ['self', 'module', 'module'], ['content-change', 'module', 'changed-content']]) { const response = await client.callTool({ name: 'compare_artifacts', arguments: { left: inventoryEvidence(evidence[left]), right: inventoryEvidence(evidence[right]) } }); assert(!response.isError, JSON.stringify(response)); writeFileSync(join(work, `${name}.comparison.json`), JSON.stringify(response, null, 2)); const result = response.structuredContent.normalized_result; comparisons[name] = { status: result.status, summary: result.summary, changes: result.changes.map(({ logical_path, classification, dimensions }) => ({ logical_path, classification, dimensions })), limitations: result.limitations }; } } finally { await client.close(); writeFileSync(join(work, 'mcp.stderr'), diagnostics); } assert.equal(comparisons['same-role'].status, 'unchanged'); assert.equal(comparisons.self.status, 'unchanged'); assert.equal(comparisons['content-change'].changes.find(c => c.logical_path === 'helper')?.classification, 'changed'); const report = { runtime: { node: process.version, platform: process.platform, arch: process.arch }, fixture: { bytes: bytesM.length, payload_bytes: payload.length, differing_offsets: differingOffsets, metadata_differences: metadataDifferences }, member: { artifact_id: leftMember.artifact_id, left_type: leftMember.format_metadata.type_code, right_type: rightMember.format_metadata.type_code, left_hash_status: leftMember.hash_status, right_hash_status: rightMember.hash_status }, comparisons }; writeFileSync(join(work, 'observed.json'), JSON.stringify(report, null, 2)); console.log(JSON.stringify(report, null, 2)); JS ``` The two carriers are 294 bytes each and differ only at byte offset 191. Each inventory contains exactly the root and `helper`, with complete `container-entries` coverage, returned counts matching its manifest, a non-null member artifact ID, and `hash_status: "verified"`. There are no runtime-option/dependency declarations, PYZ entries, duplicate paths, opaque subtrees or derived headers. The script also checks unchanged self-comparison, independently inspected identical-role input, and a valid member-content change produced by changing its marshalled integer constant to 2. ## Expected behavior The documented metadata comparison must expose the observed declared type difference at `helper`, independently of its unchanged content digest. Return a changed `helper` row with the `metadata` dimension and citations to both occurrences, and exclude it from `summary.unchanged`. The root content delta must remain visible. For this one-member fixture, the restored counts are `changed: 2` and `unchanged: 0`. Preserving content-only equivalence for the member does not satisfy the advertised occurrence-metadata comparison. ## Impact and scope P2: a caller relying on the per-path delta or unchanged count misses an observed member declaration change. The overall result remains `changed` because the carrier digest changes; this report does not claim a false overall `unchanged` result. The inspected member metadata remains correct and available, so manually reading and comparing the two retained `format_metadata.type_code` values is a demonstrated workaround. The probe performs that read and observes `m` versus `s` before requesting comparison. Observed scope is synthetic CArchive file members with verified identical bytes through the CLI-to-MCP workflow on the stated platform. Other metadata fields, producer formats, real PyInstaller bootloader builds, target execution and downstream decisions were not tested. No source mutation, data loss or security-boundary crossing was observed. ## REA version Source checkout 6dff37594c0bc942c9748b34a7a028d223e88a6d, package version 6.7.0, using its prepared compiled CLI/MCP runtime. No published-package execution or rebuild was performed. ## Runtime and platform Node.js v24.18.0; npm 11.16.0; macOS 27.0.1, build 26A434; arm64. CPython 3.14.8 generated and independently decoded the inert embedded code-object fixture during investigation; it is not used by the reproduction block. No native analysis engine or PyInstaller application was launched. ## Target and artifact context Synthetic 64-byte ELF prefixes followed by PyInstaller layout-2.1 CArchives, Python cookie version 314, one uncompressed `helper` member, a 32-byte directory record and an 88-byte cookie. Member payloads are 110 bytes. These are static parser fixtures, not runnable bootloader applications. ## Interface MCP ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence Projection of the observed forward and reverse comparisons, each from a successful MCP reply: ```json { "status": "changed", "summary": {"unchanged": 1, "added": 0, "removed": 0, "changed": 1, "unknown": 0, "contradiction": 0}, "changes": [{"logical_path": ".", "classification": "changed", "dimensions": ["content"]}], "limitations": [] } ``` The two `helper` occurrences both have artifact ID `art_f86bd5f17ffada6b56fa5e4b34ca2f892dab4ad79643b2e1ab5c2d279b3d05b6`, verified hashes and no occurrence limitations. Their only differing `format_metadata` field is `type_code`. The self and equal-role controls each return `status: "unchanged"`, `unchanged: 2`, `changed: 0`, and no change rows. The changed-content control returns `status: "changed"`, `unchanged: 0`, `changed: 2`, a root content row and a `helper` row with `content` and `relations` dimensions. These are observed results, not a proposed test outcome. - Expected source: `src/contracts/sessionToolContracts.ts:107-110` and the observed MCP tools/list description explicitly include metadata alongside content identity and graph relations; `.agents/skills/reverse-engineer-anything/references/evidence-workflows.md:48-49` documents occurrence comparison; `docs/python-packaged-applications.md:9-21` specifies retained member type metadata. The [official PyInstaller type declarations](https://raw.githubusercontent.com/pyinstaller/pyinstaller/b206650681ecc827b6870c29d3196dfd1a1d0a08/bootloader/src/pyi_archive.h) identify `m` as a module and `s` as a script; that citation explains the declared roles, not a claim that these fixtures were executed by PyInstaller. - Failure source: The executed CLI/MCP block above returns only the root delta despite differing retained member types. Source-derived explanation is at `src/domain/artifactComparison.ts:346-437`: verified file occurrences reach `changedDimensions`, but `metadataChanged` does not read `format_metadata`. Its summary at `:487-529` counts the unreported member path unchanged. `src/artifacts/pyinstaller/PyInstallerMetadata.ts:94-114` preserves the original TOC type in the inventory. - Evidence provenance: observed. The primary violation is the successful real stdio MCP comparison of unmodified CLI-produced inventory Evidence; the source explanation is separately identified as derived reasoning. - Local verification: reproduced. The forward/reverse role-change pair and all three controls completed through the public interfaces; the exact inline reproduction was replayed before delivery. - Reproduction completeness: complete. Restoration check: Replay this block and require a changed `helper` row containing `metadata`, `unchanged: 0`, and `changed: 2` for both role-change directions, while retaining the root content delta and both source citations. Preserve `unchanged: 2` with no changes for the self and equal-role controls. Preserve detection of the genuine member-content change. Do not obtain a pass by dropping the member, weakening complete coverage, or discarding its recorded type. This is a proposed restoration check; no repair was applied. ## Analysis or suspected cause Source-derived: both member occurrences have verified content and valid artifact nodes, so the availability guard allows the normal dimension comparison. That comparison examines selected node/occurrence fields but omits the producer's retained type metadata, producing no member change row. The successful wire reproduction and controls support the omission without prescribing a replacement representation or algorithm. ## Additional context Open and closed issue searches found no matching report. Related reports concern complete plist coverage (#2173), duplicate ZIP path accounting (#3956), ZIP-tail root deltas (#1485), partial root double-counting (#1844), inspection carriers/substeps (#3171, #3459, #3984), integrity attribution/correlation (#5866, #6217), ASAR summaries (#6436), and extraction notices (#6441). None restores a missing declared-type delta for a fully covered, uniquely named, verified file member. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The observed successful per-path comparison omits a meaningful declared-role change and counts the member unchanged, while its aggregate root change and original inventory facts remain available. No severe downstream incident or broader population frequency was established. - Workaround status: available - Workaround basis: The executed probe reads both original helper occurrences and directly observes their retained type_code values as m and s. Comparing those values manually recovers this specific missing fact without changing authenticated Evidence or target bytes.
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#3890
No description provided.