[GH-ISSUE #6485] [Bug] complete PyInstaller declarations compare as unavailable #3891

Closed
opened 2026-10-31 00:44:59 +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/6485

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

The registered compare_artifacts MCP tool reports unknown when a complete PyInstaller inventory containing one runtime-option declaration is passed as both left and right. Its only change row labels that same metadata occurrence unknown with dimension availability; the two occurrence IDs and manifest IDs match. The declaration has no missing payload: it is a valid zero-payload, uncompressed o record, which inspection represents as metadata with artifact_id: null and hash_status: "not-hashed" by design.

A declaration-only fixture with two repeated options and one valid d dependency reference returns three such unknown rows. A content-only PyInstaller control returns unchanged. Changing a materialized member while retaining an identical option preserves the real content deltas but leaves the aggregate comparison unknown because of the option row. These results reproduced in two completed runs of the case set through production stdio MCP.

Reproduction

Use the stated source revision with its pinned Node.js toolchain, locked dependencies and prepared compiled runtime. Dependency installation and building were not rerun during verification. Run this self-contained script from the repository root; it creates inert format fixtures and isolated HOME/XDG/temp directories, starts the production server, confirms tool registration, and calls open_binary, inspect_artifact and compare_artifacts. The self-comparisons pass the exact same unmodified returned inspection Evidence object on both sides. No fixture application, Python bytecode or external analysis engine is executed. The final trap removes the script's disposable fixture directory.

set -eu
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
export WORK
mkdir -p "$WORK/home" "$WORK/config" "$WORK/data" "$WORK/cache" "$WORK/state" "$WORK/temp"
env -i PATH="$PATH" WORK="$WORK" HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" XDG_DATA_HOME="$WORK/data" XDG_CACHE_HOME="$WORK/cache" XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/temp" node --input-type=module - <<'JS'
import { mkdirSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
const root=process.env.WORK;
function archive(members) {
  const host=Buffer.alloc(64); host.writeUInt32LE(0x464c457f); host[4]=2;host[5]=1;host[6]=1;
  host.writeUInt16LE(2,16);host.writeUInt16LE(62,18);host.writeUInt32LE(1,20);host.writeUInt16LE(64,52);
  const records=[],payloads=[]; let offset=0;
  for (const [name,type,body=''] of members) {
    const data=Buffer.from(body), text=Buffer.from(name);
    const rec=Buffer.alloc(Math.ceil((18+text.length+1)/16)*16);
    rec.writeUInt32BE(rec.length,0);rec.writeUInt32BE(offset,4);rec.writeUInt32BE(data.length,8);rec.writeUInt32BE(data.length,12);
    rec[17]=type.charCodeAt(0);text.copy(rec,18);records.push(rec);payloads.push(data);offset+=data.length;
  }
  const toc=Buffer.concat(records), cookie=Buffer.alloc(88);
  Buffer.from('4d45490c0b0a0b0e','hex').copy(cookie);
  cookie.writeUInt32BE(offset+toc.length+88,8);cookie.writeUInt32BE(offset,12);cookie.writeUInt32BE(toc.length,16);cookie.writeUInt32BE(309,20);
  Buffer.from('libpython3.9.so').copy(cookie,24);
  return Buffer.concat([host,...payloads,toc,cookie]);
}
const cases={
  option:[['pyi-contents-directory _internal','o']],
  dependency:[['shared-app:assets/data.bin','d']],
  declarations:[['pyi-contents-directory _internal','o'],['pyi-contents-directory _internal','o'],['shared-app:assets/data.bin','d']],
  content:[['main','s','print(1)\n']],
  mixed:[['pyi-contents-directory _internal','o'],['main','s','print(1)\n']],
  mixedChanged:[['pyi-contents-directory _internal','o'],['main','s','print(2)\n']],
};
for (const [name,members] of Object.entries(cases)) writeFileSync(join(root,name),archive(members));
const header=Buffer.from(JSON.stringify({files:{'missing.bin':{size:4,unpacked:true}}}));
const headerSize=8+Math.ceil(header.length/4)*4;
const asar=Buffer.alloc(8+headerSize);asar.writeUInt32LE(4,0);asar.writeUInt32LE(headerSize,4);asar.writeUInt32LE(headerSize-4,8);asar.writeUInt32LE(header.length,12);header.copy(asar,16);
writeFileSync(join(root,'unavailable.asar'),asar);
const env={PATH:process.env.PATH,REA_LOG_LEVEL:'silent',HOME:join(root,'home'),XDG_CONFIG_HOME:join(root,'config'),XDG_DATA_HOME:join(root,'data'),XDG_CACHE_HOME:join(root,'cache'),XDG_STATE_HOME:join(root,'state'),TMPDIR:join(root,'temp')};
const transport=new StdioClientTransport({command:process.execPath,args:[resolve('scripts/rea.mjs'),'mcp'],cwd:resolve('.'),env,stderr:'pipe'});
let stderr='';transport.stderr?.on('data',x=>stderr+=x);
const client=new Client({name:'artifact-comparison-probe',version:'1'});
const all={};
function projection(evidence, compared) {
 const inventory=evidence.normalized_result.substeps.find(s=>s.evidence.operation==='inventory_artifact').evidence.normalized_result;
 const result=compared.normalized_result;
 return {coverage:inventory.coverage,same_manifest:result.left_manifest_id===result.right_manifest_id,status:result.status,summary:result.summary,
  declarations:inventory.occurrences.filter(o=>o.entry_kind==='metadata').map(o=>({path:o.logical_path,artifact_id:o.artifact_id,hash_status:o.hash_status,declared_size:o.declared_size,compressed_size:o.compressed_size,source_name:o.format_metadata.source_name,type_code:o.format_metadata.type_code,dependency_status:o.format_metadata.dependency_status,limitations:o.limitations})),
  changes:result.changes.map(c=>({path:c.logical_path,classification:c.classification,dimensions:c.dimensions,same_occurrence:c.left_occurrence_id===c.right_occurrence_id})),limitations:result.limitations};
}
async function call(name,args) {
  const reply=await client.callTool({name,arguments:args});
  if(reply.isError) throw new Error(JSON.stringify(reply));
  return reply.structuredContent;
}
try {
 await client.connect(transport);
 const tools=(await client.listTools()).tools;
 console.log('registered compare_artifacts:',tools.some(t=>t.name==='compare_artifacts'));
 for(const name of [...Object.keys(cases),'unavailable.asar']) {
  await call('open_binary',{path:join(root,name)});
  const evidence=await call('inspect_artifact',{});
  const inventory=evidence.normalized_result.substeps.find(s=>s.evidence.operation==='inventory_artifact').evidence.normalized_result;
  const compared=await call('compare_artifacts',{left:evidence,right:evidence});
  all[name]={evidence,compared};
  console.log(JSON.stringify({case:name,...projection(evidence,compared)},null,2));
 }
 const changed=await call('compare_artifacts',{left:all.mixed.evidence,right:all.mixedChanged.evidence});
 all.changed=changed;
 console.log('content-change comparison',JSON.stringify(projection(all.mixed.evidence,changed),null,2));
 writeFileSync(join(root,'results.json'),JSON.stringify(all,null,2));
} finally {await client.close();writeFileSync(join(root,'server-stderr.txt'),stderr);}
JS

The option case is the reduced reproduction: the archive contains just one declaration and no materialized member. The dependency and declarations cases establish the representation of valid references and repeated options. The content-only case and the ASAR whose unpacked file is missing distinguish complete declarations from genuinely unavailable content. The final pair changes only the main payload between two mixed fixtures; it does not test changes to declaration metadata.

Expected behavior

Within the inventory's declared static scope, identical complete runtime-option declarations should compare as unchanged facts. The option self-comparison should return status: "unchanged", two unchanged occurrences including the root, zero unknowns, and no changes. A metadata declaration intentionally has no content artifact to recover or hash.

For a d entry, equality concerns the recorded dependency reference. Its dependency_status: "unresolved" and external-content limitation must remain intact; declaration equality does not establish that another executable exists or that its member bytes are equal. Genuine missing file bytes must continue to produce uncertainty, including when the same Evidence is supplied twice.

Impact and scope

P2: a supported static comparison produces false unavailable classifications and incorrect unchanged/unknown counts for complete declarations. In the mixed-content pair, the root and member content changes remain visible, but the false declaration row prevents a concrete aggregate changed result. This is a bounded result-accuracy defect; no data loss, target execution, source modification or security boundary crossing was observed.

Observed scope is synthetic PyInstaller 2.1 CArchives on one host through production MCP. Inspection retains the declaration facts, which an analyst can read directly. A practical alternative that restores the complete tool result and its aggregate status was not verified. Ignoring every declaration row would risk suppressing real declaration differences and is not a demonstrated workaround. Runnable executables produced by PyInstaller, other host platforms and downstream behavior consumers were not tested.

REA version

Source revision 6dff37594c; package metadata 6.7.0. The runtime replay used the prepared compiled source build, not a separately installed release.

Runtime and platform

Node.js v24.18.0; macOS 27.0.1, build 26A434; arm64. The checkout's locked MCP client SDK connects to node scripts/rea.mjs mcp.

Target and artifact context

Synthetic ELF64 headers with appended PyInstaller CArchives, layout 2.1 and cookie Python version 309. The minimal archive contains only a valid zero-payload runtime-option declaration. Additional cases contain valid dependency declarations, repeated runtime options or inert script payloads. A header-only ASAR declares one unavailable unpacked file as a negative control. These are parser fixtures, not runnable application builds.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: docs/python-packaged-applications.md:29-46 defines o and d as metadata declarations without content artifacts and requires zero payload with no compression; it explicitly leaves external dependency contents unresolved. src/contracts/sessionToolContracts.ts:107 and .agents/skills/reverse-engineer-anything/references/evidence-workflows.md:46-49 define comparison over occurrence paths, content, metadata and graph relations. docs/mcp-contracts.md:37-45 distinguishes complete scoped inventory from genuinely unavailable hashes and unknown coverage.
  • Failure source: Executed production MCP calls in the inline script, including exact Evidence self-comparisons and both controls. src/artifacts/pyinstaller/CArchiveMembers.ts:59-82 projects declarations without readable content; src/artifacts/inventory/scanReader.ts:296-312 hashes only files and slices; src/domain/artifactComparison.ts:346-371 requires verified hashes and artifact nodes before comparing a present occurrence's facts.
  • Evidence provenance: observed. Tool results establish the failure and controls; the source references explain their reach and representation separately.
  • Local verification: reproduced. Both the initial independent program and the exact inline reproduction completed with exit 0, confirmed registration, and returned the stated comparison results.
  • Reproduction completeness: complete.

Smallest useful result projection for the one-option case:

{
  "coverage": {"scope": "container-entries", "status": "complete"},
  "same_manifest": true,
  "status": "unknown",
  "summary": {"unchanged": 1, "added": 0, "removed": 0, "changed": 0, "unknown": 1, "contradiction": 0},
  "declarations": [{"path": "__pyinstaller__/runtime-options/0", "artifact_id": null, "hash_status": "not-hashed", "declared_size": 0, "compressed_size": 0, "source_name": "pyi-contents-directory _internal", "type_code": "o", "limitations": []}],
  "changes": [{"path": "__pyinstaller__/runtime-options/0", "classification": "unknown", "dimensions": ["availability"], "same_occurrence": true}],
  "limitations": []
}

The dependency-only self-comparison likewise reports one unknown; the repeated-option/dependency fixture reports unchanged: 1, unknown: 3. Their traversal coverage is complete and their declared manifest counts match the returned graph arrays. Each declaration has its own indexed path and occurrence identity, so repeated declarations do not create duplicate-path ambiguity. The dependency occurrence retains its explicit unresolved-external-bytes limitation.

The content-only control reports status: "unchanged", unchanged: 2, unknown: 0, and no changes. The missing ASAR companion control reports one availability unknown for missing.bin; that occurrence is a file with hash_status: "unavailable", a four-byte declared payload, and an explicit missing-companion limitation. This is genuine missing evidence despite complete directory traversal and identical comparison inputs. The mixed-content pair reports changed: 2, unknown: 1, with real root/member content changes plus the false option availability row.

Restoration check: Replay the same fixtures through the registered tools. The option-only and dependency-reference self-comparisons should classify their recorded declarations as unchanged, returning two unchanged occurrences and no change rows; the three-declaration fixture should return four unchanged occurrences. Preserve the dependency's unresolved external-content facts. Keep the content-only control unchanged and the missing-companion ASAR genuinely unknown. The mixed-content pair should retain both real content changes and yield aggregate changed with zero unknowns and one unchanged declaration. Equality must not come from an unconditional identical-Evidence shortcut, which would incorrectly erase the ASAR uncertainty.

Analysis or suspected cause

Source-derived: declaration occurrences intentionally skip content hashing, but the comparison availability gate requires both hashes and artifact nodes. That gate consequently treats a complete metadata declaration like an unavailable file before it reaches fact comparison. The observed controls support distinguishing those states without weakening missing-evidence handling; this report does not prescribe a particular implementation.

Additional context

Open and closed issue searches covered compare_artifacts, PyInstaller/unknown, runtime options, metadata/comparison, identical inventories, declaration/artifact, PyInstaller/compare, __pyinstaller__, runtime-options, and declaration/compare_artifacts, alongside the supplied full issue snapshot. Exact likely matches were read. Closed #2173 concerned root-only plist coverage; open #6436 concerns summary disclosure of genuinely unavailable ASAR content. Neither restores equality of complete declaration occurrences. Comparison carrier admission, partial-root accounting, duplicate ZIP counts, integrity attribution and extraction-provenance reports also have separate closure signals. No matching open or closed duplicate was found.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Registered MCP comparisons classify complete declaration facts as unavailable, inflate unknown counts, and prevent a concrete aggregate changed result in the executed mixed-content case. The underlying inventory and real content deltas remain available; no severe downstream incident or loss was observed.
  • Workaround status: unverified
  • Workaround basis: The original declaration facts are readable in inspection Evidence, but no alternative preserving the full comparison classification, counts and aggregate result was verified. Discarding all declaration rows could hide actual changes and does not establish a safe full-result workaround.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6485 ## 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 The registered `compare_artifacts` MCP tool reports `unknown` when a complete PyInstaller inventory containing one runtime-option declaration is passed as both `left` and `right`. Its only change row labels that same metadata occurrence `unknown` with dimension `availability`; the two occurrence IDs and manifest IDs match. The declaration has no missing payload: it is a valid zero-payload, uncompressed `o` record, which inspection represents as metadata with `artifact_id: null` and `hash_status: "not-hashed"` by design. A declaration-only fixture with two repeated options and one valid `d` dependency reference returns three such unknown rows. A content-only PyInstaller control returns `unchanged`. Changing a materialized member while retaining an identical option preserves the real content deltas but leaves the aggregate comparison `unknown` because of the option row. These results reproduced in two completed runs of the case set through production stdio MCP. ## Reproduction Use the stated source revision with its pinned Node.js toolchain, locked dependencies and prepared compiled runtime. Dependency installation and building were not rerun during verification. Run this self-contained script from the repository root; it creates inert format fixtures and isolated HOME/XDG/temp directories, starts the production server, confirms tool registration, and calls `open_binary`, `inspect_artifact` and `compare_artifacts`. The self-comparisons pass the exact same unmodified returned inspection Evidence object on both sides. No fixture application, Python bytecode or external analysis engine is executed. The final trap removes the script's disposable fixture directory. ```sh set -eu WORK=$(mktemp -d) trap 'rm -rf "$WORK"' EXIT export WORK mkdir -p "$WORK/home" "$WORK/config" "$WORK/data" "$WORK/cache" "$WORK/state" "$WORK/temp" env -i PATH="$PATH" WORK="$WORK" HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" XDG_DATA_HOME="$WORK/data" XDG_CACHE_HOME="$WORK/cache" XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/temp" node --input-type=module - <<'JS' import { mkdirSync, writeFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { Client } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; const root=process.env.WORK; function archive(members) { const host=Buffer.alloc(64); host.writeUInt32LE(0x464c457f); host[4]=2;host[5]=1;host[6]=1; host.writeUInt16LE(2,16);host.writeUInt16LE(62,18);host.writeUInt32LE(1,20);host.writeUInt16LE(64,52); const records=[],payloads=[]; let offset=0; for (const [name,type,body=''] of members) { const data=Buffer.from(body), text=Buffer.from(name); const rec=Buffer.alloc(Math.ceil((18+text.length+1)/16)*16); rec.writeUInt32BE(rec.length,0);rec.writeUInt32BE(offset,4);rec.writeUInt32BE(data.length,8);rec.writeUInt32BE(data.length,12); rec[17]=type.charCodeAt(0);text.copy(rec,18);records.push(rec);payloads.push(data);offset+=data.length; } const toc=Buffer.concat(records), cookie=Buffer.alloc(88); Buffer.from('4d45490c0b0a0b0e','hex').copy(cookie); cookie.writeUInt32BE(offset+toc.length+88,8);cookie.writeUInt32BE(offset,12);cookie.writeUInt32BE(toc.length,16);cookie.writeUInt32BE(309,20); Buffer.from('libpython3.9.so').copy(cookie,24); return Buffer.concat([host,...payloads,toc,cookie]); } const cases={ option:[['pyi-contents-directory _internal','o']], dependency:[['shared-app:assets/data.bin','d']], declarations:[['pyi-contents-directory _internal','o'],['pyi-contents-directory _internal','o'],['shared-app:assets/data.bin','d']], content:[['main','s','print(1)\n']], mixed:[['pyi-contents-directory _internal','o'],['main','s','print(1)\n']], mixedChanged:[['pyi-contents-directory _internal','o'],['main','s','print(2)\n']], }; for (const [name,members] of Object.entries(cases)) writeFileSync(join(root,name),archive(members)); const header=Buffer.from(JSON.stringify({files:{'missing.bin':{size:4,unpacked:true}}})); const headerSize=8+Math.ceil(header.length/4)*4; const asar=Buffer.alloc(8+headerSize);asar.writeUInt32LE(4,0);asar.writeUInt32LE(headerSize,4);asar.writeUInt32LE(headerSize-4,8);asar.writeUInt32LE(header.length,12);header.copy(asar,16); writeFileSync(join(root,'unavailable.asar'),asar); const env={PATH:process.env.PATH,REA_LOG_LEVEL:'silent',HOME:join(root,'home'),XDG_CONFIG_HOME:join(root,'config'),XDG_DATA_HOME:join(root,'data'),XDG_CACHE_HOME:join(root,'cache'),XDG_STATE_HOME:join(root,'state'),TMPDIR:join(root,'temp')}; const transport=new StdioClientTransport({command:process.execPath,args:[resolve('scripts/rea.mjs'),'mcp'],cwd:resolve('.'),env,stderr:'pipe'}); let stderr='';transport.stderr?.on('data',x=>stderr+=x); const client=new Client({name:'artifact-comparison-probe',version:'1'}); const all={}; function projection(evidence, compared) { const inventory=evidence.normalized_result.substeps.find(s=>s.evidence.operation==='inventory_artifact').evidence.normalized_result; const result=compared.normalized_result; return {coverage:inventory.coverage,same_manifest:result.left_manifest_id===result.right_manifest_id,status:result.status,summary:result.summary, declarations:inventory.occurrences.filter(o=>o.entry_kind==='metadata').map(o=>({path:o.logical_path,artifact_id:o.artifact_id,hash_status:o.hash_status,declared_size:o.declared_size,compressed_size:o.compressed_size,source_name:o.format_metadata.source_name,type_code:o.format_metadata.type_code,dependency_status:o.format_metadata.dependency_status,limitations:o.limitations})), changes:result.changes.map(c=>({path:c.logical_path,classification:c.classification,dimensions:c.dimensions,same_occurrence:c.left_occurrence_id===c.right_occurrence_id})),limitations:result.limitations}; } async function call(name,args) { const reply=await client.callTool({name,arguments:args}); if(reply.isError) throw new Error(JSON.stringify(reply)); return reply.structuredContent; } try { await client.connect(transport); const tools=(await client.listTools()).tools; console.log('registered compare_artifacts:',tools.some(t=>t.name==='compare_artifacts')); for(const name of [...Object.keys(cases),'unavailable.asar']) { await call('open_binary',{path:join(root,name)}); const evidence=await call('inspect_artifact',{}); const inventory=evidence.normalized_result.substeps.find(s=>s.evidence.operation==='inventory_artifact').evidence.normalized_result; const compared=await call('compare_artifacts',{left:evidence,right:evidence}); all[name]={evidence,compared}; console.log(JSON.stringify({case:name,...projection(evidence,compared)},null,2)); } const changed=await call('compare_artifacts',{left:all.mixed.evidence,right:all.mixedChanged.evidence}); all.changed=changed; console.log('content-change comparison',JSON.stringify(projection(all.mixed.evidence,changed),null,2)); writeFileSync(join(root,'results.json'),JSON.stringify(all,null,2)); } finally {await client.close();writeFileSync(join(root,'server-stderr.txt'),stderr);} JS ``` The `option` case is the reduced reproduction: the archive contains just one declaration and no materialized member. The `dependency` and `declarations` cases establish the representation of valid references and repeated options. The content-only case and the ASAR whose unpacked file is missing distinguish complete declarations from genuinely unavailable content. The final pair changes only the `main` payload between two mixed fixtures; it does not test changes to declaration metadata. ## Expected behavior Within the inventory's declared static scope, identical complete runtime-option declarations should compare as unchanged facts. The `option` self-comparison should return `status: "unchanged"`, two unchanged occurrences including the root, zero unknowns, and no changes. A metadata declaration intentionally has no content artifact to recover or hash. For a `d` entry, equality concerns the recorded dependency reference. Its `dependency_status: "unresolved"` and external-content limitation must remain intact; declaration equality does not establish that another executable exists or that its member bytes are equal. Genuine missing file bytes must continue to produce uncertainty, including when the same Evidence is supplied twice. ## Impact and scope P2: a supported static comparison produces false unavailable classifications and incorrect unchanged/unknown counts for complete declarations. In the mixed-content pair, the root and member content changes remain visible, but the false declaration row prevents a concrete aggregate `changed` result. This is a bounded result-accuracy defect; no data loss, target execution, source modification or security boundary crossing was observed. Observed scope is synthetic PyInstaller 2.1 CArchives on one host through production MCP. Inspection retains the declaration facts, which an analyst can read directly. A practical alternative that restores the complete tool result and its aggregate status was not verified. Ignoring every declaration row would risk suppressing real declaration differences and is not a demonstrated workaround. Runnable executables produced by PyInstaller, other host platforms and downstream behavior consumers were not tested. ## REA version Source revision 6dff37594c0bc942c9748b34a7a028d223e88a6d; package metadata 6.7.0. The runtime replay used the prepared compiled source build, not a separately installed release. ## Runtime and platform Node.js v24.18.0; macOS 27.0.1, build 26A434; arm64. The checkout's locked MCP client SDK connects to `node scripts/rea.mjs mcp`. ## Target and artifact context Synthetic ELF64 headers with appended PyInstaller CArchives, layout 2.1 and cookie Python version 309. The minimal archive contains only a valid zero-payload runtime-option declaration. Additional cases contain valid dependency declarations, repeated runtime options or inert script payloads. A header-only ASAR declares one unavailable unpacked file as a negative control. These are parser fixtures, not runnable application builds. ## Interface MCP ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence - Expected source: `docs/python-packaged-applications.md:29-46` defines `o` and `d` as metadata declarations without content artifacts and requires zero payload with no compression; it explicitly leaves external dependency contents unresolved. `src/contracts/sessionToolContracts.ts:107` and `.agents/skills/reverse-engineer-anything/references/evidence-workflows.md:46-49` define comparison over occurrence paths, content, metadata and graph relations. `docs/mcp-contracts.md:37-45` distinguishes complete scoped inventory from genuinely unavailable hashes and unknown coverage. - Failure source: Executed production MCP calls in the inline script, including exact Evidence self-comparisons and both controls. `src/artifacts/pyinstaller/CArchiveMembers.ts:59-82` projects declarations without readable content; `src/artifacts/inventory/scanReader.ts:296-312` hashes only files and slices; `src/domain/artifactComparison.ts:346-371` requires verified hashes and artifact nodes before comparing a present occurrence's facts. - Evidence provenance: observed. Tool results establish the failure and controls; the source references explain their reach and representation separately. - Local verification: reproduced. Both the initial independent program and the exact inline reproduction completed with exit 0, confirmed registration, and returned the stated comparison results. - Reproduction completeness: complete. Smallest useful result projection for the one-option case: ```json { "coverage": {"scope": "container-entries", "status": "complete"}, "same_manifest": true, "status": "unknown", "summary": {"unchanged": 1, "added": 0, "removed": 0, "changed": 0, "unknown": 1, "contradiction": 0}, "declarations": [{"path": "__pyinstaller__/runtime-options/0", "artifact_id": null, "hash_status": "not-hashed", "declared_size": 0, "compressed_size": 0, "source_name": "pyi-contents-directory _internal", "type_code": "o", "limitations": []}], "changes": [{"path": "__pyinstaller__/runtime-options/0", "classification": "unknown", "dimensions": ["availability"], "same_occurrence": true}], "limitations": [] } ``` The dependency-only self-comparison likewise reports one unknown; the repeated-option/dependency fixture reports `unchanged: 1, unknown: 3`. Their traversal coverage is complete and their declared manifest counts match the returned graph arrays. Each declaration has its own indexed path and occurrence identity, so repeated declarations do not create duplicate-path ambiguity. The dependency occurrence retains its explicit unresolved-external-bytes limitation. The content-only control reports `status: "unchanged"`, `unchanged: 2`, `unknown: 0`, and no changes. The missing ASAR companion control reports one `availability` unknown for `missing.bin`; that occurrence is a file with `hash_status: "unavailable"`, a four-byte declared payload, and an explicit missing-companion limitation. This is genuine missing evidence despite complete directory traversal and identical comparison inputs. The mixed-content pair reports `changed: 2, unknown: 1`, with real root/member content changes plus the false option availability row. Restoration check: Replay the same fixtures through the registered tools. The option-only and dependency-reference self-comparisons should classify their recorded declarations as unchanged, returning two unchanged occurrences and no change rows; the three-declaration fixture should return four unchanged occurrences. Preserve the dependency's unresolved external-content facts. Keep the content-only control unchanged and the missing-companion ASAR genuinely unknown. The mixed-content pair should retain both real content changes and yield aggregate `changed` with zero unknowns and one unchanged declaration. Equality must not come from an unconditional identical-Evidence shortcut, which would incorrectly erase the ASAR uncertainty. ## Analysis or suspected cause Source-derived: declaration occurrences intentionally skip content hashing, but the comparison availability gate requires both hashes and artifact nodes. That gate consequently treats a complete metadata declaration like an unavailable file before it reaches fact comparison. The observed controls support distinguishing those states without weakening missing-evidence handling; this report does not prescribe a particular implementation. ## Additional context Open and closed issue searches covered `compare_artifacts`, PyInstaller/unknown, runtime options, metadata/comparison, identical inventories, declaration/artifact, PyInstaller/compare, `__pyinstaller__`, `runtime-options`, and declaration/`compare_artifacts`, alongside the supplied full issue snapshot. Exact likely matches were read. Closed [#2173](https://github.com/morluto/rea/issues/2173) concerned root-only plist coverage; open [#6436](https://github.com/morluto/rea/issues/6436) concerns summary disclosure of genuinely unavailable ASAR content. Neither restores equality of complete declaration occurrences. Comparison carrier admission, partial-root accounting, duplicate ZIP counts, integrity attribution and extraction-provenance reports also have separate closure signals. No matching open or closed duplicate was found. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Registered MCP comparisons classify complete declaration facts as unavailable, inflate unknown counts, and prevent a concrete aggregate changed result in the executed mixed-content case. The underlying inventory and real content deltas remain available; no severe downstream incident or loss was observed. - Workaround status: unverified - Workaround basis: The original declaration facts are readable in inspection Evidence, but no alternative preserving the full comparison classification, counts and aggregate result was verified. Discarding all declaration rows could hide actual changes and does not establish a safe full-result workaround.
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#3891
No description provided.