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

|
||||
|
||||
# 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
|
||||
- version-range validation against the Didactopus core version
|
||||
- local dependency resolution across installed packs
|
||||
- richer pack manifests
|
||||
- repository artwork under `artwork/`
|
||||
- tests for dependency and compatibility behavior
|
||||
- merged learning graph builder
|
||||
- combined prerequisite DAG across packs
|
||||
- merged roadmap stage catalog
|
||||
- merged project catalog
|
||||
- namespaced concept keys (`pack::concept`)
|
||||
- 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
|
||||
- roadmap templates
|
||||
- project blueprints
|
||||
- rubrics
|
||||
- benchmark tasks
|
||||
- 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
|
||||
```
|
||||
That enables:
|
||||
- foundations + extension pack composition
|
||||
- unified learner roadmaps
|
||||
- shared project catalogs
|
||||
- safe coexistence of overlapping concept IDs via namespacing
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
mastery_signals:
|
||||
- explain a prior distribution
|
||||
- id: posterior
|
||||
title: Posterior
|
||||
prerequisites:
|
||||
- prior
|
||||
mastery_signals:
|
||||
- explain updating beliefs
|
||||
|
|
|
|||
|
|
@ -11,3 +11,4 @@ dependencies:
|
|||
- name: foundations-statistics
|
||||
min_version: 1.0.0
|
||||
max_version: 1.9.99
|
||||
overrides: []
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@ stages:
|
|||
title: Bayesian Basics
|
||||
concepts:
|
||||
- prior
|
||||
- posterior
|
||||
checkpoint:
|
||||
- compare priors
|
||||
- compare priors and posteriors
|
||||
|
|
|
|||
|
|
@ -4,3 +4,9 @@ concepts:
|
|||
prerequisites: []
|
||||
mastery_signals:
|
||||
- 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
|
||||
license: MIT
|
||||
dependencies: []
|
||||
overrides: []
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@ stages:
|
|||
title: Foundations
|
||||
concepts:
|
||||
- descriptive-statistics
|
||||
- probability-basics
|
||||
checkpoint:
|
||||
- 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",
|
||||
"domain_map",
|
||||
"evaluation",
|
||||
"graph_merge",
|
||||
"learning_graph",
|
||||
"main",
|
||||
"mentor",
|
||||
"model_provider",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from dataclasses import dataclass, field
|
|||
from pathlib import Path
|
||||
from typing import Any
|
||||
import yaml
|
||||
import networkx as nx
|
||||
|
||||
from . import __version__ as DIDACTOPUS_VERSION
|
||||
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]:
|
||||
errors: list[str] = []
|
||||
manifest_by_name = {
|
||||
r.manifest.name: r.manifest
|
||||
for r in results
|
||||
if r.manifest is not None
|
||||
}
|
||||
manifest_by_name = {r.manifest.name: r.manifest for r in results if r.manifest is not None}
|
||||
|
||||
for result in results:
|
||||
if result.manifest is None:
|
||||
|
|
@ -180,9 +177,7 @@ def check_pack_dependencies(results: list[PackValidationResult]) -> list[str]:
|
|||
for dep in result.manifest.dependencies:
|
||||
dep_manifest = manifest_by_name.get(dep.name)
|
||||
if dep_manifest is None:
|
||||
errors.append(
|
||||
f"pack '{result.manifest.name}' depends on missing pack '{dep.name}'"
|
||||
)
|
||||
errors.append(f"pack '{result.manifest.name}' depends on missing pack '{dep.name}'")
|
||||
continue
|
||||
if not _version_in_range(dep_manifest.version, dep.min_version, dep.max_version):
|
||||
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}"
|
||||
)
|
||||
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 = ""
|
||||
license: str = "unspecified"
|
||||
dependencies: list[DependencySpec] = Field(default_factory=list)
|
||||
overrides: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ConceptEntry(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from dataclasses import dataclass
|
||||
from .domain_map import DomainMap
|
||||
import networkx as nx
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -9,12 +9,13 @@ class RoadmapStage:
|
|||
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 [
|
||||
RoadmapStage(
|
||||
title=f"Stage {i+1}: {concept.title()}",
|
||||
title=f"Stage {i+1}: {concept.split('::')[-1].replace('-', ' ').title()}",
|
||||
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]:
|
||||
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
|
||||
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 .curriculum import generate_initial_roadmap
|
||||
from .domain_map import build_demo_domain_map
|
||||
from .curriculum import generate_stages_from_learning_graph
|
||||
from .evaluation import generate_rubric
|
||||
from .learning_graph import build_merged_learning_graph, generate_learner_roadmap
|
||||
from .mentor import generate_socratic_prompt
|
||||
from .model_provider import ModelProvider
|
||||
from .practice import generate_practice_task
|
||||
|
|
@ -14,7 +19,7 @@ from .project_advisor import suggest_capstone
|
|||
|
||||
|
||||
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("--goal", required=True)
|
||||
parser.add_argument(
|
||||
|
|
@ -30,10 +35,7 @@ def main() -> None:
|
|||
provider = ModelProvider(config.model_provider)
|
||||
packs = discover_domain_packs(config.artifacts.local_pack_dirs)
|
||||
dependency_errors = check_pack_dependencies(packs)
|
||||
|
||||
dmap = build_demo_domain_map(args.domain)
|
||||
roadmap = generate_initial_roadmap(dmap, args.goal)
|
||||
focus_concept = dmap.topological_sequence()[1]
|
||||
cycles = detect_dependency_cycles(packs)
|
||||
|
||||
print("== Didactopus ==")
|
||||
print("Many arms, one goal — mastery.")
|
||||
|
|
@ -52,10 +54,39 @@ def main() -> None:
|
|||
else:
|
||||
print("- all resolved")
|
||||
print()
|
||||
print("== Roadmap ==")
|
||||
for stage in roadmap:
|
||||
print(f"- {stage.title}: {stage.mastery_goal}")
|
||||
print("== Dependency Cycles ==")
|
||||
if cycles:
|
||||
for cycle in cycles:
|
||||
print(f"- cycle: {' -> '.join(cycle)}")
|
||||
print("- merged learning graph unavailable while cycles exist")
|
||||
return
|
||||
print("- none")
|
||||
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_practice_task(provider, focus_concept))
|
||||
print(suggest_capstone(provider, args.domain))
|
||||
|
|
|
|||
|
|
@ -5,6 +5,4 @@ from didactopus.config import load_config
|
|||
def test_load_example_config() -> None:
|
||||
config = load_config(Path("configs/config.example.yaml"))
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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