import Lean open Lean IO -------------------------------------------------------------------------------- -- 1. Data structures -------------------------------------------------------------------------------- inductive KBCommand where | Init (dir : String) | AddType (name : String) (parent : Option String) (domain : String) | AddInstance (name : String) (typeName : String) (domain : String) | AddProp (name : String) (domType : String) (range : String) (domain : String) | AddRel (name : String) (dom1 : String) (dom2 : String) (domain : String) | AddFact (fact : String) (domain : String) | AddRule (name : String) (statement : String) (domain : String) | AddDef (name : String) (params : String) (type : String) (body : String) (domain : String) | Remove (name : String) | List (what : String) (filter : Option String) | ShowDef (name : String) | Get (expr : String) | Search (query : String) | Deps (name : String) | Eval (expr : String) | Query (prop : String) (tactics : Option String) | Check | Commit (msg : String) | Status | Domains | DomainsStats | Help | Exit deriving Repr /-- A single KB declaration. Written to disk as a structured metadata comment immediately before the Lean source: --@kb kind= name= domain= This lets the tool reconstruct full state on startup by scanning domain files. -/ structure KBDecl where kind : String -- "type" | "instance" | "prop" | "rel" | "fact" | "rule" | "def" name : String lean : String -- Lean 4 source (may be multi-line) domain : String -- stem of the .lean file (without extension) deriving Repr, Inhabited structure KBState where root : System.FilePath decls : Array KBDecl /-- Result of a command — drives exit codes in non-interactive use. -/ structure CmdResult where ok : Bool -- false → exit code 1 continue : Bool -- false → user typed 'exit', stop the REPL def cmdOk : CmdResult := { ok := true, continue := true } def cmdErr : CmdResult := { ok := false, continue := true } def cmdQuit : CmdResult := { ok := true, continue := false } -------------------------------------------------------------------------------- -- 2. Utility functions -------------------------------------------------------------------------------- def strContains (haystack needle : String) : Bool := if needle.isEmpty then true else (haystack.splitOn needle).length > 1 def dedupStrings (xs : List String) : List String := xs.foldl (fun acc x => if acc.contains x then acc else acc ++ [x]) [] def strToLower (s : String) : String := s.map fun c => if c.toNat >= 'A'.toNat && c.toNat <= 'Z'.toNat then Char.ofNat (c.toNat + 32) else c /-- Split on the LAST occurrence of `sep` and return (before, after). -/ def splitOnLast (s sep : String) : Option (String × String) := let segs := s.splitOn sep if segs.length < 2 then none else some ((segs.dropLast |> String.intercalate sep).trim, segs.getLast!.trim) -------------------------------------------------------------------------------- -- 3. Metadata format and persistence -- -- Every declaration on disk is stored as: -- --@kb kind= name= domain= -- -- [ ...] -- (blank line) -- -- Parsing all domain files on startup fully reconstructs KBState.decls. -------------------------------------------------------------------------------- def metaPrefix : String := "--@kb " def encodeMetaLine (d : KBDecl) : String := s!"{metaPrefix}kind={d.kind} name={d.name} domain={d.domain}" /-- Parse a "--@kb ..." header. Returns (kind, name, domain) or none. -/ def parseMetaLine (line : String) : Option (String × String × String) := do guard (line.startsWith metaPrefix) let fields := (line.drop metaPrefix.length).splitOn " " let mut kind := ""; let mut name := ""; let mut domain := "" for f in fields do if f.startsWith "kind=" then kind := f.drop "kind=".length if f.startsWith "name=" then name := f.drop "name=".length if f.startsWith "domain=" then domain := f.drop "domain=".length if kind.isEmpty || name.isEmpty || domain.isEmpty then none else some (kind, name, domain) /-- Extract all --@kb-annotated declarations from the text of one domain file. -/ def parseDomainFile (contents : String) : Array KBDecl := let lines := contents.splitOn "\n" let mut decls : Array KBDecl := #[] let mut i := 0 while i < lines.length do match parseMetaLine lines[i]! with | none => i := i + 1 | some (kind, name, domain) => i := i + 1 let mut leanLines : List String := [] while i < lines.length && !(lines[i]!.startsWith metaPrefix) do leanLines := leanLines ++ [lines[i]!] i := i + 1 let lean := (leanLines.reverse.dropWhile String.isEmpty).reverse |> String.intercalate "\n" decls := decls.push { kind, name, lean, domain } decls /-- Scan all domain .lean files in `root` and reconstruct the declaration list. -/ def loadState (root : System.FilePath) : IO (Array KBDecl) := do let reserved := ["KnowledgeBase", "lakefile", "_kb_tmp"] let entries ← System.FilePath.readDir root let stems := entries.toList.filterMap fun e => let fname := e.fileName if fname.endsWith ".lean" then let stem := fname.dropRight ".lean".length if reserved.contains stem then none else some stem else none let mut all : Array KBDecl := #[] for stem in dedupStrings stems do let path := root / (stem ++ ".lean") if ← path.pathExists then all := all ++ parseDomainFile (← IO.FS.readFile path) return all -------------------------------------------------------------------------------- -- 4. Command parsing -------------------------------------------------------------------------------- def parseCommand (line : String) : Option KBCommand := let normalised := line.trim.replace "→" "->" let parts := normalised.splitOn " " |>.filter (not ∘ String.isEmpty) let raw := line.trim match parts with | ["init", dir] => some (.Init dir) -- add type | ["add", "type", name, "in", dom] => some (.AddType name none dom) | ["add", "type", name, "extends", par, "in", dom] => some (.AddType name (some par) dom) -- add instance | ["add", "instance", name, ":", ty, "in", dom] => some (.AddInstance name ty dom) -- add prop | ["add", "prop", name, ":", dt, "->", rng, "in", dom] => some (.AddProp name dt rng dom) -- add rel | ["add", "rel", name, ":", d1, "->", d2, "->", "Prop", "in", dom] => some (.AddRel name d1 d2 dom) -- add fact (suffix style — last " in " is the domain separator) | "add" :: "fact" :: _ => match splitOnLast (raw.drop "add fact ".length) " in " with | some (fact, dom) => some (.AddFact fact dom) | none => none -- add rule | "add" :: "rule" :: name :: ":" :: _ => let afterColon := raw.drop ("add rule " ++ name ++ " : ").length match splitOnLast afterColon " in " with | some (stmt, dom) => some (.AddRule name stmt dom) | none => none -- add def | "add" :: "def" :: name :: _ => let afterName := raw.drop ("add def " ++ name ++ " ").length match splitOnLast afterName " in " with | none => none | some (defBody, dom) => match splitOnLast defBody " := " with | none => none | some (lhs, body) => match splitOnLast lhs " : " with | none => none | some (params, ty) => some (.AddDef name params ty body dom) -- rm | ["rm", name] => some (.Remove name) -- list | ["list", what] => some (.List what none) | ["list", what, "of", f] => some (.List what (some f)) -- show / get / search / deps | ["show", "def", name] => some (.ShowDef name) | "get" :: _ => some (.Get (raw.drop "get ".length)) | "search" :: _ => some (.Search (raw.drop "search ".length)) | ["deps", name] => some (.Deps name) -- eval | "eval" :: _ => some (.Eval (raw.drop "eval ".length)) -- query [using ] — split on last " using " | "query" :: _ => let body := raw.drop "query ".length match splitOnLast body " using " with | some (prop, tacs) => some (.Query prop (some tacs)) | none => some (.Query body none) -- build / git | ["check"] => some .Check | "commit" :: _ => some (.Commit (raw.drop "commit ".length)) | ["status"] => some .Status -- domain info | ["domains"] => some .Domains | ["domains", "stats"] => some .DomainsStats -- meta | ["help"] => some .Help | ["exit"] => some .Exit | _ => none -------------------------------------------------------------------------------- -- 5. File-system helpers -------------------------------------------------------------------------------- def domainPath (root : System.FilePath) (domain : String) : System.FilePath := root / (domain ++ ".lean") /-- Write a declaration to disk: metadata header then Lean source, then blank line. -/ def appendToDomain (root : System.FilePath) (d : KBDecl) : IO Unit := do let path := domainPath root d.domain unless (← path.pathExists) do IO.FS.writeFile path s!"-- Knowledge Base: {d.domain}\n\n" let h ← IO.FS.Handle.mk path .append h.putStrLn (encodeMetaLine d) h.putStrLn d.lean h.putStrLn "" h.flush def runProc (cmd : String) (args : Array String) (cwd : Option String := none) : IO (UInt32 × String) := do let child ← IO.Process.spawn { cmd := cmd; args := args; cwd := cwd stdout := .piped; stderr := .piped } let stdoutTask ← IO.asTask child.stdout.readToEnd Task.Priority.dedicated let stderrOut ← child.stderr.readToEnd let stdoutOut ← IO.ofExcept stdoutTask.get let code ← child.wait return (code, stdoutOut ++ stderrOut) def runProcPrint (cmd : String) (args : Array String) (cwd : Option String := none) : IO UInt32 := do let (code, out) ← runProc cmd args cwd unless out.isEmpty do println out return code -------------------------------------------------------------------------------- -- 6. Lean snippet generators -------------------------------------------------------------------------------- def genType (name : String) (parent : Option String) : String := match parent with | none => s!"opaque {name} : Type" | some p => s!"opaque {name} : Type\n" ++ s!"-- {name} extends {p}\n" ++ s!"axiom {name}.val : {name} → {p}" def genInstance (name typeName : String) : String := s!"opaque {name} : {typeName}" def genProp (name domType range : String) : String := s!"opaque {name} : {domType} → {range}" def genRel (name dom1 dom2 : String) : String := s!"opaque {name} : {dom1} → {dom2} → Prop" def genFact (fact : String) : String := let byParen := fact.splitOn "(" if byParen.length < 2 then let byEq := fact.splitOn "=" if byEq.length >= 2 then let lhs := (byEq.headD "").trim let rhs := (byEq.tail.headD "").trim s!"axiom fact_{lhs} : {lhs} = {rhs}" else s!"-- unrecognised fact: {fact}" else let funcName := (byParen.headD "").trim let afterOpen := byParen.tail |> String.intercalate "(" let byClose := afterOpen.splitOn ")" let argsStr := (byClose.headD "").trim let args := argsStr.splitOn "," |>.map String.trim let tag := args.foldl (· ++ "_" ++ ·) funcName let argStr := String.intercalate " " args let afterClose := byClose.tail |> String.intercalate ")" let byEq := afterClose.splitOn "=" if byEq.length >= 2 then s!"axiom fact_{tag} : {funcName} {argStr} = {(byEq.tail.headD "").trim}" else s!"axiom fact_{tag} : {funcName} {argStr}" def genRule (name stmt : String) : String := s!"axiom {name} : {stmt}" def genDef (name params type body : String) : String := s!"def {name} ({params}) : {type} := {body}" -------------------------------------------------------------------------------- -- 7. Duplicate check -------------------------------------------------------------------------------- def isDuplicate (state : KBState) (kind name : String) : Bool := state.decls.any (fun d => d.kind == kind && d.name == name) -------------------------------------------------------------------------------- -- 8. Add handlers -------------------------------------------------------------------------------- def addType (stateRef : IO.Ref KBState) (name : String) (parent : Option String) (domain : String) : IO CmdResult := do let state ← stateRef.get if isDuplicate state "type" name then println s!"Error: type '{name}' already exists."; return cmdErr let d : KBDecl := { kind := "type", name, lean := genType name parent, domain } appendToDomain state.root d stateRef.modify fun s => { s with decls := s.decls.push d } println s!"Added type '{name}'{parent.map (" (extends " ++ · ++ ")") |>.getD ""} → {domain}.lean" return cmdOk def addInstance (stateRef : IO.Ref KBState) (name typeName domain : String) : IO CmdResult := do let state ← stateRef.get if isDuplicate state "instance" name then println s!"Error: instance '{name}' already exists."; return cmdErr let d : KBDecl := { kind := "instance", name, lean := genInstance name typeName, domain } appendToDomain state.root d stateRef.modify fun s => { s with decls := s.decls.push d } println s!"Added instance '{name} : {typeName}' → {domain}.lean" return cmdOk def addProp (stateRef : IO.Ref KBState) (name domType range domain : String) : IO CmdResult := do let state ← stateRef.get if isDuplicate state "prop" name then println s!"Error: property '{name}' already exists."; return cmdErr let d : KBDecl := { kind := "prop", name, lean := genProp name domType range, domain } appendToDomain state.root d stateRef.modify fun s => { s with decls := s.decls.push d } println s!"Added property '{name} : {domType} → {range}' → {domain}.lean" return cmdOk def addRel (stateRef : IO.Ref KBState) (name dom1 dom2 domain : String) : IO CmdResult := do let state ← stateRef.get if isDuplicate state "rel" name then println s!"Error: relation '{name}' already exists."; return cmdErr let d : KBDecl := { kind := "rel", name, lean := genRel name dom1 dom2, domain } appendToDomain state.root d stateRef.modify fun s => { s with decls := s.decls.push d } println s!"Added relation '{name} : {dom1} → {dom2} → Prop' → {domain}.lean" return cmdOk def addFact (stateRef : IO.Ref KBState) (fact domain : String) : IO CmdResult := do let state ← stateRef.get let lean := genFact fact -- The canonical name is the axiom name embedded in the generated source. let axiomName := if lean.startsWith "axiom " then (lean.drop "axiom ".length).splitOn " " |>.headD ("fact:" ++ fact) else "fact:" ++ fact if isDuplicate state "fact" axiomName then println s!"Warning: fact '{axiomName}' already exists."; return cmdErr let d : KBDecl := { kind := "fact", name := axiomName, lean, domain } appendToDomain state.root d stateRef.modify fun s => { s with decls := s.decls.push d } println s!"Added fact: {fact} → {domain}.lean" return cmdOk def addRule (stateRef : IO.Ref KBState) (name stmt domain : String) : IO CmdResult := do let state ← stateRef.get if isDuplicate state "rule" name then println s!"Error: rule '{name}' already exists."; return cmdErr let d : KBDecl := { kind := "rule", name, lean := genRule name stmt, domain } appendToDomain state.root d stateRef.modify fun s => { s with decls := s.decls.push d } println s!"Added rule '{name}' → {domain}.lean" return cmdOk def addDef (stateRef : IO.Ref KBState) (name params type body domain : String) : IO CmdResult := do let state ← stateRef.get if isDuplicate state "def" name then println s!"Error: definition '{name}' already exists. Use 'rm {name}' first."; return cmdErr let d : KBDecl := { kind := "def", name, lean := genDef name params type body, domain } appendToDomain state.root d stateRef.modify fun s => { s with decls := s.decls.push d } println s!"Added definition '{name}' → {domain}.lean" return cmdOk -------------------------------------------------------------------------------- -- 9. Remove — metadata-block aware -- -- Rewrites the domain file, skipping the entire --@kb block for the named decl. -- This is atomic and correct regardless of how many lines the Lean source spans. -------------------------------------------------------------------------------- def rewriteFileRemovingDecl (path : System.FilePath) (target : String) : IO Unit := do if !(← path.pathExists) then return let lines := (← IO.FS.readFile path).splitOn "\n" let mut kept : List String := [] let mut i := 0 while i < lines.length do match parseMetaLine lines[i]! with | some (_, mname, _) => if mname == target then -- Skip this metadata line and all following non-metadata content. i := i + 1 while i < lines.length && !(lines[i]!.startsWith metaPrefix) do i := i + 1 -- Skip exactly one trailing blank line if present. if i < lines.length && lines[i]!.isEmpty then i := i + 1 else kept := kept ++ [lines[i]!] i := i + 1 | none => kept := kept ++ [lines[i]!] i := i + 1 IO.FS.writeFile path (String.intercalate "\n" kept) def removeDecl (stateRef : IO.Ref KBState) (name : String) : IO CmdResult := do let state ← stateRef.get let targets := state.decls.filter (fun d => d.name == name) if targets.isEmpty then println s!"Error: '{name}' not found."; return cmdErr let dependents := state.decls.filter (fun d => d.name != name && strContains d.lean name) unless dependents.isEmpty do println s!"Warning: the following declarations reference '{name}':" for dep in dependents do println s!" [{dep.kind}] {dep.name} ({dep.domain})" println " Consider removing dependents first." for domain in dedupStrings (targets.map (·.domain)).toList do let _ ← rewriteFileRemovingDecl (domainPath state.root domain) name stateRef.modify fun s => { s with decls := s.decls.filter (fun d => d.name != name) } println s!"Removed '{name}'." return cmdOk -------------------------------------------------------------------------------- -- 10. List / ShowDef / Search / Deps / Get -------------------------------------------------------------------------------- def normKind (w : String) : String := match w with | "types" => "type" | "instances" => "instance" | "props" => "prop" | "rels" => "rel" | "facts" => "fact" | "rules" => "rule" | "defs" => "def" | _ => w def listDecls (stateRef : IO.Ref KBState) (what : String) (filter : Option String) : IO CmdResult := do let state ← stateRef.get let nk := normKind what let matchKind : KBDecl → Bool := if nk == "all" then fun _ => true else fun d => d.kind == nk let matchFilter : KBDecl → Bool := match filter with | none => fun _ => true | some f => let fl := strToLower f fun d => strContains (strToLower d.name) fl || strContains (strToLower d.domain) fl || strContains (strToLower d.lean) fl let hits := state.decls.filter (fun d => matchKind d && matchFilter d) if hits.isEmpty then println s!"(no {what} found)" else for d in hits do println s!"[{d.kind}] {d.name} ({d.domain})" return cmdOk def showDef (stateRef : IO.Ref KBState) (name : String) : IO CmdResult := do let state ← stateRef.get let hits := state.decls.filter (fun d => d.name == name) if hits.isEmpty then println s!"Error: '{name}' not found."; return cmdErr for d in hits do println s!"-- [{d.kind}] {d.name} (domain: {d.domain})" println d.lean return cmdOk def searchDecls (stateRef : IO.Ref KBState) (query : String) : IO CmdResult := do let state ← stateRef.get let q := strToLower query let hits := state.decls.filter fun d => strContains (strToLower d.name) q || strContains (strToLower d.lean) q || strContains (strToLower d.domain) q if hits.isEmpty then println s!"No results for '{query}'." else do println s!"{hits.size} result(s) for '{query}':" for d in hits do println s!" [{d.kind}] {d.name} ({d.domain})" for l in d.lean.splitOn "\n" do if strContains (strToLower l) q then println s!" {l.trim}" return cmdOk def showDeps (stateRef : IO.Ref KBState) (name : String) : IO CmdResult := do let state ← stateRef.get match state.decls.find? (fun d => d.name == name) with | none => println s!"Error: '{name}' not found."; return cmdErr | some d => let forward := state.decls.filter fun o => o.name != name && strContains d.lean o.name let reverse := state.decls.filter fun o => o.name != name && strContains o.lean name println s!"Dependencies of [{d.kind}] {name} ({d.domain}):" if forward.isEmpty then println " uses: (none)" else do println " uses:" for dep in forward do println s!" [{dep.kind}] {dep.name} ({dep.domain})" if reverse.isEmpty then println " used by: (none)" else do println " used by:" for dep in reverse do println s!" [{dep.kind}] {dep.name} ({dep.domain})" return cmdOk -------------------------------------------------------------------------------- -- 11. Eval / Get / Query -------------------------------------------------------------------------------- def buildImports (state : KBState) : IO String := do let domains := dedupStrings (state.decls.map (·.domain)).toList let mut imports := "" for domain in domains do if ← (domainPath state.root domain).pathExists then imports := imports ++ s!"import {domain}\n" return imports def runLeanSnippet (state : KBState) (snippet : String) : IO (UInt32 × String) := do let imports ← buildImports state let tmp := state.root / "_kb_tmp.lean" IO.FS.writeFile tmp (imports ++ "\n" ++ snippet ++ "\n") let result ← runProc "lean" #[tmp.toString] (some state.root.toString) try IO.FS.removeFile tmp catch _ => pure () return result def evalExpr (stateRef : IO.Ref KBState) (expr : String) : IO CmdResult := do let state ← stateRef.get let (code, out) ← runLeanSnippet state s!"#eval {expr}" if code == 0 then println out.trim; return cmdOk else do println s!"Lean error evaluating '{expr}':\n{out.trim}"; return cmdErr /-- `get ()` — evaluate a property applied to an instance. Parses "prop(inst1, inst2)" → `#eval prop inst1 inst2`. -/ def getVal (stateRef : IO.Ref KBState) (expr : String) : IO CmdResult := do let state ← stateRef.get let evalStr := let byParen := expr.splitOn "(" if byParen.length >= 2 then let func := (byParen.headD "").trim let args := ((byParen.tail |> String.intercalate "(").splitOn ")").headD "" |>.trim let argList := args.splitOn "," |>.map String.trim |> String.intercalate " " s!"{func} {argList}" else expr let (code, out) ← runLeanSnippet state s!"#eval {evalStr}" if code == 0 then println out.trim; return cmdOk else do println s!"Error evaluating '{expr}':\n{out.trim}"; return cmdErr /-- Enhanced query with optional user-supplied tactic and proof term output. Syntax: query [using ] Default tactics: first | decide | aesop | simp On success: prints "Status: Yes" and the proof term via #print. On failure: prints "Status: No" or "Status: Unknown" with the Lean error. -/ def queryProp (stateRef : IO.Ref KBState) (prop : String) (tactics : Option String) : IO CmdResult := do let state ← stateRef.get let tacs := tactics.getD "first | decide | aesop | simp" let pname := "_kb_proof" let thm := s!"set_option maxHeartbeats 8000 in\ntheorem {pname} : {prop} := by {tacs}" let (code, out) ← runLeanSnippet state thm if code == 0 then -- Also fetch the proof term. let (_, printOut) ← runLeanSnippet state (thm ++ s!"\n#print {pname}") println s!"Status: Yes" println s!"Tactic: {tacs}" let proofLine := printOut.trim unless proofLine.isEmpty do println s!"Proof:\n{proofLine}" return cmdOk else if strContains out "false" || strContains out "False" || strContains out "contradiction" then do println s!"Status: No" println s!"Tactic: {tacs}" println s!"Lean output:\n{out.trim}" return cmdErr else do println s!"Status: Unknown" println s!"Tactic: {tacs}" println s!"Lean output:\n{out.trim}" return cmdErr -------------------------------------------------------------------------------- -- 12. Build / Commit / Status -------------------------------------------------------------------------------- def existingDomains (root : System.FilePath) : IO (List String) := do let reserved := ["KnowledgeBase", "lakefile", "_kb_tmp"] let entries ← System.FilePath.readDir root return dedupStrings <| entries.toList.filterMap fun e => let fname := e.fileName if fname.endsWith ".lean" then let stem := fname.dropRight ".lean".length if reserved.contains stem then none else some stem else none def syncRootModule (root : System.FilePath) (state : KBState) : IO Unit := do let allDomains ← existingDomains root for domain in allDomains do let ownDecls := state.decls.filter (fun d => d.domain == domain) let others := allDomains.filter (· != domain) let mut needed : List String := [] for other in others do let otherDecls := state.decls.filter (fun d => d.domain == other) if ownDecls.any fun od => otherDecls.any fun od2 => strContains od.lean od2.name then needed := needed ++ [other] if !needed.isEmpty then let path := domainPath root domain if ← path.pathExists then let contents ← IO.FS.readFile path let missing := needed.filter (fun imp => !strContains contents s!"import {imp}") unless missing.isEmpty do let header := missing.map (fun imp => s!"import {imp}") |> String.intercalate "\n" IO.FS.writeFile path (header ++ "\n\n" ++ contents) let mut root' := "-- KnowledgeBase root module (auto-generated)\n\n" for stem in allDomains do root' := root' ++ s!"import {stem}\n" IO.FS.writeFile (root / "KnowledgeBase.lean") root' def checkKB (stateRef : IO.Ref KBState) : IO CmdResult := do let state ← stateRef.get syncRootModule state.root state println "Running `lake build`…" let code ← runProcPrint "lake" #["build"] (some state.root.toString) if code == 0 then println "✓ Consistent (build succeeded)."; return cmdOk else do println "✗ Build failed — see errors above."; return cmdErr def commitChanges (stateRef : IO.Ref KBState) (msg : String) : IO CmdResult := do let state ← stateRef.get let dir := state.root.toString syncRootModule state.root state println "Verifying before commit…" let buildCode ← runProcPrint "lake" #["build"] (some dir) if buildCode != 0 then println "Commit aborted: build failed."; return cmdErr let _ ← runProcPrint "git" #["add", "."] (some dir) let code ← runProcPrint "git" #["commit", "-m", msg] (some dir) if code == 0 then println s!"Committed: \"{msg}\""; return cmdOk else do println "(Nothing to commit — working tree clean.)"; return cmdOk def showStatus (stateRef : IO.Ref KBState) : IO CmdResult := do let state ← stateRef.get println "=== git status ===" let _ ← runProcPrint "git" #["status", "--short"] (some state.root.toString) println "\n=== knowledge base ===" println s!" root : {state.root}" println s!" decls : {state.decls.size} loaded" let domains := dedupStrings (state.decls.map (·.domain)).toList for dom in domains do let n := (state.decls.filter (fun d => d.domain == dom)).size println s!" {dom}: {n}" return cmdOk -------------------------------------------------------------------------------- -- 13. Init -------------------------------------------------------------------------------- def initKB (dir : String) : IO CmdResult := do let root : System.FilePath := System.FilePath.mk dir if (← runProcPrint "mkdir" #["-p", dir]) != 0 then println s!"Error: could not create '{dir}'."; return cmdErr IO.FS.writeFile (root / "lakefile.lean") "import Lake\nopen Lake DSL\n\npackage KnowledgeBase where\n\nlean_lib KnowledgeBase where\n" IO.FS.writeFile (root / "KnowledgeBase.lean") "-- KnowledgeBase root module\n-- Run `kb check` to populate this file.\n" let _ ← runProcPrint "git" #["init"] (some dir) let _ ← runProcPrint "git" #["add", "."] (some dir) let _ ← runProcPrint "git" #["commit", "-m", "Initial kb scaffold"] (some dir) println s!"Initialised knowledge base in '{dir}'." return cmdOk -------------------------------------------------------------------------------- -- 14. Domain info -------------------------------------------------------------------------------- def listDomains (stateRef : IO.Ref KBState) : IO CmdResult := do let state ← stateRef.get let domains := dedupStrings (state.decls.map (·.domain)).toList if domains.isEmpty then println "(no domains loaded — use 'add … in ' to create one)" else do println "Domains:" for dom in domains do let n := (state.decls.filter (fun d => d.domain == dom)).size println s!" {dom} ({n} declaration(s))" return cmdOk def domainsStats (stateRef : IO.Ref KBState) : IO CmdResult := do let state ← stateRef.get let domains := dedupStrings (state.decls.map (·.domain)).toList println "=== Domain statistics ===" if domains.isEmpty then println " (none)" else for dom in domains do let ds := state.decls.filter (fun d => d.domain == dom) println s!" {dom}: {ds.size}" for k in ["type","instance","prop","rel","fact","rule","def"] do let n := (ds.filter (fun d => d.kind == k)).size if n > 0 then println s!" {k}: {n}" println "\n=== By kind ===" for k in ["type","instance","prop","rel","fact","rule","def"] do let n := (state.decls.filter (fun d => d.kind == k)).size if n > 0 then println s!" {k}: {n}" println s!"\n Total: {state.decls.size}" return cmdOk -------------------------------------------------------------------------------- -- 15. Help -------------------------------------------------------------------------------- def helpText : String := "Lean4 Knowledge Base CLI (kb)\n" ++ "All `add` commands require `in ` — each domain is a .lean file.\n\n" ++ "ADD\n" ++ " add type [extends ] in \n" ++ " add instance : in \n" ++ " add prop : -> in \n" ++ " add rel : -> -> Prop in \n" ++ " add fact in prop(inst) = val or rel(a, b)\n" ++ " add rule : in \n" ++ " add def : := in \n\n" ++ "READ\n" ++ " list [of ]\n" ++ " show def Lean source of a declaration.\n" ++ " get () Retrieve a property value (runs #eval).\n" ++ " search Full-text search across names, Lean source, domains.\n" ++ " deps Forward and reverse dependency graph for a declaration.\n" ++ " eval Run #eval on a Lean expression.\n\n" ++ "QUERY\n" ++ " query \n" ++ " query using \n" ++ " Attempt to prove a proposition. Default tactics: first | decide | aesop | simp\n" ++ " Returns Status: Yes / No / Unknown and the proof term on success.\n\n" ++ "BUILD & GIT\n" ++ " check Sync imports + lake build to verify whole KB.\n" ++ " commit Verify then git-commit all changes.\n" ++ " status git status + domain summary.\n" ++ " rm Remove a declaration (warns about dependents).\n\n" ++ "DOMAINS\n" ++ " domains List domains with declaration counts.\n" ++ " domains stats Breakdown per domain and kind.\n\n" ++ " init Create a new KB repository.\n" ++ " help | exit\n\n" ++ "EXIT CODES 0 = success 1 = error\n\n" ++ "PERSISTENCE State is stored as --@kb metadata in each domain file and\n" ++ " reloaded automatically on startup.\n\n" ++ "EXAMPLE\n" ++ " add type Entity in People\n" ++ " add type Person extends Entity in People\n" ++ " add instance socrates : Person in People\n" ++ " add prop birthYear : Person -> Nat in People\n" ++ " add fact birthYear(socrates) = 470 in People\n" ++ " get birthYear(socrates)\n" ++ " deps birthYear\n" ++ " search socrates\n" ++ " query birthYear socrates = 470 using decide\n" ++ " check\n" ++ " commit \"Added Socrates\"\n" -------------------------------------------------------------------------------- -- 16. Main REPL loop -------------------------------------------------------------------------------- partial def processCommand (stateRef : IO.Ref KBState) (line : String) : IO CmdResult := do match parseCommand line with | none => println s!"Error: unknown command '{line}'." println " Hint: add commands require `in `. Run 'help' for usage." return cmdErr | some cmd => match cmd with | .Init dir => initKB dir | .AddType n p dom => addType stateRef n p dom | .AddInstance n t dom => addInstance stateRef n t dom | .AddProp n dt rng dom => addProp stateRef n dt rng dom | .AddRel n d1 d2 dom => addRel stateRef n d1 d2 dom | .AddFact f dom => addFact stateRef f dom | .AddRule n s dom => addRule stateRef n s dom | .AddDef n p t b dom => addDef stateRef n p t b dom | .Remove n => removeDecl stateRef n | .List w f => listDecls stateRef w f | .ShowDef n => showDef stateRef n | .Get e => getVal stateRef e | .Search q => searchDecls stateRef q | .Deps n => showDeps stateRef n | .Eval e => evalExpr stateRef e | .Query p t => queryProp stateRef p t | .Check => checkKB stateRef | .Commit m => commitChanges stateRef m | .Status => showStatus stateRef | .Domains => listDomains stateRef | .DomainsStats => domainsStats stateRef | .Help => println helpText; return cmdOk | .Exit => return cmdQuit partial def main : IO Unit := do let root : System.FilePath := System.FilePath.mk "." let decls ← loadState root let stateRef ← IO.mkRef (KBState.mk root decls) if decls.size > 0 then println s!"Loaded {decls.size} declaration(s) from KB." println "Lean4 Knowledge Base CLI — type 'help' for commands, 'exit' to quit." let rec loop : IO Unit := do IO.print "kb> " let line ← (← IO.getStdin).getLine let trimmed := line.trim unless trimmed.isEmpty do let result ← processCommand stateRef trimmed unless result.continue do return loop loop