Part 3: The Projection Pipeline¶
You have a knowledge graph with 200 drug concepts and 450 relationships. Great.
Your clinical decision support system doesn't speak "graph." It speaks "rules." You need to transform:
(Aspirin) --REFINES--> (Anticoagulant)
(Warfarin) --REFINES--> (Anticoagulant)
Into:
"When prescribing Aspirin, check if patient is on other Anticoagulants (e.g., Warfarin)"
And you need to do it systematically, for every relevant edge, in consistent styles.
The insight¶
The graph is the primary representation. Rules are just one projection of it.
Different consumers need different projections: - A clinical system needs warning-style rules - A training manual needs explanatory rules - An audit log needs formal rules
Same graph, different outputs. That's the projection pattern.
Source → Enricher → Target¶
qortex structures projections as a pipeline:
Graph ──→ [Source] ──→ [Enricher] ──→ [Target] ──→ Output
│ │ │
Extract rules Add context Serialize
(explicit + (antipattern, (YAML, JSON,
derived) rationale) Markdown)
Source: Extracts rules from the graph. Both explicit rules (stated in your data) and derived rules (generated from edges using templates).
Enricher: Adds context, antipatterns, and rationale. Can be template-based (fast, deterministic) or LLM-based (rich, contextual).
Target: Serializes to an output format. buildlog YAML, flat JSON, Markdown docs, whatever the consumer needs.
Edge rule templates¶
qortex has 30 built-in templates: 3 variants for each of the 10 relationship types.
| Variant | Style | Example |
|---|---|---|
| imperative | Direct command | "Ensure Aspirin is not combined with Warfarin" |
| conditional | When/then | "When prescribing Aspirin, verify no anticoagulant conflicts" |
| warning | Caution | "Combining Aspirin with Warfarin may increase bleeding risk" |
The templates are mechanical. Given an edge type and two concept names, you get a rule. No LLM needed.
Running a projection¶
from qortex.projectors.projection import Projection
from qortex.projectors.sources.flat import FlatRuleSource
from qortex.projectors.enrichers.template import TemplateEnricher
from qortex.projectors.targets.buildlog_seed import BuildlogSeedTarget
projection = Projection(
source=FlatRuleSource(backend=backend),
enricher=TemplateEnricher(domain="pharmacology"),
target=BuildlogSeedTarget(persona_name="drug_safety_rules"),
)
result = projection.project(domains=["pharmacology"])
# result is a dict in the universal schema format
One line to go from graph to actionable rules.
The output¶
persona: drug_safety_rules
version: 1
rules:
- rule: "Aspirin refines Anticoagulant; verify compatibility with other anticoagulants"
category: pharmacology
provenance:
id: derived:aspirin->anticoagulant:warning
domain: pharmacology
derivation: derived
confidence: 0.95
relation_type: refines
template_id: refines:warning
metadata:
source: qortex
rule_count: 1
Notice the provenance block. It tracks where the rule came from: which edge, which template, what confidence. Full audit trail.
Why buildlog isn't special¶
The BuildlogSeedTarget is just one target. You could write a MarkdownDocTarget or JSONAPITarget the same way.
buildlog happens to be the first consumer we built for. But the architecture doesn't privilege it. Any system that understands the universal schema can consume qortex projections.
What you learned¶
- The graph is the primary representation; rules are projections of it
- Projections follow Source → Enricher → Target pattern
- 30 edge rule templates generate rules mechanically from edges
- Provenance tracks the full derivation chain
- buildlog is one consumer, not a privileged one
Next¶
Part 4: The Consumer Loop: Who uses these rules, and how do you know if they work?