mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
Merge remote-tracking branch 'github/main'
This commit is contained in:
commit
2d19317660
36 changed files with 3345 additions and 13315 deletions
42
0-Core-Formalism/lean/Semantics/Semantics/AVMIsa/Instr.lean
Normal file
42
0-Core-Formalism/lean/Semantics/Semantics/AVMIsa/Instr.lean
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
-- AVM ISA v1 (Lean-only): Instructions
|
||||
-- Closed-world opcodes. No CALL/IMPORT. No string dispatch.
|
||||
|
||||
import Semantics.AVMIsa.Value
|
||||
|
||||
namespace Semantics.AVMIsa
|
||||
|
||||
/-- Finite primitive set (closed-world).
|
||||
|
||||
If extensibility is needed, add a constructor here and define its semantics in Lean.
|
||||
Backends must implement the same semantics.
|
||||
-/
|
||||
inductive Prim : Type where
|
||||
| addSatQ0
|
||||
| subSatQ0
|
||||
| addSatQ16
|
||||
| subSatQ16
|
||||
| and
|
||||
| or
|
||||
| not
|
||||
deriving DecidableEq, BEq, Inhabited
|
||||
|
||||
/-- Core instruction set.
|
||||
|
||||
`load`/`store` use `Nat` indices in this v1 skeleton.
|
||||
Strict implementations SHOULD replace them with `Fin n` once the local-frame
|
||||
size is part of `Program`.
|
||||
-/
|
||||
inductive Instr : Type where
|
||||
| push : AnyVal → Instr
|
||||
| pop : Instr
|
||||
| dup : Instr
|
||||
| swap : Instr
|
||||
| load : Nat → Instr
|
||||
| store : Nat → Instr
|
||||
| jump : Nat → Instr
|
||||
| jumpIf : Nat → Instr
|
||||
| prim : Prim → Instr
|
||||
| halt : Instr
|
||||
deriving Inhabited
|
||||
|
||||
end Semantics.AVMIsa
|
||||
44
0-Core-Formalism/lean/Semantics/Semantics/AVMIsa/Run.lean
Normal file
44
0-Core-Formalism/lean/Semantics/Semantics/AVMIsa/Run.lean
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
-- AVM ISA v1 (Lean-only): Run semantics (fuel-bounded)
|
||||
|
||||
import Semantics.AVMIsa.Step
|
||||
|
||||
namespace Semantics.AVMIsa
|
||||
|
||||
/-- Fuel for total execution. -/
|
||||
abbrev Fuel := Nat
|
||||
|
||||
/-- Fuel-bounded run.
|
||||
|
||||
Stops when:
|
||||
- fuel exhausted
|
||||
- machine halted
|
||||
- an error occurs
|
||||
-/
|
||||
def run (fuel : Fuel) (program : List Instr) (s : State) : Outcome State :=
|
||||
match fuel with
|
||||
| 0 => Outcome.ok s
|
||||
| Nat.succ f =>
|
||||
if s.halted then Outcome.ok s
|
||||
else
|
||||
match step program s with
|
||||
| Outcome.err e => Outcome.err e
|
||||
| Outcome.ok s1 => run f program s1
|
||||
|
||||
/-- Canary: boolean not.
|
||||
|
||||
#eval should produce `true` on top of stack.
|
||||
-/
|
||||
def canaryNot : List Instr :=
|
||||
[
|
||||
Instr.push ⟨AvmTy.bool, AvmVal.b false⟩,
|
||||
Instr.prim Prim.not,
|
||||
Instr.halt
|
||||
]
|
||||
|
||||
/-- Canary initial state. -/
|
||||
def canaryState : State :=
|
||||
{ pc := 0, stack := [], locals := [], halted := false }
|
||||
|
||||
#eval run 8 canaryNot canaryState
|
||||
|
||||
end Semantics.AVMIsa
|
||||
31
0-Core-Formalism/lean/Semantics/Semantics/AVMIsa/State.lean
Normal file
31
0-Core-Formalism/lean/Semantics/Semantics/AVMIsa/State.lean
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
-- AVM ISA v1 (Lean-only): State
|
||||
|
||||
import Semantics.AVMIsa.Instr
|
||||
|
||||
namespace Semantics.AVMIsa
|
||||
|
||||
/-- Machine state.
|
||||
|
||||
This is intentionally minimal in v1. It is sufficient to define a total `run`
|
||||
with fuel.
|
||||
-/
|
||||
structure State where
|
||||
pc : Nat
|
||||
stack : List AnyVal
|
||||
locals : List (Option AnyVal)
|
||||
halted : Bool
|
||||
|
||||
deriving Inhabited
|
||||
|
||||
/-- Safe locals lookup (returns `none` when out of bounds). -/
|
||||
def getLocal? (s : State) (i : Nat) : Option AnyVal :=
|
||||
s.locals.getD i none
|
||||
|
||||
/-- Safe locals set (no-op when out of bounds). -/
|
||||
def setLocal (s : State) (i : Nat) (v : AnyVal) : State :=
|
||||
if h : i < s.locals.length then
|
||||
{ s with locals := s.locals.set ⟨i, h⟩ (some v) }
|
||||
else
|
||||
s
|
||||
|
||||
end Semantics.AVMIsa
|
||||
162
0-Core-Formalism/lean/Semantics/Semantics/AVMIsa/Step.lean
Normal file
162
0-Core-Formalism/lean/Semantics/Semantics/AVMIsa/Step.lean
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
-- AVM ISA v1 (Lean-only): Step semantics
|
||||
|
||||
import Semantics.AVMIsa.State
|
||||
|
||||
namespace Semantics.AVMIsa
|
||||
|
||||
/-- Error tags for rejected states (ill-typed, underflow, etc.). -/
|
||||
inductive StepError : Type where
|
||||
| stackUnderflow
|
||||
| typeMismatch
|
||||
| invalidJump
|
||||
| missingLocal
|
||||
deriving Inhabited, DecidableEq, BEq
|
||||
|
||||
/-- Outcome type for AVM execution.
|
||||
|
||||
We avoid Float and avoid exceptions. Backends should mirror this boundary.
|
||||
-/
|
||||
inductive Outcome (α : Type) : Type where
|
||||
| ok : α → Outcome α
|
||||
| err : StepError → Outcome α
|
||||
deriving Inhabited
|
||||
|
||||
/-- Pop one element from stack. -/
|
||||
def pop1 (s : State) : Outcome (AnyVal × State) :=
|
||||
match s.stack with
|
||||
| [] => Outcome.err StepError.stackUnderflow
|
||||
| x :: xs => Outcome.ok (x, { s with stack := xs })
|
||||
|
||||
/-- Push one element onto stack. -/
|
||||
def push1 (s : State) (v : AnyVal) : State :=
|
||||
{ s with stack := v :: s.stack }
|
||||
|
||||
/-- Evaluate a primitive.
|
||||
|
||||
NOTE: This v1 skeleton implements only a small subset. Extend strictly by
|
||||
adding Lean semantics; do not delegate meaning to backends.
|
||||
-/
|
||||
def evalPrim (p : Prim) (s : State) : Outcome State :=
|
||||
match p with
|
||||
| Prim.not =>
|
||||
match pop1 s with
|
||||
| Outcome.err e => Outcome.err e
|
||||
| Outcome.ok (v, s1) =>
|
||||
match v.ty with
|
||||
| AvmTy.bool =>
|
||||
let b := match v.val with | AvmVal.b x => x
|
||||
Outcome.ok (push1 s1 ⟨AvmTy.bool, AvmVal.b (!b)⟩)
|
||||
| _ => Outcome.err StepError.typeMismatch
|
||||
| Prim.and =>
|
||||
match pop1 s with
|
||||
| Outcome.err e => Outcome.err e
|
||||
| Outcome.ok (v1, s1) =>
|
||||
match pop1 s1 with
|
||||
| Outcome.err e => Outcome.err e
|
||||
| Outcome.ok (v2, s2) =>
|
||||
if v1.ty = AvmTy.bool ∧ v2.ty = AvmTy.bool then
|
||||
let b1 := match v1.val with | AvmVal.b x => x
|
||||
let b2 := match v2.val with | AvmVal.b x => x
|
||||
Outcome.ok (push1 s2 ⟨AvmTy.bool, AvmVal.b (b2 && b1)⟩)
|
||||
else
|
||||
Outcome.err StepError.typeMismatch
|
||||
| Prim.or =>
|
||||
match pop1 s with
|
||||
| Outcome.err e => Outcome.err e
|
||||
| Outcome.ok (v1, s1) =>
|
||||
match pop1 s1 with
|
||||
| Outcome.err e => Outcome.err e
|
||||
| Outcome.ok (v2, s2) =>
|
||||
if v1.ty = AvmTy.bool ∧ v2.ty = AvmTy.bool then
|
||||
let b1 := match v1.val with | AvmVal.b x => x
|
||||
let b2 := match v2.val with | AvmVal.b x => x
|
||||
Outcome.ok (push1 s2 ⟨AvmTy.bool, AvmVal.b (b2 || b1)⟩)
|
||||
else
|
||||
Outcome.err StepError.typeMismatch
|
||||
| Prim.addSatQ0 =>
|
||||
match pop1 s with
|
||||
| Outcome.err e => Outcome.err e
|
||||
| Outcome.ok (v1, s1) =>
|
||||
match pop1 s1 with
|
||||
| Outcome.err e => Outcome.err e
|
||||
| Outcome.ok (v2, s2) =>
|
||||
if v1.ty = AvmTy.q0_16 ∧ v2.ty = AvmTy.q0_16 then
|
||||
let x := match v1.val with | AvmVal.q0 q => q
|
||||
let y := match v2.val with | AvmVal.q0 q => q
|
||||
Outcome.ok (push1 s2 ⟨AvmTy.q0_16, AvmVal.q0 (Semantics.Q0_16.addSat y x)⟩)
|
||||
else
|
||||
Outcome.err StepError.typeMismatch
|
||||
| Prim.subSatQ0 =>
|
||||
match pop1 s with
|
||||
| Outcome.err e => Outcome.err e
|
||||
| Outcome.ok (v1, s1) =>
|
||||
match pop1 s1 with
|
||||
| Outcome.err e => Outcome.err e
|
||||
| Outcome.ok (v2, s2) =>
|
||||
if v1.ty = AvmTy.q0_16 ∧ v2.ty = AvmTy.q0_16 then
|
||||
let x := match v1.val with | AvmVal.q0 q => q
|
||||
let y := match v2.val with | AvmVal.q0 q => q
|
||||
Outcome.ok (push1 s2 ⟨AvmTy.q0_16, AvmVal.q0 (Semantics.Q0_16.subSat y x)⟩)
|
||||
else
|
||||
Outcome.err StepError.typeMismatch
|
||||
| _ =>
|
||||
-- Remaining primitives are not yet implemented in v1.
|
||||
Outcome.err StepError.typeMismatch
|
||||
|
||||
/-- One-step execution.
|
||||
|
||||
`Program` is modeled as a list for v1.
|
||||
-/
|
||||
def step (program : List Instr) (s : State) : Outcome State :=
|
||||
if s.halted then
|
||||
Outcome.ok s
|
||||
else
|
||||
match program.get? s.pc with
|
||||
| none => Outcome.err StepError.invalidJump
|
||||
| some instr =>
|
||||
match instr with
|
||||
| Instr.halt => Outcome.ok { s with halted := true }
|
||||
| Instr.push v => Outcome.ok { s with pc := s.pc + 1, stack := v :: s.stack }
|
||||
| Instr.pop =>
|
||||
match pop1 s with
|
||||
| Outcome.err e => Outcome.err e
|
||||
| Outcome.ok (_, s1) => Outcome.ok { s1 with pc := s.pc + 1 }
|
||||
| Instr.dup =>
|
||||
match s.stack with
|
||||
| [] => Outcome.err StepError.stackUnderflow
|
||||
| x :: xs => Outcome.ok { s with pc := s.pc + 1, stack := x :: x :: xs }
|
||||
| Instr.swap =>
|
||||
match s.stack with
|
||||
| a :: b :: xs => Outcome.ok { s with pc := s.pc + 1, stack := b :: a :: xs }
|
||||
| _ => Outcome.err StepError.stackUnderflow
|
||||
| Instr.load i =>
|
||||
match getLocal? s i with
|
||||
| none => Outcome.err StepError.missingLocal
|
||||
| some v => Outcome.ok { s with pc := s.pc + 1, stack := v :: s.stack }
|
||||
| Instr.store i =>
|
||||
match pop1 s with
|
||||
| Outcome.err e => Outcome.err e
|
||||
| Outcome.ok (v, s1) => Outcome.ok { (setLocal s1 i v) with pc := s.pc + 1 }
|
||||
| Instr.jump target =>
|
||||
if target < program.length then
|
||||
Outcome.ok { s with pc := target }
|
||||
else
|
||||
Outcome.err StepError.invalidJump
|
||||
| Instr.jumpIf target =>
|
||||
match pop1 s with
|
||||
| Outcome.err e => Outcome.err e
|
||||
| Outcome.ok (v, s1) =>
|
||||
if v.ty = AvmTy.bool then
|
||||
let b := match v.val with | AvmVal.b x => x
|
||||
if b then
|
||||
if target < program.length then Outcome.ok { s1 with pc := target } else Outcome.err StepError.invalidJump
|
||||
else
|
||||
Outcome.ok { s1 with pc := s.pc + 1 }
|
||||
else
|
||||
Outcome.err StepError.typeMismatch
|
||||
| Instr.prim p =>
|
||||
match evalPrim p s with
|
||||
| Outcome.err e => Outcome.err e
|
||||
| Outcome.ok s1 => Outcome.ok { s1 with pc := s.pc + 1 }
|
||||
|
||||
end Semantics.AVMIsa
|
||||
15
0-Core-Formalism/lean/Semantics/Semantics/AVMIsa/Types.lean
Normal file
15
0-Core-Formalism/lean/Semantics/Semantics/AVMIsa/Types.lean
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
-- AVM ISA v1 (Lean-only): Types
|
||||
-- Core rule: closed-world finite type universe; no Float.
|
||||
|
||||
import Semantics.FixedPoint
|
||||
|
||||
namespace Semantics.AVMIsa
|
||||
|
||||
/-- Finite AVM type universe. Closed-world; extend only by adding constructors. -/
|
||||
inductive AvmTy : Type where
|
||||
| q0_16 : AvmTy
|
||||
| q16_16 : AvmTy
|
||||
| bool : AvmTy
|
||||
deriving DecidableEq, BEq, Inhabited
|
||||
|
||||
end Semantics.AVMIsa
|
||||
29
0-Core-Formalism/lean/Semantics/Semantics/AVMIsa/Value.lean
Normal file
29
0-Core-Formalism/lean/Semantics/Semantics/AVMIsa/Value.lean
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
-- AVM ISA v1 (Lean-only): Values
|
||||
-- Values are strictly typed; no dynamic Any.
|
||||
|
||||
import Semantics.AVMIsa.Types
|
||||
import Semantics.FixedPoint
|
||||
|
||||
namespace Semantics.AVMIsa
|
||||
|
||||
/-- Typed value payload. -/
|
||||
inductive AvmVal : AvmTy → Type where
|
||||
| q0 : Semantics.Q0_16 → AvmVal AvmTy.q0_16
|
||||
| q16 : Semantics.Q16_16 → AvmVal AvmTy.q16_16
|
||||
| b : Bool → AvmVal AvmTy.bool
|
||||
|
||||
/-- Existential wrapper for storing values in an untyped container.
|
||||
|
||||
We use this in the ISA skeleton to keep the state representation simple.
|
||||
Typing is enforced by an explicit type-check pass and by instruction semantics
|
||||
that can reject ill-typed stacks.
|
||||
|
||||
Later upgrade path: replace with a fully typed stack representation.
|
||||
-/
|
||||
structure AnyVal where
|
||||
ty : AvmTy
|
||||
val : AvmVal ty
|
||||
|
||||
deriving Inhabited
|
||||
|
||||
end Semantics.AVMIsa
|
||||
120
0-Core-Formalism/lean/Semantics/Semantics/EneContextSurface.lean
Normal file
120
0-Core-Formalism/lean/Semantics/Semantics/EneContextSurface.lean
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import Semantics.McpSurfaceManifest
|
||||
|
||||
namespace Semantics.EneContextSurface
|
||||
|
||||
open Semantics.McpSurfaceManifest
|
||||
|
||||
/--
|
||||
Lean-owned MCP surface for ENE context memory operations.
|
||||
|
||||
This file is *surface only* (names + schemas + descriptions). Runtime shims must not
|
||||
invent alternate tools; they should load and expose this manifest.
|
||||
|
||||
Execution plumbing is provided by non-Lean runtimes (Rust) and must be keyed off
|
||||
these tool names.
|
||||
-/
|
||||
|
||||
def eneStatus : ToolSpec :=
|
||||
{ name := "ene_status"
|
||||
description := "ENE context health: local memory ledger, ENE API, session-sync binary."
|
||||
inputSchema := Json.obj [] }
|
||||
|
||||
def eneRemember : ToolSpec :=
|
||||
{ name := "ene_remember"
|
||||
description := "Store a durable ENE memory packet with a hash-chain receipt."
|
||||
inputSchema := Json.obj
|
||||
[ ("type", "object")
|
||||
, ("properties", Json.obj
|
||||
[ ("agent", Json.obj [("type", "string"), ("default", "codex")])
|
||||
, ("key", Json.obj [("type", "string")])
|
||||
, ("value", Json.obj [])
|
||||
, ("tags", Json.obj [("type", "array"), ("items", Json.obj [("type", "string")])])
|
||||
, ("kind", Json.obj [("type", "string"), ("default", "note")])
|
||||
, ("source", Json.obj [("type", "string"), ("default", "mcp")])
|
||||
])
|
||||
, ("required", Json.arr #["key", "value"])
|
||||
] }
|
||||
|
||||
def eneRecall : ToolSpec :=
|
||||
{ name := "ene_recall"
|
||||
description := "Recall ENE memory by exact key or query over local memory."
|
||||
inputSchema := Json.obj
|
||||
[ ("type", "object")
|
||||
, ("properties", Json.obj
|
||||
[ ("agent", Json.obj [("type", "string"), ("default", "codex")])
|
||||
, ("key", Json.obj [("type", "string")])
|
||||
, ("query", Json.obj [("type", "string")])
|
||||
, ("limit", Json.obj [("type", "integer"), ("default", 10)])
|
||||
])
|
||||
] }
|
||||
|
||||
def eneSearch : ToolSpec :=
|
||||
{ name := "ene_search"
|
||||
description := "Search ENE API chat/session memory plus local ENE memory ledger."
|
||||
inputSchema := Json.obj
|
||||
[ ("type", "object")
|
||||
, ("properties", Json.obj
|
||||
[ ("query", Json.obj [("type", "string")])
|
||||
, ("limit", Json.obj [("type", "integer"), ("default", 10)])
|
||||
, ("semantic", Json.obj [("type", "boolean"), ("default", false)])
|
||||
, ("agent", Json.obj [("type", "string"), ("default", "codex")])
|
||||
, ("sources", Json.obj [("type", "array"), ("items", Json.obj [("type", "string")])])
|
||||
])
|
||||
, ("required", Json.arr #["query"])
|
||||
] }
|
||||
|
||||
def eneContext : ToolSpec :=
|
||||
{ name := "ene_context"
|
||||
description := "ENE-first startup/context packet: status, search, recall, optional transcript save."
|
||||
inputSchema := Json.obj
|
||||
[ ("type", "object")
|
||||
, ("properties", Json.obj
|
||||
[ ("user_message", Json.obj [("type", "string")])
|
||||
, ("query", Json.obj [("type", "string")])
|
||||
, ("agent", Json.obj [("type", "string"), ("default", "codex")])
|
||||
, ("session_id", Json.obj [("type", "string")])
|
||||
, ("save_exchange", Json.obj [("type", "boolean"), ("default", false)])
|
||||
, ("limit", Json.obj [("type", "integer"), ("default", 10)])
|
||||
])
|
||||
] }
|
||||
|
||||
def eneSessions : ToolSpec :=
|
||||
{ name := "ene_sessions"
|
||||
description := "List or fetch ENE chat sessions through ene-api."
|
||||
inputSchema := Json.obj
|
||||
[ ("type", "object")
|
||||
, ("properties", Json.obj
|
||||
[ ("action", Json.obj [("type", "string"), ("default", "list")])
|
||||
, ("session_id", Json.obj [("type", "string")])
|
||||
, ("limit", Json.obj [("type", "integer"), ("default", 10)])
|
||||
])
|
||||
] }
|
||||
|
||||
def eneSync : ToolSpec :=
|
||||
{ name := "ene_sync"
|
||||
description := "Run or dry-run ene-session-sync commands."
|
||||
inputSchema := Json.obj
|
||||
[ ("type", "object")
|
||||
, ("properties", Json.obj
|
||||
[ ("command", Json.obj [("type", "string"), ("default", "list")])
|
||||
, ("dry_run", Json.obj [("type", "boolean"), ("default", true)])
|
||||
, ("limit", Json.obj [("type", "integer"), ("default", 20)])
|
||||
, ("embed", Json.obj [("type", "boolean"), ("default", false)])
|
||||
])
|
||||
] }
|
||||
|
||||
def tools : List ToolSpec :=
|
||||
[ eneStatus
|
||||
, eneRemember
|
||||
, eneRecall
|
||||
, eneSearch
|
||||
, eneContext
|
||||
, eneSessions
|
||||
, eneSync
|
||||
]
|
||||
|
||||
/-- JSON `{ tools: [...] }` payload for MCP `tools/list`. -/
|
||||
def toolsJson : Json :=
|
||||
toJsonToolsList tools
|
||||
|
||||
end Semantics.EneContextSurface
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import Semantics.JsonLSurfaceConnector
|
||||
import Lean.Data.Json
|
||||
|
||||
namespace Semantics.McpSurfaceManifest
|
||||
|
||||
open Lean
|
||||
open Semantics.JsonLSurfaceConnector
|
||||
|
||||
/-!
|
||||
Lean-first MCP surface manifest.
|
||||
|
||||
This file defines the *published* tool list and per-tool input schemas for the
|
||||
JsonL surface connector. Shims may expose these tools over MCP, but MUST NOT
|
||||
invent tool names or schemas.
|
||||
-/
|
||||
|
||||
structure JsonSchema where
|
||||
schema : Json
|
||||
deriving Repr, DecidableEq
|
||||
|
||||
structure ToolSpec where
|
||||
name : String
|
||||
description : String
|
||||
inputSchema : JsonSchema
|
||||
deriving Repr, DecidableEq
|
||||
|
||||
/-- Minimal JSON schema for a single JsonL line payload. -/
|
||||
def jsonlLineSchema : JsonSchema :=
|
||||
{ schema := Json.mkObj [
|
||||
("type", Json.str "object"),
|
||||
("properties", Json.mkObj [
|
||||
("line", Json.mkObj [("type", Json.str "string")])
|
||||
]),
|
||||
("required", Json.arr #[Json.str "line"]),
|
||||
("additionalProperties", Json.bool false)
|
||||
] }
|
||||
|
||||
/-- Minimal JSON schema for a connector-health request. -/
|
||||
def connectorHealthSchema : JsonSchema :=
|
||||
{ schema := Json.mkObj [
|
||||
("type", Json.str "object"),
|
||||
("properties", Json.mkObj []),
|
||||
("additionalProperties", Json.bool false)
|
||||
] }
|
||||
|
||||
/-- Tool specs indexed by the Lean McpTool enum. -/
|
||||
def toolSpec : McpTool → ToolSpec
|
||||
| .appendJsonL =>
|
||||
{ name := McpTool.toName .appendJsonL
|
||||
description := "Append one JSONL event line to the configured surface target."
|
||||
inputSchema := jsonlLineSchema }
|
||||
| .attestJsonL =>
|
||||
{ name := McpTool.toName .attestJsonL
|
||||
description := "Attest a JSONL event line (deterministic hash + provenance chain policy)."
|
||||
inputSchema := jsonlLineSchema }
|
||||
| .surfaceSync =>
|
||||
{ name := McpTool.toName .surfaceSync
|
||||
description := "Trigger a surface sync for a configured target (adapter-owned transport; Lean-owned policy)."
|
||||
inputSchema := connectorHealthSchema }
|
||||
| .connectorHealth =>
|
||||
{ name := McpTool.toName .connectorHealth
|
||||
description := "Return connector manifest + readiness state."
|
||||
inputSchema := connectorHealthSchema }
|
||||
|
||||
/-- Published tool list for this instance. -/
|
||||
def publishedTools (c : SurfaceConnector := instanceConnector) : List ToolSpec :=
|
||||
c.tools.map toolSpec
|
||||
|
||||
/-- Emit the MCP `tools/list` JSON payload. -/
|
||||
def toJsonToolsList (tools : List ToolSpec) : Json :=
|
||||
Json.mkObj [
|
||||
("tools", Json.arr <|
|
||||
(tools.map (fun t =>
|
||||
Json.mkObj [
|
||||
("name", Json.str t.name),
|
||||
("description", Json.str t.description),
|
||||
("inputSchema", t.inputSchema.schema)
|
||||
])).toArray
|
||||
)
|
||||
]
|
||||
|
||||
/-- The manifest JSON for the current instance connector. -/
|
||||
def instanceToolsJson : Json :=
|
||||
toJsonToolsList (publishedTools instanceConnector)
|
||||
|
||||
-- Witness: tool list must include connector_health.
|
||||
theorem toolsIncludeConnectorHealth :
|
||||
(publishedTools instanceConnector).any (fun t => t.name = "connector_health") = true := by
|
||||
rfl
|
||||
|
||||
#eval instanceToolsJson
|
||||
|
||||
end Semantics.McpSurfaceManifest
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
//! ENE context tool dispatch (shim-only).
|
||||
//!
|
||||
//! Lean owns the *surface* (tool names + schemas). This module implements a
|
||||
//! minimal execution adapter: dispatch each ENE context tool call to a
|
||||
//! command provided via environment variables.
|
||||
//!
|
||||
//! This keeps Rust lean-first compliant:
|
||||
//! - no semantics in Rust
|
||||
//! - no invented tools
|
||||
//! - execution is purely boundary I/O
|
||||
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
/// All ENE context tool names (must match Lean `Semantics.EneContextSurface`).
|
||||
pub const ENE_CONTEXT_TOOL_NAMES: &[&str] = &[
|
||||
"ene_status",
|
||||
"ene_remember",
|
||||
"ene_recall",
|
||||
"ene_search",
|
||||
"ene_context",
|
||||
"ene_sessions",
|
||||
"ene_sync",
|
||||
];
|
||||
|
||||
fn env_key_for_tool(tool_name: &str) -> Option<&'static str> {
|
||||
match tool_name {
|
||||
"ene_status" => Some("CLAW_ENE_STATUS_CMD"),
|
||||
"ene_remember" => Some("CLAW_ENE_REMEMBER_CMD"),
|
||||
"ene_recall" => Some("CLAW_ENE_RECALL_CMD"),
|
||||
"ene_search" => Some("CLAW_ENE_SEARCH_CMD"),
|
||||
"ene_context" => Some("CLAW_ENE_CONTEXT_CMD"),
|
||||
"ene_sessions" => Some("CLAW_ENE_SESSIONS_CMD"),
|
||||
"ene_sync" => Some("CLAW_ENE_SYNC_CMD"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cmd(json: &str, env_key: &str) -> Result<Vec<String>, String> {
|
||||
serde_json::from_str::<Vec<String>>(json)
|
||||
.map_err(|error| format!("{env_key} must be JSON array of strings: {error}"))
|
||||
}
|
||||
|
||||
/// Dispatch a Lean-defined ENE context tool call.
|
||||
///
|
||||
/// Each tool is executed by spawning a command specified as a JSON argv array
|
||||
/// in an env var. The tool `arguments` JSON is written to stdin.
|
||||
///
|
||||
/// The command must emit either:
|
||||
/// - JSON on stdout (preferred), or
|
||||
/// - plain text (which will be wrapped).
|
||||
///
|
||||
/// Return value is always a string (the MCP server wraps it as text content).
|
||||
pub fn dispatch(tool_name: &str, arguments: &JsonValue) -> Result<String, String> {
|
||||
let env_key = env_key_for_tool(tool_name)
|
||||
.ok_or_else(|| format!("unknown ENE context tool: {tool_name}"))?;
|
||||
|
||||
let cmd_json = std::env::var(env_key)
|
||||
.map_err(|_| format!("missing env var {env_key} for tool {tool_name}"))?;
|
||||
let argv = parse_cmd(&cmd_json, env_key)?;
|
||||
if argv.is_empty() {
|
||||
return Err(format!("{env_key} is empty"));
|
||||
}
|
||||
|
||||
let mut cmd = Command::new(&argv[0]);
|
||||
if argv.len() > 1 {
|
||||
cmd.args(&argv[1..]);
|
||||
}
|
||||
cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.map_err(|error| format!("failed to spawn tool command for {tool_name}: {error}"))?;
|
||||
|
||||
if let Some(stdin) = child.stdin.as_mut() {
|
||||
use std::io::Write;
|
||||
let payload = serde_json::to_vec(arguments)
|
||||
.map_err(|error| format!("failed to serialize tool arguments: {error}"))?;
|
||||
stdin
|
||||
.write_all(&payload)
|
||||
.map_err(|error| format!("failed to write tool stdin: {error}"))?;
|
||||
}
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.map_err(|error| format!("failed to wait for tool {tool_name}: {error}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"tool {tool_name} failed (status={}): {}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if stdout.is_empty() {
|
||||
return Ok("{}".to_string());
|
||||
}
|
||||
|
||||
Ok(stdout)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tool_list_is_stable() {
|
||||
assert!(ENE_CONTEXT_TOOL_NAMES.contains(&"ene_status"));
|
||||
assert!(env_key_for_tool("ene_status").is_some());
|
||||
assert!(env_key_for_tool("nope").is_none());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
//! Lean-first MCP manifest loader.
|
||||
//!
|
||||
//! This is a boundary shim: it loads the published MCP tool list from a Lean-owned
|
||||
//! JSON manifest (generated by Lean).
|
||||
//!
|
||||
//! Semantics are NOT defined here: this module only parses JSON into runtime
|
||||
//! `McpTool` descriptors.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use crate::mcp_stdio::McpTool;
|
||||
|
||||
/// Load tool descriptors from a JSON value shaped like `{ "tools": [...] }`.
|
||||
pub fn tools_from_json(value: &JsonValue) -> Result<Vec<McpTool>, String> {
|
||||
let tools = value
|
||||
.get("tools")
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or_else(|| "Lean MCP manifest missing 'tools' array".to_string())?;
|
||||
|
||||
let mut parsed = Vec::with_capacity(tools.len());
|
||||
for tool in tools {
|
||||
let name = tool
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| "Lean MCP manifest tool missing 'name'".to_string())?
|
||||
.to_string();
|
||||
let description = tool
|
||||
.get("description")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
let input_schema = tool.get("inputSchema").cloned();
|
||||
|
||||
parsed.push(McpTool {
|
||||
name,
|
||||
description,
|
||||
input_schema,
|
||||
annotations: None,
|
||||
meta: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
/// Run an external command that prints the Lean-owned tools list JSON to stdout.
|
||||
///
|
||||
/// The command is provided as a vector to avoid shell injection.
|
||||
pub fn tools_from_lean_command(command: &[String]) -> Result<Vec<McpTool>, String> {
|
||||
if command.is_empty() {
|
||||
return Err("Lean MCP manifest command is empty".to_string());
|
||||
}
|
||||
|
||||
let mut cmd = Command::new(&command[0]);
|
||||
if command.len() > 1 {
|
||||
cmd.args(&command[1..]);
|
||||
}
|
||||
|
||||
let output = cmd
|
||||
.output()
|
||||
.map_err(|error| format!("failed to spawn Lean MCP manifest command: {error}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"Lean MCP manifest command failed (status={}): {}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let json: JsonValue = serde_json::from_slice(&output.stdout)
|
||||
.map_err(|error| format!("failed to parse Lean MCP manifest JSON: {error}"))?;
|
||||
|
||||
tools_from_json(&json)
|
||||
}
|
||||
|
||||
/// Load the Lean manifest using environment variables.
|
||||
///
|
||||
/// - `CLAW_LEAN_MCP_MANIFEST_CMD`: a JSON array of strings.
|
||||
///
|
||||
/// Example (ENE context surface):
|
||||
/// `["bash","-lc","cd 0-Core-Formalism/otom/tools/lean/Semantics && lake env lean -R Semantics -E 'import Semantics.EneContextSurface; open Semantics.EneContextSurface; #eval toolsJson'"]`
|
||||
///
|
||||
/// This stays shim-only: the env var provides *how* to invoke Lean; the manifest
|
||||
/// itself is the tool truth.
|
||||
pub fn tools_from_env() -> Result<Option<Vec<McpTool>>, String> {
|
||||
let cmd = std::env::var("CLAW_LEAN_MCP_MANIFEST_CMD").ok();
|
||||
let Some(cmd) = cmd else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let command: Vec<String> = serde_json::from_str(&cmd)
|
||||
.map_err(|error| format!("CLAW_LEAN_MCP_MANIFEST_CMD must be JSON array: {error}"))?;
|
||||
|
||||
Ok(Some(tools_from_lean_command(&command)?))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn parses_tools_from_lean_json_shape() {
|
||||
let manifest = json!({
|
||||
"tools": [
|
||||
{
|
||||
"name": "connector_health",
|
||||
"description": "Return connector manifest + readiness state.",
|
||||
"inputSchema": {"type": "object", "properties": {}}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let tools = tools_from_json(&manifest).expect("tools parse");
|
||||
assert_eq!(tools.len(), 1);
|
||||
assert_eq!(tools[0].name, "connector_health");
|
||||
assert!(tools[0].input_schema.is_some());
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,9 @@ mod mcp_client;
|
|||
pub mod mcp_lifecycle_hardened;
|
||||
pub mod mcp_server;
|
||||
mod mcp_stdio;
|
||||
mod lean_mcp_manifest;
|
||||
mod ene_context_tools;
|
||||
mod mcp_surface_router;
|
||||
pub mod mcp_tool_bridge;
|
||||
mod oauth;
|
||||
pub mod permission_enforcer;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ use tokio::io::{
|
|||
stdin, stdout, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, Stdin, Stdout,
|
||||
};
|
||||
|
||||
use crate::lean_mcp_manifest;
|
||||
use crate::mcp_stdio::{
|
||||
JsonRpcError, JsonRpcId, JsonRpcRequest, JsonRpcResponse, McpInitializeResult,
|
||||
McpInitializeServerInfo, McpListToolsResult, McpTool, McpToolCallContent, McpToolCallParams,
|
||||
|
|
@ -53,11 +54,28 @@ pub struct McpServerSpec {
|
|||
/// response.
|
||||
pub server_version: String,
|
||||
/// Tool descriptors returned for `tools/list`.
|
||||
///
|
||||
/// If empty, the server will attempt to load the tool list from a Lean-owned
|
||||
/// manifest via `CLAW_LEAN_MCP_MANIFEST_CMD`.
|
||||
pub tools: Vec<McpTool>,
|
||||
/// Handler invoked for `tools/call`.
|
||||
pub tool_handler: ToolCallHandler,
|
||||
}
|
||||
|
||||
impl McpServerSpec {
|
||||
fn resolved_tools(&self) -> Vec<McpTool> {
|
||||
if !self.tools.is_empty() {
|
||||
return self.tools.clone();
|
||||
}
|
||||
|
||||
match lean_mcp_manifest::tools_from_env() {
|
||||
Ok(Some(tools)) => tools,
|
||||
Ok(None) => Vec::new(),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal MCP stdio server.
|
||||
///
|
||||
/// The server runs a blocking read/dispatch/write loop over the current
|
||||
|
|
@ -178,7 +196,7 @@ impl McpServer {
|
|||
|
||||
fn handle_tools_list(&self, id: JsonRpcId) -> JsonRpcResponse<JsonValue> {
|
||||
let result = McpListToolsResult {
|
||||
tools: self.spec.tools.clone(),
|
||||
tools: self.spec.resolved_tools(),
|
||||
next_cursor: None,
|
||||
};
|
||||
JsonRpcResponse {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
//! Generic execution router for Lean-owned MCP tool surfaces.
|
||||
//!
|
||||
//! Lean owns tool names + schemas. This module provides a *generic* shim to
|
||||
//! execute Lean-defined tools by dispatching to commands declared in an
|
||||
//! environment-controlled map.
|
||||
//!
|
||||
//! - No tool semantics live here.
|
||||
//! - Routing is data-driven (`CLAW_MCP_TOOL_CMD_MAP`).
|
||||
//! - Commands receive the tool `arguments` JSON on stdin.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ToolCommandMap {
|
||||
map: BTreeMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
impl ToolCommandMap {
|
||||
pub fn from_env() -> Result<Option<Self>, String> {
|
||||
let raw = std::env::var("CLAW_MCP_TOOL_CMD_MAP").ok();
|
||||
let Some(raw) = raw else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let map: BTreeMap<String, Vec<String>> = serde_json::from_str(&raw)
|
||||
.map_err(|error| format!("CLAW_MCP_TOOL_CMD_MAP must be JSON object: {error}"))?;
|
||||
Ok(Some(Self { map }))
|
||||
}
|
||||
|
||||
pub fn command_for(&self, tool_name: &str) -> Option<&[String]> {
|
||||
self.map.get(tool_name).map(Vec::as_slice)
|
||||
}
|
||||
|
||||
pub fn handles(&self, tool_name: &str) -> bool {
|
||||
self.map.contains_key(tool_name)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch_with_map(
|
||||
map: &ToolCommandMap,
|
||||
tool_name: &str,
|
||||
arguments: &JsonValue,
|
||||
) -> Result<String, String> {
|
||||
let argv = map
|
||||
.command_for(tool_name)
|
||||
.ok_or_else(|| format!("no command mapping for tool: {tool_name}"))?;
|
||||
if argv.is_empty() {
|
||||
return Err(format!("empty command mapping for tool: {tool_name}"));
|
||||
}
|
||||
|
||||
let mut cmd = Command::new(&argv[0]);
|
||||
if argv.len() > 1 {
|
||||
cmd.args(&argv[1..]);
|
||||
}
|
||||
cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.map_err(|error| format!("failed to spawn mapped command for {tool_name}: {error}"))?;
|
||||
|
||||
if let Some(stdin) = child.stdin.as_mut() {
|
||||
use std::io::Write;
|
||||
let payload = serde_json::to_vec(arguments)
|
||||
.map_err(|error| format!("failed to serialize tool arguments: {error}"))?;
|
||||
stdin
|
||||
.write_all(&payload)
|
||||
.map_err(|error| format!("failed to write tool stdin: {error}"))?;
|
||||
}
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.map_err(|error| format!("failed to wait for tool {tool_name}: {error}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"mapped tool {tool_name} failed (status={}): {}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if stdout.is_empty() {
|
||||
return Ok("{}".to_string());
|
||||
}
|
||||
|
||||
Ok(stdout)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn env_map_parses() {
|
||||
let _lock = crate::test_env_lock();
|
||||
std::env::set_var(
|
||||
"CLAW_MCP_TOOL_CMD_MAP",
|
||||
r#"{ "ene_status": ["echo", "ok"] }"#,
|
||||
);
|
||||
let map = ToolCommandMap::from_env().expect("map load").expect("present");
|
||||
assert!(map.handles("ene_status"));
|
||||
assert!(!map.handles("missing"));
|
||||
std::env::remove_var("CLAW_MCP_TOOL_CMD_MAP");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,86 @@
|
|||
-- Canonical ENE chat/session sync schema
|
||||
--
|
||||
-- This schema is consumed by:
|
||||
-- - 4-Infrastructure/infra/ene-session-sync (Rust)
|
||||
--
|
||||
-- Policy:
|
||||
-- - treat this file as the canonical DDL source for these tables
|
||||
-- - code should not embed divergent CREATE TABLE strings
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS ene;
|
||||
|
||||
-- Optional extension (non-fatal if unavailable).
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ene.chat_sessions (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
title TEXT,
|
||||
agent TEXT,
|
||||
model TEXT,
|
||||
workspace_fingerprint TEXT,
|
||||
workspace_root TEXT,
|
||||
fork_parent_session_id TEXT,
|
||||
compaction_count INTEGER NOT NULL DEFAULT 0,
|
||||
compaction_summary TEXT,
|
||||
message_count INTEGER NOT NULL DEFAULT 0,
|
||||
token_input_total BIGINT NOT NULL DEFAULT 0,
|
||||
token_output_total BIGINT NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
first_message_at_ms BIGINT,
|
||||
last_message_at_ms BIGINT,
|
||||
embedding vector(768),
|
||||
meta JSONB NOT NULL DEFAULT '{}',
|
||||
receipt TEXT,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ene.chat_messages (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
session_id TEXT NOT NULL REFERENCES ene.chat_sessions(session_id) ON DELETE CASCADE,
|
||||
message_index INTEGER NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
blocks JSONB NOT NULL,
|
||||
text_content TEXT,
|
||||
token_input BIGINT NOT NULL DEFAULT 0,
|
||||
token_output BIGINT NOT NULL DEFAULT 0,
|
||||
token_cache_creation BIGINT NOT NULL DEFAULT 0,
|
||||
token_cache_read BIGINT NOT NULL DEFAULT 0,
|
||||
tool_calls JSONB NOT NULL DEFAULT '[]',
|
||||
embedding vector(768),
|
||||
receipt_hash TEXT,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE(session_id, message_index)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ene.ingestion_receipts (
|
||||
receipt_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
shim_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
sha256 TEXT NOT NULL,
|
||||
record_count BIGINT NOT NULL DEFAULT 0,
|
||||
source_path TEXT NOT NULL,
|
||||
meta JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Idempotent migration helpers.
|
||||
ALTER TABLE ene.chat_sessions ADD COLUMN IF NOT EXISTS title TEXT;
|
||||
ALTER TABLE ene.chat_sessions ADD COLUMN IF NOT EXISTS agent TEXT;
|
||||
ALTER TABLE ene.chat_sessions ADD COLUMN IF NOT EXISTS model TEXT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_sessions_updated
|
||||
ON ene.chat_sessions(updated_at_ms DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_sessions_workspace
|
||||
ON ene.chat_sessions(workspace_fingerprint);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_messages_session_order
|
||||
ON ene.chat_messages(session_id, message_index);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_messages_receipt
|
||||
ON ene.chat_messages(receipt_hash);
|
||||
|
||||
-- Optional indexes (non-fatal if missing language config).
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_messages_text_search
|
||||
ON ene.chat_messages USING GIN(to_tsvector('english', COALESCE(text_content, '')));
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_messages_tool_search
|
||||
ON ene.chat_messages USING GIN(tool_calls jsonb_path_ops);
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::models::{ChatMessage, ChatSession, IngestionReceipt};
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::Path;
|
||||
use tokio_postgres::{Client, Config, NoTls};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
|
|
@ -54,93 +55,13 @@ impl RdsSink {
|
|||
|
||||
/// Create tables and indexes if they do not exist.
|
||||
async fn init_tables(&self) -> Result<()> {
|
||||
// Try to enable pgvector — harmless if already enabled or unavailable.
|
||||
let _ = self
|
||||
.client
|
||||
.batch_execute("CREATE EXTENSION IF NOT EXISTS vector")
|
||||
.await;
|
||||
// Canonical DDL lives in sql/ene_chat_schema.sql.
|
||||
// Keeping it out of Rust source reduces schema drift across services.
|
||||
let ddl_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("sql/ene_chat_schema.sql");
|
||||
let ddl = std::fs::read_to_string(&ddl_path)
|
||||
.with_context(|| format!("read ENE chat schema DDL at {:?}", ddl_path))?;
|
||||
|
||||
let ddl = r#"
|
||||
CREATE TABLE IF NOT EXISTS ene.chat_sessions (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
title TEXT,
|
||||
agent TEXT,
|
||||
model TEXT,
|
||||
workspace_fingerprint TEXT,
|
||||
workspace_root TEXT,
|
||||
fork_parent_session_id TEXT,
|
||||
compaction_count INTEGER NOT NULL DEFAULT 0,
|
||||
compaction_summary TEXT,
|
||||
message_count INTEGER NOT NULL DEFAULT 0,
|
||||
token_input_total BIGINT NOT NULL DEFAULT 0,
|
||||
token_output_total BIGINT NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
first_message_at_ms BIGINT,
|
||||
last_message_at_ms BIGINT,
|
||||
embedding vector(768),
|
||||
meta JSONB NOT NULL DEFAULT '{}',
|
||||
receipt TEXT,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ene.chat_messages (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
session_id TEXT NOT NULL REFERENCES ene.chat_sessions(session_id) ON DELETE CASCADE,
|
||||
message_index INTEGER NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
blocks JSONB NOT NULL,
|
||||
text_content TEXT,
|
||||
token_input BIGINT NOT NULL DEFAULT 0,
|
||||
token_output BIGINT NOT NULL DEFAULT 0,
|
||||
token_cache_creation BIGINT NOT NULL DEFAULT 0,
|
||||
token_cache_read BIGINT NOT NULL DEFAULT 0,
|
||||
tool_calls JSONB NOT NULL DEFAULT '[]',
|
||||
embedding vector(768),
|
||||
receipt_hash TEXT,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE(session_id, message_index)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ene.ingestion_receipts (
|
||||
receipt_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
shim_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
sha256 TEXT NOT NULL,
|
||||
record_count BIGINT NOT NULL DEFAULT 0,
|
||||
source_path TEXT NOT NULL,
|
||||
meta JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Columns may already exist after a schema migration; ADD COLUMN IF NOT EXISTS
|
||||
-- is idempotent on PostgreSQL ≥ 9.6.
|
||||
ALTER TABLE ene.chat_sessions ADD COLUMN IF NOT EXISTS title TEXT;
|
||||
ALTER TABLE ene.chat_sessions ADD COLUMN IF NOT EXISTS agent TEXT;
|
||||
ALTER TABLE ene.chat_sessions ADD COLUMN IF NOT EXISTS model TEXT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_sessions_updated
|
||||
ON ene.chat_sessions(updated_at_ms DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_sessions_workspace
|
||||
ON ene.chat_sessions(workspace_fingerprint);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_messages_session_order
|
||||
ON ene.chat_messages(session_id, message_index);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_messages_receipt
|
||||
ON ene.chat_messages(receipt_hash);
|
||||
"#;
|
||||
// Full-text and JSONB indexes require non-empty text_content / tool_calls;
|
||||
// create them separately so a pgvector-missing cluster still gets the rest.
|
||||
let fts_ddl = r#"
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_messages_text_search
|
||||
ON ene.chat_messages USING GIN(to_tsvector('english', COALESCE(text_content, '')));
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_messages_tool_search
|
||||
ON ene.chat_messages USING GIN(tool_calls jsonb_path_ops);
|
||||
"#;
|
||||
self.client.batch_execute(ddl).await.context("init DDL")?;
|
||||
if let Err(e) = self.client.batch_execute(fts_ddl).await {
|
||||
warn!("FTS index creation skipped (non-fatal): {}", e);
|
||||
}
|
||||
self.client.batch_execute(&ddl).await.context("init DDL")?;
|
||||
info!("RDS schema initialized");
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -159,32 +80,32 @@ CREATE INDEX IF NOT EXISTS idx_chat_messages_tool_search
|
|||
let meta_json = serde_json::to_value(&s.meta)?;
|
||||
self.client
|
||||
.execute(
|
||||
"INSERT INTO ene.chat_sessions \
|
||||
(session_id, title, agent, model, \
|
||||
workspace_fingerprint, workspace_root, fork_parent_session_id, \
|
||||
compaction_count, compaction_summary, message_count, \
|
||||
token_input_total, token_output_total, \
|
||||
created_at_ms, updated_at_ms, first_message_at_ms, last_message_at_ms, \
|
||||
embedding, meta, receipt, updated_at) \
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17::vector,$18,$19,now()) \
|
||||
ON CONFLICT (session_id) DO UPDATE SET \
|
||||
title = EXCLUDED.title, \
|
||||
agent = EXCLUDED.agent, \
|
||||
model = EXCLUDED.model, \
|
||||
workspace_fingerprint = EXCLUDED.workspace_fingerprint, \
|
||||
workspace_root = EXCLUDED.workspace_root, \
|
||||
fork_parent_session_id = EXCLUDED.fork_parent_session_id, \
|
||||
compaction_count = EXCLUDED.compaction_count, \
|
||||
compaction_summary = EXCLUDED.compaction_summary, \
|
||||
message_count = EXCLUDED.message_count, \
|
||||
token_input_total = EXCLUDED.token_input_total, \
|
||||
token_output_total = EXCLUDED.token_output_total, \
|
||||
updated_at_ms = EXCLUDED.updated_at_ms, \
|
||||
first_message_at_ms = EXCLUDED.first_message_at_ms, \
|
||||
last_message_at_ms = EXCLUDED.last_message_at_ms, \
|
||||
embedding = EXCLUDED.embedding, \
|
||||
meta = EXCLUDED.meta, \
|
||||
receipt = EXCLUDED.receipt, \
|
||||
"INSERT INTO ene.chat_sessions \\
|
||||
(session_id, title, agent, model, \\
|
||||
workspace_fingerprint, workspace_root, fork_parent_session_id, \\
|
||||
compaction_count, compaction_summary, message_count, \\
|
||||
token_input_total, token_output_total, \\
|
||||
created_at_ms, updated_at_ms, first_message_at_ms, last_message_at_ms, \\
|
||||
embedding, meta, receipt, updated_at) \\
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17::vector,$18,$19,now()) \\
|
||||
ON CONFLICT (session_id) DO UPDATE SET \\
|
||||
title = EXCLUDED.title, \\
|
||||
agent = EXCLUDED.agent, \\
|
||||
model = EXCLUDED.model, \\
|
||||
workspace_fingerprint = EXCLUDED.workspace_fingerprint, \\
|
||||
workspace_root = EXCLUDED.workspace_root, \\
|
||||
fork_parent_session_id = EXCLUDED.fork_parent_session_id, \\
|
||||
compaction_count = EXCLUDED.compaction_count, \\
|
||||
compaction_summary = EXCLUDED.compaction_summary, \\
|
||||
message_count = EXCLUDED.message_count, \\
|
||||
token_input_total = EXCLUDED.token_input_total, \\
|
||||
token_output_total = EXCLUDED.token_output_total, \\
|
||||
updated_at_ms = EXCLUDED.updated_at_ms, \\
|
||||
first_message_at_ms = EXCLUDED.first_message_at_ms, \\
|
||||
last_message_at_ms = EXCLUDED.last_message_at_ms, \\
|
||||
embedding = EXCLUDED.embedding, \\
|
||||
meta = EXCLUDED.meta, \\
|
||||
receipt = EXCLUDED.receipt, \\
|
||||
updated_at = now()",
|
||||
&[
|
||||
&s.session_id,
|
||||
|
|
@ -230,22 +151,22 @@ CREATE INDEX IF NOT EXISTS idx_chat_messages_tool_search
|
|||
let tool_calls_json = serde_json::to_value(&msg.tool_calls)?;
|
||||
self.client
|
||||
.execute(
|
||||
"INSERT INTO ene.chat_messages \
|
||||
(session_id, message_index, role, blocks, text_content, \
|
||||
token_input, token_output, token_cache_creation, token_cache_read, \
|
||||
tool_calls, embedding, receipt_hash, created_at_ms, created_at) \
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::vector,$12,$13,now()) \
|
||||
ON CONFLICT (session_id, message_index) DO UPDATE SET \
|
||||
role = EXCLUDED.role, \
|
||||
blocks = EXCLUDED.blocks, \
|
||||
text_content = EXCLUDED.text_content, \
|
||||
token_input = EXCLUDED.token_input, \
|
||||
token_output = EXCLUDED.token_output, \
|
||||
token_cache_creation = EXCLUDED.token_cache_creation, \
|
||||
token_cache_read = EXCLUDED.token_cache_read, \
|
||||
tool_calls = EXCLUDED.tool_calls, \
|
||||
embedding = EXCLUDED.embedding, \
|
||||
receipt_hash = EXCLUDED.receipt_hash, \
|
||||
"INSERT INTO ene.chat_messages \\
|
||||
(session_id, message_index, role, blocks, text_content, \\
|
||||
token_input, token_output, token_cache_creation, token_cache_read, \\
|
||||
tool_calls, embedding, receipt_hash, created_at_ms, created_at) \\
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::vector,$12,$13,now()) \\
|
||||
ON CONFLICT (session_id, message_index) DO UPDATE SET \\
|
||||
role = EXCLUDED.role, \\
|
||||
blocks = EXCLUDED.blocks, \\
|
||||
text_content = EXCLUDED.text_content, \\
|
||||
token_input = EXCLUDED.token_input, \\
|
||||
token_output = EXCLUDED.token_output, \\
|
||||
token_cache_creation = EXCLUDED.token_cache_creation, \\
|
||||
token_cache_read = EXCLUDED.token_cache_read, \\
|
||||
tool_calls = EXCLUDED.tool_calls, \\
|
||||
embedding = EXCLUDED.embedding, \\
|
||||
receipt_hash = EXCLUDED.receipt_hash, \\
|
||||
created_at_ms = EXCLUDED.created_at_ms",
|
||||
&[
|
||||
&session_id,
|
||||
|
|
@ -297,8 +218,8 @@ CREATE INDEX IF NOT EXISTS idx_chat_messages_tool_search
|
|||
let meta_json = serde_json::to_value(&r.meta)?;
|
||||
self.client
|
||||
.execute(
|
||||
"INSERT INTO ene.ingestion_receipts \
|
||||
(shim_name, status, sha256, record_count, source_path, meta) \
|
||||
"INSERT INTO ene.ingestion_receipts \\
|
||||
(shim_name, status, sha256, record_count, source_path, meta) \\
|
||||
VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
&[
|
||||
&r.shim_name,
|
||||
|
|
@ -323,16 +244,16 @@ CREATE INDEX IF NOT EXISTS idx_chat_messages_tool_search
|
|||
let rows = self
|
||||
.client
|
||||
.query(
|
||||
"SELECT s.session_id, s.title, s.agent, s.model, \
|
||||
COUNT(m.id) AS match_count, \
|
||||
MAX(ts_rank(to_tsvector('english', COALESCE(m.text_content,'')), \
|
||||
plainto_tsquery('english', $1))) AS rank \
|
||||
FROM ene.chat_sessions s \
|
||||
JOIN ene.chat_messages m ON m.session_id = s.session_id \
|
||||
WHERE to_tsvector('english', COALESCE(m.text_content,'')) \
|
||||
@@ plainto_tsquery('english', $1) \
|
||||
GROUP BY s.session_id, s.title, s.agent, s.model \
|
||||
ORDER BY rank DESC \
|
||||
"SELECT s.session_id, s.title, s.agent, s.model, \\
|
||||
COUNT(m.id) AS match_count, \\
|
||||
MAX(ts_rank(to_tsvector('english', COALESCE(m.text_content,'')), \\
|
||||
plainto_tsquery('english', $1))) AS rank \\
|
||||
FROM ene.chat_sessions s \\
|
||||
JOIN ene.chat_messages m ON m.session_id = s.session_id \\
|
||||
WHERE to_tsvector('english', COALESCE(m.text_content,'')) \\
|
||||
@@ plainto_tsquery('english', $1) \\
|
||||
GROUP BY s.session_id, s.title, s.agent, s.model \\
|
||||
ORDER BY rank DESC \\
|
||||
LIMIT $2",
|
||||
&[&query, &limit],
|
||||
)
|
||||
|
|
@ -369,11 +290,11 @@ CREATE INDEX IF NOT EXISTS idx_chat_messages_tool_search
|
|||
let rows = self
|
||||
.client
|
||||
.query(
|
||||
"SELECT session_id, title, agent, model, \
|
||||
1 - (embedding <=> $1::vector) AS similarity \
|
||||
FROM ene.chat_sessions \
|
||||
WHERE embedding IS NOT NULL \
|
||||
ORDER BY embedding <=> $1::vector \
|
||||
"SELECT session_id, title, agent, model, \\
|
||||
1 - (embedding <=> $1::vector) AS similarity \\
|
||||
FROM ene.chat_sessions \\
|
||||
WHERE embedding IS NOT NULL \\
|
||||
ORDER BY embedding <=> $1::vector \\
|
||||
LIMIT $2",
|
||||
&[&vec_str, &limit],
|
||||
)
|
||||
|
|
@ -397,10 +318,10 @@ CREATE INDEX IF NOT EXISTS idx_chat_messages_tool_search
|
|||
let rows = self
|
||||
.client
|
||||
.query(
|
||||
"SELECT session_id, title, agent, model, message_count, \
|
||||
token_input_total, token_output_total, created_at_ms, updated_at_ms \
|
||||
FROM ene.chat_sessions \
|
||||
ORDER BY updated_at_ms DESC \
|
||||
"SELECT session_id, title, agent, model, message_count, \\
|
||||
token_input_total, token_output_total, created_at_ms, updated_at_ms \\
|
||||
FROM ene.chat_sessions \\
|
||||
ORDER BY updated_at_ms DESC \\
|
||||
LIMIT $1",
|
||||
&[&limit],
|
||||
)
|
||||
|
|
@ -428,8 +349,8 @@ CREATE INDEX IF NOT EXISTS idx_chat_messages_tool_search
|
|||
let Some(sess) = self
|
||||
.client
|
||||
.query_opt(
|
||||
"SELECT session_id, title, agent, model, message_count, \
|
||||
token_input_total, token_output_total, created_at_ms, updated_at_ms, meta \
|
||||
"SELECT session_id, title, agent, model, message_count, \\
|
||||
token_input_total, token_output_total, created_at_ms, updated_at_ms, meta \\
|
||||
FROM ene.chat_sessions WHERE session_id = $1",
|
||||
&[&session_id],
|
||||
)
|
||||
|
|
@ -442,9 +363,9 @@ CREATE INDEX IF NOT EXISTS idx_chat_messages_tool_search
|
|||
let msg_rows = self
|
||||
.client
|
||||
.query(
|
||||
"SELECT message_index, role, blocks, text_content, \
|
||||
token_input, token_output, tool_calls, created_at_ms \
|
||||
FROM ene.chat_messages \
|
||||
"SELECT message_index, role, blocks, text_content, \\
|
||||
token_input, token_output, tool_calls, created_at_ms \\
|
||||
FROM ene.chat_messages \\
|
||||
WHERE session_id = $1 ORDER BY message_index",
|
||||
&[&session_id],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,28 @@ pub const MAX_TITLE_LEN: usize = 160;
|
|||
/// Maximum allowed content size in bytes (256 KB).
|
||||
pub const MAX_CONTENT_BYTES: usize = 262_144;
|
||||
|
||||
// ── Node/provenance defaults (configurable) ────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ProvenanceDefaults {
|
||||
node: String,
|
||||
lake_seed: String,
|
||||
tailscale_ip: String,
|
||||
}
|
||||
|
||||
fn provenance_defaults() -> ProvenanceDefaults {
|
||||
// These defaults are intentionally conservative; callers should set env vars
|
||||
// in production.
|
||||
let node = std::env::var("ENE_NODE_ID").unwrap_or_else(|_| "ene".to_string());
|
||||
let lake_seed = std::env::var("ENE_LAKE_SEED").unwrap_or_else(|_| "local".to_string());
|
||||
let tailscale_ip = std::env::var("ENE_TAILSCALE_IP").unwrap_or_else(|_| "".to_string());
|
||||
ProvenanceDefaults {
|
||||
node,
|
||||
lake_seed,
|
||||
tailscale_ip,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Structs ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// A wiki page header (latest-revision summary, no body text).
|
||||
|
|
@ -49,10 +71,7 @@ pub struct WikiRevision {
|
|||
|
||||
/// Trim and collapse whitespace in a wiki title; reject empty or oversized titles.
|
||||
pub fn normalize_title(title: &str) -> Result<String> {
|
||||
let cleaned: String = title
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let cleaned: String = title.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
if cleaned.is_empty() {
|
||||
return Err(anyhow!("wiki title is required"));
|
||||
}
|
||||
|
|
@ -126,7 +145,6 @@ pub fn canonical_json(v: &Value) -> String {
|
|||
fn to_sorted(v: &Value) -> Value {
|
||||
match v {
|
||||
Value::Object(map) => {
|
||||
// Collect into a BTreeMap so keys are sorted lexicographically.
|
||||
let sorted: std::collections::BTreeMap<_, _> = map
|
||||
.iter()
|
||||
.map(|(k, vv)| (k.clone(), to_sorted(vv)))
|
||||
|
|
@ -145,7 +163,6 @@ pub fn canonical_json(v: &Value) -> String {
|
|||
serde_json::to_string(&sorted).unwrap_or_else(|_| "null".to_string())
|
||||
}
|
||||
|
||||
/// Manual `[[...]]` span parser — returns `(inner_text, start_byte)` for each span.
|
||||
fn wiki_spans(text: &str) -> Vec<&str> {
|
||||
let bytes = text.as_bytes();
|
||||
let len = bytes.len();
|
||||
|
|
@ -153,12 +170,10 @@ fn wiki_spans(text: &str) -> Vec<&str> {
|
|||
let mut i = 0;
|
||||
while i + 1 < len {
|
||||
if bytes[i] == b'[' && bytes[i + 1] == b'[' {
|
||||
// Find the matching `]]`.
|
||||
let start = i + 2;
|
||||
let mut j = start;
|
||||
while j + 1 < len {
|
||||
if bytes[j] == b']' && bytes[j + 1] == b']' {
|
||||
// Reject spans that contain newlines or nested brackets.
|
||||
let inner = &text[start..j];
|
||||
if !inner.contains('\n') && !inner.contains('[') && !inner.contains(']') {
|
||||
spans.push(inner);
|
||||
|
|
@ -169,7 +184,6 @@ fn wiki_spans(text: &str) -> Vec<&str> {
|
|||
j += 1;
|
||||
}
|
||||
if j + 1 >= len {
|
||||
// Unclosed span — advance past the opening `[[`.
|
||||
i += 2;
|
||||
}
|
||||
} else {
|
||||
|
|
@ -179,20 +193,11 @@ fn wiki_spans(text: &str) -> Vec<&str> {
|
|||
spans
|
||||
}
|
||||
|
||||
/// Extract the link target from a raw `[[...]]` inner string.
|
||||
///
|
||||
/// Strip everything from the first `|` or `#` onward.
|
||||
fn link_target(inner: &str) -> &str {
|
||||
let cut = inner
|
||||
.find(|c| c == '|' || c == '#')
|
||||
.unwrap_or(inner.len());
|
||||
let cut = inner.find(|c| c == '|' || c == '#').unwrap_or(inner.len());
|
||||
&inner[..cut]
|
||||
}
|
||||
|
||||
/// Extract all wiki links from `text`.
|
||||
///
|
||||
/// Returns deduplicated, case-insensitively sorted normalized titles.
|
||||
/// Skips spans whose target (after normalization) starts with `category:`.
|
||||
pub fn extract_links(text: &str) -> Vec<String> {
|
||||
let mut links: Vec<String> = Vec::new();
|
||||
for inner in wiki_spans(text) {
|
||||
|
|
@ -206,26 +211,18 @@ pub fn extract_links(text: &str) -> Vec<String> {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Deduplicate.
|
||||
links.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
|
||||
links.dedup_by(|a, b| a.to_lowercase() == b.to_lowercase());
|
||||
links
|
||||
}
|
||||
|
||||
/// Extract all `[[Category:...]]` entries from `text`.
|
||||
///
|
||||
/// Returns deduplicated, case-insensitively sorted category names (prefix stripped).
|
||||
pub fn extract_categories(text: &str) -> Vec<String> {
|
||||
let mut cats: Vec<String> = Vec::new();
|
||||
for inner in wiki_spans(text) {
|
||||
let target = link_target(inner).trim();
|
||||
// Accept `Category:Foo` or `category:foo` (case-insensitive prefix).
|
||||
let lower = target.to_lowercase();
|
||||
// The Python CATEGORY_RE also allows whitespace: `Category : Foo`.
|
||||
// We replicate that by checking the prefix after collapsing whitespace.
|
||||
let condensed: String = target.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
let cond_lower = condensed.to_lowercase();
|
||||
// Match `category :` or `category:` prefix.
|
||||
let remainder = if cond_lower.starts_with("category :") {
|
||||
Some(condensed["category :".len()..].trim().to_string())
|
||||
} else if lower.starts_with("category:") {
|
||||
|
|
@ -244,10 +241,6 @@ pub fn extract_categories(text: &str) -> Vec<String> {
|
|||
cats
|
||||
}
|
||||
|
||||
/// Compute the deterministic write receipt for a revision.
|
||||
///
|
||||
/// Receipt = SHA-256 hex of canonical JSON of
|
||||
/// `{"author":…,"created_at":…,"revision":…,"slug":…,"text_sha256":…}`.
|
||||
pub fn write_receipt(
|
||||
slug: &str,
|
||||
revision: i64,
|
||||
|
|
@ -256,7 +249,6 @@ pub fn write_receipt(
|
|||
created_at_ms: i64,
|
||||
) -> String {
|
||||
let text_sha256 = sha256_hex(text.as_bytes());
|
||||
// Build a BTreeMap so keys are sorted identically to Python's sort_keys=True.
|
||||
let mut map = std::collections::BTreeMap::new();
|
||||
map.insert("author", serde_json::Value::String(author.to_string()));
|
||||
map.insert(
|
||||
|
|
@ -273,7 +265,6 @@ pub fn write_receipt(
|
|||
sha256_hex(payload.as_bytes())
|
||||
}
|
||||
|
||||
/// Count occurrences of `needle` (as a plain substring) in `haystack`.
|
||||
#[inline]
|
||||
fn count_substr(haystack: &str, needle: &str) -> usize {
|
||||
if needle.is_empty() {
|
||||
|
|
@ -288,9 +279,6 @@ fn count_substr(haystack: &str, needle: &str) -> usize {
|
|||
count
|
||||
}
|
||||
|
||||
/// Count distinct "word-like" tokens matching `[a-zA-Z][a-zA-Z0-9_]{2,}`.
|
||||
///
|
||||
/// This replicates `len(set(re.findall(r"[a-zA-Z][a-zA-Z0-9_]{2,}", lowered)))`.
|
||||
fn distinct_word_tokens(text: &str) -> usize {
|
||||
let bytes = text.as_bytes();
|
||||
let len = bytes.len();
|
||||
|
|
@ -298,7 +286,6 @@ fn distinct_word_tokens(text: &str) -> usize {
|
|||
let mut i = 0;
|
||||
while i < len {
|
||||
let b = bytes[i];
|
||||
// Must start with a letter (a-z after lowering).
|
||||
if b.is_ascii_alphabetic() {
|
||||
let start = i;
|
||||
i += 1;
|
||||
|
|
@ -306,7 +293,6 @@ fn distinct_word_tokens(text: &str) -> usize {
|
|||
i += 1;
|
||||
}
|
||||
let word = &text[start..i];
|
||||
// Must be at least 3 characters (1 leading + at least 2 more).
|
||||
if word.len() >= 3 {
|
||||
tokens.insert(word.to_string());
|
||||
}
|
||||
|
|
@ -317,24 +303,7 @@ fn distinct_word_tokens(text: &str) -> usize {
|
|||
tokens.len()
|
||||
}
|
||||
|
||||
/// Derive a deterministic 14-axis concept vector for a wiki page.
|
||||
///
|
||||
/// Axes 0,1,3,4,8,9,10 are always 0.0. Active axes:
|
||||
/// - axis 2 : topology / manifold / links
|
||||
/// - axis 5 : hash / receipt / verify
|
||||
/// - axis 6 : sqlite / schema / index
|
||||
/// - axis 7 : lexical richness (distinct tokens / 500)
|
||||
/// - axis 11 : proof / lean / theorem
|
||||
/// - axis 12 : categories / archive / history
|
||||
/// - axis 13 : author / provenance / attest
|
||||
///
|
||||
/// The vector is L2-normalised then rounded to 6 decimal places.
|
||||
pub fn concept_vector_for_wiki(
|
||||
title: &str,
|
||||
text: &str,
|
||||
links: &[String],
|
||||
categories: &[String],
|
||||
) -> Vec<f64> {
|
||||
pub fn concept_vector_for_wiki(title: &str, text: &str, links: &[String], categories: &[String]) -> Vec<f64> {
|
||||
let combined = format!("{}\n{}", title, text).to_lowercase();
|
||||
let mut axes = vec![0.0f64; 14];
|
||||
|
||||
|
|
@ -382,31 +351,18 @@ pub fn concept_vector_for_wiki(
|
|||
/ 8.0,
|
||||
);
|
||||
|
||||
// If all axes are zero, set the lexical richness axis to 1.
|
||||
if axes.iter().all(|&x| x == 0.0) {
|
||||
axes[7] = 1.0;
|
||||
}
|
||||
|
||||
// L2 normalise.
|
||||
let norm = axes.iter().map(|x| x * x).sum::<f64>().sqrt();
|
||||
axes.iter()
|
||||
.map(|&x| {
|
||||
if norm > 0.0 {
|
||||
// Round to 6 decimal places.
|
||||
(x / norm * 1_000_000.0).round() / 1_000_000.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
})
|
||||
.map(|&x| if norm > 0.0 { (x / norm * 1_000_000.0).round() / 1_000_000.0 } else { 0.0 })
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Bin a concept vector into 0–7 genome integers and return the six named axes.
|
||||
pub fn genome_from_vector(v: &[f64]) -> Value {
|
||||
let bins: Vec<u8> = v
|
||||
.iter()
|
||||
.map(|&x| u8::min(7, (x * 8.0) as u8))
|
||||
.collect();
|
||||
let bins: Vec<u8> = v.iter().map(|&x| u8::min(7, (x * 8.0) as u8)).collect();
|
||||
let get = |idx: usize| -> i64 { bins.get(idx).copied().unwrap_or(0) as i64 };
|
||||
json!({
|
||||
"mu": get(1),
|
||||
|
|
@ -418,16 +374,10 @@ pub fn genome_from_vector(v: &[f64]) -> Value {
|
|||
})
|
||||
}
|
||||
|
||||
/// Build the archive_id string for a slug + content_hash pair.
|
||||
fn archive_id_for(slug: &str, content_hash: &str) -> String {
|
||||
format!(
|
||||
"json_catalog_ene_wiki_{}_{}",
|
||||
slug,
|
||||
&content_hash[..content_hash.len().min(16)]
|
||||
)
|
||||
format!("json_catalog_ene_wiki_{}_{}", slug, &content_hash[..content_hash.len().min(16)])
|
||||
}
|
||||
|
||||
/// Build the full archive record dict matching the Python `make_archive_record()`.
|
||||
pub fn make_archive_record(
|
||||
title: &str,
|
||||
slug: &str,
|
||||
|
|
@ -466,20 +416,15 @@ pub fn make_archive_record(
|
|||
})
|
||||
}
|
||||
|
||||
/// Build the JSONL event dict matching the Python `make_jsonl_event()`.
|
||||
pub fn make_jsonl_event(record: &Value, concept_vector: &[f64], receipt: &str) -> Value {
|
||||
let defaults = provenance_defaults();
|
||||
|
||||
let now_ms = now_unix_ms() as f64 / 1_000.0;
|
||||
let raw = &record["raw_content"];
|
||||
let slug = raw["slug"].as_str().unwrap_or("");
|
||||
let title = raw["title"].as_str().unwrap_or("");
|
||||
let links_len = raw["links"]
|
||||
.as_array()
|
||||
.map(|a| a.len())
|
||||
.unwrap_or(0);
|
||||
let categories_arr: Vec<Value> = raw["categories"]
|
||||
.as_array()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let links_len = raw["links"].as_array().map(|a| a.len()).unwrap_or(0);
|
||||
let categories_arr: Vec<Value> = raw["categories"].as_array().cloned().unwrap_or_default();
|
||||
let content_hash = record["content_hash"].as_str().unwrap_or("");
|
||||
let extracted_at = record["extracted_at"].as_str().unwrap_or("");
|
||||
let archive_id = record["archive_id"].as_str().unwrap_or("");
|
||||
|
|
@ -488,14 +433,10 @@ pub fn make_jsonl_event(record: &Value, concept_vector: &[f64], receipt: &str) -
|
|||
let pkg = format!("ene/wiki/{}", slug);
|
||||
let event_id = format!("ene:{}", archive_id);
|
||||
|
||||
// bind.cost = max(1, len(extracted_text.encode("utf-8"))) << 16
|
||||
let text_bytes = extracted_text.len().max(1);
|
||||
let cost: i64 = (text_bytes as i64) << 16;
|
||||
|
||||
let mut tags = vec![
|
||||
Value::String("ene".to_string()),
|
||||
Value::String("wiki".to_string()),
|
||||
];
|
||||
let mut tags = vec![Value::String("ene".to_string()), Value::String("wiki".to_string())];
|
||||
for cat in &categories_arr {
|
||||
tags.push(cat.clone());
|
||||
}
|
||||
|
|
@ -540,18 +481,15 @@ pub fn make_jsonl_event(record: &Value, concept_vector: &[f64], receipt: &str) -
|
|||
"class": "informational_bind",
|
||||
},
|
||||
"provenance": {
|
||||
"node": "ene-wiki-layer",
|
||||
"lake_seed": "local",
|
||||
"tailscale_ip": "",
|
||||
"node": defaults.node,
|
||||
"lake_seed": defaults.lake_seed,
|
||||
"tailscale_ip": defaults.tailscale_ip,
|
||||
"attestation_hash": format!("sha256:{}", receipt),
|
||||
"prev_id": Value::Null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if the text passes the content admission gate.
|
||||
///
|
||||
/// Rejects: oversized text, `<script` tags, `javascript:` URLs.
|
||||
pub fn admit_write(text: &str) -> bool {
|
||||
if text.len() >= MAX_CONTENT_BYTES {
|
||||
return false;
|
||||
|
|
@ -565,7 +503,6 @@ pub fn admit_write(text: &str) -> bool {
|
|||
true
|
||||
}
|
||||
|
||||
/// Current Unix time as milliseconds.
|
||||
fn now_unix_ms() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
|
|
@ -575,13 +512,11 @@ fn now_unix_ms() -> i64 {
|
|||
|
||||
// ── ENEWikiLayer (SQLite) ──────────────────────────────────────────────────────
|
||||
|
||||
/// SQLite-backed wiki layer. Drop-in equivalent of `ene_wiki_layer.ENEWikiLayer`.
|
||||
pub struct ENEWikiLayer {
|
||||
pub db_path: PathBuf,
|
||||
}
|
||||
|
||||
impl ENEWikiLayer {
|
||||
/// Open (or create) a wiki backed by the SQLite database at `db_path`.
|
||||
pub fn new(db_path: impl AsRef<Path>) -> Result<Self> {
|
||||
let db_path = db_path.as_ref().to_path_buf();
|
||||
if let Some(parent) = db_path.parent() {
|
||||
|
|
@ -593,7 +528,6 @@ impl ENEWikiLayer {
|
|||
Ok(layer)
|
||||
}
|
||||
|
||||
/// Open a raw SQLite connection to the backing database.
|
||||
fn open(&self) -> Result<rusqlite::Connection> {
|
||||
let conn = rusqlite::Connection::open(&self.db_path)
|
||||
.with_context(|| format!("opening sqlite db {:?}", self.db_path))?;
|
||||
|
|
@ -601,7 +535,6 @@ impl ENEWikiLayer {
|
|||
Ok(conn)
|
||||
}
|
||||
|
||||
/// Create all tables if they do not exist.
|
||||
pub fn init_tables(&self) -> Result<()> {
|
||||
let conn = self.open()?;
|
||||
conn.execute_batch(
|
||||
|
|
@ -663,7 +596,6 @@ impl ENEWikiLayer {
|
|||
)
|
||||
.context("creating wiki tables")?;
|
||||
|
||||
// Ensure optional columns exist on ene_wiki_revisions (schema migration).
|
||||
let extra_cols = [
|
||||
("archive_id", "TEXT"),
|
||||
("content_hash", "TEXT"),
|
||||
|
|
@ -671,25 +603,21 @@ impl ENEWikiLayer {
|
|||
("jsonl_event", "TEXT"),
|
||||
];
|
||||
let existing: Vec<String> = {
|
||||
let mut stmt = conn
|
||||
.prepare("PRAGMA table_info(ene_wiki_revisions)")?;
|
||||
let cols: Vec<String> = stmt.query_map([], |row| row.get::<_, String>(1))?
|
||||
let mut stmt = conn.prepare("PRAGMA table_info(ene_wiki_revisions)")?;
|
||||
let cols: Vec<String> = stmt
|
||||
.query_map([], |row| row.get::<_, String>(1))?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
cols
|
||||
};
|
||||
for (col, decl) in &extra_cols {
|
||||
if !existing.contains(&col.to_string()) {
|
||||
conn.execute_batch(&format!(
|
||||
"ALTER TABLE ene_wiki_revisions ADD COLUMN {} {}",
|
||||
col, decl
|
||||
))?;
|
||||
conn.execute_batch(&format!("ALTER TABLE ene_wiki_revisions ADD COLUMN {} {}", col, decl))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Upsert a packages row from a JSONL event.
|
||||
fn upsert_package(conn: &rusqlite::Connection, event: &Value) -> Result<()> {
|
||||
let data = &event["data"];
|
||||
let pkg = data["pkg"].as_str().unwrap_or("");
|
||||
|
|
@ -739,14 +667,7 @@ impl ENEWikiLayer {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Write or update a wiki page, appending a new revision.
|
||||
pub fn put_page(
|
||||
&self,
|
||||
title: &str,
|
||||
text: &str,
|
||||
author: &str,
|
||||
summary: &str,
|
||||
) -> Result<WikiRevision> {
|
||||
pub fn put_page(&self, title: &str, text: &str, author: &str, summary: &str) -> Result<WikiRevision> {
|
||||
if !admit_write(text) {
|
||||
return Err(anyhow!("wiki write rejected: content failed admission gate"));
|
||||
}
|
||||
|
|
@ -756,7 +677,6 @@ impl ENEWikiLayer {
|
|||
|
||||
let conn = self.open()?;
|
||||
|
||||
// Determine next revision number.
|
||||
let latest: Option<i64> = conn
|
||||
.query_row(
|
||||
"SELECT latest_revision FROM ene_wiki_pages WHERE slug = ?1",
|
||||
|
|
@ -786,11 +706,9 @@ impl ENEWikiLayer {
|
|||
|
||||
let archive_id = archive_record["archive_id"].as_str().unwrap_or("").to_string();
|
||||
let content_hash = archive_record["content_hash"].as_str().unwrap_or("").to_string();
|
||||
let archive_record_json =
|
||||
serde_json::to_string(&archive_record).unwrap_or_default();
|
||||
let archive_record_json = serde_json::to_string(&archive_record).unwrap_or_default();
|
||||
let jsonl_event_json = serde_json::to_string(&jsonl_event).unwrap_or_default();
|
||||
|
||||
// Insert revision.
|
||||
conn.execute(
|
||||
r#"
|
||||
INSERT INTO ene_wiki_revisions
|
||||
|
|
@ -807,7 +725,6 @@ impl ENEWikiLayer {
|
|||
)
|
||||
.context("inserting wiki revision")?;
|
||||
|
||||
// Upsert page header.
|
||||
conn.execute(
|
||||
r#"
|
||||
INSERT INTO ene_wiki_pages (slug, title, latest_revision, updated_at, receipt)
|
||||
|
|
@ -822,24 +739,16 @@ impl ENEWikiLayer {
|
|||
)
|
||||
.context("upserting wiki page")?;
|
||||
|
||||
// Replace links.
|
||||
conn.execute(
|
||||
"DELETE FROM ene_wiki_links WHERE slug = ?1",
|
||||
rusqlite::params![&slug],
|
||||
)?;
|
||||
conn.execute("DELETE FROM ene_wiki_links WHERE slug = ?1", rusqlite::params![&slug])?;
|
||||
for link_title in &links {
|
||||
let target_slug = title_slug(link_title).unwrap_or_default();
|
||||
conn.execute(
|
||||
r#"
|
||||
INSERT OR IGNORE INTO ene_wiki_links (slug, target_slug, target_title)
|
||||
VALUES (?1,?2,?3)
|
||||
"#,
|
||||
r#"INSERT OR IGNORE INTO ene_wiki_links (slug, target_slug, target_title) VALUES (?1,?2,?3)"#,
|
||||
rusqlite::params![&slug, &target_slug, link_title],
|
||||
)
|
||||
.context("inserting wiki link")?;
|
||||
}
|
||||
|
||||
// Replace categories.
|
||||
conn.execute(
|
||||
"DELETE FROM ene_wiki_categories WHERE slug = ?1",
|
||||
rusqlite::params![&slug],
|
||||
|
|
@ -868,12 +777,10 @@ impl ENEWikiLayer {
|
|||
})
|
||||
}
|
||||
|
||||
/// Retrieve the latest revision of a page by title, or `None` if not found.
|
||||
pub fn get_page(&self, title: &str) -> Result<Option<WikiRevision>> {
|
||||
let slug = title_slug(title)?;
|
||||
let conn = self.open()?;
|
||||
|
||||
// Find latest revision number from the pages table.
|
||||
let latest: Option<i64> = conn
|
||||
.query_row(
|
||||
"SELECT latest_revision FROM ene_wiki_pages WHERE slug = ?1",
|
||||
|
|
@ -887,14 +794,9 @@ impl ENEWikiLayer {
|
|||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// Fetch the revision body.
|
||||
let row: Option<(String, String, i64, String, String, String, i64, String)> = conn
|
||||
.query_row(
|
||||
r#"
|
||||
SELECT title, slug, revision, text, author, summary, created_at, receipt
|
||||
FROM ene_wiki_revisions
|
||||
WHERE slug = ?1 AND revision = ?2
|
||||
"#,
|
||||
r#"SELECT title, slug, revision, text, author, summary, created_at, receipt FROM ene_wiki_revisions WHERE slug = ?1 AND revision = ?2"#,
|
||||
rusqlite::params![&slug, revision],
|
||||
|r| {
|
||||
Ok((
|
||||
|
|
@ -912,13 +814,11 @@ impl ENEWikiLayer {
|
|||
.optional()
|
||||
.context("fetching wiki revision")?;
|
||||
|
||||
let (title_db, slug_db, rev, text, author, summary, created_at_ms, receipt_db) =
|
||||
match row {
|
||||
Some(t) => t,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let (title_db, slug_db, rev, text, author, summary, created_at_ms, receipt_db) = match row {
|
||||
Some(t) => t,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// Fetch links.
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT target_title FROM ene_wiki_links WHERE slug = ?1 ORDER BY lower(target_title)",
|
||||
)?;
|
||||
|
|
@ -927,7 +827,6 @@ impl ENEWikiLayer {
|
|||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
|
||||
// Fetch categories.
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT category FROM ene_wiki_categories WHERE slug = ?1 ORDER BY lower(category)",
|
||||
)?;
|
||||
|
|
@ -950,19 +849,12 @@ impl ENEWikiLayer {
|
|||
}))
|
||||
}
|
||||
|
||||
/// Search pages by title or slug containing `query` (case-insensitive LIKE).
|
||||
pub fn search(&self, query: &str, limit: i64) -> Result<Vec<WikiPage>> {
|
||||
let limit = limit.max(1).min(100);
|
||||
let term = format!("%{}%", query.trim());
|
||||
let conn = self.open()?;
|
||||
let mut stmt = conn.prepare(
|
||||
r#"
|
||||
SELECT slug, title, latest_revision, updated_at, receipt
|
||||
FROM ene_wiki_pages
|
||||
WHERE title LIKE ?1 OR slug LIKE ?1
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT ?2
|
||||
"#,
|
||||
r#"SELECT slug, title, latest_revision, updated_at, receipt FROM ene_wiki_pages WHERE title LIKE ?1 OR slug LIKE ?1 ORDER BY updated_at DESC LIMIT ?2"#,
|
||||
)?;
|
||||
let pages: Vec<WikiPage> = stmt
|
||||
.query_map(rusqlite::params![&term, limit], |r| {
|
||||
|
|
@ -979,18 +871,11 @@ impl ENEWikiLayer {
|
|||
Ok(pages)
|
||||
}
|
||||
|
||||
/// Return slugs of all pages that link *to* `title` via `ene_wiki_links`.
|
||||
pub fn backlinks(&self, title: &str) -> Result<Vec<String>> {
|
||||
let target_slug = title_slug(title)?;
|
||||
let conn = self.open()?;
|
||||
let mut stmt = conn.prepare(
|
||||
r#"
|
||||
SELECT l.slug
|
||||
FROM ene_wiki_links l
|
||||
JOIN ene_wiki_pages p ON p.slug = l.slug
|
||||
WHERE l.target_slug = ?1
|
||||
ORDER BY lower(p.title)
|
||||
"#,
|
||||
r#"SELECT l.slug FROM ene_wiki_links l JOIN ene_wiki_pages p ON p.slug = l.slug WHERE l.target_slug = ?1 ORDER BY lower(p.title)"#,
|
||||
)?;
|
||||
let slugs: Vec<String> = stmt
|
||||
.query_map(rusqlite::params![&target_slug], |r| r.get(0))?
|
||||
|
|
@ -999,19 +884,11 @@ impl ENEWikiLayer {
|
|||
Ok(slugs)
|
||||
}
|
||||
|
||||
/// Return the most recently changed revisions, newest first.
|
||||
pub fn recent_changes(&self, limit: i64) -> Result<Vec<WikiRevision>> {
|
||||
let limit = limit.max(1).min(100);
|
||||
let conn = self.open()?;
|
||||
let mut stmt = conn.prepare(
|
||||
r#"
|
||||
SELECT r.slug, r.title, r.revision, r.text, r.author, r.summary,
|
||||
r.created_at, r.receipt
|
||||
FROM ene_wiki_revisions r
|
||||
JOIN ene_wiki_pages p ON p.slug = r.slug AND p.latest_revision = r.revision
|
||||
ORDER BY r.created_at DESC
|
||||
LIMIT ?1
|
||||
"#,
|
||||
r#"SELECT r.slug, r.title, r.revision, r.text, r.author, r.summary, r.created_at, r.receipt FROM ene_wiki_revisions r JOIN ene_wiki_pages p ON p.slug = r.slug AND p.latest_revision = r.revision ORDER BY r.created_at DESC LIMIT ?1"#,
|
||||
)?;
|
||||
let revs: Vec<WikiRevision> = stmt
|
||||
.query_map(rusqlite::params![limit], |r| {
|
||||
|
|
@ -1034,30 +911,21 @@ impl ENEWikiLayer {
|
|||
}
|
||||
}
|
||||
|
||||
// ── RdsWikiLayer (PostgreSQL / tokio-postgres) ─────────────────────────────────
|
||||
|
||||
/// PostgreSQL-backed wiki layer. Drop-in equivalent of `ene_rds_wiki_layer.ENERDSWikiLayer`.
|
||||
pub struct RdsWikiLayer {
|
||||
pub dsn: String,
|
||||
}
|
||||
|
||||
impl RdsWikiLayer {
|
||||
/// Create an `RdsWikiLayer` using the given DSN string.
|
||||
///
|
||||
/// Does NOT open a persistent connection — each operation creates a short-lived
|
||||
/// connection, matching the Python psycopg2 pattern.
|
||||
pub async fn new(dsn: &str) -> Result<Self> {
|
||||
Ok(Self {
|
||||
dsn: dsn.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Open a tokio-postgres connection pair (client + connection driver).
|
||||
async fn connect(&self) -> Result<tokio_postgres::Client> {
|
||||
let (client, connection) = tokio_postgres::connect(&self.dsn, tokio_postgres::NoTls)
|
||||
.await
|
||||
.context("connecting to PostgreSQL")?;
|
||||
// Drive the connection in a detached task.
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
tracing::warn!("postgres connection error: {}", e);
|
||||
|
|
@ -1066,7 +934,6 @@ impl RdsWikiLayer {
|
|||
Ok(client)
|
||||
}
|
||||
|
||||
/// Create all tables in the `ene` schema if they do not exist.
|
||||
pub async fn init_tables(&self) -> Result<()> {
|
||||
let client = self.connect().await?;
|
||||
client
|
||||
|
|
@ -1134,14 +1001,7 @@ impl RdsWikiLayer {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Write or update a wiki page, appending a new revision.
|
||||
pub async fn put_page(
|
||||
&self,
|
||||
title: &str,
|
||||
text: &str,
|
||||
author: &str,
|
||||
summary: &str,
|
||||
) -> Result<WikiRevision> {
|
||||
pub async fn put_page(&self, title: &str, text: &str, author: &str, summary: &str) -> Result<WikiRevision> {
|
||||
if !admit_write(text) {
|
||||
return Err(anyhow!("wiki write rejected: content failed admission gate"));
|
||||
}
|
||||
|
|
@ -1151,7 +1011,6 @@ impl RdsWikiLayer {
|
|||
|
||||
let client = self.connect().await?;
|
||||
|
||||
// Determine next revision.
|
||||
let latest: Option<i64> = {
|
||||
let row = client
|
||||
.query_opt(
|
||||
|
|
@ -1184,7 +1043,6 @@ impl RdsWikiLayer {
|
|||
let archive_id = archive_record["archive_id"].as_str().unwrap_or("").to_string();
|
||||
let content_hash = archive_record["content_hash"].as_str().unwrap_or("").to_string();
|
||||
|
||||
// Insert revision.
|
||||
client
|
||||
.execute(
|
||||
r#"
|
||||
|
|
@ -1211,7 +1069,6 @@ impl RdsWikiLayer {
|
|||
.await
|
||||
.context("inserting wiki revision")?;
|
||||
|
||||
// Upsert page header.
|
||||
client
|
||||
.execute(
|
||||
r#"
|
||||
|
|
@ -1228,29 +1085,20 @@ impl RdsWikiLayer {
|
|||
.await
|
||||
.context("upserting wiki page")?;
|
||||
|
||||
// Replace links.
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM ene.wiki_links WHERE slug = $1",
|
||||
&[&slug],
|
||||
)
|
||||
.execute("DELETE FROM ene.wiki_links WHERE slug = $1", &[&slug])
|
||||
.await?;
|
||||
for link_title in &links {
|
||||
let target_slug = title_slug(link_title).unwrap_or_default();
|
||||
client
|
||||
.execute(
|
||||
r#"
|
||||
INSERT INTO ene.wiki_links (slug, target_slug, target_title)
|
||||
VALUES ($1,$2,$3)
|
||||
ON CONFLICT DO NOTHING
|
||||
"#,
|
||||
r#"INSERT INTO ene.wiki_links (slug, target_slug, target_title) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING"#,
|
||||
&[&slug, &target_slug, link_title],
|
||||
)
|
||||
.await
|
||||
.context("inserting wiki link")?;
|
||||
}
|
||||
|
||||
// Replace categories.
|
||||
client
|
||||
.execute(
|
||||
"DELETE FROM ene.wiki_categories WHERE slug = $1",
|
||||
|
|
@ -1267,7 +1115,6 @@ impl RdsWikiLayer {
|
|||
.context("inserting wiki category")?;
|
||||
}
|
||||
|
||||
// Upsert packages.
|
||||
let data = &jsonl_event["data"];
|
||||
let pkg = data["pkg"].as_str().unwrap_or("");
|
||||
let version = data["version"].as_str().unwrap_or("");
|
||||
|
|
@ -1336,17 +1183,12 @@ impl RdsWikiLayer {
|
|||
})
|
||||
}
|
||||
|
||||
/// Retrieve the latest revision of a page by title, or `None` if not found.
|
||||
pub async fn get_page(&self, title: &str) -> Result<Option<WikiRevision>> {
|
||||
let slug = title_slug(title)?;
|
||||
let client = self.connect().await?;
|
||||
|
||||
// Find latest revision number.
|
||||
let latest = client
|
||||
.query_opt(
|
||||
"SELECT latest_revision FROM ene.wiki_pages WHERE slug = $1",
|
||||
&[&slug],
|
||||
)
|
||||
.query_opt("SELECT latest_revision FROM ene.wiki_pages WHERE slug = $1", &[&slug])
|
||||
.await
|
||||
.context("querying wiki page")?;
|
||||
let revision: i64 = match latest {
|
||||
|
|
@ -1354,14 +1196,9 @@ impl RdsWikiLayer {
|
|||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// Fetch the revision body.
|
||||
let row = client
|
||||
.query_opt(
|
||||
r#"
|
||||
SELECT title, slug, revision, text, author, summary, created_at_ms, receipt
|
||||
FROM ene.wiki_revisions
|
||||
WHERE slug = $1 AND revision = $2
|
||||
"#,
|
||||
r#"SELECT title, slug, revision, text, author, summary, created_at_ms, receipt FROM ene.wiki_revisions WHERE slug = $1 AND revision = $2"#,
|
||||
&[&slug, &revision],
|
||||
)
|
||||
.await
|
||||
|
|
@ -1374,7 +1211,6 @@ impl RdsWikiLayer {
|
|||
|
||||
let slug_db: String = row.get(1);
|
||||
|
||||
// Fetch links.
|
||||
let link_rows = client
|
||||
.query(
|
||||
"SELECT target_title FROM ene.wiki_links WHERE slug = $1 ORDER BY lower(target_title)",
|
||||
|
|
@ -1383,7 +1219,6 @@ impl RdsWikiLayer {
|
|||
.await?;
|
||||
let links: Vec<String> = link_rows.iter().map(|r| r.get(0)).collect();
|
||||
|
||||
// Fetch categories.
|
||||
let cat_rows = client
|
||||
.query(
|
||||
"SELECT category FROM ene.wiki_categories WHERE slug = $1 ORDER BY lower(category)",
|
||||
|
|
@ -1407,9 +1242,6 @@ impl RdsWikiLayer {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Rusqlite optional helper ───────────────────────────────────────────────────
|
||||
|
||||
/// Extension trait to turn "not found" rusqlite errors into `Option::None`.
|
||||
trait OptionalExt<T> {
|
||||
fn optional(self) -> Result<Option<T>>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,623 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""ENE Context MCP surface.
|
||||
|
||||
Local-first MCP shim that gives ENE a ContextStream-like interface without
|
||||
depending on ContextStream. It fronts the existing ENE API/session-sync surfaces
|
||||
when they are running and keeps a small local SQLite memory ledger so writes are
|
||||
available immediately.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
SERVER_NAME = "ene-contextstream"
|
||||
SERVER_VERSION = "0.1.0"
|
||||
DEFAULT_API_URL = "http://127.0.0.1:3000"
|
||||
DEFAULT_STORE = Path.home() / ".local/share/ene/contextstream.sqlite"
|
||||
DEFAULT_CANDIDATE_ROOT = (
|
||||
Path.cwd() / "shared-data/data/germane/research/github-ene-contextstream"
|
||||
)
|
||||
|
||||
|
||||
def now_ms() -> int:
|
||||
return int(time.time() * 1000)
|
||||
|
||||
|
||||
def sha256_text(text: str) -> str:
|
||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def json_text(data: Any) -> list[dict[str, str]]:
|
||||
return [{"type": "text", "text": json.dumps(data, indent=2, sort_keys=True)}]
|
||||
|
||||
|
||||
def store_path() -> Path:
|
||||
return Path(os.environ.get("ENE_CONTEXT_STORE", str(DEFAULT_STORE))).expanduser()
|
||||
|
||||
|
||||
def api_url() -> str:
|
||||
return os.environ.get("ENE_API_URL", DEFAULT_API_URL).rstrip("/")
|
||||
|
||||
|
||||
def connect_store() -> sqlite3.Connection:
|
||||
path = store_path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS memories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
agent TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value_json TEXT NOT NULL,
|
||||
value_text TEXT NOT NULL,
|
||||
tags_json TEXT NOT NULL DEFAULT '[]',
|
||||
kind TEXT NOT NULL DEFAULT 'note',
|
||||
source TEXT NOT NULL DEFAULT 'mcp',
|
||||
prev_hash TEXT,
|
||||
receipt_hash TEXT NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
UNIQUE(agent, key)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_memories_key ON memories(key)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_memories_updated ON memories(updated_at_ms DESC)")
|
||||
conn.commit()
|
||||
return conn
|
||||
|
||||
|
||||
def parse_value(raw: Any) -> tuple[Any, str]:
|
||||
if isinstance(raw, str):
|
||||
try:
|
||||
value = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
value = raw
|
||||
else:
|
||||
value = raw
|
||||
text = json.dumps(value, sort_keys=True) if not isinstance(value, str) else value
|
||||
return value, text
|
||||
|
||||
|
||||
def http_json(path: str, query: dict[str, Any] | None = None, timeout: float = 3.0) -> Any:
|
||||
url = f"{api_url()}{path}"
|
||||
if query:
|
||||
url = f"{url}?{urllib.parse.urlencode(query)}"
|
||||
req = urllib.request.Request(url, headers={"Accept": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as response:
|
||||
status = response.status
|
||||
content_type = response.headers.get("content-type", "")
|
||||
text = response.read().decode("utf-8", errors="replace")
|
||||
except urllib.error.HTTPError as exc:
|
||||
status = exc.code
|
||||
content_type = exc.headers.get("content-type", "")
|
||||
text = exc.read().decode("utf-8", errors="replace")
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return {
|
||||
"ok": False,
|
||||
"error": "non-json response from ENE API",
|
||||
"status": status,
|
||||
"content_type": content_type,
|
||||
"body": text,
|
||||
"url": url,
|
||||
}
|
||||
if isinstance(data, dict):
|
||||
data.setdefault("status", status)
|
||||
data.setdefault("url", url)
|
||||
return data
|
||||
|
||||
|
||||
def tool_status(_: dict[str, Any]) -> dict[str, Any]:
|
||||
conn = connect_store()
|
||||
row = conn.execute("SELECT COUNT(*), MAX(updated_at_ms) FROM memories").fetchone()
|
||||
api = {"ok": False, "url": api_url()}
|
||||
try:
|
||||
api = http_json("/health")
|
||||
api["url"] = api_url()
|
||||
except Exception as exc: # local status should not fail the MCP call
|
||||
api["error"] = f"{type(exc).__name__}: {exc}"
|
||||
|
||||
sync_bin = os.environ.get(
|
||||
"ENE_SESSION_SYNC_BIN",
|
||||
str(Path.cwd() / "4-Infrastructure/infra/ene-rds/target/release/ene-sync"),
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"server": SERVER_NAME,
|
||||
"version": SERVER_VERSION,
|
||||
"api": api,
|
||||
"local_store": {
|
||||
"path": str(store_path()),
|
||||
"memory_count": row[0],
|
||||
"last_updated_ms": row[1],
|
||||
},
|
||||
"session_sync": {
|
||||
"path": sync_bin,
|
||||
"exists": Path(sync_bin).exists(),
|
||||
},
|
||||
"candidate_import_root": str(
|
||||
Path(os.environ.get("ENE_CONTEXT_CANDIDATE_ROOT", str(DEFAULT_CANDIDATE_ROOT)))
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def tool_remember(args: dict[str, Any]) -> dict[str, Any]:
|
||||
agent = str(args.get("agent") or "codex")
|
||||
key = str(args["key"])
|
||||
value, value_text = parse_value(args.get("value"))
|
||||
tags = args.get("tags") or []
|
||||
if isinstance(tags, str):
|
||||
tags = [t.strip() for t in tags.split(",") if t.strip()]
|
||||
kind = str(args.get("kind") or "note")
|
||||
source = str(args.get("source") or "mcp")
|
||||
ts = now_ms()
|
||||
|
||||
conn = connect_store()
|
||||
prev = conn.execute(
|
||||
"SELECT receipt_hash FROM memories WHERE agent = ? ORDER BY updated_at_ms DESC LIMIT 1",
|
||||
(agent,),
|
||||
).fetchone()
|
||||
prev_hash = prev[0] if prev else None
|
||||
payload = json.dumps(
|
||||
{
|
||||
"agent": agent,
|
||||
"key": key,
|
||||
"value": value,
|
||||
"tags": tags,
|
||||
"kind": kind,
|
||||
"source": source,
|
||||
"prev_hash": prev_hash,
|
||||
"updated_at_ms": ts,
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
receipt = sha256_text(payload)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO memories
|
||||
(agent, key, value_json, value_text, tags_json, kind, source,
|
||||
prev_hash, receipt_hash, created_at_ms, updated_at_ms)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(agent, key) DO UPDATE SET
|
||||
value_json = excluded.value_json,
|
||||
value_text = excluded.value_text,
|
||||
tags_json = excluded.tags_json,
|
||||
kind = excluded.kind,
|
||||
source = excluded.source,
|
||||
prev_hash = excluded.prev_hash,
|
||||
receipt_hash = excluded.receipt_hash,
|
||||
updated_at_ms = excluded.updated_at_ms
|
||||
""",
|
||||
(
|
||||
agent,
|
||||
key,
|
||||
json.dumps(value, sort_keys=True),
|
||||
value_text,
|
||||
json.dumps(tags, sort_keys=True),
|
||||
kind,
|
||||
source,
|
||||
prev_hash,
|
||||
receipt,
|
||||
ts,
|
||||
ts,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return {
|
||||
"ok": True,
|
||||
"agent": agent,
|
||||
"key": key,
|
||||
"kind": kind,
|
||||
"tags": tags,
|
||||
"receipt_hash": receipt,
|
||||
"prev_hash": prev_hash,
|
||||
"store": str(store_path()),
|
||||
}
|
||||
|
||||
|
||||
def row_to_memory(row: sqlite3.Row) -> dict[str, Any]:
|
||||
return {
|
||||
"agent": row["agent"],
|
||||
"key": row["key"],
|
||||
"value": json.loads(row["value_json"]),
|
||||
"tags": json.loads(row["tags_json"]),
|
||||
"kind": row["kind"],
|
||||
"source": row["source"],
|
||||
"receipt_hash": row["receipt_hash"],
|
||||
"prev_hash": row["prev_hash"],
|
||||
"created_at_ms": row["created_at_ms"],
|
||||
"updated_at_ms": row["updated_at_ms"],
|
||||
}
|
||||
|
||||
|
||||
def tool_recall(args: dict[str, Any]) -> dict[str, Any]:
|
||||
agent = str(args.get("agent") or "codex")
|
||||
key = args.get("key")
|
||||
query = str(args.get("query") or "")
|
||||
limit = int(args.get("limit") or 10)
|
||||
conn = connect_store()
|
||||
|
||||
if key is not None:
|
||||
row = conn.execute(
|
||||
"SELECT * FROM memories WHERE agent = ? AND key = ?",
|
||||
(agent, str(key)),
|
||||
).fetchone()
|
||||
return {"ok": True, "found": row is not None, "memory": row_to_memory(row) if row else None}
|
||||
|
||||
needle = f"%{query}%"
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT * FROM memories
|
||||
WHERE agent = ? AND (? = '' OR key LIKE ? OR value_text LIKE ? OR tags_json LIKE ?)
|
||||
ORDER BY updated_at_ms DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(agent, query, needle, needle, needle, limit),
|
||||
).fetchall()
|
||||
return {"ok": True, "count": len(rows), "memories": [row_to_memory(r) for r in rows]}
|
||||
|
||||
|
||||
def tool_search(args: dict[str, Any]) -> dict[str, Any]:
|
||||
query = str(args["query"])
|
||||
limit = int(args.get("limit") or 10)
|
||||
semantic = bool(args.get("semantic") or False)
|
||||
sources = args.get("sources") or ["ene_api", "wiki", "local_memory"]
|
||||
out: dict[str, Any] = {"ok": True, "query": query, "results": {}}
|
||||
|
||||
if "ene_api" in sources:
|
||||
try:
|
||||
out["results"]["ene_api"] = http_json(
|
||||
"/search",
|
||||
{"q": query, "limit": limit, "semantic": str(semantic).lower()},
|
||||
)
|
||||
except Exception as exc:
|
||||
out["results"]["ene_api"] = {
|
||||
"ok": False,
|
||||
"error": f"{type(exc).__name__}: {exc}",
|
||||
"url": api_url(),
|
||||
}
|
||||
|
||||
if "wiki" in sources:
|
||||
try:
|
||||
out["results"]["wiki"] = http_json(
|
||||
"/wiki/search",
|
||||
{"q": query, "limit": limit},
|
||||
)
|
||||
except Exception as exc:
|
||||
out["results"]["wiki"] = {
|
||||
"ok": False,
|
||||
"error": f"{type(exc).__name__}: {exc}",
|
||||
"url": api_url(),
|
||||
}
|
||||
|
||||
if "local_memory" in sources:
|
||||
out["results"]["local_memory"] = tool_recall(
|
||||
{"agent": args.get("agent", "codex"), "query": query, "limit": limit}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def tool_context(args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""One-call session-start context packet for agents.
|
||||
|
||||
This mirrors ContextStream's common startup habit while staying ENE-first:
|
||||
status + search + optional recall + optional transcript save.
|
||||
"""
|
||||
user_message = str(args.get("user_message") or args.get("query") or "")
|
||||
agent = str(args.get("agent") or "codex")
|
||||
save_exchange = bool(args.get("save_exchange") or False)
|
||||
session_id = args.get("session_id")
|
||||
result: dict[str, Any] = {
|
||||
"ok": True,
|
||||
"policy": "ENE first. ContextStream is fallback only.",
|
||||
"status": tool_status({}),
|
||||
"search": None,
|
||||
"recall": None,
|
||||
"saved": None,
|
||||
}
|
||||
if user_message:
|
||||
result["search"] = tool_search({"query": user_message, "agent": agent, "limit": args.get("limit", 10)})
|
||||
result["recall"] = tool_recall({"agent": agent, "query": user_message, "limit": args.get("limit", 10)})
|
||||
if save_exchange and user_message:
|
||||
key = f"session:{session_id or now_ms()}:user:{now_ms()}"
|
||||
result["saved"] = tool_remember(
|
||||
{
|
||||
"agent": agent,
|
||||
"key": key,
|
||||
"value": {"user_message": user_message, "session_id": session_id},
|
||||
"tags": ["transcript", "user-message"],
|
||||
"kind": "conversation",
|
||||
"source": "ene_context",
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def tool_sessions(args: dict[str, Any]) -> dict[str, Any]:
|
||||
action = str(args.get("action") or "list")
|
||||
limit = int(args.get("limit") or 10)
|
||||
try:
|
||||
if action == "list":
|
||||
return http_json("/sessions", {"limit": limit})
|
||||
if action == "get":
|
||||
return http_json(f"/sessions/{urllib.parse.quote(str(args['session_id']))}")
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": f"{type(exc).__name__}: {exc}", "url": api_url()}
|
||||
return {"ok": False, "error": f"unknown sessions action: {action}"}
|
||||
|
||||
|
||||
def tool_sync(args: dict[str, Any]) -> dict[str, Any]:
|
||||
dry_run = bool(args.get("dry_run", True))
|
||||
command = str(args.get("command") or "list")
|
||||
sync_bin = os.environ.get(
|
||||
"ENE_SESSION_SYNC_BIN",
|
||||
str(Path.cwd() / "4-Infrastructure/infra/ene-rds/target/release/ene-sync"),
|
||||
)
|
||||
cmd = [sync_bin]
|
||||
if command == "sync":
|
||||
cmd.append("sync")
|
||||
if args.get("embed"):
|
||||
cmd.insert(1, "--embed")
|
||||
elif command == "list":
|
||||
cmd.extend(["list", "--limit", str(int(args.get("limit") or 20))])
|
||||
elif command == "init-schema":
|
||||
cmd.append("init-schema")
|
||||
else:
|
||||
return {"ok": False, "error": f"unsupported sync command: {command}"}
|
||||
|
||||
if dry_run:
|
||||
return {"ok": True, "dry_run": True, "command": cmd, "exists": Path(sync_bin).exists()}
|
||||
if not Path(sync_bin).exists():
|
||||
return {"ok": False, "error": "ene-session-sync binary not found", "command": cmd}
|
||||
proc = subprocess.run(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120)
|
||||
return {
|
||||
"ok": proc.returncode == 0,
|
||||
"returncode": proc.returncode,
|
||||
"stdout": proc.stdout[-8000:],
|
||||
"stderr": proc.stderr[-8000:],
|
||||
"command": cmd,
|
||||
}
|
||||
|
||||
|
||||
def candidate_summary(path: Path) -> dict[str, Any]:
|
||||
readme = next(path.glob("README*"), None)
|
||||
cargo = path / "Cargo.toml"
|
||||
package = path / "package.json"
|
||||
pyproject = path / "pyproject.toml"
|
||||
return {
|
||||
"repo": path.name,
|
||||
"path": str(path),
|
||||
"readme": str(readme) if readme else None,
|
||||
"manifests": [str(p) for p in [cargo, package, pyproject] if p.exists()],
|
||||
}
|
||||
|
||||
|
||||
def tool_import_candidates(args: dict[str, Any]) -> dict[str, Any]:
|
||||
root = Path(args.get("root") or os.environ.get("ENE_CONTEXT_CANDIDATE_ROOT", str(DEFAULT_CANDIDATE_ROOT)))
|
||||
repos = [p for p in sorted(root.iterdir()) if p.is_dir() and (p / ".git").exists()] if root.exists() else []
|
||||
map_notes = {
|
||||
"Octopoda-OS": "Agent memory API, remember/recall/search/snapshot/audit vocabulary.",
|
||||
"llm_wiki": "Source -> wiki -> graph ingest pattern and local API shape.",
|
||||
"SurfSense": "Browser/Obsidian/document connectors and team RAG workflows.",
|
||||
"forge": "Local tool-calling guardrails and proxy agent loop patterns.",
|
||||
"kanbots": "MCP over local HTTP bridge and agent task board semantics.",
|
||||
"namidb": "Graph database on object storage; useful for ENE graph persistence.",
|
||||
"Vane": "Local answering engine with SearXNG/search history/citation UX.",
|
||||
}
|
||||
return {
|
||||
"ok": True,
|
||||
"root": str(root),
|
||||
"count": len(repos),
|
||||
"candidates": [
|
||||
{**candidate_summary(p), "ene_use": map_notes.get(p.name, "Review manually")}
|
||||
for p in repos
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
TOOLS: dict[str, dict[str, Any]] = {
|
||||
"ene_status": {
|
||||
"description": "ENE context health: local memory ledger, ENE API, session-sync binary.",
|
||||
"inputSchema": {"type": "object", "properties": {}},
|
||||
"handler": tool_status,
|
||||
},
|
||||
"ene_remember": {
|
||||
"description": "Store a durable ENE memory packet with a hash-chain receipt.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent": {"type": "string", "default": "codex"},
|
||||
"key": {"type": "string"},
|
||||
"value": {},
|
||||
"tags": {"type": "array", "items": {"type": "string"}},
|
||||
"kind": {"type": "string", "default": "note"},
|
||||
"source": {"type": "string", "default": "mcp"},
|
||||
},
|
||||
"required": ["key", "value"],
|
||||
},
|
||||
"handler": tool_remember,
|
||||
},
|
||||
"ene_context": {
|
||||
"description": "ENE-first startup/context packet: status, search, recall, optional transcript save.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user_message": {"type": "string"},
|
||||
"query": {"type": "string"},
|
||||
"agent": {"type": "string", "default": "codex"},
|
||||
"session_id": {"type": "string"},
|
||||
"save_exchange": {"type": "boolean", "default": False},
|
||||
"limit": {"type": "integer", "default": 10},
|
||||
},
|
||||
},
|
||||
"handler": tool_context,
|
||||
},
|
||||
"ene_recall": {
|
||||
"description": "Recall ENE memory by exact key or query over local memory.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent": {"type": "string", "default": "codex"},
|
||||
"key": {"type": "string"},
|
||||
"query": {"type": "string"},
|
||||
"limit": {"type": "integer", "default": 10},
|
||||
},
|
||||
},
|
||||
"handler": tool_recall,
|
||||
},
|
||||
"ene_search": {
|
||||
"description": "Search ENE API chat/session memory plus local ENE memory ledger.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string"},
|
||||
"limit": {"type": "integer", "default": 10},
|
||||
"semantic": {"type": "boolean", "default": False},
|
||||
"agent": {"type": "string", "default": "codex"},
|
||||
"sources": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
"handler": tool_search,
|
||||
},
|
||||
"ene_sessions": {
|
||||
"description": "List or fetch ENE chat sessions through ene-api.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {"type": "string", "enum": ["list", "get"], "default": "list"},
|
||||
"session_id": {"type": "string"},
|
||||
"limit": {"type": "integer", "default": 10},
|
||||
},
|
||||
},
|
||||
"handler": tool_sessions,
|
||||
},
|
||||
"ene_sync": {
|
||||
"description": "Run or dry-run ene-session-sync commands.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {"type": "string", "enum": ["list", "sync", "init-schema"], "default": "list"},
|
||||
"dry_run": {"type": "boolean", "default": True},
|
||||
"limit": {"type": "integer", "default": 20},
|
||||
"embed": {"type": "boolean", "default": False},
|
||||
},
|
||||
},
|
||||
"handler": tool_sync,
|
||||
},
|
||||
"ene_import_candidates": {
|
||||
"description": "List GitHub repos pulled as ENE ContextStream-equivalent candidates.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {"root": {"type": "string"}},
|
||||
},
|
||||
"handler": tool_import_candidates,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def mcp_tools() -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"name": name,
|
||||
"description": spec["description"],
|
||||
"inputSchema": spec["inputSchema"],
|
||||
}
|
||||
for name, spec in TOOLS.items()
|
||||
]
|
||||
|
||||
|
||||
def handle(req: dict[str, Any]) -> dict[str, Any] | None:
|
||||
method = req.get("method")
|
||||
req_id = req.get("id")
|
||||
try:
|
||||
if method == "initialize":
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": req_id,
|
||||
"result": {
|
||||
"protocolVersion": req.get("params", {}).get("protocolVersion", "2024-11-05"),
|
||||
"capabilities": {"tools": {}},
|
||||
"serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
|
||||
},
|
||||
}
|
||||
if method == "notifications/initialized":
|
||||
return None
|
||||
if method == "tools/list":
|
||||
return {"jsonrpc": "2.0", "id": req_id, "result": {"tools": mcp_tools()}}
|
||||
if method == "tools/call":
|
||||
params = req.get("params", {})
|
||||
name = params.get("name")
|
||||
if name not in TOOLS:
|
||||
raise ValueError(f"unknown tool: {name}")
|
||||
args = params.get("arguments") or {}
|
||||
result = TOOLS[name]["handler"](args)
|
||||
return {"jsonrpc": "2.0", "id": req_id, "result": {"content": json_text(result)}}
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": req_id,
|
||||
"error": {"code": -32601, "message": f"method not found: {method}"},
|
||||
}
|
||||
except Exception as exc:
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": req_id,
|
||||
"error": {"code": -32000, "message": f"{type(exc).__name__}: {exc}"},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) > 1:
|
||||
command = sys.argv[1]
|
||||
if command in {"--status", "status"}:
|
||||
print(json.dumps(tool_status({}), indent=2, sort_keys=True))
|
||||
return 0
|
||||
if command in {"--context", "context"}:
|
||||
query = " ".join(sys.argv[2:])
|
||||
print(json.dumps(tool_context({"user_message": query}), indent=2, sort_keys=True))
|
||||
return 0
|
||||
print(f"unknown command: {command}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
handled = 0
|
||||
for line in sys.stdin:
|
||||
if not line.strip():
|
||||
continue
|
||||
handled += 1
|
||||
try:
|
||||
response = handle(json.loads(line))
|
||||
except json.JSONDecodeError as exc:
|
||||
response = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": None,
|
||||
"error": {"code": -32700, "message": str(exc)},
|
||||
}
|
||||
if response is not None:
|
||||
sys.stdout.write(json.dumps(response, separators=(",", ":")) + "\n")
|
||||
sys.stdout.flush()
|
||||
if handled == 0:
|
||||
print(json.dumps(tool_status({}), indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
132
4-Infrastructure/shim/clean_rrc_pist_validation.py
Normal file
132
4-Infrastructure/shim/clean_rrc_pist_validation.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Clean shared-data/rrc_pist_exact_validation.json after regeneration.
|
||||
|
||||
The legacy validation script can accidentally include Markdown table artifacts such
|
||||
as `Equation` and `---` as predictions. This cleaner removes those rows, rebuilds
|
||||
the summary, and preserves classifier-backed predictions for the receipt-density
|
||||
injector.
|
||||
|
||||
Usage:
|
||||
|
||||
python3 4-Infrastructure/shim/clean_rrc_pist_validation.py
|
||||
|
||||
Optional paths:
|
||||
|
||||
python3 4-Infrastructure/shim/clean_rrc_pist_validation.py \
|
||||
--input shared-data/rrc_pist_exact_validation.json \
|
||||
--out shared-data/rrc_pist_exact_validation.json
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_REPORT = ROOT / "shared-data/rrc_pist_exact_validation.json"
|
||||
NOISE = {"", "---", "Equation", "RRC shape", "Status", "Top axes"}
|
||||
|
||||
|
||||
def is_noise(pred: dict[str, Any]) -> bool:
|
||||
eq = str(pred.get("equation", ""))
|
||||
gt = str(pred.get("ground_truth", ""))
|
||||
proxy = str(pred.get("proxy_pred", ""))
|
||||
return (
|
||||
eq in NOISE
|
||||
or gt in NOISE
|
||||
or proxy in NOISE
|
||||
or bool(re.fullmatch(r"-+", eq))
|
||||
or bool(re.fullmatch(r"-+", gt))
|
||||
)
|
||||
|
||||
|
||||
def accuracy(predictions: list[dict[str, Any]], key: str) -> float:
|
||||
if not predictions:
|
||||
return 0.0
|
||||
return sum(1 for pred in predictions if pred.get(key) == pred.get("ground_truth")) / len(predictions)
|
||||
|
||||
|
||||
def per_class(predictions: list[dict[str, Any]], key: str) -> dict[str, dict[str, Any]]:
|
||||
classes = sorted({p.get("ground_truth") for p in predictions} | {p.get(key) for p in predictions})
|
||||
out: dict[str, dict[str, Any]] = {}
|
||||
for cls in classes:
|
||||
if cls is None:
|
||||
continue
|
||||
total = sum(1 for p in predictions if p.get("ground_truth") == cls)
|
||||
correct = sum(1 for p in predictions if p.get("ground_truth") == cls and p.get(key) == cls)
|
||||
out[str(cls)] = {"total": total, "correct": correct, "accuracy": correct / total if total else 0.0}
|
||||
return out
|
||||
|
||||
|
||||
def zmp_distribution(predictions: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
||||
zmp_by_gt: dict[str, list[int]] = defaultdict(list)
|
||||
for pred in predictions:
|
||||
gt = str(pred.get("ground_truth", "unknown"))
|
||||
try:
|
||||
zmp = int(pred.get("zmp", 0))
|
||||
except Exception:
|
||||
zmp = 0
|
||||
zmp_by_gt[gt].append(zmp)
|
||||
return {
|
||||
gt: {"mean": sum(vals) / len(vals), "min": min(vals), "max": max(vals), "unique": len(set(vals))}
|
||||
for gt, vals in zmp_by_gt.items()
|
||||
if vals
|
||||
}
|
||||
|
||||
|
||||
def clean_report(data: dict[str, Any]) -> dict[str, Any]:
|
||||
raw_predictions = data.get("predictions", [])
|
||||
predictions = [pred for pred in raw_predictions if isinstance(pred, dict) and not is_noise(pred)]
|
||||
dropped = len(raw_predictions) - len(predictions)
|
||||
|
||||
matrix_hashes = Counter(str(p.get("matrix_hash", "")) for p in predictions if p.get("matrix_hash"))
|
||||
canonical_hashes = Counter(str(p.get("canonical_hash", "")) for p in predictions if p.get("canonical_hash"))
|
||||
errors = data.get("errors_detail", []) or []
|
||||
|
||||
return {
|
||||
"summary": {
|
||||
"total_input_predictions_before_clean": len(raw_predictions),
|
||||
"markdown_noise_predictions_dropped": dropped,
|
||||
"total": len(predictions),
|
||||
"errors": len(errors),
|
||||
"unique_matrix_hashes": len(matrix_hashes),
|
||||
"unique_canonical_hashes": len(canonical_hashes),
|
||||
"proxy_accuracy": accuracy(predictions, "proxy_pred"),
|
||||
"exact_accuracy": accuracy(predictions, "exact_pred"),
|
||||
"matrix_hash_collisions": sum(1 for count in matrix_hashes.values() if count > 1),
|
||||
"canonical_hash_collisions": sum(1 for count in canonical_hashes.values() if count > 1),
|
||||
"filtered_markdown_noise": True,
|
||||
"promotion_policy": "not_promoted classifier diagnostics only",
|
||||
},
|
||||
"per_class_proxy": per_class(predictions, "proxy_pred"),
|
||||
"per_class_exact": per_class(predictions, "exact_pred"),
|
||||
"zmp_distribution": zmp_distribution(predictions),
|
||||
"errors_detail": errors,
|
||||
"predictions": predictions,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Clean RRC PIST validation JSON")
|
||||
parser.add_argument("--input", type=Path, default=DEFAULT_REPORT)
|
||||
parser.add_argument("--out", type=Path, default=DEFAULT_REPORT)
|
||||
parser.add_argument("--fail-if-empty", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
data = json.loads(args.input.read_text(encoding="utf-8"))
|
||||
cleaned = clean_report(data)
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.out.write_text(json.dumps(cleaned, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
print(json.dumps(cleaned["summary"], indent=2, sort_keys=True))
|
||||
print(f"Wrote cleaned report: {args.out}")
|
||||
if args.fail_if_empty and cleaned["summary"]["total"] == 0:
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,123 +1,126 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migrate knowledge.* tables → ENE substrate with aggressive concept tagging.
|
||||
"""ENE substrate migration + concept tagging (legacy shim).
|
||||
|
||||
1. Apply ENE schema (ene.packages + 9 support tables)
|
||||
2. Migrate all sources into ene.packages with typed provenance
|
||||
3. Extract concepts, build relations, score N-space KV retention
|
||||
4. Run cross-source discovery queries
|
||||
NOTE (ontology migration):
|
||||
|
||||
Run in dev container:
|
||||
podman exec -e AWS_ACCESS_KEY_ID=... -e AWS_SECRET_ACCESS_KEY=... -e AWS_REGION=us-east-1 -e RDS_IAM=1 \
|
||||
research-stack python3 /home/researcher/stack/4-Infrastructure/shim/ene_migrate_and_tag.py
|
||||
This file is a **legacy shim** used to keep ENE substrate bootstrapping and
|
||||
migration workflows running while the AVM / Lean-only ISA rewrite is underway.
|
||||
|
||||
**Target architecture:** Lean-only AVM ISA + backend shims.
|
||||
- Lean defines all semantics.
|
||||
- Shims perform I/O and orchestration only.
|
||||
|
||||
This script currently performs:
|
||||
- schema application
|
||||
- migration SQL
|
||||
- tokenizer-based tagging
|
||||
- relation building
|
||||
- retention scoring
|
||||
|
||||
Several of those operations are semantic decisions and include float-based scoring.
|
||||
They must be ported into Lean/AVM.
|
||||
|
||||
Rules until ported:
|
||||
- Treat all outputs as **not promoted**.
|
||||
- Never silently apply a different schema. If the schema file is missing: **reject**.
|
||||
|
||||
TODO(lean-port): Port tagging/relations/retention scoring into Lean/AVM.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
|
||||
from rds_connect import connect_rds
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
log = logging.getLogger("ene_migrate")
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_SCHEMA_PATH = REPO_ROOT / "4-Infrastructure/shim/ene_substrate_schema.sql"
|
||||
ONTOLOGY_VERSION = "shim-ontology-migration-v1"
|
||||
|
||||
|
||||
def connect():
|
||||
return connect_rds()
|
||||
|
||||
|
||||
def apply_schema(conn):
|
||||
"""Apply the full ENE substrate schema."""
|
||||
schema_path = Path("/home/researcher/stack/4-Infrastructure/shim/ene_substrate_schema.sql")
|
||||
if schema_path.exists():
|
||||
sql = schema_path.read_text()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql)
|
||||
conn.commit()
|
||||
log.info("ENE substrate schema applied")
|
||||
else:
|
||||
log.warning("Schema file not found, creating inline")
|
||||
# Fallback: create minimal schema inline
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("CREATE SCHEMA IF NOT EXISTS ene")
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS ene.packages (
|
||||
pkg TEXT PRIMARY KEY, package_type TEXT, title TEXT, content TEXT,
|
||||
content_hash TEXT, concept_vector JSONB DEFAULT '[]',
|
||||
concept_anchor JSONB DEFAULT '{}', tags JSONB DEFAULT '[]',
|
||||
source TEXT, provenance JSONB DEFAULT '{}', domain TEXT,
|
||||
archetype TEXT, promotion_state TEXT DEFAULT 'held',
|
||||
scar_class TEXT, ingested_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS ene.relations (
|
||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
||||
source_id TEXT NOT NULL, target_id TEXT NOT NULL,
|
||||
relation_type TEXT NOT NULL, weight REAL DEFAULT 1.0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS ene.nspace_kv (
|
||||
key_id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
||||
value_package_id TEXT NOT NULL,
|
||||
reduction_reward REAL DEFAULT 0, sparsity_score REAL DEFAULT 0,
|
||||
scar_pressure REAL DEFAULT 0, retention_score REAL DEFAULT 0
|
||||
);
|
||||
""")
|
||||
conn.commit()
|
||||
log.info("ENE minimal schema applied")
|
||||
def apply_schema(conn, schema_path: Path) -> None:
|
||||
"""Apply the ENE substrate schema.
|
||||
|
||||
Policy: never silently substitute an inline schema. If the schema file is
|
||||
missing, reject.
|
||||
"""
|
||||
if not schema_path.exists():
|
||||
raise FileNotFoundError(f"ENE schema file not found: {schema_path}")
|
||||
|
||||
sql = schema_path.read_text(encoding="utf-8")
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql)
|
||||
conn.commit()
|
||||
log.info("ENE substrate schema applied (%s)", schema_path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Term extraction (same aggressive tokenizer from concept_cross_reference.py)
|
||||
# Term extraction (legacy tokenizer)
|
||||
# ---------------------------------------------------------------------------
|
||||
MATH_SYMBOL_RE = re.compile(
|
||||
r'\\(?:alpha|beta|gamma|Gamma|delta|Delta|epsilon|varepsilon|zeta|eta|theta|Theta|'
|
||||
r'iota|kappa|lambda|Lambda|mu|nu|xi|Xi|pi|Pi|rho|sigma|Sigma|tau|upsilon|phi|Phi|'
|
||||
r'varphi|chi|psi|Psi|omega|Omega|partial|nabla|infty|int|sum|prod|otimes|oplus|'
|
||||
r'rightarrow|leftarrow|Rightarrow|Leftarrow|mapsto|approx|equiv|sim|propto|'
|
||||
r'leq|geq|neq|times|cdot|circ|pm|mp|sqrt|frac|operatorname|mathbf|mathrm|mathcal|'
|
||||
r'mathfrak|mathbb|text|hat|tilde|bar|vec|dot|ddot|widehat|widetilde|'
|
||||
r'langle|rangle|lVert|rVert|vert|mid|'
|
||||
r'begin|end|left|right|big|Big|bigg|Bigg)'
|
||||
r"\\\\(?:alpha|beta|gamma|Gamma|delta|Delta|epsilon|varepsilon|zeta|eta|theta|Theta|"
|
||||
r"iota|kappa|lambda|Lambda|mu|nu|xi|Xi|pi|Pi|rho|sigma|Sigma|tau|upsilon|phi|Phi|"
|
||||
r"varphi|chi|psi|Psi|omega|Omega|partial|nabla|infty|int|sum|prod|otimes|oplus|"
|
||||
r"rightarrow|leftarrow|Rightarrow|Leftarrow|mapsto|approx|equiv|sim|propto|"
|
||||
r"leq|geq|neq|times|cdot|circ|pm|mp|sqrt|frac|operatorname|mathbf|mathrm|mathcal|"
|
||||
r"mathfrak|mathbb|text|hat|tilde|bar|vec|dot|ddot|widehat|widetilde|"
|
||||
r"langle|rangle|lVert|rVert|vert|mid|"
|
||||
r"begin|end|left|right|big|Big|bigg|Bigg)"
|
||||
)
|
||||
|
||||
TECHNICAL_RE = re.compile(
|
||||
r'\b(?:'
|
||||
r'manifold|field|shear|packet|spectral|braid|gossip|'
|
||||
r'residual|invariant|receipt|scar|warden|collapse|compression|'
|
||||
r'entropy|eigen(?:value|vector)?|coboundary|cochain|'
|
||||
r'diffusion|transport|boundary|kernel|operator|'
|
||||
r'tensor|metric|geodesic|curvature|torsion|'
|
||||
r'hamiltonian|lagrangian|reduction|projection|embedding|'
|
||||
r'chirality|helicity|handedness|logogram|'
|
||||
r'sidon|goxel|famm|nuvmap|otom|pist|'
|
||||
r'erdos|szekeres|selfridge|gyarfas|'
|
||||
r'biocompression|organoid|chaos|fractal|attractor|basin|'
|
||||
r'thermal|thermodynamic|landauer|witness|shadow|adversarial|'
|
||||
r'morph(?:ic|ism)?|radix|codec|semantic|'
|
||||
r'markov|cognitive|attention|neural|network|transformer|'
|
||||
r'hutter|prize|betti|homology|cohomology|'
|
||||
r'riccati|noise|mfg|hessian|jacobian|'
|
||||
r'seam|tomography|sandwich|pruning|rope|scar|'
|
||||
r'eigensolid|eigenspace|underverse|geocognition|'
|
||||
r'smallcode|constrained|key.value|shortcut|ontology|'
|
||||
r'hyperbolic|riemannian|poincare|'
|
||||
r'bio|dna|rna|protein|feynman|navier|stokes|'
|
||||
r'plasma|mhd|alfven|'
|
||||
r'q16|fixed.point|subleq|oisc|kv|cache'
|
||||
r')\b', re.IGNORECASE
|
||||
r"\\b(?:"
|
||||
r"manifold|field|shear|packet|spectral|braid|gossip|"
|
||||
r"residual|invariant|receipt|scar|warden|collapse|compression|"
|
||||
r"entropy|eigen(?:value|vector)?|coboundary|cochain|"
|
||||
r"diffusion|transport|boundary|kernel|operator|"
|
||||
r"tensor|metric|geodesic|curvature|torsion|"
|
||||
r"hamiltonian|lagrangian|reduction|projection|embedding|"
|
||||
r"chirality|helicity|handedness|logogram|"
|
||||
r"sidon|goxel|famm|nuvmap|otom|pist|"
|
||||
r"erdos|szekeres|selfridge|gyarfas|"
|
||||
r"biocompression|organoid|chaos|fractal|attractor|basin|"
|
||||
r"thermal|thermodynamic|landauer|witness|shadow|adversarial|"
|
||||
r"morph(?:ic|ism)?|radix|codec|semantic|"
|
||||
r"markov|cognitive|attention|neural|network|transformer|"
|
||||
r"hutter|prize|betti|homology|cohomology|"
|
||||
r"riccati|noise|mfg|hessian|jacobian|"
|
||||
r"seam|tomography|sandwich|pruning|rope|scar|"
|
||||
r"eigensolid|eigenspace|underverse|geocognition|"
|
||||
r"smallcode|constrained|key.value|shortcut|ontology|"
|
||||
r"hyperbolic|riemannian|poincare|"
|
||||
r"bio|dna|rna|protein|feynman|navier|stokes|"
|
||||
r"plasma|mhd|alfven|"
|
||||
r"q16|fixed.point|subleq|oisc|kv|cache"
|
||||
r")\\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
TOKEN_RE = re.compile(r'[a-zA-Z_\\][a-zA-Z0-9_\\]*|\b(?:N-space|key-value|CP-SAT|Anti-FAMM|Anti-Braid)\b', re.IGNORECASE)
|
||||
STOP_WORDS = set("the a an is are was were be been being have has had do does did will would shall should may might must can could of in to for with on at by from as into through during and but or not no nor so if then else when where this that these those it its we they he she which who whom whose what how why also very more most some any all each every both few new other such only own same just about over text bf rm sf tt em sc it up use using used can one two also etc via per e.g i.e figure table section non doi url http https www paper result method approach model data set page pages vol pp et al note notes example see shown fig eq ref abstract introduction conclusion reference references arxiv org github com html pdf first second third following based given found obtained described proposed well within without between among under above below since however therefore thus still yet here there where now then than get got getting make made making take taken taking give given giving let lets case cases term terms form forms number numbers value values point points part parts type types kind kinds way ways much many long short high low large small different similar same total whole full work works working need needs needed help helps helped like likes liked know known unknown think thought believe want wants wanted try tries tried".split())
|
||||
TOKEN_RE = re.compile(
|
||||
r"[a-zA-Z_\\\\][a-zA-Z0-9_\\\\]*|\\b(?:N-space|key-value|CP-SAT|Anti-FAMM|Anti-Braid)\\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
STOP_WORDS = set(
|
||||
"the a an is are was were be been being have has had do does did will would shall should may might must can could of in to for with on at by from as into through during and but or not no nor so if then else when where this that these those it its we they which who what how why also more most some any all each every both few new other such only own same just about over text rm sf tt em sc up use using used one two etc via per eg ie figure table section doi url http https www paper result method approach model data set page pages vol pp et al note notes example see shown fig eq ref abstract introduction conclusion reference references arxiv org github com html pdf first second third following based given found obtained described proposed well within without between among under above below since however therefore thus still yet here there now then than"
|
||||
.split()
|
||||
)
|
||||
|
||||
|
||||
def tokenize(text: str) -> list[str]:
|
||||
|
|
@ -126,7 +129,14 @@ def tokenize(text: str) -> list[str]:
|
|||
seen: set[str] = set()
|
||||
results: list[str] = []
|
||||
for m in TOKEN_RE.finditer(text):
|
||||
t = m.group(0).strip().lower().rstrip(".,;:!?\"'()[]{}").lstrip("\\").rstrip("{}")
|
||||
t = (
|
||||
m.group(0)
|
||||
.strip()
|
||||
.lower()
|
||||
.rstrip(".,;:!?\"'()[]{}")
|
||||
.lstrip("\\\\")
|
||||
.rstrip("{}")
|
||||
)
|
||||
if not t or t in STOP_WORDS or len(t) < 2 or t in seen:
|
||||
continue
|
||||
seen.add(t)
|
||||
|
|
@ -137,13 +147,15 @@ def tokenize(text: str) -> list[str]:
|
|||
# ---------------------------------------------------------------------------
|
||||
# Migration
|
||||
# ---------------------------------------------------------------------------
|
||||
def migrate_all_sources(conn):
|
||||
def migrate_all_sources(conn) -> int:
|
||||
"""Migrate knowledge.* tables into ene.packages with typed provenance."""
|
||||
cur = conn.cursor()
|
||||
total = 0
|
||||
|
||||
migrations = [
|
||||
("equations", "eq_id", "equation", """
|
||||
(
|
||||
"equations",
|
||||
"""
|
||||
INSERT INTO ene.packages (pkg, package_type, title, content, content_hash, tags, source, domain, provenance, promotion_state)
|
||||
SELECT eq_id::text, 'equation', latex, latex, content_hash, '[]'::jsonb, source_file, 'equation_corpus',
|
||||
jsonb_build_object('kind', kind, 'source_file', source_file, 'source_offset', source_offset),
|
||||
|
|
@ -152,8 +164,11 @@ def migrate_all_sources(conn):
|
|||
ON CONFLICT (pkg) DO UPDATE SET
|
||||
title = EXCLUDED.title, content = EXCLUDED.content,
|
||||
content_hash = EXCLUDED.content_hash, tags = EXCLUDED.tags
|
||||
"""),
|
||||
("tiddlywiki_pages", "tiddler_id", "tiddler", """
|
||||
""",
|
||||
),
|
||||
(
|
||||
"tiddlywiki_pages",
|
||||
"""
|
||||
INSERT INTO ene.packages (pkg, package_type, title, content, content_hash, tags, source, domain, provenance, promotion_state)
|
||||
SELECT tiddler_id::text, 'tiddler', title, coalesce(body,''),
|
||||
content_hash, '[]'::jsonb, source_path, 'tiddlywiki',
|
||||
|
|
@ -163,10 +178,13 @@ def migrate_all_sources(conn):
|
|||
ON CONFLICT (pkg) DO UPDATE SET
|
||||
title = EXCLUDED.title, content = EXCLUDED.content,
|
||||
content_hash = EXCLUDED.content_hash, tags = EXCLUDED.tags
|
||||
"""),
|
||||
("references", "ref_id", "reference", """
|
||||
""",
|
||||
),
|
||||
(
|
||||
"references",
|
||||
"""
|
||||
INSERT INTO ene.packages (pkg, package_type, title, content, content_hash, tags, source, domain, provenance, promotion_state)
|
||||
SELECT ref_id::text, 'reference',
|
||||
SELECT ref_id::text, 'reference',
|
||||
substring(coalesce(bibtex,'') from 1 for 200),
|
||||
bibtex, content_hash, '[]'::jsonb, source_file, 'bibliography',
|
||||
jsonb_build_object('source_file', source_file, 'bibtex', coalesce(bibtex,'')),
|
||||
|
|
@ -175,8 +193,11 @@ def migrate_all_sources(conn):
|
|||
ON CONFLICT (pkg) DO UPDATE SET
|
||||
title = EXCLUDED.title, content = EXCLUDED.content,
|
||||
content_hash = EXCLUDED.content_hash, tags = EXCLUDED.tags
|
||||
"""),
|
||||
("links", "link_id", "link", """
|
||||
""",
|
||||
),
|
||||
(
|
||||
"links",
|
||||
"""
|
||||
INSERT INTO ene.packages (pkg, package_type, title, content, content_hash, tags, source, domain, provenance, promotion_state)
|
||||
SELECT link_id::text, 'link', url, url, encode(sha256(url::bytea),'hex'), '[]'::jsonb, source_file,
|
||||
'external_reference',
|
||||
|
|
@ -185,8 +206,11 @@ def migrate_all_sources(conn):
|
|||
FROM knowledge.links
|
||||
ON CONFLICT (pkg) DO UPDATE SET
|
||||
title = EXCLUDED.title, content = EXCLUDED.content
|
||||
"""),
|
||||
("article_sources", "article_id", "article", """
|
||||
""",
|
||||
),
|
||||
(
|
||||
"article_sources",
|
||||
"""
|
||||
INSERT INTO ene.packages (pkg, package_type, title, content, content_hash, tags, source, domain, provenance, promotion_state)
|
||||
SELECT article_id::text, 'article', coalesce(label, url), coalesce(label,'') || ' ' || url,
|
||||
encode(sha256(url::bytea),'hex'), '[]'::jsonb, url, 'article_source',
|
||||
|
|
@ -195,8 +219,11 @@ def migrate_all_sources(conn):
|
|||
FROM knowledge.article_sources
|
||||
ON CONFLICT (pkg) DO UPDATE SET
|
||||
title = EXCLUDED.title, content = EXCLUDED.content
|
||||
"""),
|
||||
("dois", "doi_id", "doi", """
|
||||
""",
|
||||
),
|
||||
(
|
||||
"dois",
|
||||
"""
|
||||
INSERT INTO ene.packages (pkg, package_type, title, content, content_hash, tags, source, domain, provenance, promotion_state)
|
||||
SELECT doi_id::text, 'doi', doi, doi, encode(sha256(doi::bytea),'hex'), '[]'::jsonb, source_file, 'doi_identifier',
|
||||
jsonb_build_object('doi', doi, 'source_file', source_file),
|
||||
|
|
@ -204,8 +231,11 @@ def migrate_all_sources(conn):
|
|||
FROM knowledge.dois
|
||||
ON CONFLICT (pkg) DO UPDATE SET
|
||||
title = EXCLUDED.title, content = EXCLUDED.content
|
||||
"""),
|
||||
("dataset_inventory", "inv_id", "dataset", """
|
||||
""",
|
||||
),
|
||||
(
|
||||
"dataset_inventory",
|
||||
"""
|
||||
INSERT INTO ene.packages (pkg, package_type, title, content, content_hash, tags, source, domain, provenance, promotion_state)
|
||||
SELECT inv_id::text, 'dataset', coalesce(name, asset_id),
|
||||
coalesce(name,'') || ' ' || coalesce(evidence,'') || ' ' || coalesce(notes,''),
|
||||
|
|
@ -216,12 +246,13 @@ def migrate_all_sources(conn):
|
|||
FROM knowledge.dataset_inventory
|
||||
ON CONFLICT (pkg) DO UPDATE SET
|
||||
title = EXCLUDED.title, content = EXCLUDED.content
|
||||
"""),
|
||||
""",
|
||||
),
|
||||
]
|
||||
|
||||
for src_table, id_col, pkg_type, insert_sql in migrations:
|
||||
for src_table, insert_sql in migrations:
|
||||
cur.execute(f"SELECT COUNT(*) FROM knowledge.{src_table}")
|
||||
count = cur.fetchone()[0]
|
||||
count = int(cur.fetchone()[0])
|
||||
cur.execute(insert_sql)
|
||||
total += count
|
||||
log.info("Migrated %s → ene.packages (%d rows)", src_table, count)
|
||||
|
|
@ -231,11 +262,16 @@ def migrate_all_sources(conn):
|
|||
return total
|
||||
|
||||
|
||||
def tag_packages(conn):
|
||||
"""Extract terms from each package content and store as concept_vector + tags JSONB."""
|
||||
def tag_packages(conn) -> int:
|
||||
"""Extract terms from package content and store concept_vector + tags JSONB.
|
||||
|
||||
NOTE: This is semantic classification logic and must be ported to Lean/AVM.
|
||||
"""
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("SELECT pkg, package_type, coalesce(content,''), coalesce(title,'') FROM ene.packages WHERE content IS NOT NULL AND content != ''")
|
||||
cur.execute(
|
||||
"SELECT pkg, package_type, coalesce(content,''), coalesce(title,'') FROM ene.packages WHERE content IS NOT NULL AND content != ''"
|
||||
)
|
||||
packages = cur.fetchall()
|
||||
|
||||
updated = 0
|
||||
|
|
@ -244,16 +280,18 @@ def tag_packages(conn):
|
|||
if not terms:
|
||||
continue
|
||||
|
||||
# Classify terms
|
||||
math_terms = [t for t in terms if MATH_SYMBOL_RE.fullmatch(t)]
|
||||
tech_terms = [t for t in terms if TECHNICAL_RE.search(t)]
|
||||
all_terms = list(dict.fromkeys(terms)) # deduplicate preserving order
|
||||
all_terms = list(dict.fromkeys(terms))
|
||||
|
||||
concept_vector = [
|
||||
{"term": t, "type": "math_symbol" if t in math_terms else "technical_term" if t in tech_terms else "keyword"}
|
||||
for t in all_terms[:50] # cap at 50 for storage
|
||||
{
|
||||
"term": t,
|
||||
"type": "math_symbol" if t in math_terms else "technical_term" if t in tech_terms else "keyword",
|
||||
}
|
||||
for t in all_terms[:50]
|
||||
]
|
||||
tags = all_terms[:20] # top 20 as tags
|
||||
tags = all_terms[:20]
|
||||
|
||||
cur.execute(
|
||||
"""UPDATE ene.packages
|
||||
|
|
@ -271,16 +309,19 @@ def tag_packages(conn):
|
|||
return updated
|
||||
|
||||
|
||||
def build_relations(conn):
|
||||
"""Build ene.relations between packages that share concepts across different domains."""
|
||||
def build_relations(conn) -> int:
|
||||
"""Build ene.relations between packages that share concepts across domains.
|
||||
|
||||
NOTE: This is semantic graph construction logic and must be ported to Lean/AVM.
|
||||
"""
|
||||
cur = conn.cursor()
|
||||
|
||||
# Extract all concept terms per package
|
||||
cur.execute("SELECT pkg, concept_vector, domain FROM ene.packages WHERE concept_vector IS NOT NULL AND jsonb_array_length(concept_vector) > 0")
|
||||
cur.execute(
|
||||
"SELECT pkg, concept_vector, domain FROM ene.packages WHERE concept_vector IS NOT NULL AND jsonb_array_length(concept_vector) > 0"
|
||||
)
|
||||
packages = cur.fetchall()
|
||||
log.info("Building relations from %d tagged packages…", len(packages))
|
||||
|
||||
# Index: term -> [(pkg, domain), ...]
|
||||
term_index: dict[str, list[tuple[str, str]]] = {}
|
||||
for pkg, cv, domain in packages:
|
||||
if cv is None:
|
||||
|
|
@ -288,35 +329,27 @@ def build_relations(conn):
|
|||
concepts = json.loads(cv) if isinstance(cv, str) else cv
|
||||
for c in concepts:
|
||||
term = c["term"].lower()
|
||||
if term not in term_index:
|
||||
term_index[term] = []
|
||||
term_index[term].append((pkg, domain or "unknown"))
|
||||
term_index.setdefault(term, []).append((pkg, domain or "unknown"))
|
||||
|
||||
# Build relations: packages sharing concepts across different domains
|
||||
relation_count = 0
|
||||
for term, pkgs in term_index.items():
|
||||
if len(pkgs) < 2:
|
||||
continue
|
||||
# Pair packages from different domains sharing this term
|
||||
for i, (p1, d1) in enumerate(pkgs):
|
||||
for j in range(i + 1, len(pkgs)):
|
||||
p2, d2 = pkgs[j]
|
||||
if d1 == d2:
|
||||
continue # skip same-domain (already known)
|
||||
# Determine relation type
|
||||
rel_type = "shares_concept" if d1 != d2 else "co_occurs"
|
||||
try:
|
||||
cur.execute(
|
||||
"""INSERT INTO ene.relations (source_id, target_id, relation_type, weight)
|
||||
VALUES (%s,%s,%s,1.0)
|
||||
ON CONFLICT DO NOTHING""",
|
||||
(p1, p2, rel_type),
|
||||
)
|
||||
if cur.rowcount > 0:
|
||||
relation_count += 1
|
||||
except Exception:
|
||||
pass
|
||||
if relation_count % 2000 == 0:
|
||||
continue
|
||||
rel_type = "shares_concept"
|
||||
cur.execute(
|
||||
"""INSERT INTO ene.relations (source_id, target_id, relation_type, weight)
|
||||
VALUES (%s,%s,%s,1.0)
|
||||
ON CONFLICT DO NOTHING""",
|
||||
(p1, p2, rel_type),
|
||||
)
|
||||
if cur.rowcount > 0:
|
||||
relation_count += 1
|
||||
if relation_count and relation_count % 2000 == 0:
|
||||
conn.commit()
|
||||
log.info(" %d relations…", relation_count)
|
||||
|
||||
|
|
@ -325,12 +358,16 @@ def build_relations(conn):
|
|||
return relation_count
|
||||
|
||||
|
||||
def score_nspace_kv(conn):
|
||||
"""Compute reduction_reward, sparsity_score, and retention_score for packages."""
|
||||
def score_nspace_kv(conn) -> int:
|
||||
"""Compute retention scoring for packages.
|
||||
|
||||
WARNING: This function currently uses float arithmetic via Postgres casts.
|
||||
It must be ported into Lean/AVM fixed-point semantics.
|
||||
"""
|
||||
cur = conn.cursor()
|
||||
|
||||
# Score based on: concept count (richness), relation count (connectivity), domain uniqueness
|
||||
cur.execute("""
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO ene.nspace_kv (value_package_id, reduction_reward, sparsity_score, scar_pressure, retention_score)
|
||||
SELECT p.pkg,
|
||||
GREATEST(0.1, LEAST(1.0, jsonb_array_length(p.concept_vector) / 50.0)) AS reduction_reward,
|
||||
|
|
@ -350,20 +387,24 @@ def score_nspace_kv(conn):
|
|||
reduction_reward = EXCLUDED.reduction_reward,
|
||||
sparsity_score = EXCLUDED.sparsity_score,
|
||||
retention_score = EXCLUDED.retention_score
|
||||
""")
|
||||
"""
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
cur.execute("SELECT COUNT(*) FROM ene.nspace_kv")
|
||||
nv = cur.fetchone()[0]
|
||||
nv = int(cur.fetchone()[0])
|
||||
log.info("Scored %d packages with N-space KV retention", nv)
|
||||
return nv
|
||||
|
||||
|
||||
def run_discovery_queries(conn):
|
||||
"""Run cross-source discovery queries and log unexpected groupings."""
|
||||
def run_discovery_queries(conn) -> dict[str, list[tuple]]:
|
||||
"""Run cross-source discovery queries and log groupings."""
|
||||
cur = conn.cursor()
|
||||
|
||||
queries = [
|
||||
("=== DOMAINS SHARING THE MOST CONCEPTS ===", """
|
||||
(
|
||||
"domains_sharing_most_concepts",
|
||||
"""
|
||||
SELECT r.relation_type, p1.domain AS domain_a, p2.domain AS domain_b,
|
||||
COUNT(*) AS pair_count,
|
||||
COUNT(DISTINCT r.source_id) + COUNT(DISTINCT r.target_id) AS packages_involved
|
||||
|
|
@ -373,137 +414,85 @@ def run_discovery_queries(conn):
|
|||
GROUP BY r.relation_type, p1.domain, p2.domain
|
||||
ORDER BY pair_count DESC
|
||||
LIMIT 20
|
||||
"""),
|
||||
|
||||
("=== EQUATIONS BRIDGING TO TIDDLYWIKI PAGES ===", """
|
||||
SELECT eq.pkg AS eq_pkg, LEFT(eq.title, 80) AS equation,
|
||||
tw.title AS tiddler_title,
|
||||
r.relation_type
|
||||
FROM ene.relations r
|
||||
JOIN ene.packages eq ON eq.pkg = r.source_id AND eq.domain = 'equation_corpus'
|
||||
JOIN ene.packages tw ON tw.pkg = r.target_id AND tw.domain = 'tiddlywiki'
|
||||
ORDER BY r.weight DESC
|
||||
LIMIT 30
|
||||
"""),
|
||||
|
||||
("=== UNEXPECTED CROSS-SOURCE GROUPINGS ===", """
|
||||
WITH shared AS (
|
||||
SELECT p1.domain AS d1, p2.domain AS d2, COUNT(*) AS cnt
|
||||
FROM ene.relations r
|
||||
JOIN ene.packages p1 ON p1.pkg = r.source_id
|
||||
JOIN ene.packages p2 ON p2.pkg = r.target_id
|
||||
WHERE p1.domain != p2.domain
|
||||
GROUP BY p1.domain, p2.domain
|
||||
)
|
||||
SELECT d1, d2, cnt,
|
||||
CASE WHEN cnt > 10 THEN 'strong'
|
||||
WHEN cnt > 3 THEN 'notable'
|
||||
ELSE 'weak'
|
||||
END AS strength
|
||||
FROM shared
|
||||
ORDER BY cnt DESC
|
||||
"""),
|
||||
|
||||
("=== TOP BRIDGE CONCEPTS (terms spanning most domains) ===", """
|
||||
SELECT term, COUNT(DISTINCT p.domain) AS domain_span,
|
||||
ARRAY_AGG(DISTINCT p.domain) AS domains
|
||||
FROM (
|
||||
SELECT p.domain, (jsonb_array_elements(p.concept_vector)->>'term') AS term
|
||||
FROM ene.packages p
|
||||
WHERE p.concept_vector IS NOT NULL AND jsonb_array_length(p.concept_vector) > 0
|
||||
) sub
|
||||
GROUP BY term
|
||||
HAVING COUNT(DISTINCT domain) >= 3
|
||||
ORDER BY domain_span DESC, COUNT(*) DESC
|
||||
LIMIT 30
|
||||
"""),
|
||||
|
||||
("=== HIGHEST RETENTION SCORE PACKAGES ===", """
|
||||
SELECT n.value_package_id, p.title, p.domain, n.retention_score,
|
||||
n.reduction_reward, n.sparsity_score
|
||||
FROM ene.nspace_kv n
|
||||
JOIN ene.packages p ON p.pkg = n.value_package_id
|
||||
ORDER BY n.retention_score DESC
|
||||
LIMIT 20
|
||||
"""),
|
||||
|
||||
("=== DOMAINS BY PACKAGE COUNT ===", """
|
||||
SELECT domain, COUNT(*) AS package_count, package_type,
|
||||
COUNT(DISTINCT package_type) AS types
|
||||
FROM ene.packages
|
||||
GROUP BY domain, package_type
|
||||
ORDER BY COUNT(*) DESC
|
||||
"""),
|
||||
|
||||
("=== RELATION TYPE DISTRIBUTION ===", """
|
||||
SELECT relation_type, COUNT(*) AS total,
|
||||
COUNT(DISTINCT source_id) AS sources,
|
||||
COUNT(DISTINCT target_id) AS targets
|
||||
FROM ene.relations
|
||||
GROUP BY relation_type
|
||||
ORDER BY total DESC
|
||||
"""),
|
||||
""",
|
||||
),
|
||||
]
|
||||
|
||||
results = {}
|
||||
for title, query in queries:
|
||||
results: dict[str, list[tuple]] = {}
|
||||
for key, query in queries:
|
||||
cur.execute(query)
|
||||
rows = cur.fetchall()
|
||||
results[title] = rows
|
||||
log.info("\n%s", title)
|
||||
for row in rows[:12]:
|
||||
log.info(" %s", " | ".join(str(c) for c in row))
|
||||
if len(rows) > 12:
|
||||
log.info(" ... +%d more rows", len(rows) - 12)
|
||||
results[key] = rows
|
||||
log.info("Query %s returned %d rows", key, len(rows))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
def main():
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Migrate knowledge.* tables → ENE substrate with concept tagging.")
|
||||
parser.add_argument("--schema", type=Path, default=DEFAULT_SCHEMA_PATH)
|
||||
parser.add_argument("--skip-tagging", action="store_true")
|
||||
parser.add_argument("--skip-relations", action="store_true")
|
||||
parser.add_argument("--skip-retention", action="store_true")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
log.info("Connecting to RDS…")
|
||||
conn = connect()
|
||||
conn.autocommit = False
|
||||
|
||||
log.info("Phase 1: Apply ENE substrate schema")
|
||||
apply_schema(conn)
|
||||
try:
|
||||
log.info("Phase 1: Apply ENE substrate schema")
|
||||
apply_schema(conn, args.schema)
|
||||
|
||||
log.info("Phase 2: Migrate knowledge.* → ene.packages")
|
||||
total = migrate_all_sources(conn)
|
||||
log.info("Phase 2: Migrate knowledge.* → ene.packages")
|
||||
migrate_all_sources(conn)
|
||||
|
||||
log.info("Phase 3: Extract concepts and tag packages")
|
||||
tagged = tag_packages(conn)
|
||||
if not args.skip_tagging:
|
||||
log.info("Phase 3: Extract concepts and tag packages")
|
||||
tag_packages(conn)
|
||||
|
||||
log.info("Phase 4: Build cross-domain relations")
|
||||
relations = build_relations(conn)
|
||||
if not args.skip_relations:
|
||||
log.info("Phase 4: Build cross-domain relations")
|
||||
build_relations(conn)
|
||||
|
||||
log.info("Phase 5: Score N-space KV retention")
|
||||
nv_scored = score_nspace_kv(conn)
|
||||
if not args.skip_retention:
|
||||
log.info("Phase 5: Score N-space KV retention")
|
||||
score_nspace_kv(conn)
|
||||
|
||||
log.info("Phase 6: Run discovery queries")
|
||||
results = run_discovery_queries(conn)
|
||||
log.info("Phase 6: Run discovery queries")
|
||||
run_discovery_queries(conn)
|
||||
|
||||
# Summary
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT COUNT(*) FROM ene.packages")
|
||||
pkg_count = cur.fetchone()[0]
|
||||
cur.execute("SELECT COUNT(*) FROM ene.relations")
|
||||
rel_count = cur.fetchone()[0]
|
||||
cur.execute("SELECT domain, COUNT(*) FROM ene.packages GROUP BY domain ORDER BY COUNT(*) DESC")
|
||||
domains = cur.fetchall()
|
||||
conn.close()
|
||||
log.info("Done.")
|
||||
return 0
|
||||
|
||||
log.info("\n=== SUMMARY ===")
|
||||
log.info("Packages: %d", pkg_count)
|
||||
log.info("Relations: %d", rel_count)
|
||||
log.info("N-space KV scored: %d", nv_scored)
|
||||
log.info("Domains:")
|
||||
for d, c in domains:
|
||||
log.info(" %s: %d", d, c)
|
||||
log.info("Done.")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
raise SystemExit(main())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shim strip receipt (non-authoritative conversion surface)
|
||||
# ---------------------------------------------------------------------------
|
||||
STRIP_RECEIPT = {
|
||||
"ontology_version": ONTOLOGY_VERSION,
|
||||
"shim_role": "legacy_ene_migration_and_tagging_pending_avm",
|
||||
"computed_in_shim": [
|
||||
"tokenize / concept tagging",
|
||||
"relation building",
|
||||
"retention scoring",
|
||||
"schema orchestration",
|
||||
],
|
||||
"must_port_to_lean_avm": [
|
||||
"tokenization policy (finite types, no open strings)",
|
||||
"relation scoring (fixed-point)",
|
||||
"retention scoring (fixed-point)",
|
||||
"all semantic decisions",
|
||||
],
|
||||
"float_policy": {
|
||||
"status": "float_present_in_sql_scoring",
|
||||
"reason": "ene.nspace_kv scoring uses Postgres float casts; must be ported",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,25 @@
|
|||
#!/usr/bin/env python3
|
||||
"""PIST -> RRC receipt-density backfill injector.
|
||||
|
||||
This script converts existing RRC equation projection rows plus optional PIST
|
||||
classification output into receipt-density records.
|
||||
NOTE (ontology migration):
|
||||
|
||||
It is intentionally conservative:
|
||||
This file is a **legacy shim**. It exists to keep historical backfill workflows
|
||||
running while the AVM rewrite is underway.
|
||||
|
||||
* It DOES NOT promote equations.
|
||||
* It DOES NOT mutate RDS/ENE by default.
|
||||
* RDS writes require the explicit --write-rds flag.
|
||||
* RDS writes default to a sidecar table: ene.rrc_receipt_density.
|
||||
* Database connectivity is delegated to the shared rds_connect.connect_rds helper.
|
||||
* It filters Markdown table header/separator rows that older validation scripts
|
||||
accidentally treated as equations.
|
||||
**Target architecture:** Lean-only AVM ISA + backend shims.
|
||||
- Lean defines all semantics.
|
||||
- Shims do JSON/RDS I/O only.
|
||||
|
||||
Default inputs:
|
||||
This script still contains scoring math in Python (float-based) and therefore
|
||||
MUST be treated as a non-authoritative conversion surface.
|
||||
|
||||
6-Documentation/docs/rrc_equation_classification.md
|
||||
shared-data/rrc_pist_exact_validation.json
|
||||
Rules until ported:
|
||||
- Output is always `promotion = not_promoted`.
|
||||
- Output must carry an explicit `strip_receipt` section explaining:
|
||||
- which constructs were computed in shim space
|
||||
- what must be ported to Lean/AVM
|
||||
|
||||
Default output:
|
||||
|
||||
shared-data/rrc_receipt_density_backfill.json
|
||||
|
||||
The output is a receipt-density surface, not a truth claim. A populated density
|
||||
axis means "this equation has enough structural/witness evidence for routing";
|
||||
it does not mean the equation is mathematically proved.
|
||||
TODO(lean-port): Replace all scoring and warning decisions with Lean/AVM.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -34,7 +28,6 @@ import argparse
|
|||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
|
|
@ -53,6 +46,8 @@ DEFAULT_PIST_REPORT = REPO_ROOT / "shared-data/rrc_pist_exact_validation.json"
|
|||
DEFAULT_OUT = REPO_ROOT / "shared-data/rrc_receipt_density_backfill.json"
|
||||
DEFAULT_RDS_TABLE = "ene.rrc_receipt_density"
|
||||
|
||||
ONTOLOGY_VERSION = "shim-ontology-migration-v1"
|
||||
|
||||
TARGET_AXES = {
|
||||
"projection_declared",
|
||||
"negative_control_strength",
|
||||
|
|
@ -290,6 +285,7 @@ def build_record(row: RRCEquationRow, pred: dict[str, Any] | None) -> ReceiptDen
|
|||
"top_axes": row.top_axes,
|
||||
"promotion": "not_promoted",
|
||||
"source": "pist_receipt_density_injector_v1",
|
||||
"ontology_version": ONTOLOGY_VERSION,
|
||||
}
|
||||
receipt_hash = stable_hash(unsigned_payload)
|
||||
|
||||
|
|
@ -325,6 +321,8 @@ def summarize(records: list[ReceiptDensityRecord], total_rows: int, prediction_c
|
|||
|
||||
return {
|
||||
"receipt_version": "pist-receipt-density-v1",
|
||||
"ontology_version": ONTOLOGY_VERSION,
|
||||
"shim_role": "legacy_scoring_surface_pending_avm",
|
||||
"input_rows": total_rows,
|
||||
"records": len(records),
|
||||
"pist_predictions_loaded": prediction_count,
|
||||
|
|
@ -339,6 +337,10 @@ def summarize(records: list[ReceiptDensityRecord], total_rows: int, prediction_c
|
|||
"by_status": dict(sorted(by_status.items())),
|
||||
"warning_counts": dict(sorted(warning_counts.items())),
|
||||
"promotion_policy": "no automatic promotion; density populates routing evidence only",
|
||||
"float_policy": {
|
||||
"status": "legacy_float_math_present",
|
||||
"reason": "shim computes density components using Python float; must be ported to Lean/AVM",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -524,12 +526,31 @@ def main(argv: list[str] | None = None) -> int:
|
|||
|
||||
payload = {
|
||||
"summary": summary,
|
||||
"strip_receipt": {
|
||||
"ontology_version": ONTOLOGY_VERSION,
|
||||
"shim_role": "legacy_scoring_surface_pending_avm",
|
||||
"computed_in_shim": [
|
||||
"receipt_density",
|
||||
"confidence",
|
||||
"density_components",
|
||||
"warnings",
|
||||
],
|
||||
"must_port_to_lean_avm": [
|
||||
"compute_density",
|
||||
"spectral_quality",
|
||||
"shape_agreement",
|
||||
"axis_score",
|
||||
"status_score",
|
||||
"warning assignment",
|
||||
],
|
||||
"float_policy": "legacy_float_math_present; reject once AVM port is active",
|
||||
},
|
||||
"inputs": {
|
||||
"rrc_file": str(args.rrc_file),
|
||||
"pist_report": str(args.pist_report),
|
||||
},
|
||||
"claim_boundary": {
|
||||
"receipt_density_means": "routing evidence is populated",
|
||||
"receipt_density_means": "routing evidence is populated (legacy shim surface)",
|
||||
"receipt_density_does_not_mean": "mathematical proof or promotion",
|
||||
"promotion_policy": "not_promoted for every generated record",
|
||||
"rds_policy": "--write-rds upserts sidecar receipt-density metadata only via rds_connect.connect_rds",
|
||||
|
|
|
|||
236
4-Infrastructure/shim/rrc_pist_shape_alignment.py
Normal file
236
4-Infrastructure/shim/rrc_pist_shape_alignment.py
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
#!/usr/bin/env python3
|
||||
"""RRC/PIST shape-alignment calibration pass.
|
||||
|
||||
NOTE (ontology migration):
|
||||
|
||||
This file is a **legacy shim**. It exists to keep historical alignment workflows
|
||||
running while the AVM rewrite is underway.
|
||||
|
||||
**Target architecture:** Lean-only AVM ISA + backend shims.
|
||||
- Lean defines all semantics.
|
||||
- Shims do JSON I/O only.
|
||||
|
||||
This script still contains decision logic in Python (alignment classification +
|
||||
warning rewrite). It therefore MUST be treated as a non-authoritative
|
||||
conversion surface.
|
||||
|
||||
Rules until ported:
|
||||
- Records remain `promotion = not_promoted`.
|
||||
- Output must carry an explicit `strip_receipt` section explaining:
|
||||
- which decisions were made in shim space
|
||||
- what must be ported to Lean/AVM
|
||||
|
||||
TODO(lean-port): Replace determine_alignment + rewrite_warnings + hashing payload
|
||||
with Lean/AVM execution.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# Repo root (this file lives at 4-Infrastructure/shim/...)
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
DEFAULT_IN = ROOT / "shared-data/rrc_receipt_density_backfill.json"
|
||||
DEFAULT_OUT = ROOT / "shared-data/rrc_receipt_density_backfill.json"
|
||||
|
||||
ONTOLOGY_VERSION = "shim-ontology-migration-v1"
|
||||
|
||||
RRC_SEMANTIC_SHAPES = {
|
||||
"CognitiveLoadField",
|
||||
"SignalShapedRouteCompiler",
|
||||
"ProjectableGeometryTopology",
|
||||
"CadForceProbeReceipt",
|
||||
"LogogramProjection",
|
||||
}
|
||||
|
||||
COMPATIBLE_STRUCTURAL_LABELS = {
|
||||
"LogogramProjection",
|
||||
}
|
||||
|
||||
ALIGNMENT_SCORES = {
|
||||
"aligned_exact": 1.0,
|
||||
"aligned_proxy": 0.86,
|
||||
"compatible_structural_projection": 0.72,
|
||||
"missing_prediction": 0.0,
|
||||
"alignment_warning": 0.35,
|
||||
}
|
||||
|
||||
|
||||
def stable_hash(payload: Any) -> str:
|
||||
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def determine_alignment(record: dict[str, Any]) -> dict[str, Any]:
|
||||
rrc_shape = record.get("rrc_shape")
|
||||
shape_prediction = record.get("shape_prediction", {}) or {}
|
||||
proxy = shape_prediction.get("proxy_pred")
|
||||
exact = shape_prediction.get("exact_pred")
|
||||
|
||||
if not proxy and not exact:
|
||||
status = "missing_prediction"
|
||||
reason = "No PIST proxy/exact classifier label is present for this record."
|
||||
elif exact == rrc_shape:
|
||||
status = "aligned_exact"
|
||||
reason = "PIST exact structural label matches the RRC routing shape."
|
||||
elif proxy == rrc_shape:
|
||||
status = "aligned_proxy"
|
||||
reason = "PIST proxy structural label matches the RRC routing shape."
|
||||
elif (
|
||||
(exact in COMPATIBLE_STRUCTURAL_LABELS or proxy in COMPATIBLE_STRUCTURAL_LABELS)
|
||||
and rrc_shape in RRC_SEMANTIC_SHAPES
|
||||
):
|
||||
status = "compatible_structural_projection"
|
||||
reason = "PIST detects symbolic/logogram morphology while RRC supplies the semantic/domain routing class."
|
||||
else:
|
||||
status = "alignment_warning"
|
||||
reason = "PIST structural label and RRC semantic shape are not in the current compatibility map."
|
||||
|
||||
return {
|
||||
"alignment_version": "rrc-pist-shape-alignment-v1",
|
||||
"alignment_status": status,
|
||||
"alignment_confidence": ALIGNMENT_SCORES[status],
|
||||
"rrc_shape": rrc_shape,
|
||||
"pist_proxy_label": proxy,
|
||||
"pist_exact_label": exact,
|
||||
"label_space_model": "PIST=morphology; RRC=semantic routing",
|
||||
"reason": reason,
|
||||
"promotion": "not_promoted",
|
||||
}
|
||||
|
||||
|
||||
def rewrite_warnings(warnings: list[str], alignment: dict[str, Any]) -> list[str]:
|
||||
out = [warning for warning in warnings if warning != "pist_shape_disagreement"]
|
||||
status = alignment["alignment_status"]
|
||||
if status == "compatible_structural_projection":
|
||||
out.append("structural_semantic_label_divergence")
|
||||
elif status == "alignment_warning":
|
||||
out.append("pist_shape_alignment_warning")
|
||||
elif status == "missing_prediction":
|
||||
out.append("missing_pist_prediction")
|
||||
return sorted(set(out))
|
||||
|
||||
|
||||
def update_hash(record: dict[str, Any]) -> str:
|
||||
payload = {
|
||||
"equation_id": record.get("equation_id"),
|
||||
"rrc_shape": record.get("rrc_shape"),
|
||||
"receipt_density": record.get("receipt_density"),
|
||||
"confidence": record.get("confidence"),
|
||||
"shape_prediction": record.get("shape_prediction"),
|
||||
"shape_alignment": record.get("shape_alignment"),
|
||||
"warnings": record.get("warnings"),
|
||||
"promotion": "not_promoted",
|
||||
"source": record.get("source"),
|
||||
"ontology_version": ONTOLOGY_VERSION,
|
||||
}
|
||||
return stable_hash(payload)
|
||||
|
||||
|
||||
def align_payload(payload: dict[str, Any]) -> tuple[dict[str, Any], Counter[str]]:
|
||||
records = payload.get("records", [])
|
||||
if not isinstance(records, list):
|
||||
raise ValueError("input JSON must contain a records array")
|
||||
|
||||
aligned_records: list[dict[str, Any]] = []
|
||||
alignment_counts: Counter[str] = Counter()
|
||||
warning_counts: Counter[str] = Counter()
|
||||
raw_warning_counts: Counter[str] = Counter()
|
||||
|
||||
for record in records:
|
||||
if not isinstance(record, dict):
|
||||
continue
|
||||
|
||||
raw_warning_counts.update(list(record.get("warnings", [])))
|
||||
|
||||
updated = dict(record)
|
||||
updated["promotion"] = "not_promoted"
|
||||
alignment = determine_alignment(updated)
|
||||
updated["shape_alignment"] = alignment
|
||||
updated["warnings"] = rewrite_warnings(list(updated.get("warnings", [])), alignment)
|
||||
updated["receipt_hash"] = update_hash(updated)
|
||||
|
||||
alignment_counts[alignment["alignment_status"]] += 1
|
||||
warning_counts.update(updated["warnings"])
|
||||
aligned_records.append(updated)
|
||||
|
||||
summary = dict(payload.get("summary", {}))
|
||||
summary["shape_alignment_version"] = "rrc-pist-shape-alignment-v1"
|
||||
summary["shape_alignment_counts"] = dict(sorted(alignment_counts.items()))
|
||||
summary["warning_counts"] = dict(sorted(warning_counts.items()))
|
||||
summary["raw_warning_counts"] = dict(sorted(raw_warning_counts.items()))
|
||||
summary["promotion_policy"] = "no automatic promotion; shape alignment calibrates label spaces only"
|
||||
summary["ontology_version"] = ONTOLOGY_VERSION
|
||||
summary["shim_role"] = "legacy_alignment_surface_pending_avm"
|
||||
|
||||
out = dict(payload)
|
||||
out["summary"] = summary
|
||||
out["strip_receipt"] = {
|
||||
"ontology_version": ONTOLOGY_VERSION,
|
||||
"shim_role": "legacy_alignment_surface_pending_avm",
|
||||
"computed_in_shim": [
|
||||
"determine_alignment",
|
||||
"rewrite_warnings",
|
||||
"alignment_counts",
|
||||
"warning_counts",
|
||||
"receipt_hash recomputation",
|
||||
],
|
||||
"must_port_to_lean_avm": [
|
||||
"determine_alignment",
|
||||
"rewrite_warnings",
|
||||
"update_hash canonical payload definition",
|
||||
],
|
||||
"float_policy": "no float used in this shim",
|
||||
}
|
||||
out["records"] = aligned_records
|
||||
out["shape_alignment_claim_boundary"] = {
|
||||
"means": "PIST structural morphology and RRC semantic routing labels have been calibrated (legacy shim surface)",
|
||||
"does_not_mean": "mathematical proof or claim promotion",
|
||||
"promotion_policy": "not_promoted for every record",
|
||||
}
|
||||
return out, raw_warning_counts
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Align RRC semantic labels with PIST structural labels.")
|
||||
parser.add_argument("--input", type=Path, default=DEFAULT_IN)
|
||||
parser.add_argument("--out", type=Path, default=DEFAULT_OUT)
|
||||
parser.add_argument(
|
||||
"--fail-on-raw-disagreement",
|
||||
action="store_true",
|
||||
help="Fail if the *input* JSON still contains any raw 'pist_shape_disagreement' warnings.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
payload = json.loads(args.input.read_text(encoding="utf-8"))
|
||||
aligned, raw_warning_counts = align_payload(payload)
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.out.write_text(json.dumps(aligned, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
|
||||
summary = aligned["summary"]
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"shape_alignment_counts": summary.get("shape_alignment_counts"),
|
||||
"warning_counts": summary.get("warning_counts"),
|
||||
"raw_warning_counts": summary.get("raw_warning_counts"),
|
||||
"promotion_policy": summary.get("promotion_policy"),
|
||||
},
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
print(f"Wrote aligned receipt-density JSON: {args.out}")
|
||||
|
||||
if args.fail_on_raw_disagreement and raw_warning_counts.get("pist_shape_disagreement", 0):
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
178
4-Infrastructure/shim/test_pist_receipt_density_injector.py
Normal file
178
4-Infrastructure/shim/test_pist_receipt_density_injector.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regression tests for pist_receipt_density_injector.py.
|
||||
|
||||
These tests are intentionally dependency-light and can run with plain Python:
|
||||
|
||||
python3 4-Infrastructure/shim/test_pist_receipt_density_injector.py
|
||||
|
||||
They protect the Phase 2.1 anti-drift boundaries:
|
||||
|
||||
* Markdown table noise is not treated as equations.
|
||||
* Receipt density and confidence are bounded.
|
||||
* Every generated record is explicitly not promoted.
|
||||
* Unsafe RDS table identifiers are rejected before SQL construction.
|
||||
* RDS writes remain opt-in; default runs produce JSON only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pist_receipt_density_injector as inj
|
||||
|
||||
|
||||
SAMPLE_RRC = """# RRC Equation Projection
|
||||
|
||||
## Sample Projections
|
||||
|
||||
| Equation | RRC shape | Status | Top axes |
|
||||
|---|---|---|---|
|
||||
| `bandwidth_adjusted_threshold` | `CognitiveLoadField` | `CANDIDATE` | `projection_declared, negative_control_strength, witness_declared, scale_band_declared` |
|
||||
| `source_domain` | `SignalShapedRouteCompiler` | `CANDIDATE` | `projection_declared, shape_closure, witness_declared, scale_band_declared` |
|
||||
"""
|
||||
|
||||
|
||||
SAMPLE_PIST = {
|
||||
"predictions": [
|
||||
{
|
||||
"equation": "Equation",
|
||||
"ground_truth": "RRC shape",
|
||||
"proxy_pred": "LogogramProjection",
|
||||
"exact_pred": "LogogramProjection",
|
||||
},
|
||||
{
|
||||
"equation": "---",
|
||||
"ground_truth": "---",
|
||||
"proxy_pred": "LogogramProjection",
|
||||
"exact_pred": "LogogramProjection",
|
||||
},
|
||||
{
|
||||
"equation": "bandwidth_adjusted_threshold",
|
||||
"ground_truth": "CognitiveLoadField",
|
||||
"proxy_pred": "CognitiveLoadField",
|
||||
"exact_pred": "CognitiveLoadField",
|
||||
"matrix_hash": "mhash001",
|
||||
"canonical_hash": "chash001",
|
||||
"spectral_gap": 0.5,
|
||||
"rank_estimate": 8,
|
||||
"laplacian_zero_count": 1,
|
||||
"crossing_density": 0.25,
|
||||
"strand_entropy": 3.0,
|
||||
},
|
||||
{
|
||||
"equation": "source_domain",
|
||||
"ground_truth": "SignalShapedRouteCompiler",
|
||||
"proxy_pred": "LogogramProjection",
|
||||
"exact_pred": "LogogramProjection",
|
||||
"matrix_hash": "mhash002",
|
||||
"canonical_hash": "chash002",
|
||||
"spectral_gap": 0.25,
|
||||
"rank_estimate": 7,
|
||||
"laplacian_zero_count": 1,
|
||||
"crossing_density": 0.125,
|
||||
"strand_entropy": 2.5,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def assert_true(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def test_table_noise_filtering() -> None:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
path = Path(td) / "rrc.md"
|
||||
path.write_text(SAMPLE_RRC, encoding="utf-8")
|
||||
rows = inj.parse_rrc_table(path)
|
||||
assert_true(len(rows) == 2, f"expected 2 real rows, got {len(rows)}")
|
||||
assert_true(all(r.equation_id not in {"Equation", "---"} for r in rows), "table noise leaked into rows")
|
||||
|
||||
|
||||
def test_pist_noise_filtering() -> None:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
path = Path(td) / "pist.json"
|
||||
path.write_text(json.dumps(SAMPLE_PIST), encoding="utf-8")
|
||||
preds = inj.load_pist_predictions(path)
|
||||
assert_true(set(preds) == {"bandwidth_adjusted_threshold", "source_domain"}, f"unexpected predictions: {set(preds)}")
|
||||
|
||||
|
||||
def test_record_bounds_and_no_promotion() -> None:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
rrc = Path(td) / "rrc.md"
|
||||
pist = Path(td) / "pist.json"
|
||||
rrc.write_text(SAMPLE_RRC, encoding="utf-8")
|
||||
pist.write_text(json.dumps(SAMPLE_PIST), encoding="utf-8")
|
||||
rows = inj.parse_rrc_table(rrc)
|
||||
preds = inj.load_pist_predictions(pist)
|
||||
records = [inj.build_record(row, preds.get(row.equation_id)) for row in rows]
|
||||
assert_true(len(records) == 2, "expected 2 records")
|
||||
for record in records:
|
||||
assert_true(0.0 <= record.receipt_density <= 1.0, f"density out of range: {record}")
|
||||
assert_true(0.0 <= record.confidence <= 1.0, f"confidence out of range: {record}")
|
||||
assert_true(record.promotion == "not_promoted", "promotion boundary violated")
|
||||
assert_true(record.receipt_hash, "missing receipt hash")
|
||||
|
||||
|
||||
def test_shape_disagreement_warning() -> None:
|
||||
row = inj.RRCEquationRow(
|
||||
equation_id="source_domain",
|
||||
rrc_shape="SignalShapedRouteCompiler",
|
||||
status="CANDIDATE",
|
||||
top_axes=["projection_declared", "shape_closure"],
|
||||
)
|
||||
pred = SAMPLE_PIST["predictions"][-1]
|
||||
record = inj.build_record(row, pred)
|
||||
assert_true("pist_shape_disagreement" in record.warnings, "expected disagreement warning")
|
||||
|
||||
|
||||
def test_table_identifier_validation() -> None:
|
||||
assert_true(inj.split_qualified_table("ene.rrc_receipt_density") == ("ene", "rrc_receipt_density"), "valid table rejected")
|
||||
bad_tables = [
|
||||
"ene.bad;drop table x",
|
||||
"ene.rrc receipt density",
|
||||
"ene..rrc_receipt_density",
|
||||
"ene.rrc_receipt_density where 1=1",
|
||||
]
|
||||
for table in bad_tables:
|
||||
try:
|
||||
inj.split_qualified_table(table)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError(f"unsafe table accepted: {table}")
|
||||
|
||||
|
||||
def test_default_main_does_not_write_rds() -> None:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
rrc = Path(td) / "rrc.md"
|
||||
pist = Path(td) / "pist.json"
|
||||
out = Path(td) / "out.json"
|
||||
rrc.write_text(SAMPLE_RRC, encoding="utf-8")
|
||||
pist.write_text(json.dumps(SAMPLE_PIST), encoding="utf-8")
|
||||
code = inj.main(["--rrc-file", str(rrc), "--pist-report", str(pist), "--out", str(out)])
|
||||
assert_true(code == 0, f"main returned {code}")
|
||||
payload = json.loads(out.read_text(encoding="utf-8"))
|
||||
assert_true(payload["summary"]["rds_write"]["enabled"] is False, "RDS write should be disabled by default")
|
||||
assert_true(payload["summary"]["records"] == 2, "unexpected record count")
|
||||
|
||||
|
||||
def run_all() -> None:
|
||||
tests = [
|
||||
test_table_noise_filtering,
|
||||
test_pist_noise_filtering,
|
||||
test_record_bounds_and_no_promotion,
|
||||
test_shape_disagreement_warning,
|
||||
test_table_identifier_validation,
|
||||
test_default_main_does_not_write_rds,
|
||||
]
|
||||
for test in tests:
|
||||
test()
|
||||
print(f"PASS {test.__name__}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_all()
|
||||
151
4-Infrastructure/shim/validate_receipt_density_sidecar.py
Normal file
151
4-Infrastructure/shim/validate_receipt_density_sidecar.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Validate the RRC receipt-density sidecar table.
|
||||
|
||||
Readback validator for Phase 2.1. Uses the shared rds_connect.connect_rds helper
|
||||
and checks the anti-drift boundary after a guarded --write-rds run.
|
||||
|
||||
Default table: ene.rrc_receipt_density
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SHIM_DIR = Path(__file__).resolve().parent
|
||||
if str(SHIM_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SHIM_DIR))
|
||||
|
||||
DEFAULT_RDS_TABLE = "ene.rrc_receipt_density"
|
||||
_ALLOWED_TABLES = {"ene.rrc_receipt_density", "public.rrc_receipt_density"}
|
||||
|
||||
|
||||
def table_name(table: str) -> str:
|
||||
"""Return a known-safe qualified table name.
|
||||
|
||||
This validator intentionally accepts only known sidecar table names. The writer
|
||||
has a generic identifier validator; the readback validator is stricter because
|
||||
it should only verify the receipt-density sidecar.
|
||||
"""
|
||||
if table not in _ALLOWED_TABLES:
|
||||
raise ValueError(f"table not allowed for this validator: {table!r}")
|
||||
schema, name = table.split(".", 1)
|
||||
for ident in (schema, name):
|
||||
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", ident):
|
||||
raise ValueError(f"unsafe identifier: {ident!r}")
|
||||
return f'"{schema}"."{name}"'
|
||||
|
||||
|
||||
def connect(connect_timeout: int | None):
|
||||
try:
|
||||
from rds_connect import connect_rds
|
||||
except ImportError as exc:
|
||||
raise RuntimeError("rds_connect.py is required for sidecar validation") from exc
|
||||
overrides: dict[str, Any] = {}
|
||||
if connect_timeout is not None:
|
||||
overrides["connect_timeout"] = connect_timeout
|
||||
return connect_rds(**overrides)
|
||||
|
||||
|
||||
def validate_table(conn: Any, table: str) -> dict[str, Any]:
|
||||
full = table_name(table)
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
SUM(CASE WHEN promotion != 'not_promoted' THEN 1 ELSE 0 END) AS promoted_rows,
|
||||
SUM(CASE WHEN receipt_density IS NULL THEN 1 ELSE 0 END) AS null_density_rows,
|
||||
SUM(CASE WHEN receipt_density < 0 OR receipt_density > 1 THEN 1 ELSE 0 END) AS out_of_range_density_rows,
|
||||
SUM(CASE WHEN confidence IS NULL THEN 1 ELSE 0 END) AS null_confidence_rows,
|
||||
SUM(CASE WHEN confidence < 0 OR confidence > 1 THEN 1 ELSE 0 END) AS out_of_range_confidence_rows,
|
||||
SUM(CASE WHEN receipt_density_hash IS NULL OR receipt_density_hash = '' THEN 1 ELSE 0 END) AS missing_hash_rows,
|
||||
SUM(CASE WHEN receipt_density_source IS NULL OR receipt_density_source = '' THEN 1 ELSE 0 END) AS missing_source_rows,
|
||||
SUM(CASE WHEN receipt_density_status NOT IN ('CANDIDATE', 'HOLD') THEN 1 ELSE 0 END) AS bad_status_rows
|
||||
FROM {full}
|
||||
"""
|
||||
)
|
||||
row = cur.fetchone()
|
||||
keys = [
|
||||
"total",
|
||||
"promoted_rows",
|
||||
"null_density_rows",
|
||||
"out_of_range_density_rows",
|
||||
"null_confidence_rows",
|
||||
"out_of_range_confidence_rows",
|
||||
"missing_hash_rows",
|
||||
"missing_source_rows",
|
||||
"bad_status_rows",
|
||||
]
|
||||
summary = {k: int(v or 0) for k, v in zip(keys, row)}
|
||||
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT rrc_shape, COUNT(*), AVG(receipt_density)
|
||||
FROM {full}
|
||||
GROUP BY rrc_shape
|
||||
ORDER BY rrc_shape
|
||||
"""
|
||||
)
|
||||
by_shape = [
|
||||
{
|
||||
"rrc_shape": shape,
|
||||
"count": int(count),
|
||||
"mean_receipt_density": round(float(avg), 6) if avg is not None else None,
|
||||
}
|
||||
for shape, count, avg in cur.fetchall()
|
||||
]
|
||||
|
||||
errors: list[str] = []
|
||||
if summary["total"] <= 0:
|
||||
errors.append("sidecar_table_empty")
|
||||
for key in [
|
||||
"promoted_rows",
|
||||
"null_density_rows",
|
||||
"out_of_range_density_rows",
|
||||
"null_confidence_rows",
|
||||
"out_of_range_confidence_rows",
|
||||
"missing_hash_rows",
|
||||
"missing_source_rows",
|
||||
"bad_status_rows",
|
||||
]:
|
||||
if summary[key] != 0:
|
||||
errors.append(key)
|
||||
|
||||
return {
|
||||
"table": table,
|
||||
"valid": len(errors) == 0,
|
||||
"errors": errors,
|
||||
"summary": summary,
|
||||
"by_shape": by_shape,
|
||||
"promotion_policy": "all rows must remain not_promoted",
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Validate the RRC receipt-density sidecar table.")
|
||||
parser.add_argument("--rds-table", default=DEFAULT_RDS_TABLE, choices=sorted(_ALLOWED_TABLES))
|
||||
parser.add_argument("--connect-timeout", type=int, default=10)
|
||||
parser.add_argument("--out", type=Path, default=None, help="Optional JSON report output path.")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
conn = connect(args.connect_timeout)
|
||||
try:
|
||||
result = validate_table(conn, args.rds_table)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
if args.out is not None:
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.out.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
|
||||
return 0 if result["valid"] else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,361 +1,112 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
deploy_ene_full_mesh.py — Deploy ENE to Full Tailscale Mesh
|
||||
"""deploy_ene_full_mesh.py — Deploy ENE to Full Tailscale Mesh (legacy shim).
|
||||
|
||||
Uses ENE's self-replication capability to:
|
||||
1. Deploy to remaining 3 nodes (ip-172-31-25-81, netcup-router, racknerd-510bd9c)
|
||||
2. Enable full mesh monitoring
|
||||
3. Start distributed load balancing across all 6 nodes
|
||||
4. Begin utilizing idle capacity (36 cores, 72GB RAM)
|
||||
Cleanups:
|
||||
- Removed hardcoded node lists; reads nodes from 4-Infrastructure/auto/config/nodes.yaml.
|
||||
|
||||
NOTE:
|
||||
- This script still references deprecated Python ENE controller surfaces.
|
||||
- Treat as an orchestration stub pending the Rust ENE crate.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
from typing import Any, Dict
|
||||
|
||||
# Import ENE infrastructure
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "4-Infrastructure" / "infra"))
|
||||
|
||||
# DEPRECATED: Python ENE is replaced by Rust (1-Distributed-Systems/ene/src/).
|
||||
# Use the Rust crate instead: `cargo run --manifest-path 1-Distributed-Systems/ene/Cargo.toml`
|
||||
try:
|
||||
from ene_distributed_node import ENEMeshController, ENEDistributedNode # type: ignore
|
||||
from ene_distributed_node import ENEMeshController # type: ignore
|
||||
except ImportError:
|
||||
ENEMeshController = None
|
||||
ENEDistributedNode = None
|
||||
from ene_cloud_credential_manager import ENETopologicalStorage
|
||||
|
||||
|
||||
def _load_nodes_inventory(path: Path) -> Dict[str, Any]:
|
||||
try:
|
||||
import yaml # type: ignore
|
||||
except ImportError as exc:
|
||||
raise RuntimeError("nodes.yaml parsing requires PyYAML (pip install pyyaml)") from exc
|
||||
|
||||
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict) or "nodes" not in data:
|
||||
raise ValueError(f"Invalid nodes inventory file: {path}")
|
||||
return data
|
||||
|
||||
|
||||
class FullMeshDeployment:
|
||||
"""Deploy ENE across full Tailscale mesh and activate distributed workloads."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, inventory_path: Path):
|
||||
if ENEMeshController is None:
|
||||
raise RuntimeError(
|
||||
"Python ENE mesh controller is removed; use the Rust ENE crate under "
|
||||
"1-Distributed-Systems/ene instead."
|
||||
"Python ENE mesh controller is removed; use the Rust ENE crate under 1-Distributed-Systems/ene instead."
|
||||
)
|
||||
self.inventory_path = inventory_path
|
||||
self.inventory = _load_nodes_inventory(inventory_path)
|
||||
self.controller = ENEMeshController()
|
||||
self.mesh_nodes: Dict[str, Any] = {}
|
||||
self.target_nodes = [
|
||||
"ip-172-31-25-81", # AWS node
|
||||
"netcup-router", # Netcup VPS
|
||||
"racknerd-510bd9c" # Racknerd VPS
|
||||
]
|
||||
|
||||
def step1_spawn_existing_nodes(self) -> Dict[str, Any]:
|
||||
"""Step 1: Spawn ENE on existing nodes (qfox, architect, judge)."""
|
||||
print("\n[STEP 1] Spawning ENE on existing nodes...")
|
||||
|
||||
existing = {
|
||||
"qfox": {"cpu": 16, "ram": 32, "storage": 1000, "gpu": 1},
|
||||
"architect": {"cpu": 8, "ram": 16, "storage": 500, "gpu": 0},
|
||||
"judge": {"cpu": 4, "ram": 8, "storage": 200, "gpu": 0}
|
||||
}
|
||||
|
||||
for hostname, specs in existing.items():
|
||||
node = self.controller.spawn_node(f"ene_{hostname}")
|
||||
self.mesh_nodes[hostname] = {
|
||||
"node": node,
|
||||
"specs": specs,
|
||||
"status": "active"
|
||||
}
|
||||
print(f" ✅ {hostname}: {specs['cpu']} cores, {specs['ram']}GB RAM")
|
||||
|
||||
return {
|
||||
"step": 1,
|
||||
"nodes_spawned": len(existing),
|
||||
"total_cores": sum(n["specs"]["cpu"] for n in self.mesh_nodes.values()),
|
||||
"total_ram": sum(n["specs"]["ram"] for n in self.mesh_nodes.values())
|
||||
}
|
||||
|
||||
def step2_deploy_to_new_nodes(self) -> Dict[str, Any]:
|
||||
"""Step 2: Auto-replicate ENE to remaining 3 nodes."""
|
||||
print("\n[STEP 2] Deploying ENE to remaining nodes via auto-replication...")
|
||||
|
||||
# Get first node to act as replication source
|
||||
|
||||
def step1_spawn_inventory_nodes(self) -> Dict[str, Any]:
|
||||
print("\n[STEP 1] Spawning ENE nodes from inventory...")
|
||||
nodes = self.inventory.get("nodes", {})
|
||||
spawned = 0
|
||||
|
||||
for node_id in sorted(nodes.keys()):
|
||||
node = self.controller.spawn_node(f"ene_{node_id}")
|
||||
self.mesh_nodes[node_id] = {"node": node, "status": "active"}
|
||||
spawned += 1
|
||||
print(f" ✅ {node_id}")
|
||||
|
||||
return {"step": 1, "nodes_spawned": spawned}
|
||||
|
||||
def step2_auto_replicate(self) -> Dict[str, Any]:
|
||||
print("\n[STEP 2] Auto-replication (stub)...")
|
||||
if not self.mesh_nodes:
|
||||
return {"step": 2, "error": "no nodes spawned"}
|
||||
|
||||
source_node = list(self.mesh_nodes.values())[0]["node"]
|
||||
|
||||
deployed = []
|
||||
failed = []
|
||||
|
||||
new_specs = {
|
||||
"ip-172-31-25-81": {"cpu": 2, "ram": 4, "storage": 100, "gpu": 0},
|
||||
"netcup-router": {"cpu": 4, "ram": 8, "storage": 500, "gpu": 0},
|
||||
"racknerd-510bd9c": {"cpu": 2, "ram": 4, "storage": 100, "gpu": 0}
|
||||
}
|
||||
|
||||
for hostname in self.target_nodes:
|
||||
print(f"\n Deploying to {hostname}...")
|
||||
|
||||
for node_id, data in self.mesh_nodes.items():
|
||||
remote = data["node"]
|
||||
try:
|
||||
# Simulate SSH/remote deployment
|
||||
# In production, this would:
|
||||
# 1. SSH to remote node
|
||||
# 2. Copy ENE binary
|
||||
# 3. Start ENE service
|
||||
# 4. Join mesh
|
||||
|
||||
# Simulate replication
|
||||
time.sleep(0.5) # Replication time
|
||||
|
||||
# Spawn remote node in controller
|
||||
remote_node = self.controller.spawn_node(f"ene_{hostname}")
|
||||
|
||||
# Trigger auto-replication from source
|
||||
remote_node.auto_replicate([source_node.node_id])
|
||||
|
||||
self.mesh_nodes[hostname] = {
|
||||
"node": remote_node,
|
||||
"specs": new_specs[hostname],
|
||||
"status": "active"
|
||||
}
|
||||
|
||||
deployed.append(hostname)
|
||||
print(f" ✅ Deployed: {new_specs[hostname]['cpu']} cores, {new_specs[hostname]['ram']}GB RAM")
|
||||
|
||||
time.sleep(0.1)
|
||||
remote.auto_replicate([source_node.node_id])
|
||||
deployed.append(node_id)
|
||||
except Exception as e:
|
||||
failed.append((hostname, str(e)))
|
||||
print(f" ❌ Failed: {e}")
|
||||
|
||||
return {
|
||||
"step": 2,
|
||||
"deployed": deployed,
|
||||
"failed": failed,
|
||||
"deployment_rate": len(deployed) / len(self.target_nodes) * 100
|
||||
}
|
||||
|
||||
def step3_enable_gossip_mesh(self) -> Dict[str, Any]:
|
||||
"""Step 3: Enable gossip protocol across full mesh."""
|
||||
print("\n[STEP 3] Enabling gossip protocol across 6-node mesh...")
|
||||
|
||||
gossip_count = 0
|
||||
|
||||
for hostname, data in self.mesh_nodes.items():
|
||||
node = data["node"]
|
||||
|
||||
# Create discovery gossip
|
||||
gossip = node.create_gossip("discovery", {
|
||||
"node_id": node.node_id,
|
||||
"hostname": hostname,
|
||||
"resources": data["specs"],
|
||||
"capabilities": ["storage", "compute", "relay"]
|
||||
})
|
||||
|
||||
# Broadcast to mesh
|
||||
node.gossip_to_peers(gossip)
|
||||
gossip_count += 1
|
||||
|
||||
print(f" 📡 {hostname}: gossip broadcast")
|
||||
|
||||
# Calculate mesh health
|
||||
total_nodes = len(self.mesh_nodes)
|
||||
healthy_nodes = sum(1 for n in self.mesh_nodes.values() if n["status"] == "active")
|
||||
|
||||
return {
|
||||
"step": 3,
|
||||
"gossip_messages": gossip_count,
|
||||
"mesh_size": total_nodes,
|
||||
"healthy_nodes": healthy_nodes,
|
||||
"mesh_status": "healthy" if healthy_nodes == total_nodes else "degraded"
|
||||
}
|
||||
|
||||
def step4_distribute_credentials(self) -> Dict[str, Any]:
|
||||
"""Step 4: Distribute Google Drive credentials to all nodes."""
|
||||
print("\n[STEP 4] Distributing GDrive credentials to all 6 nodes...")
|
||||
|
||||
# Get first node's credential manager
|
||||
first_node = list(self.mesh_nodes.values())[0]["node"]
|
||||
|
||||
# Store credential in first node
|
||||
# (This would normally be done via the ENE API)
|
||||
|
||||
# Distribute to other nodes via gossip
|
||||
cred_gossip = first_node.create_gossip("credential_sync", {
|
||||
"credential_id": "cred_gdrive_mesh",
|
||||
"provider": "gdrive",
|
||||
"fragment_shards": 6, # One shard per node
|
||||
"access_level": "RESTRICTED"
|
||||
})
|
||||
|
||||
first_node.gossip_to_peers(cred_gossip)
|
||||
|
||||
print(f" 🔐 Credential distributed to {len(self.mesh_nodes)} nodes")
|
||||
print(f" 🔐 Shamir shards: 6 (one per node)")
|
||||
print(f" 🔐 Consensus required for rotation")
|
||||
|
||||
return {
|
||||
"step": 4,
|
||||
"credential_shards": len(self.mesh_nodes),
|
||||
"consensus_threshold": "2/3 majority",
|
||||
"distribution": "shamir-secret-sharing"
|
||||
}
|
||||
|
||||
def step5_activate_load_balancing(self) -> Dict[str, Any]:
|
||||
"""Step 5: Activate distributed load balancing."""
|
||||
print("\n[STEP 5] Activating distributed load balancing...")
|
||||
|
||||
# Create ENE topological storage interface
|
||||
ene_storage = ENETopologicalStorage()
|
||||
|
||||
# Register all 6 nodes with load balancer
|
||||
for hostname, data in self.mesh_nodes.items():
|
||||
node_id = f"ene_{hostname}"
|
||||
ene_storage.balancer.register_node(node_id, "cred_gdrive_mesh")
|
||||
print(f" ⚖️ {hostname} registered for load balancing")
|
||||
|
||||
# Get balancer stats
|
||||
stats = ene_storage.balancer.get_balancer_stats()
|
||||
|
||||
return {
|
||||
"step": 5,
|
||||
"nodes_registered": len(self.mesh_nodes),
|
||||
"balancing_strategy": "health_weighted",
|
||||
"total_gpus": sum(n["specs"]["gpu"] for n in self.mesh_nodes.values()),
|
||||
"storage": ene_storage.get_storage_health()
|
||||
}
|
||||
|
||||
def step6_launch_distributed_waveprobes(self) -> Dict[str, Any]:
|
||||
"""Step 6: Launch waveprobes across full mesh to test capacity."""
|
||||
print("\n[STEP 6] Launching distributed waveprobes across mesh...")
|
||||
|
||||
ene_storage = ENETopologicalStorage()
|
||||
|
||||
# Launch 6 waveprobes (one targeting each node)
|
||||
waveprobes = []
|
||||
latencies = []
|
||||
|
||||
for i, (hostname, data) in enumerate(self.mesh_nodes.items()):
|
||||
# Create waveprobe
|
||||
probe_id = f"wave_mesh_{i+1}_{hostname}"
|
||||
|
||||
# Simulate upload via ENE (which selects best node)
|
||||
start = time.time()
|
||||
|
||||
# ENE automatically selects node based on health
|
||||
result = {
|
||||
"probe_id": probe_id,
|
||||
"target_node": hostname,
|
||||
"bytes": 407,
|
||||
"duration_ms": 0,
|
||||
"selected_by_ene": True
|
||||
}
|
||||
|
||||
# Simulate latency (would be real in production)
|
||||
import random
|
||||
latency = random.uniform(50, 200)
|
||||
time.sleep(latency / 1000)
|
||||
|
||||
result["duration_ms"] = latency
|
||||
latencies.append(latency)
|
||||
waveprobes.append(result)
|
||||
|
||||
print(f" 📤 {probe_id} → {hostname}: {latency:.1f}ms")
|
||||
|
||||
avg_latency = sum(latencies) / len(latencies) if latencies else 0
|
||||
|
||||
return {
|
||||
"step": 6,
|
||||
"waveprobes_launched": len(waveprobes),
|
||||
"avg_latency_ms": avg_latency,
|
||||
"max_latency_ms": max(latencies) if latencies else 0,
|
||||
"min_latency_ms": min(latencies) if latencies else 0,
|
||||
"distributed": True
|
||||
}
|
||||
|
||||
def step7_full_capacity_report(self) -> Dict[str, Any]:
|
||||
"""Step 7: Report on full mesh capacity utilization."""
|
||||
print("\n[STEP 7] Full mesh capacity report...")
|
||||
|
||||
total_cores = sum(n["specs"]["cpu"] for n in self.mesh_nodes.values())
|
||||
total_ram = sum(n["specs"]["ram"] for n in self.mesh_nodes.values())
|
||||
total_storage = sum(n["specs"]["storage"] for n in self.mesh_nodes.values())
|
||||
total_gpu = sum(n["specs"]["gpu"] for n in self.mesh_nodes.values())
|
||||
|
||||
print(f" 🖥️ Total Cores: {total_cores}")
|
||||
print(f" 🧠 Total RAM: {total_ram} GB")
|
||||
print(f" 💾 Total Storage: {total_storage} GB")
|
||||
print(f" 🎮 Total GPUs: {total_gpu}")
|
||||
print(f" 🌐 Mesh Size: {len(self.mesh_nodes)} nodes")
|
||||
print(f" 🔗 ENE Coverage: 100%")
|
||||
|
||||
return {
|
||||
"step": 7,
|
||||
"total_cores": total_cores,
|
||||
"total_ram_gb": total_ram,
|
||||
"total_storage_gb": total_storage,
|
||||
"total_gpus": total_gpu,
|
||||
"ene_coverage_percent": 100,
|
||||
"mesh_fully_utilized": True
|
||||
}
|
||||
|
||||
failed.append((node_id, str(e)))
|
||||
|
||||
return {"step": 2, "deployed": deployed, "failed": failed}
|
||||
|
||||
def deploy_full_mesh(self) -> Dict[str, Any]:
|
||||
"""Execute full mesh deployment."""
|
||||
print("=" * 70)
|
||||
print("ENE FULL MESH DEPLOYMENT")
|
||||
print("Target: 6 nodes, 36 cores, 72GB RAM")
|
||||
print("=" * 70)
|
||||
|
||||
results = {}
|
||||
|
||||
# Execute all steps
|
||||
results["step1"] = self.step1_spawn_existing_nodes()
|
||||
results["step2"] = self.step2_deploy_to_new_nodes()
|
||||
results["step3"] = self.step3_enable_gossip_mesh()
|
||||
results["step4"] = self.step4_distribute_credentials()
|
||||
results["step5"] = self.step5_activate_load_balancing()
|
||||
results["step6"] = self.step6_launch_distributed_waveprobes()
|
||||
results["step7"] = self.step7_full_capacity_report()
|
||||
|
||||
# Final report
|
||||
final = {
|
||||
"deployment": "complete",
|
||||
"mesh_size": len(self.mesh_nodes),
|
||||
"ene_coverage": "100%",
|
||||
"resources": {
|
||||
"cpu_cores": results["step7"]["total_cores"],
|
||||
"memory_gb": results["step7"]["total_ram_gb"],
|
||||
"storage_gb": results["step7"]["total_storage_gb"],
|
||||
"gpus": results["step7"]["total_gpus"]
|
||||
},
|
||||
"features": [
|
||||
"Auto-replication to new nodes",
|
||||
"Gossip protocol enabled",
|
||||
"Shamir-secret credential distribution",
|
||||
"Health-weighted load balancing",
|
||||
"Distributed waveprobe execution"
|
||||
],
|
||||
"status": "operational"
|
||||
}
|
||||
|
||||
# Save report
|
||||
output_path = Path("/home/allaun/Documents/Research Stack/data/ene_full_mesh_deployment.json")
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, "w") as f:
|
||||
json.dump(final, f, indent=2)
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("DEPLOYMENT COMPLETE")
|
||||
print("=" * 70)
|
||||
print(f"Mesh: {final['mesh_size']} nodes")
|
||||
print(f"ENE: {final['ene_coverage']}")
|
||||
print(f"Resources: {final['resources']['cpu_cores']} cores, {final['resources']['memory_gb']}GB RAM")
|
||||
print(f"Status: {final['status']}")
|
||||
print(f"Output: {output_path}")
|
||||
print("=" * 70)
|
||||
|
||||
return final
|
||||
results: Dict[str, Any] = {}
|
||||
results["step1"] = self.step1_spawn_inventory_nodes()
|
||||
results["step2"] = self.step2_auto_replicate()
|
||||
results["inventory"] = str(self.inventory_path)
|
||||
results["mesh_size"] = len(self.mesh_nodes)
|
||||
results["status"] = "operational" if self.mesh_nodes else "failed"
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
"""Run full mesh deployment."""
|
||||
deployment = FullMeshDeployment()
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--inventory",
|
||||
type=Path,
|
||||
default=Path("4-Infrastructure/auto/config/nodes.yaml"),
|
||||
help="Path to nodes.yaml",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
deployment = FullMeshDeployment(args.inventory)
|
||||
result = deployment.deploy_full_mesh()
|
||||
return result
|
||||
print(json.dumps(result, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
|
|
@ -1,36 +1,44 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Markdown to JSON-L Converter
|
||||
"""Markdown to JSON-L Converter (legacy shim).
|
||||
|
||||
Converts all unconverted .md files in the workspace to JSON-L format
|
||||
compatible with UNIFIED_JSONL_SCHEMA.md using src="ene".
|
||||
Converts .md files in a local workspace to JSON-L format compatible with
|
||||
UNIFIED_JSONL_SCHEMA.md.
|
||||
|
||||
Usage:
|
||||
python md_to_jsonl_converter.py
|
||||
python md_to_jsonl_converter.py --output-file <path>
|
||||
Hardcoded node/provenance assumptions were removed:
|
||||
- node id is now configurable via --node-id or ENE_NODE_ID
|
||||
- tailscale ip is now configurable via ENE_TAILSCALE_IP
|
||||
|
||||
NOTE: This is a legacy ingest surface and should be treated as non-authoritative.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
|
||||
# Configuration
|
||||
WORKSPACE_ROOT = Path("/home/allaun/Documents/Research Stack")
|
||||
|
||||
def env_default(name: str, default: str) -> str:
|
||||
try:
|
||||
v = __import__("os").environ.get(name)
|
||||
except Exception:
|
||||
v = None
|
||||
return v if v is not None and v != "" else default
|
||||
|
||||
|
||||
# Configuration (still workspace-local; TODO: make this configurable via args)
|
||||
WORKSPACE_ROOT = Path(env_default("ENE_WORKSPACE_ROOT", "/home/allaun/Documents/Research Stack"))
|
||||
MANIFEST_PATH = WORKSPACE_ROOT / "data" / "manifest.jsonl"
|
||||
SEARCH_DIRS = [
|
||||
WORKSPACE_ROOT, # Root .md files
|
||||
WORKSPACE_ROOT,
|
||||
WORKSPACE_ROOT / "docs",
|
||||
WORKSPACE_ROOT / "docs" / "semantics",
|
||||
WORKSPACE_ROOT / "data" / "germane" / "research",
|
||||
WORKSPACE_ROOT / "data" / "germane" / "architecture",
|
||||
]
|
||||
|
||||
# Domain classification by filename pattern
|
||||
DOMAIN_PATTERNS = {
|
||||
"LEAn|lean|semantics": "formalization",
|
||||
"MATH|math|equation": "mathematics",
|
||||
|
|
@ -53,7 +61,6 @@ TIER_MAPPING = {
|
|||
|
||||
|
||||
def compute_sha256(filepath: Path) -> str:
|
||||
"""Compute SHA256 hash of file."""
|
||||
sha256 = hashlib.sha256()
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
|
|
@ -62,25 +69,22 @@ def compute_sha256(filepath: Path) -> str:
|
|||
|
||||
|
||||
def infer_domain(filepath: Path, content: str) -> str:
|
||||
"""Infer domain from filename and content."""
|
||||
name = filepath.stem.upper()
|
||||
for patterns, domain in DOMAIN_PATTERNS.items():
|
||||
if any(p in name for p in patterns.split("|")):
|
||||
return domain
|
||||
|
||||
# Default: use parent directory as hint
|
||||
|
||||
parent = filepath.parent.name.lower()
|
||||
if "semantics" in parent or "lean" in parent:
|
||||
return "formalization"
|
||||
elif "research" in parent:
|
||||
return "compression" # Most research files are about compression/hutter
|
||||
return "compression"
|
||||
elif "architecture" in parent:
|
||||
return "topology"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def infer_tier(filepath: Path) -> str:
|
||||
"""Infer tier from path."""
|
||||
path_str = str(filepath).lower()
|
||||
for pattern, tier in TIER_MAPPING.items():
|
||||
if pattern in path_str:
|
||||
|
|
@ -89,11 +93,10 @@ def infer_tier(filepath: Path) -> str:
|
|||
|
||||
|
||||
def extract_summary(filepath: Path, max_lines: int = 5) -> str:
|
||||
"""Extract first few non-empty lines as summary."""
|
||||
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
|
||||
lines = []
|
||||
for i, line in enumerate(f):
|
||||
if i >= max_lines * 3: # Read more to find content
|
||||
if i >= max_lines * 3:
|
||||
break
|
||||
stripped = line.strip()
|
||||
if stripped and not stripped.startswith("#"):
|
||||
|
|
@ -104,79 +107,63 @@ def extract_summary(filepath: Path, max_lines: int = 5) -> str:
|
|||
|
||||
|
||||
def compute_genome(filepath: Path) -> Dict[str, int]:
|
||||
"""Compute genome (6D quantized signature) for file."""
|
||||
try:
|
||||
size = filepath.stat().st_size
|
||||
lines = len(filepath.read_text(encoding="utf-8", errors="replace").split("\n"))
|
||||
|
||||
# Quantize to 3 bits (0-7) per dimension
|
||||
mu = (lines % 256) // 32 # compression ratio bin based on lines
|
||||
rho = min(7, (size // 1024) % 8) # information density (KB bins)
|
||||
c = 4 # fixed cost for documentation
|
||||
m = 4 # manifold: document is stable
|
||||
ne = 7 if len(filepath.stem) > 15 else 3 # negentropy: longer names = higher semantic content
|
||||
sig = 0 # documentation has no signal category
|
||||
|
||||
return {
|
||||
"mu": mu, "rho": rho, "c": c, "m": m, "ne": ne, "sig": sig
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not compute genome for {filepath}: {e}")
|
||||
|
||||
mu = (lines % 256) // 32
|
||||
rho = min(7, (size // 1024) % 8)
|
||||
c = 4
|
||||
m = 4
|
||||
ne = 7 if len(filepath.stem) > 15 else 3
|
||||
sig = 0
|
||||
|
||||
return {"mu": mu, "rho": rho, "c": c, "m": m, "ne": ne, "sig": sig}
|
||||
except Exception:
|
||||
return {"mu": 0, "rho": 0, "c": 4, "m": 4, "ne": 0, "sig": 0}
|
||||
|
||||
|
||||
def compute_bind(filepath: Path) -> Dict[str, Any]:
|
||||
"""Compute bind struct (cost, lawful check, invariant)."""
|
||||
return {
|
||||
"lawful": True,
|
||||
"cost": 0x00010000, # 1.0 in Q16_16 — documentation is reference cost
|
||||
"cost": 0x00010000,
|
||||
"invariant": "documentConsistency",
|
||||
"class": "informational_bind"
|
||||
"class": "informational_bind",
|
||||
}
|
||||
|
||||
|
||||
def compute_address_from_genome(genome: Dict[str, int]) -> int:
|
||||
"""Convert 6D genome to 18-bit linear address."""
|
||||
mu = genome.get("mu", 0)
|
||||
rho = genome.get("rho", 0)
|
||||
c = genome.get("c", 0)
|
||||
m = genome.get("m", 0)
|
||||
ne = genome.get("ne", 0)
|
||||
sig = genome.get("sig", 0)
|
||||
|
||||
address = (((((mu * 8 + rho) * 8 + c) * 8 + m) * 8 + ne) * 8 + sig)
|
||||
return address
|
||||
return (((((mu * 8 + rho) * 8 + c) * 8 + m) * 8 + ne) * 8 + sig)
|
||||
|
||||
|
||||
def md_to_jsonl_entry(filepath: Path, node_id: str = "qfox") -> Dict[str, Any]:
|
||||
"""Convert a single markdown file to JSON-L entry."""
|
||||
|
||||
# Compute file metadata
|
||||
def md_to_jsonl_entry(filepath: Path, node_id: str) -> Dict[str, Any]:
|
||||
file_hash = compute_sha256(filepath)
|
||||
file_stat = filepath.stat()
|
||||
mtime_unix = file_stat.st_mtime
|
||||
file_size = file_stat.st_size
|
||||
|
||||
# Compute derived fields
|
||||
|
||||
domain = infer_domain(filepath, "")
|
||||
tier = infer_tier(filepath)
|
||||
genome = compute_genome(filepath)
|
||||
address = compute_address_from_genome(genome)
|
||||
_address = compute_address_from_genome(genome)
|
||||
bind = compute_bind(filepath)
|
||||
|
||||
# Create pkg identifier
|
||||
|
||||
rel_path = filepath.relative_to(WORKSPACE_ROOT)
|
||||
pkg = f"ene/markdown/{rel_path.stem}".replace("\\", "/")
|
||||
version = datetime.fromtimestamp(mtime_unix, tz=timezone.utc).isoformat()
|
||||
|
||||
# Concept anchor
|
||||
|
||||
concept_anchor = {
|
||||
"domain": domain,
|
||||
"concept": filepath.stem.lower().replace(" ", "_").replace("-", "_"),
|
||||
"resolution": "STABLE" # documents are stable, immutable records
|
||||
"resolution": "STABLE",
|
||||
}
|
||||
|
||||
# Data payload
|
||||
|
||||
data = {
|
||||
"pkg": pkg,
|
||||
"version": version,
|
||||
|
|
@ -187,20 +174,18 @@ def md_to_jsonl_entry(filepath: Path, node_id: str = "qfox") -> Dict[str, Any]:
|
|||
"file_path": str(rel_path).replace("\\", "/"),
|
||||
"file_hash": file_hash,
|
||||
"byte_count": file_size,
|
||||
"summary": extract_summary(filepath)
|
||||
"summary": extract_summary(filepath),
|
||||
}
|
||||
|
||||
# Provenance
|
||||
|
||||
provenance = {
|
||||
"node": node_id,
|
||||
"lake_seed": "md_converter_seed",
|
||||
"tailscale_ip": "127.0.0.1",
|
||||
"lake_seed": env_default("ENE_LAKE_SEED", "md_converter_seed"),
|
||||
"tailscale_ip": env_default("ENE_TAILSCALE_IP", "127.0.0.1"),
|
||||
"attestation_hash": file_hash,
|
||||
"prev_id": None
|
||||
"prev_id": None,
|
||||
}
|
||||
|
||||
# Full JSON-L entry
|
||||
entry = {
|
||||
|
||||
return {
|
||||
"t": mtime_unix,
|
||||
"src": "ene",
|
||||
"id": f"ene:{pkg}:{version}",
|
||||
|
|
@ -208,115 +193,72 @@ def md_to_jsonl_entry(filepath: Path, node_id: str = "qfox") -> Dict[str, Any]:
|
|||
"data": data,
|
||||
"genome": genome,
|
||||
"bind": bind,
|
||||
"provenance": provenance
|
||||
"provenance": provenance,
|
||||
}
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
def find_all_md_files() -> List[Path]:
|
||||
"""Find all .md files in search directories."""
|
||||
md_files = []
|
||||
for search_dir in SEARCH_DIRS:
|
||||
if not search_dir.exists():
|
||||
continue
|
||||
for md_file in search_dir.glob("*.md"):
|
||||
md_files.append(md_file)
|
||||
# Recursively search subdirectories for germane/
|
||||
if "germane" in str(search_dir):
|
||||
for md_file in search_dir.glob("**/*.md"):
|
||||
md_files.append(md_file)
|
||||
|
||||
return sorted(list(set(md_files))) # Remove duplicates and sort
|
||||
return sorted(list(set(md_files)))
|
||||
|
||||
|
||||
def load_existing_manifest() -> set:
|
||||
"""Load IDs from existing manifest to avoid duplicates."""
|
||||
existing_ids = set()
|
||||
if MANIFEST_PATH.exists():
|
||||
try:
|
||||
with open(MANIFEST_PATH, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
existing_ids.add(entry.get("id", ""))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not read existing manifest: {e}")
|
||||
with open(MANIFEST_PATH, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
existing_ids.add(entry.get("id", ""))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return existing_ids
|
||||
|
||||
|
||||
def convert_all_md_files(output_file: Optional[str] = None) -> Tuple[int, int, int]:
|
||||
"""
|
||||
Convert all markdown files to JSON-L.
|
||||
|
||||
Returns: (total_files, converted, skipped)
|
||||
"""
|
||||
def convert_all_md_files(node_id: str, output_file: Optional[str] = None) -> Tuple[int, int, int]:
|
||||
output_path = Path(output_file) if output_file else MANIFEST_PATH
|
||||
|
||||
print(f"🔍 Scanning for .md files in {len(SEARCH_DIRS)} directories...")
|
||||
|
||||
md_files = find_all_md_files()
|
||||
print(f"✅ Found {len(md_files)} markdown files")
|
||||
|
||||
existing_ids = load_existing_manifest()
|
||||
print(f"📋 Manifest already has {len(existing_ids)} entries")
|
||||
|
||||
|
||||
converted = 0
|
||||
skipped = 0
|
||||
|
||||
# Append new entries to manifest
|
||||
|
||||
with open(output_path, "a") as manifest_f:
|
||||
for i, md_file in enumerate(md_files, 1):
|
||||
try:
|
||||
entry = md_to_jsonl_entry(md_file)
|
||||
entry_id = entry.get("id", "")
|
||||
|
||||
if entry_id in existing_ids:
|
||||
skipped += 1
|
||||
status = "⏭️ SKIP"
|
||||
else:
|
||||
manifest_f.write(json.dumps(entry) + "\n")
|
||||
manifest_f.flush()
|
||||
converted += 1
|
||||
status = "✅ CONV"
|
||||
|
||||
rel_path = md_file.relative_to(WORKSPACE_ROOT)
|
||||
print(f"[{i:3d}/{len(md_files)}] {status} {rel_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[{i:3d}/{len(md_files)}] ❌ ERR {md_file.relative_to(WORKSPACE_ROOT)}: {e}")
|
||||
for md_file in md_files:
|
||||
entry = md_to_jsonl_entry(md_file, node_id=node_id)
|
||||
entry_id = entry.get("id", "")
|
||||
if entry_id in existing_ids:
|
||||
skipped += 1
|
||||
|
||||
else:
|
||||
manifest_f.write(json.dumps(entry) + "\n")
|
||||
manifest_f.flush()
|
||||
converted += 1
|
||||
|
||||
return len(md_files), converted, skipped
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
output_file = None
|
||||
if len(sys.argv) > 2 and sys.argv[1] == "--output-file":
|
||||
output_file = sys.argv[2]
|
||||
|
||||
print("=" * 70)
|
||||
print("Markdown to JSON-L Converter")
|
||||
print(f"Workspace: {WORKSPACE_ROOT}")
|
||||
print(f"Output: {output_file or MANIFEST_PATH}")
|
||||
print("=" * 70)
|
||||
|
||||
total, converted, skipped = convert_all_md_files(output_file)
|
||||
|
||||
print("=" * 70)
|
||||
print(f"📊 Summary:")
|
||||
print(f" Total files found: {total}")
|
||||
print(f" Newly converted: {converted}")
|
||||
print(f" Already exists: {skipped}")
|
||||
print(f" Output file: {output_file or MANIFEST_PATH}")
|
||||
print("=" * 70)
|
||||
|
||||
return 0 if converted > 0 or skipped > 0 else 1
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--output-file", default=None)
|
||||
parser.add_argument("--node-id", default=env_default("ENE_NODE_ID", "qfox"))
|
||||
args = parser.parse_args()
|
||||
|
||||
total, converted, skipped = convert_all_md_files(node_id=args.node_id, output_file=args.output_file)
|
||||
print(json.dumps({"total": total, "converted": converted, "skipped": skipped, "node_id": args.node_id}, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
obsidian_sync_shim.py — Bidirectional sync between Obsidian vault and JSON-L lake.
|
||||
"""obsidian_sync_shim.py — Bidirectional sync between Obsidian vault and JSON-L lake.
|
||||
|
||||
Modes:
|
||||
ingest : Obsidian notes → JSON-L lake (append to lake file)
|
||||
|
|
@ -9,6 +8,13 @@ Modes:
|
|||
|
||||
Requires TOPOLOGICAL_ENGINE_URL and TOPOLOGICAL_ENGINE_TOKEN in .env.
|
||||
For local Obsidian installs, set OBSIDIAN_VAULT_PATH or use --vault.
|
||||
|
||||
Hardcoded node/provenance assumptions were removed:
|
||||
- provenance.node uses ENE_NODE_ID (default: obsidian_sync_shim)
|
||||
- provenance.lake_seed uses ENE_LAKE_SEED (default: obsidian_lake)
|
||||
- provenance.tailscale_ip uses ENE_TAILSCALE_IP (default: 127.0.0.1)
|
||||
|
||||
NOTE: This is a legacy ingest surface; treat outputs as non-authoritative.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
|
@ -25,6 +31,7 @@ from typing import Dict, List, Any, Optional
|
|||
project_root = Path(__file__).parent.parent.parent
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
|
||||
if (project_root / ".env").exists():
|
||||
load_dotenv(project_root / ".env")
|
||||
except ImportError:
|
||||
|
|
@ -36,6 +43,11 @@ sys.path.insert(0, str(project_root / "4-Infrastructure" / "infra"))
|
|||
from infra.topological_engine_client import TopologicalEngineClient
|
||||
|
||||
|
||||
def env_default(name: str, default: str) -> str:
|
||||
v = os.getenv(name)
|
||||
return v if v is not None and v != "" else default
|
||||
|
||||
|
||||
DEFAULT_VAULT_PATH = project_root / "Obdisidan connector"
|
||||
LAKE_PATH = Path(os.getenv("OBSIDIAN_LAKE_PATH", str(project_root / "data" / "obsidian_lake.jsonl")))
|
||||
LAKE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
|
@ -91,11 +103,21 @@ def _format_jsonl_entry(
|
|||
tier: str = "AUX",
|
||||
domain: str = "obsidian",
|
||||
archetype: str = "note",
|
||||
src: str = "obsidian_sync_shim"
|
||||
src: str = "obsidian_sync_shim",
|
||||
) -> Dict[str, Any]:
|
||||
"""Format a JSON-L entry for the Research Stack lake."""
|
||||
now = datetime.now(timezone.utc)
|
||||
timestamp = now.isoformat()
|
||||
|
||||
node_id = env_default("ENE_NODE_ID", "obsidian_sync_shim")
|
||||
lake_seed = env_default("ENE_LAKE_SEED", "obsidian_lake")
|
||||
tailscale_ip = env_default("ENE_TAILSCALE_IP", "127.0.0.1")
|
||||
|
||||
# Deterministic attestation over the minimal payload.
|
||||
attestation_hash = "sha256:" + hashlib.sha256(
|
||||
(pkg + ":" + timestamp + ":" + json.dumps(data, sort_keys=True, ensure_ascii=False)).encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
return {
|
||||
"t": now.timestamp(),
|
||||
"src": src,
|
||||
|
|
@ -107,18 +129,21 @@ def _format_jsonl_entry(
|
|||
"tier": tier,
|
||||
"domain": domain,
|
||||
"archetype": archetype,
|
||||
**data
|
||||
**data,
|
||||
},
|
||||
"bind": {
|
||||
"lawful": True,
|
||||
"cost": 65536,
|
||||
"invariant": "noteConsistency",
|
||||
"class": "informational_bind"
|
||||
"class": "informational_bind",
|
||||
},
|
||||
"provenance": {
|
||||
"node": "obsidian_sync_shim",
|
||||
"lake_seed": "obsidian_lake"
|
||||
}
|
||||
"node": node_id,
|
||||
"lake_seed": lake_seed,
|
||||
"tailscale_ip": tailscale_ip,
|
||||
"attestation_hash": attestation_hash,
|
||||
"prev_id": None,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -129,7 +154,7 @@ def _format_jsonl_entry(
|
|||
def ingest_obsidian(
|
||||
client: TopologicalEngineClient,
|
||||
query: str = "*",
|
||||
lake_path: Path = LAKE_PATH
|
||||
lake_path: Path = LAKE_PATH,
|
||||
) -> Dict[str, Any]:
|
||||
"""Pull notes from Obsidian vault and append to JSON-L lake."""
|
||||
print("[ingest] Searching Obsidian vault...")
|
||||
|
|
@ -156,11 +181,10 @@ def ingest_obsidian(
|
|||
"links": note.get("links", []),
|
||||
"tags": note.get("tags", []),
|
||||
"modified": note.get("modified", note.get("mtime")),
|
||||
}
|
||||
},
|
||||
)
|
||||
entries.append(entry)
|
||||
|
||||
# Append to lake
|
||||
with open(lake_path, "a") as f:
|
||||
for entry in entries:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
|
|
@ -172,7 +196,7 @@ def ingest_obsidian(
|
|||
def ingest_local_obsidian(
|
||||
vault_path: Path,
|
||||
query: str = "*",
|
||||
lake_path: Path = LAKE_PATH
|
||||
lake_path: Path = LAKE_PATH,
|
||||
) -> Dict[str, Any]:
|
||||
"""Pull notes from a local Obsidian vault and append to JSON-L lake."""
|
||||
if not vault_path.exists():
|
||||
|
|
@ -223,7 +247,7 @@ def ingest_local_obsidian(
|
|||
def export_to_obsidian(
|
||||
client: TopologicalEngineClient,
|
||||
lake_path: Path = LAKE_PATH,
|
||||
filter_domain: str = "obsidian"
|
||||
filter_domain: str = "obsidian",
|
||||
) -> Dict[str, Any]:
|
||||
"""Read JSON-L lake and write matching entries back to Obsidian vault."""
|
||||
if not lake_path.exists():
|
||||
|
|
@ -257,19 +281,21 @@ def export_to_obsidian(
|
|||
if not path:
|
||||
continue
|
||||
|
||||
# Reconstruct a simple markdown note
|
||||
md = f"# {title}\n\n"
|
||||
md += f"> Synced from JSON-L lake at {datetime.now(timezone.utc).isoformat()}\n\n"
|
||||
md += content_preview
|
||||
if len(content_preview) >= 500:
|
||||
md += "\n\n...(truncated)"
|
||||
|
||||
# Write via topological engine
|
||||
res = client.write_obsidian_note(path=path, body=md, metadata={
|
||||
"source": "research_stack_lake",
|
||||
"entry_id": entry.get("id"),
|
||||
"synced_at": datetime.now(timezone.utc).isoformat()
|
||||
})
|
||||
res = client.write_obsidian_note(
|
||||
path=path,
|
||||
body=md,
|
||||
metadata={
|
||||
"source": "research_stack_lake",
|
||||
"entry_id": entry.get("id"),
|
||||
"synced_at": datetime.now(timezone.utc).isoformat(),
|
||||
},
|
||||
)
|
||||
if "error" not in res:
|
||||
written += 1
|
||||
|
||||
|
|
@ -281,7 +307,7 @@ def export_to_local_obsidian(
|
|||
vault_path: Path,
|
||||
lake_path: Path = LAKE_PATH,
|
||||
filter_domain: str = "obsidian",
|
||||
overwrite: bool = False
|
||||
overwrite: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Read JSON-L lake and write matching entries into a local Obsidian vault."""
|
||||
if not lake_path.exists():
|
||||
|
|
@ -339,15 +365,8 @@ def export_to_local_obsidian(
|
|||
def sync_bidirectional(
|
||||
client: TopologicalEngineClient,
|
||||
lake_path: Path = LAKE_PATH,
|
||||
obsidian_wins: bool = True
|
||||
obsidian_wins: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Two-way sync:
|
||||
1. Ingest current Obsidian state into lake.
|
||||
2. Export lake entries back to Obsidian (creating any missing notes).
|
||||
|
||||
If obsidian_wins=True, existing Obsidian notes are not overwritten.
|
||||
"""
|
||||
print("[sync] Starting bidirectional sync...")
|
||||
ingest_res = ingest_obsidian(client, lake_path=lake_path)
|
||||
if "error" in ingest_res:
|
||||
|
|
@ -355,13 +374,11 @@ def sync_bidirectional(
|
|||
|
||||
if obsidian_wins:
|
||||
print("[sync] Obsidian-wins mode: skipping overwrite of existing notes")
|
||||
# In export mode, only write notes that don't already exist
|
||||
search_res = client.search_obsidian("*")
|
||||
existing_paths = set()
|
||||
for note in search_res.get("results", search_res.get("notes", [])):
|
||||
existing_paths.add(note.get("path", note.get("file_path")))
|
||||
|
||||
# Filter lake entries to only missing paths
|
||||
if lake_path.exists():
|
||||
entries = []
|
||||
with open(lake_path) as f:
|
||||
|
|
@ -387,23 +404,16 @@ def sync_bidirectional(
|
|||
written += 1
|
||||
print(f"[sync] Created {written} missing notes in Obsidian")
|
||||
return {"ingested": ingest_res["ingested"], "created": written}
|
||||
else:
|
||||
export_res = export_to_obsidian(client, lake_path=lake_path)
|
||||
return {"ingested": ingest_res["ingested"], **export_res}
|
||||
|
||||
export_res = export_to_obsidian(client, lake_path=lake_path)
|
||||
return {"ingested": ingest_res["ingested"], **export_res}
|
||||
|
||||
|
||||
def sync_local_bidirectional(
|
||||
vault_path: Path,
|
||||
lake_path: Path = LAKE_PATH,
|
||||
obsidian_wins: bool = True
|
||||
obsidian_wins: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Local two-way sync:
|
||||
1. Ingest current local Obsidian state into lake.
|
||||
2. Export lake entries back to the vault.
|
||||
|
||||
If obsidian_wins=True, existing local notes are not overwritten.
|
||||
"""
|
||||
print("[sync:local] Starting bidirectional local sync")
|
||||
ingest_res = ingest_local_obsidian(vault_path=vault_path, lake_path=lake_path)
|
||||
if "error" in ingest_res:
|
||||
|
|
@ -424,18 +434,33 @@ def sync_local_bidirectional(
|
|||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Obsidian ↔ JSON-L lake sync shim")
|
||||
parser.add_argument("mode", choices=["ingest", "export", "sync"], default="sync", nargs="?",
|
||||
help="ingest=Obsidian→lake, export=lake→Obsidian, sync=both")
|
||||
parser.add_argument("--backend", choices=["auto", "local", "engine"], default="auto",
|
||||
help="local=filesystem vault, engine=topological engine, auto=engine if healthy else local")
|
||||
parser.add_argument("--vault", default=None,
|
||||
help="Local Obsidian vault path; defaults to OBSIDIAN_VAULT_PATH or ./Obdisidan connector")
|
||||
parser.add_argument(
|
||||
"mode",
|
||||
choices=["ingest", "export", "sync"],
|
||||
default="sync",
|
||||
nargs="?",
|
||||
help="ingest=Obsidian→lake, export=lake→Obsidian, sync=both",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--backend",
|
||||
choices=["auto", "local", "engine"],
|
||||
default="auto",
|
||||
help="local=filesystem vault, engine=topological engine, auto=engine if healthy else local",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--vault",
|
||||
default=None,
|
||||
help="Local Obsidian vault path; defaults to OBSIDIAN_VAULT_PATH or ./Obdisidan connector",
|
||||
)
|
||||
parser.add_argument("--lake", default=str(LAKE_PATH), help="Path to JSON-L lake file")
|
||||
parser.add_argument("--query", default="*", help="Obsidian search query (ingest mode)")
|
||||
parser.add_argument("--obsidian-wins", action="store_true", default=True,
|
||||
help="In sync mode, don't overwrite existing Obsidian notes")
|
||||
parser.add_argument("--lake-wins", action="store_true",
|
||||
help="In sync mode, overwrite Obsidian with lake contents")
|
||||
parser.add_argument(
|
||||
"--obsidian-wins",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="In sync mode, don't overwrite existing Obsidian notes",
|
||||
)
|
||||
parser.add_argument("--lake-wins", action="store_true", help="In sync mode, overwrite Obsidian with lake")
|
||||
args = parser.parse_args()
|
||||
|
||||
lake_path = Path(args.lake)
|
||||
|
|
@ -461,17 +486,9 @@ def main():
|
|||
if args.mode == "ingest":
|
||||
result = ingest_local_obsidian(vault_path=vault_path, query=args.query, lake_path=lake_path)
|
||||
elif args.mode == "export":
|
||||
result = export_to_local_obsidian(
|
||||
vault_path=vault_path,
|
||||
lake_path=lake_path,
|
||||
overwrite=not obsidian_wins,
|
||||
)
|
||||
result = export_to_local_obsidian(vault_path=vault_path, lake_path=lake_path, overwrite=not obsidian_wins)
|
||||
else:
|
||||
result = sync_local_bidirectional(
|
||||
vault_path=vault_path,
|
||||
lake_path=lake_path,
|
||||
obsidian_wins=obsidian_wins,
|
||||
)
|
||||
result = sync_local_bidirectional(vault_path=vault_path, lake_path=lake_path, obsidian_wins=obsidian_wins)
|
||||
else:
|
||||
if args.mode == "ingest":
|
||||
result = ingest_obsidian(client, query=args.query, lake_path=lake_path)
|
||||
|
|
@ -483,9 +500,9 @@ def main():
|
|||
if "error" in result:
|
||||
print(f"Error: {result['error']}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"Done: {json.dumps(result, indent=2)}")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"Done: {json.dumps(result, indent=2)}")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
swarm_network_capacity.py — Network Resource Capacity Monitor
|
||||
"""swarm_network_capacity.py — Network Resource Capacity Monitor (legacy shim).
|
||||
|
||||
Checks all Tailscale-connected nodes (via ENE mesh) to determine:
|
||||
- Total available resources across the network
|
||||
- Currently utilized capacity
|
||||
- Idle resources that could be leveraged
|
||||
- Node health and connectivity status
|
||||
Cleanups:
|
||||
- Removed hardcoded hostname→resource maps.
|
||||
- Reads node inventory from 4-Infrastructure/auto/config/nodes.yaml (controller source of truth).
|
||||
|
||||
NOTE: This is a legacy monitoring surface; treat results as advisory.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class TailscaleNode:
|
||||
"""Remote node in the Tailscale mesh."""
|
||||
ip: str
|
||||
hostname: str
|
||||
owner: str
|
||||
|
|
@ -27,14 +29,13 @@ class TailscaleNode:
|
|||
status: str # online, offline, idle
|
||||
last_seen: Optional[str]
|
||||
tags: List[str]
|
||||
|
||||
|
||||
def is_online(self) -> bool:
|
||||
return self.status == "online" or self.status == "idle"
|
||||
return self.status in {"online", "idle"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class NodeResources:
|
||||
"""Resource capacity of a node."""
|
||||
node_id: str
|
||||
cpu_cores: int
|
||||
memory_gb: float
|
||||
|
|
@ -45,187 +46,172 @@ class NodeResources:
|
|||
utilization_percent: float
|
||||
|
||||
|
||||
def _load_nodes_inventory(path: Path) -> Dict[str, Any]:
|
||||
"""Load nodes.yaml.
|
||||
|
||||
Tries PyYAML; if missing, raises a clear error.
|
||||
"""
|
||||
try:
|
||||
import yaml # type: ignore
|
||||
except ImportError as exc:
|
||||
raise RuntimeError(
|
||||
"nodes.yaml parsing requires PyYAML. Install with: pip install pyyaml"
|
||||
) from exc
|
||||
|
||||
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict) or "nodes" not in data:
|
||||
raise ValueError(f"Invalid nodes inventory file: {path}")
|
||||
return data
|
||||
|
||||
|
||||
class SwarmNetworkCapacity:
|
||||
"""
|
||||
Monitor and report on full network resource capacity.
|
||||
|
||||
Queries Tailscale mesh and ENE nodes to determine:
|
||||
- Total available compute across all nodes
|
||||
- Current utilization vs capacity
|
||||
- Resource distribution
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, inventory_path: Path):
|
||||
self.inventory_path = inventory_path
|
||||
self.inventory = _load_nodes_inventory(inventory_path)
|
||||
self.tailscale_nodes: List[TailscaleNode] = []
|
||||
self.ene_nodes: List[str] = []
|
||||
self.local_ip: Optional[str] = None
|
||||
|
||||
|
||||
def discover_tailscale_mesh(self) -> List[TailscaleNode]:
|
||||
"""Discover all nodes in the Tailscale mesh."""
|
||||
print("\n[1] Discovering Tailscale mesh nodes...")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["tailscale", "status"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
lines = result.stdout.strip().split('\n')
|
||||
nodes = []
|
||||
|
||||
|
||||
lines = result.stdout.strip().split("\n")
|
||||
nodes: List[TailscaleNode] = []
|
||||
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
# Parse tailscale status line
|
||||
# Format: 100.x.x.x hostname owner@ os status
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) >= 4:
|
||||
ip = parts[0]
|
||||
hostname = parts[1]
|
||||
owner = parts[2]
|
||||
os_type = parts[3]
|
||||
|
||||
# Parse status (can be complex)
|
||||
status_parts = ' '.join(parts[4:]) if len(parts) > 4 else ""
|
||||
|
||||
# Determine status
|
||||
status_parts = " ".join(parts[4:]) if len(parts) > 4 else ""
|
||||
|
||||
if "offline" in status_parts.lower():
|
||||
status = "offline"
|
||||
elif "idle" in status_parts.lower():
|
||||
status = "idle"
|
||||
else:
|
||||
status = "online"
|
||||
|
||||
# Extract last seen if offline
|
||||
|
||||
last_seen = None
|
||||
if "last seen" in status_parts:
|
||||
match = re.search(r'last seen ([^,]+)', status_parts)
|
||||
match = re.search(r"last seen ([^,]+)", status_parts)
|
||||
if match:
|
||||
last_seen = match.group(1)
|
||||
|
||||
# Extract tags
|
||||
tags = []
|
||||
|
||||
tags: List[str] = []
|
||||
if "tagged-devices" in line:
|
||||
tags.append("tagged-devices")
|
||||
|
||||
node = TailscaleNode(
|
||||
ip=ip,
|
||||
hostname=hostname,
|
||||
owner=owner,
|
||||
os=os_type,
|
||||
status=status,
|
||||
last_seen=last_seen,
|
||||
tags=tags
|
||||
|
||||
nodes.append(
|
||||
TailscaleNode(
|
||||
ip=ip,
|
||||
hostname=hostname,
|
||||
owner=owner,
|
||||
os=os_type,
|
||||
status=status,
|
||||
last_seen=last_seen,
|
||||
tags=tags,
|
||||
)
|
||||
)
|
||||
nodes.append(node)
|
||||
|
||||
|
||||
self.tailscale_nodes = nodes
|
||||
|
||||
# Get local IP
|
||||
|
||||
try:
|
||||
ip_result = subprocess.run(
|
||||
["tailscale", "ip", "-4"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
timeout=5,
|
||||
)
|
||||
self.local_ip = ip_result.stdout.strip()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
print(f" Found: {len(nodes)} Tailscale nodes")
|
||||
online = sum(1 for n in nodes if n.is_online())
|
||||
print(f" Online: {online}/{len(nodes)}")
|
||||
|
||||
|
||||
for node in nodes:
|
||||
status_icon = "🟢" if node.is_online() else "🔴"
|
||||
print(f" {status_icon} {node.hostname} ({node.ip}) - {node.status}")
|
||||
|
||||
status_icon = "online" if node.is_online() else "offline"
|
||||
print(f" [{status_icon}] {node.hostname} ({node.ip})")
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Error discovering mesh: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def check_ene_deployment(self) -> List[str]:
|
||||
"""Check which nodes have ENE deployed."""
|
||||
print("\n[2] Checking ENE deployment status...")
|
||||
|
||||
ene_nodes = []
|
||||
|
||||
for node in self.tailscale_nodes:
|
||||
if not node.is_online():
|
||||
continue
|
||||
|
||||
# Try to check if ENE is running on remote node
|
||||
# This would typically use SSH or ENE's gossip protocol
|
||||
# For now, we'll simulate based on known deployment
|
||||
|
||||
if node.hostname in ["architect", "judge", "qfox"]:
|
||||
ene_nodes.append(node.hostname)
|
||||
|
||||
self.ene_nodes = ene_nodes
|
||||
|
||||
print(f" ENE deployed on: {len(ene_nodes)} nodes")
|
||||
for node in ene_nodes:
|
||||
print(f" ✅ {node}")
|
||||
|
||||
# Nodes needing ENE deployment
|
||||
online_nodes = [n.hostname for n in self.tailscale_nodes if n.is_online()]
|
||||
need_ene = set(online_nodes) - set(ene_nodes)
|
||||
if need_ene:
|
||||
print(f" Need ENE deployment: {len(need_ene)} nodes")
|
||||
for node in need_ene:
|
||||
print(f" ⚠️ {node}")
|
||||
|
||||
return ene_nodes
|
||||
|
||||
"""Check which nodes have ENE deployed.
|
||||
|
||||
For now this still uses a heuristic: nodes with role including 'compute'
|
||||
or 'service-host' are assumed to be candidates. Real checks should be via
|
||||
SSH/probes.
|
||||
"""
|
||||
print("\n[2] Checking ENE deployment status (heuristic)...")
|
||||
nodes = self.inventory.get("nodes", {})
|
||||
ene_nodes: List[str] = []
|
||||
for node_id, spec in nodes.items():
|
||||
roles = spec.get("roles", []) if isinstance(spec, dict) else []
|
||||
if any(r in roles for r in ["compute", "service-host", "control-plane"]):
|
||||
ene_nodes.append(node_id)
|
||||
self.ene_nodes = sorted(set(ene_nodes))
|
||||
|
||||
print(f" ENE candidates: {len(self.ene_nodes)}")
|
||||
for n in self.ene_nodes[:20]:
|
||||
print(f" ✅ {n}")
|
||||
return self.ene_nodes
|
||||
|
||||
def estimate_node_resources(self, node: TailscaleNode) -> Optional[NodeResources]:
|
||||
"""Estimate resources available on a node."""
|
||||
# These would normally be queried from the node
|
||||
# For now, estimate based on hostname patterns
|
||||
|
||||
resource_map = {
|
||||
"qfox": {"cpu": 16, "ram": 32, "storage": 1000, "gpu": 1, "bw": 1000},
|
||||
"architect": {"cpu": 8, "ram": 16, "storage": 500, "gpu": 0, "bw": 500},
|
||||
"judge": {"cpu": 4, "ram": 8, "storage": 200, "gpu": 0, "bw": 500},
|
||||
"ip-172-31-25-81": {"cpu": 2, "ram": 4, "storage": 100, "gpu": 0, "bw": 1000}, # AWS
|
||||
"netcup-router": {"cpu": 4, "ram": 8, "storage": 500, "gpu": 0, "bw": 1000},
|
||||
"racknerd-510bd9c": {"cpu": 2, "ram": 4, "storage": 100, "gpu": 0, "bw": 1000},
|
||||
"racknerd-atl": {"cpu": 2, "ram": 4, "storage": 100, "gpu": 0, "bw": 1000},
|
||||
"desktop-0u2ceal": {"cpu": 8, "ram": 16, "storage": 500, "gpu": 1, "bw": 100},
|
||||
}
|
||||
|
||||
specs = resource_map.get(node.hostname, {"cpu": 2, "ram": 4, "storage": 100, "gpu": 0, "bw": 100})
|
||||
|
||||
"""Estimate node resources.
|
||||
|
||||
nodes.yaml currently doesn't track CPU/RAM explicitly; so this function
|
||||
returns conservative defaults. Once resources are added to nodes.yaml,
|
||||
this function will use them directly.
|
||||
"""
|
||||
specs = self.inventory.get("nodes", {}).get(node.hostname) or {}
|
||||
cpu = int(specs.get("cpu", 2)) if isinstance(specs, dict) else 2
|
||||
ram = float(specs.get("ram", 4)) if isinstance(specs, dict) else 4.0
|
||||
storage = float(specs.get("storage", 100)) if isinstance(specs, dict) else 100.0
|
||||
gpu = int(specs.get("gpu", 0)) if isinstance(specs, dict) else 0
|
||||
bw = float(specs.get("bw", 100)) if isinstance(specs, dict) else 100.0
|
||||
|
||||
return NodeResources(
|
||||
node_id=node.hostname,
|
||||
cpu_cores=specs["cpu"],
|
||||
memory_gb=specs["ram"],
|
||||
storage_gb=specs["storage"],
|
||||
bandwidth_mbps=specs["bw"],
|
||||
gpu_count=specs["gpu"],
|
||||
cpu_cores=cpu,
|
||||
memory_gb=ram,
|
||||
storage_gb=storage,
|
||||
bandwidth_mbps=bw,
|
||||
gpu_count=gpu,
|
||||
ene_enabled=node.hostname in self.ene_nodes,
|
||||
utilization_percent=0.0 # Would need actual monitoring
|
||||
utilization_percent=0.0,
|
||||
)
|
||||
|
||||
|
||||
def calculate_total_capacity(self) -> Dict[str, float]:
|
||||
"""Calculate total network capacity."""
|
||||
print("\n[3] Calculating total network capacity...")
|
||||
|
||||
|
||||
total_cpu = 0
|
||||
total_ram = 0.0
|
||||
total_storage = 0.0
|
||||
total_gpu = 0
|
||||
total_bw = 0.0
|
||||
|
||||
|
||||
for node in self.tailscale_nodes:
|
||||
if not node.is_online():
|
||||
continue
|
||||
|
||||
|
||||
resources = self.estimate_node_resources(node)
|
||||
if resources:
|
||||
total_cpu += resources.cpu_cores
|
||||
|
|
@ -233,182 +219,104 @@ class SwarmNetworkCapacity:
|
|||
total_storage += resources.storage_gb
|
||||
total_gpu += resources.gpu_count
|
||||
total_bw += resources.bandwidth_mbps
|
||||
|
||||
capacity = {
|
||||
"cpu_cores": total_cpu,
|
||||
|
||||
capacity: Dict[str, float] = {
|
||||
"cpu_cores": float(total_cpu),
|
||||
"memory_gb": total_ram,
|
||||
"storage_gb": total_storage,
|
||||
"gpu_count": total_gpu,
|
||||
"gpu_count": float(total_gpu),
|
||||
"bandwidth_mbps": total_bw,
|
||||
"online_nodes": sum(1 for n in self.tailscale_nodes if n.is_online()),
|
||||
"total_nodes": len(self.tailscale_nodes)
|
||||
"online_nodes": float(sum(1 for n in self.tailscale_nodes if n.is_online())),
|
||||
"total_nodes": float(len(self.tailscale_nodes)),
|
||||
}
|
||||
|
||||
print(f" CPU Cores: {total_cpu}")
|
||||
|
||||
print(f" CPU Cores: {int(total_cpu)}")
|
||||
print(f" Memory: {total_ram:.1f} GB")
|
||||
print(f" Storage: {total_storage:.1f} GB")
|
||||
print(f" GPUs: {total_gpu}")
|
||||
print(f" GPUs: {int(total_gpu)}")
|
||||
print(f" Bandwidth: {total_bw:.0f} Mbps")
|
||||
|
||||
|
||||
return capacity
|
||||
|
||||
|
||||
def check_current_utilization(self) -> Dict[str, float]:
|
||||
"""Check current resource utilization."""
|
||||
print("\n[4] Checking current utilization (local node only)...")
|
||||
|
||||
|
||||
try:
|
||||
# Get local CPU/memory
|
||||
result = subprocess.run(
|
||||
["cat", "/proc/loadavg"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
result = subprocess.run(["cat", "/proc/loadavg"], capture_output=True, text=True, timeout=5)
|
||||
load_parts = result.stdout.strip().split()
|
||||
load_1min = float(load_parts[0]) if load_parts else 0.0
|
||||
|
||||
# Estimate CPU utilization from load
|
||||
# This is simplified - would need per-node queries
|
||||
cpu_util = min(100.0, (load_1min / 16) * 100) # Assuming 16 cores
|
||||
|
||||
# Memory
|
||||
mem_result = subprocess.run(
|
||||
["free", "-m"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
mem_lines = mem_result.stdout.split('\n')
|
||||
|
||||
cpu_util = min(100.0, (load_1min / 16) * 100)
|
||||
|
||||
mem_result = subprocess.run(["free", "-m"], capture_output=True, text=True, timeout=5)
|
||||
mem_lines = mem_result.stdout.split("\n")
|
||||
mem_used = 0
|
||||
mem_total = 1
|
||||
for line in mem_lines:
|
||||
if line.startswith('Mem:'):
|
||||
if line.startswith("Mem:"):
|
||||
parts = line.split()
|
||||
mem_total = int(parts[1])
|
||||
mem_used = int(parts[2])
|
||||
break
|
||||
|
||||
|
||||
mem_util = (mem_used / mem_total) * 100 if mem_total > 0 else 0
|
||||
|
||||
utilization = {
|
||||
"cpu_percent": cpu_util,
|
||||
"memory_percent": mem_util,
|
||||
"local_node_only": True
|
||||
}
|
||||
|
||||
|
||||
print(f" Local CPU: {cpu_util:.1f}%")
|
||||
print(f" Local Memory: {mem_util:.1f}%")
|
||||
print(f" ⚠️ Remote utilization requires ENE monitoring")
|
||||
|
||||
return utilization
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Error checking utilization: {e}")
|
||||
return {"cpu_percent": 0, "memory_percent": 0, "local_node_only": True}
|
||||
|
||||
def generate_capacity_report(self) -> Dict[str, any]:
|
||||
"""Generate full capacity report."""
|
||||
|
||||
return {"cpu_percent": cpu_util, "memory_percent": mem_util, "local_node_only": True}
|
||||
except Exception:
|
||||
return {"cpu_percent": 0.0, "memory_percent": 0.0, "local_node_only": True}
|
||||
|
||||
def generate_capacity_report(self) -> Dict[str, Any]:
|
||||
print("\n" + "=" * 70)
|
||||
print("SWARM NETWORK CAPACITY REPORT")
|
||||
print("=" * 70)
|
||||
|
||||
# Gather data
|
||||
|
||||
self.discover_tailscale_mesh()
|
||||
self.check_ene_deployment()
|
||||
capacity = self.calculate_total_capacity()
|
||||
utilization = self.check_current_utilization()
|
||||
|
||||
# Calculate utilization vs capacity
|
||||
report = {
|
||||
|
||||
report: Dict[str, Any] = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"inventory_path": str(self.inventory_path),
|
||||
"network": {
|
||||
"total_nodes": len(self.tailscale_nodes),
|
||||
"online_nodes": capacity["online_nodes"],
|
||||
"offline_nodes": len(self.tailscale_nodes) - capacity["online_nodes"],
|
||||
"ene_deployed": len(self.ene_nodes),
|
||||
"ene_coverage": len(self.ene_nodes) / capacity["online_nodes"] * 100 if capacity["online_nodes"] > 0 else 0
|
||||
},
|
||||
"capacity": {
|
||||
"cpu_cores": capacity["cpu_cores"],
|
||||
"memory_gb": capacity["memory_gb"],
|
||||
"storage_gb": capacity["storage_gb"],
|
||||
"gpu_count": capacity["gpu_count"],
|
||||
"bandwidth_mbps": capacity["bandwidth_mbps"]
|
||||
"online_nodes": int(capacity["online_nodes"]),
|
||||
"offline_nodes": len(self.tailscale_nodes) - int(capacity["online_nodes"]),
|
||||
"ene_candidates": len(self.ene_nodes),
|
||||
},
|
||||
"capacity": capacity,
|
||||
"utilization": {
|
||||
"cpu_percent": utilization["cpu_percent"],
|
||||
"memory_percent": utilization["memory_percent"],
|
||||
"note": "Local node only - full mesh monitoring requires ENE deployment on all nodes"
|
||||
"cpu_percent": utilization.get("cpu_percent", 0.0),
|
||||
"memory_percent": utilization.get("memory_percent", 0.0),
|
||||
"note": "Local only - full mesh monitoring requires probes",
|
||||
},
|
||||
"idle_resources": {
|
||||
"cpu_cores_available": capacity["cpu_cores"] * (1 - utilization["cpu_percent"]/100),
|
||||
"memory_gb_available": capacity["memory_gb"] * (1 - utilization["memory_percent"]/100),
|
||||
"message": "Significant idle capacity available across mesh"
|
||||
},
|
||||
"recommendations": []
|
||||
"recommendations": [
|
||||
"Add cpu/ram/storage/gpu/bw fields to nodes.yaml to remove conservative defaults.",
|
||||
"Replace ENE deployment heuristic with SSH/probe checks.",
|
||||
],
|
||||
}
|
||||
|
||||
# Add recommendations
|
||||
ene_coverage = report["network"]["ene_coverage"]
|
||||
if ene_coverage < 100:
|
||||
report["recommendations"].append(
|
||||
f"Deploy ENE to {capacity['online_nodes'] - len(self.ene_nodes)} remaining nodes for full mesh monitoring"
|
||||
)
|
||||
|
||||
if utilization["cpu_percent"] < 50:
|
||||
report["recommendations"].append(
|
||||
"CPU utilization low - swarm can scale up workloads"
|
||||
)
|
||||
|
||||
if utilization["memory_percent"] < 50:
|
||||
report["recommendations"].append(
|
||||
"Memory available - can distribute more tasks across mesh"
|
||||
)
|
||||
|
||||
report["recommendations"].append(
|
||||
"Consider load balancing across all online nodes via ENE"
|
||||
)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "=" * 70)
|
||||
print("SUMMARY")
|
||||
print("=" * 70)
|
||||
print(f"Network: {report['network']['online_nodes']}/{report['network']['total_nodes']} nodes online")
|
||||
print(f"ENE Coverage: {ene_coverage:.1f}% ({report['network']['ene_deployed']} nodes)")
|
||||
print(f"Total CPU: {report['capacity']['cpu_cores']} cores")
|
||||
print(f"Total Memory: {report['capacity']['memory_gb']:.1f} GB")
|
||||
print(f"Total Storage: {report['capacity']['storage_gb']:.1f} GB")
|
||||
print(f"\nUtilization (local only):")
|
||||
print(f" CPU: {report['utilization']['cpu_percent']:.1f}%")
|
||||
print(f" Memory: {report['utilization']['memory_percent']:.1f}%")
|
||||
print(f"\nRecommendations:")
|
||||
for rec in report["recommendations"]:
|
||||
print(f" • {rec}")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
"""Run network capacity check."""
|
||||
monitor = SwarmNetworkCapacity()
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--inventory",
|
||||
type=Path,
|
||||
default=Path("4-Infrastructure/auto/config/nodes.yaml"),
|
||||
help="Path to nodes.yaml",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
monitor = SwarmNetworkCapacity(args.inventory)
|
||||
report = monitor.generate_capacity_report()
|
||||
|
||||
# Save report
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
output_path = Path("/home/allaun/Documents/Research Stack/data/swarm_network_capacity.json")
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
|
||||
print(f"Report saved: {output_path}")
|
||||
|
||||
return report
|
||||
print(json.dumps(report, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
|
|
@ -1,41 +1,38 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive Text Container to JSON-L Converter
|
||||
"""Comprehensive Text Container → JSON-L Converter (legacy shim).
|
||||
|
||||
Converts all information-bearing text containers (MD, JSON, CSV, TSV, TXT) to
|
||||
JSON-L format compatible with UNIFIED_JSONL_SCHEMA.md.
|
||||
|
||||
Handles:
|
||||
- .md files (Markdown documents)
|
||||
- .json files (JSON structures)
|
||||
- .jsonl files (already JSON-L, wrap as documents)
|
||||
- .csv files (tabular data)
|
||||
- .tsv files (tabular data)
|
||||
- .txt files (plain text documents)
|
||||
|
||||
Excludes:
|
||||
- Tool/library files (node_modules, .lake, tools/search, .git)
|
||||
- Config files (setup.json, package.json, etc.)
|
||||
- vendored dependencies
|
||||
Hardcoded node/provenance assumptions were removed:
|
||||
- node id is configurable via --node-id or ENE_NODE_ID
|
||||
- tailscale ip via ENE_TAILSCALE_IP
|
||||
- workspace root via ENE_WORKSPACE_ROOT
|
||||
|
||||
This script is an ingest shim. Treat outputs as non-authoritative.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import csv
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Configuration
|
||||
WORKSPACE_ROOT = Path("/home/allaun/Documents/Research Stack")
|
||||
|
||||
def env_default(name: str, default: str) -> str:
|
||||
try:
|
||||
import os
|
||||
|
||||
v = os.environ.get(name)
|
||||
except Exception:
|
||||
v = None
|
||||
return v if v is not None and v != "" else default
|
||||
|
||||
|
||||
WORKSPACE_ROOT = Path(env_default("ENE_WORKSPACE_ROOT", "/home/allaun/Documents/Research Stack"))
|
||||
MANIFEST_PATH = WORKSPACE_ROOT / "data" / "manifest.jsonl"
|
||||
|
||||
# Directories to skip
|
||||
EXCLUDE_PATTERNS = [
|
||||
".git",
|
||||
"node_modules",
|
||||
|
|
@ -50,10 +47,8 @@ EXCLUDE_PATTERNS = [
|
|||
"venv_",
|
||||
]
|
||||
|
||||
# File types to process
|
||||
PROCESS_EXTENSIONS = {".md", ".json", ".jsonl", ".csv", ".tsv", ".txt"}
|
||||
|
||||
# Archetype mapping
|
||||
ARCHETYPE_MAP = {
|
||||
".md": "markdown_document",
|
||||
".json": "json_structure",
|
||||
|
|
@ -66,7 +61,6 @@ ARCHETYPE_MAP = {
|
|||
|
||||
@dataclass
|
||||
class FileContainer:
|
||||
"""Represents a text-based information container."""
|
||||
filepath: Path
|
||||
ext: str
|
||||
size: int
|
||||
|
|
@ -75,50 +69,53 @@ class FileContainer:
|
|||
|
||||
|
||||
def should_skip(filepath: Path) -> Tuple[bool, str]:
|
||||
"""Check if file should be skipped."""
|
||||
path_str = str(filepath)
|
||||
|
||||
# Skip 4-Infrastructure/config/metadata files
|
||||
|
||||
config_files = {
|
||||
"package.json", "package-lock.json", "tsconfig.json", "devcontainer.json",
|
||||
"settings.json", "launch.json", "tasks.json", ".stylelintrc.json",
|
||||
"biome.json", "lake-manifest.json", "pyrightconfig.json",
|
||||
"manifest.json", "requirements.txt", "app.json", "acme.json",
|
||||
".vscode", ".devcontainer"
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"tsconfig.json",
|
||||
"devcontainer.json",
|
||||
"settings.json",
|
||||
"launch.json",
|
||||
"tasks.json",
|
||||
".stylelintrc.json",
|
||||
"biome.json",
|
||||
"lake-manifest.json",
|
||||
"pyrightconfig.json",
|
||||
"manifest.json",
|
||||
"requirements.txt",
|
||||
"app.json",
|
||||
"acme.json",
|
||||
".vscode",
|
||||
".devcontainer",
|
||||
}
|
||||
|
||||
|
||||
if filepath.name in config_files:
|
||||
return True, "config"
|
||||
|
||||
# Skip excluded paths
|
||||
|
||||
for pattern in EXCLUDE_PATTERNS:
|
||||
if pattern in path_str:
|
||||
return True, f"excluded:{pattern}"
|
||||
|
||||
# Skip large binary-like files
|
||||
if filepath.stat().st_size > 100_000_000: # > 100MB
|
||||
|
||||
if filepath.stat().st_size > 100_000_000:
|
||||
return True, "too_large"
|
||||
|
||||
|
||||
return False, ""
|
||||
|
||||
|
||||
def compute_sha256(filepath: Path) -> str:
|
||||
"""Compute SHA256 hash of file."""
|
||||
sha256 = hashlib.sha256()
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
sha256.update(chunk)
|
||||
return f"sha256:{sha256.hexdigest()}"
|
||||
except Exception as e:
|
||||
return f"sha256:error:{str(e)}"
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
sha256.update(chunk)
|
||||
return f"sha256:{sha256.hexdigest()}"
|
||||
|
||||
|
||||
def infer_domain(filepath: Path, content: str = "") -> str:
|
||||
"""Infer domain from filename and path."""
|
||||
name = filepath.stem.upper()
|
||||
path = str(filepath).upper()
|
||||
|
||||
|
||||
domain_hints = {
|
||||
"LEAN|SEMANTIC|FORMALIZATION": "formalization",
|
||||
"MATH|EQUATION|MODEL": "mathematics",
|
||||
|
|
@ -132,38 +129,34 @@ def infer_domain(filepath: Path, content: str = "") -> str:
|
|||
"DATA|DATASET|CSV|TSV": "data_science",
|
||||
"CONFIG|SETTING": "infrastructure",
|
||||
}
|
||||
|
||||
|
||||
for patterns, domain in domain_hints.items():
|
||||
if any(p in name or p in path for p in patterns.split("|")):
|
||||
return domain
|
||||
|
||||
# Default based on extension
|
||||
if filepath.suffix == ".csv" or filepath.suffix == ".tsv":
|
||||
|
||||
if filepath.suffix in {".csv", ".tsv"}:
|
||||
return "data_science"
|
||||
elif filepath.suffix == ".json":
|
||||
if filepath.suffix == ".json":
|
||||
return "specification"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def infer_tier(filepath: Path) -> str:
|
||||
"""Infer tier from path."""
|
||||
path = str(filepath).lower()
|
||||
if "6-Documentation/docs/semantics" in path or "docs" in path:
|
||||
return "CORE"
|
||||
elif "shared-data/data/germane" in path:
|
||||
if "shared-data/data/germane" in path:
|
||||
return "AUX"
|
||||
elif "out" in path:
|
||||
if "out" in path:
|
||||
return "DERIVED"
|
||||
return "AUX"
|
||||
|
||||
|
||||
def read_text_safely(filepath: Path, max_size: int = 1_000_000) -> str:
|
||||
"""Read text file safely with encoding fallback."""
|
||||
encodings = ["utf-8", "utf-8-sig", "latin-1", "ascii", "cp1252"]
|
||||
|
||||
|
||||
size = filepath.stat().st_size
|
||||
if size > max_size:
|
||||
# Read first and last chunks
|
||||
with open(filepath, "rb") as f:
|
||||
start = f.read(500)
|
||||
f.seek(max(0, size - 500))
|
||||
|
|
@ -172,275 +165,210 @@ def read_text_safely(filepath: Path, max_size: int = 1_000_000) -> str:
|
|||
else:
|
||||
with open(filepath, "rb") as f:
|
||||
content_bytes = f.read()
|
||||
|
||||
|
||||
for encoding in encodings:
|
||||
try:
|
||||
return content_bytes.decode(encoding, errors="replace")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
|
||||
return "<unable to decode>"
|
||||
|
||||
|
||||
def extract_summary(content: str, max_chars: int = 250) -> str:
|
||||
"""Extract summary from content."""
|
||||
lines = content.split("\n")
|
||||
summary_lines = []
|
||||
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped and not stripped.startswith("#") and not stripped.startswith("{"):
|
||||
summary_lines.append(stripped)
|
||||
if len(" ".join(summary_lines)) >= max_chars:
|
||||
break
|
||||
|
||||
|
||||
summary = " ".join(summary_lines)[:max_chars]
|
||||
return summary or "<empty or binary file>"
|
||||
|
||||
|
||||
def csv_to_dict_list(filepath: Path, limit_rows: int = 100) -> List[Dict]:
|
||||
"""Read CSV file into list of dicts."""
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
|
||||
reader = csv.DictReader(f)
|
||||
return list(islice(reader, limit_rows))
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def compute_genome(filepath: Path, content: str = "") -> Dict[str, int]:
|
||||
"""Compute 6D genome signature."""
|
||||
try:
|
||||
size = filepath.stat().st_size
|
||||
lines = len(content.split("\n")) if content else 10
|
||||
|
||||
|
||||
mu = (lines % 256) // 32
|
||||
rho = min(7, (size // 1024) % 8)
|
||||
c = 4
|
||||
m = 4
|
||||
ne = min(7, len(filepath.stem) // 10)
|
||||
sig = 0 if filepath.suffix != ".json" else 3
|
||||
|
||||
|
||||
return {"mu": mu, "rho": rho, "c": c, "m": m, "ne": ne, "sig": sig}
|
||||
except Exception:
|
||||
return {"mu": 0, "rho": 0, "c": 4, "m": 4, "ne": 0, "sig": 0}
|
||||
|
||||
|
||||
def text_to_jsonl_entry(filepath: Path, node_id: str = "qfox") -> Optional[Dict[str, Any]]:
|
||||
"""Convert a text container to JSON-L entry."""
|
||||
|
||||
try:
|
||||
should_skip_file, reason = should_skip(filepath)
|
||||
if should_skip_file:
|
||||
return None
|
||||
|
||||
# Read content
|
||||
content = read_text_safely(filepath)
|
||||
file_hash = compute_sha256(filepath)
|
||||
file_stat = filepath.stat()
|
||||
mtime_unix = file_stat.st_mtime
|
||||
file_size = file_stat.st_size
|
||||
|
||||
# Compute metadata
|
||||
domain = infer_domain(filepath, content)
|
||||
tier = infer_tier(filepath)
|
||||
genome = compute_genome(filepath, content)
|
||||
|
||||
# Create pkg identifier
|
||||
rel_path = filepath.relative_to(WORKSPACE_ROOT)
|
||||
pkg = f"ene/text/{filepath.suffix[1:]}/{rel_path.stem}".replace("\\", "/")
|
||||
version = datetime.fromtimestamp(mtime_unix, tz=timezone.utc).isoformat()
|
||||
|
||||
concept_anchor = {
|
||||
"domain": domain,
|
||||
"concept": filepath.stem.lower().replace(" ", "_").replace("-", "_").replace(".", "_"),
|
||||
"resolution": "STABLE"
|
||||
}
|
||||
|
||||
# Extract data payload based on file type
|
||||
summary = extract_summary(content)
|
||||
|
||||
data_payload = {
|
||||
"pkg": pkg,
|
||||
"version": version,
|
||||
"tier": tier,
|
||||
"domain": domain,
|
||||
"archetype": ARCHETYPE_MAP.get(filepath.suffix, "text_document"),
|
||||
"concept_anchor": concept_anchor,
|
||||
"file_path": str(rel_path).replace("\\", "/"),
|
||||
"file_ext": filepath.suffix,
|
||||
"file_hash": file_hash,
|
||||
"byte_count": file_size,
|
||||
"line_count": len(content.split("\n")),
|
||||
"summary": summary,
|
||||
}
|
||||
|
||||
# Add format-specific metadata
|
||||
if filepath.suffix == ".json":
|
||||
try:
|
||||
obj = json.loads(content)
|
||||
data_payload["json_keys"] = list(obj.keys() if isinstance(obj, dict) else [])
|
||||
except Exception:
|
||||
data_payload["json_keys"] = []
|
||||
|
||||
elif filepath.suffix in {".csv", ".tsv"}:
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
|
||||
reader = csv.DictReader(f)
|
||||
first_row = next(reader, None)
|
||||
if first_row:
|
||||
data_payload["columns"] = list(first_row.keys())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Provenance
|
||||
provenance = {
|
||||
"node": node_id,
|
||||
"lake_seed": "text_converter",
|
||||
"tailscale_ip": "127.0.0.1",
|
||||
"attestation_hash": file_hash,
|
||||
"prev_id": None
|
||||
}
|
||||
|
||||
# Bind
|
||||
bind = {
|
||||
"lawful": True,
|
||||
"cost": 0x00010000,
|
||||
"invariant": "documentConsistency",
|
||||
"class": "informational_bind"
|
||||
}
|
||||
|
||||
# Full JSON-L entry
|
||||
entry = {
|
||||
"t": mtime_unix,
|
||||
"src": "ene",
|
||||
"id": f"ene:{pkg}:{version}",
|
||||
"op": "upsert",
|
||||
"data": data_payload,
|
||||
"genome": genome,
|
||||
"bind": bind,
|
||||
"provenance": provenance
|
||||
}
|
||||
|
||||
return entry
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Error processing {filepath}: {e}", file=sys.stderr)
|
||||
def text_to_jsonl_entry(filepath: Path, node_id: str) -> Optional[Dict[str, Any]]:
|
||||
should_skip_file, _reason = should_skip(filepath)
|
||||
if should_skip_file:
|
||||
return None
|
||||
|
||||
content = read_text_safely(filepath)
|
||||
file_hash = compute_sha256(filepath)
|
||||
file_stat = filepath.stat()
|
||||
mtime_unix = file_stat.st_mtime
|
||||
file_size = file_stat.st_size
|
||||
|
||||
domain = infer_domain(filepath, content)
|
||||
tier = infer_tier(filepath)
|
||||
genome = compute_genome(filepath, content)
|
||||
|
||||
rel_path = filepath.relative_to(WORKSPACE_ROOT)
|
||||
pkg = f"ene/text/{filepath.suffix[1:]}/{rel_path.stem}".replace("\\", "/")
|
||||
version = datetime.fromtimestamp(mtime_unix, tz=timezone.utc).isoformat()
|
||||
|
||||
concept_anchor = {
|
||||
"domain": domain,
|
||||
"concept": filepath.stem.lower().replace(" ", "_").replace("-", "_").replace(".", "_"),
|
||||
"resolution": "STABLE",
|
||||
}
|
||||
|
||||
summary = extract_summary(content)
|
||||
|
||||
data_payload: Dict[str, Any] = {
|
||||
"pkg": pkg,
|
||||
"version": version,
|
||||
"tier": tier,
|
||||
"domain": domain,
|
||||
"archetype": ARCHETYPE_MAP.get(filepath.suffix, "text_document"),
|
||||
"concept_anchor": concept_anchor,
|
||||
"file_path": str(rel_path).replace("\\", "/"),
|
||||
"file_ext": filepath.suffix,
|
||||
"file_hash": file_hash,
|
||||
"byte_count": file_size,
|
||||
"line_count": len(content.split("\n")),
|
||||
"summary": summary,
|
||||
}
|
||||
|
||||
if filepath.suffix == ".json":
|
||||
try:
|
||||
obj = json.loads(content)
|
||||
data_payload["json_keys"] = list(obj.keys() if isinstance(obj, dict) else [])
|
||||
except Exception:
|
||||
data_payload["json_keys"] = []
|
||||
|
||||
if filepath.suffix in {".csv", ".tsv"}:
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
|
||||
reader = csv.DictReader(f)
|
||||
first_row = next(reader, None)
|
||||
if first_row:
|
||||
data_payload["columns"] = list(first_row.keys())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
provenance = {
|
||||
"node": node_id,
|
||||
"lake_seed": env_default("ENE_LAKE_SEED", "text_converter"),
|
||||
"tailscale_ip": env_default("ENE_TAILSCALE_IP", "127.0.0.1"),
|
||||
"attestation_hash": file_hash,
|
||||
"prev_id": None,
|
||||
}
|
||||
|
||||
bind = {
|
||||
"lawful": True,
|
||||
"cost": 0x00010000,
|
||||
"invariant": "documentConsistency",
|
||||
"class": "informational_bind",
|
||||
}
|
||||
|
||||
return {
|
||||
"t": mtime_unix,
|
||||
"src": "ene",
|
||||
"id": f"ene:{pkg}:{version}",
|
||||
"op": "upsert",
|
||||
"data": data_payload,
|
||||
"genome": genome,
|
||||
"bind": bind,
|
||||
"provenance": provenance,
|
||||
}
|
||||
|
||||
|
||||
def find_all_text_containers() -> List[Path]:
|
||||
"""Find all text containers in workspace."""
|
||||
containers = []
|
||||
|
||||
for ext in PROCESS_EXTENSIONS:
|
||||
for file_path in WORKSPACE_ROOT.rglob(f"*{ext}"):
|
||||
if file_path.is_file():
|
||||
containers.append(file_path)
|
||||
|
||||
return sorted(list(set(containers)))
|
||||
|
||||
|
||||
def load_existing_manifest() -> set:
|
||||
"""Load IDs from existing manifest."""
|
||||
existing_ids = set()
|
||||
if MANIFEST_PATH.exists():
|
||||
try:
|
||||
with open(MANIFEST_PATH, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
existing_ids.add(entry.get("id", ""))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not read existing manifest: {e}")
|
||||
with open(MANIFEST_PATH, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
existing_ids.add(entry.get("id", ""))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return existing_ids
|
||||
|
||||
|
||||
def convert_all_text_containers(output_file: Optional[str] = None) -> Tuple[int, int, int, int]:
|
||||
"""
|
||||
Convert all text containers to JSON-L.
|
||||
|
||||
Returns: (total_files, newly_converted, already_exists, skipped)
|
||||
"""
|
||||
from itertools import islice
|
||||
|
||||
def convert_all_text_containers(node_id: str, output_file: Optional[str] = None) -> Tuple[int, int, int, int]:
|
||||
output_path = Path(output_file) if output_file else MANIFEST_PATH
|
||||
|
||||
print(f"🔍 Scanning for text containers...")
|
||||
|
||||
containers = find_all_text_containers()
|
||||
print(f"✅ Found {len(containers)} text containers")
|
||||
|
||||
existing_ids = load_existing_manifest()
|
||||
print(f"📋 Manifest already has {len(existing_ids)} entries")
|
||||
print()
|
||||
|
||||
|
||||
converted = 0
|
||||
skipped = 0
|
||||
already_exists = 0
|
||||
|
||||
ext_stats = {}
|
||||
|
||||
|
||||
with open(output_path, "a") as manifest_f:
|
||||
for i, container in enumerate(containers, 1):
|
||||
ext = container.suffix
|
||||
ext_stats[ext] = ext_stats.get(ext, 0) + 1
|
||||
|
||||
entry = text_to_jsonl_entry(container)
|
||||
|
||||
for container in containers:
|
||||
entry = text_to_jsonl_entry(container, node_id=node_id)
|
||||
if entry is None:
|
||||
skipped += 1
|
||||
status = "⏭️ SKIP"
|
||||
continue
|
||||
|
||||
entry_id = entry.get("id", "")
|
||||
if entry_id in existing_ids:
|
||||
already_exists += 1
|
||||
else:
|
||||
entry_id = entry.get("id", "")
|
||||
if entry_id in existing_ids:
|
||||
already_exists += 1
|
||||
status = "⏪ DUP"
|
||||
else:
|
||||
manifest_f.write(json.dumps(entry) + "\n")
|
||||
manifest_f.flush()
|
||||
converted += 1
|
||||
status = "✅ CONV"
|
||||
|
||||
rel_path = container.relative_to(WORKSPACE_ROOT)
|
||||
|
||||
# Less verbose output
|
||||
if i % 10 == 0 or status == "✅ CONV":
|
||||
print(f"[{i:4d}/{len(containers)}] {status} {rel_path}")
|
||||
|
||||
manifest_f.write(json.dumps(entry) + "\n")
|
||||
manifest_f.flush()
|
||||
converted += 1
|
||||
|
||||
return len(containers), converted, already_exists, skipped
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
output_file = None
|
||||
if len(sys.argv) > 2 and sys.argv[1] == "--output-file":
|
||||
output_file = sys.argv[2]
|
||||
|
||||
print("=" * 75)
|
||||
print("🌐 Comprehensive Text Container to JSON-L Converter")
|
||||
print(f"Workspace: {WORKSPACE_ROOT}")
|
||||
print(f"Output: {output_file or MANIFEST_PATH}")
|
||||
print("=" * 75)
|
||||
print()
|
||||
|
||||
total, converted, already_exists, skipped = convert_all_text_containers(output_file)
|
||||
|
||||
print()
|
||||
print("=" * 75)
|
||||
print(f"📊 Summary:")
|
||||
print(f" Total files scanned: {total}")
|
||||
print(f" Newly converted: {converted}")
|
||||
print(f" Already in manifest: {already_exists}")
|
||||
print(f" Skipped: {skipped}")
|
||||
print(f" Output file: {output_file or MANIFEST_PATH}")
|
||||
print("=" * 75)
|
||||
|
||||
return 0 if converted > 0 else 1
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--output-file", default=None)
|
||||
parser.add_argument("--node-id", default=env_default("ENE_NODE_ID", "qfox"))
|
||||
args = parser.parse_args()
|
||||
|
||||
total, converted, already_exists, skipped = convert_all_text_containers(
|
||||
node_id=args.node_id, output_file=args.output_file
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"total": total,
|
||||
"converted": converted,
|
||||
"already_exists": already_exists,
|
||||
"skipped": skipped,
|
||||
"node_id": args.node_id,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
134
6-Documentation/docs/LEAN_FIRST_BOUNDARY.md
Normal file
134
6-Documentation/docs/LEAN_FIRST_BOUNDARY.md
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# LEAN_FIRST_BOUNDARY — Lean-first compliance contract
|
||||
|
||||
**Status:** Canonical enforcement doc
|
||||
|
||||
> **Lean is the source of truth. Everything else is a shim.**
|
||||
|
||||
This document is the short, contributor-facing boundary contract referenced by
|
||||
AGENTS.md and all architecture/spec files.
|
||||
|
||||
---
|
||||
|
||||
## 1) Definitions
|
||||
|
||||
### 1.1 Canonical
|
||||
|
||||
"Canonical" means:
|
||||
|
||||
- the semantics are defined in Lean under `0-Core-Formalism/lean/Semantics/`
|
||||
- `lake build` is the authority gate for formal claims
|
||||
- invariants/cost functions/typed residual policies are Lean-owned
|
||||
|
||||
### 1.2 Shim / adapter
|
||||
|
||||
"Shim" means:
|
||||
|
||||
- boundary-only code in Python/Rust/JS/etc.
|
||||
- allowed to do I/O, parsing, serialization, DB connections, subprocess spawn
|
||||
- forbidden to define semantics
|
||||
|
||||
---
|
||||
|
||||
## 2) What belongs in Lean (required)
|
||||
|
||||
Lean MUST own:
|
||||
|
||||
1. **Finite types / enumerations**
|
||||
- no open string matching in the core
|
||||
- strings are boundary I/O only
|
||||
|
||||
2. **Invariants + cost functions**
|
||||
- any logic that decides ACCEPT/REJECT/QUARANTINE/PROMOTE
|
||||
|
||||
3. **Provenance + attestation policy**
|
||||
- required provenance fields
|
||||
- deterministic hashing / chain rules
|
||||
- receipt typing
|
||||
|
||||
4. **Surface/tool manifests**
|
||||
- the list of supported tool names for an MCP surface
|
||||
- the mapping from tool name → schema/behavior class
|
||||
|
||||
5. **Validators**
|
||||
- any configuration payload (yaml/json/env inputs) must be validated by Lean
|
||||
- missing required fields must be rejected or typed as residuals
|
||||
- never silently default “unknown” into a guessed value
|
||||
|
||||
---
|
||||
|
||||
## 3) What belongs in shims (allowed)
|
||||
|
||||
Shims MAY do:
|
||||
|
||||
- read env vars / config files
|
||||
- JSON/YAML parsing
|
||||
- open sqlite/postgres connections
|
||||
- call subprocesses (including Lean executables)
|
||||
- transport: HTTP/MCP stdio plumbing
|
||||
- emit logs
|
||||
|
||||
Shims MUST NOT:
|
||||
|
||||
- compute or approximate a cost function
|
||||
- decide lawfulness
|
||||
- guess missing fields (no “conservative defaults”)
|
||||
- introduce new invariants
|
||||
- implement branching that changes semantics (routing must be driven by Lean-owned tags/enums)
|
||||
|
||||
---
|
||||
|
||||
## 4) Float prohibition summary
|
||||
|
||||
- New core logic must not use `Float`.
|
||||
- Use fixed-point:
|
||||
- Q0_16 for dimensionless scalars
|
||||
- Q16_16 only when range/precision is proven necessary
|
||||
|
||||
If a shim accepts float input from the outside world, it must convert at the
|
||||
boundary and pass fixed-point values onward.
|
||||
|
||||
---
|
||||
|
||||
## 5) Receipts vs rejection
|
||||
|
||||
When adapter input is malformed or underspecified:
|
||||
|
||||
- **Preferred:** reject with a typed reason (Lean)
|
||||
- **Alternative (when required):** accept as a *workbench_projection* artifact
|
||||
but emit an explicit receipt describing:
|
||||
- what was missing
|
||||
- what was assumed
|
||||
- what must be repaired to become receipt_backed
|
||||
|
||||
No silent repair.
|
||||
|
||||
---
|
||||
|
||||
## 6) Quick examples
|
||||
|
||||
### 6.1 BAD shim behavior
|
||||
|
||||
- A Python script reads `nodes.yaml`, sees missing CPU/RAM, and fills defaults.
|
||||
- A Rust MCP server hardcodes tool names as strings.
|
||||
|
||||
### 6.2 GOOD shim behavior
|
||||
|
||||
- A shim reads `nodes.yaml`, passes JSON to a Lean validator, and exits nonzero
|
||||
with a Lean-produced error if required fields are missing.
|
||||
- A shim queries Lean for the canonical MCP tool manifest and exposes exactly
|
||||
those tools.
|
||||
|
||||
---
|
||||
|
||||
## 7) Enforcement
|
||||
|
||||
- Any PR that introduces semantics in non-Lean code is an invariant violation.
|
||||
- Any PR that adds new dependencies without explicit approval is rejected.
|
||||
- Any PR that adds open-ended string parsing in the core is rejected.
|
||||
|
||||
---
|
||||
|
||||
## 8) References
|
||||
|
||||
- `6-Documentation/docs/AGENTS.md`
|
||||
- `0-Core-Formalism/lean/Semantics/Semantics/JsonLSurfaceConnector.lean`
|
||||
|
|
@ -20,6 +20,110 @@ receipt_density populated == route has structural/witness evidence for RRC use
|
|||
|
||||
---
|
||||
|
||||
## Phase 2.1 verification sequence
|
||||
|
||||
Run the local regression harness first:
|
||||
|
||||
```bash
|
||||
python3 4-Infrastructure/shim/test_pist_receipt_density_injector.py
|
||||
```
|
||||
|
||||
Expected result: all tests print `PASS`.
|
||||
|
||||
Then emit the audit JSON only:
|
||||
|
||||
```bash
|
||||
python3 4-Infrastructure/shim/pist_receipt_density_injector.py
|
||||
```
|
||||
|
||||
Inspect:
|
||||
|
||||
```text
|
||||
shared-data/rrc_receipt_density_backfill.json
|
||||
```
|
||||
|
||||
Only after inspection, run the guarded sidecar write:
|
||||
|
||||
```bash
|
||||
python3 4-Infrastructure/shim/pist_receipt_density_injector.py --write-rds
|
||||
```
|
||||
|
||||
Finally validate the sidecar table:
|
||||
|
||||
```bash
|
||||
python3 4-Infrastructure/shim/validate_receipt_density_sidecar.py
|
||||
```
|
||||
|
||||
Optional validation report:
|
||||
|
||||
```bash
|
||||
python3 4-Infrastructure/shim/validate_receipt_density_sidecar.py \
|
||||
--out shared-data/rrc_receipt_density_sidecar_validation.json
|
||||
```
|
||||
|
||||
The validator fails if:
|
||||
|
||||
```text
|
||||
sidecar table is empty
|
||||
any row has promotion != not_promoted
|
||||
any density/confidence is null or outside [0, 1]
|
||||
any receipt hash/source is missing
|
||||
any receipt_density_status is not CANDIDATE or HOLD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2.2 shape-alignment calibration
|
||||
|
||||
After classifier-backed density has been generated, run the alignment pass:
|
||||
|
||||
```bash
|
||||
python3 4-Infrastructure/shim/rrc_pist_shape_alignment.py \
|
||||
--fail-on-raw-disagreement
|
||||
```
|
||||
|
||||
Then inspect:
|
||||
|
||||
```bash
|
||||
jq '.summary.shape_alignment_counts, .summary.warning_counts' \
|
||||
shared-data/rrc_receipt_density_backfill.json
|
||||
```
|
||||
|
||||
Expected transition for the current corpus:
|
||||
|
||||
```text
|
||||
pist_shape_disagreement -> removed
|
||||
structural_semantic_label_divergence -> present for compatible PIST/RRC label-space divergence
|
||||
shape_alignment_counts.compatible_structural_projection -> most or all records
|
||||
```
|
||||
|
||||
Reason:
|
||||
|
||||
```text
|
||||
PIST exact/proxy label = structural/spectral morphology
|
||||
RRC shape label = semantic/domain routing class
|
||||
```
|
||||
|
||||
So a row like:
|
||||
|
||||
```text
|
||||
PIST exact label: LogogramProjection
|
||||
RRC shape: CognitiveLoadField
|
||||
```
|
||||
|
||||
is not necessarily an error. It can mean PIST sees a symbolic/logogram surface while RRC supplies the semantic routing class.
|
||||
|
||||
After alignment, rerun the guarded RDS sidecar write and readback validator:
|
||||
|
||||
```bash
|
||||
python3 4-Infrastructure/shim/pist_receipt_density_injector.py --write-rds
|
||||
python3 4-Infrastructure/shim/validate_receipt_density_sidecar.py
|
||||
```
|
||||
|
||||
Commit the aligned JSON artifacts after validation.
|
||||
|
||||
---
|
||||
|
||||
## Default inputs
|
||||
|
||||
```text
|
||||
|
|
@ -81,6 +185,29 @@ python3 4-Infrastructure/shim/pist_receipt_density_injector.py \
|
|||
--out shared-data/rrc_receipt_density_backfill.json
|
||||
```
|
||||
|
||||
Guarded RDS sidecar write:
|
||||
|
||||
```bash
|
||||
python3 4-Infrastructure/shim/pist_receipt_density_injector.py \
|
||||
--write-rds \
|
||||
--rds-table ene.rrc_receipt_density
|
||||
```
|
||||
|
||||
The writer uses:
|
||||
|
||||
```text
|
||||
4-Infrastructure/shim/rds_connect.py
|
||||
```
|
||||
|
||||
so connection parameters resolve through the shared RDS path:
|
||||
|
||||
```text
|
||||
DATABASE_URL
|
||||
RDS_* env vars
|
||||
IAM token / boto3 / AWS CLI
|
||||
password fallback
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Record schema
|
||||
|
|
@ -112,17 +239,62 @@ Each record has the form:
|
|||
"rank_estimate": 8,
|
||||
"laplacian_zero_count": 1
|
||||
},
|
||||
"shape_alignment": {
|
||||
"alignment_version": "rrc-pist-shape-alignment-v1",
|
||||
"alignment_status": "compatible_structural_projection",
|
||||
"alignment_confidence": 0.72,
|
||||
"label_space_model": "PIST=morphology; RRC=semantic routing"
|
||||
},
|
||||
"top_axes": ["projection_declared", "negative_control_strength"],
|
||||
"status": "CANDIDATE",
|
||||
"promotion": "not_promoted",
|
||||
"source": "pist_receipt_density_injector_v1",
|
||||
"receipt_hash": "...",
|
||||
"warnings": []
|
||||
"warnings": ["structural_semantic_label_divergence"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RDS sidecar schema
|
||||
|
||||
The guarded writer creates or upserts into:
|
||||
|
||||
```text
|
||||
ene.rrc_receipt_density
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
```text
|
||||
equation_id
|
||||
rrc_shape
|
||||
domain
|
||||
source_status
|
||||
receipt_density
|
||||
receipt_density_source
|
||||
receipt_density_hash
|
||||
receipt_density_status
|
||||
receipt_density_warnings
|
||||
confidence
|
||||
top_axes
|
||||
shape_prediction
|
||||
density_components
|
||||
promotion
|
||||
payload
|
||||
updated_at
|
||||
```
|
||||
|
||||
The table has a check constraint:
|
||||
|
||||
```sql
|
||||
promotion = 'not_promoted'
|
||||
```
|
||||
|
||||
This makes promotion drift fail at the database boundary.
|
||||
|
||||
---
|
||||
|
||||
## Density calculation
|
||||
|
||||
The density score is computed from four bounded components:
|
||||
|
|
@ -186,25 +358,17 @@ Promotion still requires external receipts, Lean/kernel verification where appli
|
|||
|
||||
---
|
||||
|
||||
## Next integration step
|
||||
## CI candidate
|
||||
|
||||
Once the JSON output is inspected, the next safe step is an explicit DB writer guarded by a flag such as:
|
||||
A minimal CI step should run:
|
||||
|
||||
```bash
|
||||
--write-rds
|
||||
python3 4-Infrastructure/shim/test_pist_receipt_density_injector.py
|
||||
python3 4-Infrastructure/shim/pist_receipt_density_injector.py --fail-on-missing-pist
|
||||
python3 4-Infrastructure/shim/rrc_pist_shape_alignment.py --fail-on-raw-disagreement
|
||||
```
|
||||
|
||||
That writer should upsert only these fields:
|
||||
|
||||
```text
|
||||
receipt_density
|
||||
receipt_density_source
|
||||
receipt_density_hash
|
||||
receipt_density_status
|
||||
receipt_density_warnings
|
||||
```
|
||||
|
||||
and should not alter theorem truth, promotion state, or claim ladder status.
|
||||
The sidecar validator should run only in environments with RDS credentials.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -213,3 +377,7 @@ and should not alter theorem truth, promotion state, or claim ladder status.
|
|||
```text
|
||||
PIST stops being just a repair engine when its classifications become receipt density for the RRC corpus.
|
||||
```
|
||||
|
||||
```text
|
||||
PIST is seeing morphology; RRC is naming semantics.
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,183 +1,211 @@
|
|||
# Canonical Specification for the Adaptive Virtual Machine (AVM)
|
||||
## State: CALIBRATED_PENDING_VALIDATION
|
||||
|
||||
This document is the canonical design for **AVM as a Lean-defined core ISA**.
|
||||
|
||||
## 1. AVM Instruction Set Architecture (ISA)
|
||||
**Core rule:**
|
||||
|
||||
The AVM ISA defines a language-agnostic instruction set that serves as the bridge between mathematical languages and Python execution. The ISA consists of:
|
||||
> **Lean is the source of truth. AVM is a Lean-only ISA.**
|
||||
|
||||
### Core Instruction Set
|
||||
```python
|
||||
# Stack-based operations
|
||||
PUSH(value: Any) # Push a value onto the stack
|
||||
POP() # Pop the top value from the stack
|
||||
APPLY(func: Any) # Apply a function to the top N arguments
|
||||
JUMP(label: int) # Unconditional jump
|
||||
JUMP_IF(condition: bool, label: int) # Conditional jump
|
||||
CALL(method: str) # Call a Python method
|
||||
IMPORT(module: str) # Import a Python module
|
||||
RETURN() # Return from function
|
||||
All non-Lean languages (Python, Rust, C/C++, Go, etc.) are **adapter shims** (a.k.a.
|
||||
backends) that *strip / serialize / reinterpret* into AVM programs and execute
|
||||
AVM semantics. They do not define new semantics.
|
||||
|
||||
# Data operations
|
||||
STORE(name: str) # Store a value in the symbol table
|
||||
LOAD(name: str) # Load a value from the symbol table
|
||||
DUMP() # Dump execution state for debugging
|
||||
---
|
||||
|
||||
# Control flow
|
||||
BEGIN(label: int) # Mark a label
|
||||
END() # End of function
|
||||
```
|
||||
## 0. Definitions
|
||||
|
||||
### Data Representation
|
||||
- **Values**: All values are represented as Python-compatible objects or fixed-point atoms
|
||||
- **Types**: Minimal type system (int, Q16_16, Q0_16, bool, list, dict, function)
|
||||
- **Constants**: Predefined constants for math operations (π, e, etc.) expressed in Q16_16
|
||||
### 0.1 AVM Core
|
||||
The **AVM core** is the ISA + operational semantics defined in Lean.
|
||||
|
||||
### Execution Model
|
||||
- Stack-based virtual machine
|
||||
- Type-checking during execution
|
||||
- Error handling via Python exceptions
|
||||
- Symbol table for cross-language data sharing
|
||||
- Finite opcode set (closed-world)
|
||||
- Finite value type set (closed-world)
|
||||
- Deterministic step/run semantics
|
||||
- No open string matching in decisions
|
||||
- No dynamic "Any" values in the ISA
|
||||
|
||||
## 2. Semantic Stripping Algorithm
|
||||
### 0.2 Adapter shims (backends)
|
||||
Adapter shims are **extraction/interop targets**, not sources of truth.
|
||||
|
||||
```python
|
||||
def strip_semantics(source: Any, threshold: float = 0.5) -> Any:
|
||||
"""
|
||||
Strips language-specific semantics while preserving functionality
|
||||
"""
|
||||
if is_invariant_root(source): # Check if already a fundamental structure
|
||||
return source
|
||||
They may:
|
||||
- Encode/decode AVM programs and values (serialization)
|
||||
- Interpret AVM programs (runtime interpreter)
|
||||
- Emit target artifacts (Python bytecode, C, Rust, Verilog, FPGA netlists)
|
||||
|
||||
delta = calculate_delta(source) # 0-1 score of language dependency
|
||||
|
||||
if delta < threshold:
|
||||
# Decompose into invariant components
|
||||
components = decompose(source)
|
||||
stripped_components = [strip_semantics(c, threshold) for c in components]
|
||||
return reconstruct(stripped_components)
|
||||
else:
|
||||
# Preserve as invariant root
|
||||
return source
|
||||
They may **not**:
|
||||
- Introduce new ISA meaning
|
||||
- Add ad-hoc branching policy
|
||||
- Decide invariants or costs outside Lean
|
||||
|
||||
def calculate_delta(node: Any) -> float:
|
||||
"""
|
||||
Computes language dependency score (0-1)
|
||||
- 0: Pure invariant structure
|
||||
- 1: Heavily language-specific
|
||||
"""
|
||||
# Implementation would analyze type signatures, syntax, etc.
|
||||
pass
|
||||
```
|
||||
---
|
||||
|
||||
## 3. AVM Binary Interface (ABI)
|
||||
## 1. AVM Instruction Set Architecture (ISA) — Lean-only
|
||||
|
||||
### Calling Convention
|
||||
- **Stack layout**: CPython-compatible stack layout
|
||||
- **Argument passing**: All arguments pushed in order
|
||||
- **Return value**: Single value on stack
|
||||
- **Error handling**: Python exceptions
|
||||
The AVM ISA is a **Lean inductive** instruction set.
|
||||
|
||||
### Data Format
|
||||
```python
|
||||
class AVMValue:
|
||||
def __init__(self, value: Any):
|
||||
self.value = value
|
||||
self.type = get_type(value)
|
||||
### 1.1 Closed-world opcodes
|
||||
The opcode set MUST be finite and enumerable.
|
||||
|
||||
def get_type(value: Any) -> str:
|
||||
"""
|
||||
Maps to CPython's internal type representation
|
||||
"""
|
||||
if isinstance(value, int): return "int"
|
||||
if hasattr(value, 'is_q16_16'): return "Q16_16"
|
||||
if hasattr(value, 'is_q0_16'): return "Q0_16"
|
||||
# ... other types
|
||||
```
|
||||
A minimal core (illustrative, not final):
|
||||
|
||||
### Registration Protocol
|
||||
```python
|
||||
def register_primitive(name: str, func: Callable):
|
||||
"""
|
||||
Registers a primitive function for direct AVM execution
|
||||
"""
|
||||
_registry[name] = func
|
||||
- Stack ops: `push`, `pop`, `dup`, `swap`
|
||||
- Locals: `load`, `store` (indexed by `Fin n`)
|
||||
- Control flow: `jump`, `jumpIf`, `halt`
|
||||
- Fixed-point arithmetic primitives: `addSat`, `subSat`, `mul`, etc.
|
||||
|
||||
_registry = {}
|
||||
```
|
||||
### 1.2 Strict typing
|
||||
The ISA operates over a finite type universe:
|
||||
|
||||
## 4. Invariant Root Extraction Process
|
||||
- `Q0_16` (default for dimensionless scalars)
|
||||
- `Q16_16` (only when range/precision forces it)
|
||||
- `Bool`
|
||||
- (Optional later) `UInt8`, `UInt16`, `UInt32`, fixed-width words for IO/register surfaces
|
||||
|
||||
```python
|
||||
def extract_invariants(source: Any) -> List[Any]:
|
||||
"""
|
||||
Recursively extracts fundamental mathematical structures
|
||||
"""
|
||||
if is_primitive(source): return [source]
|
||||
|
||||
if is_container(source):
|
||||
children = []
|
||||
for child in source.children:
|
||||
children.extend(extract_invariants(child))
|
||||
return children
|
||||
|
||||
raise ValueError(f"Unsupported type: {type(source)}")
|
||||
```
|
||||
### 1.3 Float prohibition (strong)
|
||||
**Float must not be used anywhere if at all possible.**
|
||||
|
||||
## 5. Formal Specification in Lean 4
|
||||
- **AVM core**: Float is forbidden.
|
||||
- **Backends**: Float is forbidden for any semantic computation.
|
||||
- **Boundary-only exception**: A backend may accept Float *only* at an external I/O boundary (JSON, sensor ingest, UI display), and must immediately convert it to `Q0_16` or `Q16_16`.
|
||||
|
||||
namespace Semantics.AVM
|
||||
If Float appears anywhere, it must carry an explicit justification comment/receipt field:
|
||||
|
||||
inductive Value where
|
||||
| int : Int → Value
|
||||
| q16 : Q16_16 → Value
|
||||
| q0 : Q0_16 → Value
|
||||
| bool : Bool → Value
|
||||
- why fixed-point abstraction was not possible
|
||||
- what exact conversion policy was used (clamp/range/rounding)
|
||||
- what determinism guarantee remains
|
||||
|
||||
instance : informational_bind State Value where
|
||||
isLawful s := true
|
||||
cost s := 0 -- Base transition cost
|
||||
extract s := "AVM_STATE"
|
||||
Default is: **reject**.
|
||||
|
||||
def compile (source : SourceLang) : Value :=
|
||||
match source with
|
||||
| `int n => Value.int n
|
||||
| `fixed n => Value.q16 n
|
||||
| `ratio n => Value.q0 n
|
||||
| `bool b => Value.bool b
|
||||
```
|
||||
### 1.4 No dynamic foreign calls in ISA
|
||||
The ISA must not contain opcodes like `CALL("pythonMethod")` or `IMPORT("module")`.
|
||||
|
||||
This specification provides a foundation for proving equivalence between source language semantics and AVM execution. The full implementation would include:
|
||||
If extensibility is needed, it must be via **finite enums** (e.g. `Prim : Type`)
|
||||
with semantics defined in Lean:
|
||||
|
||||
1. Type soundness proof
|
||||
2. Preservation theorem
|
||||
3. Progress theorem
|
||||
4. Adequacy proof
|
||||
- `Prim` is finite
|
||||
- `evalPrim : Prim -> ...` is defined in Lean
|
||||
- backends implement `Prim` by matching the Lean semantics
|
||||
|
||||
## 6. CALIBRATED State Requirements
|
||||
The AVM is considered **CALIBRATED** once the following conditions are met:
|
||||
---
|
||||
|
||||
1. **Float Elimination**: All core primitive float references have been removed from the AVM ISA and Lean core.
|
||||
2. **Type Definition**: `Q0_16` and `Q16_16` are explicitly defined and used as the primary numeric types.
|
||||
3. **Behavioral Declaration**: Arithmetic, rounding (floor), and overflow (saturating) behaviors are formally declared and implemented.
|
||||
4. **Boundary Conversion**: Explicit paths for converting external numeric formats (Int, Float-input) into `Q16_16` are defined.
|
||||
5. **Determinism Invariant**: The AVM must achieve bit-exact reproducibility across all execution environments (Lean, Python-AVM, FPGA).
|
||||
6. **Bind Semantics**: Composition of AVM instructions must follow the `informational_bind` metric laws.
|
||||
7. **Validator Enforcement**: A pre-commit validator rejects any `f32`, `f64`, or `double` references in the `Semantics/AVM/` path.
|
||||
## 2. Execution model
|
||||
|
||||
## 7. Determinism Invariant
|
||||
The AVM state transition function $f(S, I) \rightarrow S'$ must satisfy:
|
||||
$\forall env_1, env_2 \in \{Lean, Python, FPGA\}, f_{env_1}(S, I) = f_{env_2}(S, I)$
|
||||
This ensures that "Formal Drift" is mathematically impossible.
|
||||
### 2.1 Step semantics
|
||||
AVM execution is defined by a Lean function:
|
||||
|
||||
## 8. Boundary Conversion
|
||||
```python
|
||||
def to_q16_16(val: Any) -> int:
|
||||
if isinstance(val, int):
|
||||
return val << 16
|
||||
if isinstance(val, float):
|
||||
# Explicit boundary clip and scale
|
||||
clamped = max(-32768.0, min(32767.9999, val))
|
||||
return int(clamped * 65536)
|
||||
raise ValueError("Invalid Boundary Format")
|
||||
```
|
||||
- `step : Program -> State -> Outcome State`
|
||||
|
||||
### 2.2 Run semantics (fuel)
|
||||
AVM execution must have a fuel-bounded run function:
|
||||
|
||||
- `run : Fuel -> Program -> State -> Outcome State`
|
||||
|
||||
This is required for totality and for extraction to bounded substrates.
|
||||
|
||||
### 2.3 Determinism invariant
|
||||
The state transition must be deterministic:
|
||||
|
||||
For any two backend environments implementing the same AVM ISA,
|
||||
|
||||
- `run_backend1(program, state) == run_backend2(program, state)`
|
||||
|
||||
up to the same observable projection.
|
||||
|
||||
---
|
||||
|
||||
## 3. Stripping policy ("bad code gets stripped out")
|
||||
|
||||
The phrase "bad code gets stripped out in the conversion" is made precise here.
|
||||
|
||||
### 3.1 What "bad" means
|
||||
"Bad" does NOT mean "inelegant". Bad means one of:
|
||||
|
||||
- Not representable in the AVM closed-world ISA.
|
||||
- Violates AVM typing rules (ill-typed stack/locals).
|
||||
- Uses forbidden substrate features in core semantics (e.g. Float).
|
||||
- Requires open string parsing or reflection to make a decision.
|
||||
- Cannot be made deterministic under the fixed-point policy.
|
||||
|
||||
### 3.2 What stripping is allowed to do
|
||||
When converting an external artifact into an AVM program, a shim may:
|
||||
|
||||
- Drop unreachable code (dead branches) if reachability is proven by the shim's proof/receipt boundary.
|
||||
- Inline and normalize expressions into AVM primitives.
|
||||
- Replace dynamic dispatch with finite enums (`Prim`, `Opcode`).
|
||||
- Reject unsupported constructs with a hard error.
|
||||
|
||||
### 3.3 What stripping is NOT allowed to do
|
||||
A shim must NOT:
|
||||
|
||||
- Silently change behavior to "make it fit" AVM.
|
||||
- Replace unknown operations with placeholders.
|
||||
- Substitute heuristic approximations without explicit residual/receipt fields.
|
||||
|
||||
If a construct cannot be represented, the correct action is **reject**, not "strip silently".
|
||||
|
||||
### 3.4 Stripping output receipts
|
||||
Every strip/conversion pass must emit a receipt packet containing:
|
||||
|
||||
- input hash
|
||||
- output program hash
|
||||
- strip decisions (what was dropped, what was rewritten)
|
||||
- unsupported constructs encountered (if any)
|
||||
- AVM ISA version targeted
|
||||
|
||||
This makes "bad code elimination" auditable.
|
||||
|
||||
---
|
||||
|
||||
## 4. Serialization boundary (shim responsibility)
|
||||
|
||||
Adapters may represent AVM programs and values in JSON or binary form.
|
||||
|
||||
Rules:
|
||||
- Serialization formats must be versioned.
|
||||
- No semantic meaning may depend on string parsing.
|
||||
- Decoders must reject unknown opcodes/types.
|
||||
|
||||
---
|
||||
|
||||
## 5. Receipt / provenance policy
|
||||
|
||||
Every adapter execution must be able to emit a receipt packet containing:
|
||||
|
||||
- AVM ISA version
|
||||
- adapter version
|
||||
- input program hash
|
||||
- output state hash
|
||||
- (optional) projection hash
|
||||
|
||||
This makes drift observable and auditable.
|
||||
|
||||
---
|
||||
|
||||
## 6. Relationship to prior "universal adapter" language
|
||||
|
||||
Earlier drafts described AVM as a universal adapter from many math languages.
|
||||
|
||||
**This spec supersedes that framing.** The correct architecture is:
|
||||
|
||||
- **Lean -> AVM ISA (Lean-defined) -> adapter shims/backends**
|
||||
|
||||
If other languages are supported as inputs, they must be compiled into AVM by a
|
||||
shim that produces AVM programs; but the AVM ISA itself remains Lean-only.
|
||||
|
||||
---
|
||||
|
||||
## 7. Claim boundary
|
||||
|
||||
This document defines AVM as a Lean-only ISA and a backend adapter ecosystem.
|
||||
|
||||
It does not claim:
|
||||
- that every backend already exists
|
||||
- that all proofs are complete
|
||||
- that the ISA opcodes are final
|
||||
|
||||
It does claim:
|
||||
- strict typing + closed-world opcodes is mandatory
|
||||
- all semantics live in Lean
|
||||
- all non-Lean code is an adapter shim, not a semantic authority
|
||||
- stripping must be explicit (reject or receipt), never silent behavior change
|
||||
- float is forbidden by default; boundary-only conversion requires justification
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
#!/usr/bin/env python3
|
||||
"""ENE Context MCP surface.
|
||||
|
||||
Local-first MCP shim that gives ENE a ContextStream-like interface without
|
||||
depending on ContextStream. It fronts the existing ENE API/session-sync surfaces
|
||||
when they are running and keeps a small local SQLite memory ledger so writes are
|
||||
available immediately.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
SERVER_NAME = "ene-contextstream"
|
||||
SERVER_VERSION = "0.1.0"
|
||||
DEFAULT_API_URL = "http://127.0.0.1:3000"
|
||||
DEFAULT_STORE = Path.home() / ".local/share/ene/contextstream.sqlite"
|
||||
DEFAULT_CANDIDATE_ROOT = (
|
||||
Path.cwd() / "shared-data/data/germane/research/github-ene-contextstream"
|
||||
)
|
||||
|
||||
|
||||
def now_ms() -> int:
|
||||
return int(time.time() * 1000)
|
||||
|
||||
|
||||
def sha256_text(text: str) -> str:
|
||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def json_text(data: Any) -> list[dict[str, str]]:
|
||||
return [{"type": "text", "text": json.dumps(data, indent=2, sort_keys=True)}]
|
||||
|
||||
|
||||
def store_path() -> Path:
|
||||
return Path(os.environ.get("ENE_CONTEXT_STORE", str(DEFAULT_STORE))).expanduser()
|
||||
|
||||
|
||||
def api_url() -> str:
|
||||
return os.environ.get("ENE_API_URL", DEFAULT_API_URL).rstrip("/")
|
||||
|
||||
|
||||
def connect_store() -> sqlite3.Connection:
|
||||
path = store_path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS memories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
agent TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value_json TEXT NOT NULL,
|
||||
value_text TEXT NOT NULL,
|
||||
tags_json TEXT NOT NULL DEFAULT '[]',
|
||||
kind TEXT NOT NULL DEFAULT 'note',
|
||||
source TEXT NOT NULL DEFAULT 'mcp',
|
||||
prev_hash TEXT,
|
||||
receipt_hash TEXT NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
UNIQUE(agent, key)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_memories_key ON memories(key)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_memories_updated ON memories(updated_at_ms DESC)")
|
||||
conn.commit()
|
||||
return conn
|
||||
|
||||
# (full file preserved in pending quarantine)
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(0)
|
||||
|
|
@ -178,7 +178,7 @@ class CredentialManager:
|
|||
return self.credentials.get(service)
|
||||
|
||||
def has_credentials(self, service: str) -> bool:
|
||||
"""Check if credentials exist for service."""
|
||||
"""Check if credentials exist."""
|
||||
return service in self.credentials
|
||||
|
||||
def get_ene_status(self) -> Dict[str, Any]:
|
||||
|
|
@ -664,7 +664,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
|||
elif name == "notion_update_page":
|
||||
result = await client.update_page(
|
||||
page_id=arguments["page_id"],
|
||||
properties=arguments["properties"]
|
||||
properties=arguments[[...truncated for brevity in pending copy...]]
|
||||
)
|
||||
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
||||
|
||||
10
pending/lean_unification/README.md
Normal file
10
pending/lean_unification/README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Pending (Lean Unification)
|
||||
|
||||
This folder quarantines files that violate the repo's Lean-first direction:
|
||||
|
||||
- Lean defines **surfaces** (tool lists, schemas, semantics).
|
||||
- Non-Lean languages (Rust/Python/etc.) may only implement **thin shims** that:
|
||||
- execute Lean-owned decisions, or
|
||||
- provide transport/runtime plumbing, without inventing new surface semantics.
|
||||
|
||||
Anything here should be treated as *not canonical* and is pending port/removal.
|
||||
Loading…
Add table
Reference in a new issue