| 1 | //! The input contracts the model sees: the canonical union schema, and the |
| 2 | //! per-action schemas the hidden legacy aliases still advertise. |
| 3 | |
| 4 | use serde_json::{Value, json}; |
| 5 | |
| 6 | /// The union schema for the canonical `github` tool: `action` plus every |
| 7 | /// field any allowed action reads. `read_only` drops the write fields so the |
| 8 | /// Plan-mode surface cannot even describe a mutation. |
| 9 | pub(super) fn canonical_schema(allowed_actions: &[&str], read_only: bool) -> Value { |
| 10 | let actions: Vec<&str> = allowed_actions.to_vec(); |
| 11 | let mut properties = serde_json::Map::new(); |
| 12 | properties.insert( |
| 13 | "action".to_string(), |
| 14 | json!({ |
| 15 | "type": "string", |
| 16 | "enum": actions, |
| 17 | "description": "Action to perform." |
| 18 | }), |
| 19 | ); |
| 20 | properties.insert( |
| 21 | "number".to_string(), |
| 22 | json!({ "type": "integer", "minimum": 1, "description": "Issue/PR number (all actions)." }), |
| 23 | ); |
| 24 | properties.insert( |
| 25 | "include_comments".to_string(), |
| 26 | json!({ "type": "boolean", "default": true, "description": "(action=issue_context)" }), |
| 27 | ); |
| 28 | properties.insert( |
| 29 | "include_diff".to_string(), |
| 30 | json!({ "type": "boolean", "default": false, "description": "(action=pr_context)" }), |
| 31 | ); |
| 32 | if !read_only { |
| 33 | properties.insert( |
| 34 | "target".to_string(), |
| 35 | json!({ "type": "string", "enum": ["issue", "pr"], "description": "(action=comment)" }), |
| 36 | ); |
| 37 | properties.insert( |
| 38 | "body".to_string(), |
| 39 | json!({ "type": "string", "description": "Comment body (action=comment)." }), |
| 40 | ); |
| 41 | properties.insert( |
| 42 | "evidence".to_string(), |
| 43 | json!({ |
| 44 | "type": "object", |
| 45 | "description": "Evidence object (action=comment/close_*). Close actions require files_changed, tests_run, final_status.", |
| 46 | "properties": { |
| 47 | "files_changed": { "type": "array", "items": { "type": "string" } }, |
| 48 | "tests_run": { "type": "array", "items": { "type": "string" } }, |
| 49 | "commits": { "type": "array", "items": { "type": "string" } }, |
| 50 | "final_status": { "type": "string" } |
| 51 | } |
| 52 | }), |
| 53 | ); |
| 54 | properties.insert( |
| 55 | "acceptance_criteria".to_string(), |
| 56 | json!({ "type": "array", "items": { "type": "string" }, "minItems": 1, "description": "(action=close_issue/close_pr)" }), |
| 57 | ); |
| 58 | properties.insert( |
| 59 | "comment".to_string(), |
| 60 | json!({ "type": "string", "description": "Optional closing comment (action=close_issue/close_pr)." }), |
| 61 | ); |
| 62 | properties.insert( |
| 63 | "allow_dirty".to_string(), |
| 64 | json!({ "type": "boolean", "default": false, "description": "(action=close_issue/close_pr)" }), |
| 65 | ); |
| 66 | properties.insert( |
| 67 | "dry_run".to_string(), |
| 68 | json!({ "type": "boolean", "default": false, "description": "(action=comment/close_issue/close_pr)" }), |
| 69 | ); |
| 70 | } |
| 71 | json!({ |
| 72 | "type": "object", |
| 73 | "properties": properties, |
| 74 | "additionalProperties": false |
| 75 | }) |
| 76 | } |
| 77 | |
| 78 | /// The exact schema the legacy per-action tool exposed, kept so hidden alias |
| 79 | /// registrations report an identical contract to the pre-unification tools. |
| 80 | pub(super) fn legacy_action_schema(action: &str) -> Value { |
| 81 | match action { |
| 82 | "issue_context" => json!({ |
| 83 | "type": "object", |
| 84 | "properties": { |
| 85 | "number": { "type": "integer", "minimum": 1 }, |
| 86 | "include_comments": { "type": "boolean", "default": true } |
| 87 | }, |
| 88 | "required": ["number"], |
| 89 | "additionalProperties": false |
| 90 | }), |
| 91 | "pr_context" => json!({ |
| 92 | "type": "object", |
| 93 | "properties": { |
| 94 | "number": { "type": "integer", "minimum": 1 }, |
| 95 | "include_diff": { "type": "boolean", "default": false } |
| 96 | }, |
| 97 | "required": ["number"], |
| 98 | "additionalProperties": false |
| 99 | }), |
| 100 | "comment" => json!({ |
| 101 | "type": "object", |
| 102 | "properties": { |
| 103 | "target": { "type": "string", "enum": ["issue", "pr"] }, |
| 104 | "number": { "type": "integer", "minimum": 1 }, |
| 105 | "body": { "type": "string" }, |
| 106 | "evidence": { "type": "object" }, |
| 107 | "dry_run": { "type": "boolean", "default": false } |
| 108 | }, |
| 109 | "required": ["target", "number", "body", "evidence"], |
| 110 | "additionalProperties": false |
| 111 | }), |
| 112 | // close_issue / close_pr share the close schema. |
| 113 | _ => close_input_schema(), |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | fn close_input_schema() -> Value { |
| 118 | json!({ |
| 119 | "type": "object", |
| 120 | "properties": { |
| 121 | "number": { "type": "integer", "minimum": 1 }, |
| 122 | "acceptance_criteria": { "type": "array", "items": { "type": "string" }, "minItems": 1 }, |
| 123 | "evidence": { |
| 124 | "type": "object", |
| 125 | "properties": { |
| 126 | "files_changed": { "type": "array", "items": { "type": "string" } }, |
| 127 | "tests_run": { "type": "array", "items": { "type": "string" } }, |
| 128 | "commits": { "type": "array", "items": { "type": "string" } }, |
| 129 | "final_status": { "type": "string" } |
| 130 | }, |
| 131 | "required": ["files_changed", "tests_run", "final_status"] |
| 132 | }, |
| 133 | "comment": { "type": "string" }, |
| 134 | "allow_dirty": { "type": "boolean", "default": false }, |
| 135 | "dry_run": { "type": "boolean", "default": false } |
| 136 | }, |
| 137 | "required": ["number", "acceptance_criteria", "evidence"], |
| 138 | "additionalProperties": false |
| 139 | }) |
| 140 | } |
| 141 |