Skip to content

feat: attest source compiler validation context#33

Draft
charles-cooper wants to merge 10 commits into
masterfrom
typed-source-validation-evidence
Draft

feat: attest source compiler validation context#33
charles-cooper wants to merge 10 commits into
masterfrom
typed-source-validation-evidence

Conversation

@charles-cooper

@charles-cooper charles-cooper commented Jul 17, 2026

Copy link
Copy Markdown
Member

co-authored by gpt 5.6 sol

Summary

Make source-validation attribution explicit and typed. Source compiles use the full selected uv project/workspace environment, report the compiler and dependency artifacts actually loaded, and classify failures without inferring dependencies or rejection from compiler text.

Why

The previous validation path translated selected dependency declarations into --with arguments and retried failures using import names or compiler stderr. A nonzero process result alone could then be labeled as compiler rejection, even for an incoherent project/compiler declaration, an adapter failure, or a crashed process.

The immediate consumer is alan-app's source-translation classifier: it needs producer-owned evidence that distinguishes a declared-context compiler rejection from environment, launch, timeout, adapter, fixed-target, and internal failures.

Design

  • Use the selected uv project with all groups and extras. Discover an owning uv workspace from its declared member/exclusion model, attest the root manifest, lock, and local/workspace sources, and keep the member as uv --project.
  • Enforce an existing owning-workspace lock with --frozen, or resolve from a temporary project/workspace mirror so the source tree is not mutated.
  • Give only an active PEP 508 Vyper requirement in uv's selected dependency model project authority. Evaluate environment markers, and do not treat Poetry-only tables as uv dependencies.
  • When the project does not provide Vyper, layer the inferred compiler onto a Python request compatible with the project's requires-python; otherwise let uv select the project interpreter.
  • Require a declared project compiler to satisfy the source pragma before compilation. The parent passes the deterministic accepted-version set to a stdlib-only project-environment runner, so adapter evidence does not depend on the project installing packaging.
  • Treat only a managed Vyper exception as compiler rejection. Signals and internal exceptions are compiler-internal; launch, timeout, environment, and adapter failures remain distinct.
  • Emit report schema v4 with producer identity and separate source and target attestations: declared source snapshot and compiler declarations, authority rule, resolved compiler executable/artifact identity, manifest/lock/resolved package and source context, process completion and exit status, validated sources, exact compile attempt, typed failure origin, and compiler output on failure.
  • Give target failures typed origins for fixed-target rewrites and dependencies.
  • Remove source-validation import matching, package-name mappings, syntax-floor inference, and output-format or transformed-source retries. Target compatibility retries remain limited to target compilation.

Safety invariants

  • failure_origin = "compiler" requires a genuine managed Vyper rejection, not merely a nonzero return code.
  • A project pin that conflicts with a source pragma is an environment-resolution failure before the compiler starts.
  • Compiler and dependency authority comes from active declarations and the selected uv project/workspace model, never package-name, path-proximity, Poetry-table, or diagnostic-text inference.
  • compiler_output is populated only from compiler process stdout/stderr; environment-manager and adapter prose is excluded.
  • Project resolution does not write a new lockfile into an unlocked source tree.
  • Target compilation uses the requested target compiler while retaining the declared dependency environment.
  • Every reported source attempt identifies the exact source bytes, exit status, and origin; source validation does not retry.

Failure prevented

  • A locked packages/app workspace member no longer loses the root uv.lock or root { workspace = true } source mapping.
  • A project requiring Python 3.13+ no longer fails before launch because an inferred compiler forced Python 3.11.
  • Inactive marker-gated and Poetry-only Vyper declarations no longer displace the inferred compiler.
  • Project environments without packaging no longer crash the adapter before it writes structured evidence.

Verification

  • uv run pytest tests/test_compiler.py tests/test_cli_integration.py — 256 passed.
  • The focused regressions exercise a real locked uv workspace member/source mapping, a Python 3.13+ project interpreter, inactive-marker and Poetry-only declarations, and a runner process started with python -S.
  • Touched-file Ruff lint and format checks passed.

Diff breakdown

Whole-PR comparison: master@942b1fb4b066dc8b.

Category Files Added Removed
Production code 13 1,751 651
Tests 10 1,342 933
Config / CI / workflows 0 0 0
Docs / prompts 2 38 19
Generated / lockfiles 0 0 0
Total 25 3,131 1,603

Concept delta

Whole-PR conceptual comparison: master@942b1fb4b066dc8b.

Concept class Added Removed Net
Files/modules +1 -0 +1
Production capability abstractions +9 -4 +5
Decision authorities +4 -4 0
Retry/inference branches +0 -5 -5

