Repository navigation
fix(artifacts): decide how compare_artifacts compares every format_metadata kind - #8231
Conversation
ESP32-family app and bootloader images (magic 0xE9) had no reader, so open_binary refused OTA downloads, app partitions and bootloaders. Add the esp-image container format behind inspect_artifact and extract_artifact. A pure decoder reads each byte once and keeps only fixed-size fields. Derived facts cover the chip, from a table cited to ESP-IDF v6.1; segment classification as ESP-IDF's should_map and should_load; checksum and appended SHA-256 results; app and bootloader descriptors; Secure Boot v2 signature blocks, observed and unverified; and undescribed trailing byte ranges. Truncation and undescribed bytes make coverage unknown. ESP8266 images are refused with a specific reason, and ciphertext stays unrecognized. Add the format to every target, Evidence, snapshot, live-summary and artifact vocabulary, document it, and add an optional verify:esp-images lane that compares esptool-built images with esptool image-info and espsecure signature-info-v2. Refs morluto#3615
The lane compared descriptor fields only when esptool image-info printed one, so a descriptor that REA reports and esptool does not print passed without comparison. Compare the descriptor kind in both directions. Record that the lane passed for all 13 images with esptool and espsecure v5.6.0, the version the layout facts cite, as well as with 4.12.0.
The decoder and the derived facts each defined the segment-header, appended-digest and flash-sector sizes, the erased-byte scan and the offset formatter. Define them once with the other fixed-layout structures and import them in both places. The signature slot count is the one constant that drives the slot scan. Correct the observedEnd comment: it is the end of the last chunk read.
ESP-IDF's verify_segment_header rejects a segment of 16 MiB or more unless the chip maps flash through 32-bit cache addresses (SOC_SPI_MEM_SUPPORT_CACHE_32BIT_ADDR_MAP: ESP32-S3, P4, C5, C61, H4 and S31 in v6.1). Record that capability in the chip table and add a limitation for such segments; unknown chips get none. `loaded` repeats should_load. It does not mean the bootloader accepts the segment: verify_load_addresses still rejects one outside the chip's RAM, which REA does not model. Say so in the docs, the skill reference and the inventory limitation.
The lane started work before checking its tools, so a missing RISC-V target, ld.lld, esptool or espsecure surfaced as a raw spawn or command error. Check each one before building and name the variable to set. Warn when esptool is not v5.6.0, the release the layout facts cite. Assert each segment's class for the ESP32-C3 app, its variants and the bootloader, as the Xtensa image already did. Extract the app and the signed image through the CLI and compare every file with the bytes at esptool's segment offsets or in the appended signature sector; the source's SHA-256 must be unchanged and equal the reported root_sha256.
The Verification section listed the lane's checks but not its gaps: only ESP32-C3 and ESP32 images come from a real producer, no real project build, OTA download or flash dump is inspected, no signature is verified, and the lane does not run in CI. Record the timing of the final lane run on esptool v5.6.0.
Segment entries hash only their data, so compare_artifacts reported a segment that moved to another load address with the same bytes as unchanged; only the image itself changed. Compare each segment's load_addr and data_len as its declaration, as Node SEA and PyInstaller members are compared, and count a changed declaration as a metadata change, also when content is unavailable. Header offsets are layout and the classification is derived, so neither is compared.
The lane ran its tools through execFile. A timeout killed only the direct child, so a linker or assembler that Clang or GCC had started could keep running while the lane removed its workspace. Run every command through createVerifierCommands, check the tools inside withVerifierCleanup, and remove the workspace only after commands.close() has stopped every tree; a failed close keeps the workspace. Only a nonzero exit now counts as a refusal or an expected esptool failure; timeouts and spawn failures propagate. Supervising each command roughly doubles the lane's run time.
The cancellation test requested cancel or close on setImmediate, which can land while the file is still being opened and checked, so it never showed that decoding had started. It also passed against a reader that ignored both requests while decoding. Acknowledge the decoder's first chunk, then cancel or close, join the operation, and assert that no later chunk reaches the decoder and that the reader ends closed.
Segments are compared by their declaration. The image's and the signature blocks' format_metadata are not: every field comes from their own bytes, which content comparison already covers, and a block's image_digest_matches follows the image's content.
compare_artifacts listed the format_metadata kinds it compared by hand, so a new kind went uncompared without notice; ESP segment load addresses were first missed this way. One table beside the schemas now gives every kind a declaration projection or the reason comparison uses none of its facts, and a kind without an entry does not compile. Consumers read one result per pair instead of naming formats. - Godot PCK entries stay uncompared. Godot looks members up by the simplified path without res://, which logical_path holds apart from Unicode normalization. Godot 4.3 exports store res:// and 4.4.1 exports do not, so comparing source_path would mark every member changed. The reader admits only the encryption flag, which the occurrence's encrypted field records. - A changed PyInstaller declaration is now a metadata change also when the member's content is unavailable, as for ASAR, Node SEA and ESP segments. It was unknown before, for no recorded reason. - Root kinds and ESP signature blocks stay uncompared: their fields come from their own bytes, which content comparison covers. - A declaration that only one side records now adds one generic limitation for every compared kind. Only Node SEA added one before, with its own text.
…tent The record rule compared two metadata occurrences by their declaration alone. A hand-built record that names content, with an equal declaration, then hid a content change; PyInstaller records already did so before the table. The rule now needs both records without an artifact_id, which is every metadata record a reader emits. Review changes in the same area: - The unknown-declaration limitation also covers declarations of different kinds and non-container observations, and tests compare it with its constant. - docs/godot-pck.md gives each Godot version's lookup path, the backslash case, and that a member that becomes encrypted compares unknown. The table points to it instead of restating it. - The PyInstaller regression moves to an acceptance test whose MCP client validates the advertised output schema, and the domain tests keep one representative case per behavior.
An encrypted member has no content. Comparison then took the content-unavailable fallback, which counted only ASAR storage and format declarations, so a Godot PCK, ZIP or PYZ member that became encrypted compared unknown although both sides record `encrypted`. The record rule compared node-less metadata records by format declaration alone, so a PYZ namespace record whose encryption flipped compared unchanged. `OccurrenceMetadata` now owns the encryption comparison beside ASAR storage and the format declaration. Every occurrence records `encrypted`, so a pair's comparison is never unknown. The fallback, the record rule and the metadata dimension read it there; `metadataChanged` no longer compares `encrypted` itself. The record rule compares all of `OccurrenceMetadata`: a changed value is changed in metadata, with availability when another is unknown. Decision: occurrence limitations still do not count for node-less records. Readers derive them from the declared facts (the PYZ caveat follows `encrypted`), and their wording can change between REA versions while the record stays the same. The fallback does not compare them either. Behavior changes: - A pair with unavailable content whose `encrypted` differs is changed [metadata, availability], not unknown [availability]. - A node-less record whose `encrypted` differs is changed [metadata], not unchanged. - A hand-built node-less record that records ASAR storage now compares it; no reader emits such records. Remaining: - The fallback still ignores `entry_kind` and `executable`, which readers also declare, so a file that becomes a symlink, or a mode change on an encrypted ZIP member, still compares unknown. `executable` has its own dimension, so its fix needs content-free checks split from node checks. Each is its own behavior decision. - Node-bearing pairs still compare limitation wording; records do not. Whether limitation wording is ever a comparison fact needs one decision.
…vailable When either side lacked verified content, comparison took a fallback that counted only ASAR storage, encryption and format declarations. It compared `entry_kind` only in the metadata check of pairs with content, and `executable` only beside the node checks. A ZIP member that became a symlink, whose target REA does not read, and an encrypted ZIP member whose Unix mode gained an execute bit therefore compared unknown, although readers declare both fields on every occurrence. `OccurrenceMetadata` now owns the entry kind comparison beside encryption; it is never unknown. Comparison checks fall into two groups: declared facts that need no content (`executable` and the metadata declarations), and facts that need both content nodes (content, kind, format, size, relations and node metadata). The fallback, the record rule and pairs with content share the declared checks and one rule for unknown declarations. Kind and format stay content checks: without content they are guesses from the path. Behavior changes: - Unless both sides have content, a pair whose entry kind differs is changed [metadata, availability], not unknown [availability]. - Such a pair whose `executable` differs is changed [executable, availability], with metadata when another declared fact also differs. - A hand-built node-less record whose `executable` differs is changed [executable]. No reader emits such records. Remaining: declared sizes and integrity digests still compare only through observed bytes, so they do not count when content is unavailable.
On POSIX hosts the directory reader recorded a symlink's own permission bits as `executable`. Linux reports 0777 for every symlink and macOS usually 0755, so directory, app bundle and DMG symlinks read true. zip.js, the PyInstaller reader and the Windows branch record false. The previous commit compares `executable` without content, so a symlink that two readers or two hosts record differently compared changed in `executable`, and a non-executable file that became a symlink also reported `executable`. `hostExecutableMarking` now gives a symlink no marking on any host: running a symlink runs its target, and the link's own bits do not control that. Behavior change: directory, app bundle and DMG inventories record `executable: false` for symlinks. Occurrence IDs do not change, but the graph digests and manifest IDs of inventories with symlinks do. Comparing an inventory stored before this change with a new one therefore reports changed [executable, availability] once for each such symlink.
Pairs with content compared node and occurrence limitations as metadata, while node-less records, the content-unavailable fallback and inventory limitations did not. Limitations explain what an observation could establish. Readers mostly derive them from fields that comparison already reads, from other occurrences or from what REA could read, and their wording changes between REA versions. So adding `A` beside an unchanged ZIP member `a` marked `a` changed in metadata through its case-collision note, and an older and a newer inventory of equal bytes could differ wherever a reader reworded a note. No comparison path reads limitations now. Inventory limitations still appear in the result, and occurrence limitations stay in the inventory Evidence. Readers write no node limitations, so only hand-built inventories compared them. Behavior change: for a pair with content, limitation text no longer adds metadata. A pair that differs only there compares unchanged, or unknown [availability] when a declaration is unknown. Remaining: a few facts appear only in limitation text, or also in fields that comparison does not read, so their changes are no longer reported while the compared bytes stay equal: an ASAR block integrity table or an unsupported integrity declaration, a Godot member's all-zero MD5, a ZIP name's leading `./` (also in `source_path`), and a PyInstaller PYZ image digest (also in `pyz_sha256`; the archive root still reports the change). Each needs a field or a comparison decision of its own if it matters.
Without observed bytes on both sides, comparison ignored the containers' declared integrity claims. Two encrypted Godot PCK members therefore compared unknown even when their declared MD5s showed that the script had changed. Godot's exporter stores the MD5 of a member's original bytes, so the MD5 does not depend on the encryption IV. This comes from the 4.3, 4.4.1 and 4.7.1 exporter sources; no real encrypted export was compared. When either side has no content, claims of one algorithm that differ now make the path changed in metadata and availability. Equal claims add no change, because they are unverified, and observed bytes still decide when both sides have them. Godot PCK and ASAR readers record such claims. For a readable member, a directory that declares or omits an MD5 only decides whether REA can check its bytes, so such a member compares unchanged. An encrypted member with an MD5 on only one side stays unknown. Remaining: declared sizes still compare only through observed bytes.
docs/mcp-contracts.md named the format_metadata kinds that compare_artifacts compares in prose, by hand, so a kind added to the declaration table could go unnamed. The integration branch already omits esp-flash-partition, which the ESP flash layout branch adds to the table. The paragraph now names each compared kind by its identifier. A conformance test requires it to name exactly the kinds whose table entry has a declaration, which artifactFormatMetadata.ts exports as COMPARED_FORMAT_METADATA_KINDS, and no kind that comparison ignores.
Six artifact comparison acceptance tests each copied the same MCP steps: open_binary, inspect_artifact, the inventory_artifact substep Evidence, then compare_artifacts. The coverage test repeated the inspect steps three times and kept its own inventory lookup. tests/fixtures/artifactComparisonMcp.ts now owns these steps on the schema-validating connectLocalToolsMcp client. connectArtifactComparisonMcp() returns call, inspect(path) and compare(left, right). inventoryEvidence() reads the inventory Evidence that an inspection retains, from MCP or CLI output. Each test still writes its own authored input and keeps its expectations. The coverage lookup matched evidence.operation, not the substep operation. The inspection schema fixes both to inventory_artifact, so both select the same substep. The plist acceptance test does not change. It compares the inspection Evidence itself across CLI and MCP and passes the comparison Evidence to verify_reconstruction and find_changed_behavior. No assertion or scenario changes.
Upstream squash-merged this branch's ESP image prototype (ea2b386) as morluto#7498 (64196b0), with maintainer commits on top. The merge therefore uses the prototype as the base: files this branch did not change after the prototype take main's version, and the nine files it did change merge against the prototype. Git's own base, d6f88be, gave 32 false conflicts and kept src/domain/firmware/espImage.fixture.ts, which upstream moved to tests/fixtures/espImage.ts. Main now compares format metadata with one function per format: morluto#7932 (morluto) keeps known metadata when content hashes are missing, morluto#7771 adds FIT images, and morluto#3787 adds .NET bundle members. This merge keeps the declaration table as the one owner of what each kind declares, and adopts main's comparison semantics: - When either side's content is unavailable, a path lists its known differences and is unknown unless a compared declaration or ASAR storage changed (morluto#7932). - A field that the producer could not read compares unknown on either side, and the other fields still compare. A table entry names such fields; fit-image names its unreadable_fields (morluto#7771). - Limitation wording stays compared, as main compares it. Table decisions for main's new kinds: - unityfs-entry compares source_path and flags, hex-record-region compares start_address and length, and unityfs, hex-records, uimage and uimage-entry are not compared. These repeat the decisions that embedded-integration made. - fit-image compares main's fitImageDeclaration fields; fit is not compared, because its fields come from the FIT's own bytes. - dotnet-bundle-entry compares file_type, which tells the host how to treat a member. Its header versions come from the host's own bytes. Main did not compare this kind. fitImageDeclaration, nodeSeaEntryDeclaration, espImageSegmentDeclaration and pyInstallerEntryDeclaration move into the table. The generic limitation for a declaration that one side omits replaces main's Node SEA limitation, and a changed PyInstaller declaration stays changed when content is unavailable, as for the other compared kinds. Tests: main's unit tests keep their packingObservation calls on the shared observation helper. A new acceptance test compares .NET bundle members by file type. Docs name every compared kind and the per-field unknowns, and the UnityFS, .NET and HEX guides name their compared fields.
…-comparison The base branch now merges upstream main, which compares a path without content as morluto#7932 does: the path lists the differences that are still known and is unknown unless a declaration changed. This branch adopts that rule for encryption: - An encryption change no longer makes a path without content changed. A member that becomes encrypted compares unknown in metadata and availability, as main already reports it. declarationChanged keeps only ASAR storage and format declarations. - OccurrenceMetadata still owns the encryption comparison, and the metadata dimension reads it there instead of comparing encrypted again. - A node-less record still compares its declaration, encryption and ASAR storage, and a difference in them is changed. A record has no content whose identity could be unknown, and its limitations still do not count. Changed expectation: "reports a Godot PCK member that becomes encrypted" in tests/acceptance/artifactComparisonGodotPck.test.ts now expects unknown, not changed, with the same metadata and availability dimensions. The contract, Godot PCK and PyInstaller docs state the rule for paths without content and for records.
The base branches now merge upstream main and adopt its comparison of paths without content (morluto#7932, morluto): such a path lists the differences that are still known and is unknown unless a declaration changed. Upstream's artifactComparisonUnavailableMetadata acceptance test requires this for a symlink compared with an executable file. This merge revises this branch to that rule: - 2fc3bd2 compared entry kind and executable without content and called the path changed. Main's classifyPath replaces its fallback, declaredChecks, nodeChecks and classifyChecks: an entry kind or executable difference without content is now unknown, with the difference in its dimensions. entry_kind leaves OccurrenceMetadata, because main compares it where it lists differences. A node-less record still compares executable, as changed in executable. - 9dbf90b stopped comparing limitation wording. Main compares occurrence and node limitations as metadata, so that change is undone with its unit test and its ZIP acceptance test. With this merge, a ZIP member `a` compares changed in metadata when a sibling `A` is added, because the reader adds a case-collision limitation to `a`. - 0c92f1c compared declared digests without content and called the path changed. Differing claims of one algorithm now add metadata to the unknown path: they are unverified, like equal claims. - The symlink executable marking (a6e09ec), the contract conformance test (1a068ac) and the shared MCP fixture (964be74) are kept. The contract paragraph names every kind the table compares, and the new .NET bundle acceptance test uses the shared fixture. - The ESP comparison test takes main's fixture path, tests/fixtures/ espImage.ts, with the shared MCP fixture. Changed expectations, each for the rule above unless stated: - artifactComparisonZip.test.ts, "reports a ZIP member that becomes a symlink": unknown [metadata, availability], not changed. - artifactComparisonZip.test.ts, "reports an executable mode change on an encrypted ZIP member": unknown [executable, availability], not changed. - artifactComparisonZip.test.ts, "keeps a ZIP member unchanged when a case-colliding sibling is added": removed with 9dbf90b. - artifactComparisonCoverage.test.ts, "reports a directory file that becomes a symlink without marking the link executable": unknown [metadata, availability], not changed. The link still records executable false. - artifactComparisonGodotPck.test.ts, "compares encrypted Godot PCK members by their declared MD5": differing MD5s give unknown [metadata, availability], not changed. An MD5 on one side now gives [metadata, availability], not [availability], because the reader's all-zero MD5 limitation is compared again. - artifactComparison.test.ts, "does not compare limitation wording beside content": removed with 9dbf90b. The contract and Godot PCK docs state these rules, and that occurrence limitation text is a metadata difference.
Upstream main compares no .NET bundle member facts. Comparing `file_type` changed `compare_artifacts` output beyond this branch's fixes, so the kind stays uncompared until that is decided. A changed manifest still shows as a content change of the host.
The comparison table compared a region's start_address and length. A region's path names its start and end addresses, and its length follows from them, so two regions that pair at one path always declare the same facts: the entry could never find a change. It only made a region with equal bytes unknown when the other side recorded no metadata, such as a directory of extracted regions. The kind is not compared, as on main. The contract paragraph also stops naming .NET bundle members, which 3e51a77 left uncompared.
The record rule follows the comparison table, not PyInstaller, but only PyInstaller records exercised it. An ESP segment record shows both outcomes: an equal declaration compares unchanged and a moved one changed in metadata, without the availability that the fallback for unread content would add.
The record rule compares ASAR storage beside the declaration and `encrypted`, but no test recorded it on a metadata record. A changed value is changed in metadata, and a value that only one side records is unknown in availability, as for any occurrence.
# Conflicts: # docs/mcp-contracts.md
[Medium impact] The PR appears safe to merge; no new actionable issue remains. SummaryMoves format-declaration comparison into one exhaustive table and adds UnityFS member comparison.
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["Paired occurrences"] --> B["Project declarations through declarationsByKind"]
B --> C["Compare readable facts and retain unknowns"]
C --> D{"Both are metadata records without content?"}
D -->|Yes| E["Compare declaration, encryption, executable marking and ASAR storage"]
D -->|No| F["Compare content when available; retain known differences otherwise"]
E --> G["Change rows and limitations"]
F --> G
Reviews (3) · Last reviewed commit: "Merge upstream main into fix-artifacts-c..." · Reviewed by Greptile |
The table compares a UnityFS member's declared source_path and raw flags, but no comparison test covered the kind, so dropping either field from the projection left every test passing. Bundles with equal member bytes now show both: other flags, or a stored path in decomposed form, compare changed in metadata, and reordered members, whose index and stream offset are layout, compare unchanged.
The UnityFS comparison test arrived from the bottom branch with its own copy of the open_binary, inspect_artifact and compare_artifacts steps, which tests/fixtures/artifactComparisonMcp.ts owns here. It now uses connectArtifactComparisonMcp() like the other comparison acceptance tests. No assertion changes.
The docs conformance test found its paragraph by an exact English phrase, so rewording "also compares" failed it while the named kinds stayed right. The paragraph now sits between BEGIN and END markers, as the allocation report's sections do, and the test reads the kinds between them. It still requires exactly the kinds whose table entry has a declaration.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
@Entrpi, could you please resolve the merge conflicts with |
Main's ESP flash layout (morluto#8228) and UF2/DfuSe (morluto#8230) changes compare their declarations through per-format fields. Here the table owns every kind: esp-flash-partition and dfuse-element get table entries with main's fields, and esp-flash-image, uf2, uf2-region and dfuse state why they are not compared. espFlashPartitionDeclaration, its EspFlashPartitionMetadata alias, dfuSeElementDeclaration and pyInstallerEntryDeclaration are removed, and the contract paragraph names the two compared kinds.
|
Thanks for merging #8227–#8230. |
Part of the embedded roadmap #7955.
Summary
compare_artifactscompares someformat_metadatadeclarations. Onmain, the compared kinds are named by hand in six per-format functions and fields, so a new kind goes uncompared and nothing reports it. ESP segment load addresses were first missed this way, and UnityFS member flags still are. This PR puts the decision in one table beside the schemas. Each kind either projects the declaration that comparison compares or states why comparison uses none of its facts, and a kind without an entry does not compile. A conformance test checks that the contract names exactly the compared kinds.Problem and expected behavior
maind48e472fachanged[metadata]unknown[metadata, availability]changed[metadata, availability]flagschange, with equal byteschanged[metadata]executablefrom the link's own mode bits (Linux reports 0777 for every symlink)executableunknown[availability]unknown[metadata, availability]The first four rows were executed on
mainwith hand-built inventories (see Evidence). The last two are derived from source, and so are row 4's FIT and PyInstaller cases.On
main,declarationChangedmakes a path without contentchangedwhen ASAR storage or a Node SEA, ESP segment, DfuSe element, ESP flash partition or FIT declaration changed, but not a PyInstaller declaration. PyInstaller declaration comparison (142cf188a) is older than that exception, and no test or commit gives a reason to leave it out. Here every compared declaration counts, so such a result's status can bechangedinstead ofunknown.main's sentence indocs/python-packaged-applications.md, "Missing declaration facts and unavailable non-metadata content remain unknown.", predates the exception; this PR rewords it.The generic limitation replaces "Node SEA member metadata is recorded on only one side for matching paths; …" with "Format metadata declarations are missing on one side or differ in kind for matching paths; omitted metadata may be a legacy or non-container observation and leaves the complete declaration unavailable."
Unchanged from
main: #7932's rule that a path without content isunknownunless a declaration changed, FIT per-field unknowns, and the comparison of occurrence limitation text.dotnet-bundle-entry, the Bun kinds,hex-record-region,esp-flash-image,uf2,uf2-regionanddfusestay uncompared, andesp-flash-partitionanddfuse-elementcompare the same fields as onmain. A HEX region's path names its start and end addresses, so its declaration could never differ between paired regions.Change and scope
src/domain/artifactFormatMetadata.ts:declarationsByKinddecides all 26 kinds. Compared:unityfs-entry(source_pathand rawflags; new),pyinstaller-entry,node-sea-entry,esp-image-segment,dfuse-element,esp-flash-partitionandfit-image(with its unreadable fields). Each other kind states why it is not compared.main's six*Declarationfunctions are removed.src/domain/artifactComparison.ts:OccurrenceMetadataholds four comparisons: ASAR storage, encryption, the table's declaration and that declaration's field availability. A declaration changed when ASAR storage or the table's declaration changed. A node-less metadata record compares its declaration,encrypted,executableand any recorded ASAR storage. Here a node-less record is ametadataoccurrence withoutartifact_idon both sides, with a compared declaration on at least one side. Its limitations are not compared, as onmain. A metadata record that has content keeps the content comparison.classifyPyInstallerDeclarationandcompareFitImageDeclarationsare removed.src/artifacts/hostExecutableMarking.ts: a symlink has no executable marking on any host, because running a symlink runs its target.Docs: the contract in
docs/mcp-contracts.md, and the Godot PCK, HEX/S-record, UnityFS and PyInstaller guides.Commits, in stack order:
9e65be9e3,88869713e,3e51a7760(.NET bundle members stay uncompared, as onmain) and7978b9400(HEX regions too);OccurrenceMetadata:2b66531c7;executableon records, symlinks and declared digests:2fc3bd2c1,a6e09ece3,0c92f1c57;1a068ac21,964be7465;9ca54d6b0,eb46be7fc;9d5e31a95(a UnityFS comparison test),8177a12c4(which moves it to the shared fixture) and6b59c4f51(the docs test finds the compared kinds by marker).The merges of
mainadopt fix(artifacts): preserve known metadata without content hashes #7932, so some earlier commit messages describe superseded behavior. For example,2fc3bd2c1sayschanged, and9dbf90bd9stops comparing limitation text; that change is undone. The branch also holds feat(artifacts): inventory and extract ESP32 firmware images #7498's pre-squash commits. Their content is already inmain, so they add nothing to the diff.Not included: declared sizes still count only through observed bytes. Limitation text still counts as
metadata, as onmain, so a new reader caveat can mark an unchanged member changed: adding ZIP memberAdoes this toathrough its case-collision note. Whether limitation text is a comparison fact needs its own decision.Contract and boundary impact
declarationsByKindowns each kind's projection, andoccurrenceMetadataowns each pair's comparison. The symlink change is at the directory reader (hostExecutableMarking). The superseded per-format functions and fields are removed.executable: false, as Windows, zip.js and the PyInstaller reader already do.unknown[executable, availability].docs/public/product-catalog.json), package, or installation impact: none.Expected effects:
Evidence and regression coverage
Tests added or updated:
src/domain/artifactComparison.test.ts: one case per rule above, including HEX regions and ESP signature blocks staying uncompared, and the record rule for another compared kind and for ASAR storage.res://spellings, encryption and declared MD5s.flagsor a decomposed stored path; reordered members stay unchanged.tests/conformance/artifactComparisonDocs.test.ts, which reads the compared kinds between<!-- BEGIN/END:compare-artifacts:compared-format-metadata-kinds -->markers, andsrc/artifacts/hostExecutableMarking.test.ts.main's Fit, UnavailableMetadata, HistoricalSummary and Plist acceptance tests are unchanged and pass.Base reproduction or other evidence, executed on
maind48e472fa:src/domain/artifactComparison.test.ts(at2304a0596) fails 6 of its 32 tests onmain's source:unknown, and a record's occurrence fields and ASAR storage are not compared.main's results for the first four rows of the table above; for row 4 it covered Node SEA and ESP segments. It passes on2304a0596.User-visible CLI/MCP output (if applicable): the change rows in the table above, which the acceptance tests assert through MCP.
Remaining proof gaps: all inputs are synthetic fixtures. No real PyInstaller build with link entries or legacy namespace encryption was compared, and no real encrypted Godot export. The MD5 rule relies on Godot's exporter source (4.3, 4.4.1 and 4.7.1), which hashes a member's original bytes.
Observed, derived, and inferred claims remain distinguishable.
Artifact identity, source provenance, and failed attempts remain preserved.
Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.
Validation performed
All on
c0ed34063, Node 24.22.1, macOS:npm run check:fast— pass (typecheck and lint, nothing cached).npm run format:check,npm run docs:check,npm run verify:module-boundaries— pass.npx knip— one finding, the unused exported typeDarwinHelperCacheinsrc/process/DarwinHelperCache.ts; knip reports it onmain6e9ee97a6too, so none comes from this PR.npm run test:focusedover 99 files — 1,216 passed, 4 skipped (Linux-only extraction cases). The files are every test that callscompare_artifacts, the symlink marking test, the HEX reader and MCP tests, the docs conformance test, andmain's artifact and firmware tests changed since3b647132c, including the ESP flash, UF2, DfuSe and iNES tests.flags, or withoutsource_path, in theunityfs-entryprojection,tests/acceptance/artifactComparisonUnityFs.test.tsfails at the matching assertion. With "also compares" reworded,tests/conformance/artifactComparisonDocs.test.tsstill passes; withunityfs-entryremoved from the marked paragraph, it fails.npm run test:localonmaind48e472fa, in a throwaway worktree, with this PR's comparison unit tests (at2304a0596) and a scratch test — results under Evidence.CI passed on
c0ed34063(run).Compatibility, safety, and release
feat-artifacts-esp-flash-layout) and the UF2/DfuSe PR (branchfeat-artifacts-uf2-dfuse) each change howcompare_artifactscomparesformat_metadata. Whichever merges later adds comparison-table entries for the new kinds (esp-flash-partition,dfuse-elementand their siblings), as the fork's integration branch already does; the table does not compile while a kind lacks an entry.Author readiness
Human owner: @Entrpi
Before merge
type(scope): outcome.