diff --git a/Cargo.lock b/Cargo.lock index 2921f386..a23e2915 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3136,7 +3136,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai-tool-profiles" -version = "0.10.3" +version = "0.10.4" dependencies = [ "async-graphql", "async-graphql-parser", diff --git a/Cargo.toml b/Cargo.toml index 5582326f..cd748517 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ futures = "0.3" getrandom = "0.3" graphql-composition = "=0.12.2" graphql-orm = { path = "crates/graphql-orm", version = "0.30.0", default-features = false } -graphql-orm-ai-tool-profiles = { path = "crates/graphql-orm-ai-tool-profiles", version = "0.10.3" } +graphql-orm-ai-tool-profiles = { path = "crates/graphql-orm-ai-tool-profiles", version = "0.10.4" } graphql-orm-backup = { path = "crates/graphql-orm-backup", version = "0.7.2", default-features = false } graphql-orm-operation-catalog = { path = "crates/graphql-orm-operation-catalog", version = "0.4.0" } graphql-orm-router-protocol = { path = "crates/graphql-orm-router-protocol", version = "0.2.1" } diff --git a/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md b/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md index 5b2c6a9f..10e6c4d8 100644 --- a/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md +++ b/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md @@ -3,13 +3,29 @@ title: "graphql-orm-ai-tool-profiles changelog" kind: reference status: active owner: graphql-orm-ai-maintainers -last_reviewed: 2026-09-01 +last_reviewed: 2026-09-02 review_by: 2027-02-11 supersedes: [] --- # Changelog +## [0.10.4] - 2026-09-02 + +### Fixed + +- Capability discovery now ranks public resolver descriptions and root + operation identity ahead of incidental nested-field vocabulary, splits + PascalCase public names into searchable terms, and uses explicit mechanical + result shape only to break semantic ties. + +### Security + +- A weak shape match can no longer displace a resolver-authored semantic match. + Shape metadata alone still cannot admit an otherwise unrelated capability, + and exact namespace, kind, entity/operation, authority and execution checks + remain unchanged. + ## [0.10.3] - 2026-09-01 ### Added diff --git a/crates/graphql-orm-ai-tool-profiles/Cargo.toml b/crates/graphql-orm-ai-tool-profiles/Cargo.toml index ea55b370..ac7ed714 100644 --- a/crates/graphql-orm-ai-tool-profiles/Cargo.toml +++ b/crates/graphql-orm-ai-tool-profiles/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graphql-orm-ai-tool-profiles" -version = "0.10.3" +version = "0.10.4" edition = "2024" authors = ["Toby Martin "] description = "Backend-neutral GraphQL AI tool profile compiler and manifest contracts" diff --git a/crates/graphql-orm-ai-tool-profiles/MIGRATION.md b/crates/graphql-orm-ai-tool-profiles/MIGRATION.md index 1b71fa29..bf60c863 100644 --- a/crates/graphql-orm-ai-tool-profiles/MIGRATION.md +++ b/crates/graphql-orm-ai-tool-profiles/MIGRATION.md @@ -3,13 +3,22 @@ title: "graphql-orm-ai-tool-profiles migration guide" kind: reference status: active owner: graphql-orm-ai-maintainers -last_reviewed: 2026-09-01 +last_reviewed: 2026-09-02 review_by: 2027-02-11 supersedes: [] --- # Migration Guide +## 0.10.3 to 0.10.4: resolver-first semantic discovery ranking + +Adopt `graphql-orm-ai-tool-profiles` 0.10.4 from one reviewed full monorepo +revision. No capability-index wire shape or execution contract changes. +Discovery now tokenizes PascalCase public names, ranks public resolver/root +semantics ahead of nested field vocabulary, and uses explicit result shape only +as a semantic tie-breaker. Hosts should retain exact namespace, kind and +entity/operation filters whenever they know those canonical values. + ## 0.10.2 to 0.10.3: proof-bearing pre-dispatch provider failure Adopt `graphql-orm-ai-tool-profiles` 0.10.3 with `graphql-orm-ai` 0.95.12 from diff --git a/crates/graphql-orm-ai-tool-profiles/README.md b/crates/graphql-orm-ai-tool-profiles/README.md index 7cc90cd2..32889445 100644 --- a/crates/graphql-orm-ai-tool-profiles/README.md +++ b/crates/graphql-orm-ai-tool-profiles/README.md @@ -3,7 +3,7 @@ title: "graphql-orm-ai-tool-profiles" kind: reference status: active owner: graphql-orm-ai-maintainers -last_reviewed: 2026-09-01 +last_reviewed: 2026-09-02 review_by: 2027-02-11 supersedes: [] --- @@ -24,7 +24,7 @@ are separate runtime decisions and must remain default-deny. ```toml [dependencies] -graphql-orm-ai-tool-profiles = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.10.3" } +graphql-orm-ai-tool-profiles = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.10.4" } serde_json = "1" ``` @@ -134,12 +134,15 @@ semantic summaries and exact fingerprints. It intentionally contains no JSON Schema, GraphQL document/SDL, database name, resolver URL, policy expression, credential, authority or secret/hidden field. -`AiCapabilityIndex::search` provides bounded deterministic discovery with -exact namespace/kind/entity filters and stable ID tie-breaking. Explicit list, -details, search, keyset, or aggregate intent ranks the matching compiler-owned -operation shape first; public entity, execution-target, and namespace relevance -rank next. Every candidate still requires positive lexical relevance, and -non-matching shapes remain eligible. Search +`AiCapabilityIndex::search` provides bounded deterministic semantic discovery +with exact namespace/kind/entity filters and stable ID tie-breaking. Public +resolver descriptions, operation/name/entity identity, execution target and +namespace rank ahead of nested field or relationship vocabulary. Explicit +mechanical result-shape intent is only a tie-breaker among otherwise relevant +candidates, so a weak list-shaped match cannot displace a resolver whose +description names the requested business result. PascalCase operation and +entity names contribute their component terms. Every candidate still requires +positive semantic relevance, and non-matching shapes remain eligible. Search returns exact candidate/index/schema/semantic/target-policy fingerprints but grants no authority. Each entry also carries conservative compiler-owned root and total result-record bounds for later planning. The runtime package owns diff --git a/crates/graphql-orm-ai-tool-profiles/src/capability_index.rs b/crates/graphql-orm-ai-tool-profiles/src/capability_index.rs index e1aeac1a..8602e63b 100644 --- a/crates/graphql-orm-ai-tool-profiles/src/capability_index.rs +++ b/crates/graphql-orm-ai-tool-profiles/src/capability_index.rs @@ -832,11 +832,15 @@ fn entry_matches_query(entry: &AiCapabilityIndexEntry, query: &AiCapabilitySearc #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] struct SearchRank { + root_semantics: u64, + nested_semantics: u64, shape: u8, - entity: u16, - target: u16, - namespace: u16, - lexical: u64, +} + +impl SearchRank { + fn has_semantic_match(self) -> bool { + self.root_semantics > 0 || self.nested_semantics > 0 + } } fn rank_entries<'a>( @@ -851,7 +855,7 @@ fn rank_entries<'a>( .filter(|(_, entry)| entry_matches_query(entry, query)) .filter_map(|(target_id, entry)| { let rank = search_rank(target_id, entry, &terms, shape_intent); - (rank.lexical > 0).then_some((rank, entry)) + rank.has_semantic_match().then_some((rank, entry)) }) .collect::>(); ranked.sort_by(|left, right| { @@ -1356,7 +1360,8 @@ fn search_rank( terms: &BTreeSet, shape_intent: Option, ) -> SearchRank { - let mut score = 0_u64; + let mut root_semantics = 0_u64; + let mut nested_semantics = 0_u64; let id = search_terms(entry.id.as_str()); let name = search_terms(&entry.name); let operation = search_terms(&entry.operation_name); @@ -1366,6 +1371,7 @@ fn search_rank( .map(search_terms) .unwrap_or_default(); let target = search_terms(target_id.as_str()); + let namespace = search_terms(&entry.namespace); let description = search_terms(&entry.description); let relationships = entry .relationships @@ -1383,24 +1389,23 @@ fn search_rank( .flat_map(|field| search_terms(&format!("{} {}", field.name, field.description))) .collect::>(); for term in terms { - if search_shape_term(term) && entry.operation_shape != AiCapabilityOperationShape::Custom { + if search_shape_term(term) { continue; } - score += u64::from(id.contains(term)) * 10; - score += u64::from(name.contains(term)) * 9; - score += u64::from(operation.contains(term)) * 8; - score += u64::from(entity.contains(term)) * 8; - score += u64::from(relationships.contains(term)) * 7; - score += u64::from(fields.contains(term)) * 4; - score += u64::from(description.contains(term)) * 3; + root_semantics += u64::from(id.contains(term)) * 12; + root_semantics += u64::from(name.contains(term)) * 11; + root_semantics += u64::from(operation.contains(term)) * 10; + root_semantics += u64::from(entity.contains(term)) * 9; + root_semantics += u64::from(target.contains(term)) * 8; + root_semantics += u64::from(namespace.contains(term)) * 8; + root_semantics += u64::from(description.contains(term)) * 7; + nested_semantics += u64::from(relationships.contains(term)) * 3; + nested_semantics += u64::from(fields.contains(term)) * 2; } SearchRank { + root_semantics, + nested_semantics, shape: u8::from(shape_intent == Some(entry.operation_shape)), - entity: u16::try_from(terms.intersection(&entity).count()).unwrap_or(u16::MAX), - target: u16::try_from(terms.intersection(&target).count()).unwrap_or(u16::MAX), - namespace: u16::try_from(terms.intersection(&search_terms(&entry.namespace)).count()) - .unwrap_or(u16::MAX), - lexical: score, } } @@ -1447,13 +1452,43 @@ fn search_terms(value: &str) -> BTreeSet { } fn search_tokens(value: &str) -> Vec { - value - .split(|character: char| !character.is_ascii_alphanumeric()) - .filter_map(|term| { - let term = semantic_key(term); - (!term.is_empty()).then_some(term) - }) - .collect() + let bytes = value.as_bytes(); + let mut tokens = Vec::new(); + let mut token = String::new(); + for (index, byte) in bytes.iter().copied().enumerate() { + if !byte.is_ascii_alphanumeric() { + push_search_token(&mut tokens, &mut token); + continue; + } + let previous = index + .checked_sub(1) + .and_then(|previous| bytes.get(previous)); + let next = bytes.get(index + 1); + let camel_boundary = !token.is_empty() + && byte.is_ascii_uppercase() + && previous.is_some_and(|previous| { + previous.is_ascii_lowercase() + || previous.is_ascii_digit() + || (previous.is_ascii_uppercase() && next.is_some_and(u8::is_ascii_lowercase)) + }); + if camel_boundary { + push_search_token(&mut tokens, &mut token); + } + token.push(char::from(byte)); + } + push_search_token(&mut tokens, &mut token); + tokens +} + +fn push_search_token(tokens: &mut Vec, token: &mut String) { + if token.is_empty() { + return; + } + let semantic = semantic_key(token); + token.clear(); + if !semantic.is_empty() { + tokens.push(semantic); + } } fn semantic_key(value: &str) -> String { @@ -1683,6 +1718,120 @@ mod tests { ); } + #[test] + fn resolver_description_semantics_outrank_incidental_nested_list_matches() { + let semantic = semantic_catalogue(); + let target_id = GraphqlExecutionTargetId::parse("communications-service").expect("target"); + let mut mailbox = static_entry( + descriptor( + "communications.query.activity", + "Returns messages from the monitored support mailbox for operator triage.", + 0, + ), + &target_id, + "schema-v1", + &semantic.fingerprint, + "policy-v1", + ) + .expect("mailbox entry"); + mailbox.kind = AiCapabilityKind::GeneratedQuery; + mailbox.name = "CurrentActivity".to_owned(); + mailbox.operation_name = "CurrentActivity".to_owned(); + mailbox.operation_shape = AiCapabilityOperationShape::Custom; + mailbox.fingerprint = entry_fingerprint(&mailbox); + + let mut records = static_entry( + descriptor("records.query.customer_cards", "List customer records.", 0), + &target_id, + "schema-v1", + &semantic.fingerprint, + "policy-v1", + ) + .expect("record entry"); + records.kind = AiCapabilityKind::GeneratedQuery; + records.name = "List CustomerRecord".to_owned(); + records.entity_name = Some("CustomerRecord".to_owned()); + records.operation_name = "CustomerRecords".to_owned(); + records.operation_shape = AiCapabilityOperationShape::List; + records.relationships = vec![AiCapabilityRelationshipSummary { + name: "Assignments".to_owned(), + description: "Recent support activity associated with the customer.".to_owned(), + target_entity: "Assignment".to_owned(), + to_many: true, + arguments: BTreeSet::new(), + }]; + records.fingerprint = entry_fingerprint(&records); + + let entries = + BTreeMap::from([(mailbox.id.clone(), mailbox), (records.id.clone(), records)]); + let index = AiCapabilityIndex { + version: AI_CAPABILITY_INDEX_VERSION, + target_id, + schema_fingerprint: "schema-v1".to_owned(), + semantic_catalogue_fingerprint: semantic.fingerprint, + target_policy_fingerprint: "policy-v1".to_owned(), + entries, + fingerprint: "a".repeat(64), + limits: AiCapabilityIndexLimits::default(), + }; + let result = index + .search(&AiCapabilitySearchQuery { + text: "list recent messages from the support mailbox".to_owned(), + namespace: None, + kind: Some(AiCapabilityKind::GeneratedQuery), + entity_or_operation: None, + maximum_results: 1, + }) + .expect("resolver-hint search"); + assert_eq!(result.candidates.len(), 1); + assert_eq!( + result.candidates[0].id.as_str(), + "communications.query.activity" + ); + } + + #[test] + fn camel_case_operation_names_supply_searchable_semantic_terms() { + let semantic = semantic_catalogue(); + let target_id = GraphqlExecutionTargetId::parse("application").expect("target"); + let mut entry = static_entry( + descriptor("application.query.activity", "Returns bounded records.", 0), + &target_id, + "schema-v1", + &semantic.fingerprint, + "policy-v1", + ) + .expect("entry"); + entry.kind = AiCapabilityKind::GeneratedQuery; + entry.name = "MonitoredMailboxMessages".to_owned(); + entry.operation_name = "MonitoredMailboxMessages".to_owned(); + entry.fingerprint = entry_fingerprint(&entry); + let index = AiCapabilityIndex { + version: AI_CAPABILITY_INDEX_VERSION, + target_id, + schema_fingerprint: "schema-v1".to_owned(), + semantic_catalogue_fingerprint: semantic.fingerprint, + target_policy_fingerprint: "policy-v1".to_owned(), + entries: BTreeMap::from([(entry.id.clone(), entry)]), + fingerprint: "b".repeat(64), + limits: AiCapabilityIndexLimits::default(), + }; + let result = index + .search(&AiCapabilitySearchQuery { + text: "mailbox messages".to_owned(), + namespace: None, + kind: Some(AiCapabilityKind::GeneratedQuery), + entity_or_operation: None, + maximum_results: 1, + }) + .expect("camel-case search"); + assert_eq!(result.candidates.len(), 1); + assert_eq!( + result.candidates[0].operation_name, + "MonitoredMailboxMessages" + ); + } + #[test] fn ordinary_request_vocabulary_does_not_infer_operation_shape() { for text in [ diff --git a/crates/graphql-orm-ai/docs/implementation-status.md b/crates/graphql-orm-ai/docs/implementation-status.md index 6dd22a29..aaea9030 100644 --- a/crates/graphql-orm-ai/docs/implementation-status.md +++ b/crates/graphql-orm-ai/docs/implementation-status.md @@ -3,7 +3,7 @@ title: "Implementation Status" kind: reference status: active owner: graphql-orm-ai-maintainers -last_reviewed: 2026-09-01 +last_reviewed: 2026-09-02 review_by: 2027-02-01 supersedes: [] --- @@ -12,7 +12,7 @@ supersedes: [] `graphql-orm-ai` is at crate version `0.95.14` with AI schema module `0.64.0`. It uses workspace `graphql-orm` `0.30.0`, backend-neutral -`graphql-orm-ai-tool-profiles` `0.10.3`, and external `agql-auth` +`graphql-orm-ai-tool-profiles` `0.10.4`, and external `agql-auth` `0.19.0` at `1d2e9fe2e1576105212a7b340a11abf8cad0382d`. Completed stateless local-provider turns can carry a proof-bearing contained diff --git a/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock b/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock index 4fcb9281..234b7643 100644 --- a/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock +++ b/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock @@ -1329,7 +1329,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai" -version = "0.95.13" +version = "0.95.14" dependencies = [ "agql-auth", "async-graphql", @@ -1357,7 +1357,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai-tool-profiles" -version = "0.10.3" +version = "0.10.4" dependencies = [ "async-graphql", "async-graphql-parser", diff --git a/docs/plans/active/ai-production-readiness/README.md b/docs/plans/active/ai-production-readiness/README.md index 3991e7d9..684f0020 100644 --- a/docs/plans/active/ai-production-readiness/README.md +++ b/docs/plans/active/ai-production-readiness/README.md @@ -3,8 +3,8 @@ title: GraphQL ORM AI production-readiness plan kind: plan status: active owner: graphql-orm-ai-maintainers -last_reviewed: 2026-08-16 -review_by: 2026-09-01 +last_reviewed: 2026-09-02 +review_by: 2026-10-01 supersedes: - crates/graphql-orm-ai/docs/completion-plan.md --- @@ -59,7 +59,7 @@ retention, or restore proofs remain closed. ## Current checkpoint -Package 0.81.0 and AI schema module 0.60.0 provide the protected runtime, +Package 0.95.14 and AI schema module 0.64.0 provide the protected runtime, provider adapters, exact completed-batch adoption, retention foundations, restore planning, and readiness observation contracts. Database-derived collection covers bounded conservative run classification, approval and diff --git a/docs/plans/active/documentation-experience/README.md b/docs/plans/active/documentation-experience/README.md index fa8d3b71..3f345854 100644 --- a/docs/plans/active/documentation-experience/README.md +++ b/docs/plans/active/documentation-experience/README.md @@ -3,8 +3,8 @@ title: Documentation experience plan kind: plan status: active owner: workspace-maintainers -last_reviewed: 2026-08-12 -review_by: 2026-09-01 +last_reviewed: 2026-09-02 +review_by: 2026-10-01 supersedes: [] --- diff --git a/docs/reference/workspace-packages.md b/docs/reference/workspace-packages.md index 59674264..86e4b4aa 100644 --- a/docs/reference/workspace-packages.md +++ b/docs/reference/workspace-packages.md @@ -20,7 +20,7 @@ changes. | --- | --- | --- | --- | --- | | `graphql-orm` | `0.30.0` | `crates/graphql-orm` | `sqlite` | `graphql-orm-macros`, `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | | `graphql-orm-ai` | `0.95.14` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` | -| `graphql-orm-ai-tool-profiles` | `0.10.3` | `crates/graphql-orm-ai-tool-profiles` | none | `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | +| `graphql-orm-ai-tool-profiles` | `0.10.4` | `crates/graphql-orm-ai-tool-profiles` | none | `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | | `graphql-orm-backup` | `0.7.2` | `crates/graphql-orm-backup` | `local` | `graphql-orm` (optional), `graphql-orm-storage` | | `graphql-orm-macros` | `0.30.0` | `crates/graphql-orm-macros` | `sqlite` | none | | `graphql-orm-operation-catalog` | `0.4.0` | `crates/graphql-orm-operation-catalog` | none | `graphql-orm-router-protocol` (optional) | diff --git a/docs/templates/investigation.md b/docs/templates/investigation.md index 323b14a9..9b33727d 100644 --- a/docs/templates/investigation.md +++ b/docs/templates/investigation.md @@ -3,8 +3,8 @@ title: TEMPLATE Investigation kind: investigation status: draft owner: workspace-maintainers -last_reviewed: 2026-08-01 -review_by: 2026-09-01 +last_reviewed: 2026-09-02 +review_by: 2026-10-01 supersedes: [] --- diff --git a/docs/templates/plan.md b/docs/templates/plan.md index 9b5c79a0..ad42a016 100644 --- a/docs/templates/plan.md +++ b/docs/templates/plan.md @@ -3,8 +3,8 @@ title: TEMPLATE Active initiative plan kind: plan status: draft owner: workspace-maintainers -last_reviewed: 2026-08-01 -review_by: 2026-09-01 +last_reviewed: 2026-09-02 +review_by: 2026-10-01 supersedes: [] ---