Added file/module:

  • compiler_runner.py — isolates compiler execution inside the selected environment and writes typed process evidence before the parent classifies the result.

Added production capability abstractions:

  • Validation attestation model — binds declaration, compiler, dependency, source-set, attempt, exit, origin, and compiler-only output into one report contract.
  • Dependency-context model — carries manifest, lock, Python constraint, declared source, and resolved package identities.
  • Resolved compiler/package content identities — prove which executable, distribution artifact, and dependency artifacts actually ran.
  • Declared project-environment resolver — runs the full selected uv dependency model rather than reconstructing import-matched --with arguments.
  • uv workspace ownership selector — binds a member to its declared owning root before lock/mirror choice.
  • Compiler adapter boundary — separates managed compiler exceptions from launch, timeout, signal, environment, adapter, and internal failures.
  • Import-closure resolver — gives dependency compilation a concrete source closure instead of stderr-driven package guesses.
  • Target overlay materializer — validates rewritten roots and dependencies in one deterministic staged tree.
  • Closure output/archive writer — emits only the validated closure while preserving source-tree immutability.

Removed production capability abstractions:

  • Import-matched dependency injector — removed because partial dependency reconstruction was not the declared environment.
  • Missing-module package retry — removed because compiler text cannot authorize a dependency mutation.
  • Final-newline source retry — removed because transformed-source retry weakens exact-source attribution.
  • Syntax-floor compiler selector — removed because syntax inspection is heuristic version authority.

Authority changes:

  • +1 project compiler authority: an active Vyper requirement in uv's selected model, concretized by the owning lock when present, is authoritative.
  • +1 deterministic pragma authority: exact/ranged pragma selection uses the documented version rule only when the project has no active Vyper requirement.
  • +1 typed execution-origin authority: the adapter's failure_origin replaces exception-text and generic-return-code attribution.
  • +1 content-identity authority: manifest, lock, compiler, package, and source hashes bind evidence to the execution.
  • -1 Poetry-table authority: Poetry-only declarations no longer override what uv actually resolves.
  • -1 import/package-name authority: imports, built-in mappings, and normalized package guesses no longer select dependencies.
  • -1 diagnostic-text authority: stderr classes and wording no longer decide blame or routing.
  • -1 syntax-shape authority: source syntax no longer biases compiler selection.

Removed retry/inference branches:

  • ModuleNotFound dependency retry — no guessed package is added after compiler output.
  • Final-newline retry — source bytes are compiled once as submitted.
  • Source output-format retry — source evidence is not reconstructed across compiler attempts.
  • Transformed-source retry — a failed original source cannot be replaced to manufacture source-valid evidence.
  • Generic nonzero rejection inference — nonzero results require a typed managed-compiler origin.

Weight and smaller alternative

The added surface is weight-bearing because the report consumer must distinguish user-source rejection from toolchain failure using evidence produced at execution time. The smaller alternatives—nearest-lock search, dependency-name matching, importing packaging from the project, or parsing uv/compiler text—would reproduce individual examples but would restore heuristic attribution.

Scope and rollout

Schema v4 supersedes v3 as a clean report-contract cutover. There are no compatibility aliases, heuristic fallbacks, dual-schema mode, app-side changes, or merge in this PR. The PR remains draft with the requested reviewer fixes pushed.

@banteg banteg left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes based on four reproducible project-environment failures at this head. The standard repository tests do not cover these cross-project boundaries: uv workspace members lose the root lock/source mappings; source fallback can force an interpreter rejected by the project; inactive or unsupported dependency declarations are treated as active Vyper authority; and the compiler adapter imports packaging from environments that are not guaranteed to provide it.

Comment thread src/vyupgrade/compiler.py
Comment thread src/vyupgrade/compiler.py Outdated
Comment thread src/vyupgrade/compiler.py Outdated
Comment thread src/vyupgrade/compiler_runner.py Outdated
@charles-cooper

Copy link
Copy Markdown
Member Author

co-authored by gpt 5.6 sol

@banteg, all four issues you reported are addressed in b066dc8:

  • A workspace member lost the root uv.lock and workspace source mappings → discover the owning uv workspace, attest its lock and local sources, mirror the whole workspace, and still run the selected member → src/vyupgrade/compiler.py.
  • Source fallback could force the managed Python 3.11 pin even when the project rejected it → use the project's compatible interpreter when the managed pin conflicts with requires-python, while injecting only the inferred compiler → src/vyupgrade/compiler.py.
  • Inactive marker-gated or Poetry-only Vyper declarations could incorrectly become compiler authority → parse and evaluate active PEP 508 requirements from the uv-resolved project model and exclude Poetry-only declarations → src/vyupgrade/compiler.py.
  • The project-environment runner could fail importing packaging → compute version-coherence evidence in vyupgrade's environment and pass it to a stdlib-only runner → src/vyupgrade/compiler.py, src/vyupgrade/compiler_runner.py.

