Feature: Temporal Data Model (Revision-Aware RDF) with 3 Storage Tiers As a developer I want temporal metadata on UKO nodes with revision chains and tier-aware queries So that the ACMS can track knowledge graph evolution and enable temporal-archaeology # ---- TemporalMetadata model ---- Scenario: Create a valid TemporalMetadata with defaults Given a TemporalMetadata with valid_from now Then the temporal metadata is_current should be true And the temporal metadata valid_until should be None And the temporal metadata is_revision_of should be None And the temporal metadata valid_from should have UTC timezone Scenario: Create a TemporalMetadata with all fields Given a TemporalMetadata with valid_from now and valid_until tomorrow and is_current false and is_revision_of "uko-py:class/Foo_v1" Then the temporal metadata is_current should be false And the temporal metadata valid_until should not be None And the temporal metadata is_revision_of should be "uko-py:class/Foo_v1" Scenario: TemporalMetadata is frozen Given a TemporalMetadata with valid_from now Then modifying the temporal metadata is_current should raise an error Scenario: TemporalMetadata rejects empty is_revision_of Then creating a TemporalMetadata with empty is_revision_of should raise ValueError Scenario: TemporalMetadata rejects whitespace-only is_revision_of Then creating a TemporalMetadata with whitespace is_revision_of should raise ValueError Scenario: TemporalNode rejects zero-width Unicode node_uri Then creating a TemporalNode with zero-width Unicode node_uri should raise ValueError Scenario: TemporalMetadata rejects zero-width Unicode is_revision_of Then creating a TemporalMetadata with zero-width Unicode is_revision_of should raise ValueError Scenario: TemporalMetadata adds UTC to naive valid_from Given a TemporalMetadata with naive valid_from Then the temporal metadata valid_from should have UTC timezone Scenario: TemporalMetadata adds UTC to naive valid_until Given a TemporalMetadata with naive valid_until Then the temporal metadata valid_until should have UTC timezone Scenario: TemporalMetadata converts non-UTC aware datetime to UTC Given a TemporalMetadata with non-UTC timezone Then the temporal metadata valid_from should have UTC timezone Scenario: TemporalMetadata coerces ISO-8601 string datetime to UTC Given a TemporalMetadata with valid_from as ISO-8601 string Then the temporal metadata valid_from should have UTC timezone Scenario: TemporalMetadata rejects valid_until before valid_from Then creating a TemporalMetadata with valid_until before valid_from should raise ValueError Scenario: TemporalMetadata rejects is_current with valid_until set Then creating a TemporalMetadata with is_current true and valid_until set should raise ValueError Scenario: TemporalMetadata rejects is_current false without valid_until Then creating a TemporalMetadata with is_current false and no valid_until should raise ValueError # ---- TemporalNode model ---- Scenario: Create a valid TemporalNode Given a TemporalNode with uri "uko-py:class/Auth_v1" and resource "RES001" and path "src/auth.py" without range Then the temporal node uri should be "uko-py:class/Auth_v1" And the temporal node source_resource should be "RES001" And the temporal node source_path should be "src/auth.py" And the temporal node source_range should be None And the temporal node should have temporal metadata Scenario: TemporalNode with source_range Given a TemporalNode with uri "uko-py:class/Auth_v1" and resource "RES001" and path "src/auth.py" and range "15:1-87:0" Then the temporal node source_range should be "15:1-87:0" Scenario: TemporalNode is frozen Given a TemporalNode with uri "uko-py:class/Auth_v1" and resource "RES001" and path "src/auth.py" without range Then modifying the temporal node uri should raise an error Scenario: TemporalNode rejects empty node_uri Then creating a TemporalNode with empty node_uri should raise ValueError Scenario: TemporalNode rejects whitespace-only node_uri Then creating a TemporalNode with whitespace node_uri should raise ValueError Scenario: TemporalNode rejects empty source_resource Then creating a TemporalNode with empty source_resource should raise ValueError Scenario: TemporalNode rejects empty source_path Then creating a TemporalNode with empty source_path should raise ValueError Scenario: TemporalNode rejects empty source_range Then creating a TemporalNode with empty source_range should raise ValueError Scenario: TemporalNode rejects whitespace-only source_range Then creating a TemporalNode with whitespace source_range should raise ValueError # ---- RevisionChain model ---- Scenario: Create a RevisionChain with no predecessors Given a RevisionChain with only current_uri "uko-py:class/Auth_v1" Then the revision chain depth should be 1 And the revision chain predecessors should be empty And the revision chain all_uris should contain "uko-py:class/Auth_v1" Scenario: Create a RevisionChain with predecessors Given a RevisionChain with current_uri "uko-py:class/Auth_v3" and predecessors "uko-py:class/Auth_v1,uko-py:class/Auth_v2" Then the revision chain depth should be 3 And the revision chain predecessors should have 2 entries And the revision chain all_uris should have 3 entries Scenario: RevisionChain is frozen Given a RevisionChain with only current_uri "uko-py:class/Auth_v1" Then modifying the revision chain current_uri should raise an error Scenario: RevisionChain rejects empty current_uri Then creating a RevisionChain with empty current_uri should raise ValueError Scenario: RevisionChain rejects whitespace-only current_uri Then creating a RevisionChain with whitespace current_uri should raise ValueError Scenario: RevisionChain rejects empty predecessor URI Then creating a RevisionChain with empty predecessor should raise ValueError Scenario: RevisionChain rejects current_uri in predecessors Then creating a RevisionChain with current_uri in predecessors should raise ValueError Scenario: RevisionChain rejects duplicate predecessors Then creating a RevisionChain with duplicate predecessors should raise ValueError # ---- TierRetentionConfig model ---- Scenario: TierRetentionConfig defaults match spec Given a TierRetentionConfig with defaults Then the warm retention hours should be 24 And the cold retention days should be 90 Scenario: TierRetentionConfig is frozen Given a TierRetentionConfig with defaults Then modifying the retention warm hours should raise an error Scenario: TierRetentionConfig rejects zero warm hours Then creating a TierRetentionConfig with warm_retention_hours 0 should raise ValueError Scenario: TierRetentionConfig rejects zero cold days Then creating a TierRetentionConfig with cold_retention_days 0 should raise ValueError # ---- TierQueryResult model ---- Scenario: Create a TierQueryResult with no nodes Given a TierQueryResult for HOT tier with CURRENT scope Then the tier query result nodes should be empty And the tier query result tier should be HOT And the tier query result temporal_scope should be CURRENT Scenario: TierQueryResult is frozen Given a TierQueryResult for HOT tier with CURRENT scope Then modifying the tier query result tier should raise an error # ---- TemporalBackend protocol ---- Scenario: InMemoryTemporalBackend satisfies TemporalBackend protocol Given an InMemoryTemporalBackend instance Then it should satisfy the TemporalBackend protocol # ---- InMemoryTemporalBackend: store and get ---- Scenario: Store a node and get current Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" When I get the current node for base "uko-py:class/Auth" Then the current node uri should be "uko-py:class/Auth_v1" Scenario: Get current returns None when no match Given an InMemoryTemporalBackend instance When I get the current node for base "uko-py:class/NonExistent" Then the current node should be None Scenario: Get current does not match unrelated URI prefix Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/AuthManager_v1" for resource "RES001" at "src/auth.py" When I get the current node for base "uko-py:class/Auth" Then the current node should be None # ---- InMemoryTemporalBackend: create revision ---- Scenario: Create a revision marks old node historical Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" When I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2" Then the old node "uko-py:class/Auth_v1" should not be current And the old node "uko-py:class/Auth_v1" should have valid_until set And the new node "uko-py:class/Auth_v2" should be current And the new node "uko-py:class/Auth_v2" should have is_revision_of "uko-py:class/Auth_v1" Scenario: Create revision from nonexistent URI raises KeyError Given an InMemoryTemporalBackend instance Then creating a revision from nonexistent "uko-py:class/Missing" should raise KeyError Scenario: Create revision with blank URI raises ValueError Given an InMemoryTemporalBackend instance Then creating a revision from blank URI should raise ValueError Scenario: Create revision with same URI as current raises ValueError Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" Then creating a revision from "uko-py:class/Auth_v1" to itself should raise ValueError Scenario: Store node rejects duplicate URI Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" Then storing a duplicate node "uko-py:class/Auth_v1" should raise ValueError Scenario: Create revision rejects URI that already exists Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" And a stored temporal node "uko-py:class/Auth_v2" for resource "RES001" at "src/auth.py" Then creating a revision from "uko-py:class/Auth_v1" to existing "uko-py:class/Auth_v2" should raise ValueError Scenario: Create revision rejects wrong is_revision_of on new node Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" Then creating a revision from "uko-py:class/Auth_v1" with wrong is_revision_of should raise ValueError Scenario: Create revision rejects is_current false on new node Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" Then creating a revision from "uko-py:class/Auth_v1" with is_current false should raise ValueError # ---- InMemoryTemporalBackend: get history ---- Scenario: Get history with ALL scope returns all versions Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2" When I get the history for base "uko-py:class/Auth" with scope ALL Then the history should have 2 nodes And the history should be ordered newest first Scenario: Get history with CURRENT scope returns only current Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2" When I get the history for base "uko-py:class/Auth" with scope CURRENT Then the history should have 1 nodes Scenario: Get history with blank base URI raises ValueError Given an InMemoryTemporalBackend instance Then getting history for blank base URI should raise ValueError # ---- InMemoryTemporalBackend: revision chain ---- Scenario: Get revision chain for single node Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" When I get the revision chain for "uko-py:class/Auth_v1" Then the revision chain should have depth 1 And the revision chain current should be "uko-py:class/Auth_v1" Scenario: Get revision chain after multiple revisions Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2" And I create a revision from "uko-py:class/Auth_v2" to "uko-py:class/Auth_v3" When I get the revision chain for "uko-py:class/Auth_v3" Then the revision chain should have depth 3 And the revision chain current should be "uko-py:class/Auth_v3" And the revision chain predecessors should include "uko-py:class/Auth_v1" And the revision chain predecessors should include "uko-py:class/Auth_v2" Scenario: Get revision chain from middle node includes full chain Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2" And I create a revision from "uko-py:class/Auth_v2" to "uko-py:class/Auth_v3" When I get the revision chain for "uko-py:class/Auth_v1" Then the revision chain should have depth 3 Scenario: Get revision chain for nonexistent URI raises KeyError Given an InMemoryTemporalBackend instance Then getting revision chain for nonexistent URI should raise KeyError Scenario: Fully-historical revision chain raises ValueError Given an InMemoryTemporalBackend instance And a stored temporal node with all nodes historical Then getting revision chain for fully-historical node should raise ValueError # ---- InMemoryTemporalBackend: query by tier ---- Scenario: Hot tier query returns only current nodes Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2" When I query the HOT tier Then the tier query should return 1 node And the tier query node should be current Scenario: Cold tier query returns all temporal versions Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2" When I query the COLD tier Then the tier query should return 2 nodes # ---- InMemoryTemporalBackend: mark historical ---- Scenario: Mark a node as historical Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" When I mark "uko-py:class/Auth_v1" as historical Then the marked node should not be current And the marked node should have valid_until set Scenario: Mark historical with blank URI raises ValueError Given an InMemoryTemporalBackend instance Then marking blank URI as historical should raise ValueError Scenario: Mark historical with nonexistent URI raises KeyError Given an InMemoryTemporalBackend instance Then marking nonexistent URI as historical should raise KeyError Scenario: Mark historical on already-historical node raises ValueError Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" And I mark "uko-py:class/Auth_v1" as historical Then marking already-historical "uko-py:class/Auth_v1" should raise ValueError Scenario: Create revision on already-historical node raises ValueError Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2" Then creating a revision from historical "uko-py:class/Auth_v1" to "uko-py:class/Auth_v3" should raise ValueError Scenario: Get revision chain identifies current head explicitly Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2" When I get the revision chain for "uko-py:class/Auth_v1" Then the revision chain current_uri should be "uko-py:class/Auth_v2" And the revision chain predecessors should contain "uko-py:class/Auth_v1" Scenario: Get revision chain excludes sibling branch nodes Given an InMemoryTemporalBackend instance And a stored temporal node with branching revision chain When I get the revision chain for "uko-py:class/Branch_B" Then the revision chain should not contain "uko-py:class/Branch_C" And the revision chain current_uri should be "uko-py:class/Branch_B" # ---- InMemoryTemporalBackend: diff-coverage scenarios ---- Scenario: Get history with RECENT scope returns current and recently expired nodes Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2" When I get the history for base "uko-py:class/Auth" with scope RECENT Then the history should have 2 nodes Scenario: Revision chain with cycle guard stops traversal Given an InMemoryTemporalBackend instance And a stored temporal node with cyclic revision chain When I get the revision chain for "uko-py:class/Cycle_B" Then the revision chain should have depth 2 Scenario: Revision chain with missing predecessor stops traversal Given an InMemoryTemporalBackend instance And a stored temporal node with dangling predecessor When I get the revision chain for "uko-py:class/Dangling_B" Then the revision chain should have depth 1 Scenario: Warm tier query returns current and recently expired nodes Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2" When I query the WARM tier Then the tier query should return 2 nodes Scenario: Query by tier with RECENT scope filters within tier pool Given an InMemoryTemporalBackend instance And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py" And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2" When I query the HOT tier with scope RECENT Then the tier query should return 1 node # ---- TemporalService ---- Scenario: TemporalService store initial node Given a TemporalService with InMemoryTemporalBackend When I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py" Then the stored node should be current And the stored node should have no predecessor Scenario: TemporalService create revision Given a TemporalService with InMemoryTemporalBackend And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py" When I create a service revision from "uko-py:class/Foo_v1" to "uko-py:class/Foo_v2" for resource "RES001" at "src/foo.py" Then the service revision node should be current And the service revision node should have is_revision_of "uko-py:class/Foo_v1" Scenario: TemporalService get current Given a TemporalService with InMemoryTemporalBackend And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py" When I get the service current for "uko-py:class/Foo" Then the service current node uri should be "uko-py:class/Foo_v1" Scenario: TemporalService query by tier uses default scope Given a TemporalService with InMemoryTemporalBackend And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py" When I query HOT tier via the service Then the service tier query scope should be CURRENT Scenario: TemporalService query by tier allows scope override Given a TemporalService with InMemoryTemporalBackend And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py" When I query HOT tier via the service with scope ALL Then the service tier query scope should be ALL Scenario: TemporalService mark historical Given a TemporalService with InMemoryTemporalBackend And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py" When I mark "uko-py:class/Foo_v1" as historical via the service Then the service marked node should not be current Scenario: TemporalService rejects None backend Then creating a TemporalService with None backend should raise TypeError Scenario: TemporalService rejects blank node_uri Given a TemporalService with InMemoryTemporalBackend Then storing an initial node with blank URI via the service should raise ValueError Scenario: TemporalService rejects blank source_resource Given a TemporalService with InMemoryTemporalBackend Then storing an initial node with blank source_resource via the service should raise ValueError Scenario: TemporalService get history returns matching versions Given a TemporalService with InMemoryTemporalBackend And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py" When I create a service revision from "uko-py:class/Foo_v1" to "uko-py:class/Foo_v2" for resource "RES001" at "src/foo.py" And I get the service history for "uko-py:class/Foo" with scope ALL Then the service history should have 2 nodes Scenario: TemporalService get history with CURRENT scope returns only current Given a TemporalService with InMemoryTemporalBackend And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py" When I create a service revision from "uko-py:class/Foo_v1" to "uko-py:class/Foo_v2" for resource "RES001" at "src/foo.py" And I get the service history for "uko-py:class/Foo" with scope CURRENT Then the service history should have 1 nodes Scenario: TemporalService get history rejects blank base URI Given a TemporalService with InMemoryTemporalBackend Then getting service history for blank base URI should raise ValueError Scenario: TemporalService get revision chain returns full chain Given a TemporalService with InMemoryTemporalBackend And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py" When I create a service revision from "uko-py:class/Foo_v1" to "uko-py:class/Foo_v2" for resource "RES001" at "src/foo.py" And I get the service revision chain for "uko-py:class/Foo_v2" Then the service revision chain depth should be 2 And the service revision chain current should be "uko-py:class/Foo_v2" Scenario: TemporalService get revision chain rejects blank URI Given a TemporalService with InMemoryTemporalBackend Then getting service revision chain for blank URI should raise ValueError Scenario: TemporalService query COLD with CURRENT scope returns only current Given a TemporalService with InMemoryTemporalBackend And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py" When I create a service revision from "uko-py:class/Foo_v1" to "uko-py:class/Foo_v2" for resource "RES001" at "src/foo.py" And I query COLD tier via the service with scope CURRENT Then the service tier query should return 1 node And the service tier query node should be current Scenario: TemporalService default retention matches spec Given a TemporalService with InMemoryTemporalBackend Then the service retention warm hours should be 24 And the service retention cold days should be 90 Scenario: TemporalService WARM tier uses RECENT default scope Given a TemporalService with InMemoryTemporalBackend And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py" When I query WARM tier via the service Then the service tier query scope should be RECENT Scenario: TemporalService COLD tier uses ALL default scope Given a TemporalService with InMemoryTemporalBackend And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py" When I query COLD tier via the service Then the service tier query scope should be ALL Scenario: TemporalService create revision with same URI raises ValueError Given a TemporalService with InMemoryTemporalBackend And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py" Then creating a service revision from "uko-py:class/Foo_v1" to itself should raise ValueError Scenario: TemporalService mark historical on already-historical raises ValueError Given a TemporalService with InMemoryTemporalBackend And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py" When I mark "uko-py:class/Foo_v1" as historical via the service Then marking "uko-py:class/Foo_v1" as historical again via the service should raise ValueError # ---- BackendSet temporal typing ---- Scenario: BackendSet accepts TemporalBackend Given an InMemoryTemporalBackend instance When I create a BackendSet with the temporal backend Then the BackendSet temporal field should not be None Scenario: StrategyConfig extra deep-freezes nested dicts Given a StrategyConfig with nested extra dict Then mutating the nested extra dict should raise TypeError Scenario: StrategyConfig extra deep-freezes nested lists to tuples Given a StrategyConfig with nested list in extra Then the nested list should be frozen as a tuple # ---- Structural assertions (Phase 3.1) ---- Scenario: TemporalMetadata has all spec-required fields Then TemporalMetadata should have field "valid_from" And TemporalMetadata should have field "valid_until" And TemporalMetadata should have field "is_current" And TemporalMetadata should have field "is_revision_of" Scenario: TemporalNode has all spec-required fields Then TemporalNode should have field "node_uri" And TemporalNode should have field "source_resource" And TemporalNode should have field "source_path" And TemporalNode should have field "source_range" And TemporalNode should have field "temporal" Scenario: RevisionChain has all spec-required fields Then RevisionChain should have field "current_uri" And RevisionChain should have field "predecessors" Scenario: TierRetentionConfig has all spec-required fields Then TierRetentionConfig should have field "warm_retention_hours" And TierRetentionConfig should have field "cold_retention_days" Scenario: TierQueryResult has all spec-required fields Then TierQueryResult should have field "nodes" And TierQueryResult should have field "tier" And TierQueryResult should have field "temporal_scope"