Skip to content

Let console-codegen take several spec files - #155

Draft
shellygr wants to merge 10 commits into
masterfrom
shelly/codegen-multi-spec
Draft

Let console-codegen take several spec files#155
shellygr wants to merge 10 commits into
masterfrom
shelly/codegen-multi-spec

Conversation

@shellygr

Copy link
Copy Markdown
Contributor

Stacked on #148 (shelly/natspec-conf-path) — retarget to master once that merges.

Why

InputData.specs is already a list and the workflow is plumbed for N specs, but the CLI mapped its triad to a one-element list, so nothing could hand it more than one. upload_input said as much: "The legacy CLI triad is single-spec; map it to a one-element specs list… The pipeline is plumbed for N specs."

There is a producer now. Natspec emits one spec per component — four for a modest contract — and each carries its own copy of the shared ERC20 ghost model. The two workarounds are both bad:

  • Merge by hand. On a real four-spec set the collisions were 3 ghosts (balanceByToken, allowanceByToken, sumWithdrawn), 8 definitions/functions (N, MIN_RECIPIENTS, balanceOfCVL, transferCVL, transferFromCVL, approveCVL, allowanceCVL, recordOutbound) and four separate methods blocks.
  • Run codegen per spec. Four unrelated implementations, each satisfying one component's rules and ignoring the others.

Change

spec_file becomes nargs="+":

console-codegen views.spec withdrawal.spec registry.spec IFoo.sol design.md

The three-argument form parses exactly as before, so existing invocations and scripts are unaffected.

vfs_path keys the specs downstream (audit's resume artifact indexes by it), so the naming is deliberate:

Invocation vfs_paths
one spec rules.spec — unchanged, so artifacts recorded by single-spec runs stay valid
several specs their file names (views.spec, withdrawal.spec, …)
several specs, colliding names refused, naming the offender

The collision case matters because distinct directories can hold same-named specs (core/vault.spec, periphery/vault.spec), and sharing a key would silently drop one. Refusing beats inventing a suffix the caller never asked for.

Tests

tests/test_codegen_multi_spec_input.py — 5 cases covering the parser (one spec, several specs) and the mapping (conventional name, file names, collision refused). pytest on this file plus #148's three → 11 passed.

shellygr and others added 6 commits August 11, 2026 02:00
`temp_certora_file` yields a path already relative to the project root, so it
carries the `certora/` segment (its docstring: "callers use it verbatim (no
`certora/` prefixing)"). `ConfigurationBuilder._build_to` prefixed it a second
time and yielded `<root>/certora/certora/run_<uid>.conf`, a path nothing ever
wrote to, so every natspec typecheck died with:

    read_from_conf_file: /tmp/tmpXXXXXXXX/certora/certora/run_YYYY.conf: not found

`publish` is gated on a passing typecheck, so greenfield natspec runs could
never emit a spec: the authoring agents produced complete, judge-approved CVL,
burned their remaining turns retrying, and gave up. The pipeline still wrote
the interface and the stub, which made the failure look like missing specs
rather than a broken backend.

The sibling call in the same `with` block, `with_verify(spec_file=...)`, already
uses the yielded path verbatim.

Adds a regression test asserting `build_to` yields a path that exists and sits
under exactly one `certora` segment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`with_verify` re-prefixed `certora/` onto a spec path that already carried it,
so the conf's `verify` attribute read

    <Contract>:certora/certora/generated_<uid>.spec

The Certora CLI validates that path against the process CWD (which typecheck.py
sets to the project root), so it rejected every spec with

    attribute/flag 'verify': file certora/certora/generated_<uid>.spec not found

This is the same mistake as the conf-location bug in the previous commit, one
layer up: fixing only that one moved the failure from "conf not found" to
"spec not found" without unblocking publish.

Widens the regression test to assert the invariant both bugs broke — every path
the conf hands to the CLI must resolve, from the project root, to a file on
disk. The test now fails on either bug alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Certora's scene assembly requires every entry in the conf's `files` to compile
to bytecode. An interface never does, so one interface entry fails the build
for every spec authored in that session:

    Contract IFoo has no bytecode. It may be caused because the contract is
    abstract, or is missing constructor code.

The pipeline registers only stubs, but the CVL-authoring agent registered the
interface too — reasonably, since its spec references it and
`register_verification_file` invited "any contract source the spec references".
There is no unregister tool, so a session poisoned itself irrecoverably: the
agent's own diagnosis after its sixth rejected publish was "blocking error is
scene assembly of the pre-registered interface-only file".

The registration also outlived the run that made it. FILES_NS is keyed by
document digest and not by cache namespace, so re-running the same document
under a fresh `--cache-ns` reused the poisoned entry. Hence two guards:
`register` refuses these paths, and `read_all` filters them so entries written
before this commit stay out of the conf.

The interfaces are in the scene regardless, via the stubs' imports. Verified by
running the real typecheck gate against a generated interface + stub with
`rule sanity { assert true; }`: with the interface excluded from `files`, the
gate passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`_compile_stub` gates every stub update and trusted solc's exit status. An
`abstract contract` satisfies that happily: solc returns 0 and emits no
bytecode. Certora's scene assembly then rejects the verification unit —

    Contract StreamShareSplitter has no bytecode. It may be caused because the
    contract is abstract, or is missing constructor code.

— failing every subsequent typecheck in the session. `publish` is gated on the
typecheck, so nothing the CVL author does afterwards can recover: the bad stub
is already in the VFS and the spec is not what is wrong.

This is not hypothetical. A registry agent asked for storage fields and got
back `abstract contract StreamShareSplitter is IStreamShareSplitter`, which the
validator accepted. All four components then produced judge-approved specs that
could never be published, while the artifact dumped at the end was the pristine
concrete stub from generation — so the failure was invisible in the output.

Ask solc for the bytecode via --combined-json and require it to be non-empty.
The rejection message names the two ways a contract ends up without bytecode so
the agent can act on it within its retry loop.

Note the neighbouring case is already safe: a contract that inherits a function
it does not implement is a hard solc error ("should be marked as abstract"), so
the exit-status path catches that one. Only explicit `abstract` slipped through.
Both are pinned by tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The workflow is already plumbed for N specs (`InputData.specs`), but the CLI
mapped its triad to a one-element list, so there was no way to hand it more than
one -- as `upload_input`'s comment noted, no producer needed it at the time.

There is one now. Natspec emits one spec per component, four for a modest
contract, and each carries its own copy of the shared ERC20 ghost model. Merging
them by hand means reconciling duplicate ghosts, definitions and `methods`
blocks; running codegen once per spec instead yields four unrelated
implementations, each satisfying one component and ignoring the rest.

`spec_file` becomes `nargs="+"`. The three-argument form parses exactly as
before, so existing invocations are unaffected.

`vfs_path` keys the specs downstream (audit's resume artifact indexes by it), so
several specs are named after their files while a single spec keeps the
conventional `rules.spec` -- recorded artifacts from single-spec runs stay
valid. Two specs whose file names collide are refused by name rather than
silently sharing a key, which would drop one of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Base automatically changed from shelly/natspec-conf-path to master September 4, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant