diff --git a/bad-generated-pack/concepts.yaml b/bad-generated-pack/concepts.yaml index 1c3e587..676a9d2 100644 --- a/bad-generated-pack/concepts.yaml +++ b/bad-generated-pack/concepts.yaml @@ -1,13 +1,7 @@ concepts: - - id: prior-and-posterior - title: Prior and Posterior - description: Beliefs before and after evidence. - prerequisites: [] - - id: posterior-analysis - title: Posterior Analysis - description: Beliefs before and after evidence. - prerequisites: [] - - id: statistics-and-probability - title: Statistics and Probability - description: General overview. - prerequisites: [] + - id: duplicate + title: First + description: Tiny. + - id: duplicate + title: Second + description: Tiny. diff --git a/bad-generated-pack/pack.yaml b/bad-generated-pack/pack.yaml index ecadbb9..590cbc9 100644 --- a/bad-generated-pack/pack.yaml +++ b/bad-generated-pack/pack.yaml @@ -1 +1,3 @@ -display_name Imported Pack Broken YAML +name: broken-pack +display_name: Broken Pack +version: 0.1.0-draft diff --git a/bad-generated-pack/projects.yaml b/bad-generated-pack/projects.yaml index 4d053f9..097f205 100644 --- a/bad-generated-pack/projects.yaml +++ b/bad-generated-pack/projects.yaml @@ -1 +1,5 @@ -projects: [] +projects: + - id: bad-project + title: Bad Project + prerequisites: + - missing-concept diff --git a/bad-generated-pack/roadmap.yaml b/bad-generated-pack/roadmap.yaml index 1b1c7b4..5302f25 100644 --- a/bad-generated-pack/roadmap.yaml +++ b/bad-generated-pack/roadmap.yaml @@ -1,9 +1,5 @@ stages: - id: stage-1 - title: Foundations + title: Bad Stage concepts: - - statistics-and-probability - - id: stage-2 - title: Advanced Inference - concepts: - - posterior-analysis + - missing-concept diff --git a/bad-generated-pack/rubrics.yaml b/bad-generated-pack/rubrics.yaml index cd1f79a..af7ebe0 100644 --- a/bad-generated-pack/rubrics.yaml +++ b/bad-generated-pack/rubrics.yaml @@ -1,5 +1,4 @@ rubrics: - - id: basic-rubric - title: Basic Rubric - criteria: - - correctness + - id: + title: Broken Rubric + criteria: invalid diff --git a/docs/faq.md b/docs/faq.md index ff43624..67baaf7 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,28 +1,32 @@ # FAQ -## Why add import validation? +## Why add a full pack validator? -Because reducing startup friction does not mean hiding risk. A user still needs -a clear signal about whether a generated draft pack is structurally usable. +Because import safety is not only about whether files exist. It is also about +whether the pack makes sense as a Didactopus artifact set. -## How does this support the activation-energy goal? +## How does this help with the activation-energy problem? -It removes uncertainty from the handoff step. Users can see whether a draft pack -looks valid before committing it into a workspace. +It reduces uncertainty at a crucial point. Users can see whether a generated pack +is coherent enough to work with before losing momentum in manual debugging. -## What does the preview check do? +## What does it validate? -In this scaffold it checks: +In this scaffold it validates: - required files -- basic YAML parsing -- key metadata presence -- concept count -- overwrite conditions +- YAML parsing +- metadata presence +- duplicate concept ids +- roadmap references +- project prerequisite references +- rubric structure +- weak concept entries -## Does preview guarantee correctness? +## Does validation guarantee quality? -No. It is a safety and structure check, not a guarantee of pedagogical quality. +No. It checks structural coherence, not whether the pack is the best possible +representation of a domain. -## Can import still overwrite an existing workspace? +## Where are validation results shown? -Yes, but only if overwrite is explicitly allowed. +They are included in import preview results and surfaced in the UI. diff --git a/generated-pack/roadmap.yaml b/generated-pack/roadmap.yaml index 8b2f6ca..cf25257 100644 --- a/generated-pack/roadmap.yaml +++ b/generated-pack/roadmap.yaml @@ -1,13 +1,8 @@ stages: - id: stage-1 - title: Prior Beliefs + title: Bayes Basics concepts: - bayes-prior - - id: stage-2 - title: Posterior Updating - concepts: - bayes-posterior - - id: stage-3 - title: Model Checking - concepts: - - model-checking + checkpoint: + - Compare prior and posterior beliefs. diff --git a/pyproject.toml b/pyproject.toml index 72c17de..0852eb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "didactopus" version = "0.1.0" -description = "Didactopus: import validation and safety layer" +description = "Didactopus: full pack validation layer" readme = "README.md" requires-python = ">=3.10" license = {text = "MIT"} diff --git a/src/didactopus/import_validator.py b/src/didactopus/import_validator.py index c0f6840..5392cdd 100644 --- a/src/didactopus/import_validator.py +++ b/src/didactopus/import_validator.py @@ -1,56 +1,17 @@ from __future__ import annotations from pathlib import Path -import yaml from .review_schema import ImportPreview - -REQUIRED_FILES = ["pack.yaml", "concepts.yaml"] +from .pack_validator import validate_pack_directory def preview_draft_pack_import(source_dir: str | Path, workspace_id: str, overwrite_required: bool = False) -> ImportPreview: - source = Path(source_dir) - preview = ImportPreview(source_dir=str(source), workspace_id=workspace_id, overwrite_required=overwrite_required) - - if not source.exists(): - preview.errors.append(f"Source directory does not exist: {source}") - return preview - if not source.is_dir(): - preview.errors.append(f"Source path is not a directory: {source}") - return preview - - for filename in REQUIRED_FILES: - if not (source / filename).exists(): - preview.errors.append(f"Missing required file: {filename}") - - pack_data = {} - concepts_data = {} - if not preview.errors: - try: - pack_data = yaml.safe_load((source / "pack.yaml").read_text(encoding="utf-8")) or {} - except Exception as exc: - preview.errors.append(f"Could not parse pack.yaml: {exc}") - try: - concepts_data = yaml.safe_load((source / "concepts.yaml").read_text(encoding="utf-8")) or {} - except Exception as exc: - preview.errors.append(f"Could not parse concepts.yaml: {exc}") - - if not preview.errors: - if "name" not in pack_data: - preview.warnings.append("pack.yaml has no 'name' field.") - if "display_name" not in pack_data: - preview.warnings.append("pack.yaml has no 'display_name' field.") - concepts = concepts_data.get("concepts", []) - if not isinstance(concepts, list): - preview.errors.append("concepts.yaml top-level 'concepts' is not a list.") - else: - preview.summary = { - "pack_name": pack_data.get("name", ""), - "display_name": pack_data.get("display_name", ""), - "version": pack_data.get("version", ""), - "concept_count": len(concepts), - "has_conflict_report": (source / "conflict_report.md").exists(), - "has_review_report": (source / "review_report.md").exists(), - } - if len(concepts) == 0: - preview.warnings.append("concepts.yaml contains zero concepts.") - - preview.ok = len(preview.errors) == 0 + result = validate_pack_directory(source_dir) + preview = ImportPreview( + source_dir=str(Path(source_dir)), + workspace_id=workspace_id, + overwrite_required=overwrite_required, + ok=result["ok"], + errors=list(result["errors"]), + warnings=list(result["warnings"]), + summary=dict(result["summary"]), + ) return preview diff --git a/src/didactopus/pack_validator.py b/src/didactopus/pack_validator.py index 763204f..0eb3538 100644 --- a/src/didactopus/pack_validator.py +++ b/src/didactopus/pack_validator.py @@ -11,53 +11,33 @@ def _safe_load_yaml(path: Path, errors: list[str], label: str): errors.append(f"Could not parse {label}: {exc}") return {} -def load_pack_artifacts(source_dir: str | Path) -> dict: +def validate_pack_directory(source_dir: str | Path) -> dict: source = Path(source_dir) errors: list[str] = [] + warnings: list[str] = [] + summary: dict = {} + if not source.exists(): - return {"ok": False, "errors": [f"Source directory does not exist: {source}"], "warnings": [], "summary": {}, "artifacts": {}} + return {"ok": False, "errors": [f"Source directory does not exist: {source}"], "warnings": [], "summary": {}} if not source.is_dir(): - return {"ok": False, "errors": [f"Source path is not a directory: {source}"], "warnings": [], "summary": {}, "artifacts": {}} + return {"ok": False, "errors": [f"Source path is not a directory: {source}"], "warnings": [], "summary": {}} for filename in REQUIRED_FILES: if not (source / filename).exists(): errors.append(f"Missing required file: {filename}") + if errors: - return {"ok": False, "errors": errors, "warnings": [], "summary": {}, "artifacts": {}} + return {"ok": False, "errors": errors, "warnings": warnings, "summary": summary} pack_data = _safe_load_yaml(source / "pack.yaml", errors, "pack.yaml") concepts_data = _safe_load_yaml(source / "concepts.yaml", errors, "concepts.yaml") roadmap_data = _safe_load_yaml(source / "roadmap.yaml", errors, "roadmap.yaml") projects_data = _safe_load_yaml(source / "projects.yaml", errors, "projects.yaml") rubrics_data = _safe_load_yaml(source / "rubrics.yaml", errors, "rubrics.yaml") - return { - "ok": len(errors) == 0, - "errors": errors, - "warnings": [], - "summary": {}, - "artifacts": { - "pack": pack_data, - "concepts": concepts_data, - "roadmap": roadmap_data, - "projects": projects_data, - "rubrics": rubrics_data, - }, - } -def validate_pack_directory(source_dir: str | Path) -> dict: - loaded = load_pack_artifacts(source_dir) - errors = list(loaded["errors"]) - warnings = list(loaded["warnings"]) - summary = dict(loaded["summary"]) - if not loaded["ok"]: + if errors: return {"ok": False, "errors": errors, "warnings": warnings, "summary": summary} - pack_data = loaded["artifacts"]["pack"] - concepts_data = loaded["artifacts"]["concepts"] - roadmap_data = loaded["artifacts"]["roadmap"] - projects_data = loaded["artifacts"]["projects"] - rubrics_data = loaded["artifacts"]["rubrics"] - for field in ["name", "display_name", "version"]: if field not in pack_data: warnings.append(f"pack.yaml has no '{field}' field.") diff --git a/src/didactopus/review_bridge_server.py b/src/didactopus/review_bridge_server.py index a07c20c..4749120 100644 --- a/src/didactopus/review_bridge_server.py +++ b/src/didactopus/review_bridge_server.py @@ -120,7 +120,7 @@ class ReviewBridgeHandler(BaseHTTPRequestHandler): json_response(self, 404, {"error": "not found"}) def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="Didactopus local review bridge server with import validation") + parser = argparse.ArgumentParser(description="Didactopus local review bridge server with full pack validation") parser.add_argument("--config", default="configs/config.example.yaml") return parser diff --git a/tests/test_import_validator.py b/tests/test_import_validator.py index 1c4cd49..5663029 100644 --- a/tests/test_import_validator.py +++ b/tests/test_import_validator.py @@ -2,18 +2,16 @@ from pathlib import Path from didactopus.import_validator import preview_draft_pack_import def test_valid_preview(tmp_path: Path) -> None: - src = tmp_path / "src" - src.mkdir() - (src / "pack.yaml").write_text("name: p\ndisplay_name: P\nversion: 0.1.0\n", encoding="utf-8") - (src / "concepts.yaml").write_text("concepts: []\n", encoding="utf-8") - preview = preview_draft_pack_import(src, "ws1") + (tmp_path / "pack.yaml").write_text("name: p\ndisplay_name: P\nversion: 0.1.0\n", encoding="utf-8") + (tmp_path / "concepts.yaml").write_text("concepts:\n - id: c1\n title: C1\n description: A full enough description.\n", encoding="utf-8") + (tmp_path / "roadmap.yaml").write_text("stages: []\n", encoding="utf-8") + (tmp_path / "projects.yaml").write_text("projects: []\n", encoding="utf-8") + (tmp_path / "rubrics.yaml").write_text("rubrics: []\n", encoding="utf-8") + preview = preview_draft_pack_import(tmp_path, "ws1") assert preview.ok is True - assert preview.summary["concept_count"] == 0 + assert preview.summary["concept_count"] == 1 def test_missing_required_file(tmp_path: Path) -> None: - src = tmp_path / "src" - src.mkdir() - (src / "pack.yaml").write_text("name: p\n", encoding="utf-8") - preview = preview_draft_pack_import(src, "ws1") + (tmp_path / "pack.yaml").write_text("name: p\n", encoding="utf-8") + preview = preview_draft_pack_import(tmp_path, "ws1") assert preview.ok is False - assert any("Missing required file" in e for e in preview.errors) diff --git a/tests/test_pack_validator.py b/tests/test_pack_validator.py index 82c5d40..3c13676 100644 --- a/tests/test_pack_validator.py +++ b/tests/test_pack_validator.py @@ -9,3 +9,13 @@ def test_valid_pack(tmp_path: Path) -> None: (tmp_path / "rubrics.yaml").write_text("rubrics:\n - id: r1\n title: R1\n criteria: [correctness]\n", encoding="utf-8") result = validate_pack_directory(tmp_path) assert result["ok"] is True + +def test_cross_file_errors(tmp_path: Path) -> None: + (tmp_path / "pack.yaml").write_text("name: p\ndisplay_name: P\nversion: 0.1.0\n", encoding="utf-8") + (tmp_path / "concepts.yaml").write_text("concepts:\n - id: dup\n title: A\n description: Thin.\n - id: dup\n title: B\n description: Thin.\n", encoding="utf-8") + (tmp_path / "roadmap.yaml").write_text("stages:\n - id: s1\n title: S1\n concepts: [missing]\n", encoding="utf-8") + (tmp_path / "projects.yaml").write_text("projects:\n - id: p1\n title: P1\n prerequisites: [missing]\n", encoding="utf-8") + (tmp_path / "rubrics.yaml").write_text("rubrics:\n - id: r1\n title: R1\n criteria: broken\n", encoding="utf-8") + result = validate_pack_directory(tmp_path) + assert result["ok"] is False + assert any("Duplicate concept id" in e for e in result["errors"]) diff --git a/tests/test_workspace_manager.py b/tests/test_workspace_manager.py index c9e4630..5bfe9cf 100644 --- a/tests/test_workspace_manager.py +++ b/tests/test_workspace_manager.py @@ -1,11 +1,17 @@ from pathlib import Path from didactopus.workspace_manager import WorkspaceManager -def test_import_preview_and_overwrite_warning(tmp_path: Path) -> None: - src = tmp_path / "srcpack" +def make_valid_pack(src: Path): src.mkdir() (src / "pack.yaml").write_text("name: x\ndisplay_name: X\nversion: 0.1.0\n", encoding="utf-8") - (src / "concepts.yaml").write_text("concepts: []\n", encoding="utf-8") + (src / "concepts.yaml").write_text("concepts:\n - id: c1\n title: C1\n description: A full enough description.\n", encoding="utf-8") + (src / "roadmap.yaml").write_text("stages: []\n", encoding="utf-8") + (src / "projects.yaml").write_text("projects: []\n", encoding="utf-8") + (src / "rubrics.yaml").write_text("rubrics: []\n", encoding="utf-8") + +def test_import_preview_and_overwrite_warning(tmp_path: Path) -> None: + src = tmp_path / "srcpack" + make_valid_pack(src) mgr = WorkspaceManager(tmp_path / "registry.json", tmp_path / "roots") mgr.create_workspace("ws2", "Workspace Two") preview = mgr.preview_import(src, "ws2") @@ -13,9 +19,7 @@ def test_import_preview_and_overwrite_warning(tmp_path: Path) -> None: def test_import_draft_pack_requires_overwrite_flag(tmp_path: Path) -> None: src = tmp_path / "srcpack" - src.mkdir() - (src / "pack.yaml").write_text("name: x\ndisplay_name: X\nversion: 0.1.0\n", encoding="utf-8") - (src / "concepts.yaml").write_text("concepts: []\n", encoding="utf-8") + make_valid_pack(src) mgr = WorkspaceManager(tmp_path / "registry.json", tmp_path / "roots") mgr.create_workspace("ws2", "Workspace Two") try: diff --git a/webui/src/App.jsx b/webui/src/App.jsx index 508ba6a..c02feb7 100644 --- a/webui/src/App.jsx +++ b/webui/src/App.jsx @@ -46,11 +46,7 @@ export default function App() { }); const data = await res.json(); setImportPreview(data); - if (data.ok) { - setMessage("Import preview ready."); - } else { - setMessage("Import preview found blocking errors."); - } + setMessage(data.ok ? "Import preview ready." : "Import preview found blocking errors."); } async function importWorkspace() { @@ -157,10 +153,10 @@ export default function App() {
-

Didactopus Import Validation

+

Didactopus Full Pack Validation

Reduce the activation-energy hump from generated draft packs to curated review workspaces - by previewing structure, warnings, and overwrite risk before import. + by previewing structural coherence, warnings, and overwrite risk before import.

{message}
@@ -205,14 +201,17 @@ export default function App() {
Overwrite Required: {String(importPreview.overwrite_required)}
Pack: {importPreview.summary?.display_name || importPreview.summary?.pack_name || "-"}
Version: {importPreview.summary?.version || "-"}
-
Concept Count: {importPreview.summary?.concept_count ?? "-"}
+
Concepts: {importPreview.summary?.concept_count ?? "-"}
+
Roadmap Stages: {importPreview.summary?.roadmap_stage_count ?? "-"}
+
Projects: {importPreview.summary?.project_count ?? "-"}
+
Rubrics: {importPreview.summary?.rubric_count ?? "-"}
-

Errors

+

Validation Errors

-

Warnings

+

Validation Warnings