This packet replaces the old Task 14 notes. It is aligned to Phase 14 / Task 14 in docs/plans/2026-01-25-end-state.md and to the current knowledge-graph service code in this repo.
Use this as the exact execution guide for an agent implementing Task 14.
Enable bounded, deterministic reasoning for the Knowledge Graph service with explicit profile selection and separable inferred data.
Key outcomes:
urn:sea:inferred:<snapshot_id>.Current state in this repo (verify quickly before coding):
services/knowledge-graph/src/adapters/oxigraph_adapter.py
rdflib.Graph, not pyoxigraph._graph.query_sparql and query_sparql_graph query the single graph.services/knowledge-graph/src/api/routes.py
POST /kg/sparql takes SparqlRequest { query, format }.services/knowledge-graph/src/config.py is SHACL-only.load_precomputed_artifacts can load explicit.nq + inferred.nq or merged.nq.If any of the above differs, adjust this packet to match the repo before coding.
Create:
services/knowledge-graph/src/reasoner.pyservices/knowledge-graph/src/rules/custom_ruleset_v1.rqnonerdfsowlrl-litecustom_ruleset_v1Define a small, stable interface:
1
2
3
4
5
6
7
8
9
10
11
ReasoningProfile = Literal["none", "rdfs", "owlrl-lite", "custom_ruleset_v1"]
@dataclass(frozen=True)
class ReasoningOutput:
profile: ReasoningProfile
snapshot_id: str
explicit_triples: int
inferred_triples: int
rule_set_hash: str
duration_ms: float
inferred_graph: Graph # inferred triples only
owlrl.RDFSClosure (from owlrl).owlrl API in the venv before coding (do not guess signature).custom_ruleset_v1.rq.Compute rule_set_hash as sha256 of the ruleset file contents (or a fixed string for rdfs/owlrl-lite).
Example:
rdfs: sha256("profile:rdfs")owlrl-lite: sha256("profile:owlrl-lite")custom_ruleset_v1: sha256(file_bytes)Why: Named graphs are required to keep inferred data separable.
In OxigraphAdapter:
Graph usage with a dataset that supports named graphs.
rdflib.Dataset (preferred) or ConjunctiveGraph.urn:sea:inferred:<snapshot_id>Use the existing canonical hash logic to derive a snapshot ID without mutating state:
content_hash = sha256(canonicalize(explicit_graph))snapshot_id = f"ifl:snap:{content_hash[:16]}"Snapshot model decision (resolved): metadata-only content IDs. The system does not store historical
graph versions; snapshot_id is a deterministic label for the current explicit graph only.
Do not rely on _current_snapshot_id for correctness; compute from graph content.
Each inferred triple must be annotated with:
source snapshot hashreasoning profilerule-set hashUse RDF reification (standard, deterministic):
For each inferred triple (s, p, o) in the inferred named graph:
_:stmt with:
rdf:subject srdf:predicate prdf:object osea:inferredFromSnapshot "<content_hash>"sea:reasoningProfile "<profile>"sea:ruleSetHash "<rule_set_hash>"Use SEA = Namespace("http://sea-forge.com/schema/core#") (already present in adapter).
Update SparqlRequest in services/knowledge-graph/src/api/routes.py:
reasoning_profile: str = "none"snapshot_id: str | None = None (optional; must match current computed snapshot)Update adapter methods to accept these:
query_sparql(query: str, reasoning_profile: ReasoningProfile = "none", snapshot_id: str | None = None) -> dictquery_sparql_graph(query: str, format: str = "turtle", reasoning_profile: ReasoningProfile = "none", snapshot_id: str | None = None) -> strreasoning_profile == "none" → query explicit graph only (default behavior).reasoning_profile != "none":
Reasoner.urn:sea:inferred:<snapshot_id>.Snapshot handling: if snapshot_id is provided, it must equal the current computed snapshot ID.
If it does not match, return 400 (or ignore and log a warning, but prefer 400 to avoid ambiguity).
Include inferred in response when profile != none:
1
2
3
4
5
6
7
8
9
10
"inferred": {
"enabled": true,
"profile": "rdfs",
"snapshot_id": "ifl:snap:...",
"graph_uri": "urn:sea:inferred:<snapshot_id>",
"explicit_triples": 123,
"inferred_triples": 45,
"rule_set_hash": "<sha256>",
"duration_ms": 12.3
}
If profile is none, omit inferred or set enabled=false.
inferred object in the response body.turtle),
must emit equivalent metadata via HTTP response headers:
X-Inferred-EnabledX-Inferred-ProfileX-Inferred-Snapshot-IdX-Inferred-Graph-UriX-Inferred-Explicit-TriplesX-Inferred-Inferred-TriplesX-Inferred-Rule-Set-HashX-Inferred-Duration-MsOptional (opt-in): allow embedding metadata as triples in a named metadata graph when
include_metadata=true; do not reject reasoning requests for non-JSON formats.
Update load_precomputed_artifacts to keep explicit/inferred separate:
explicit.nq + inferred.nq exist:
explicit.nq into default graph.explicit.nq.inferred.nq into named inferred graph using the current snapshot hash only after validation.merged.nq exists:
Update services/knowledge-graph/pyproject.toml:
owlrl is listed in dependencies.rdflib already exists.Create services/knowledge-graph/tests/test_reasoning.py with at least these tests:
:A rdfs:subClassOf :B:x rdf:type :A?s a :Breasoning_profile="none".:x appears with reasoning_profile="rdfs".rdfs.urn:sea:inferred:<snapshot_id> exists.sea:inferredFromSnapshotsea:reasoningProfilesea:ruleSetHashcustom_ruleset_v1.rq that produces a deterministic inference.reasoning_profile="custom_ruleset_v1".rdflib.compare.to_isomorphic).Update:
services/knowledge-graph/README.mddocs/howto/use-knowledge-graph-service.mdAdd:
reasoning_profile + snapshot_id request fieldsinferred response metadataRun:
1
pytest services/knowledge-graph/tests/test_reasoning.py -v
Optional:
1
pytest services/knowledge-graph/tests -v
Only mark Task 14 complete after the test passes then open a pull request.
Status: ✅ COMPLETED (2026-01-27)
All 8 tests pass:
Implementation summary:
src/reasoner.py with Reasoner class and 4 profilessrc/rules/custom_ruleset_v1.rq with SPARQL CONSTRUCT rulesrdflib.Dataset for named graph supportreasoning_profile and snapshot_id parametersload_precomputed_artifacts for explicit/inferred separationowlrl>=6.0.2 to dependencies