forked from 1b-agent/opencadstudio-upstream
feat(text): add text_audit spell-check and dry-run replacement simulation
- Adds ext_audit operation under ocs_read, control bridge, and headless server - Implements pairs / dry_run_pairs simulation returning diffs, coordinates, and unmatched queries - Implements check_terms suspect matching and dictionary vocabulary verification - Adds dry_run: true to ext_replace to preview modifications without mutating entities or pushing undo - Supports bounds, layers, handles, and scope filtering - Updates MCP tool definitions, schemas, and automation test suites
This commit is contained in:
1 parent
88169c1d7a
commit
97ea84d99a
7 files changed
+510
-33
No files matched your search
@@ -52,16 +52,18 @@ def legacy(server: Path) -> None:
|
|||||||
names = set(definitions)
|
names = set(definitions)
|
||||||
assert names == TOOLS, names
|
assert names == TOOLS, names
|
||||||
execute_request = definitions["ocs_execute"]["inputSchema"]["properties"]["request"]
|
execute_request = definitions["ocs_execute"]["inputSchema"]["properties"]["request"]
|
||||||
assert len(execute_request["oneOf"]) == 38
|
assert len(execute_request["oneOf"]) == 39
|
||||||
op_enum = execute_request["properties"]["op"]["enum"]
|
op_enum = execute_request["properties"]["op"]["enum"]
|
||||||
for shipped in ("entities_create", "entities_delete", "entities_transform",
|
for shipped in ("entities_create", "entities_delete", "entities_transform",
|
||||||
"block_define", "block_delete", "file_identity", "xdata_set",
|
"block_define", "block_delete", "file_identity", "xdata_set",
|
||||||
"view_focus", "wblock", "plot",
|
"view_focus", "wblock", "plot",
|
||||||
"entities_copy_to", "group_create", "selection_set_save",
|
"entities_copy_to", "group_create", "selection_set_save",
|
||||||
"selection_set_load", "close", "sysvar", "layout_create",
|
"selection_set_load", "close", "sysvar", "layout_create",
|
||||||
"page_setup_set"):
|
"page_setup_set", "text_replace"):
|
||||||
assert shipped in op_enum, shipped
|
assert shipped in op_enum, shipped
|
||||||
assert "xdata_get" in definitions["ocs_read"]["inputSchema"]["properties"]["op"]["enum"]
|
assert "xdata_get" in definitions["ocs_read"]["inputSchema"]["properties"]["op"]["enum"]
|
||||||
|
assert "text_search" in definitions["ocs_read"]["inputSchema"]["properties"]["op"]["enum"]
|
||||||
|
assert "text_audit" in definitions["ocs_read"]["inputSchema"]["properties"]["op"]["enum"]
|
||||||
assert execute_request["properties"]["steps"]["maxItems"] == 64
|
assert execute_request["properties"]["steps"]["maxItems"] == 64
|
||||||
assert execute_request["properties"]["cmd"]["examples"][0] == "LINE 0,0 10,10"
|
assert execute_request["properties"]["cmd"]["examples"][0] == "LINE 0,0 10,10"
|
||||||
assert "set_properties" in execute_request["properties"]["op"]["enum"]
|
assert "set_properties" in execute_request["properties"]["op"]["enum"]
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Two request styles share the channel, told apart by the `protocol` field:
|
|||||||
|
|
||||||
| Style | Shape | Operations |
|
| Style | Shape | Operations |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| protocol 1 | `{"protocol":1,"request_id":"…","document_id":N,"op":"…"}` — the same requests and replies as `ocs_execute` / `ocs_read`, described in the README | `new`, `open`, `save`, `save_verified`, `activate`, `select`, `run`, `start`, `input`, `cancel`, `stop`, `action`, `property`, `set_properties`, `embed_image`, `undo`, `redo`; reads `state`, `capabilities`, `audit`, `query`, `entities`, `records`, `record_schema`, `layers`, `header`, `properties`, `measure`, `history`, `commands`, `events`, `operation` |
|
| protocol 1 | `{"protocol":1,"request_id":"…","document_id":N,"op":"…"}` — the same requests and replies as `ocs_execute` / `ocs_read`, described in the README | `new`, `open`, `save`, `save_verified`, `activate`, `select`, `run`, `start`, `input`, `cancel`, `stop`, `action`, `property`, `set_properties`, `embed_image`, `undo`, `redo`, `text_replace`; reads `state`, `capabilities`, `audit`, `text_audit`, `text_search`, `query`, `entities`, `records`, `record_schema`, `layers`, `header`, `properties`, `measure`, `history`, `commands`, `events`, `operation` |
|
||||||
| legacy | `{"op":"…"}` without `protocol` | `new`, `open`, `run`, `entities`, `audit`, `query`, `records`, `record_schema`, `capabilities`, `layers`, `header`, `select`, `save`, `undo`, `redo` |
|
| legacy | `{"op":"…"}` without `protocol` | `new`, `open`, `run`, `entities`, `audit`, `query`, `records`, `record_schema`, `capabilities`, `layers`, `header`, `select`, `save`, `undo`, `redo` |
|
||||||
|
|
||||||
The two styles differ in how a waiting command is treated. A protocol-1 `run`
|
The two styles differ in how a waiting command is treated. A protocol-1 `run`
|
||||||
|
|||||||
@@ -680,6 +680,7 @@ impl OpenCADStudio {
|
|||||||
"audit" => self.document_audit(&req),
|
"audit" => self.document_audit(&req),
|
||||||
"query" => self.entity_query(&req),
|
"query" => self.entity_query(&req),
|
||||||
"text_search" => self.automation_text_search(&req),
|
"text_search" => self.automation_text_search(&req),
|
||||||
|
"text_audit" => self.automation_text_audit(&req),
|
||||||
"text_replace" => self.automation_text_replace(&req),
|
"text_replace" => self.automation_text_replace(&req),
|
||||||
"records" => self.record_query(&req),
|
"records" => self.record_query(&req),
|
||||||
"record_schema" => self.record_schema(&req),
|
"record_schema" => self.record_schema(&req),
|
||||||
|
|||||||
@@ -472,6 +472,7 @@ impl OpenCADStudio {
|
|||||||
| "get_selection"
|
| "get_selection"
|
||||||
| "audit"
|
| "audit"
|
||||||
| "text_search"
|
| "text_search"
|
||||||
|
| "text_audit"
|
||||||
);
|
);
|
||||||
if !query && !self.control.enabled {
|
if !query && !self.control.enabled {
|
||||||
return (
|
return (
|
||||||
@@ -731,6 +732,7 @@ impl OpenCADStudio {
|
|||||||
"xdata_get" => self.xdata_read(&req),
|
"xdata_get" => self.xdata_read(&req),
|
||||||
"get_selection" => self.control_get_selection(),
|
"get_selection" => self.control_get_selection(),
|
||||||
"text_search" => self.control_text_search(&req).unwrap_or_else(|e| e),
|
"text_search" => self.control_text_search(&req).unwrap_or_else(|e| e),
|
||||||
|
"text_audit" => self.control_text_audit(&req).unwrap_or_else(|e| e),
|
||||||
"history" => {
|
"history" => {
|
||||||
json!({"ok":true,"entries":self.command_line.history.iter().map(|e|json!({"kind":format!("{:?}",e.kind),"text":e.text})).collect::<Vec<_>>()})
|
json!({"ok":true,"entries":self.command_line.history.iter().map(|e|json!({"kind":format!("{:?}",e.kind),"text":e.text})).collect::<Vec<_>>()})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -655,3 +655,112 @@ fn text_replace_op_batch_replaces_preserves_formatting_and_undoes() {
|
|||||||
assert_eq!(q_orig2["count"], 1);
|
assert_eq!(q_orig2["count"], 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_audit_op_simulates_dry_run_checks_terms_and_dictionary() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let r = mutate(
|
||||||
|
&mut app,
|
||||||
|
r#"{"protocol":1,"op":"entities_create","request_id":"ta_c1","document_id":{doc},"entities":[
|
||||||
|
{"type":"Text","position":[0,0,0],"value":"CIRTUITS 28-09-2026","layer":"0"},
|
||||||
|
{"type":"MText","position":[10,0,0],"value":"{\\fCentury Gothic;CUVE EAU NON TRAITE}","layer":"0"},
|
||||||
|
{"type":"Text","position":[20,0,0],"value":"POMPE A EAU","layer":"0"}
|
||||||
|
]}"#,
|
||||||
|
);
|
||||||
|
assert_eq!(r["ok"], true, "{}", r["error"]);
|
||||||
|
|
||||||
|
// Test text_audit with pairs, check_terms, and dictionary
|
||||||
|
let audit_res = app.automation_op(
|
||||||
|
r#"{
|
||||||
|
"op":"text_audit",
|
||||||
|
"pairs":[
|
||||||
|
{"find":"CIRTUITS","replace":"CIRCUITS"},
|
||||||
|
{"find":"NON TRAITE","replace":"NON TRAITÉE"},
|
||||||
|
{"find":"INEXISTANT_PAIR","replace":"REPLACEMENT"}
|
||||||
|
],
|
||||||
|
"check_terms":["CIRTUITS", "NON TRAITE", "INEXISTANT_TERM"],
|
||||||
|
"dictionary":["EAU", "POMPE", "28-09-2026"]
|
||||||
|
}"#,
|
||||||
|
);
|
||||||
|
assert_eq!(audit_res["ok"], true, "{}", audit_res["error"]);
|
||||||
|
|
||||||
|
// Check summary
|
||||||
|
assert_eq!(audit_res["summary"]["pairs_total"], 3);
|
||||||
|
assert_eq!(audit_res["summary"]["pairs_unmatched"], 1);
|
||||||
|
assert_eq!(audit_res["summary"]["check_terms_total"], 3);
|
||||||
|
assert_eq!(audit_res["summary"]["check_terms_found"], 2);
|
||||||
|
|
||||||
|
// Unmatched reporting
|
||||||
|
assert_eq!(audit_res["unmatched_pairs"], json!(["INEXISTANT_PAIR"]));
|
||||||
|
assert_eq!(audit_res["unmatched_check_terms"], json!(["INEXISTANT_TERM"]));
|
||||||
|
|
||||||
|
// Simulated changes has 2 entries (Text and MText)
|
||||||
|
let changes = audit_res["simulated_changes"].as_array().unwrap();
|
||||||
|
assert_eq!(changes.len(), 2);
|
||||||
|
assert_eq!(changes[0]["find"], "CIRTUITS");
|
||||||
|
assert_eq!(changes[0]["replace"], "CIRCUITS");
|
||||||
|
assert_eq!(changes[0]["replaced"], 1);
|
||||||
|
assert!(changes[1]["after"].as_str().unwrap().contains("NON TRAITÉE"));
|
||||||
|
|
||||||
|
// Suspect matches
|
||||||
|
let suspects = audit_res["suspect_matches"].as_array().unwrap();
|
||||||
|
assert_eq!(suspects.len(), 2);
|
||||||
|
|
||||||
|
// Verify document was NOT mutated (pure read op)
|
||||||
|
let q = app.automation_op(r#"{"op":"text_search","find":"CIRTUITS"}"#);
|
||||||
|
assert_eq!(q["ok"], true);
|
||||||
|
assert_eq!(q["count"], 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_replace_op_supports_dry_run_without_mutating_or_undo() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let r = mutate(
|
||||||
|
&mut app,
|
||||||
|
r#"{"protocol":1,"op":"entities_create","request_id":"tr_dry1","document_id":{doc},"entities":[
|
||||||
|
{"type":"Text","position":[0,0,0],"value":"TEST BEFORE REPLACE","layer":"0"}
|
||||||
|
]}"#,
|
||||||
|
);
|
||||||
|
assert_eq!(r["ok"], true, "{}", r["error"]);
|
||||||
|
|
||||||
|
// Run text_replace with dry_run = true
|
||||||
|
let dry_res = mutate(
|
||||||
|
&mut app,
|
||||||
|
r#"{"protocol":1,"op":"text_replace","request_id":"tr_sim1","document_id":{doc},"find":"BEFORE","replace":"AFTER","dry_run":true}"#,
|
||||||
|
);
|
||||||
|
assert_eq!(dry_res["ok"], true, "{}", dry_res["error"]);
|
||||||
|
assert_eq!(dry_res["result"]["dry_run"], true);
|
||||||
|
assert_eq!(dry_res["result"]["replaced"], 1);
|
||||||
|
assert_eq!(dry_res["result"]["entities_changed"], 1);
|
||||||
|
|
||||||
|
// Verify the document was NOT modified
|
||||||
|
let q_before = app.automation_op(r#"{"op":"text_search","find":"TEST BEFORE REPLACE"}"#);
|
||||||
|
assert_eq!(q_before["ok"], true);
|
||||||
|
assert_eq!(q_before["count"], 1);
|
||||||
|
|
||||||
|
let q_after = app.automation_op(r#"{"op":"text_search","find":"TEST AFTER REPLACE"}"#);
|
||||||
|
assert_eq!(q_after["ok"], true);
|
||||||
|
assert_eq!(q_after["count"], 0);
|
||||||
|
|
||||||
|
// Now run with dry_run = false
|
||||||
|
let real_res = mutate(
|
||||||
|
&mut app,
|
||||||
|
r#"{"protocol":1,"op":"text_replace","request_id":"tr_real1","document_id":{doc},"find":"BEFORE","replace":"AFTER","dry_run":false}"#,
|
||||||
|
);
|
||||||
|
assert_eq!(real_res["ok"], true, "{}", real_res["error"]);
|
||||||
|
assert_eq!(real_res["result"]["replaced"], 1);
|
||||||
|
|
||||||
|
// Now it was modified
|
||||||
|
let q_after2 = app.automation_op(r#"{"op":"text_search","find":"TEST AFTER REPLACE"}"#);
|
||||||
|
assert_eq!(q_after2["ok"], true);
|
||||||
|
assert_eq!(q_after2["count"], 1);
|
||||||
|
|
||||||
|
// Undo restores it
|
||||||
|
app.automation_op(r#"{"op":"undo"}"#);
|
||||||
|
let q_undone = app.automation_op(r#"{"op":"text_search","find":"TEST BEFORE REPLACE"}"#);
|
||||||
|
assert_eq!(q_undone["ok"], true);
|
||||||
|
assert_eq!(q_undone["count"], 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+389
-29
@@ -758,7 +758,14 @@ impl OpenCADStudio {
|
|||||||
let limit = req["limit"].as_u64().unwrap_or(500).min(5000) as usize;
|
let limit = req["limit"].as_u64().unwrap_or(500).min(5000) as usize;
|
||||||
|
|
||||||
let matcher = TextMatcher::new(find.to_string(), match_case, whole_word, ignore_accents);
|
let matcher = TextMatcher::new(find.to_string(), match_case, whole_word, ignore_accents);
|
||||||
let i = self.active_tab;
|
let i = if let Some(doc_id) = req.get("document_id").and_then(|v| v.as_u64()) {
|
||||||
|
self.tabs
|
||||||
|
.iter()
|
||||||
|
.position(|t| t.id == doc_id)
|
||||||
|
.ok_or_else(|| failure("document_not_found", "Specified document_id not found"))?
|
||||||
|
} else {
|
||||||
|
self.active_tab
|
||||||
|
};
|
||||||
let scene = &self.tabs[i].scene;
|
let scene = &self.tabs[i].scene;
|
||||||
let doc = &scene.document;
|
let doc = &scene.document;
|
||||||
|
|
||||||
@@ -870,13 +877,342 @@ impl OpenCADStudio {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `text_audit` — comprehensive text quality check, spell-check, and replacement dry-run.
|
||||||
|
pub(crate) fn control_text_audit(&self, req: &Value) -> Result<Value, Value> {
|
||||||
|
let i = if let Some(doc_id) = req.get("document_id").and_then(|v| v.as_u64()) {
|
||||||
|
self.tabs
|
||||||
|
.iter()
|
||||||
|
.position(|t| t.id == doc_id)
|
||||||
|
.ok_or_else(|| failure("document_not_found", "Specified document_id not found"))?
|
||||||
|
} else {
|
||||||
|
self.active_tab
|
||||||
|
};
|
||||||
|
|
||||||
|
let match_case = req["match_case"].as_bool().unwrap_or(false);
|
||||||
|
let whole_word = req["whole_word"].as_bool().unwrap_or(false);
|
||||||
|
let ignore_accents = req["ignore_accents"].as_bool().unwrap_or(!match_case);
|
||||||
|
let scope = req["scope"].as_str().unwrap_or("all");
|
||||||
|
let layer_filter = req["layer"].as_str();
|
||||||
|
let handles_filter = parse_handles_filter(req);
|
||||||
|
let types_filter = parse_types_filter(req);
|
||||||
|
let bounds_filter = parse_bounds_filter(req)?;
|
||||||
|
let limit = req["limit"].as_u64().unwrap_or(1000).min(10000) as usize;
|
||||||
|
|
||||||
|
// Parse dry-run replacement pairs if supplied
|
||||||
|
let mut pairs: Vec<(String, String)> = Vec::new();
|
||||||
|
let pairs_val = req.get("pairs").or_else(|| req.get("dry_run_pairs"));
|
||||||
|
if let Some(pair_array) = pairs_val.and_then(|v| v.as_array()) {
|
||||||
|
for item in pair_array {
|
||||||
|
let find = item["find"].as_str().unwrap_or("");
|
||||||
|
let replace = item["replace"].as_str().unwrap_or("");
|
||||||
|
if !find.is_empty() {
|
||||||
|
pairs.push((find.to_string(), replace.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse check terms if supplied
|
||||||
|
let mut check_terms: Vec<String> = Vec::new();
|
||||||
|
if let Some(terms_array) = req.get("check_terms").and_then(|v| v.as_array()) {
|
||||||
|
for item in terms_array {
|
||||||
|
if let Some(t) = item.as_str() {
|
||||||
|
if !t.is_empty() {
|
||||||
|
check_terms.push(t.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse dictionary if supplied
|
||||||
|
let mut dictionary: Vec<String> = Vec::new();
|
||||||
|
if let Some(dict_array) = req.get("dictionary").and_then(|v| v.as_array()) {
|
||||||
|
for item in dict_array {
|
||||||
|
if let Some(w) = item.as_str() {
|
||||||
|
if !w.is_empty() {
|
||||||
|
dictionary.push(w.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pair_matchers: Vec<(TextMatcher, String)> = pairs
|
||||||
|
.iter()
|
||||||
|
.map(|(f, r)| {
|
||||||
|
(
|
||||||
|
TextMatcher::new(f.clone(), match_case, whole_word, ignore_accents),
|
||||||
|
r.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let check_matchers: Vec<TextMatcher> = check_terms
|
||||||
|
.iter()
|
||||||
|
.map(|t| TextMatcher::new(t.clone(), match_case, whole_word, ignore_accents))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let dict_matchers: Vec<TextMatcher> = dictionary
|
||||||
|
.iter()
|
||||||
|
.map(|w| TextMatcher::new(w.clone(), false, true, true))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut pair_hit_counts = vec![0usize; pair_matchers.len()];
|
||||||
|
let mut check_hit_counts = vec![0usize; check_matchers.len()];
|
||||||
|
|
||||||
|
let scene = &self.tabs[i].scene;
|
||||||
|
let doc = &scene.document;
|
||||||
|
|
||||||
|
let mut entities_scanned = 0usize;
|
||||||
|
let mut entities_matched = 0usize;
|
||||||
|
let mut total_occurrences = 0usize;
|
||||||
|
let mut simulated_changes = Vec::new();
|
||||||
|
let mut suspect_matches = Vec::new();
|
||||||
|
let mut unrecognized_words = Vec::new();
|
||||||
|
|
||||||
|
for entity in doc.entities() {
|
||||||
|
let common = entity.common();
|
||||||
|
let handle = common.handle;
|
||||||
|
|
||||||
|
let in_active = scene.entity_belongs_to_active_space(handle);
|
||||||
|
match scope {
|
||||||
|
"active_space" | "model_space" if !in_active => continue,
|
||||||
|
"blocks" if in_active => continue,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(l) = layer_filter {
|
||||||
|
if !common.layer.eq_ignore_ascii_case(l) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref h_list) = handles_filter {
|
||||||
|
if !h_list.contains(&handle) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref t_list) = types_filter {
|
||||||
|
if !t_list
|
||||||
|
.iter()
|
||||||
|
.any(|t| crate::app::automation::entity_type_matches(entity, t))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(b) = bounds_filter {
|
||||||
|
let (min, max) = crate::scene::convert::tess::entity_bounds_in(doc, entity);
|
||||||
|
if max[0] < b[0] || max[1] < b[1] || min[0] > b[2] || min[1] > b[3] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((raw_val, plain_text, pos)) = entity_text_info(entity, doc) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
entities_scanned += 1;
|
||||||
|
let mut entity_had_match = false;
|
||||||
|
|
||||||
|
// 1. Dry run pairs simulation
|
||||||
|
if !pair_matchers.is_empty() {
|
||||||
|
match entity {
|
||||||
|
EntityType::MText(_) => {
|
||||||
|
let mut cur_raw = raw_val.clone();
|
||||||
|
for (idx, (matcher, replacement)) in pair_matchers.iter().enumerate() {
|
||||||
|
let (new_raw, count) =
|
||||||
|
replace_mtext_safe(&cur_raw, matcher, replacement, true);
|
||||||
|
if count > 0 {
|
||||||
|
pair_hit_counts[idx] += count;
|
||||||
|
total_occurrences += count;
|
||||||
|
entity_had_match = true;
|
||||||
|
if simulated_changes.len() < limit {
|
||||||
|
simulated_changes.push(json!({
|
||||||
|
"handle": format!("{:X}", handle.value()),
|
||||||
|
"type": "MText",
|
||||||
|
"layer": common.layer,
|
||||||
|
"position": pos,
|
||||||
|
"find": pairs[idx].0,
|
||||||
|
"replace": pairs[idx].1,
|
||||||
|
"before": cur_raw,
|
||||||
|
"after": new_raw.clone(),
|
||||||
|
"replaced": count,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
cur_raw = new_raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let mut cur_text = plain_text.clone();
|
||||||
|
for (idx, (matcher, replacement)) in pair_matchers.iter().enumerate() {
|
||||||
|
let (new_text, count) =
|
||||||
|
matcher.replace_all_in(&cur_text, replacement, true);
|
||||||
|
if count > 0 {
|
||||||
|
pair_hit_counts[idx] += count;
|
||||||
|
total_occurrences += count;
|
||||||
|
entity_had_match = true;
|
||||||
|
if simulated_changes.len() < limit {
|
||||||
|
simulated_changes.push(json!({
|
||||||
|
"handle": format!("{:X}", handle.value()),
|
||||||
|
"type": crate::entities::names::ui_name(entity),
|
||||||
|
"layer": common.layer,
|
||||||
|
"position": pos,
|
||||||
|
"find": pairs[idx].0,
|
||||||
|
"replace": pairs[idx].1,
|
||||||
|
"before": cur_text,
|
||||||
|
"after": new_text.clone(),
|
||||||
|
"replaced": count,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
cur_text = new_text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check terms
|
||||||
|
for (idx, matcher) in check_matchers.iter().enumerate() {
|
||||||
|
if matcher.matches_text_or_raw(&plain_text, &raw_val) {
|
||||||
|
check_hit_counts[idx] += 1;
|
||||||
|
entity_had_match = true;
|
||||||
|
if suspect_matches.len() < limit {
|
||||||
|
suspect_matches.push(json!({
|
||||||
|
"handle": format!("{:X}", handle.value()),
|
||||||
|
"type": crate::entities::names::ui_name(entity),
|
||||||
|
"layer": common.layer,
|
||||||
|
"position": pos,
|
||||||
|
"term": check_terms[idx],
|
||||||
|
"plain_text": plain_text,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Dictionary check
|
||||||
|
if !dict_matchers.is_empty() {
|
||||||
|
for word in plain_text.split(|c: char| !c.is_alphabetic()) {
|
||||||
|
let w = word.trim();
|
||||||
|
if w.len() >= 3 && !dict_matchers.iter().any(|dm| dm.matches(w)) {
|
||||||
|
if unrecognized_words.len() < limit
|
||||||
|
&& !unrecognized_words
|
||||||
|
.iter()
|
||||||
|
.any(|item: &Value| item["word"].as_str() == Some(w))
|
||||||
|
{
|
||||||
|
unrecognized_words.push(json!({
|
||||||
|
"word": w,
|
||||||
|
"handle": format!("{:X}", handle.value()),
|
||||||
|
"position": pos,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Insert attributes
|
||||||
|
if let EntityType::Insert(insert) = entity {
|
||||||
|
for attr in &insert.attributes {
|
||||||
|
let raw = attr.get_value();
|
||||||
|
let plain = decode_dxf_escapes(raw);
|
||||||
|
|
||||||
|
for (idx, (matcher, replacement)) in pair_matchers.iter().enumerate() {
|
||||||
|
let (new_text, count) =
|
||||||
|
matcher.replace_all_in(&plain, replacement, true);
|
||||||
|
if count > 0 {
|
||||||
|
pair_hit_counts[idx] += count;
|
||||||
|
total_occurrences += count;
|
||||||
|
entity_had_match = true;
|
||||||
|
if simulated_changes.len() < limit {
|
||||||
|
simulated_changes.push(json!({
|
||||||
|
"handle": format!("{:X}", handle.value()),
|
||||||
|
"type": "Insert",
|
||||||
|
"attribute_tag": attr.tag,
|
||||||
|
"layer": common.layer,
|
||||||
|
"position": [insert.insert_point.x, insert.insert_point.y, insert.insert_point.z],
|
||||||
|
"find": pairs[idx].0,
|
||||||
|
"replace": pairs[idx].1,
|
||||||
|
"before": plain.clone(),
|
||||||
|
"after": new_text,
|
||||||
|
"replaced": count,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (idx, matcher) in check_matchers.iter().enumerate() {
|
||||||
|
if matcher.matches_text_or_raw(&plain, raw) {
|
||||||
|
check_hit_counts[idx] += 1;
|
||||||
|
entity_had_match = true;
|
||||||
|
if suspect_matches.len() < limit {
|
||||||
|
suspect_matches.push(json!({
|
||||||
|
"handle": format!("{:X}", handle.value()),
|
||||||
|
"type": "Insert",
|
||||||
|
"attribute_tag": attr.tag,
|
||||||
|
"layer": common.layer,
|
||||||
|
"position": [insert.insert_point.x, insert.insert_point.y, insert.insert_point.z],
|
||||||
|
"term": check_terms[idx],
|
||||||
|
"plain_text": plain,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if entity_had_match {
|
||||||
|
entities_matched += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmatched pairs / queries
|
||||||
|
let unmatched_pairs: Vec<String> = pair_hit_counts
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, &count)| count == 0)
|
||||||
|
.map(|(idx, _)| pairs[idx].0.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let unmatched_check_terms: Vec<String> = check_hit_counts
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, &count)| count == 0)
|
||||||
|
.map(|(idx, _)| check_terms[idx].clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"ok": true,
|
||||||
|
"summary": {
|
||||||
|
"entities_scanned": entities_scanned,
|
||||||
|
"entities_matched": entities_matched,
|
||||||
|
"occurrences_matched": total_occurrences,
|
||||||
|
"pairs_total": pairs.len(),
|
||||||
|
"pairs_unmatched": unmatched_pairs.len(),
|
||||||
|
"check_terms_total": check_terms.len(),
|
||||||
|
"check_terms_found": check_terms.len() - unmatched_check_terms.len(),
|
||||||
|
},
|
||||||
|
"unmatched_pairs": unmatched_pairs,
|
||||||
|
"unmatched_check_terms": unmatched_check_terms,
|
||||||
|
"simulated_changes": simulated_changes,
|
||||||
|
"suspect_matches": suspect_matches,
|
||||||
|
"unrecognized_words": unrecognized_words,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
/// Execute text replace across candidates with single-transaction undo support.
|
/// Execute text replace across candidates with single-transaction undo support.
|
||||||
pub(crate) fn execute_text_replace(
|
pub(crate) fn execute_text_replace(
|
||||||
&mut self,
|
&mut self,
|
||||||
req: &Value,
|
req: &Value,
|
||||||
push_undo: bool,
|
push_undo: bool,
|
||||||
) -> Result<Value, Value> {
|
) -> Result<Value, Value> {
|
||||||
let i = self.active_tab;
|
let i = if let Some(doc_id) = req.get("document_id").and_then(|v| v.as_u64()) {
|
||||||
|
self.tabs
|
||||||
|
.iter()
|
||||||
|
.position(|t| t.id == doc_id)
|
||||||
|
.ok_or_else(|| failure("document_not_found", "Specified document_id not found"))?
|
||||||
|
} else {
|
||||||
|
self.active_tab
|
||||||
|
};
|
||||||
|
|
||||||
// Parse search/replace pairs
|
// Parse search/replace pairs
|
||||||
let mut pairs: Vec<(String, String)> = Vec::new();
|
let mut pairs: Vec<(String, String)> = Vec::new();
|
||||||
@@ -905,6 +1241,7 @@ impl OpenCADStudio {
|
|||||||
let match_case = req["match_case"].as_bool().unwrap_or(false);
|
let match_case = req["match_case"].as_bool().unwrap_or(false);
|
||||||
let whole_word = req["whole_word"].as_bool().unwrap_or(false);
|
let whole_word = req["whole_word"].as_bool().unwrap_or(false);
|
||||||
let ignore_accents = req["ignore_accents"].as_bool().unwrap_or(!match_case);
|
let ignore_accents = req["ignore_accents"].as_bool().unwrap_or(!match_case);
|
||||||
|
let dry_run = req["dry_run"].as_bool().unwrap_or(false);
|
||||||
let replace_all = req["replace_all"].as_bool().unwrap_or(true);
|
let replace_all = req["replace_all"].as_bool().unwrap_or(true);
|
||||||
let scope = req["scope"].as_str().unwrap_or("all");
|
let scope = req["scope"].as_str().unwrap_or("all");
|
||||||
let layer_filter = req["layer"].as_str();
|
let layer_filter = req["layer"].as_str();
|
||||||
@@ -917,7 +1254,7 @@ impl OpenCADStudio {
|
|||||||
.map(|(f, r)| (TextMatcher::new(f, match_case, whole_word, ignore_accents), r))
|
.map(|(f, r)| (TextMatcher::new(f, match_case, whole_word, ignore_accents), r))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if push_undo {
|
if push_undo && !dry_run {
|
||||||
self.push_undo_snapshot(i, "TEXT REPLACE");
|
self.push_undo_snapshot(i, "TEXT REPLACE");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -998,9 +1335,11 @@ impl OpenCADStudio {
|
|||||||
if before_val.is_none() {
|
if before_val.is_none() {
|
||||||
before_val = Some(text.value.clone());
|
before_val = Some(text.value.clone());
|
||||||
}
|
}
|
||||||
text.value = new_val;
|
if !dry_run {
|
||||||
|
text.value = new_val.clone();
|
||||||
|
}
|
||||||
entity_replaced += count;
|
entity_replaced += count;
|
||||||
after_val = Some(text.value.clone());
|
after_val = Some(new_val);
|
||||||
} else {
|
} else {
|
||||||
// Check if matching decoded DXF escapes
|
// Check if matching decoded DXF escapes
|
||||||
let decoded = decode_dxf_escapes(&text.value);
|
let decoded = decode_dxf_escapes(&text.value);
|
||||||
@@ -1011,9 +1350,11 @@ impl OpenCADStudio {
|
|||||||
if before_val.is_none() {
|
if before_val.is_none() {
|
||||||
before_val = Some(text.value.clone());
|
before_val = Some(text.value.clone());
|
||||||
}
|
}
|
||||||
text.value = new_val;
|
if !dry_run {
|
||||||
|
text.value = new_val.clone();
|
||||||
|
}
|
||||||
entity_replaced += c;
|
entity_replaced += c;
|
||||||
after_val = Some(text.value.clone());
|
after_val = Some(new_val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1027,9 +1368,11 @@ impl OpenCADStudio {
|
|||||||
if before_val.is_none() {
|
if before_val.is_none() {
|
||||||
before_val = Some(mtext.value.clone());
|
before_val = Some(mtext.value.clone());
|
||||||
}
|
}
|
||||||
mtext.value = new_val;
|
if !dry_run {
|
||||||
|
mtext.value = new_val.clone();
|
||||||
|
}
|
||||||
entity_replaced += count;
|
entity_replaced += count;
|
||||||
after_val = Some(mtext.value.clone());
|
after_val = Some(new_val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1041,9 +1384,11 @@ impl OpenCADStudio {
|
|||||||
if before_val.is_none() {
|
if before_val.is_none() {
|
||||||
before_val = Some(ad.default_value.clone());
|
before_val = Some(ad.default_value.clone());
|
||||||
}
|
}
|
||||||
ad.default_value = new_val;
|
if !dry_run {
|
||||||
|
ad.default_value = new_val.clone();
|
||||||
|
}
|
||||||
entity_replaced += count;
|
entity_replaced += count;
|
||||||
after_val = Some(ad.default_value.clone());
|
after_val = Some(new_val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1055,9 +1400,11 @@ impl OpenCADStudio {
|
|||||||
if before_val.is_none() {
|
if before_val.is_none() {
|
||||||
before_val = Some(ae.get_value().to_string());
|
before_val = Some(ae.get_value().to_string());
|
||||||
}
|
}
|
||||||
ae.set_value(new_val);
|
if !dry_run {
|
||||||
|
ae.set_value(new_val.clone());
|
||||||
|
}
|
||||||
entity_replaced += count;
|
entity_replaced += count;
|
||||||
after_val = Some(ae.get_value().to_string());
|
after_val = Some(new_val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1071,9 +1418,11 @@ impl OpenCADStudio {
|
|||||||
if before_val.is_none() {
|
if before_val.is_none() {
|
||||||
before_val = Some(base.text.clone());
|
before_val = Some(base.text.clone());
|
||||||
}
|
}
|
||||||
base.text = new_val;
|
if !dry_run {
|
||||||
|
base.text = new_val.clone();
|
||||||
|
}
|
||||||
entity_replaced += count;
|
entity_replaced += count;
|
||||||
after_val = Some(base.text.clone());
|
after_val = Some(new_val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1085,7 +1434,9 @@ impl OpenCADStudio {
|
|||||||
matcher.replace_all_in(attr.get_value(), replacement, replace_all);
|
matcher.replace_all_in(attr.get_value(), replacement, replace_all);
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
let old_val = attr.get_value().to_string();
|
let old_val = attr.get_value().to_string();
|
||||||
attr.set_value(new_val.clone());
|
if !dry_run {
|
||||||
|
attr.set_value(new_val.clone());
|
||||||
|
}
|
||||||
total_replaced += count;
|
total_replaced += count;
|
||||||
changes_log.push(json!({
|
changes_log.push(json!({
|
||||||
"handle": format!("{:X}", handle.value()),
|
"handle": format!("{:X}", handle.value()),
|
||||||
@@ -1121,33 +1472,37 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if total_replaced == 0 {
|
if total_replaced == 0 {
|
||||||
if push_undo {
|
if push_undo && !dry_run {
|
||||||
self.discard_last_undo_entry(i);
|
self.discard_last_undo_entry(i);
|
||||||
}
|
}
|
||||||
return Ok(json!({
|
return Ok(json!({
|
||||||
"ok": true,
|
"ok": true,
|
||||||
|
"dry_run": dry_run,
|
||||||
"replaced": 0,
|
"replaced": 0,
|
||||||
"entities_changed": 0,
|
"entities_changed": 0,
|
||||||
"changes": [],
|
"changes": [],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh scene caches and dirty flags
|
if !dry_run {
|
||||||
self.invalidate_property_targets(i, &changed_handles);
|
// Refresh scene caches and dirty flags
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.invalidate_property_targets(i, &changed_handles);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].scene.bump_geometry();
|
||||||
self.refresh_properties();
|
self.tabs[i].dirty = true;
|
||||||
|
self.refresh_properties();
|
||||||
|
|
||||||
self.command_line.push_output(
|
self.command_line.push_output(
|
||||||
format!(
|
format!(
|
||||||
"TEXT REPLACE: replaced {total_replaced} occurrence(s) in {} object(s).",
|
"TEXT REPLACE: replaced {total_replaced} occurrence(s) in {} object(s).",
|
||||||
changed_handles.len()
|
changed_handles.len()
|
||||||
)
|
)
|
||||||
.as_str(),
|
.as_str(),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"ok": true,
|
"ok": true,
|
||||||
|
"dry_run": dry_run,
|
||||||
"replaced": total_replaced,
|
"replaced": total_replaced,
|
||||||
"entities_changed": changed_handles.len(),
|
"entities_changed": changed_handles.len(),
|
||||||
"changes": changes_log,
|
"changes": changes_log,
|
||||||
@@ -1166,6 +1521,11 @@ impl OpenCADStudio {
|
|||||||
self.control_text_search(req).unwrap_or_else(|e| e)
|
self.control_text_search(req).unwrap_or_else(|e| e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Headless automation text audit.
|
||||||
|
pub(crate) fn automation_text_audit(&self, req: &Value) -> Value {
|
||||||
|
self.control_text_audit(req).unwrap_or_else(|e| e)
|
||||||
|
}
|
||||||
|
|
||||||
/// Headless automation text replace.
|
/// Headless automation text replace.
|
||||||
pub(crate) fn automation_text_replace(&mut self, req: &Value) -> Value {
|
pub(crate) fn automation_text_replace(&mut self, req: &Value) -> Value {
|
||||||
self.execute_text_replace(req, true).unwrap_or_else(|e| e)
|
self.execute_text_replace(req, true).unwrap_or_else(|e| e)
|
||||||
|
|||||||
+4
-1
@@ -43,6 +43,7 @@ const READ_OPS: &[&str] = &[
|
|||||||
"xdata_get",
|
"xdata_get",
|
||||||
"audit",
|
"audit",
|
||||||
"text_search",
|
"text_search",
|
||||||
|
"text_audit",
|
||||||
];
|
];
|
||||||
const EXECUTE_OPS: &[&str] = &[
|
const EXECUTE_OPS: &[&str] = &[
|
||||||
"new",
|
"new",
|
||||||
@@ -1064,6 +1065,7 @@ fn execute_request_schema() -> Value {
|
|||||||
"match_case":{"type":"boolean","default":false,"description":"Case-sensitive text search/replace."},
|
"match_case":{"type":"boolean","default":false,"description":"Case-sensitive text search/replace."},
|
||||||
"whole_word":{"type":"boolean","default":false,"description":"Match only whole words (bounded by non-alphanumeric characters)."},
|
"whole_word":{"type":"boolean","default":false,"description":"Match only whole words (bounded by non-alphanumeric characters)."},
|
||||||
"ignore_accents":{"type":"boolean","description":"Ignore accents/diacritics in text (defaults to true when match_case is false)."},
|
"ignore_accents":{"type":"boolean","description":"Ignore accents/diacritics in text (defaults to true when match_case is false)."},
|
||||||
|
"dry_run":{"type":"boolean","default":false,"description":"When true, simulates replacement without mutating entities or pushing undo."},
|
||||||
"replace_all":{"type":"boolean","default":true,"description":"Replace all occurrences within matching entities (default true)."},
|
"replace_all":{"type":"boolean","default":true,"description":"Replace all occurrences within matching entities (default true)."},
|
||||||
"scope":{"type":"string","enum":["all","active_space","blocks"],"default":"all","description":"Text search/replace scope."},
|
"scope":{"type":"string","enum":["all","active_space","blocks"],"default":"all","description":"Text search/replace scope."},
|
||||||
"path":{"type":"string","minLength":1,"description":"Absolute path: drawing for open or save, image file for embed_image, target DWG/DXF for wblock, target PDF for plot; save_verified requires .dwg or .dxf."},
|
"path":{"type":"string","minLength":1,"description":"Absolute path: drawing for open or save, image file for embed_image, target DWG/DXF for wblock, target PDF for plot; save_verified requires .dwg or .dxf."},
|
||||||
@@ -1223,7 +1225,7 @@ fn tool_definitions() -> Value {
|
|||||||
{
|
{
|
||||||
"name":"ocs_read",
|
"name":"ocs_read",
|
||||||
"description":"Discover capabilities and record schemas, or read state, complete database records, command manifests, entities, properties, kernel measurements and spatial relationships, history, events or operation status from a live OCS session.",
|
"description":"Discover capabilities and record schemas, or read state, complete database records, command manifests, entities, properties, kernel measurements and spatial relationships, history, events or operation status from a live OCS session.",
|
||||||
"inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"op":{"type":"string","enum":READ_OPS,"default":"state"},"parameters":{"type":"object","description":"Operation-specific filters.","properties":{"name":{"type":"string","description":"Command name or record name."},"search":{"type":"string","description":"Case-insensitive command or record-type search."},"find":{"type":"string","description":"Text query string for text_search."},"match_case":{"type":"boolean","default":false,"description":"Case-sensitive text search."},"whole_word":{"type":"boolean","default":false,"description":"Match only whole words."},"ignore_accents":{"type":"boolean","description":"Ignore accents/diacritics in text (defaults to true when match_case is false)."},"scope":{"type":"string","enum":["all","active_space","blocks"],"default":"all","description":"Text search scope."},"document_id":{"type":"integer","minimum":0},"path":{"type":"string","description":"Optional intended output path for audit; extension determines target format."},"target_format":{"type":"string","enum":["dwg","dxf"],"description":"Intended output format for audit."},"target_version":{"type":"string","enum":["R14","2000","2004","2007","2010","2013","2018","AC1014","AC1015","AC1018","AC1021","AC1024","AC1027","AC1032"],"description":"Intended CAD output version for audit."},"collection":{"type":"string","description":"Record collection, all for records, or omit to discover collections and schema types."},"handle":{"type":"string"},"handles":{"type":"array","items":{"type":"string"},"description":"Exact entity or record handles."},"type":{"type":"string","description":"Entity or record type filter; for record_schema, returns its complete type graph and writable field paths."},"layer":{"type":"string","description":"Layer name filter for query."},"detail":{"type":"string","enum":["summary","geometry","full"],"default":"geometry","description":"Entity detail returned by query."},"fields":{"type":"array","items":{"type":"string"},"description":"Return only these entity fields plus handle."},"paths":{"type":"array","items":{"type":"string"},"description":"Project RFC 6901 JSON Pointer paths relative to record.properties."},"where":{"type":"array","description":"All property filters must match.","items":{"type":"object","properties":{"path":{"type":"string"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","contains","starts_with","ends_with","in","exists","not_exists"],"default":"eq"},"value":{}},"required":["path"],"additionalProperties":false}},"near":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Rank planar curves by exact kernel distance to this world XY point."},"point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"World point whose object snap the snap op reports."},"from":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Base point for perpendicular and tangent snaps (snap op)."},"contains_point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Return closed planar curves containing this world XY point."},"bounds":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4,"description":"Filter entities whose world XY bounds overlap [min_x,min_y,max_x,max_y]."},"intersections":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":2,"description":"Return exact kernel intersections between two planar curve handles."},"after":{"type":"integer","minimum":0,"description":"Event cursor."},"request_id":{"type":"string","description":"Operation id to query."},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":10000}},"additionalProperties":false}},"required":["ocs_session_id"],"additionalProperties":false},
|
"inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"op":{"type":"string","enum":READ_OPS,"default":"state"},"parameters":{"type":"object","description":"Operation-specific filters.","properties":{"name":{"type":"string","description":"Command name or record name."},"search":{"type":"string","description":"Case-insensitive command or record-type search."},"find":{"type":"string","description":"Text query string for text_search."},"match_case":{"type":"boolean","default":false,"description":"Case-sensitive text search."},"whole_word":{"type":"boolean","default":false,"description":"Match only whole words."},"ignore_accents":{"type":"boolean","description":"Ignore accents/diacritics in text (defaults to true when match_case is false)."},"scope":{"type":"string","enum":["all","active_space","blocks"],"default":"all","description":"Text search scope."},"check_terms":{"type":"array","items":{"type":"string"},"description":"List of terms or suspect misspellings to flag in text_audit."},"dictionary":{"type":"array","items":{"type":"string"},"description":"Known valid words for text_audit dictionary check."},"pairs":{"type":"array","description":"Find/replace pairs for dry-run simulation in text_audit.","items":{"type":"object","properties":{"find":{"type":"string"},"replace":{"type":"string"}},"required":["find","replace"]}},"dry_run_pairs":{"type":"array","description":"Alias for pairs in text_audit.","items":{"type":"object","properties":{"find":{"type":"string"},"replace":{"type":"string"}},"required":["find","replace"]}},"document_id":{"type":"integer","minimum":0},"path":{"type":"string","description":"Optional intended output path for audit; extension determines target format."},"target_format":{"type":"string","enum":["dwg","dxf"],"description":"Intended output format for audit."},"target_version":{"type":"string","enum":["R14","2000","2004","2007","2010","2013","2018","AC1014","AC1015","AC1018","AC1021","AC1024","AC1027","AC1032"],"description":"Intended CAD output version for audit."},"collection":{"type":"string","description":"Record collection, all for records, or omit to discover collections and schema types."},"handle":{"type":"string"},"handles":{"type":"array","items":{"type":"string"},"description":"Exact entity or record handles."},"type":{"type":"string","description":"Entity or record type filter; for record_schema, returns its complete type graph and writable field paths."},"layer":{"type":"string","description":"Layer name filter for query."},"detail":{"type":"string","enum":["summary","geometry","full"],"default":"geometry","description":"Entity detail returned by query."},"fields":{"type":"array","items":{"type":"string"},"description":"Return only these entity fields plus handle."},"paths":{"type":"array","items":{"type":"string"},"description":"Project RFC 6901 JSON Pointer paths relative to record.properties."},"where":{"type":"array","description":"All property filters must match.","items":{"type":"object","properties":{"path":{"type":"string"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","contains","starts_with","ends_with","in","exists","not_exists"],"default":"eq"},"value":{}},"required":["path"],"additionalProperties":false}},"near":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Rank planar curves by exact kernel distance to this world XY point."},"point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"World point whose object snap the snap op reports."},"from":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Base point for perpendicular and tangent snaps (snap op)."},"contains_point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Return closed planar curves containing this world XY point."},"bounds":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4,"description":"Filter entities whose world XY bounds overlap [min_x,min_y,max_x,max_y]."},"intersections":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":2,"description":"Return exact kernel intersections between two planar curve handles."},"after":{"type":"integer","minimum":0,"description":"Event cursor."},"request_id":{"type":"string","description":"Operation id to query."},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":10000}},"additionalProperties":false}},"required":["ocs_session_id"],"additionalProperties":false},
|
||||||
"outputSchema":read_output_schema(),
|
"outputSchema":read_output_schema(),
|
||||||
"annotations":{"title":"Read OCS state","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}
|
"annotations":{"title":"Read OCS state","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}
|
||||||
},
|
},
|
||||||
@@ -1595,6 +1597,7 @@ mod tests {
|
|||||||
assert!(READ_OPS.contains(&"record_schema"));
|
assert!(READ_OPS.contains(&"record_schema"));
|
||||||
assert!(READ_OPS.contains(&"audit"));
|
assert!(READ_OPS.contains(&"audit"));
|
||||||
assert!(READ_OPS.contains(&"text_search"));
|
assert!(READ_OPS.contains(&"text_search"));
|
||||||
|
assert!(READ_OPS.contains(&"text_audit"));
|
||||||
assert!(EXECUTE_OPS.contains(&"set_properties"));
|
assert!(EXECUTE_OPS.contains(&"set_properties"));
|
||||||
assert!(EXECUTE_OPS.contains(&"save_verified"));
|
assert!(EXECUTE_OPS.contains(&"save_verified"));
|
||||||
assert!(EXECUTE_OPS.contains(&"text_replace"));
|
assert!(EXECUTE_OPS.contains(&"text_replace"));
|
||||||
|
|||||||
Reference in new issue
Block a user