diff --git a/CHANGELOG.md b/CHANGELOG.md index a77a717..6a5e19f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,3 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Python bindings via PyO3 (`aml-python`) - AML language specification in `docs/spec/` - Conformance test suite in `tests/conformance/` +- `extends=` attribute on `` nodes for interface inheritance + (ADR-011). Enables semantically correct multi-level AML hierarchies where one + interface specialises another. The attribute is metadata and validation only + in this release — resolution remains unchanged. + +### Deprecated + +- Using `implements=` on `` nodes. This now produces a + validation warning. Migrate to `extends=`. The attribute will become a hard + error in a future release. diff --git a/crates/aml-core/src/ast.rs b/crates/aml-core/src/ast.rs index dc0d56c..32b19d1 100644 --- a/crates/aml-core/src/ast.rs +++ b/crates/aml-core/src/ast.rs @@ -52,6 +52,13 @@ pub enum NodeKind { /// An interface definition — registered but not executed. InterfaceDefinition { name: String, + /// Parent interface name for interface inheritance (specialisation). + /// An interface with `extends` narrows the parent contract; it is still + /// abstract and cannot be invoked directly. + extends: Option, + /// Captured from a legacy `implements=` attribute on an interface node. + /// The validator emits a deprecation warning when this is `Some`. + legacy_implements: Option, description: Option, /// Typed parameter declarations (empty for legacy text-only interfaces). params: Vec, diff --git a/crates/aml-core/src/executor.rs b/crates/aml-core/src/executor.rs index 8fcd80a..f5299f8 100644 --- a/crates/aml-core/src/executor.rs +++ b/crates/aml-core/src/executor.rs @@ -323,6 +323,7 @@ mod tests { .register_interface( "testing".into(), None, + None, Vec::new(), Vec::new(), None, @@ -388,6 +389,7 @@ mod tests { .register_interface( "failing".into(), None, + None, Vec::new(), Vec::new(), None, @@ -428,6 +430,7 @@ mod tests { .register_interface( "failing".into(), None, + None, Vec::new(), Vec::new(), None, @@ -493,6 +496,7 @@ mod tests { .register_interface( "failing".into(), None, + None, Vec::new(), Vec::new(), None, @@ -534,6 +538,7 @@ mod tests { .register_interface( "failing".into(), None, + None, Vec::new(), Vec::new(), None, @@ -578,6 +583,7 @@ mod tests { .register_interface( "failing".into(), None, + None, Vec::new(), Vec::new(), None, diff --git a/crates/aml-core/src/parser.rs b/crates/aml-core/src/parser.rs index 72b9655..a0165bd 100644 --- a/crates/aml-core/src/parser.rs +++ b/crates/aml-core/src/parser.rs @@ -1681,6 +1681,8 @@ fn build_node_kind(attrs: &HashMap, offset: usize) -> Result, pub description: Option, /// Typed parameter declarations (empty for legacy text-only interfaces). pub params: Vec, @@ -44,6 +47,14 @@ pub enum RegistryError { implementation: String, interface: String, }, + ExtendsUnknownInterface { + child: String, + parent: String, + }, + ExtendsInterfaceCycle { + /// The cycle path, e.g. `"A -> B -> C -> A"`. + cycle: String, + }, } impl std::fmt::Display for RegistryError { @@ -64,6 +75,15 @@ impl std::fmt::Display for RegistryError { "implementation '{implementation}' references unknown interface '{interface}'" ) } + Self::ExtendsUnknownInterface { child, parent } => { + write!( + f, + "interface '{child}' extends unknown interface '{parent}'" + ) + } + Self::ExtendsInterfaceCycle { cycle } => { + write!(f, "interface extends cycle detected: {cycle}") + } } } } @@ -90,6 +110,7 @@ impl SkillRegistry { pub fn register_interface( &mut self, name: String, + extends: Option, description: Option, params: Vec, returns: Vec, @@ -105,6 +126,7 @@ impl SkillRegistry { name.clone(), InterfaceEntry { name, + extends, description, params, returns, @@ -156,6 +178,7 @@ impl SkillRegistry { match kind { NodeKind::InterfaceDefinition { name, + extends, description, params, returns, @@ -163,8 +186,10 @@ impl SkillRegistry { writes, skill_refs, tool_constraints, + .. } => self.register_interface( name.clone(), + extends.clone(), description.clone(), params.clone(), returns.clone(), @@ -220,9 +245,12 @@ impl SkillRegistry { .unwrap_or_default() } - /// Validate that all implementations reference known interfaces. + /// Validate that all implementations reference known interfaces, and that + /// the interface `extends` hierarchy is acyclic and references known interfaces. pub fn validate(&self) -> Vec { let mut errors = Vec::new(); + + // Check implementation → interface references. for entry in self.implementations.values() { if !self.interfaces.contains_key(&entry.implements) { errors.push(RegistryError::ImplementsUnknownInterface { @@ -231,6 +259,62 @@ impl SkillRegistry { }); } } + + // Check interface → parent references and detect cycles. + for entry in self.interfaces.values() { + if let Some(ref parent) = entry.extends { + if !self.interfaces.contains_key(parent) { + errors.push(RegistryError::ExtendsUnknownInterface { + child: entry.name.clone(), + parent: parent.clone(), + }); + } + } + } + + // Cycle detection: walk the extends chain for each interface. + // We only report a cycle once (anchored at the first node in alphabetical + // order within the cycle to keep output deterministic). + let mut reported_cycles: Vec = Vec::new(); + let mut interface_names: Vec = + self.interfaces.keys().cloned().collect(); + interface_names.sort(); + + for start in &interface_names { + if reported_cycles.contains(start) { + continue; + } + let mut path: Vec = Vec::new(); + let mut current = start.clone(); + loop { + if let Some(pos) = path.iter().position(|n| n == ¤t) { + // Cycle found — extract the cyclic portion. + let cycle_nodes = &path[pos..]; + let cycle_str = { + let mut s = cycle_nodes.join(" -> "); + s.push_str(" -> "); + s.push_str(¤t); + s + }; + // Mark all nodes in the cycle so we don't re-report. + for node in cycle_nodes { + reported_cycles.push(node.clone()); + } + errors.push(RegistryError::ExtendsInterfaceCycle { cycle: cycle_str }); + break; + } + path.push(current.clone()); + match self + .interfaces + .get(¤t) + .and_then(|e| e.extends.as_deref()) + { + Some(parent) => current = parent.to_string(), + None => break, + } + } + } + errors } } @@ -244,6 +328,7 @@ mod tests { let mut reg = SkillRegistry::new(); reg.register_interface( "testing".into(), + None, Some("Run tests".into()), Vec::new(), Vec::new(), @@ -275,6 +360,7 @@ mod tests { reg.register_interface( "testing".into(), None, + None, Vec::new(), Vec::new(), None, @@ -287,6 +373,7 @@ mod tests { .register_interface( "testing".into(), None, + None, Vec::new(), Vec::new(), None, @@ -314,4 +401,134 @@ mod tests { let errors = reg.validate(); assert_eq!(errors.len(), 1); } + + #[test] + fn test_extends_unknown_parent_is_error() { + let mut reg = SkillRegistry::new(); + reg.register_interface( + "child".into(), + Some("nonexistent-parent".into()), + None, + Vec::new(), + Vec::new(), + None, + None, + Vec::new(), + Vec::new(), + ) + .unwrap(); + let errors = reg.validate(); + assert!( + errors + .iter() + .any(|e| matches!(e, RegistryError::ExtendsUnknownInterface { child, .. } if child == "child")), + "expected ExtendsUnknownInterface; got: {errors:?}" + ); + } + + #[test] + fn test_extends_self_cycle_is_error() { + let mut reg = SkillRegistry::new(); + reg.register_interface( + "self-loop".into(), + Some("self-loop".into()), + None, + Vec::new(), + Vec::new(), + None, + None, + Vec::new(), + Vec::new(), + ) + .unwrap(); + let errors = reg.validate(); + assert!( + errors + .iter() + .any(|e| matches!(e, RegistryError::ExtendsInterfaceCycle { .. })), + "expected cycle error for self-extension; got: {errors:?}" + ); + } + + #[test] + fn test_extends_two_node_cycle_is_error() { + let mut reg = SkillRegistry::new(); + reg.register_interface( + "a".into(), + Some("b".into()), + None, + Vec::new(), + Vec::new(), + None, + None, + Vec::new(), + Vec::new(), + ) + .unwrap(); + reg.register_interface( + "b".into(), + Some("a".into()), + None, + Vec::new(), + Vec::new(), + None, + None, + Vec::new(), + Vec::new(), + ) + .unwrap(); + let errors = reg.validate(); + assert!( + errors + .iter() + .any(|e| matches!(e, RegistryError::ExtendsInterfaceCycle { .. })), + "expected cycle error for A→B→A; got: {errors:?}" + ); + } + + #[test] + fn test_valid_extends_hierarchy_no_errors() { + let mut reg = SkillRegistry::new(); + // root + reg.register_interface( + "root".into(), + None, + None, + Vec::new(), + Vec::new(), + None, + None, + Vec::new(), + Vec::new(), + ) + .unwrap(); + // child extends root + reg.register_interface( + "child".into(), + Some("root".into()), + None, + Vec::new(), + Vec::new(), + None, + None, + Vec::new(), + Vec::new(), + ) + .unwrap(); + // grandchild extends child + reg.register_interface( + "grandchild".into(), + Some("child".into()), + None, + Vec::new(), + Vec::new(), + None, + None, + Vec::new(), + Vec::new(), + ) + .unwrap(); + let errors = reg.validate(); + assert!(errors.is_empty(), "unexpected errors: {errors:?}"); + } } diff --git a/crates/aml-core/src/resolver.rs b/crates/aml-core/src/resolver.rs index b7ba491..8690a55 100644 --- a/crates/aml-core/src/resolver.rs +++ b/crates/aml-core/src/resolver.rs @@ -173,6 +173,7 @@ mod tests { reg.register_interface( "testing".into(), None, + None, Vec::new(), Vec::new(), None, diff --git a/crates/aml-core/src/validator.rs b/crates/aml-core/src/validator.rs index 6a4a504..3ac1bc7 100644 --- a/crates/aml-core/src/validator.rs +++ b/crates/aml-core/src/validator.rs @@ -289,6 +289,8 @@ fn validate_kind(kind: &NodeKind, span: Span, errors: &mut Vec) } NodeKind::InterfaceDefinition { name, + extends, + legacy_implements, params, returns, reads, @@ -304,6 +306,43 @@ fn validate_kind(kind: &NodeKind, span: Span, errors: &mut Vec) severity: Severity::Error, }); } + // Validate `extends` when present. + if let Some(ext) = extends { + if ext.is_empty() { + errors.push(ValidationError { + message: "extends attribute must be a non-empty interface name".to_string(), + span: Some(span), + severity: Severity::Error, + }); + } + } + // Emit deprecation warning when `implements=` was used on an interface node. + if let Some(leg) = legacy_implements { + match extends { + Some(ext) if ext != leg => { + // Both present with different values — semantic conflict. + errors.push(ValidationError { + message: format!( + "interface definition has both extends=\"{ext}\" and \ + implements=\"{leg}\"; use only extends= on interface nodes" + ), + span: Some(span), + severity: Severity::Error, + }); + } + _ => { + // Either only implements= or they agree — emit deprecation warning. + errors.push(ValidationError { + message: format!( + "implements=\"{leg}\" on an interface definition is deprecated; \ + use extends=\"{leg}\" instead" + ), + span: Some(span), + severity: Severity::Warning, + }); + } + } + } validate_interface_declarations(params, returns, reads, writes, span, errors); validate_skill_refs(skill_refs, span, errors); validate_tool_constraints(tool_constraints, span, errors); @@ -1734,4 +1773,93 @@ mod tests { .iter() .any(|e| e.message.contains("skill ref must have a non-empty name"))); } + + // ── extends= tests ─────────────────────────────────────────────────────── + + #[test] + fn test_extends_parsed_and_stored() { + let doc = parse( + r#" +"#, + ) + .unwrap(); + let errors = validate(&doc.nodes); + assert!( + errors.is_empty(), + "unexpected errors on valid extends: {errors:?}" + ); + if let Node::Skill { + kind: NodeKind::InterfaceDefinition { name, extends, .. }, + .. + } = &doc.nodes[0] + { + assert_eq!(name, "dde-simple"); + assert_eq!(extends.as_deref(), Some("diagram-driven-execution")); + } else { + panic!("expected InterfaceDefinition"); + } + } + + #[test] + fn test_extends_empty_value_is_error() { + let doc = parse(r#""#).unwrap(); + let errors = validate(&doc.nodes); + assert!( + errors + .iter() + .any(|e| e.message.contains("extends attribute must be a non-empty") + && e.severity == Severity::Error), + "expected error for empty extends; got: {errors:?}" + ); + } + + #[test] + fn test_legacy_implements_on_interface_emits_warning() { + let doc = parse( + r#""#, + ) + .unwrap(); + let errors = validate(&doc.nodes); + assert!( + errors + .iter() + .any(|e| e.message.contains("deprecated") && e.severity == Severity::Warning), + "expected deprecation warning for implements= on interface; got: {errors:?}" + ); + } + + #[test] + fn test_extends_and_implements_conflict_is_error() { + let doc = parse( + r#""#, + ) + .unwrap(); + let errors = validate(&doc.nodes); + assert!( + errors + .iter() + .any(|e| e.message.contains("extends=") && e.severity == Severity::Error), + "expected error for extends/implements conflict; got: {errors:?}" + ); + } + + #[test] + fn test_extends_and_implements_same_value_emits_warning_only() { + let doc = parse( + r#""#, + ) + .unwrap(); + let errors = validate(&doc.nodes); + // Should warn (deprecated) but not error + assert!( + errors + .iter() + .any(|e| e.severity == Severity::Warning), + "expected deprecation warning; got: {errors:?}" + ); + assert!( + !errors.iter().any(|e| e.severity == Severity::Error), + "unexpected error when extends and implements agree; got: {errors:?}" + ); + } } diff --git a/crates/aml-core/tests/smoke_directives.rs b/crates/aml-core/tests/smoke_directives.rs index 5bb04e3..33c64d3 100644 --- a/crates/aml-core/tests/smoke_directives.rs +++ b/crates/aml-core/tests/smoke_directives.rs @@ -18,6 +18,7 @@ fn build_registry() -> SkillRegistry { let mut reg = SkillRegistry::new(); reg.register_interface( "code-review".into(), + None, Some("Review code".into()), Vec::new(), Vec::new(), @@ -39,6 +40,7 @@ fn build_registry() -> SkillRegistry { .unwrap(); reg.register_interface( "testing".into(), + None, Some("Run tests".into()), Vec::new(), Vec::new(), diff --git a/crates/aml-python/src/lib.rs b/crates/aml-python/src/lib.rs index fb45ba0..23e508b 100644 --- a/crates/aml-python/src/lib.rs +++ b/crates/aml-python/src/lib.rs @@ -123,11 +123,17 @@ impl AmlRegistry { } /// Register an interface. - #[pyo3(signature = (name, description=None))] - fn register_interface(&mut self, name: String, description: Option) -> PyResult<()> { + #[pyo3(signature = (name, extends=None, description=None))] + fn register_interface( + &mut self, + name: String, + extends: Option, + description: Option, + ) -> PyResult<()> { self.inner .register_interface( name, + extends, description, Vec::new(), Vec::new(), diff --git a/docs/adrs/README.md b/docs/adrs/README.md index c72dbb7..105a47f 100644 --- a/docs/adrs/README.md +++ b/docs/adrs/README.md @@ -15,3 +15,5 @@ This directory contains the Architecture Decision Records (ADRs) for the AML pro | [ADR-007](adr-007-independent-harness.md) | APM harness independent from AML | Accepted | | [ADR-008](adr-008-rust-pyo3.md) | Core runtime in Rust + PyO3 | Accepted | | [ADR-009](adr-009-usage-guide-skill.md) | Teach AML via usage guide skill | Accepted | +| [ADR-010](adr-010-directive-tags.md) | Directive tags for runtime instructions | Accepted | +| [ADR-011](adr-011-extends-attribute.md) | `extends=` attribute for interface inheritance | Accepted | diff --git a/docs/adrs/adr-011-extends-attribute.md b/docs/adrs/adr-011-extends-attribute.md new file mode 100644 index 0000000..750838b --- /dev/null +++ b/docs/adrs/adr-011-extends-attribute.md @@ -0,0 +1,109 @@ +# ADR-011: `extends=` Attribute for Interface Inheritance + +## Status +Accepted + +## Context + +AML has a two-level model for skill relationships: + +- `define="interface"` — an abstract contract (e.g. `diagram-driven-execution`) +- `define="implementation"` — a concrete realisation linked via `implements=` + +Multi-level capability hierarchies are common in practice. The DDE project +defines a three-level structure: + +``` +diagram-driven-execution ← top-level interface + ├── dde-simple ← sub-interface (mode contract) + └── dde-advanced ← sub-interface (mode contract) + └── (implementations) +``` + +`dde-simple` and `dde-advanced` are **interfaces**, not implementations — they +cannot be invoked directly. They narrow the parent contract with mode-specific +constraints. In UML terms this is **interface inheritance** (extends), not +**class realisation** (implements). + +Before this ADR, the only way to express the parent link was: + +```xml + +``` + +This is semantically incorrect. `implements=` means "I am a concrete executable +that satisfies this contract." Using it on a `define="interface"` node implies +the interface is directly invocable, which is wrong and confuses tooling and +human reviewers alike. + +## Decision + +Add an `extends=` attribute to `` nodes: + +```xml + +``` + +**Semantics:** +- `extends` is valid only on `define="interface"` nodes. +- It names the parent interface that this interface specialises. +- The `extends` graph is **metadata and validation only** for this release. + Implementations registered for a child interface are NOT automatically + candidates when resolving an invocation targeting the parent interface. + This avoids contract-compatibility problems without a full subtype rule-set. + +**Validation rules (new):** +- `extends=""` → hard error (empty parent name). +- `extends="X"` and `implements="Y"` (different values) on same InterfaceDef → hard error. +- `implements=` on `define="interface"` (without a matching `extends=`) → deprecation warning; will become a hard error in a future release. + +**Registry rules (new):** +- `SkillRegistry::validate()` checks that `extends` references a known interface + (`ExtendsUnknownInterface` error if not). +- Cycle detection: self-extension and transitive cycles produce + `ExtendsInterfaceCycle` errors. + +## Rationale + +- **Semantic correctness** — `extends` is the correct term for interface + specialisation in every major type system (Java, TypeScript, UML). Using + `implements` on abstract nodes is misleading. +- **Tooling safety** — A runtime or graph traversal tool that sees + `implements=` on a `define="interface"` node could incorrectly treat the + interface as directly invocable. +- **Precedent** — Every multi-level AML hierarchy (capability > sub-capability + > implementation) hits this gap. Fixing it now prevents the problem from + compounding as skills grow more compositional. +- **Metadata-only for v1** — Transitive resolution (child impls satisfy parent + invocations) requires contract-compatibility rules (parameter sets, return + types, tool constraints) that are not yet specified. Scoping `extends` to + metadata avoids premature complexity. + +## Migration Path for Existing Consumers + +Replace: +```xml + +``` +With: +```xml + +``` + +This is a non-breaking change for any parser that does not yet validate +attribute names. Existing documents using `implements=` on interface nodes +will emit a deprecation **warning** (not an error) during this release. + +## Consequences + +- The `InterfaceDefinition` AST node gains two new fields: `extends` and + `legacy_implements`. +- `SkillRegistry::InterfaceEntry` gains `extends: Option`. +- `SkillRegistry::register_interface` gains an `extends` parameter. +- `SkillRegistry::validate()` now also checks the `extends` graph for + unknown parents and cycles. +- Python bindings: `register_interface` gains an optional `extends` keyword + argument (default `None`). +- Resolution behaviour is unchanged — `extends` carries no runtime semantics. diff --git a/docs/spec/attributes.md b/docs/spec/attributes.md index 1822e73..61f2395 100644 --- a/docs/spec/attributes.md +++ b/docs/spec/attributes.md @@ -13,6 +13,7 @@ This document is the canonical reference for attribute semantics. | `name` | string | 1-128 chars, `[a-z0-9-/]` | — | All node types | Yes (on definitions and Lookup invocations) | | `interface` | string | 1-128 chars, `[a-z0-9-/]` | — | Invocation | No | | `impl` | string | 1-128 chars, `[a-z0-9-/]` | — | Invocation | No | +| `extends` | string | 1-128 chars, `[a-z0-9-/]` | — | InterfaceDef | No | | `implements` | string | 1-128 chars, `[a-z0-9-/]` | — | ImplementationDef | Yes | | `language` | string | free text, lowercase recommended | — | Invocation, ImplementationDef | No | | `framework` | string | free text, lowercase recommended | — | Invocation, ImplementationDef | No | @@ -42,6 +43,9 @@ The following combinations are **invalid** and MUST be rejected during validatio | `name` + `interface` (on Invocation) | Use one resolution mode: name OR interface | | `name` + `impl` (on Invocation) | Use one resolution mode: name OR impl | | `allow` + `deny` (on ToolDirective) | Use one constraint mode: whitelist OR blacklist | +| `extends` + `implements` (on InterfaceDef, different values) | Conflicting parent declarations | +| `extends` on ImplementationDef or Invocation | `extends` is only valid on interface definitions | +| `implements` on InterfaceDef | Deprecated — use `extends` instead (warning now, error in a future release) | ## Co-occurrence Rules @@ -82,7 +86,47 @@ Directive tags are determined by tag name, not by attributes: Directives are **not** skill nodes. They instruct the runtime about execution environment — tool constraints, session isolation, or subagent delegation. -## Name Format +## `extends=` — Interface Inheritance + +The `extends` attribute expresses **interface inheritance** (specialisation): + +```xml + +... + + +... + + +... +``` + +**Semantics:** + +- An interface with `extends` is still abstract and **cannot be invoked directly**. +- The hierarchy is **metadata and validation only** — implementations registered for + a child interface are NOT automatically candidates for the parent interface at + resolution time. Use explicit `implements=` to link an implementation to each + interface it satisfies. +- The `extends` graph MUST be acyclic. Cycles (including self-extension) are hard + errors detected by `SkillRegistry::validate()`. +- The named parent MUST be a registered interface, or `SkillRegistry::validate()` + reports `ExtendsUnknownInterface`. + +**Difference from `implements=`:** + +| Attribute | Semantics | Valid on | +|---|---|---| +| `extends` | Interface ← Interface (specialisation) | `define="interface"` | +| `implements` | Implementation ← Interface (realisation) | `define="implementation"` | + +Using `implements=` on an interface definition is **deprecated** (validation +warning). Migrate to `extends=` to express interface inheritance. This will +become a hard error in a future release. + + Names follow this pattern: `[a-z0-9]([a-z0-9-]*[a-z0-9])?(/[a-z0-9]([a-z0-9-]*[a-z0-9])?)?` diff --git a/docs/spec/grammar.ebnf b/docs/spec/grammar.ebnf index 77fcf5f..5e4131b 100644 --- a/docs/spec/grammar.ebnf +++ b/docs/spec/grammar.ebnf @@ -43,8 +43,9 @@ InterfaceDef ::= '' -InterfaceDefAttrs ::= DescriptionAttr? +InterfaceDefAttrs ::= (DescriptionAttr | ExtendsAttr)* DescriptionAttr ::= 'description' '=' QuotedString +ExtendsAttr ::= 'extends' '=' QuotedString # parent interface name (interface inheritance) InterfaceBody ::= (Text | ParamDecl | ReturnsDecl | ReadsDecl | WritesDecl | SkillRefDecl | ToolConstraintDecl)* @@ -174,6 +175,11 @@ OnFailureAttr ::= 'on-failure' '=' QuotedString # "halt", "skip", or "partial" # - without any of: name, allow, deny # - without name # - Directives inside definition bodies +# - extends + implements on InterfaceDef with different values (conflict) +# - extends with empty value on InterfaceDef +# +# These are DEPRECATED and emit a validation WARNING: +# - implements= on define="interface" nodes — use extends= instead # --- LEXICAL RULES --- QuotedString ::= '"' [^"]* '"' # double-quoted only diff --git a/docs/spec/resolution.md b/docs/spec/resolution.md index e2aa97f..6678c77 100644 --- a/docs/spec/resolution.md +++ b/docs/spec/resolution.md @@ -116,7 +116,22 @@ All resolution errors are **fatal for the node** — the node cannot execute. The executor's failure propagation rules (see `execution.md`) determine whether the error is fatal for the entire document. -## Determinism Guarantee +## Interface Inheritance and Resolution + +The `extends=` attribute on interface definitions expresses **interface +specialisation** (UML-style interface inheritance). It is **metadata-only** +with respect to resolution: + +- Implementations registered for a **child** interface are NOT automatically + considered candidates when resolving an invocation targeting the **parent**. +- To make an implementation satisfy multiple interfaces in the hierarchy, register + it explicitly with `implements=` pointing to each interface it fulfils. + +This scoping is intentional for v1 — transitive resolution would require +contract-compatibility rules (parameter sets, return types, tool constraints) +that have not yet been defined. See ADR-011 for rationale. + + Given: - The same `SkillRegistry` state (same registered interfaces, implementations, defaults) diff --git a/docs/src/content/docs/api/python-api.mdx b/docs/src/content/docs/api/python-api.mdx index 27e5011..d08f35c 100644 --- a/docs/src/content/docs/api/python-api.mdx +++ b/docs/src/content/docs/api/python-api.mdx @@ -42,7 +42,7 @@ result = execute(doc, registry) | Method | Description | |--------|-------------| | `AmlRegistry()` | Create empty registry | -| `register_interface(name, description=None)` | Register an interface | +| `register_interface(name, extends=None, description=None)` | Register an interface | | `register_implementation(name, implements, language=None, framework=None, description=None, priority=0)` | Register an implementation | | `register_from_document(doc)` | Register all definitions from a parsed document | | `validate()` | Return list of validation errors | diff --git a/docs/src/content/docs/api/rust-api.mdx b/docs/src/content/docs/api/rust-api.mdx index 7424874..76cc04e 100644 --- a/docs/src/content/docs/api/rust-api.mdx +++ b/docs/src/content/docs/api/rust-api.mdx @@ -21,7 +21,7 @@ let doc = parse(r#"Run tests"#).unwrap(); use aml_core::SkillRegistry; let mut registry = SkillRegistry::new(); -registry.register_interface("testing".into(), Some("Run tests".into())).unwrap(); +registry.register_interface("testing".into(), None, Some("Run tests".into())).unwrap(); registry.register_implementation( "pytest-impl".into(), "testing".into(),