Skip to content

Change-contract pipeline: one verdict envelope for every change source - #93

Merged
zzet merged 14 commits into
mainfrom
feat/change-contract-pipeline
Jun 15, 2026
Merged

Change-contract pipeline: one verdict envelope for every change source#93
zzet merged 14 commits into
mainfrom
feat/change-contract-pipeline

Conversation

@zzet

@zzet zzet commented Jun 14, 2026

Copy link
Copy Markdown
Owner

What

Adds one change_contract primitive that any change source lowers into, runs through a single pipeline, and emits one verdict envelope — plus the surrounding edit-safety surface. Built feature-by-feature (one complete feature per commit).

change sources                  one pipeline                      one verdict
 WorkspaceEdit                ① LOWER   → changed-symbol set
 diff range A..B   ──┐        ② PREDICT → blast radius / sim
 symbol set          ├──────► ③ EVALUATE→ guards+arch+events     → verdict
 {file,[ranges]}   ──┘        ④ SCORE   → blast × PageRank          allow/warn/refuse
                              ⑤ CLASSIFY→ behavioral/structural…    reasons[] (+conf)
                              ⑥ EMIT    → envelope                  verification_command
                                                                    stop_condition
                                                                    edit_strategy

The analysis emits a verdict (data the agent reads); a thin enforcement layer (a pretooluse hook) is the only thing that turns a refuse into a block. The graph advises; it doesn't wall.

New surface

  • change_contract — lower a WorkspaceEdit (speculative simulation with broken-caller detection), a git diff range (source:diff base:…), an explicit symbol set, or file line-ranges; returns {verdict: allow|warn|refuse} with reasons + confidence, a risk score, a change classification, a verification_command, a checkable stop_condition, and an edit_strategy (named refactor technique + CC-impact + safety signals). lens:api focuses the verdict on the public surface (cross-file consumers + API contracts). risk_gate:true requires a TTL'd impact-review ack (ack:true, stored as a development memory) for load-bearing symbols.
  • symbols_for_ranges — the standalone lowering primitive: (file, [ranges]) → enclosing symbols.
  • analyze kind=suggest_boundaries — seeds an architecture: block from the detected Leiden communities (the graph teaches its own layer rules).

Edit-safety changes

  • Pre-write parse gate on edit_file / write_file: refuses an edit that would introduce new tree-sitter parse errors (override allow_parse_errors; disable GORTEX_EDIT_PARSE_GATE=0).
  • RuleFamily interface with per-rule severity tiers (error→refuse, warn→warn, info→annotate) and except-globs on guard / architecture rules; a config-driven event-boundary rule family over the pub/sub graph.
  • Blast-radius pagination: explain_change_impact / detect_changes return by_depth_counts by default with summary_only / offset / limit.
  • Propagate-delete: safe_delete_symbol propagate:true patches surviving call sites (standalone calls removed and parse-gate validated; embedded references flagged manual).
  • Guarded replace: edit_file expected_occurrences:N refuses unless the match count is exactly N.

Verification

  • go build ./... and the CGO cmd/gortex binary build green.
  • All touched packages pass go test and go test -race (internal/mcp, internal/analysis, internal/config).
  • golangci-lint clean on the touched packages.
  • The cmd/gortex wire-contract golden is unchanged (no graph node/edge schema change).
  • Each feature carries unit + handler-level tests.

zzet added 14 commits June 15, 2026 00:26
edit_file and write_file now parse the candidate content with tree-sitter
before the atomic swap and refuse a write that would leave the file more
syntactically broken than it already was. Only a regression blocks — editing
an already-broken file, or one whose language the gate cannot parse, is never
refused. Pass allow_parse_errors=true to override; set GORTEX_EDIT_PARSE_GATE=0
to disable globally.
Lowers a set of (file, line-range) pairs to the graph symbols those ranges
touch — the input adapter for any range-shaped change source (editor selection,
LSP code-action range, diff hunk). Adds fileSymbolIndex.enclosingForRange, which
returns the smallest enclosing symbol at each covered line so a range inside one
function yields that function and a range spanning two yields both.
One envelope every change source lowers into: a WorkspaceEdit (speculative
simulation with broken-caller detection), a git diff range, an explicit symbol
set, or file line-ranges. Runs LOWER -> PREDICT -> EVALUATE (guards +
architecture) -> SCORE (blast x centrality) -> CLASSIFY (behavioral /
structural / runtime_drift / metadata_only) -> EMIT, returning one verdict
{allow|warn|refuse} with reasons, risk, a verification_command, and a checkable
stop_condition. The analysis advises; a pretooluse hook decides.
When a change touches a heavy symbol, the envelope's edit_strategy now names a
refactoring technique (Extract Method / Introduce Parameter Object), ranks its
complexity impact, and lists graph-derived safety signals (covering tests,
tracked callers, broken-caller status) — so the gate offers the safe path, not
just a refusal.
Co-change omissions surface files the changed set historically moves with but
left out (mined file-level co-change edges, thresholded). The lens=api mode
focuses the verdict on the public surface: each changed exported symbol reports
its cross-file consumers and participating API contracts, so a diff between two
refs (source=diff base=…) flags breaking-change risk and contract drift on the
API it touches.
…t globs

Generalises the change_contract evaluator to run every registered RuleFamily
over the changed set (guards + architecture today; events / taint plug in the
same way). Guard and architecture rules gain a severity tier — error maps to a
change_contract refuse, warn to warn, info to annotate (default warn, so a rule
advises until it opts into blocking) — and an except-glob list that exempts
matching paths.
A declarative events: block in .gortex.yaml constrains the pub/sub graph —
which paths may produce or consume a topic, whether a produced topic must have
a consumer, and which paths are forbidden from it. EventBoundaryFamily walks
the emit / produces_topic / listens_on / consumes_topic edges of each changed
symbol and plugs into change_contract as a RuleFamily like guards and
architecture.
analyze kind=suggest_boundaries turns the Leiden community partition into a
starter architecture: block — each community becomes a candidate layer (named
from its common path prefix) and the observed cross-community call edges become
its allow list. Outputs ready-to-paste .gortex.yaml that the change_contract
architecture family enforces, so the graph bootstraps its own layer rules.
…ories

Load-bearing symbols (high fan-in / centrality) can require a prior, TTL'd
impact-review ack: change_contract risk_gate:true refuses a changed set whose
gated symbols lack a fresh ack. The ack is stored as a development memory keyed
on the symbol (change_contract ack:true), so it is graph state that survives
restarts and surfaces for the next agent. TTL and thresholds are env-tunable;
the gate is opt-in.
explain_change_impact and detect_changes now always return by_depth_counts (the
'47 affected, 3 at depth-1' headline) and page the heavy by_depth rows: offset /
limit serve a window in depth order, and summary_only drops the rows entirely.
Default limit keeps existing callers' shapes intact while giving large blast
radii a cheap summary by default.
safe_delete_symbol gains a propagate mode: instead of refusing when a symbol
still has callers, it builds a per-caller plan — standalone statement calls are
removed outright (each removal parse-gate validated so it can't corrupt a file),
embedded references are flagged for manual patching. dry_run returns the plan;
applying it patches the removable sites and then deletes the symbol, so the
delete leaves no dangling reference behind.
edit_file gains expected_occurrences: when set, the edit refuses unless
old_string matches exactly that many locations, so a wrong-cardinality sweep
(replace_all hitting more or fewer sites than intended) is caught before it
writes rather than after.
- config round-trip: mirror the new GuardRule severity/except fields.
- syntax-health test: pass allow_parse_errors so the post-write health warning
  (not the new pre-write parse gate) stays the assertion under test.
- suggest_boundaries: satisfy staticcheck (Fprintf) and ineffassign, drop a
  dead map.
Adds the change-contract capability bullet (change_contract / symbols_for_ranges
/ parse gate / propagate-delete) and lists the new suggest_boundaries analyze
kind (now 61).
@zzet
zzet merged commit c5dd389 into main Jun 15, 2026
10 checks passed
@zzet
zzet deleted the feat/change-contract-pipeline branch June 15, 2026 07:38
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