diff --git a/lib/sqlite_index.ml b/lib/sqlite_index.ml index b76d29b..cd39399 100644 --- a/lib/sqlite_index.ml +++ b/lib/sqlite_index.ml @@ -15,6 +15,14 @@ type summary = { holder : string option; } +type exploration_summary = { + id : string; + mode : Row.work_mode; + phase : Exploration.phase option; + attempt_count : int; + critique_count : int; +} + module Db = struct open Sqlite @@ -35,6 +43,12 @@ module Db = struct let claimed_at = field issues "claimed_at" (nullable float) let closed_at = field issues "closed_at" (nullable float) let blocked = field issues "blocked" int + let explorations = table "explorations" + let exploration_id = field explorations "id" text + let exploration_mode = field explorations "mode" text + let exploration_phase = field explorations "phase" (nullable text) + let attempt_count = field explorations "attempt_count" int + let critique_count = field explorations "critique_count" int let summaries = table "summaries" let summary_id = field summaries "id" text let summary_json = field summaries "json" text @@ -109,6 +123,14 @@ module Db = struct coldef closed_at; coldef ~not_null:true blocked; ]; + Schema.table explorations + [ + coldef ~primary_key:true exploration_id; + coldef ~not_null:true exploration_mode; + coldef exploration_phase; + coldef ~not_null:true attempt_count; + coldef ~not_null:true critique_count; + ]; Schema.table summaries [ coldef ~not_null:true summary_id; coldef ~not_null:true summary_json; @@ -191,6 +213,10 @@ module Db = struct Schema.index "issues_by_filed_at" issues [ key filed_at; key id ]; Schema.index "issues_by_closed_at" issues [ key closed_at; key id ]; Schema.index "issues_by_blocked" issues [ key blocked; key status ]; + Schema.index "explorations_by_mode_phase" explorations + [ key exploration_mode; key exploration_phase; key exploration_id ]; + Schema.index "explorations_by_phase" explorations + [ key exploration_phase; key exploration_id ]; Schema.index "labels_by_label" labels [ key label; key issue_id ]; Schema.index "dependencies_by_target" dependencies [ key target_id; key dependent_id ]; @@ -275,6 +301,24 @@ module Db = struct let fields_rows db = from row_fields |> select2 fields_id fields_json |> run db + let exploration_rows db ?mode ?phase () = + let q = from explorations in + let q = + match mode with + | None -> q + | Some value -> q |> where (exploration_mode =. value) + in + let q = + match phase with + | None -> q + | Some value -> q |> where (exploration_phase =. Some value) + in + q + |> select5 exploration_id exploration_mode exploration_phase attempt_count + critique_count + |> order_by exploration_id `Asc + |> run db + let cross_store_dependents db = from foreign_dependents |> select2 foreign_target foreign_count |> run db @@ -380,6 +424,38 @@ let disclosure_of_name = function | "published" -> Published | _ -> invalid_arg "sqlite index: invalid disclosure" +let mode_name = function + | Row.Delegate -> "delegate" + | Routine -> "routine" + | Deliberate -> "deliberate" + | Learn -> "learn" + +let mode_of_name = function + | "delegate" -> Row.Delegate + | "routine" -> Routine + | "deliberate" -> Deliberate + | "learn" -> Learn + | _ -> invalid_arg "sqlite index: invalid exploration mode" + +let phase_name = function + | Exploration.Diverge -> "diverge" + | Converge -> "converge" + | Synthesised -> "synthesised" + | Judged -> "judged" + | Executing -> "executing" + | Learning -> "learning" + | Understood -> "understood" + +let phase_of_name = function + | "diverge" -> Exploration.Diverge + | "converge" -> Converge + | "synthesised" -> Synthesised + | "judged" -> Judged + | "executing" -> Executing + | "learning" -> Learning + | "understood" -> Understood + | _ -> invalid_arg "sqlite index: invalid exploration phase" + let summary_json (row : summary) = let field key value = Json.member (Json.name key) value in Json.object' @@ -483,6 +559,29 @@ let insert_row db ~blocked row = Db.closed_at <-- closed_at row; (Db.blocked <-- if blocked then 1L else 0L); ]; + Option.iter + (fun mode -> + let phase, attempts, critiques = + match Row.exploration row with + | None -> (None, 0, 0) + | Some (Exploration.Deliberating d as state) -> + ( Some (Exploration.phase state), + List.length d.attempts, + List.length d.critiques ) + | Some (Exploration.Learning_path l as state) -> + ( Some (Exploration.phase state), + (if Option.is_some l.human_attempt then 1 else 0), + if Option.is_some l.ai_critique then 1 else 0 ) + in + insert db Db.explorations + [ + Db.exploration_id <-- Row.id row; + Db.exploration_mode <-- mode_name mode; + Db.exploration_phase <-- Option.map phase_name phase; + Db.attempt_count <-- Int64.of_int attempts; + Db.critique_count <-- Int64.of_int critiques; + ]) + (Row.work_mode row); insert_summary db row; List.iter (fun value -> @@ -856,6 +955,27 @@ let fields ~db ~tree = let stats_rows ~db ~tree = stats_with_ids ~db ~tree |> Result.map (List.map snd) +let explorations ~db ~tree ?mode ?phase () = + match Sqlite.Schema.check db Db.schema with + | Error mismatch -> + err_index "sqlite index: %a" Sqlite.Schema.pp_mismatch mismatch + | Ok () -> + if Db.source_tree db <> [ Git.Hash.to_hex tree ] then + Error "sqlite index: source tree changed" + else + let mode = Option.map mode_name mode in + let phase = Option.map phase_name phase in + Db.exploration_rows db ?mode ?phase () + |> List.map (fun (id, mode, phase, attempt_count, critique_count) -> + { + id; + mode = mode_of_name mode; + phase = Option.map phase_of_name phase; + attempt_count = Int64.to_int attempt_count; + critique_count = Int64.to_int critique_count; + }) + |> fun rows -> Ok rows + let dependency_rows ~db ~tree = match Sqlite.Schema.check db Db.schema with | Error mismatch -> diff --git a/lib/sqlite_index.mli b/lib/sqlite_index.mli index c7f1cd0..40220db 100644 --- a/lib/sqlite_index.mli +++ b/lib/sqlite_index.mli @@ -17,6 +17,24 @@ type summary = { val summary_json : summary -> string (** Canonical JSON list summary, encoded while rebuilding the local index. *) +type exploration_summary = { + id : string; + mode : Core.Row.work_mode; + phase : Exploration.phase option; + attempt_count : int; + critique_count : int; +} +(** Rebuilt from canonical Git rows; unpublished drafts do not appear. *) + +val explorations : + db:Sqlite.t -> + tree:Git.Hash.t -> + ?mode:Core.Row.work_mode -> + ?phase:Exploration.phase -> + unit -> + (exploration_summary list, string) result +(** Query a validated local projection of one canonical tree. *) + val stats_rows : db:Sqlite.t -> tree:Git.Hash.t -> (Stats.row list, string) result (** Read lifecycle and dependency observations from one validated, typed local