From 3779fa4ca248efb306b3c6ca0a4eb6a6d9d2d202 Mon Sep 17 00:00:00 2001 From: chihiro <120623063+gumybit@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:05:33 +0900 Subject: [PATCH] =?UTF-8?q?Binding=E6=B8=88=E3=81=BF=E3=83=87=E3=83=BC?= =?UTF-8?q?=E3=82=BF=E4=BF=9D=E5=AD=98=E5=85=88=E3=81=AE=E6=9C=AA=E5=88=86?= =?UTF-8?q?=E9=A1=9E=E5=87=A6=E7=90=86=E3=82=92=E7=A2=BA=E8=AA=8D=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/binding_draft_validation.rs | 51 +++++++++++++++++++-- src/project_setup.rs | 17 +++++++ tests/cli.rs | 80 +++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 4 deletions(-) diff --git a/src/binding_draft_validation.rs b/src/binding_draft_validation.rs index a765807..a2b8e3b 100644 --- a/src/binding_draft_validation.rs +++ b/src/binding_draft_validation.rs @@ -332,16 +332,37 @@ fn validate_binding_artifact( ); let observed_symbols = string_set(&inventory["symbols"]); let observed_resources = string_set(&inventory["resources"]); - let candidates = inventory["framework_candidates"] + let mut candidates = inventory["framework_candidates"] .as_array() .into_iter() .flatten() .filter_map(|candidate| { candidate["binding_key"] .as_str() - .map(|key| (key, candidate)) + .map(|key| (key.to_owned(), candidate.clone())) }) .collect::>(); + for observation in inventory["observations"] + .as_array() + .into_iter() + .flatten() + .filter(|observation| observation["kind"] == "other_method_call") + { + let Some(resource) = observation["resource"].as_str() else { + continue; + }; + let Some(method) = observation["method"].as_str() else { + continue; + }; + candidates + .entry(format!("{resource}.{method}")) + .or_insert_with(|| { + serde_json::json!({ + "symbol": observation["symbol"], + "resource": resource, + }) + }); + } let bound_symbols = validate_symbols( &bindings["symbols"], &observed_symbols, @@ -506,7 +527,7 @@ fn validate_resources( #[allow(clippy::too_many_arguments)] fn validate_methods( value: &Value, - candidates: &BTreeMap<&str, &Value>, + candidates: &BTreeMap, bound_symbols: &BTreeSet, resource_refs: &BTreeMap>, accepted: &BTreeSet<&str>, @@ -524,7 +545,7 @@ fn validate_methods( return; }; for (physical, record) in records { - let candidate = candidates.get(physical.as_str()).copied(); + let candidate = candidates.get(physical.as_str()); if candidate.is_none() { issue( issues, @@ -758,6 +779,28 @@ fn validate_unused_bindings( used_resources.insert(resource); } } + for observation in inventory["observations"] + .as_array() + .into_iter() + .flatten() + .filter(|observation| observation["kind"] == "other_method_call") + .filter(|observation| { + let Some(resource) = observation["resource"].as_str() else { + return false; + }; + let Some(method) = observation["method"].as_str() else { + return false; + }; + retained_methods.contains(format!("{resource}.{method}").as_str()) + }) + { + if let Some(symbol) = observation["symbol"].as_str() { + used_symbols.insert(symbol); + } + if let Some(resource) = observation["resource"].as_str() { + used_resources.insert(resource); + } + } for symbol in bound_symbols { if !used_symbols.contains(symbol.as_str()) { issue( diff --git a/src/project_setup.rs b/src/project_setup.rs index 763ff57..7ea20be 100644 --- a/src/project_setup.rs +++ b/src/project_setup.rs @@ -561,10 +561,21 @@ fn binding_artifact_template( ) }) .collect::>(); + let bound_resources = observations + .iter() + .filter(|observation| observation.kind != SourceObservationKind::OtherMethodCall) + .map(|observation| observation.resource.as_str()) + .chain( + candidates + .iter() + .map(|candidate| candidate.resource.as_str()), + ) + .collect::>(); let relevant = observations .iter() .filter(|observation| { observation.kind != SourceObservationKind::OtherMethodCall + || bound_resources.contains(observation.resource.as_str()) || candidate_calls.contains(&( observation.symbol.as_str(), observation.resource.as_str(), @@ -602,6 +613,12 @@ fn binding_artifact_template( .iter() .filter(|candidate| candidate.method_binding_required) .map(|candidate| candidate.binding_key.clone()) + .chain( + relevant + .iter() + .filter(|observation| observation.kind == SourceObservationKind::OtherMethodCall) + .map(|observation| format!("{}.{}", observation.resource, observation.method)), + ) .collect::>(); let methods = method_keys .into_iter() diff --git a/tests/cli.rs b/tests/cli.rs index 5c231d8..b1a665c 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1880,6 +1880,86 @@ fn project_observe_refuses_a_symlinked_draft_output() { let _ = fs::remove_file(outside); } +#[test] +fn project_observe_offers_unclassified_methods_on_a_resource_with_a_builtin_effect() { + let project = TestProject::new(); + fs::write( + project.root.join("src/place_order.py"), + "def place_order(order):\n existing = orders.select()\n orders.insert(order)\n return existing\n", + ) + .unwrap(); + let draft_relative = ".adf/repository-observation.draft.yaml"; + let observed = project.run(&[ + "project", + "observe", + "--analysis-root", + "src", + "--output", + draft_relative, + ]); + assert_success(&observed); + + let draft_path = project.root.join(draft_relative); + let mut draft = read_yaml(&draft_path); + let binding = draft["binding_artifacts"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|artifact| artifact["path"] == "src/place_order.py") + .unwrap(); + assert!(binding["bindings"]["methods"]["orders.select"].is_object()); + binding["bindings"] = json!({ + "symbols": { + "place_order": { + "logical_ref": "operation.place-order", + "owner": "team.ordering", + "authority_ref": "decision.repository-bindings" + } + }, + "resources": { + "orders": { + "logical_refs": {"data": "data.orders"}, + "owner": "team.ordering", + "authority_ref": "decision.repository-bindings" + } + }, + "methods": { + "orders.select": { + "fact_kinds": ["sensitive_data_access"], + "owner": "team.ordering", + "authority_ref": "decision.repository-bindings" + } + } + }); + let publish_binding = draft["binding_artifacts"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|artifact| artifact["path"] == "src/publish_order.py") + .unwrap(); + publish_binding["bindings"] = json!({ + "symbols": { + "publish_order": { + "logical_ref": "operation.place-order", + "owner": "team.ordering", + "authority_ref": "decision.repository-bindings" + } + }, + "resources": { + "order_events": { + "logical_refs": {"integration": "integration.order-events"}, + "owner": "team.ordering", + "authority_ref": "decision.repository-bindings" + } + }, + "methods": {} + }); + write_yaml(&draft_path, &draft); + + let validation = project.run(&["project", "validate-bindings", "--draft", draft_relative]); + assert_success(&validation); +} + #[test] fn project_validates_and_promotes_a_reviewed_draft_safely() { let project = TestProject::new();