feat(evm): inspect lossless Solidity terminal CBOR metadata (#2790)

feat(evm): retain lossless terminal CBOR metadata

Refs morluto/rea#2388

Co-authored-by: Matheus Breguêz <matbrgz@gmail.com>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
morluto 2026-10-13 21:55:30 +08:00 committed by GitHub
parent a3daf8a07b
commit 48a683ac8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1002 additions and 5 deletions

View File

@ -43,3 +43,5 @@ jobs:
echo "REA_VERIFY_SOLC_MODULE=${evm_fixture_root}/node_modules/solc/index.js" >> "${GITHUB_ENV}"
- name: Verify source-owned compiled bytecode through public CLI and MCP
run: npm run verify:evm:interface
- name: Verify content-hash-free and CBOR-free owned compiler artifacts
run: npm run verify:solidity:metadata

View File

@ -39,6 +39,18 @@ license](https://github.com/morluto/rea/blob/main/third_party/evmole/README.md)
- Upstream also returns terminal metadata during selector recovery. Its reported
values remain in `raw_result`; integer precision and non-text-key coverage are
unknown in the JS binding. Full bytecode retains the original encoded metadata.
- `bytecode.metadata` independently interprets a plausible terminal CBOR root
map from the selected bytes. Integers use decimal strings; byte strings use hex,
and scalar keys retain their types. The recorded offset/length addresses the
original bytecode, so summary delivery does not copy an opaque CBOR payload.
Nested containers have partial scalar coverage; unsupported or malformed
candidates report `not-decoded` with a diagnostic. Decoding is bounded to 64
container levels and the terminal payload's encoded size.
- An unavailable trailer does not prove metadata absence or Solidity origin.
Compiler-version bytes/text are metadata declarations, not verified compiler,
source-file or deployed-contract identity. Historical omission parses as a null
unknown facet without changing the original Evidence record. Metadata/source
URLs are not fetched.
- Selectors, argument strings and state mutability are **inferences**. Function
names/signatures are not looked up. Optimized code may produce different
mutability candidates; recovered types are not a verified source ABI.
@ -125,3 +137,15 @@ The real verification lane traces representative raw/hex CLI workflows and check
that only declared Node/prlimit launchers and process ownership inspection run.
Socket traces permit local stdio socket pairs and reject attempted network requests.
These observations verify those fixtures; they are not a general network sandbox.
For the independent metadata parser, `npm run verify:solidity:metadata` uses an
existing caller-selected solc 0.8.30 module:
```sh
REA_VERIFY_SOLC_MODULE=/absolute/path/to/solc/index.js npm run verify:solidity:metadata
```
It compiles the owned fixture with unoptimized, viaIR, content-hash-free and
CBOR-free settings, verifies the observed trailer facts and reports local decode
timings. No contract is executed. This lane does not establish the complete
Linux EVM provider's CLI/MCP, resource-limit or cleanup behavior.

View File

@ -0,0 +1,33 @@
# Implementation plan
1. Use cborg 6.1.3 (Apache-2.0) as a pinned format decoder, with a bounded
tokenizer at its owning adapter boundary. Decode maps as Map, preserve integer
precision and strictly validate emitted text bytes before conversion.
2. Add the portable metadata-result schema in src/domain/evm/. Use exact byte
ranges into the already retained bytecode rather than copying an opaque CBOR
hex payload into every selected summary.
3. Add src/evm/SolidityMetadata.ts to interpret the terminal length/root-map
candidate. Return unavailable, decoded, partial or not-decoded facts; keep
unsupported nested values explicit instead of recursively expanding them.
4. Compute the facts in EvmoleInterfaceWorker from its selected bytes. The host
provider validates ranges/identity and projects the additive metadata field.
Preserve raw engine output without rewriting its unknown-precision metadata.
5. Ensure the canonical EVM bytecode shape carries the optional field into complete
and summary Evidence. Include decoding profile, coverage and limitations.
6. Add producer, worker, historical contract and public MCP boundary coverage.
Compile an owned Solidity fixture with a pinned caller-supplied solc module,
and verify its CBOR compiler version. Run relevant static/architecture checks.
7. Extend the existing real Linux CLI/MCP verifier with independent metadata
assertions. Run the compiler-setting matrix in its CI lane and classify its
verifier as EVM-owned, with a production CI-planner regression.
No new execution provider or target authority is introduced: a format parser is
added inside the existing EVM worker. Linux x64 provider admission, target-free
semantics, cleanup, limits and complete original Evidence remain unchanged.
No metadata JSON or source URL is automatically acquired.
The parser bounds nesting to 64 and token/collection growth to the selected
terminal payload's byte count (the format's terminal length is at most 65535).
Every data item consumes encoded bytes; broad declared collection lengths cannot
justify allocation without that lower bound. Expanded results still pass the
worker's existing 16 MiB output check.

View File

