Add git-aware SX tools: changed, diff_branch, blame, doc_gen, playwright
- sx_changed: list .sx files changed since a ref with structural summaries - sx_diff_branch: structural diff of all .sx changes vs base ref - sx_blame: git blame for .sx files, optionally focused on a tree path - sx_doc_gen: generate component docs from defcomp/defisland signatures - sx_playwright: run Playwright browser tests with structured results Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -502,6 +502,171 @@ let rec handle_tool name args =
|
|||||||
Out_channel.with_open_text file (fun oc -> output_string oc source);
|
Out_channel.with_open_text file (fun oc -> output_string oc source);
|
||||||
text_result (Printf.sprintf "OK — reformatted %s (%d bytes, %d forms)" file (String.length source) (List.length exprs))
|
text_result (Printf.sprintf "OK — reformatted %s (%d bytes, %d forms)" file (String.length source) (List.length exprs))
|
||||||
|
|
||||||
|
| "sx_changed" ->
|
||||||
|
let base_ref = args |> member "ref" |> to_string_option |> Option.value ~default:"main" in
|
||||||
|
let project_dir = try Sys.getenv "SX_PROJECT_DIR" with Not_found ->
|
||||||
|
let spec_dir = try Sys.getenv "SX_SPEC_DIR" with Not_found -> "spec" in
|
||||||
|
Filename.dirname spec_dir
|
||||||
|
in
|
||||||
|
let cmd = Printf.sprintf "cd %s && git diff --name-only %s -- '*.sx' '*.sxc' 2>/dev/null" project_dir base_ref in
|
||||||
|
let ic = Unix.open_process_in cmd in
|
||||||
|
let files = ref [] in
|
||||||
|
(try while true do files := input_line ic :: !files done with End_of_file -> ());
|
||||||
|
ignore (Unix.close_process_in ic);
|
||||||
|
let changed = List.rev !files in
|
||||||
|
if changed = [] then text_result (Printf.sprintf "No .sx files changed since %s" base_ref)
|
||||||
|
else begin
|
||||||
|
let lines = List.map (fun rel ->
|
||||||
|
let full = Filename.concat project_dir rel in
|
||||||
|
try
|
||||||
|
let tree = parse_file full in
|
||||||
|
let summary = value_to_string (call_sx "summarise" [tree; Number 1.0]) in
|
||||||
|
Printf.sprintf "=== %s ===\n%s" rel summary
|
||||||
|
with _ -> Printf.sprintf "=== %s === (parse error or deleted)" rel
|
||||||
|
) changed in
|
||||||
|
text_result (String.concat "\n\n" lines)
|
||||||
|
end
|
||||||
|
|
||||||
|
| "sx_diff_branch" ->
|
||||||
|
let base_ref = args |> member "ref" |> to_string_option |> Option.value ~default:"main" in
|
||||||
|
let project_dir = try Sys.getenv "SX_PROJECT_DIR" with Not_found ->
|
||||||
|
let spec_dir = try Sys.getenv "SX_SPEC_DIR" with Not_found -> "spec" in
|
||||||
|
Filename.dirname spec_dir
|
||||||
|
in
|
||||||
|
let cmd = Printf.sprintf "cd %s && git diff --name-only %s -- '*.sx' '*.sxc' 2>/dev/null" project_dir base_ref in
|
||||||
|
let ic = Unix.open_process_in cmd in
|
||||||
|
let files = ref [] in
|
||||||
|
(try while true do files := input_line ic :: !files done with End_of_file -> ());
|
||||||
|
ignore (Unix.close_process_in ic);
|
||||||
|
let changed = List.rev !files in
|
||||||
|
if changed = [] then text_result (Printf.sprintf "No .sx files changed since %s" base_ref)
|
||||||
|
else begin
|
||||||
|
let lines = List.filter_map (fun rel ->
|
||||||
|
let full = Filename.concat project_dir rel in
|
||||||
|
(* Get the base version via git show *)
|
||||||
|
let base_cmd = Printf.sprintf "cd %s && git show %s:%s 2>/dev/null" project_dir base_ref rel in
|
||||||
|
let ic2 = Unix.open_process_in base_cmd in
|
||||||
|
let base_lines = ref [] in
|
||||||
|
(try while true do base_lines := input_line ic2 :: !base_lines done with End_of_file -> ());
|
||||||
|
ignore (Unix.close_process_in ic2);
|
||||||
|
let base_src = String.concat "\n" (List.rev !base_lines) in
|
||||||
|
try
|
||||||
|
let tree_b = parse_file full in
|
||||||
|
if base_src = "" then
|
||||||
|
Some (Printf.sprintf "=== %s (new file) ===\n%s" rel
|
||||||
|
(value_to_string (call_sx "summarise" [tree_b; Number 1.0])))
|
||||||
|
else begin
|
||||||
|
let tree_a = List (Sx_parser.parse_all base_src) in
|
||||||
|
let diff = value_to_string (call_sx "tree-diff" [tree_a; tree_b]) in
|
||||||
|
if diff = "No differences" then None
|
||||||
|
else Some (Printf.sprintf "=== %s ===\n%s" rel diff)
|
||||||
|
end
|
||||||
|
with _ -> Some (Printf.sprintf "=== %s === (parse error)" rel)
|
||||||
|
) changed in
|
||||||
|
if lines = [] then text_result "All changed .sx files are structurally identical to base"
|
||||||
|
else text_result (String.concat "\n\n" lines)
|
||||||
|
end
|
||||||
|
|
||||||
|
| "sx_blame" ->
|
||||||
|
let file = args |> member "file" |> to_string in
|
||||||
|
let path_str_arg = args |> member "path" |> to_string_option in
|
||||||
|
let project_dir = try Sys.getenv "SX_PROJECT_DIR" with Not_found ->
|
||||||
|
let spec_dir = try Sys.getenv "SX_SPEC_DIR" with Not_found -> "spec" in
|
||||||
|
Filename.dirname spec_dir
|
||||||
|
in
|
||||||
|
(* Get the node's source span by parsing and finding line numbers *)
|
||||||
|
let tree = parse_file file in
|
||||||
|
let target_src = match path_str_arg with
|
||||||
|
| Some ps ->
|
||||||
|
let path = resolve_path tree ps in
|
||||||
|
let node = call_sx "navigate" [tree; path] in
|
||||||
|
if is_nil node then None
|
||||||
|
else Some (Sx_types.inspect node)
|
||||||
|
| None -> None
|
||||||
|
in
|
||||||
|
let rel_file = relative_path ~base:project_dir file in
|
||||||
|
let cmd = match target_src with
|
||||||
|
| Some src ->
|
||||||
|
(* Find the line range containing this source fragment *)
|
||||||
|
let first_line = String.sub src 0 (min 40 (String.length src)) in
|
||||||
|
let escaped = String.concat "" (List.of_seq (Seq.map (fun c ->
|
||||||
|
if c = '(' || c = ')' || c = '[' || c = ']' || c = '.' || c = '*' || c = '+' || c = '?' || c = '{' || c = '}' || c = '\\' || c = '|' || c = '^' || c = '$'
|
||||||
|
then Printf.sprintf "\\%c" c else String.make 1 c
|
||||||
|
) (String.to_seq first_line))) in
|
||||||
|
Printf.sprintf "cd %s && git blame -L '/%s/,+10' -- %s 2>/dev/null || git blame -- %s 2>/dev/null | head -20" project_dir escaped rel_file rel_file
|
||||||
|
| None ->
|
||||||
|
Printf.sprintf "cd %s && git blame -- %s 2>/dev/null | head -30" project_dir rel_file
|
||||||
|
in
|
||||||
|
let ic = Unix.open_process_in cmd in
|
||||||
|
let lines = ref [] in
|
||||||
|
(try while true do lines := input_line ic :: !lines done with End_of_file -> ());
|
||||||
|
ignore (Unix.close_process_in ic);
|
||||||
|
text_result (String.concat "\n" (List.rev !lines))
|
||||||
|
|
||||||
|
| "sx_doc_gen" ->
|
||||||
|
let dir = args |> member "dir" |> to_string in
|
||||||
|
let files = glob_sx_files dir in
|
||||||
|
let all_docs = List.concat_map (fun path ->
|
||||||
|
let rel = relative_path ~base:dir path in
|
||||||
|
try
|
||||||
|
let exprs = Sx_parser.parse_all (In_channel.with_open_text path In_channel.input_all) in
|
||||||
|
List.filter_map (fun expr ->
|
||||||
|
match expr with
|
||||||
|
| List (Symbol head :: Symbol name :: rest) | ListRef { contents = Symbol head :: Symbol name :: rest } ->
|
||||||
|
(match head with
|
||||||
|
| "defcomp" | "defisland" ->
|
||||||
|
let params_str = match rest with
|
||||||
|
| List ps :: _ | ListRef { contents = ps } :: _ ->
|
||||||
|
let keys = List.filter_map (fun p -> match p with
|
||||||
|
| Symbol s when s <> "&key" && s <> "&rest" && not (String.length s > 0 && s.[0] = '&') -> Some s
|
||||||
|
| List (Symbol s :: _) when s <> "&key" && s <> "&rest" -> Some (Printf.sprintf "%s (typed)" s)
|
||||||
|
| _ -> None) ps
|
||||||
|
in
|
||||||
|
let has_rest = List.exists (fun p -> match p with Symbol "&rest" -> true | _ -> false) ps in
|
||||||
|
let key_str = if keys = [] then "" else " Keys: " ^ String.concat ", " keys ^ "\n" in
|
||||||
|
let rest_str = if has_rest then " Children: yes\n" else "" in
|
||||||
|
key_str ^ rest_str
|
||||||
|
| _ -> ""
|
||||||
|
in
|
||||||
|
Some (Printf.sprintf "## %s `%s`\nDefined in: %s\nType: %s\n%s" head name rel head params_str)
|
||||||
|
| "defmacro" ->
|
||||||
|
Some (Printf.sprintf "## %s `%s`\nDefined in: %s\nType: macro\n" head name rel)
|
||||||
|
| _ -> None)
|
||||||
|
| _ -> None
|
||||||
|
) exprs
|
||||||
|
with _ -> []
|
||||||
|
) files in
|
||||||
|
if all_docs = [] then text_result "(no components found)"
|
||||||
|
else text_result (String.concat "\n" all_docs)
|
||||||
|
|
||||||
|
| "sx_playwright" ->
|
||||||
|
let project_dir = try Sys.getenv "SX_PROJECT_DIR" with Not_found ->
|
||||||
|
let spec_dir = try Sys.getenv "SX_SPEC_DIR" with Not_found -> "spec" in
|
||||||
|
Filename.dirname spec_dir
|
||||||
|
in
|
||||||
|
let spec = args |> member "spec" |> to_string_option in
|
||||||
|
let spec_arg = match spec with Some s -> " " ^ s | None -> "" in
|
||||||
|
let cmd = Printf.sprintf "cd %s/tests/playwright && npx playwright test%s --reporter=line 2>&1" project_dir spec_arg in
|
||||||
|
let ic = Unix.open_process_in cmd in
|
||||||
|
let lines = ref [] in
|
||||||
|
(try while true do lines := input_line ic :: !lines done with End_of_file -> ());
|
||||||
|
ignore (Unix.close_process_in ic);
|
||||||
|
let all_lines = List.rev !lines in
|
||||||
|
let fails = List.filter (fun l -> let t = String.trim l in
|
||||||
|
String.length t > 1 && (t.[0] = '\xE2' (* ✘ *) || (String.length t > 4 && String.sub t 0 4 = "FAIL"))) all_lines in
|
||||||
|
let summary = List.find_opt (fun l ->
|
||||||
|
try let _ = Str.search_forward (Str.regexp "passed\\|failed") l 0 in true
|
||||||
|
with Not_found -> false) (List.rev all_lines) in
|
||||||
|
let result = match summary with
|
||||||
|
| Some s ->
|
||||||
|
if fails = [] then s
|
||||||
|
else s ^ "\n\nFailures:\n" ^ String.concat "\n" fails
|
||||||
|
| None ->
|
||||||
|
let last_n = List.filteri (fun i _ -> i >= List.length all_lines - 10) all_lines in
|
||||||
|
String.concat "\n" last_n
|
||||||
|
in
|
||||||
|
text_result result
|
||||||
|
|
||||||
| "sx_write_file" ->
|
| "sx_write_file" ->
|
||||||
let file = args |> member "file" |> to_string in
|
let file = args |> member "file" |> to_string in
|
||||||
let source = args |> member "source" |> to_string in
|
let source = args |> member "source" |> to_string in
|
||||||
@@ -789,6 +954,19 @@ let tool_definitions = `List [
|
|||||||
("new_name", `Assoc [("type", `String "string"); ("description", `String "New symbol name")]);
|
("new_name", `Assoc [("type", `String "string"); ("description", `String "New symbol name")]);
|
||||||
("dry_run", `Assoc [("type", `String "boolean"); ("description", `String "Preview changes without writing (default: false)")])]
|
("dry_run", `Assoc [("type", `String "boolean"); ("description", `String "Preview changes without writing (default: false)")])]
|
||||||
["dir"; "old_name"; "new_name"];
|
["dir"; "old_name"; "new_name"];
|
||||||
|
tool "sx_changed" "List .sx files changed since a git ref (default: main) with depth-1 summaries."
|
||||||
|
[("ref", `Assoc [("type", `String "string"); ("description", `String "Git ref to diff against (default: main)")])]
|
||||||
|
[];
|
||||||
|
tool "sx_diff_branch" "Structural diff of all .sx changes on current branch vs a base ref. Shows ADDED/REMOVED/CHANGED per file."
|
||||||
|
[("ref", `Assoc [("type", `String "string"); ("description", `String "Base ref (default: main)")])]
|
||||||
|
[];
|
||||||
|
tool "sx_blame" "Git blame for an .sx file, optionally focused on a tree path."
|
||||||
|
[file_prop; path_prop] ["file"];
|
||||||
|
tool "sx_doc_gen" "Generate component documentation from all defcomp/defisland/defmacro signatures in a directory."
|
||||||
|
[dir_prop] ["dir"];
|
||||||
|
tool "sx_playwright" "Run Playwright browser tests for the SX docs site. Optionally specify a single spec file."
|
||||||
|
[("spec", `Assoc [("type", `String "string"); ("description", `String "Optional spec file name (e.g. demo-interactions.spec.js)")])]
|
||||||
|
[];
|
||||||
]
|
]
|
||||||
|
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user