Learning graph additions.
This commit is contained in:
parent
ffcdc9a88d
commit
6c04e1e7bf
60
README.md
60
README.md
|
|
@ -1,49 +1,27 @@
|
||||||

|
|
||||||
|
|
||||||
# Didactopus
|
# Didactopus
|
||||||
|
|
||||||
**Didactopus** is a local-first AI-assisted autodidactic mastery platform designed to help motivated learners achieve genuine mastery through Socratic mentoring, structured practice, project work, verification, and competency-based evaluation.
|
**Didactopus** is a local-first AI-assisted autodidactic mastery platform.
|
||||||
|
|
||||||
**Tagline:** *Many arms, one goal — mastery.*
|
This revision moves the system from simple concept merging toward a true **merged learning graph**.
|
||||||
|
|
||||||
## This revision adds
|
## Added in this revision
|
||||||
|
|
||||||
- dependency and compatibility checks for domain packs
|
- merged learning graph builder
|
||||||
- version-range validation against the Didactopus core version
|
- combined prerequisite DAG across packs
|
||||||
- local dependency resolution across installed packs
|
- merged roadmap stage catalog
|
||||||
- richer pack manifests
|
- merged project catalog
|
||||||
- repository artwork under `artwork/`
|
- namespaced concept keys (`pack::concept`)
|
||||||
- tests for dependency and compatibility behavior
|
- optional concept override support in `pack.yaml`
|
||||||
|
- learner-facing roadmap generation from merged packs
|
||||||
|
- CLI reporting for merged graph statistics
|
||||||
|
- tests for merged learning graph behavior
|
||||||
|
|
||||||
## Domain packs
|
## Why this matters
|
||||||
|
|
||||||
Didactopus supports portable, versioned **domain packs** that can contain:
|
Didactopus can now use multiple compatible packs to build one composite domain model rather than treating packs as isolated fragments.
|
||||||
|
|
||||||
- concept maps
|
That enables:
|
||||||
- roadmap templates
|
- foundations + extension pack composition
|
||||||
- project blueprints
|
- unified learner roadmaps
|
||||||
- rubrics
|
- shared project catalogs
|
||||||
- benchmark tasks
|
- safe coexistence of overlapping concept IDs via namespacing
|
||||||
- resource guides
|
|
||||||
|
|
||||||
Packs can depend on other packs, enabling layered curricula and reusable foundations.
|
|
||||||
|
|
||||||
## Artwork
|
|
||||||
|
|
||||||
The repository includes whimsical project art at:
|
|
||||||
|
|
||||||
- `artwork/didactopus-mascot.png`
|
|
||||||
|
|
||||||
Suggested future additions:
|
|
||||||
- `artwork/didactopus-banner.png`
|
|
||||||
- `artwork/didactopus-logo.png`
|
|
||||||
|
|
||||||
## Quick start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m venv .venv
|
|
||||||
source .venv/bin/activate
|
|
||||||
pip install -e .[dev]
|
|
||||||
python -m didactopus.main --domain "statistics" --goal "practical mastery"
|
|
||||||
pytest
|
|
||||||
```
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Merged Learning Graph
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The merged learning graph is the first learner-facing composite model built from multiple domain packs.
|
||||||
|
|
||||||
|
## Features in this revision
|
||||||
|
|
||||||
|
- namespaced concept keys: `pack-name::concept-id`
|
||||||
|
- merged prerequisite DAG
|
||||||
|
- stage catalog across packs
|
||||||
|
- project catalog across packs
|
||||||
|
- optional overrides for previously defined concepts
|
||||||
|
|
||||||
|
## Override model
|
||||||
|
|
||||||
|
A pack manifest may include:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
overrides:
|
||||||
|
- foundations-statistics::descriptive-statistics
|
||||||
|
```
|
||||||
|
|
||||||
|
If a pack defines concept `descriptive-statistics` and lists the namespaced target above, it may replace that concept in the merged graph.
|
||||||
|
|
||||||
|
That is intentionally explicit and conservative.
|
||||||
|
|
||||||
|
## Future work
|
||||||
|
|
||||||
|
- merged rubric graph
|
||||||
|
- stage dependency inference
|
||||||
|
- learner-specific subgraph extraction
|
||||||
|
- adaptive sequencing from the merged DAG
|
||||||
|
|
@ -4,3 +4,9 @@ concepts:
|
||||||
prerequisites: []
|
prerequisites: []
|
||||||
mastery_signals:
|
mastery_signals:
|
||||||
- explain a prior distribution
|
- explain a prior distribution
|
||||||
|
- id: posterior
|
||||||
|
title: Posterior
|
||||||
|
prerequisites:
|
||||||
|
- prior
|
||||||
|
mastery_signals:
|
||||||
|
- explain updating beliefs
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,4 @@ dependencies:
|
||||||
- name: foundations-statistics
|
- name: foundations-statistics
|
||||||
min_version: 1.0.0
|
min_version: 1.0.0
|
||||||
max_version: 1.9.99
|
max_version: 1.9.99
|
||||||
|
overrides: []
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,6 @@ stages:
|
||||||
title: Bayesian Basics
|
title: Bayesian Basics
|
||||||
concepts:
|
concepts:
|
||||||
- prior
|
- prior
|
||||||
|
- posterior
|
||||||
checkpoint:
|
checkpoint:
|
||||||
- compare priors
|
- compare priors and posteriors
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,9 @@ concepts:
|
||||||
prerequisites: []
|
prerequisites: []
|
||||||
mastery_signals:
|
mastery_signals:
|
||||||
- explain central tendency
|
- explain central tendency
|
||||||
|
- id: probability-basics
|
||||||
|
title: Probability Basics
|
||||||
|
prerequisites:
|
||||||
|
- descriptive-statistics
|
||||||
|
mastery_signals:
|
||||||
|
- explain event probability
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,4 @@ description: Shared foundations for statistics learning.
|
||||||
author: Wesley R. Elsberry
|
author: Wesley R. Elsberry
|
||||||
license: MIT
|
license: MIT
|
||||||
dependencies: []
|
dependencies: []
|
||||||
|
overrides: []
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,6 @@ stages:
|
||||||
title: Foundations
|
title: Foundations
|
||||||
concepts:
|
concepts:
|
||||||
- descriptive-statistics
|
- descriptive-statistics
|
||||||
|
- probability-basics
|
||||||
checkpoint:
|
checkpoint:
|
||||||
- summarize a dataset
|
- summarize a dataset
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
concepts:
|
||||||
|
- id: descriptive-statistics
|
||||||
|
title: Descriptive Statistics (Overridden)
|
||||||
|
prerequisites: []
|
||||||
|
mastery_signals:
|
||||||
|
- explain central tendency in an updated way
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
name: override-foundations
|
||||||
|
display_name: Override Foundations Pack
|
||||||
|
version: 0.1.0
|
||||||
|
schema_version: "1"
|
||||||
|
didactopus_min_version: 0.1.0
|
||||||
|
didactopus_max_version: 0.9.99
|
||||||
|
description: Demonstrates explicit concept override.
|
||||||
|
author: Wesley R. Elsberry
|
||||||
|
license: MIT
|
||||||
|
dependencies:
|
||||||
|
- name: foundations-statistics
|
||||||
|
min_version: 1.0.0
|
||||||
|
max_version: 2.0.0
|
||||||
|
overrides:
|
||||||
|
- foundations-statistics::descriptive-statistics
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
projects:
|
||||||
|
- id: override-project
|
||||||
|
title: Override Project
|
||||||
|
difficulty: intermediate
|
||||||
|
prerequisites:
|
||||||
|
- descriptive-statistics
|
||||||
|
deliverables:
|
||||||
|
- comparison memo
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
stages:
|
||||||
|
- id: stage-1
|
||||||
|
title: Override Stage
|
||||||
|
concepts:
|
||||||
|
- descriptive-statistics
|
||||||
|
checkpoint:
|
||||||
|
- compare revised explanations
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
rubrics:
|
||||||
|
- id: override-rubric
|
||||||
|
title: Override Rubric
|
||||||
|
criteria:
|
||||||
|
- correctness
|
||||||
|
- comparison
|
||||||
|
|
@ -8,6 +8,8 @@ __all__ = [
|
||||||
"curriculum",
|
"curriculum",
|
||||||
"domain_map",
|
"domain_map",
|
||||||
"evaluation",
|
"evaluation",
|
||||||
|
"graph_merge",
|
||||||
|
"learning_graph",
|
||||||
"main",
|
"main",
|
||||||
"mentor",
|
"mentor",
|
||||||
"model_provider",
|
"model_provider",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import yaml
|
import yaml
|
||||||
|
import networkx as nx
|
||||||
|
|
||||||
from . import __version__ as DIDACTOPUS_VERSION
|
from . import __version__ as DIDACTOPUS_VERSION
|
||||||
from .artifact_schemas import (
|
from .artifact_schemas import (
|
||||||
|
|
@ -168,11 +169,7 @@ def discover_domain_packs(base_dirs: list[str | Path]) -> list[PackValidationRes
|
||||||
|
|
||||||
def check_pack_dependencies(results: list[PackValidationResult]) -> list[str]:
|
def check_pack_dependencies(results: list[PackValidationResult]) -> list[str]:
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
manifest_by_name = {
|
manifest_by_name = {r.manifest.name: r.manifest for r in results if r.manifest is not None}
|
||||||
r.manifest.name: r.manifest
|
|
||||||
for r in results
|
|
||||||
if r.manifest is not None
|
|
||||||
}
|
|
||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
if result.manifest is None:
|
if result.manifest is None:
|
||||||
|
|
@ -180,9 +177,7 @@ def check_pack_dependencies(results: list[PackValidationResult]) -> list[str]:
|
||||||
for dep in result.manifest.dependencies:
|
for dep in result.manifest.dependencies:
|
||||||
dep_manifest = manifest_by_name.get(dep.name)
|
dep_manifest = manifest_by_name.get(dep.name)
|
||||||
if dep_manifest is None:
|
if dep_manifest is None:
|
||||||
errors.append(
|
errors.append(f"pack '{result.manifest.name}' depends on missing pack '{dep.name}'")
|
||||||
f"pack '{result.manifest.name}' depends on missing pack '{dep.name}'"
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
if not _version_in_range(dep_manifest.version, dep.min_version, dep.max_version):
|
if not _version_in_range(dep_manifest.version, dep.min_version, dep.max_version):
|
||||||
errors.append(
|
errors.append(
|
||||||
|
|
@ -190,3 +185,25 @@ def check_pack_dependencies(results: list[PackValidationResult]) -> list[str]:
|
||||||
f"{dep.min_version}..{dep.max_version}, but found {dep_manifest.version}"
|
f"{dep.min_version}..{dep.max_version}, but found {dep_manifest.version}"
|
||||||
)
|
)
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def build_dependency_graph(results: list[PackValidationResult]) -> nx.DiGraph:
|
||||||
|
graph = nx.DiGraph()
|
||||||
|
valid_results = [r for r in results if r.manifest is not None and r.is_valid]
|
||||||
|
for result in valid_results:
|
||||||
|
graph.add_node(result.manifest.name)
|
||||||
|
for result in valid_results:
|
||||||
|
for dep in result.manifest.dependencies:
|
||||||
|
if dep.name in graph:
|
||||||
|
graph.add_edge(dep.name, result.manifest.name)
|
||||||
|
return graph
|
||||||
|
|
||||||
|
|
||||||
|
def detect_dependency_cycles(results: list[PackValidationResult]) -> list[list[str]]:
|
||||||
|
graph = build_dependency_graph(results)
|
||||||
|
return [cycle for cycle in nx.simple_cycles(graph)]
|
||||||
|
|
||||||
|
|
||||||
|
def topological_pack_order(results: list[PackValidationResult]) -> list[str]:
|
||||||
|
graph = build_dependency_graph(results)
|
||||||
|
return list(nx.topological_sort(graph))
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ class PackManifest(BaseModel):
|
||||||
author: str = ""
|
author: str = ""
|
||||||
license: str = "unspecified"
|
license: str = "unspecified"
|
||||||
dependencies: list[DependencySpec] = Field(default_factory=list)
|
dependencies: list[DependencySpec] = Field(default_factory=list)
|
||||||
|
overrides: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class ConceptEntry(BaseModel):
|
class ConceptEntry(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from .domain_map import DomainMap
|
import networkx as nx
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -9,12 +9,13 @@ class RoadmapStage:
|
||||||
mastery_goal: str
|
mastery_goal: str
|
||||||
|
|
||||||
|
|
||||||
def generate_initial_roadmap(domain_map: DomainMap, goal: str) -> list[RoadmapStage]:
|
def generate_stages_from_learning_graph(graph: nx.DiGraph) -> list[RoadmapStage]:
|
||||||
|
sequence = list(nx.topological_sort(graph))
|
||||||
return [
|
return [
|
||||||
RoadmapStage(
|
RoadmapStage(
|
||||||
title=f"Stage {i+1}: {concept.title()}",
|
title=f"Stage {i+1}: {concept.split('::')[-1].replace('-', ' ').title()}",
|
||||||
concepts=[concept],
|
concepts=[concept],
|
||||||
mastery_goal=f"Demonstrate applied understanding of {concept} toward goal: {goal}",
|
mastery_goal=f"Demonstrate applied understanding of {concept}.",
|
||||||
)
|
)
|
||||||
for i, concept in enumerate(domain_map.topological_sequence())
|
for i, concept in enumerate(sequence)
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,3 @@ class DomainMap:
|
||||||
|
|
||||||
def topological_sequence(self) -> list[str]:
|
def topological_sequence(self) -> list[str]:
|
||||||
return list(nx.topological_sort(self.graph))
|
return list(nx.topological_sort(self.graph))
|
||||||
|
|
||||||
|
|
||||||
def build_demo_domain_map(domain_name: str) -> DomainMap:
|
|
||||||
dmap = DomainMap(domain_name)
|
|
||||||
dmap.add_concept(ConceptNode("foundations", "Core assumptions and terminology"))
|
|
||||||
dmap.add_concept(ConceptNode("methods", "Basic methods", ["foundations"]))
|
|
||||||
dmap.add_concept(ConceptNode("analysis", "Applying methods", ["methods"]))
|
|
||||||
dmap.add_concept(ConceptNode("projects", "Real-world capstones", ["analysis"]))
|
|
||||||
return dmap
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .artifact_registry import PackValidationResult, topological_pack_order
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MergedConceptGraph:
|
||||||
|
concept_by_id: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||||
|
source_pack_by_concept: dict[str, str] = field(default_factory=dict)
|
||||||
|
conflicts: list[str] = field(default_factory=list)
|
||||||
|
load_order: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
def merge_pack_concepts(results: list[PackValidationResult]) -> MergedConceptGraph:
|
||||||
|
merged = MergedConceptGraph()
|
||||||
|
manifest_by_name = {
|
||||||
|
result.manifest.name: result
|
||||||
|
for result in results
|
||||||
|
if result.manifest is not None and result.is_valid
|
||||||
|
}
|
||||||
|
merged.load_order = topological_pack_order(results)
|
||||||
|
|
||||||
|
for pack_name in merged.load_order:
|
||||||
|
result = manifest_by_name[pack_name]
|
||||||
|
concepts_file = result.loaded_files.get("concepts")
|
||||||
|
if concepts_file is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for concept in concepts_file.concepts:
|
||||||
|
if concept.id in merged.concept_by_id:
|
||||||
|
previous_pack = merged.source_pack_by_concept[concept.id]
|
||||||
|
merged.conflicts.append(
|
||||||
|
f"concept '{concept.id}' defined in both '{previous_pack}' and '{pack_name}'"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
merged.concept_by_id[concept.id] = {
|
||||||
|
"id": concept.id,
|
||||||
|
"title": concept.title,
|
||||||
|
"prerequisites": list(concept.prerequisites),
|
||||||
|
"mastery_signals": list(concept.mastery_signals),
|
||||||
|
}
|
||||||
|
merged.source_pack_by_concept[concept.id] = pack_name
|
||||||
|
|
||||||
|
return merged
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
import networkx as nx
|
||||||
|
|
||||||
|
from .artifact_registry import PackValidationResult, topological_pack_order
|
||||||
|
|
||||||
|
|
||||||
|
def namespaced_concept(pack_name: str, concept_id: str) -> str:
|
||||||
|
return f"{pack_name}::{concept_id}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MergedLearningGraph:
|
||||||
|
graph: nx.DiGraph = field(default_factory=nx.DiGraph)
|
||||||
|
concept_data: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||||
|
stage_catalog: list[dict[str, Any]] = field(default_factory=list)
|
||||||
|
project_catalog: list[dict[str, Any]] = field(default_factory=list)
|
||||||
|
conflicts: list[str] = field(default_factory=list)
|
||||||
|
load_order: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_override_targets(results: list[PackValidationResult]) -> set[str]:
|
||||||
|
targets: set[str] = set()
|
||||||
|
for result in results:
|
||||||
|
if result.manifest is None or not result.is_valid:
|
||||||
|
continue
|
||||||
|
targets.update(result.manifest.overrides)
|
||||||
|
return targets
|
||||||
|
|
||||||
|
|
||||||
|
def build_merged_learning_graph(results: list[PackValidationResult]) -> MergedLearningGraph:
|
||||||
|
merged = MergedLearningGraph()
|
||||||
|
valid = {
|
||||||
|
r.manifest.name: r
|
||||||
|
for r in results
|
||||||
|
if r.manifest is not None and r.is_valid
|
||||||
|
}
|
||||||
|
merged.load_order = topological_pack_order(results)
|
||||||
|
override_targets = _build_override_targets(results)
|
||||||
|
|
||||||
|
# Add concepts
|
||||||
|
for pack_name in merged.load_order:
|
||||||
|
result = valid[pack_name]
|
||||||
|
concepts_file = result.loaded_files.get("concepts")
|
||||||
|
if concepts_file is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for concept in concepts_file.concepts:
|
||||||
|
local_key = namespaced_concept(pack_name, concept.id)
|
||||||
|
|
||||||
|
# explicit override support: same local concept ID may replace one namespaced target
|
||||||
|
replaced = False
|
||||||
|
for target in result.manifest.overrides:
|
||||||
|
if target.split("::")[-1] == concept.id and target in merged.concept_data:
|
||||||
|
merged.concept_data[target] = {
|
||||||
|
"id": concept.id,
|
||||||
|
"title": concept.title,
|
||||||
|
"pack": pack_name,
|
||||||
|
"prerequisites": [],
|
||||||
|
"mastery_signals": list(concept.mastery_signals),
|
||||||
|
"overridden_by": local_key,
|
||||||
|
}
|
||||||
|
replaced = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if replaced:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if local_key in merged.concept_data:
|
||||||
|
merged.conflicts.append(f"duplicate namespaced concept key: {local_key}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
merged.concept_data[local_key] = {
|
||||||
|
"id": concept.id,
|
||||||
|
"title": concept.title,
|
||||||
|
"pack": pack_name,
|
||||||
|
"prerequisites": list(concept.prerequisites),
|
||||||
|
"mastery_signals": list(concept.mastery_signals),
|
||||||
|
}
|
||||||
|
merged.graph.add_node(local_key)
|
||||||
|
|
||||||
|
# Add prerequisite edges within each pack
|
||||||
|
for pack_name in merged.load_order:
|
||||||
|
result = valid[pack_name]
|
||||||
|
concepts_file = result.loaded_files.get("concepts")
|
||||||
|
if concepts_file is None:
|
||||||
|
continue
|
||||||
|
for concept in concepts_file.concepts:
|
||||||
|
concept_key = namespaced_concept(pack_name, concept.id)
|
||||||
|
if concept_key not in merged.graph:
|
||||||
|
continue
|
||||||
|
for prereq in concept.prerequisites:
|
||||||
|
prereq_key = namespaced_concept(pack_name, prereq)
|
||||||
|
if prereq_key in merged.graph:
|
||||||
|
merged.graph.add_edge(prereq_key, concept_key)
|
||||||
|
else:
|
||||||
|
merged.conflicts.append(
|
||||||
|
f"missing namespaced prerequisite '{prereq_key}' for concept '{concept_key}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Merge stage catalog
|
||||||
|
for pack_name in merged.load_order:
|
||||||
|
result = valid[pack_name]
|
||||||
|
roadmap_file = result.loaded_files.get("roadmap")
|
||||||
|
if roadmap_file is None:
|
||||||
|
continue
|
||||||
|
for stage in roadmap_file.stages:
|
||||||
|
merged.stage_catalog.append(
|
||||||
|
{
|
||||||
|
"id": f"{pack_name}::{stage.id}",
|
||||||
|
"pack": pack_name,
|
||||||
|
"title": stage.title,
|
||||||
|
"concepts": [namespaced_concept(pack_name, c) for c in stage.concepts],
|
||||||
|
"checkpoint": list(stage.checkpoint),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Merge project catalog
|
||||||
|
for pack_name in merged.load_order:
|
||||||
|
result = valid[pack_name]
|
||||||
|
projects_file = result.loaded_files.get("projects")
|
||||||
|
if projects_file is None:
|
||||||
|
continue
|
||||||
|
for project in projects_file.projects:
|
||||||
|
merged.project_catalog.append(
|
||||||
|
{
|
||||||
|
"id": f"{pack_name}::{project.id}",
|
||||||
|
"pack": pack_name,
|
||||||
|
"title": project.title,
|
||||||
|
"difficulty": project.difficulty,
|
||||||
|
"prerequisites": [namespaced_concept(pack_name, p) for p in project.prerequisites],
|
||||||
|
"deliverables": list(project.deliverables),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def generate_learner_roadmap(merged: MergedLearningGraph) -> list[dict[str, Any]]:
|
||||||
|
roadmap: list[dict[str, Any]] = []
|
||||||
|
for i, concept_key in enumerate(nx.topological_sort(merged.graph), start=1):
|
||||||
|
data = merged.concept_data[concept_key]
|
||||||
|
roadmap.append(
|
||||||
|
{
|
||||||
|
"stage_number": i,
|
||||||
|
"concept_key": concept_key,
|
||||||
|
"title": data["title"],
|
||||||
|
"pack": data["pack"],
|
||||||
|
"prerequisites": list(merged.graph.predecessors(concept_key)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return roadmap
|
||||||
|
|
@ -2,11 +2,16 @@ import argparse
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .artifact_registry import check_pack_dependencies, discover_domain_packs
|
from .artifact_registry import (
|
||||||
|
check_pack_dependencies,
|
||||||
|
detect_dependency_cycles,
|
||||||
|
discover_domain_packs,
|
||||||
|
topological_pack_order,
|
||||||
|
)
|
||||||
from .config import load_config
|
from .config import load_config
|
||||||
from .curriculum import generate_initial_roadmap
|
from .curriculum import generate_stages_from_learning_graph
|
||||||
from .domain_map import build_demo_domain_map
|
|
||||||
from .evaluation import generate_rubric
|
from .evaluation import generate_rubric
|
||||||
|
from .learning_graph import build_merged_learning_graph, generate_learner_roadmap
|
||||||
from .mentor import generate_socratic_prompt
|
from .mentor import generate_socratic_prompt
|
||||||
from .model_provider import ModelProvider
|
from .model_provider import ModelProvider
|
||||||
from .practice import generate_practice_task
|
from .practice import generate_practice_task
|
||||||
|
|
@ -14,7 +19,7 @@ from .project_advisor import suggest_capstone
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(description="Didactopus mastery scaffold")
|
parser = argparse.ArgumentParser(description="Didactopus merged learning graph scaffold")
|
||||||
parser.add_argument("--domain", required=True)
|
parser.add_argument("--domain", required=True)
|
||||||
parser.add_argument("--goal", required=True)
|
parser.add_argument("--goal", required=True)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
|
@ -30,10 +35,7 @@ def main() -> None:
|
||||||
provider = ModelProvider(config.model_provider)
|
provider = ModelProvider(config.model_provider)
|
||||||
packs = discover_domain_packs(config.artifacts.local_pack_dirs)
|
packs = discover_domain_packs(config.artifacts.local_pack_dirs)
|
||||||
dependency_errors = check_pack_dependencies(packs)
|
dependency_errors = check_pack_dependencies(packs)
|
||||||
|
cycles = detect_dependency_cycles(packs)
|
||||||
dmap = build_demo_domain_map(args.domain)
|
|
||||||
roadmap = generate_initial_roadmap(dmap, args.goal)
|
|
||||||
focus_concept = dmap.topological_sequence()[1]
|
|
||||||
|
|
||||||
print("== Didactopus ==")
|
print("== Didactopus ==")
|
||||||
print("Many arms, one goal — mastery.")
|
print("Many arms, one goal — mastery.")
|
||||||
|
|
@ -52,10 +54,39 @@ def main() -> None:
|
||||||
else:
|
else:
|
||||||
print("- all resolved")
|
print("- all resolved")
|
||||||
print()
|
print()
|
||||||
print("== Roadmap ==")
|
print("== Dependency Cycles ==")
|
||||||
for stage in roadmap:
|
if cycles:
|
||||||
print(f"- {stage.title}: {stage.mastery_goal}")
|
for cycle in cycles:
|
||||||
|
print(f"- cycle: {' -> '.join(cycle)}")
|
||||||
|
print("- merged learning graph unavailable while cycles exist")
|
||||||
|
return
|
||||||
|
print("- none")
|
||||||
print()
|
print()
|
||||||
|
print("== Pack Load Order ==")
|
||||||
|
for name in topological_pack_order(packs):
|
||||||
|
print(f"- {name}")
|
||||||
|
print()
|
||||||
|
merged = build_merged_learning_graph(packs)
|
||||||
|
learner_roadmap = generate_learner_roadmap(merged)
|
||||||
|
print("== Merged Learning Graph ==")
|
||||||
|
print(f"- concepts: {len(merged.concept_data)}")
|
||||||
|
print(f"- prerequisite edges: {merged.graph.number_of_edges()}")
|
||||||
|
print(f"- roadmap stages: {len(merged.stage_catalog)}")
|
||||||
|
print(f"- projects: {len(merged.project_catalog)}")
|
||||||
|
if merged.conflicts:
|
||||||
|
for conflict in merged.conflicts:
|
||||||
|
print(f" * conflict: {conflict}")
|
||||||
|
else:
|
||||||
|
print("- no merge conflicts")
|
||||||
|
print()
|
||||||
|
print("== Learner Roadmap ==")
|
||||||
|
for item in learner_roadmap[:8]:
|
||||||
|
print(f"- Stage {item['stage_number']}: {item['concept_key']} ({item['title']})")
|
||||||
|
print()
|
||||||
|
if learner_roadmap:
|
||||||
|
focus_concept = learner_roadmap[0]["concept_key"]
|
||||||
|
else:
|
||||||
|
focus_concept = args.domain
|
||||||
print(generate_socratic_prompt(provider, focus_concept))
|
print(generate_socratic_prompt(provider, focus_concept))
|
||||||
print(generate_practice_task(provider, focus_concept))
|
print(generate_practice_task(provider, focus_concept))
|
||||||
print(suggest_capstone(provider, args.domain))
|
print(suggest_capstone(provider, args.domain))
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,4 @@ from didactopus.config import load_config
|
||||||
def test_load_example_config() -> None:
|
def test_load_example_config() -> None:
|
||||||
config = load_config(Path("configs/config.example.yaml"))
|
config = load_config(Path("configs/config.example.yaml"))
|
||||||
assert config.model_provider.mode == "local_first"
|
assert config.model_provider.mode == "local_first"
|
||||||
assert config.platform.verification_required is True
|
|
||||||
assert config.platform.mastery_threshold == 0.8
|
|
||||||
assert "domain-packs" in config.artifacts.local_pack_dirs
|
assert "domain-packs" in config.artifacts.local_pack_dirs
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
from didactopus.artifact_registry import discover_domain_packs
|
||||||
|
from didactopus.learning_graph import (
|
||||||
|
build_merged_learning_graph,
|
||||||
|
generate_learner_roadmap,
|
||||||
|
namespaced_concept,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _acyclic_results():
|
||||||
|
return [
|
||||||
|
r for r in discover_domain_packs(["domain-packs"])
|
||||||
|
if r.manifest and r.manifest.name in {
|
||||||
|
"foundations-statistics",
|
||||||
|
"bayes-extension",
|
||||||
|
"override-foundations",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_namespaced_concept() -> None:
|
||||||
|
assert namespaced_concept("pack", "concept") == "pack::concept"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_merged_learning_graph() -> None:
|
||||||
|
merged = build_merged_learning_graph(_acyclic_results())
|
||||||
|
assert "foundations-statistics::probability-basics" in merged.concept_data
|
||||||
|
assert "bayes-extension::posterior" in merged.concept_data
|
||||||
|
assert len(merged.stage_catalog) >= 3
|
||||||
|
assert len(merged.project_catalog) >= 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_override_updates_target_concept() -> None:
|
||||||
|
merged = build_merged_learning_graph(_acyclic_results())
|
||||||
|
data = merged.concept_data["foundations-statistics::descriptive-statistics"]
|
||||||
|
assert data["title"] == "Descriptive Statistics (Overridden)"
|
||||||
|
assert data["pack"] == "override-foundations"
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_learner_roadmap() -> None:
|
||||||
|
merged = build_merged_learning_graph(_acyclic_results())
|
||||||
|
roadmap = generate_learner_roadmap(merged)
|
||||||
|
assert len(roadmap) >= 3
|
||||||
|
assert all("concept_key" in item for item in roadmap)
|
||||||
Loading…
Reference in New Issue