@ -0,0 +1,43 @@
# Solidity terminal metadata inspection
Issue: https://github.com/morluto/rea/issues/2388
## Outcome
The existing EVM interface operation should retain independently decoded terminal
CBOR candidate facts from the exact selected bytes instead of relying on precision
unknowns in the engine's reported metadata. This increment covers trailer ranges,
bounded decoding, exact integer/non-text scalar key evidence and compiler-version
representations.
## Requirements
- Preserve the existing carrier/decoded identities and original full bytecode.
- Report a plausible terminal root-map candidate separately from proof of Solidity
origin. Missing or non-map trailers remain unavailable, not proven absent.
- Interpret scalar root-map keys/values losslessly: integers as decimal strings,
bytes as hex, strings, booleans and null. Preserve undecoded/unsupported values
through the original bytecode plus exact candidate offset/length.
- Report partial and not-decoded coverage, parser/profile failures and unsupported
CBOR forms explicitly. Do not silently coerce unknown keys into object properties.
- Interpret admitted solc release/prerelease representations only from observed
entries; preserve ambiguity if duplicate compiler-version keys occur.
- Keep the additive field compatible with historical Evidence and retain
it through selected summary delivery and public CLI/MCP.
Historical omission parses as a null unknown facet without mutating the source.
- Bound nesting before decoder recursion and collection lengths before allocation.
Keep parsing inside the existing owned worker and its resource/output budgets.
- Do not fetch metadata or source content, contact RPC or execute contract code.
## Acceptance and limits
Owned compiler fixtures and malformed/synthetic trailers cover integers beyond
Number precision, non-text keys, nested/uninterpreted values, invalid UTF-8,
invalid lengths, unsupported tags and nesting bounds. Public boundary tests
preserve the field and older omission. A real pinned solc artifact verifies actual
metadata facts; the existing Linux-only EVM host profile remains unchanged.
## Deferred issue criteria
Binding a supplied metadata JSON to IPFS/Swarm commitments is a later independent
increment of #2388. This PR must reference rather than close the whole issue.

View File

@ -0,0 +1,8 @@
# Tasks
- [x] Define single-feature requirements and implementation plan before code.
- [x] Add the pinned decoder and bounded owner boundary.
- [x] Implement lossless scalar metadata facts and compiler-version evidence.
- [x] Integrate worker/provider contracts and summary/historical compatibility.
- [x] Verify producer, worker and public boundary behavior with owned fixtures.
- [x] Run relevant repository checks and prepare a separately scoped PR.

View File

@ -0,0 +1,43 @@
# Verification
After integrating upstream c462a4ebd, 47 tests passed
across the parser, domain contract, application service, compiled worker and MCP
boundary (five files). Compiled-worker goldens include BOM text, partial nested
values, unavailable trailers, malformed UTF-8 and unsupported floats. Five
upstream adapter tests also passed after its stricter integer validation landed.
All six static tasks and architecture guards
passed. After integration, test discovery found all 1004 files exactly once.
The CI-planner selection passed all 85 tests, including the new EVM-verifier case.
The parser regressions cover exact large integers and typed keys, malformed UTF-8,
duplicate keys, excessive nesting, unsupported tags/floats, length bounds and
historical omission. A real defect in the decoder's default text handling removed
an initial U+FEFF. The owning tokenizer now validates and decodes the retained
text bytes with BOM preservation; independent fixtures check both values and
compiler-key identity.
The owned fixture was compiled with caller-selected solc 0.8.30, using unoptimized,
optimized via-IR, content-hash-free and CBOR-free settings. All four metadata
checks passed again after integration. No contract was executed. Local first decode
took approximately 0.81 ms or less. The originally reported approximately 2.92 ms
for 100 warm iterations included equality assertions and is not a decode-only
measurement. The verifier now measures parsing separately from those assertions;
updated warm timings have not been collected. These historical figures describe
these fixtures and this host, not a general workload.
Peak process RSS
included the compiler (299536 KiB); complete process-family CPU/memory and agent
token costs were not measured.
Compiled worker tests exercise unchanged EVMole WASM on this macOS host. MCP tests
use the production server and contracts with an injected provider seam. The full
Linux-only provider CLI/MCP, resource-limit and cleanup lane remains unverified
here. Metadata JSON commitment binding remains deferred under issue #2388.
The Linux CLI/MCP verifier asserts exact compiler bytes, declared version and
candidate ranges for its existing owned raw/hex artifacts. Its CI lane additionally
runs the new compiler-setting matrix. A regression using the actual CI planner
checks that edits to the new verifier select the EVM lane.
The later merge/review correction adds decoded-summary and omitted historical
metadata coverage. Tests and provider reruns for that correction were skipped at
the maintainer's request; the earlier runs above do not validate the changed head.

View File

