name: zpd description: > Expert guide for SKUEL's Zone of Proximal Development — the pedagogical gravity well. Use when working on ZPD assessment, readiness scoring, blocking gaps, compound evidence, behavioral readiness, recommended actions, ZPD snapshots, or when asking "what should this learner do next?". TRIGGER when: implementing learning recommendations, working on Askesis intelligence, modifying ZPDService, ZPDBackend, ZPDSnapshotHandler, UserContext.zpd_assessment, DailyPlanningMixin P5 learning, or when designing features that need curriculum-aware learner positioning. allowed-tools: Read, Grep, Glob
Zone of Proximal Development (ZPD)
"The intelligence layer that makes the learning loop adaptive."
ZPD is the capstone computation on UserContext — it synthesizes curriculum graph traversal, behavioral signals, life path alignment, and compound evidence into actionable learning priorities. Without ZPD, intelligence services react to isolated domain signals. With ZPD, the system knows where the learner is in the curriculum, what they're ready for, and how it connects to their life direction.
Where ZPD Fits: The System Layers
┌────────────────────────────────────────────┐
│ 5. Semantics (coherence) │
├────────────────────────────────────────────┤
│ 4. Knowledge Graph (structural memory) │
├────────────────────────────────────────────┤
│ 3. Saved Interactions (compounding) │
├────────────────────────────────────────────┤
│ 2. ZPD + UserContext (intelligence) ◄──── │ THIS SKILL
├────────────────────────────────────────────┤
│ 1. Learning Loop (base) │
└────────────────────────────────────────────┘
ZPD is Layer 2 — the intelligence layer. It reads the knowledge graph (Layer 4) to understand curriculum structure, reads saved interactions (Layer 3) to understand learner engagement, and feeds the learning loop (Layer 1) with what to do next. The learning loop is the base; ZPD makes it adaptive.
Architecture
ZPDBackend (adapters/persistence/neo4j/zpd_backend.py)
↓ raw graph data (9-tuple: zones, prereqs, evidence, submissions)
ZPDService (core/services/zpd/zpd_service.py)
↓ readiness scoring + behavioral enrichment + recommended actions
ZPDAssessment (core/models/zpd/zpd_assessment.py)
↓ frozen dataclass snapshot consumed by:
├── UserContext.zpd_assessment (capstone of build_rich())
├── DailyPlanningMixin P5 (learning priority)
├── LearningIntelligenceMixin (optimal next path steps)
└── AskesisService (pedagogical scaffolding)
Service Layer (business logic)
File: core/services/zpd/zpd_service.py
class ZPDService:
def __init__(
self,
backend: ZPDBackendOperations,
choices_intelligence: ChoicesIntelligenceService | None = None,
habits_intelligence: HabitsIntelligenceService | None = None,
) -> None: ...
# PUBLIC API (ZPDOperations protocol)
async def assess_zone(
self, user_uid: UserUID, context: UserContext | None = None
) -> Result[ZPDAssessment]: ...
async def get_proximal_ku_uids(self, user_uid: UserUID) -> Result[list[str]]: ...
async def get_readiness_score(self, user_uid: UserUID, ku_uid: str) -> Result[float]: ...
Key private methods:
_compute_readiness_scores()— fraction of prerequisites met per proximal KU_compute_behavioral_readiness()— choices (65%) + habits (35%) signals_build_zone_evidence()— compound mastery tracking per current-zone KU_build_recommended_actions()— three action types: unblock, learn, reinforce
Not the same as
PrerequisiteChecker. ZPD's_compute_readiness_scoresis a per-proximal-KU zone assessment (curriculum-graph positioning) and is deliberately separate fromPrerequisiteChecker.check_prerequisites(the per-task/goal one-off readiness + learning-requirements lens — see PREREQUISITE_CHECKER_PATTERN.md). They share the 0.7 mastery threshold and the sameknowledge_masterydata, but serve different use cases — the #254/#255 mastery-gap consolidation did not fold ZPD into it.
Backend Layer (Cypher queries)
File: adapters/persistence/neo4j/zpd_backend.py
Single-roundtrip _ZONE_QUERY — 6 steps in one Cypher query:
- Current zone — KUs via APPLIES_KNOWLEDGE (tasks, journals) + REINFORCES_KNOWLEDGE (habits). Returns per-source lists for compound evidence.
- Proximal zone — adjacent via PREREQUISITE_FOR, COMPLEMENTARY_TO, LP ORGANIZES. Excludes already-engaged KUs.
- Prerequisite graph — total vs met prerequisites per proximal KU (readiness scoring).
- Engaged Learning Paths — LPs the user is partially traversing.
- Blocking gaps — prerequisite KUs not met that gate proximal KUs.
- Submission scores — via
FULFILLS_EXERCISE -> APPLIES_KNOWLEDGEjoin.
Guard: get_ku_count() — ZPD requires 3+ KUs in the curriculum graph. Below threshold, returns empty assessment.
Assessment Model
File: core/models/zpd/zpd_assessment.py
@dataclass(frozen=True)
class ZoneEvidence:
ku_uid: str
submission_count: int = 0
best_submission_score: float = 0.0
habit_reinforcement: bool = False
task_application: bool = False
entry_application: bool = False # (UserEntry)-[:APPLIES_KNOWLEDGE] — ADR-069
@property
def signal_count(self) -> int: ... # count of active signal types (0-4)
@property
def is_confirmed(self) -> bool: ... # True when 2+ signal types
@dataclass(frozen=True)
class ZPDAction:
entity_uid: EntityUID
entity_type: str # "path_step"
action_type: str # "learn", "reinforce", "unblock"
priority: float # 0.0-1.0
rationale: str
ku_uid: str | None = None
@dataclass(frozen=True)
class ZPDAssessment:
current_zone: list[str] # KU UIDs engaged
proximal_zone: list[str] # KU UIDs structurally adjacent, not engaged
engaged_paths: list[str] # LP UIDs partially traversed
readiness_scores: dict[str, float] # proximal KU → readiness 0.0-1.0
blocking_gaps: list[str] # prerequisite KU UIDs not met
behavioral_readiness: float # 0.0-1.0 aggregate
# Life path integration
life_path_alignment: float = 0.0
life_path_uid: str | None = None
# Recommended actions — three types
recommended_actions: tuple[ZPDAction, ...] = ()
# Zone evidence tracking (compound mastery)
zone_evidence: dict[str, ZoneEvidence] = field(default_factory=dict)
# Submission-derived scores
submission_scores: dict[str, float] = field(default_factory=dict)
def is_empty(self) -> bool: ...
def top_proximal_ku_uids(self, n=5) -> list[str]: ...
def top_recommended_actions(self, n=5) -> list[ZPDAction]: ...
def confirmed_zone_uids(self) -> list[str]: ...
Protocols
File: core/ports/zpd_protocols.py
@runtime_checkable
class ZPDBackendOperations(Protocol):
async def get_ku_count(self) -> int: ...
async def get_zone_data(self, user_uid: UserUID) -> Result[tuple[...]]: ...
@runtime_checkable
class ZPDOperations(Protocol):
async def assess_zone(self, user_uid: UserUID, context: UserContext | None = None) -> Result[ZPDAssessment]: ...
async def get_proximal_ku_uids(self, user_uid: UserUID) -> Result[list[str]]: ...
async def get_readiness_score(self, user_uid: UserUID, ku_uid: str) -> Result[float]: ...
Three Action Types
ZPD generates recommended actions that reflect the learning loop:
| Action | Priority | When | Loop Role |
|---|---|---|---|
| unblock | ZPDWeights.UNBLOCK_PRIORITY (0.9) |
Prerequisite KU blocks proximal KUs | Removes structural barriers |
| learn | readiness × LEARN_READINESS + alignment × LEARN_ALIGNMENT + behavior × LEARN_BEHAVIORAL |
Proximal KU ready for engagement | Advances the zone |
| reinforce | (1 - signal_strength) × REINFORCE_GAP + alignment × REINFORCE_ALIGNMENT + behavior × REINFORCE_BEHAVIORAL |
Current-zone KU has < 2 signal types | Compounds mastery |
Unblock actions are always highest priority — a blocking gap is the most leveraged learning move because it unlocks the most new territory.
Learn actions advance the zone by engaging new KUs. Priority favors KUs where prerequisites are met and that align with the life path.
Reinforce actions target current-zone KUs with thin evidence. A KU with only 1 signal type (e.g., just a task) needs a second signal type (e.g., a journal reflection) to reach compound-confirmed status.
Behavioral Readiness
Behavioral readiness enriches zone assessment with signals about how the learner is living:
Choices Intelligence (ZPDWeights.BEHAVIORAL_CHOICES_WEIGHT = 65%)
choices_score = (
adherence * ZPDWeights.CHOICES_ADHERENCE # principle adherence
+ consistency * ZPDWeights.CHOICES_CONSISTENCY # decision consistency
+ quality_rate * ZPDWeights.CHOICES_QUALITY # high-quality decision rate
- conflict_penalty # min(CHOICES_CONFLICT_PENALTY_CAP, conflicts * CHOICES_CONFLICT_PENALTY_PER)
)
Habits Intelligence (ZPDWeights.BEHAVIORAL_HABITS_WEIGHT = 35%)
habits_score = mean_reinforcement_strength - at_risk_penalty
# mean_reinforcement_strength: mean of habit strengths overlapping current_zone
# at_risk_penalty: min(HABITS_AT_RISK_PENALTY_CAP, at_risk_count * HABITS_AT_RISK_PENALTY_PER)
Combined
if both available: behavioral = choices * ZPDWeights.BEHAVIORAL_CHOICES_WEIGHT + habits * ZPDWeights.BEHAVIORAL_HABITS_WEIGHT
if only choices: behavioral = choices_score
if only habits: behavioral = habits_score
if neither: behavioral = ZPDWeights.BEHAVIORAL_NEUTRAL_DEFAULT # 0.5, neutral (CORE tier)
Event-Driven Snapshot Persistence
File: core/services/zpd/zpd_event_handler.py
ZPDSnapshotHandler takes ZPD snapshots on pedagogically significant events:
| Event | Trigger | Signal |
|---|---|---|
SubmissionApproved |
Student work validated | Mastery signal |
ReportSubmitted |
Teacher feedback delivered | Feedback loop closed |
KnowledgeMastered |
KU mastery confirmed | Zone shift |
PathStepCompleted |
Curriculum progress | LP advancement |
LearningPathProgressUpdated |
LP milestone | Path progression |
Snapshot Backend: adapters/persistence/neo4j/zpd_snapshot_backend.py
- Single
:ZPDHistorynode per user (MVP — no history array) - Fields:
latest_assessed_at, zone counts, behavioral readiness, life path alignment, trigger event, snapshot count - MERGE on
(User)-[:HAS_ZPD_HISTORY]->(ZPDHistory)
UserContext Integration
ZPD is the capstone computation of build_rich():
# In UserContextBuilder.build_rich():
# ... all other context fields populated first (~250 fields) ...
# ZPD runs LAST — it synthesizes everything above
if zpd_service is not None:
assessment = await zpd_service.assess_zone(user_uid, context=context)
context.zpd_assessment = assessment.value
Field: UserContext.zpd_assessment: ZPDAssessment | None
Noneon standardbuild()or CORE tier- Populated only on
build_rich()with FULL intelligence tier
Consumers:
DailyPlanningMixin.get_ready_to_work_on_today()— P5 Learning: usesrecommended_actionsLearningIntelligenceMixin.get_optimal_next_path_steps()— primary ranking signalAskesisService— reads assessment for scaffolding decisions
Implementation Checklist
When modifying ZPD, verify:
- Backend query returns all 9 tuple elements — current_zone, proximal_zone, engaged_paths, prereq_data, blocking_gaps, task_engaged, journal_engaged, habit_engaged, submission_data
- No APOC in queries — SKUEL001 compliance; use pure Cypher for set operations
- Guard condition preserved —
get_ku_count() < 3returns empty assessment - Behavioral readiness defaults to 0.5 when intelligence services unavailable
- All three action types generated — unblock, learn, reinforce
- ZPDAssessment is frozen — immutable snapshot, no mutations after creation
- Event handler wired — ZPD snapshot events subscribed in
_create_intelligence_hub()inservices_bootstrap/_intelligence_hub.py - Protocol compliance —
ZPDOperationsandZPDBackendOperationsboth@runtime_checkable
Anti-Patterns
Don't query the graph from ZPDService
# WRONG — ZPDService should not contain Cypher
class ZPDService:
async def assess_zone(self):
records = await self._driver.execute_query("MATCH ...") # No!
# CORRECT — delegate to ZPDBackend
class ZPDService:
async def assess_zone(self):
graph_result = await self._backend.get_zone_data(user_uid)
Don't use ZPD on CORE tier
# WRONG — ZPD is FULL tier only
context = await builder.build(user_uid) # standard build
actions = context.zpd_assessment.top_recommended_actions() # None!
# CORRECT — check tier
if context.zpd_assessment is not None:
actions = context.zpd_assessment.top_recommended_actions()
Don't skip compound evidence
# WRONG — treating any engagement as "mastered"
if ku_uid in assessment.current_zone:
mark_as_mastered(ku_uid) # One signal ≠ mastery
# CORRECT — require compound evidence
evidence = assessment.zone_evidence.get(ku_uid)
if evidence and evidence.is_confirmed:
mark_as_mastered(ku_uid) # 2+ signal types
Don't ignore blocking gaps
# WRONG — recommending proximal KUs without checking gaps
for ku_uid in assessment.proximal_zone:
recommend(ku_uid)
# CORRECT — unblock actions first, then learn
actions = assessment.top_recommended_actions()
# Actions are pre-sorted: unblock (0.9) > learn > reinforce
Key Source Files
| File | Purpose |
|---|---|
core/services/zpd/zpd_service.py |
Business logic — readiness, behavioral enrichment, actions |
core/services/zpd/zpd_event_handler.py |
Event-driven snapshot persistence |
core/models/zpd/zpd_assessment.py |
ZPDAssessment, ZoneEvidence, ZPDAction frozen dataclasses |
core/ports/zpd_protocols.py |
ZPDOperations + ZPDBackendOperations protocols |
adapters/persistence/neo4j/zpd_backend.py |
Cypher zone traversal query |
adapters/persistence/neo4j/zpd_snapshot_backend.py |
ZPDHistory node persistence |
docs/user-guides/zpd.md |
User-facing ZPD guide |
docs/roadmap/zpd-service-architecture.md |
Design rationale |
docs/architecture/ASKESIS_PEDAGOGICAL_ARCHITECTURE.md |
Pedagogical vision |
Related Skills
- learning-loop — The base layer ZPD serves
- user-context-intelligence — Consumes ZPDAssessment
- neo4j-cypher-patterns — Graph query patterns used in ZPDBackend
- base-analytics-service — Analytics that read zone data
- prompt-templates — Askesis templates that consume ZPD context