Build an on-chain dependency graph from any contract address.
EDG discovers contract dependencies from deployed bytecode and live calls. It needs no ABI, no signature database, and no protocol-specific integration.
Warning
EDG v0.1.0 is beta software.
The graph schema, the metric definitions, and the API can change before 1.0. Use a discovered graph as auditable evidence, not as a complete description of a protocol.
EDG reports incomplete discovery explicitly. It uses frontier nodes, problem records, and flow-completeness metrics. The known limits are in What EDG does not see.
Contributions, bug reports, and disagreement are welcome. The most useful report is a root address where EDG gives a confident and wrong answer. To send one:
- read CONTRIBUTING.md,
- open an issue,
- or start a discussion.
Quickstart · Viewer · Output · Discovery model · Completeness · Limits · Prior art
The mark is a dependency graph. A root branches to resolved nodes. One dashed node marks the point where the crawl stopped.
Most dependency crawlers read the ABI, select the getters that they recognize, and call those getters. This method is a whitelist in the shape of a graph. It covers the contracts that a person listed in advance, and it gives a small, confident, wrong answer for all other contracts. Morpho V1 and Morpho V2 are already two different shapes. Euler is a third shape.
EDG starts from one observation, and that observation removes the whitelist:
You do not need the name of a function to learn that it returns an address.
The generic discovery path has four steps:
- Recover the function selectors from the deployed bytecode.
- Call each recovered selector with no arguments.
- Inspect the successful return data for addresses.
- Follow the discovered addresses recursively.
The address-returning getters of a contract are the dependency list that the contract declares about itself. EDG reads that list without prior knowledge of the interface. A Morpho market struct and an Euler vault module expose their dependencies through different functions, but they follow the same code path. A protocol that is deployed after this code was written also works, with no change to the code.
Protocol-specific extractors add semantics such as allocates_to, priced_by, or owned_by. They do not control which contracts are eligible for generic discovery. This separation is central to the architecture:
Generic discovery controls graph reachability. Protocol knowledge adds interpretation.
EDG requires Python 3.13+ and uv.
git clone https://github.com/linstan1/edg
cd edg
uv sync
export EDG_RPC_URL_ETHEREUM=https://your-rpc-endpoint
# Gauntlet USDC Prime — etherscan.io/address/0xdd0f28e19C1780eb6396170735D45153D261490d
uv run python -m edg.discovery.run \
0xdd0f28e19C1780eb6396170735D45153D261490dThe command crawls the supplied address and prints:
- the discovered entities and relationships,
- the pinned block and the run identifier,
- the evidence coverage,
- the flow completeness,
- and all discovery problems from the crawl.
Use --save to keep the complete graph and its evidence:
uv run python -m edg.discovery.run \
0xdd0f28e19C1780eb6396170735D45153D261490d \
--save graph.jsonA standard crawl needs no database, no Docker, and no external API key.
EDG is not on PyPI. Install the tagged release from GitHub:
uv pip install "git+https://github.com/linstan1/edg@v0.1.0"or:
pip install \
"economic-dependency-graph @ git+https://github.com/linstan1/edg@v0.1.0"Ethereum mainnet is chain 1. All other chains require an explicit RPC endpoint:
export EDG_RPC_URLS='{
"8453":"https://mainnet.base.org",
"42161":"https://arb1.arbitrum.io/rpc"
}'
uv run python -m edg.discovery.run \
0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb \
--chain-id 8453If a chain has no configured endpoint, EDG refuses the crawl. The CLI and the API both refuse it. This rule prevents a chain identifier on state that EDG read from a different network. An earlier version accepted the crawl. --chain-id 8453 then read the Ethereum node and stamped chain_id: 8453 on every entity, edge, and evidence row. The result was a complete, well-evidenced graph of the wrong chain. GET /chains reports the chains that a deployment can read.
Gauntlet USDC Prime
Crawling 0xdd0f28e19C1780eb6396170735D45153D261490d on chain 1
Block 25659419 | run 226b7906-…-23f63e50b364 | 4720 evidence reads
Entities: 430 Relationships: 808
Generic sweep: 160 contracts probed ontology-free after recognition
Evidence coverage: entities 430/430, edges 808/808
Flow completeness: 94.6% over 168 contracts (6 never probed, 3 naming an
address the crawl never reached)
entity kinds: {'vault': 1, 'token': 6, 'multisig': 16, 'eoa': 254,
'protocol': 1, 'market': 8, 'oracle_feed': 7,
'contract': 126, 'price_feed': 11}
edges:
Gauntlet USDC Prime --deposits_into--> Morpho Blue
Gauntlet USDC Prime --allocates_to--> cbBTC/USDC (56.3% NAV)
cbBTC/USDC --priced_by--> 0xA6D6950c9F177F1De7f7757FB33539e3Ec60182a
0xA6D6950c… --derives_price_from--> BTC / USD
BTC / USD --owned_by--> Safe 4/9
Safe 4/9 --can_set--> BTC / USD
Safe 4/9 --signed_by--> 0x80efdee7f82d0f2abf3bf29a9a85edf0b42c9e19
…
[!] PROBLEMS SURFACED (32):
- 0x21f73D42…: selector extraction unreliable — dependencies unprobed
- unreadable (transport): multicall 26 selectors on 0xbec897d7…:
Web3RPCError: {'code': -32000, 'message': 'out of gas'}
- market 0x54efdee0 has no collateralAsset (idle market?)
The graph holds 430 entities and 808 relationships. Every entity and every relationship has supporting evidence.
The flow-completeness score is below 100% because EDG never probed six contracts, and three discovered addresses were out of reach of the crawler.
The count of evidence reads can differ between runs, even when the graph is identical. EDG retries a transport failure, and each attempt writes its own evidence row. The structural graph output stays reproducible when the address and the block are fixed.
| Flag | Default | Description |
|---|---|---|
--chain-id |
1 |
Chain to crawl |
--block |
latest | Pin all state reads to a specific block |
--rpc |
$EDG_RPC_URL_ETHEREUM |
Override the configured RPC endpoint |
--save PATH |
— | Save the complete graph payload as JSON |
--max-nodes |
5000 |
Maximum number of graph nodes |
--max-depth |
2 |
Maximum vault-to-vault recursion depth |
--no-sweep |
off | Disable generic expansion after protocol recognition |
--cache |
off | Serve eligible reads from PostgreSQL |
--persist |
off | Persist the resulting graph to PostgreSQL |
A dependency graph has no natural terminal depth. A vault reaches its markets. The markets reach their oracles. The oracles reach administrative contracts, and those contracts reach signer addresses and more contracts. Without a stop condition, "crawl this vault" becomes "crawl Ethereum".
--max-nodes and --max-depth therefore bound the traversal explicitly.
When the traversal stops at one of these limits, EDG marks the node as a frontier. The node stays in the graph, and meta.problems reports the incomplete expansion. A node that EDG explored completely, and that has no outgoing dependencies, is a terminal. The completeness metric uses this distinction. The crawl above converges at 430 nodes inside the default budget.
EDG includes an interactive React Flow viewer. Type any address, select Crawl, and read the graph.
Run the API:
export EDG_RPC_URL_ETHEREUM=https://your-rpc-endpoint
uv run uvicorn edg.main:app --port 8000Then run the viewer:
cd viewer
npm install
npm run devThe development server is available at:
http://localhost:5173
Vite proxies /api/* requests to the local EDG API. Local development needs no CORS configuration and no base URL. The complete reference is in docs/VIEWER.md.
The viewer shows the discovery state directly.
- A dashed border marks a frontier node with dependencies that EDG did not explore completely.
- A
bytecode-onlybadge shows that the classification came from the deployed code, and not from a protocol-specific extractor. - A red
never probedlabel shows that EDG reached the contract, but has no positive evidence that generic probing ran. - Control edges are red, and allocation edge width scales with NAV share where that value is available. You can therefore see who can change what before you read a label.
meta.problemshas its own panel, because it is part of the result and not a log.- The run panel takes the flow-completeness score from
edg.flowness, and does not compute its own version.
Most cards show an address in the place of a name. This is not a rendering fault. Most contracts return no name on chain, and an invented name is the same mistake as an invented edge.
The payload drives the interface. The viewer holds no protocol model, so a new adapter in the crawler needs no change here.
A crawl produces three primary objects:
Entities represent addresses or derived protocol objects.
Relationships connect those entities.
Evidence records the state reads that produced them.
The Gauntlet USDC Prime example produces 430 entities and 808 relationships across nine hops. The image below draws every node, and omits nothing.
Horizontal position is hop distance from the crawl root. Each hop wraps into a block, and the count is printed above it. Dashed rings are the 25 frontiers. Hover any dot in the raw SVG to read its label and address.
The shape is the finding. Hops 5 and 6 grow to 116 and 158 nodes, because several oracle dependencies resolve into multisig contracts, which then resolve into their signer sets. The dense cross-hatching between those hops is the same few multisigs behind oracle after oracle. A single column at hop 9 is the last thing in reach before the graph closes.
A smaller labelled rendering shows the first six hops:
Read it from left to right. The vault allocates to Morpho markets. An oracle prices each market. Each oracle derives its price from Chainlink feeds. A Safe owns those feeds, and the signers of the Safe are the last hop. One discovered path is:
vault
→ Morpho market
→ oracle
→ Chainlink feed
→ Safe
→ signer
That last chain is the point. Nobody told the crawler that a Chainlink aggregator has an owner, or that a Safe decomposes into signers. EDG found an address-returning getter on a feed and followed it. It then found a contract whose bytecode dispatches the Safe interface, and it continued. Generic traversal produces the economic fact: a 4-of-9 multisig can re-point the price feed that the largest position of this vault depends on. A model of Chainlink, written in advance, does not produce that fact.
EDG adds the economic interpretation where it has enough protocol knowledge to name the relationship. The traversal itself does not depend on those semantic labels.
A saved graph payload renders as SVG:
uv run python scripts/render_graph.py \
graph.json full.svg \
--style full
uv run python scripts/render_graph.py \
graph.json detail.svg \
--style detail \
--max-nodes 60full draws every node in the payload, and says so in the footer. detail produces labelled cards and therefore crops the graph. Its subtitle states which fraction is on screen. A crop with a caption that gives the full node count is the picture equivalent of a graph that hides its gaps.
Both styles use the Python standard library only, produce a self-contained SVG, and respect prefers-color-scheme.
One command re-checks every count on this page: the images, the table below, and the completeness score.
uv run python scripts/regen_assets.py # re-crawl and verify
uv run python scripts/regen_assets.py --write # and regenerate the imagesThe script re-crawls every address that this page quotes, at the pinned block. It exits non-zero if a count drifted. It then rewrites the SVGs from the same payloads that it checked, so the images cannot describe a different crawl from the text. A scheduled workflow runs it every week.
The vault above is a Morpho vault because the project started there. The crawler does not require one. The table below shows a --max-nodes 200 crawl of each root, with the same binary, the same code path, and no adapter.
| Root address | Protocol | Nodes | Edges |
|---|---|---|---|
0x8787…fA4E2 |
Aave V3 Pool | 200 (budget) | 235 |
0x8586…F075A |
EigenLayer StrategyManager | 65 | 153 |
0xbEbc…F1C7 |
Curve 3pool (Vyper) | 18 | 19 |
0x88e6…5640 |
Uniswap V3 USDC/ETH | 12 | 17 |
0xae7a…7fE84 |
Lido stETH | 4 | 3 |
0x0000…2e1e |
ENS registry | 2 | 1 |
0xC02a…6Cc2 |
WETH | 1 | 0 |
One node and no edges is the correct answer for WETH, and not a failure. EDG probed all eleven selectors of WETH, and none of them returns an address. A tool that invents dependencies there is worse.
Two of these rows work only because the table found bugs. The Aave Pool is a transparent proxy that dispatches five selectors of its own. Extraction therefore looked reliable, and EDG skipped proxy resolution. The crawl reported one node and no frontier, and an entire lending protocol behind the proxy stayed unseen. Curve uses Vyper dispatch, which reads the selector back through MLOAD before the compare. The recognizer did not accept that form, so Curve fell to the unprobed fallback set. Before these two fixes, this table read 1 and 1 in the place of 200 and 18.
The generic crawler derives dependencies from runtime behaviour, and not from an ABI.
flowchart LR
A[address] --> B{extractor<br/>recognizes it?}
B -->|yes| C[adapter path:<br/>names allocations,<br/>markets, roles]
B -->|no| D[classify from<br/>own bytecode]
C --> E[generic expansion]
D --> E
E --> F[recover selectors<br/>from deployed code]
F --> G[call each with<br/>no arguments]
G --> H[decode addresses<br/>from return shape]
H --> I[confirm against<br/>bytecode at target]
I --> J[emit node + edge]
J --> K[generic sweep:<br/>every contract,<br/>recognized ones included]
K --> E
J --> L[flowness:<br/>completeness scoring]
discovery/bytecode.py extracts the candidate selectors from the deployed EVM bytecode. It keeps each PUSH4 value that a comparison uses, because that comparison is the leaf of a selector dispatch. It supports the common Solidity and Vyper dispatch patterns:
EQ,XORorSUB, thenISZERO,- the stack manipulation that via-IR Solidity compilation emits.
EDG removes the trailing CBOR metadata first. A four-byte value inside an IPFS hash therefore cannot fabricate a selector.
A binary-search dispatcher needs no separate handling. Its function leaves still use an equality comparison. EDG ignores the branch pivots, which use GT or LT, because a pivot operand is a boundary and not a function that the contract implements.
discovery/probe.py calls each recovered selector with no arguments.
When Multicall3 is available, discovery/batch.py collapses up to 100 sub-calls into one eth_call. One contract then costs two round trips, whatever the number of its selectors. EDG reads the bytecode at the expected address to confirm that Multicall3 is deployed, and never assumes it.
If Multicall3 is absent, the crawler falls back to JSON-RPC batching. Batching changes the RPC cost. It does not change the discovery semantics.
EDG does not need the function name to inspect the returned value. It recognizes several return shapes:
| Return shape | Interpretation |
|---|---|
| 32 bytes, top 12 zero, value ≥ 2¹²⁸ | scalar address candidate |
| offset word + length word + N words | address array — decode each element |
| N × 32 bytes | tuple of unknown layout — scan every slot |
EDG applies the array reading and the tuple reading together, and unions the results. A flat tuple whose first slot holds the value 32 is byte-identical to an array header. A commitment to one reading turns an ambiguity into a silent absence.
EDG then validates the candidate addresses before they enter the graph.
Each valid discovered address becomes a graph entity, and EDG crawls it in turn.
After protocol-specific extraction finishes, EDG runs generic expansion across every contract in the graph, and includes the contracts that an extractor already claimed. This step makes "no whitelist" true and not only intended. meta.generic_sweep reports the count, which is 160 on the crawl above.
Protocol knowledge flows in one direction. An extractor can enrich a node or name an edge. It can never gate whether the node or the edge exists. Recognition describes what it found. It does not end the search.
EDG cannot treat every raw 32-byte value as an address. decimals() returns 18. That value is a well-formed ABI word with twelve zero bytes on top, and it is not an address. Packed numeric fields inside a struct cause the same problem.
EDG applies a magnitude filter to a scalar candidate first.
For a candidate from an uncertain tuple layout, EDG also checks whether bytecode exists at the target address, and discards the candidate that fails this check. This second check rejects, and does not only annotate. A slot scan of an unknown struct layout can hold a packed integer that is large enough to clear the magnitude floor, such as sqrtPriceX96 in Uniswap V3.
A scalar return or an array return keeps medium confidence instead. An EOA in that position is what a legitimate owner() result or a signer list looks like.
EDG separates execution failure from transport failure. These are opposite facts, and a crawler that collapses them returns a shrunken graph with a complete score.
A confirmed EVM revert means that the call ran and failed.
A timeout, a malformed provider response, a rate limit, or another transport error does not establish that the contract lacks the selector. EDG retries a transport failure and reports it. It never converts it into negative discovery evidence, and never caches it.
The classification is asymmetric by design. Only a positively identified revert counts as a revert. All other results count as transport. An allowlist-based classifier once cached json.JSONDecodeError from an HTML 502 body as "this function does not exist", and cached it permanently, because a cache entry is block-pinned and is never invalidated. EDG also does not read out of gas as a revert. Either the contract ran out of gas, or the node hit its per-call cap, so the call must be retried.
This asymmetry has a cost, and the cost is worth a statement. EDG raises a transport failure rather than records it, so one read with that classification can end a crawl. Providers also spell the same EVM fault differently. Geth says invalid jump destination. Reth and Alchemy say EVM error: InvalidJump. A matcher that accepted only the spaced spelling sent a genuine execution failure down the transport path, and a crawl of the ENS registry died on it. It reported "cannot reach the RPC endpoint" while the endpoint answered correctly. EDG now normalizes a provider fault message before it matches it.
Multicall3 adds a smaller version of the same hazard. aggregate3 reports an unsuccessful sub-call as (false, returndata), and a bare revert() is byte-identical to a gas-starved sub-call. EDG surfaces an unconfirmed failure as a revert for that run, and never caches it. If every call in a group fails, EDG re-reads the whole group serially, where each call gets its own gas budget.
EDG scores completeness over every contract in the resulting graph.
A contract counts as explored only when positive evidence shows that its discovery path ran. The absence of an outgoing edge is therefore not evidence of completeness. Silence never counts as success.
This rule lets EDG separate four states:
- a fully explored contract with no discovered dependencies,
- a contract that EDG could not probe,
- a contract where the traversal stopped at a configured bound,
- and a discovered target that the crawler never reached.
The subtle failure mode is a metric that reads one evidence format from one code path. The ontology-free prober records raw hex returns. A scorer that recognizes only decoded 0x…40 strings sees none of them. The whole discovery path then becomes invisible to the score, and still looks complete. flowness.py reads both formats. It also judges a scalar-prone word against the bytecode at the target, so it neither misses a real gap nor invents a false one.
The inverse error matters as much. EDG must never mark a node as a frontier when it expanded that node completely. A false frontier corrupts the score in the opposite direction.
The Gauntlet USDC Prime example reports:
Flow completeness: 94.6% over 168 contracts
EDG never probed six contracts, and three discovered addresses were out of reach of the crawler. A score below 100 with an attached worklist is the intended output.
This metric applies the open-world assumption to a crawler. An edge that is absent from the graph is unknown, and not false. docs/METRICS.md gives the complete definition, compares it against established completeness measures for knowledge graphs, and states what it cannot see.
The generic discovery mechanism has four known structural limits. Each one surfaces as a frontier marker, and never as an absent edge.
Generic probing supplies no function arguments. A dependency that only a function such as
getMarket(bytes32)
markets(uint256)
getRoleMember(bytes32,uint256)exposes is therefore invisible to zero-argument probing. The bytecode alone cannot give the argument. A protocol extractor can recover some of these relationships when their structure is known.
A computed jump table, a perfect-hash dispatcher, an EIP-2535 diamond, and other unusual dispatch structures can prevent reliable selector recovery. Those contracts reach the over-collecting push4_fallback set.
EDG records that set and does not probe it, because the set includes selectors that the contract only calls on other contracts. To probe them invites false edges. This limit accounts for 21 of the 32 problems on the crawl above.
EDG resolves:
- EIP-1967 proxies,
- EIP-1167 minimal proxies.
For these contracts, EDG recovers the selectors from the implementation and calls them against the proxy, where they dispatch and where the storage lives. EDG does not resolve a beacon proxy or a diamond.
A generic edge means that contract A returned the address of contract B through a specific selector. Without protocol recognition, EDG makes no claim about the economic meaning of that relationship.
Such an edge stays a generic references relationship until an overlay can classify it more precisely. An honest references edge beats a guessed semantic one.
EDG pins every on-chain read to the crawl block. The same address at the same block therefore reproduces the same graph.
An evidence record holds the contract, the selector, the raw result, the extraction path, and the block number:
{
"source_type": "state_read",
"contract_address": "0xdd0f…490d",
"function_sig": "0x8da5cb5b",
"raw_payload": {
"result": "000…a4b1",
"reverted": false,
"via": "multicall3"
},
"extraction_method": "rpc_eth_call_raw_selector",
"block_number": 25659419
}Every entity and every relationship cites the evidence records that produced it. An edge cites the selector that returned its target, and not the first read in the group. A citation that does not check out is worse than no citation, because it looks checkable.
Graph equality is structural and not byte-for-byte, because EDG generates entity and evidence identifiers per run.
EDG marks a fact from an external index separately, because that fact reflects the query time and not the pinned chain state.
Recovery of selectors from bytecode is not new. If that step were the whole idea, this project would be a worse whatsabi.
| Tool | What it does | How EDG differs |
|---|---|---|
| whatsabi | Selector recovery, 4byte and Sourcify resolution, proxy detection | Does step 1, and more thoroughly. Calls nothing and builds no graph. |
| evmole | Static extraction of selectors, argument types, and return types | Infers the return shape without a call. Stops at the interface of one contract. |
| heimdall-rs | Decompiler with ABI inference | Same scope: one contract, and no dependency following. |
| Slither / Surya | Call and dependency graphs | Source-level, and therefore unusable on an unverified contract. |
| DeFiLlama adapters, Credmark | Protocol dependency mapping | Per-protocol whitelists, which is the approach that this project argues against. |
The difference here is the composition and the accounting. EDG recovers selectors, probes them live with no arguments, extracts addresses from the return shape, and recurses. It then applies a completeness metric that scores every contract and counts only positive evidence of probing.
src/edg/
├── discovery/
│ ├── bytecode.py selector recovery, metadata stripping, EIP-1167 detection
│ ├── probe.py zero-arg probing, return-shape address extraction
│ ├── batch.py Multicall3 and JSON-RPC batching
│ ├── chain.py block-pinned reader, transport-vs-revert, evidence
│ ├── crawl.py orchestration, the generic sweep, frontier marking
│ ├── cache.py block-pinned read cache (optional)
│ └── extractors.py protocol overlays — enrich only, never gate
├── flowness.py completeness scoring over the finished graph
├── domain.py Entity / Relationship / Evidence
└── api/ FastAPI wrapper (GET /crawl)
viewer/ React Flow front end — any address, live crawl, interactive
scripts/render_graph.py static SVG renderer for a saved payload (stdlib only)
scripts/regen_assets.py re-crawls every address in this README, and rebuilds its images
docs/ARCHITECTURE.md the design in depth, and the reason for each part
docs/METRICS.md flow completeness: what it measures, and what it cannot
docs/DATA-MODEL.md the definition of a node, an edge, and an evidence row
docs/VIEWER.md how to run the viewer, and what each badge means
tests/ unit tests, no network required
CHANGELOG.md what changed, and the bug behind each change
Run the test suite:
uv run pytest -qRun the linter:
uv run ruff check .Run the API:
uv run uvicorn edg.main:app # GET /crawl?address=0x…The test and lint tools are a dependency-group, so the uv sync above installs them. There is no second install step. The normal unit-test suite does not access the network.
PostgreSQL is optional, and only the persistence layer uses it. The database tests skip when PostgreSQL is absent. Start PostgreSQL with:
docker-compose up -dTwo tests need a live RPC endpoint. They stay off unless you set:
export EDG_LIVE_TESTS=1This keeps CI hermetic. A suite that depends on a public endpoint fails for reasons that have no relation to the change under test, and a flaky CI teaches people to ignore it.
EDG reads environment variables only. It supports no .env files, by policy. Settings sets env_file=None, so a stray file cannot be read.
| Variable | Default | Description |
|---|---|---|
EDG_RPC_URL_ETHEREUM |
a public endpoint | Chain 1 RPC endpoint. Set this. The default rate-limits. |
EDG_RPC_URLS |
{} |
Endpoints for other chains, as {"8453":"https://…"}. EDG refuses a chain that is absent here. |
EDG_CORS_ORIGINS |
localhost:5173 | Origins that can call the API. Needed for a deployed viewer. |
EDG_DB_* |
edg @ localhost:5432 |
Optional PostgreSQL configuration |
EDG_LIVE_TESTS |
unset | Set to 1 to enable the network tests |
For a deployed viewer, VITE_EDG_API points a built bundle at an API on another origin. When it is unset, the development viewer uses the Vite proxy.
EDG is beta software, and contributions are welcome. Send code, a bug report, or an argument that something here is wrong.
The most valuable report is a root address whose graph is confidently wrong:
- a dependency that EDG missed,
- an edge that EDG invented,
- or a node that EDG presented as a terminal when it was a frontier.
Give the address and the block. EDG pins every read, so a reproducible case can be diagnosed. A contract whose dispatch this crawler cannot read is equally useful, such as a diamond, an exotic dispatcher, or an unusual proxy.
Read CONTRIBUTING.md before you submit a change. There is one rule: nothing about discovery can be predefined. The honesty invariants are not negotiable. A change that makes the graph look more complete because it sees less is the one kind of patch that gets rejected on sight. Tests enforce both rules, in the place of review vigilance.
Release history: CHANGELOG.md. Security policy: SECURITY.md.
MIT. See LICENSE.