@ -2,7 +2,7 @@ REA npm dependency inventory
Prepared from the root package-lock.json; refresh this inventory when locked dependencies change.
License identifiers are upstream lockfile declarations, not a legal assessment.
Runtime includes transitive and optional platform packages; development includes build, test and website tooling.
Locked package entries: 586; runtime: 174.
Locked package entries: 587; runtime: 175.
Full runtime copyright/license/NOTICE texts: npm-runtime-notices.txt.
@algolia/abtesting@1.25.0 | development | MIT
@ -1430,6 +1430,11 @@ canonicalize@3.0.0 | runtime | Apache-2.0
Package: https://www.npmjs.com/package/canonicalize/v/3.0.0
Archive: https://registry.npmjs.org/canonicalize/-/canonicalize-3.0.0.tgz
Integrity: sha512-yYLfHyDMIXRyRqsKBRLX023riFLpXY2YOfdtqKXZRZy9qsfOJ9U+4F9YZL7MEzL5+ziN2x2nlBvY/Voi3EBljA==
cborg@6.1.3 | runtime | Apache-2.0
Lockfile path: node_modules/cborg
Package: https://www.npmjs.com/package/cborg/v/6.1.3
Archive: https://registry.npmjs.org/cborg/-/cborg-6.1.3.tgz
Integrity: sha512-Kb/wD/AFuVcqvVjR+HYNOjNiuhVJ2C+RWxQE2Yqq2WVlVkRjdvVLkkM6QsL3NMsBRh+rgNc+aXmcraA+X0HObQ==
ccount@2.0.1 | development | MIT
Lockfile path: node_modules/ccount
Package: https://www.npmjs.com/package/ccount/v/2.0.1

View File

@ -1,7 +1,7 @@
REA third-party npm runtime copyright, license and NOTICE texts
Collected from integrity-verified locked npm archives. Release-specific supplements and upstream notice gaps are identified in their entries.
External engines are separately obtained; see THIRD_PARTY_NOTICES.md.
Runtime dependency fingerprint (SHA-256): 5de9cf97e7c327da1892efeff30b40507e4c7f8e496b5d305b8016c162bc15e3
Runtime dependency fingerprint (SHA-256): 63d5b55e027a0cfb3e77989ca337aca905f1652d10cfbfc4d4dd876d3ecd4f3a
===== @babel/helper-string-parser@8.0.0 =====
Declared license: MIT
@ -2456,6 +2456,234 @@ Scope: npm runtime dependency, including optional platform packages.
limitations under the License.
===== cborg@6.1.3 =====
Declared license: Apache-2.0
Archive: https://registry.npmjs.org/cborg/-/cborg-6.1.3.tgz
Integrity: sha512-Kb/wD/AFuVcqvVjR+HYNOjNiuhVJ2C+RWxQE2Yqq2WVlVkRjdvVLkkM6QsL3NMsBRh+rgNc+aXmcraA+X0HObQ==
Scope: npm runtime dependency, including optional platform packages.
--- package/LICENSE ---
Copyright 2020 Rod Vagg
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
--- Apache-2.0 full license (licenses/Apache-2.0.txt) ---
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
===== clean-git-ref@2.0.1 =====
Declared license: Apache-2.0
Archive: https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz

10
package-lock.json generated
View File

@ -24,6 +24,7 @@
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"canonicalize": "3.0.0",
"cborg": "6.1.3",
"cross-spawn": "7.0.6",
"diff": "9.0.0",
"evmole": "0.9.3",
@ -4181,6 +4182,15 @@
"node": ">=18"
}
},
"node_modules/cborg": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/cborg/-/cborg-6.1.3.tgz",
"integrity": "sha512-Kb/wD/AFuVcqvVjR+HYNOjNiuhVJ2C+RWxQE2Yqq2WVlVkRjdvVLkkM6QsL3NMsBRh+rgNc+aXmcraA+X0HObQ==",
"license": "Apache-2.0",
"bin": {
"cborg": "lib/bin.js"
}
},
"node_modules/ccount": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",

View File

@ -270,6 +270,7 @@
"verify:wasm:package": "npm run build:cached && node scripts/verify-packaged-wasm-artifact.mjs",
"verify:wasm:artifact": "npm run build:cached && node scripts/verify-wasm-artifact.mjs",
"verify:evm:interface": "npm run build:cached && node scripts/verify-evm-interface.mjs",
"verify:solidity:metadata": "npm run build:cached && node scripts/verify-solidity-metadata.mjs",
"verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs",
"mcp-catalog:check:built": "node scripts/generate-mcp-tool-catalog.mjs --check",
"compile": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && node scripts/bundle-runtime.mjs",
@ -309,6 +310,7 @@
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"canonicalize": "3.0.0",
"cborg": "6.1.3",
"cross-spawn": "7.0.6",
"diff": "9.0.0",
"evmole": "0.9.3",

View File

@ -218,6 +218,7 @@ export const SCOPES = {
"src/artifacts/readStableArtifact*.ts",
"src/artifacts/ArtifactReader.ts",
"scripts/verify-evm-interface.mjs",
"scripts/verify-solidity-metadata.mjs",
"scripts/fixtures/evm-interface.sol",
"third_party/evmole/**",
],

View File

