@@ -7,50 +7,47 @@ use crate::store::Store;
77pub fn list_tools ( ) -> Vec < ToolDefinition > {
88 vec ! [
99 ToolDefinition {
10- name: "get_model " . into( ) ,
11- description: "Returns both the desired and actual domain models , including bounded \
10+ name: "show_model " . into( ) ,
11+ description: "Returns the domain model for the specified state , including bounded \
1212 contexts, entities, services, events, rules, and conventions. \
13- Shows pending changes status. \
14- Use this before writing any new code to understand the system structure ."
13+ Use 'desired' to see the target model, 'actual' to see what is \
14+ implemented. Shows pending changes status when viewing desired ."
1515 . into( ) ,
1616 input_schema: json!( {
1717 "type" : "object" ,
18- "properties" : { } ,
19- "required" : [ ]
20- } ) ,
21- } ,
22- ToolDefinition {
23- name: "model_health" . into( ) ,
24- description: "Returns a structured health report for the domain model, computed \
25- via Datalog inference from the CozoDB knowledge graph. Includes: \
26- overall score (0-100), circular dependencies, layer violations, \
27- missing invariants on aggregate roots, god contexts (>10 entities+services), \
28- unsourced events, orphan contexts, and per-context complexity. \
29- Use this to programmatically branch on model quality."
30- . into( ) ,
31- input_schema: json!( {
32- "type" : "object" ,
33- "properties" : { } ,
18+ "properties" : {
19+ "action" : {
20+ "type" : "string" ,
21+ "enum" : [ "desired" , "actual" ] ,
22+ "description" : "Which model state to show (default: desired)"
23+ }
24+ } ,
3425 "required" : [ ]
3526 } ) ,
3627 } ,
3728 ToolDefinition {
38- name: "scrutinize " . into( ) ,
29+ name: "review_model " . into( ) ,
3930 description: "Run Datalog-based analysis queries over the domain model knowledge graph. \
40- Supports predefined analyses (transitive_deps, circular_deps, \
41- layer_violations, impact_analysis, aggregate_quality, dependency_graph, \
42- field_usage, method_search, shared_fields) \
43- and arbitrary Datalog queries. All relations have a `state` column \
44- ('desired' | 'actual') for set-differencing. Relations: \
45- context, context_dep, entity, service, service_dep, event, \
46- value_object, repository, invariant, field, method, method_param, vo_rule."
31+ Supports predefined analyses: \
32+ 'health' — structured health report (score 0-100, circular deps, \
33+ layer violations, missing invariants, god contexts, unsourced events, \
34+ orphan contexts, per-context complexity); \
35+ 'transitive_deps', 'circular_deps', 'layer_violations', \
36+ 'impact_analysis', 'aggregate_quality', 'dependency_graph', \
37+ 'field_usage', 'method_search', 'shared_fields' — predefined graph queries; \
38+ 'datalog' — arbitrary Datalog queries. \
39+ All relations have a `state` column ('desired' | 'actual') for \
40+ set-differencing. Relations: context, context_dep, entity, service, \
41+ service_dep, event, value_object, repository, invariant, field, method, \
42+ method_param, vo_rule."
4743 . into( ) ,
4844 input_schema: json!( {
4945 "type" : "object" ,
5046 "properties" : {
5147 "analysis" : {
5248 "type" : "string" ,
5349 "enum" : [
50+ "health" ,
5451 "transitive_deps" ,
5552 "circular_deps" ,
5653 "layer_violations" ,
@@ -101,54 +98,79 @@ pub fn call_tool(
10198 args : & Value ,
10299) -> ToolCallResult {
103100 match name {
104- "get_model" => {
101+ "show_model" => {
102+ let action = args. get ( "action" )
103+ . and_then ( |v| v. as_str ( ) )
104+ . unwrap_or ( "desired" ) ;
105105 let canonical = crate :: store:: cozo:: canonicalize_path ( workspace_path) ;
106106
107- // Build overview from Datalog relations — no in-memory DomainRegistry
108- let desired_overview = build_model_overview ( store, & canonical, "desired" ) ;
109- let actual_overview = build_model_overview ( store, & canonical, "actual" ) ;
110-
111- let has_actual = actual_overview. get ( "bounded_contexts" )
112- . and_then ( |v| v. as_array ( ) )
113- . is_some_and ( |a| !a. is_empty ( ) ) ;
114-
115- // Use pure Datalog diff for sync check
116- let ( status, pending_count) = if has_actual {
117- let changes = store. diff_graph ( workspace_path) . ok ( )
118- . and_then ( |v| v. get ( "pending_changes" ) . cloned ( ) )
119- . and_then ( |v| v. as_array ( ) . cloned ( ) )
120- . unwrap_or_default ( ) ;
121- if changes. is_empty ( ) {
122- ( "in_sync" , 0 )
123- } else {
124- ( "pending_changes" , changes. len ( ) )
125- }
126- } else {
127- ( "no_actual" , 0 )
128- } ;
129-
130- let overview = json ! ( {
131- "desired" : desired_overview,
132- "actual" : if has_actual { actual_overview } else { json!( null) } ,
133- "status" : status,
134- "pending_change_count" : pending_count,
135- } ) ;
136-
137- text_result ( serde_json:: to_string ( & overview) . unwrap ( ) )
138- }
107+ match action {
108+ "desired" => {
109+ let overview = build_model_overview ( store, & canonical, "desired" ) ;
110+
111+ let actual_overview = build_model_overview ( store, & canonical, "actual" ) ;
112+ let has_actual = actual_overview. get ( "bounded_contexts" )
113+ . and_then ( |v| v. as_array ( ) )
114+ . is_some_and ( |a| !a. is_empty ( ) ) ;
115+
116+ let ( status, pending_count) = if has_actual {
117+ let changes = store. diff_graph ( workspace_path) . ok ( )
118+ . and_then ( |v| v. get ( "pending_changes" ) . cloned ( ) )
119+ . and_then ( |v| v. as_array ( ) . cloned ( ) )
120+ . unwrap_or_default ( ) ;
121+ if changes. is_empty ( ) {
122+ ( "in_sync" , 0 )
123+ } else {
124+ ( "pending_changes" , changes. len ( ) )
125+ }
126+ } else {
127+ ( "no_actual" , 0 )
128+ } ;
139129
140- "model_health" => {
141- match store. model_health ( workspace_path) {
142- Ok ( health) => text_result ( serde_json:: to_string ( & health) . unwrap ( ) ) ,
143- Err ( e) => error_result ( format ! ( "model_health query failed: {e}" ) ) ,
130+ let result = json ! ( {
131+ "state" : "desired" ,
132+ "model" : overview,
133+ "status" : status,
134+ "pending_change_count" : pending_count,
135+ } ) ;
136+ text_result ( serde_json:: to_string ( & result) . unwrap ( ) )
137+ }
138+ "actual" => {
139+ let overview = build_model_overview ( store, & canonical, "actual" ) ;
140+ let has_actual = overview. get ( "bounded_contexts" )
141+ . and_then ( |v| v. as_array ( ) )
142+ . is_some_and ( |a| !a. is_empty ( ) ) ;
143+
144+ if !has_actual {
145+ let result = json ! ( {
146+ "state" : "actual" ,
147+ "model" : null,
148+ "message" : "No actual model exists. Run scan_model to extract it from source code."
149+ } ) ;
150+ text_result ( serde_json:: to_string ( & result) . unwrap ( ) )
151+ } else {
152+ let result = json ! ( {
153+ "state" : "actual" ,
154+ "model" : overview,
155+ } ) ;
156+ text_result ( serde_json:: to_string ( & result) . unwrap ( ) )
157+ }
158+ }
159+ _ => error_result ( format ! ( "Unknown action '{action}'. Use 'desired' or 'actual'." ) ) ,
144160 }
145161 }
146162
147- "scrutinize " => {
163+ "review_model " => {
148164 let analysis = args[ "analysis" ] . as_str ( ) . unwrap_or ( "" ) ;
149165 let canonical = crate :: store:: cozo:: canonicalize_path ( workspace_path) ;
150166
151167 match analysis {
168+ "health" => {
169+ match store. model_health ( workspace_path) {
170+ Ok ( health) => text_result ( serde_json:: to_string ( & health) . unwrap ( ) ) ,
171+ Err ( e) => error_result ( format ! ( "health analysis failed: {e}" ) ) ,
172+ }
173+ }
152174 "transitive_deps" => {
153175 let context = match args[ "context" ] . as_str ( ) {
154176 Some ( c) => c,
@@ -351,7 +373,7 @@ pub fn call_tool(
351373 Err ( e) => error_result ( format ! ( "Shared fields query failed: {e}" ) ) ,
352374 }
353375 }
354- _ => error_result ( format ! ( "Unknown analysis type: '{}'. Valid types: transitive_deps, circular_deps, layer_violations, impact_analysis, aggregate_quality, dependency_graph, field_usage, method_search, shared_fields, datalog" , analysis) ) ,
376+ _ => error_result ( format ! ( "Unknown analysis type: '{}'. Valid types: health, transitive_deps, circular_deps, layer_violations, impact_analysis, aggregate_quality, dependency_graph, field_usage, method_search, shared_fields, datalog" , analysis) ) ,
355377 }
356378 }
357379
@@ -683,7 +705,7 @@ mod tests {
683705 #[ test]
684706 fn test_list_tools_count ( ) {
685707 let tools = list_tools ( ) ;
686- assert_eq ! ( tools. len( ) , 3 ) ;
708+ assert_eq ! ( tools. len( ) , 2 ) ;
687709 }
688710
689711 #[ test]
@@ -694,29 +716,38 @@ mod tests {
694716 // Save desired + accept to create actual
695717 store. save_desired ( ws, & model) . unwrap ( ) ;
696718 store. accept ( ws) . unwrap ( ) ;
697- let result = call_tool ( & store, ws, "get_model " , & json ! ( { } ) ) ;
719+ let result = call_tool ( & store, ws, "show_model " , & json ! ( { } ) ) ;
698720 let text = match & result. content [ 0 ] {
699721 ContentBlock :: Text { text } => text,
700722 } ;
701723 let parsed: Value = serde_json:: from_str ( text) . unwrap ( ) ;
702- assert ! ( parsed. get ( " desired") . is_some ( ) ) ;
703- assert ! ( parsed. get( "actual " ) . is_some( ) ) ;
724+ assert_eq ! ( parsed[ "state" ] , " desired") ;
725+ assert ! ( parsed. get( "model " ) . is_some( ) ) ;
704726 assert_eq ! ( parsed[ "status" ] , "in_sync" ) ;
705727 assert_eq ! ( parsed[ "pending_change_count" ] , 0 ) ;
728+
729+ // Also verify actual action
730+ let result = call_tool ( & store, ws, "show_model" , & json ! ( { "action" : "actual" } ) ) ;
731+ let text = match & result. content [ 0 ] {
732+ ContentBlock :: Text { text } => text,
733+ } ;
734+ let parsed: Value = serde_json:: from_str ( text) . unwrap ( ) ;
735+ assert_eq ! ( parsed[ "state" ] , "actual" ) ;
736+ assert ! ( parsed. get( "model" ) . is_some( ) ) ;
706737 }
707738
708739 #[ test]
709740 fn test_overview_no_actual_shows_status ( ) {
710741 let store = test_store ( ) ;
711742 let ws = "/tmp/test-no-actual" ;
712743 store. save_desired ( ws, & test_model ( ) ) . unwrap ( ) ;
713- let result = call_tool ( & store, ws, "get_model " , & json ! ( { } ) ) ;
744+ let result = call_tool ( & store, ws, "show_model " , & json ! ( { "action" : "actual" } ) ) ;
714745 let text = match & result. content [ 0 ] {
715746 ContentBlock :: Text { text } => text,
716747 } ;
717748 let parsed: Value = serde_json:: from_str ( text) . unwrap ( ) ;
718- assert_eq ! ( parsed[ "status " ] , "no_actual " ) ;
719- assert_eq ! ( parsed[ "actual " ] , json!( null) ) ;
749+ assert_eq ! ( parsed[ "state " ] , "actual " ) ;
750+ assert_eq ! ( parsed[ "model " ] , json!( null) ) ;
720751 }
721752
722753 #[ test]
@@ -731,7 +762,7 @@ mod tests {
731762 }
732763 store. save_desired ( ws, & model) . unwrap ( ) ;
733764
734- let result = call_tool ( & store, ws, "scrutinize " , & json ! ( {
765+ let result = call_tool ( & store, ws, "review_model " , & json ! ( {
735766 "analysis" : "circular_deps"
736767 } ) ) ;
737768 let text = match & result. content [ 0 ] {
@@ -767,7 +798,7 @@ mod tests {
767798 } ) ;
768799 store. save_desired ( ws, & model) . unwrap ( ) ;
769800
770- let result = call_tool ( & store, ws, "scrutinize " , & json ! ( {
801+ let result = call_tool ( & store, ws, "review_model " , & json ! ( {
771802 "analysis" : "transitive_deps" ,
772803 "context" : "Notifications"
773804 } ) ) ;
@@ -789,7 +820,7 @@ mod tests {
789820 let model = test_model ( ) ;
790821 store. save_desired ( ws, & model) . unwrap ( ) ;
791822
792- let result = call_tool ( & store, ws, "scrutinize " , & json ! ( {
823+ let result = call_tool ( & store, ws, "review_model " , & json ! ( {
793824 "analysis" : "datalog" ,
794825 "query" : "?[name] := *entity{workspace: $ws, name}"
795826 } ) ) ;
@@ -808,7 +839,7 @@ mod tests {
808839 let model = test_model ( ) ;
809840 store. save_desired ( ws, & model) . unwrap ( ) ;
810841
811- let result = call_tool ( & store, ws, "scrutinize " , & json ! ( {
842+ let result = call_tool ( & store, ws, "review_model " , & json ! ( {
812843 "analysis" : "dependency_graph"
813844 } ) ) ;
814845 let text = match & result. content [ 0 ] {
@@ -823,7 +854,7 @@ mod tests {
823854 #[ test]
824855 fn test_query_model_missing_param ( ) {
825856 let store = test_store ( ) ;
826- let result = call_tool ( & store, "/tmp/x" , "scrutinize " , & json ! ( {
857+ let result = call_tool ( & store, "/tmp/x" , "review_model " , & json ! ( {
827858 "analysis" : "transitive_deps"
828859 } ) ) ;
829860 assert_eq ! ( result. is_error, Some ( true ) ) ;
0 commit comments