- modules/database/schemas/nodes/exams/exam_nodes.py: neontology node classes for ExamBoard/Specification/SpecPoint/ExamPaper/Question/Part/Region (uuid_string joins to Supabase exam_questions.id / exam_response_areas.id / eb_exams.exam_code). - run/initialization/init_exam_graph.py: idempotent init — creates the shared public cc.public.exams database, 10 uniqueness constraints, and seeds AQA + AQA-PHYS-8463 (GCSE Physics) with its 8 top-level topic SpecPoints. Applied + verified on dev Neo4j (192.168.0.209, enterprise): db online, 10 constraints, AQA-[:PUBLISHES]->AQA-PHYS-8463-[:HAS_SPEC_POINT]->8 points. Full sub-point catalogue is a later data task. spec_code AQA-PHYS-8463 must match the eb_exams seed (S4-3). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
93 lines
3.2 KiB
Python
93 lines
3.2 KiB
Python
"""
|
|
Neontology node schemas for the cc.public.exams knowledge graph.
|
|
|
|
cc.public.exams is a dedicated, shared, public Neo4j database — co-primary/authoritative for the
|
|
exam knowledge graph (specs, spec-points, paper→question→part→region structure, ASSESSES links).
|
|
Supabase remains source of truth for operational data (geometry, marks, submissions); the two
|
|
layers join on shared UUIDs:
|
|
|
|
exam_questions.id <-> Question|Part.uuid_string (container -> Question, leaf -> Part)
|
|
exam_response_areas.id <-> Region.uuid_string
|
|
eb_exams.exam_code <-> ExamPaper.exam_code
|
|
eb_specifications.spec_code <-> Specification.spec_code
|
|
|
|
Ownership: created by an infra-init step; read by all authenticated API calls; written by the API
|
|
service role only (no direct client writes).
|
|
"""
|
|
from typing import ClassVar, Optional
|
|
from ..base_nodes import CCBaseNode
|
|
|
|
|
|
class ExamBaseNode(CCBaseNode):
|
|
__primarylabel__: ClassVar[str] = ''
|
|
|
|
|
|
class ExamBoardNode(ExamBaseNode):
|
|
__primarylabel__: ClassVar[str] = 'ExamBoard'
|
|
code: str # 'AQA'
|
|
name: str
|
|
|
|
|
|
class SpecificationNode(ExamBaseNode):
|
|
__primarylabel__: ClassVar[str] = 'Specification'
|
|
spec_code: str # 'AQA-PHYS-8463' (== Supabase eb_specifications.spec_code)
|
|
exam_board_code: str
|
|
subject_code: Optional[str] = None
|
|
award_code: Optional[str] = None
|
|
title: Optional[str] = None
|
|
|
|
|
|
class SpecPointNode(ExamBaseNode):
|
|
__primarylabel__: ClassVar[str] = 'SpecPoint'
|
|
ref: str # '4.1', '4.2.1'
|
|
description: str
|
|
spec_code: str
|
|
exam_board_code: str
|
|
|
|
|
|
class ExamPaperNode(ExamBaseNode):
|
|
__primarylabel__: ClassVar[str] = 'ExamPaper'
|
|
exam_code: str # == Supabase eb_exams.exam_code
|
|
spec_code: str
|
|
paper_code: Optional[str] = None
|
|
tier: Optional[str] = None
|
|
session: Optional[str] = None
|
|
title: Optional[str] = None
|
|
page_count: Optional[int] = None
|
|
|
|
|
|
class QuestionNode(ExamBaseNode): # roll-up container; uuid_string == exam_questions.id
|
|
__primarylabel__: ClassVar[str] = 'Question'
|
|
exam_code: str
|
|
label: str # '01'
|
|
order: int
|
|
max_marks: float
|
|
|
|
|
|
class PartNode(ExamBaseNode): # leaf; uuid_string == exam_questions.id
|
|
__primarylabel__: ClassVar[str] = 'Part'
|
|
exam_code: str
|
|
label: str # '01.1'
|
|
order: int
|
|
max_marks: float
|
|
answer_type: str
|
|
mark_scheme_type: str
|
|
|
|
|
|
class RegionNode(ExamBaseNode): # uuid_string == exam_response_areas.id
|
|
__primarylabel__: ClassVar[str] = 'Region'
|
|
page: int
|
|
kind: str # 'response' | 'context'
|
|
response_form: str
|
|
|
|
|
|
# Relationship reference (written by the projection / linker, not modelled as classes here):
|
|
# (:ExamBoard)-[:PUBLISHES]->(:Specification)
|
|
# (:Specification)-[:HAS_SPEC_POINT]->(:SpecPoint)
|
|
# (:Specification)-[:HAS_PAPER]->(:ExamPaper)
|
|
# (:ExamPaper)-[:HAS_QUESTION]->(:Question)
|
|
# (:Question)-[:HAS_PART]->(:Part) # nested questions allowed
|
|
# (:Part)-[:HAS_REGION]->(:Region)
|
|
# (:Part)-[:ASSESSES]->(:SpecPoint) # from exam_questions.spec_ref
|
|
# (:SpecPoint)-[:TEACHES]->(:LearningStatement) # DEFERRED cross-db bridge
|