@ -225,6 +225,26 @@ try {
for (const mode of ["cli", "mcp"]) {
const report = await inspect(mode, path, encoding);
assert.equal(report.bytecode.hex, hex);
assert.equal(report.bytecode.metadata.status, "decoded");
assert.deepEqual(report.bytecode.metadata.compiler_version, {
representation: "release-bytes",
value: "0.8.30",
});
assert.equal(
report.bytecode.metadata.bytecode_offset +
report.bytecode.metadata.cbor_bytes +
2,
Buffer.from(hex, "hex").length,
);
assert.ok(
report.bytecode.metadata.entries.some(
({ key, value }) =>
key.kind === "text" &&
key.value === "solc" &&
value.kind === "bytes" &&
value.value === "00081e",
),
);
assert.deepEqual(
report.functions
.filter(({ dispatch }) => dispatch === "abi")

View File

@ -0,0 +1,105 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { isAbsolute } from "node:path";
import { pathToFileURL } from "node:url";
import { performance } from "node:perf_hooks";
import { inspectSolidityMetadata } from "../dist/evm/SolidityMetadata.js";
const compilerModule = process.env.REA_VERIFY_SOLC_MODULE;
if (!isAbsolute(compilerModule ?? ""))
throw new Error(
"Select an absolute caller-installed solc 0.8.30 module with REA_VERIFY_SOLC_MODULE.",
);
const solc = (await import(pathToFileURL(compilerModule).href)).default;
assert.equal(solc.version(), "0.8.30+commit.73712a01.Emscripten.clang");
const source = await readFile(
new URL("./fixtures/evm-interface.sol", import.meta.url),
"utf8",
);
const cases = [
{ name: "unoptimized", optimizer: false, viaIR: false, metadata: {} },
{ name: "viaIR", optimizer: true, viaIR: true, metadata: {} },
{
name: "without-content-hash",
optimizer: false,
viaIR: false,
metadata: { bytecodeHash: "none" },
},
{
name: "without-CBOR",
optimizer: false,
viaIR: false,
metadata: { appendCBOR: false },
},
];
const reports = [];
for (const fixture of cases) {
const compiled = JSON.parse(
solc.compile(
JSON.stringify({
language: "Solidity",
sources: { "fixture.sol": { content: source } },
settings: {
optimizer: { enabled: fixture.optimizer },
viaIR: fixture.viaIR,
metadata: fixture.metadata,
outputSelection: { "*": { "*": ["evm.deployedBytecode.object"] } },
},
}),
),
);
assert.ok(
!(compiled.errors ?? []).some((error) => error.severity === "error"),
JSON.stringify(compiled.errors),
);
const bytes = Buffer.from(
compiled.contracts["fixture.sol"].InterfaceFixture.evm.deployedBytecode
.object,
"hex",
);
const cpu = process.cpuUsage();
const started = performance.now();
const parsed = inspectSolidityMetadata(bytes);
const firstMs = performance.now() - started;
const firstCpu = process.cpuUsage(cpu);
if (fixture.metadata.appendCBOR === false)
assert.equal(parsed.status, "unavailable");
else {
assert.equal(parsed.status, "decoded");
assert.deepEqual(parsed.compiler_version, {
representation: "release-bytes",
value: "0.8.30",
});
assert.equal(parsed.bytecode_offset + parsed.cbor_bytes + 2, bytes.length);
if (fixture.metadata.bytecodeHash === "none")
assert.deepEqual(
parsed.entries.map(({ key }) => key),
[{ kind: "text", value: "solc" }],
);
}
const warmResults = new Array(100);
const warmStart = performance.now();
for (let index = 0; index < warmResults.length; index++)
warmResults[index] = inspectSolidityMetadata(bytes);
const warmMs = performance.now() - warmStart;
for (const result of warmResults) assert.deepEqual(result, parsed);
reports.push({
fixture: fixture.name,
bytes: bytes.length,
status: parsed.status,
first_decode_ms: firstMs,
first_decode_cpu_us: firstCpu,
warm_100_decode_ms: warmMs,
});
}
console.log(
JSON.stringify({
ok: true,
compiler_version: solc.version(),
contract_execution: "not-performed",
cases: reports,
process_peak_rss_kib: process.resourceUsage().maxRSS,
measurements:
"In-process decode timings; peak RSS includes the compiler. End-to-end Linux provider/CLI costs and agent tokens were not measured.",
}),
);

View File

@ -2,6 +2,14 @@ import { expect, it } from "vitest";
import { evmInterfaceSchema } from "./evmInterface.js";
import { evmInterfaceFixture } from "../../../tests/fixtures/evm/interface.js";
it("accepts historical metadata omission as an unknown facet without changing the source", () => {
const value = evmInterfaceFixture();
const { metadata: _metadata, ...oldBytecode } = value.bytecode;
const old = { ...value, bytecode: oldBytecode };
expect(evmInterfaceSchema.parse(old).bytecode.metadata).toBeNull();
expect(old.bytecode).not.toHaveProperty("metadata");
});
it.each(["byte-count", "body-offset", "selector", "authenticity"])(
"rejects false interface evidence: %s",
(problem) => {

View File

@ -1,6 +1,7 @@
import { z } from "zod";
import { localPathStringSchema } from "../localPath.js";
import { solidityMetadataSchema } from "./solidityMetadata.js";
const byteOffset = z.number().int().nonnegative();
const sha256 = z.string().regex(/^[a-f0-9]{64}$/);
@ -34,6 +35,7 @@ export const evmInterfaceSchema = z
kind: z.literal("unknown"),
deployment_authenticity: z.literal("unknown"),
hardfork: z.literal("unknown"),
metadata: solidityMetadataSchema.nullable().default(null),
}),
evidence_kind: z.literal("inferred"),
functions: z.array(
@ -62,6 +64,23 @@ export const evmInterfaceSchema = z
message:
"Selected byte count does not match the bytecode hex representation.",
});
const metadata = value.bytecode.metadata;
if (
metadata !== null &&
metadata.status !== "unavailable" &&
(metadata.bytecode_offset === null ||
metadata.cbor_bytes === null ||
metadata.bytecode_offset + metadata.cbor_bytes + 2 !==
value.bytecode.bytes ||
metadata.cbor_bytes !==
Number.parseInt(value.bytecode.hex.slice(-4), 16))
)
context.addIssue({
code: "custom",
path: ["bytecode", "metadata"],
message:
"Metadata range must match the terminal length suffix in selected bytecode.",
});
for (const [index, fn] of value.functions.entries())
if (fn.bytecode_offset >= value.bytecode.bytes)
context.addIssue({

View File

@ -0,0 +1,89 @@
import { z } from "zod";
/** Scalar view of CBOR; uninterpreted containers remain in original bytecode. */
export const solidityMetadataScalarSchema = z.discriminatedUnion("kind", [
z.strictObject({ kind: z.literal("text"), value: z.string().max(65535) }),
z.strictObject({
kind: z.literal("integer"),
value: z
.string()
.max(21)
.regex(/^-?(?:0|[1-9][0-9]*)$/),
}),
z.strictObject({
kind: z.literal("bytes"),
value: z
.string()
.max(131070)
.regex(/^(?:[a-f0-9]{2})*$/),
}),
z.strictObject({ kind: z.literal("boolean"), value: z.boolean() }),
z.strictObject({ kind: z.literal("null") }),
z.strictObject({
kind: z.literal("uninterpreted"),
container: z.enum(["array", "map"]),
}),
]);
/** A trailer candidate, not authentication of Solidity origin or deployment. */
export const solidityMetadataSchema = z
.strictObject({
profile: z.literal("terminal-cbor-scalars-v1"),
status: z.enum(["unavailable", "decoded", "partial", "not-decoded"]),
bytecode_offset: z.number().int().nonnegative().nullable(),
cbor_bytes: z.number().int().positive().max(65535).nullable(),
entries: z
.array(
z.strictObject({
key: solidityMetadataScalarSchema,
value: solidityMetadataScalarSchema,
}),
)
.max(32767),
compiler_version: z
.strictObject({
representation: z.enum(["release-bytes", "prerelease-text"]),
value: z.string().min(1).max(65535),
})
.nullable(),
diagnostic: z.string().max(512).nullable(),
})
.superRefine((value, context) => {
const unavailable = value.status === "unavailable";
if (
unavailable
? value.bytecode_offset !== null || value.cbor_bytes !== null
: value.bytecode_offset === null || value.cbor_bytes === null
)
context.addIssue({
code: "custom",
message: "Only unavailable metadata omits the candidate range.",
});
const unexamined = unavailable || value.status === "not-decoded";
if (
unexamined &&
(value.entries.length !== 0 || value.compiler_version !== null)
)
context.addIssue({
code: "custom",
message:
"Unavailable interpretation cannot report decoded entries or compiler identity.",
});
const uninterpreted = value.entries.some(
({ key, value: entryValue }) =>
key.kind === "uninterpreted" || entryValue.kind === "uninterpreted",
);
if (
(value.status === "decoded" && uninterpreted) ||
(value.status === "partial" && !uninterpreted)
)
context.addIssue({
code: "custom",
message: "Scalar coverage must preserve uninterpreted containers.",
});
});
export type SolidityMetadata = z.infer<typeof solidityMetadataSchema>;
export type SolidityMetadataScalar = z.infer<
typeof solidityMetadataScalarSchema
>;

View File

@ -30,6 +30,7 @@ import {
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { InspectEvmInterfaceInput } from "../domain/evm/evmInterface.js";
import { evmInterfaceSchema } from "../domain/evm/evmInterface.js";
import { solidityMetadataSchema } from "../domain/evm/solidityMetadata.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { err, ok, type Result } from "../domain/result.js";
@ -67,6 +68,7 @@ const replySchema = z.discriminatedUnion("ok", [
sha256: z.string().regex(/^[a-f0-9]{64}$/),
bytes: z.number().int().nonnegative(),
hex: z.string().regex(/^(?:[a-f0-9]{2})*$/),
metadata: solidityMetadataSchema.optional(),
}),
raw: evmoleInterfaceReplySchema,
}),
@ -331,6 +333,7 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort {
"Recovered selectors, argument strings and mutability are upstream inferences, not verified ABI signatures or names. Empty results do not prove the absence of callable dispatch.",
"Bytecode kind, hardfork and deployed-contract authenticity are unknown. This inspection performs no target runtime execution, chain/RPC lookup or network request.",
"Original carrier and decoded bytecode SHA-256 are distinct identities; SHA-256 is not the Ethereum Keccak code hash.",
"Terminal CBOR metadata is an independently decoded candidate, not proof of Solidity origin, source identity or deployment. Nested or unsupported values retain explicit unavailable interpretation and original byte ranges.",
"The owned worker has a 256 MiB JavaScript heap, 3 GiB virtual address-space limit and 30-second deadline. Virtual address space is distinct from resident memory; limits fail without partial success.",
];
const validated = evmInterfaceSchema.safeParse({

View File

@ -6,6 +6,7 @@ import {
EvmCarrierFailure,
} from "./EvmBytecodeCarrier.js";
import { projectEvmoleInterface } from "./EvmoleInterfaceAdapter.js";
import { inspectSolidityMetadata } from "./SolidityMetadata.js";
import {
EVM_INTERFACE_LIMITS,
EVM_FILE_SIZE_FAILURE_EXIT,
@ -64,6 +65,7 @@ const main = async (): Promise<void> => {
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
hex: Buffer.from(bytes).toString("hex"),
metadata: inspectSolidityMetadata(bytes),
},
raw: inferred.raw,
};

View File

@ -0,0 +1,96 @@
import { expect, it } from "vitest";
import { inspectSolidityMetadata } from "./SolidityMetadata.js";
const bytecode = (hex: string) => {
const payload = Buffer.from(hex, "hex");
const length = Buffer.alloc(2);
length.writeUInt16BE(payload.length);
return Buffer.concat([Buffer.from("00", "hex"), payload, length]);
};
it("reads a compiler version from actual trailer bytes without a magic-prefix assumption", () => {
const result = inspectSolidityMetadata(bytecode("a164736f6c634300081e"));
expect(result).toMatchObject({
status: "decoded",
bytecode_offset: 1,
cbor_bytes: 10,
compiler_version: { representation: "release-bytes", value: "0.8.30" },
entries: [
{
key: { kind: "text", value: "solc" },
value: { kind: "bytes", value: "00081e" },
},
],
});
});
it("preserves integer and non-text key precision beyond Number", () => {
expect(
inspectSolidityMetadata(bytecode("a1011bffffffffffffffff")).entries,
).toEqual([
{
key: { kind: "integer", value: "1" },
value: { kind: "integer", value: "18446744073709551615" },
},
]);
expect(
inspectSolidityMetadata(bytecode("a1003bffffffffffffffff")).entries[0]
?.value,
).toEqual({
kind: "integer",
value: "-18446744073709551616",
});
});
it("retains opaque nested values as partial rather than fabricating scalar values", () => {
expect(inspectSolidityMetadata(bytecode("a161788100"))).toMatchObject({
status: "partial",
entries: [
{
key: { kind: "text", value: "x" },
value: { kind: "uninterpreted", container: "array" },
},
],
});
});
it("preserves a UTF-8 BOM inside a CBOR text value rather than stripping it", () => {
expect(
inspectSolidityMetadata(bytecode("a1617864efbbbf61")).entries[0]?.value,
).toEqual({
kind: "text",
value: "\ufeffa",
});
expect(
inspectSolidityMetadata(bytecode("a167efbbbf736f6c634300081e"))
.compiler_version,
).toBeNull();
});
it.each([
["invalid text", "a1617861ff"],
["truncated value", "a16178"],
["unsupported tag", "a16178c000"],
["floating point", "a16178fa3f800000"],
["duplicate key", "a2617801617802"],
["excessive nesting", "a16178" + "81".repeat(65) + "00"],
["impossible collection", "a161788a00"],
])("keeps exact ranges and unavailable interpretation for %s", (_name, hex) => {
expect(inspectSolidityMetadata(bytecode(hex))).toMatchObject({
status: "not-decoded",
bytecode_offset: 1,
entries: [],
compiler_version: null,
});
});
it.each(["", "60006000f3", "000000", "00010001"])(
"does not claim absent Solidity metadata for an unavailable trailer: %s",
(hex) =>
expect(inspectSolidityMetadata(Buffer.from(hex, "hex"))).toMatchObject({
status: "unavailable",
bytecode_offset: null,
cbor_bytes: null,
entries: [],
}),
);

158
src/evm/SolidityMetadata.ts Normal file
View File

@ -0,0 +1,158 @@
import { decode, Tokenizer, Type, type Token } from "cborg";
import type {
SolidityMetadata,
SolidityMetadataScalar,
} from "../domain/evm/solidityMetadata.js";
const PROFILE = "terminal-cbor-scalars-v1";
const MAX_DEPTH = 64;
const utf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
const decodeOptions = {
useMaps: true,
allowBigInt: true,
allowIndefinite: false,
allowUndefined: false,
allowInfinity: false,
allowNaN: false,
rejectDuplicateMapKeys: true,
retainStringBytes: true,
} as const;
/** Bound recursive decoder entry before collection interpretation/allocation. */
class BoundedMetadataTokenizer extends Tokenizer {
readonly remaining: number[] = [];
constructor(bytes: Uint8Array) {
super(bytes, decodeOptions);
}
override next(): Token {
while (this.remaining.at(-1) === 0) this.remaining.pop();
const token = super.next();
if (
Type.equals(token.type, Type.tag) ||
Type.equals(token.type, Type.float)
)
throw new Error(
"CBOR tags and floating-point values are outside this metadata profile.",
);
if (Type.equals(token.type, Type.string)) {
if (token.byteValue === undefined)
throw new Error(
"CBOR text bytes were unavailable for strict UTF-8 validation.",
);
token.value = utf8.decode(token.byteValue);
}
const parent = this.remaining.length - 1;
if (parent >= 0) this.remaining[parent]!--;
if (
Type.equals(token.type, Type.map) ||
Type.equals(token.type, Type.array)
) {
const count: unknown = token.value;
const children =
typeof count === "number"
? count * (Type.equals(token.type, Type.map) ? 2 : 1)
: NaN;
if (
!Number.isSafeInteger(children) ||
children < 0 ||
children > this.data.length
)
throw new Error(
"Declared CBOR collection exceeds the encoded item-byte bound.",
);
if (this.remaining.length >= MAX_DEPTH)
throw new Error("CBOR nesting exceeds the 64-level metadata profile.");
this.remaining.push(children);
}
return token;
}
}
const scalar = (value: unknown): SolidityMetadataScalar => {
if (typeof value === "string") return { kind: "text", value };
if (typeof value === "bigint")
return { kind: "integer", value: value.toString() };
if (typeof value === "number" && Number.isSafeInteger(value))
return { kind: "integer", value: String(value) };
if (typeof value === "boolean") return { kind: "boolean", value };
if (value === null) return { kind: "null" };
if (value instanceof Uint8Array)
return { kind: "bytes", value: Buffer.from(value).toString("hex") };
if (value instanceof Map) return { kind: "uninterpreted", container: "map" };
if (Array.isArray(value))
return { kind: "uninterpreted", container: "array" };
throw new Error(
"CBOR decoder returned a value outside the admitted profile.",
);
};
/** Parse only a plausible terminal root-map candidate; never establish absence. */
export const inspectSolidityMetadata = (
bytes: Uint8Array,
): SolidityMetadata => {
const empty: SolidityMetadata = {
profile: PROFILE,
status: "unavailable",
bytecode_offset: null,
cbor_bytes: null,
entries: [],
compiler_version: null,
diagnostic:
"No plausible terminal CBOR map was identified; Solidity origin and metadata absence remain unknown.",
};
if (bytes.length < 3) return empty;
const length = bytes[bytes.length - 2]! * 256 + bytes[bytes.length - 1]!;
const offset = bytes.length - 2 - length;
if (length === 0 || offset < 0 || bytes[offset]! >> 5 !== 5) return empty;
const selected = bytes.subarray(offset, bytes.length - 2);
const result: SolidityMetadata = {
...empty,
status: "not-decoded",
bytecode_offset: offset,
cbor_bytes: length,
};
try {
const raw: unknown = decode(selected, {
...decodeOptions,
tokenizer: new BoundedMetadataTokenizer(selected),
});
if (!(raw instanceof Map))
throw new Error("Terminal CBOR candidate did not decode to a root map.");
for (const [key, value] of raw as Map<unknown, unknown>)
result.entries.push({ key: scalar(key), value: scalar(value) });
result.status = result.entries.some(
({ key, value }) =>
key.kind === "uninterpreted" || value.kind === "uninterpreted",
)
? "partial"
: "decoded";
result.diagnostic =
result.status === "partial"
? "Nested containers are uninterpreted; their exact bytes remain at the recorded range in original bytecode."
: null;
const version: unknown = raw.get("solc");
if (version instanceof Uint8Array && version.length === 3)
result.compiler_version = {
representation: "release-bytes",
value: [...version].join("."),
};
else if (typeof version === "string" && version.length > 0)
result.compiler_version = {
representation: "prerelease-text",
value: version,
};
return result;
} catch (cause: unknown) {
return {
...result,
status: "not-decoded",
entries: [],
compiler_version: null,
diagnostic: (cause instanceof Error
? cause.message
: String(cause)
).slice(0, 512),
};
}
};

View File

@ -0,0 +1,75 @@
import { execFile } from "node:child_process";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { expect, it } from "vitest";
import { solidityMetadataSchema } from "../../../src/domain/evm/solidityMetadata.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
/** Real compiled worker and unchanged WASM engine; no Linux provider-admission claim. */
it.each([
["compiler", "00a164736f6c634300081e000a", "decoded"],
["integer", "00a1011bffffffffffffffff000b", "decoded"],
["BOM text", "00a1617864efbbbf610008", "decoded"],
["nested value", "00a1617881000005", "partial"],
["unavailable trailer", "00", "unavailable"],
["unsupported tag", "00a16178c0000005", "not-decoded"],
["floating point", "00a16178fa3f8000000008", "not-decoded"],
["invalid UTF-8", "00a1617861ff0005", "not-decoded"],
])(
"retains independent %s metadata through the actual worker",
async (kind, hex, status) => {
const directory = await createTestTempDirectory("rea-solidity-worker-");
const snapshot = join(directory, "selected.hex");
const reply = join(directory, "reply.json");
const request = join(directory, "request.json");
await writeFile(snapshot, hex);
await writeFile(
request,
JSON.stringify({
snapshot_path: snapshot,
reply_path: reply,
failure_marker_path: join(directory, "resource.failure"),
encoding: "hex",
}),
);
await promisify(execFile)(
process.execPath,
[
"--max-old-space-size=256",
"--disable-wasm-trap-handler",
new URL("../../../dist/evm/EvmoleInterfaceWorker.js", import.meta.url)
.pathname,
request,
],
{ timeout: 30_000, maxBuffer: 1024 * 1024 },
);
const value = JSON.parse(await readFile(reply, "utf8"));
expect(value.ok, JSON.stringify(value)).toBe(true);
expect(value.bytecode.hex).toBe(hex);
const metadata = solidityMetadataSchema.parse(value.bytecode.metadata);
expect(metadata.status).toBe(status);
if (kind === "integer")
expect(metadata.entries[0]?.value).toEqual({
kind: "integer",
value: "18446744073709551615",
});
if (kind === "compiler")
expect(metadata.compiler_version?.value).toBe("0.8.30");
if (kind === "BOM text")
expect(metadata.entries[0]?.value).toEqual({
kind: "text",
value: "\ufeffa",
});
if (kind === "nested value")
expect(metadata.entries[0]?.value).toEqual({
kind: "uninterpreted",
container: "array",
});
if (kind === "unavailable trailer") {
expect(metadata.bytecode_offset).toBeNull();
expect(metadata.entries).toEqual([]);
expect(metadata.compiler_version).toBeNull();
}
},
);

View File

@ -449,6 +449,10 @@ it.each([
sqlite: false,
},
],
[
"scripts/verify-solidity-metadata.mjs",
{ code: true, evm: true, browser: false, package: false },
],
[
"src/browser/execution/evaluate.ts",
{

View File

@ -1,4 +1,5 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { createHash } from "node:crypto";
import { Ajv2020 } from "ajv/dist/2020.js";
import { expect, it, onTestFinished, vi } from "vitest";
import { EvmInterfaceService } from "../../../src/application/evm/EvmInterfaceService.js";
@ -10,6 +11,7 @@ import {
} from "../../../src/domain/analysisErrorCore.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { evmInterfaceSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
import { inspectSolidityMetadata } from "../../../src/evm/SolidityMetadata.js";
import { err, ok } from "../../../src/domain/result.js";
import { silentLogger } from "../../../src/logger.js";
import { createServer } from "../../../src/server/createServer.js";
@ -26,8 +28,21 @@ import {
evmInterfaceExecution,
} from "../../fixtures/evm/interface.js";
it("advertises exact valid schemas and records inline EVM interface evidence without a binary target", async () => {
const decodedMetadataFixture = () => {
const bytes = Buffer.from("00a1011bffffffffffffffff000b", "hex");
const value = evmInterfaceFixture();
value.bytecode = {
...value.bytecode,
hex: bytes.toString("hex"),
bytes: bytes.length,
sha256: createHash("sha256").update(bytes).digest("hex"),
metadata: inspectSolidityMetadata(bytes),
};
return value;
};
it("advertises exact valid schemas and records inline EVM interface evidence without a binary target", async () => {
const value = decodedMetadataFixture();
const inspect = vi.fn(() =>
Promise.resolve(ok(evmInterfaceExecution(value))),
);
@ -280,7 +295,8 @@ it("settles the admitted EVM operation after SDK cancellation and accepts a subs
});
it("returns the exact inferred interface summary while preserving complete defaults and retained source", async () => {
const value = evmInterfaceFixture();
const value = decodedMetadataFixture();
expect(value.bytecode.metadata?.status).toBe("decoded");
value.functions = [
{
selector: "0x11223344",
@ -419,6 +435,7 @@ it.each(["valid", "wrong-subject", "no-subject"])(
"preserves historical portable EVM Evidence and rejects incompatible identity: %s",
async (scenario) => {
const value = evmInterfaceFixture();
const { metadata: _metadata, ...historicalBytecode } = value.bytecode;
const source = createEvidence(
scenario === "no-subject"
? undefined
@ -434,7 +451,7 @@ it.each(["valid", "wrong-subject", "no-subject"])(
{
operation: "inspect_evm_interface",
parameters: { path: value.artifact.path, encoding: "hex" },
result: value,
result: { ...value, bytecode: historicalBytecode },
confidence: "inferred",
rawResult: { original: true },
},
@ -463,6 +480,7 @@ it.each(["valid", "wrong-subject", "no-subject"])(
expect(view.parent_evidence_id).toBe(source.evidence_id);
expect(view.summary.functions).toEqual([]);
expect(view.summary.discovery_completeness).toBe("unknown");
expect(view.summary.bytecode.metadata).toBeNull();
expect(view.coverage).toMatchObject({
total: 0,
examined: 0,

View File

@ -18,6 +18,7 @@ export const evmInterfaceFixture = (
kind: "unknown",
deployment_authenticity: "unknown",
hardfork: "unknown",
metadata: null,
},
evidence_kind: "inferred",
functions: [],