Skip to content

Commit a4b72d3

Browse files
committed
Complete pipeline rewrite: correct BFS, O(1) permissions, 5 checks, seed data, frontend
1 parent 08ab279 commit a4b72d3

11 files changed

Lines changed: 857 additions & 172 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SUPABASE_URL=your_supabase_project_url_here
2+
SUPABASE_KEY=your_supabase_anon_key_here

backend/filters.py

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,34 @@
1-
from datetime import datetime
1+
from datetime import datetime, timezone
22

3-
def check_dept_match(user_id, node_id, db):
4-
# Fetch user's department
5-
user = db.table("users").select("department").eq("id", user_id).single().execute()
6-
# Fetch node's department
7-
node = db.table("nodes").select("department").eq("id", node_id).single().execute()
8-
9-
return user.data["department"] == node.data["department"]
3+
def check_isolation(node, org_id):
4+
"""Check 1: Node must belong to same org"""
5+
return node.get("org_id") == org_id
106

11-
def check_temporal_validity(node_id, db):
12-
# Fetch node expiry date
13-
node = db.table("nodes").select("expiry_date").eq("id", node_id).single().execute()
14-
expiry_date = node.data["expiry_date"] # Expecting ISO format string
15-
16-
return datetime.fromisoformat(expiry_date) > datetime.now()
17-
18-
def check_compliance(user_id, node_id, db):
19-
# Fetch user clearance and node MNPI status
20-
user = db.table("users").select("clearance_level").eq("id", user_id).single().execute()
21-
node = db.table("nodes").select("is_mnpi").eq("id", node_id).single().execute()
22-
23-
# If node is MNPI, user clearance must be 'high'
24-
if node.data["is_mnpi"]:
25-
return user.data["clearance_level"] == "high"
7+
def check_compliance(node, user_compliance_clearance):
8+
"""Check 2: MNPI-tagged nodes excluded if user has no clearance"""
9+
node_tags = node.get("compliance_tags") or []
10+
for tag in node_tags:
11+
if tag not in user_compliance_clearance:
12+
return False
2613
return True
2714

28-
def is_highly_derivable(node_id, db):
29-
# Check if node is marked as general knowledge (too basic/derivable)
30-
node = db.table("nodes").select("category").eq("id", node_id).single().execute()
31-
return node.data["category"] == "general_knowledge"
15+
def check_permission(node, permission_map):
16+
"""Check 3: Node's hierarchy level must be >= user's ceiling level"""
17+
level_id = node.get("hierarchy_level_id")
18+
return permission_map.get(level_id, False)
19+
20+
def check_temporal(node):
21+
"""Check 4: Exclude superseded and expired nodes"""
22+
if node.get("status") == "SUPERSEDED":
23+
return False
24+
valid_until = node.get("valid_until")
25+
if valid_until:
26+
expiry = datetime.fromisoformat(valid_until.replace("Z", "+00:00"))
27+
if expiry < datetime.now(timezone.utc):
28+
return False
29+
return True
3230

33-
def check_user_constraints(user_id, node_id, db):
34-
# Custom rule: Check if node is on user's specific blocklist
35-
blocklist = db.table("user_constraints").select("blocked_node_id").eq("user_id", user_id).execute()
36-
blocked_ids = [row["blocked_node_id"] for row in blocklist.data]
37-
38-
return node_id not in blocked_ids
31+
def check_derivability(node):
32+
"""Check 5: Exclude nodes with derivability_score >= 0.7"""
33+
score = node.get("derivability_score", 0)
34+
return float(score) < 0.7

backend/permission_compiler.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
1-
def compile_user_permissions(user_id, supabase_client):
1+
def compile_permissions(user, hierarchy_levels):
22
"""
3-
Builds an O(1) lookup hashmap for all user permissions.
4-
Call this ONCE at the start of your request.
3+
Builds O(1) lookup hashmap compiled ONCE per session.
4+
5+
Input: user record + all hierarchy levels
6+
Output: {hierarchy_level_id: True/False} — can this user read this level?
7+
8+
Rule:
9+
- ADMIN: can read everything
10+
- HOD/EDITOR/QUALITY: can read levels where level_number >= user ceiling_level
11+
- VIEWER: can read levels where level_number >= user ceiling_level
512
"""
6-
try:
7-
response = supabase_client.table('user_permissions')\
8-
.select('node_id, access_level')\
9-
.eq('user_id', user_id)\
10-
.execute()
13+
role = user.get("role", "VIEWER")
14+
ceiling = user.get("ceiling_level", 15)
15+
16+
permission_map = {}
17+
18+
for level in hierarchy_levels:
19+
level_id = level["id"]
20+
level_number = level.get("level_number", 15)
1121

