Files
cleveragents-core/features/temporal_data_model.feature
T
hamza.khyari d5f7f15215
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 14s
CI / build (pull_request) Successful in 14s
CI / quality (pull_request) Successful in 20s
CI / security (pull_request) Successful in 34s
CI / typecheck (pull_request) Successful in 39s
CI / unit_tests (pull_request) Successful in 2m50s
CI / integration_tests (pull_request) Successful in 3m22s
CI / docker (pull_request) Successful in 40s
CI / coverage (pull_request) Successful in 5m49s
CI / benchmark-regression (pull_request) Successful in 35m15s
feat(acms): implement Temporal Data Model (Revision-Aware RDF) with 3 storage tiers
Temporal metadata fields (validFrom, validUntil, isCurrent, isRevisionOf)
on UKO InformationUnit nodes enable revision chain tracking: when code
changes, old nodes are marked historical and new revision nodes are
created with back-links. Three storage tiers (hot/warm/cold) filter
nodes by temporal scope (current/recent/all) with configurable retention
(warm_retention_hours default 24h, cold_retention_days default 90d).

Includes TemporalMetadata, TemporalNode, RevisionChain, TierQueryResult,
TierRetentionConfig frozen domain models, TemporalBackend protocol,
InMemoryTemporalBackend stub, TemporalService with structlog and DI,
BackendSet.temporal typing upgrade from object|None to TemporalBackend|None.
67 Behave scenarios, 8 Robot Framework tests, ASV benchmarks, and
reference documentation.

ISSUES CLOSED: #577
2026-03-11 01:36:06 +00:00

497 lines
27 KiB
Gherkin

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"