/- Infoductor.Foundation.Edit — Edit monad + Context comonad ============================================================ Phase B of `docs/ALGEBRA_PLAN.md`. Defines the abstract `Edit` monad (a thread of source mutations) and the abstract `Context` comonad (ambient information at a code position), plus the distributive law that lets `Context`-aware functions produce `Edit`-valued results. This module is *pure data*: it does not depend on the Lean LSP, Lean.Elab APIs, or any I/O backend. LSP-specific bindings (a real `MakeEditLinkProps.ofReplaceRange` integration; a Code Action provider; a `Lean.Server.CodeActionContext` consumer) live in a separate `Algebra/EditLSP.lean` module and import this one. The headless interpreter `runHeadless` applies an `Edit` to an in-memory source buffer and emits the post-edit text. This is what `lake exe algebra-restructure` uses (per ALGEBRA_PLAN §5.3 No-LSP fallback). -/ import Infoductor.Foundation.Meta namespace Infoductor -- ── EditOp — the leaf of every edit ────────────────────────────────────────── /-- A single source-mutation request. The `position` field names the spot in source; `newContent` is what to write there; `oldContent` (if known) is recorded for audit / undo. `EditOp` is the meta-CTerm leaf — emitted by `restructure` as part of an `Edit α` computation. -/ structure EditOp where position : MetaPosition newContent : MetaArtifact oldContent : Option MetaArtifact := none description : String := "" deriving Inhabited instance : Repr EditOp where reprPrec e _ := s!"EditOp(at={repr e.position}, new={e.newContent.toString}, desc={e.description})" -- ── The Edit monad ────────────────────────────────────────────────────────── -- A computation that may emit zero or more `EditOp`s. Pure value α -- is the result of the computation; the list is the side-effect. /-- The Edit monad: a value-and-edit-list pair. Composition appends edit lists; `pure` emits nothing. Implementation note: we use the bare-bones `α × List EditOp` representation rather than the IO-threaded `run : CodeActionContext → IO (...)` form proposed in ALGEBRA_PLAN §3.1. The IO and LSP threading are *implementation choices* that don't affect the algebraic structure; lifting from this pure form into the LSP form is a `MonadLift Edit (ReaderT … IO)` instance in `EditLSP.lean`. See `docs/ALGEBRA_PLAN.md` §3.1 for the design discussion. -/ structure Edit (α : Type) where result : α ops : List EditOp deriving Inhabited namespace Edit /-- Lift a value into the trivial edit (zero ops). -/ def pure {α : Type} (a : α) : Edit α := { result := a, ops := [] } /-- Sequence: concatenate edit lists. -/ def bind {α β : Type} (m : Edit α) (f : α → Edit β) : Edit β := let n := f m.result { result := n.result, ops := m.ops ++ n.ops } /-- Emit a single edit op with no result. -/ def emit (op : EditOp) : Edit Unit := { result := (), ops := [op] } /-- Emit several edit ops. -/ def emitMany (ops : List EditOp) : Edit Unit := { result := (), ops := ops } instance : Monad Edit where pure := Edit.pure bind := Edit.bind /-- Functor instance. Maps the result; leaves ops untouched. -/ instance : Functor Edit where map f m := { result := f m.result, ops := m.ops } end Edit -- ── The Context comonad ───────────────────────────────────────────────────── -- "What's around here?" At each meta-position, expose ambient info: -- the current artifact, the position, the namespace, and whatever -- additional metadata the elaborator pass populates. /-- Ambient information at a meta-position. `here` is the focal artifact; `pos` is its location; `siblings` is a list of nearby artifacts that the restructuring might wish to consult (e.g., to rename a definition consistently with its consumers). Per ALGEBRA_PLAN §3.1 the full Context carries `Lean.Environment` and `QuestionGraph` fields too; those are populated only when running inside the elaborator (post-import). The pure-data form here works headless. -/ structure Context (α : Type) where here : α pos : MetaPosition siblings : List MetaArtifact := [] deriving Inhabited namespace Context /-- Comonad `extract`: the focal artifact at the current position. -/ def extract {α : Type} (c : Context α) : α := c.here /-- Comonad `extend`: relocate an `α` value to a context-dependent `β` value, i.e. apply a context-aware function pointwise. -/ def extend {α β : Type} (f : Context α → β) (c : Context α) : Context β := { here := f c, pos := c.pos, siblings := c.siblings } instance : Functor Context where map f c := { here := f c.here, pos := c.pos, siblings := c.siblings } end Context -- ── The distributive law: Context → Edit ──────────────────────────────────── -- The standard "comonad-to-monad" distributive setup. A context- -- aware decision produces an edit; lifting it into a Context-keyed -- Edit yields the contextualized edit. /-- Lift a context-aware decision into an `Edit`. Per ALGEBRA_PLAN §3.2 this is the *distributive law* between the Context comonad and the Edit monad. -/ def Context.contextualEdit {α β : Type} (decide : Context α → Edit β) (c : Context α) : Edit β := decide c -- ── Classifier-at-position evaluation ─────────────────────────────────────── -- Decide whether a `MetaClassifier` matches a `MetaPosition`. Used -- by `restructure` to pick `witness` vs `fallback`. /-- Decide whether `φ` matches `pos`, syntactically. For `.underAttribute` and `.dependencyOf` we conservatively answer `false` here — those require Lean.Environment and live in the LSP integration module. The lattice operations `.meet` and `.join` are the obvious Boolean combinations; `.always` is `true`, `.never` is `false`. -/ def MetaClassifier.atPosition : MetaClassifier → MetaPosition → Bool | .always, _ => true | .never, _ => false | .atDecl n, p => decide (n = p.declName) | .inFile s, p => decide (s = p.filePath) | .underAttribute _, _ => false -- needs environment; resolved in LSP module | .dependencyOf _, _ => false -- needs dep graph; resolved in LSP module | .inNamespace n, p => -- syntactic prefix check: is `p.declName` lexically under `n`? n.isPrefixOf p.declName | .meet a b, p => a.atPosition p && b.atPosition p | .join a b, p => a.atPosition p || b.atPosition p /-- Lattice laws on `atPosition` — the meta-mirror of the cubical face-formula laws (`FaceFormula.eval`). `meet` is conjunction, `join` is disjunction, `always`/`never` are top/bottom. -/ theorem MetaClassifier.atPosition_always (p : MetaPosition) : MetaClassifier.always.atPosition p = true := rfl theorem MetaClassifier.atPosition_never (p : MetaPosition) : MetaClassifier.never.atPosition p = false := rfl theorem MetaClassifier.atPosition_meet (a b : MetaClassifier) (p : MetaPosition) : (a.meet b).atPosition p = (a.atPosition p && b.atPosition p) := rfl theorem MetaClassifier.atPosition_join (a b : MetaClassifier) (p : MetaPosition) : (a.join b).atPosition p = (a.atPosition p || b.atPosition p) := rfl end Infoductor