The schema remains v4, and the report shape is unchanged. Would you please re-review when convenient?

@charles-cooper
charles-cooper requested a review from banteg July 17, 2026 20:03

@banteg banteg left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original four findings are verified fixed at b066dc8: all four repros now pass and schema v4 reports the intended authority and workspace evidence. Requesting changes for four additional environment-boundary issues found during the whole-PR re-review: unlocked sibling path sources break in temporary mirrors; marker evaluation uses vyupgrade's Python instead of the selected project Python; target Python pins are discarded; and managed compiler execution depends on POSIX-only signals.

Comment thread src/vyupgrade/compiler.py Outdated

with tempfile.TemporaryDirectory(prefix="vyupgrade-project-") as raw_directory:
temporary_root = Path(raw_directory)
_mirror_project_root(root, temporary_root)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve declared path sources outside the mirrored root. An unlocked project with a valid tool.uv.sources entry such as helper = { path = "../helper" } works under native uv, but this mirror moves the project under a random temporary parent without recreating the sibling. I reproduced both source and target validation failing with Distribution not found at .../T/helper. The dependency context even attests the original helper tree, so the execution no longer matches the reported declared source. Mirror or remap declared external path sources while keeping the original relative layout.

Comment thread src/vyupgrade/compiler.py Outdated
if root != "vyper":
roots.add(root)
return tuple(sorted(roots))
if requirement.marker is None or requirement.marker.evaluate({"extra": extra or ""}):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Evaluate markers for the selected project interpreter. Marker.evaluate() defaults to vyupgrade's current Python, which can differ from the interpreter uv selects for the project. With vyupgrade on 3.13, a project requiring >=3.11,<3.12 and declaring vyper==0.4.1 when python_version < 3.12 should give the project compiler authority. This code marks it inactive, injects 0.4.3 from the pragma, and the report contains both Vyper 0.4.1 and 0.4.3 while claiming source authority. That silently validates with the wrong compiler. Build the marker environment from the selected project Python rather than the parent process.

Comment thread src/vyupgrade/compiler.py Outdated
project_compiler: bool,
project_declares_vyper: bool,
) -> bool:
if not project_compiler or project_declares_vyper:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Do not silently discard an explicit target Python pin. This always returns true for target compilation, so --target-python is removed in every project environment. A project requiring >=3.13 still passes when invoked with --target-python 3.12, and the attested compiler artifact is under Python 3.13. README documents source-python and target-python as pins for each compiler subprocess. Preserve an explicit compatible pin, or fail clearly when it conflicts with the project constraint instead of silently selecting another interpreter.

Comment thread src/vyupgrade/compiler_runner.py Outdated
tempfile.TemporaryFile("w+", encoding="utf-8") as stdout,
tempfile.TemporaryFile("w+", encoding="utf-8") as stderr,
):
previous_handler = signal.signal(signal.SIGALRM, _timeout_handler)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Use a portable timeout for managed compilation. Windows does not provide signal.SIGALRM, signal.ITIMER_REAL, or signal.setitimer, so every managed compiler run fails here before _parse_args executes. The package publishes a generic pure-Python console tool with no OS restriction. Keep the compiler classification boundary, but implement the timeout without POSIX-only signals or explicitly guard and support the Windows path.

@charles-cooper

charles-cooper commented Jul 19, 2026

Copy link
Copy Markdown
Member Author

co-authored by gpt 5.6 sol

@banteg, the four environment-boundary findings from the latest review are addressed at c007c68:

  • Unlocked sibling path sources are recreated at their declared relative locations in the temporary project layout; both source and target compilation are covered.
  • Dependency markers are evaluated with the interpreter selected for the project, and the same marker environment now drives compiler authority, declarations, and command construction.
  • Explicit target Python pins are preserved when compatible and produce a typed environment failure when they conflict with requires-python.
  • Managed compiler timeouts now use a subprocess boundary rather than POSIX alarm signals, while preserving compiler rejection, timeout, adapter failure, and crash classifications, including non-signal worker exits on Windows.

The full suite passes (862 tests), Ruff passes, and all PR checks are green. Ready for re-review when convenient.

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.

2 participants