12-
# Hashmap: {node_id: access_level}
13-
return {item['node_id']: item['access_level'] for item in response.data}
14-
except Exception as e:
15-
print(f"Error compiling permissions: {e}")
16-
return {}
17-
18-
def check_permission(node_id, permission_map):
19-
# O(1) lookup
20-
return node_id in permission_map
22+
if role == "ADMIN":
23+
permission_map[level_id] = True
24+
else:
25+
# User can read this level if level_number >= their ceiling
26+
# (higher number = lower in hierarchy = less sensitive)
27+
permission_map[level_id] = level_number >= ceiling
28+
29+
return permission_map

backend/traversal.py

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,39 @@
1-
def get_reachable_nodes(entry_node_id, supabase_client):
1+
from collections import deque
2+
3+
def get_reachable_nodes(entry_level_id, supabase_client):
24
"""
3-
Performs BFS to traverse the DAG upward to find all ancestors.
4-
Optimized to handle parent-child relationships efficiently.
5+
BFS upward through hierarchy_levels DAG.
6+
Returns dict: {hierarchy_level_id: distance_from_entry}
57
"""
6-
visited = set()
7-
queue = [entry_node_id]
8-
8+
visited = {} # {level_id: distance}
9+
queue = deque()
10+
queue.append((entry_level_id, 0))
11+
12+
# First load ALL hierarchy levels into memory (one DB call)
13+
response = supabase_client.table("hierarchy_levels").select("id, parent_ids").execute()
14+
level_map = {row["id"]: row.get("parent_ids") or [] for row in response.data}
15+
916
while queue:
10-
current_id = queue.pop(0)
11-
12-
if current_id not in visited:
13-
visited.add(current_id)
14-
15-
try:
16-
# Fetch only the parent_id for the current node
17-
# Note: Adjust 'parent_id' column name if your schema uses an array
18-
response = supabase_client.table('nodes')\
19-
.select('parent_id')\
20-
.eq('id', current_id)\
21-
.execute()
22-
23-
# Check for valid data
24-
if response.data:
25-
for row in response.data:
26-
# Handle case where parent_id might be None for root nodes
27-
p_id = row.get('parent_id')
28-
if p_id and p_id not in visited:
29-
queue.append(p_id)
30-
31-
except Exception as e:
32-
print(f"Error traversing DAG at node {current_id}: {e}")
33-
continue
34-
35-
return list(visited)
17+
current_id, distance = queue.popleft()
18+
if current_id in visited:
19+
continue
20+
visited[current_id] = distance
21+
22+
# Walk UP to parents
23+
for parent_id in level_map.get(current_id, []):
24+
if parent_id not in visited:
25+
queue.append((parent_id, distance + 1))
26+
27+
return visited # {level_id: distance}
28+
3629

37-
def get_children_of_node(parent_node_id, supabase_client):
30+
def inject_zone2_nodes(supabase_client):
3831
"""
39-
Retrieves all direct children for a node.
32+
Fetch all Zone 2 (GLOBAL) nodes — injected after BFS, before 5 checks.
33+
Returns list of node dicts.
4034
"""
41-
try:
42-
response = supabase_client.table('nodes')\
43-
.select('id')\
44-
.eq('parent_id', parent_node_id)\
45-
.execute()
46-
47-
return [row['id'] for row in response.data] if response.data else []
48-
except Exception as e:
49-
print(f"Error fetching children for parent {parent_node_id}: {e}")
50-
return []
35+
response = supabase_client.table("knowledge_nodes")\
36+
.select("*")\
37+
.eq("zone", 2)\
38+
.execute()
39+
return response.data or []

0 commit comments

Comments
 (0)