Feature: UKO Analyzer Plugin Framework
  As a developer
  I want pluggable domain analyzers that produce UKO triples from resources
  So that the ACMS can index code and documents into the UKO knowledge graph

  # ---- UKOTriple model ----

  Scenario: Create a valid UKOTriple with object_uri
    Given a UKOTriple with subject "uko://code/module/foo" predicate "rdf:type" and object_uri "uko-code:Module"
    Then the triple subject_uri should be "uko://code/module/foo"
    And the triple predicate should be "rdf:type"
    And the triple object_uri should be "uko-code:Module"
    And the triple confidence should be 1.0

  Scenario: Create a UKOTriple with object_value
    Given a UKOTriple with subject "uko://code/class/foo/Bar" predicate "rdfs:label" and object_value "Bar"
    Then the triple object_value should be "Bar"

  Scenario: UKOTriple rejects empty subject_uri
    Then creating a UKOTriple with empty subject_uri should raise ValueError

  Scenario: UKOTriple rejects empty predicate
    Then creating a UKOTriple with empty predicate should raise ValueError

  Scenario: UKOTriple rejects confidence above 1.0
    Then creating a UKOTriple with confidence 1.5 should raise ValueError

  Scenario: UKOTriple rejects confidence below 0.0
    Then creating a UKOTriple with confidence -0.1 should raise ValueError

  Scenario: UKOTriple is immutable
    Given a UKOTriple with subject "uko://a" predicate "rdf:type" and object_uri "uko:B"
    Then modifying the triple confidence should raise an error

  # ---- AnalyzerRegistry ----

  Scenario: Register and look up an analyzer by extension
    Given an AnalyzerRegistry with a PythonAnalyzer registered
    When I look up the analyzer for extension ".py"
    Then the analyzer domain should be "python"

  Scenario: Registry returns None for unregistered extension
    Given an empty AnalyzerRegistry
    When I look up the analyzer for extension ".rs"
    Then the lookup result should be None

  Scenario: Registry rejects non-protocol objects
    Given an empty AnalyzerRegistry
    Then registering a plain object should raise TypeError

  Scenario: Registry rejects analyzer with no extensions
    Given an empty AnalyzerRegistry
    Then registering an analyzer with no extensions should raise ValueError

  Scenario: Registry lists all extensions
    Given an AnalyzerRegistry with both Python and Markdown analyzers
    Then the registry should list extensions including ".py" and ".md"

  Scenario: Registry counts analyzers
    Given an AnalyzerRegistry with both Python and Markdown analyzers
    Then the registry length should be 2

  Scenario: Registry first-registered-wins for duplicate extensions
    Given an AnalyzerRegistry with a PythonAnalyzer registered
    When I register another analyzer for ".py"
    And I look up the analyzer for extension ".py"
    Then the analyzer domain should be "python"

  # ---- PythonAnalyzer ----

  Scenario: PythonAnalyzer satisfies AnalyzerProtocol
    Given a PythonAnalyzer instance
    Then it should satisfy the AnalyzerProtocol

  Scenario: PythonAnalyzer supports .py and .pyi extensions
    Given a PythonAnalyzer instance
    Then its supported extensions should include ".py" and ".pyi"

  Scenario: PythonAnalyzer extracts module declaration
    Given a PythonAnalyzer instance
    When I analyze Python content "x = 1\n" with resource URI "src/foo.py"
    Then the triples should contain a triple with predicate "rdf:type" and object_uri "uko-code:Module"

  Scenario: PythonAnalyzer extracts class definition
    Given a PythonAnalyzer instance
    When I analyze Python content with a class "MyClass"
    Then the triples should contain a triple with predicate "rdf:type" and object_uri "uko-py:Class"
    And the triples should contain a triple with predicate "rdfs:label" and object_value "MyClass"

  Scenario: PythonAnalyzer extracts class containment
    Given a PythonAnalyzer instance
    When I analyze Python content with a class "MyClass"
    Then the triples should contain a triple with predicate "uko:contains" linking module to class

  Scenario: PythonAnalyzer extracts function definition
    Given a PythonAnalyzer instance
    When I analyze Python content with a function "do_work"
    Then the triples should contain a triple with predicate "rdf:type" and object_uri "uko-py:Function"
    And the triples should contain a triple with predicate "rdfs:label" and object_value "do_work"

  Scenario: PythonAnalyzer extracts method inside class
    Given a PythonAnalyzer instance
    When I analyze Python content with class "Cls" and method "run"
    Then the triples should contain a method triple with predicate "rdfs:label" and object_value "run"

  Scenario: PythonAnalyzer extracts import statement
    Given a PythonAnalyzer instance
    When I analyze Python content with import "os"
    Then the triples should contain a triple with predicate "uko:references" referencing "os"

  Scenario: PythonAnalyzer extracts from-import statement
    Given a PythonAnalyzer instance
    When I analyze Python content with from-import "pathlib"
    Then the triples should contain a triple with predicate "uko:references" referencing "pathlib"

  Scenario: PythonAnalyzer extracts module docstring
    Given a PythonAnalyzer instance
    When I analyze Python content with docstring "Module docs."
    Then the triples should contain a triple with predicate "uko-doc:hasDocstring" and object_value "Module docs."

  Scenario: PythonAnalyzer extracts class docstring
    Given a PythonAnalyzer instance
    When I analyze Python content with class docstring
    Then the triples should contain a docstring triple for the class

  Scenario: PythonAnalyzer handles syntax errors gracefully
    Given a PythonAnalyzer instance
    When I analyze malformed Python content
    Then the analyzer result should be an empty triple list

  Scenario: PythonAnalyzer rejects empty content
    Given a PythonAnalyzer instance
    Then analyzing empty content should raise ValueError

  Scenario: PythonAnalyzer rejects empty resource_uri
    Given a PythonAnalyzer instance
    Then analyzing with empty resource_uri should raise ValueError

  Scenario: PythonAnalyzer extracts base class inheritance
    Given a PythonAnalyzer instance
    When I analyze Python content with a class inheriting from "BaseClass"
    Then the triples should contain a triple with predicate "uko-py:inheritsFrom" and object_value "BaseClass"

  # ---- MarkdownAnalyzer ----

  Scenario: MarkdownAnalyzer satisfies AnalyzerProtocol
    Given a MarkdownAnalyzer instance
    Then it should satisfy the AnalyzerProtocol

  Scenario: MarkdownAnalyzer supports .md and .markdown extensions
    Given a MarkdownAnalyzer instance
    Then its supported extensions should include ".md" and ".markdown"

  Scenario: MarkdownAnalyzer extracts document declaration
    Given a MarkdownAnalyzer instance
    When I analyze Markdown content "# Hello\nWorld\n" with resource URI "docs/readme.md"
    Then the triples should contain a triple with predicate "rdf:type" and object_uri "uko-doc:Document"

  Scenario: MarkdownAnalyzer extracts headings as sections
    Given a MarkdownAnalyzer instance
    When I analyze Markdown content with heading "Introduction"
    Then the triples should contain a triple with predicate "rdf:type" and object_uri "uko-doc:Section"
    And the triples should contain a triple with predicate "uko-doc:headingText" and object_value "Introduction"
    And the triples should contain a triple with predicate "uko-doc:headingLevel" and object_value "1"

  Scenario: MarkdownAnalyzer extracts nested heading hierarchy
    Given a MarkdownAnalyzer instance
    When I analyze Markdown with nested headings
    Then the triples should contain section containment for nested sections

  Scenario: MarkdownAnalyzer extracts fenced code blocks
    Given a MarkdownAnalyzer instance
    When I analyze Markdown content with a Python code block
    Then the triples should contain a triple with predicate "rdf:type" and object_uri "uko-code:CodeBlock"
    And the triples should contain a triple with predicate "uko-code:language" and object_value "python"

  Scenario: MarkdownAnalyzer extracts links
    Given a MarkdownAnalyzer instance
    When I analyze Markdown content with a link to "https://example.com"
    Then the triples should contain a triple with predicate "uko:references" and object_value "https://example.com"

  Scenario: MarkdownAnalyzer rejects empty content
    Given a MarkdownAnalyzer instance
    Then analyzing empty markdown content should raise ValueError

  Scenario: MarkdownAnalyzer rejects empty resource_uri
    Given a MarkdownAnalyzer instance
    Then analyzing markdown with empty resource_uri should raise ValueError

  Scenario: MarkdownAnalyzer handles bare code fences
    Given a MarkdownAnalyzer instance
    When I analyze Markdown content with a bare code fence
    Then the triples should contain a code block triple

  # ---- Coverage gap: AnalyzerRegistry.get_for_extension("") ----

  Scenario: Registry returns None for empty extension
    Given an AnalyzerRegistry with a PythonAnalyzer registered
    When I look up the analyzer for an empty extension
    Then the lookup result should be None

  Scenario: Registry list_analyzers returns all registered analyzers
    Given an AnalyzerRegistry with both Python and Markdown analyzers
    Then the registry list_analyzers should return 2 analyzers

  # ---- Coverage gap: PythonAnalyzer function docstring ----

  Scenario: PythonAnalyzer extracts function docstring
    Given a PythonAnalyzer instance
    When I analyze Python content with a function with docstring
    Then the triples should contain a triple with predicate "uko-doc:hasDocstring" for a function

  # ---- Coverage gap: PythonAnalyzer method docstring ----

  Scenario: PythonAnalyzer extracts method docstring
    Given a PythonAnalyzer instance
    When I analyze Python content with class and method with docstring
    Then the triples should contain a triple with predicate "uko-doc:hasDocstring" for a method

  # ---- Coverage gap: PythonAnalyzer dotted base class (ast.Attribute) ----

  Scenario: PythonAnalyzer extracts dotted base class inheritance
    Given a PythonAnalyzer instance
    When I analyze Python content with a class inheriting from "collections.abc.Mapping"
    Then the triples should contain a triple with predicate "uko-py:inheritsFrom" and object_value "collections.abc.Mapping"

  # ---- Coverage gap: MarkdownAnalyzer domain property ----

  Scenario: MarkdownAnalyzer domain is "markdown"
    Given a MarkdownAnalyzer instance
    Then the analyzer domain property should be "markdown"

  # ---- Coverage gap: MarkdownAnalyzer sibling heading pops stack ----

  Scenario: MarkdownAnalyzer handles sibling headings at same level
    Given a MarkdownAnalyzer instance
    When I analyze Markdown with sibling headings at the same level
    Then each sibling section should be contained by the document
