Apply ZIP update: 100-didactopus-full-pack-validation-update.zip [2026-03-14T13:18:53]

This commit is contained in:
welsberr 2026-03-14 13:29:55 -04:00
parent 8c9776d6fe
commit f3ea3da848
15 changed files with 102 additions and 156 deletions

View File

@ -1,13 +1,7 @@
concepts: concepts:
- id: prior-and-posterior - id: duplicate
title: Prior and Posterior title: First
description: Beliefs before and after evidence. description: Tiny.
prerequisites: [] - id: duplicate
- id: posterior-analysis title: Second
title: Posterior Analysis description: Tiny.
description: Beliefs before and after evidence.
prerequisites: []
- id: statistics-and-probability
title: Statistics and Probability
description: General overview.
prerequisites: []

View File

@ -1 +1,3 @@
display_name Imported Pack Broken YAML name: broken-pack
display_name: Broken Pack
version: 0.1.0-draft

View File

@ -1 +1,5 @@
projects: [] projects:
- id: bad-project
title: Bad Project
prerequisites:
- missing-concept

View File

@ -1,9 +1,5 @@
stages: stages:
- id: stage-1 - id: stage-1
title: Foundations title: Bad Stage
concepts: concepts:
- statistics-and-probability - missing-concept
- id: stage-2
title: Advanced Inference
concepts:
- posterior-analysis

View File

@ -1,5 +1,4 @@
rubrics: rubrics:
- id: basic-rubric - id:
title: Basic Rubric title: Broken Rubric
criteria: criteria: invalid
- correctness

View File

@ -1,28 +1,32 @@
# FAQ # FAQ
## Why add import validation? ## Why add a full pack validator?
Because reducing startup friction does not mean hiding risk. A user still needs Because import safety is not only about whether files exist. It is also about
a clear signal about whether a generated draft pack is structurally usable. 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 It reduces uncertainty at a crucial point. Users can see whether a generated pack
looks valid before committing it into a workspace. 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 - required files
- basic YAML parsing - YAML parsing
- key metadata presence - metadata presence
- concept count - duplicate concept ids
- overwrite conditions - 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.

View File

@ -1,13 +1,8 @@
stages: stages:
- id: stage-1 - id: stage-1
title: Prior Beliefs title: Bayes Basics
concepts: concepts:
- bayes-prior - bayes-prior
- id: stage-2
title: Posterior Updating
concepts:
- bayes-posterior - bayes-posterior
- id: stage-3 checkpoint:
title: Model Checking - Compare prior and posterior beliefs.
concepts:
- model-checking

View File

@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "didactopus" name = "didactopus"
version = "0.1.0" version = "0.1.0"
description = "Didactopus: import validation and safety layer" description = "Didactopus: full pack validation layer"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
license = {text = "MIT"} license = {text = "MIT"}

View File

@ -1,56 +1,17 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
import yaml
from .review_schema import ImportPreview from .review_schema import ImportPreview
from .pack_validator import validate_pack_directory
REQUIRED_FILES = ["pack.yaml", "concepts.yaml"]
def preview_draft_pack_import(source_dir: str | Path, workspace_id: str, overwrite_required: bool = False) -> ImportPreview: def preview_draft_pack_import(source_dir: str | Path, workspace_id: str, overwrite_required: bool = False) -> ImportPreview:
source = Path(source_dir) result = validate_pack_directory(source_dir)
preview = ImportPreview(source_dir=str(source), workspace_id=workspace_id, overwrite_required=overwrite_required) preview = ImportPreview(
source_dir=str(Path(source_dir)),
if not source.exists(): workspace_id=workspace_id,
preview.errors.append(f"Source directory does not exist: {source}") overwrite_required=overwrite_required,
return preview ok=result["ok"],
if not source.is_dir(): errors=list(result["errors"]),
preview.errors.append(f"Source path is not a directory: {source}") warnings=list(result["warnings"]),
return preview summary=dict(result["summary"]),
)
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
return preview return preview

View File

@ -11,53 +11,33 @@ def _safe_load_yaml(path: Path, errors: list[str], label: str):
errors.append(f"Could not parse {label}: {exc}") errors.append(f"Could not parse {label}: {exc}")
return {} return {}
def load_pack_artifacts(source_dir: str | Path) -> dict: def validate_pack_directory(source_dir: str | Path) -> dict:
source = Path(source_dir) source = Path(source_dir)
errors: list[str] = [] errors: list[str] = []
warnings: list[str] = []
summary: dict = {}
if not source.exists(): 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(): 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: for filename in REQUIRED_FILES:
if not (source / filename).exists(): if not (source / filename).exists():
errors.append(f"Missing required file: {filename}") errors.append(f"Missing required file: {filename}")
if errors: 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") pack_data = _safe_load_yaml(source / "pack.yaml", errors, "pack.yaml")
concepts_data = _safe_load_yaml(source / "concepts.yaml", errors, "concepts.yaml") concepts_data = _safe_load_yaml(source / "concepts.yaml", errors, "concepts.yaml")
roadmap_data = _safe_load_yaml(source / "roadmap.yaml", errors, "roadmap.yaml") roadmap_data = _safe_load_yaml(source / "roadmap.yaml", errors, "roadmap.yaml")
projects_data = _safe_load_yaml(source / "projects.yaml", errors, "projects.yaml") projects_data = _safe_load_yaml(source / "projects.yaml", errors, "projects.yaml")
rubrics_data = _safe_load_yaml(source / "rubrics.yaml", errors, "rubrics.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: if errors:
loaded = load_pack_artifacts(source_dir)
errors = list(loaded["errors"])
warnings = list(loaded["warnings"])
summary = dict(loaded["summary"])
if not loaded["ok"]:
return {"ok": False, "errors": errors, "warnings": warnings, "summary": summary} 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"]: for field in ["name", "display_name", "version"]:
if field not in pack_data: if field not in pack_data:
warnings.append(f"pack.yaml has no '{field}' field.") warnings.append(f"pack.yaml has no '{field}' field.")

View File

@ -120,7 +120,7 @@ class ReviewBridgeHandler(BaseHTTPRequestHandler):
json_response(self, 404, {"error": "not found"}) json_response(self, 404, {"error": "not found"})
def build_parser() -> argparse.ArgumentParser: 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") parser.add_argument("--config", default="configs/config.example.yaml")
return parser return parser

View File

@ -2,18 +2,16 @@ from pathlib import Path
from didactopus.import_validator import preview_draft_pack_import from didactopus.import_validator import preview_draft_pack_import
def test_valid_preview(tmp_path: Path) -> None: def test_valid_preview(tmp_path: Path) -> None:
src = tmp_path / "src" (tmp_path / "pack.yaml").write_text("name: p\ndisplay_name: P\nversion: 0.1.0\n", encoding="utf-8")
src.mkdir() (tmp_path / "concepts.yaml").write_text("concepts:\n - id: c1\n title: C1\n description: A full enough description.\n", encoding="utf-8")
(src / "pack.yaml").write_text("name: p\ndisplay_name: P\nversion: 0.1.0\n", encoding="utf-8") (tmp_path / "roadmap.yaml").write_text("stages: []\n", encoding="utf-8")
(src / "concepts.yaml").write_text("concepts: []\n", encoding="utf-8") (tmp_path / "projects.yaml").write_text("projects: []\n", encoding="utf-8")
preview = preview_draft_pack_import(src, "ws1") (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.ok is True
assert preview.summary["concept_count"] == 0 assert preview.summary["concept_count"] == 1
def test_missing_required_file(tmp_path: Path) -> None: def test_missing_required_file(tmp_path: Path) -> None:
src = tmp_path / "src" (tmp_path / "pack.yaml").write_text("name: p\n", encoding="utf-8")
src.mkdir() preview = preview_draft_pack_import(tmp_path, "ws1")
(src / "pack.yaml").write_text("name: p\n", encoding="utf-8")
preview = preview_draft_pack_import(src, "ws1")
assert preview.ok is False assert preview.ok is False
assert any("Missing required file" in e for e in preview.errors)

View File

@ -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") (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) result = validate_pack_directory(tmp_path)
assert result["ok"] is True 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"])

View File

@ -1,11 +1,17 @@
from pathlib import Path from pathlib import Path
from didactopus.workspace_manager import WorkspaceManager from didactopus.workspace_manager import WorkspaceManager
def test_import_preview_and_overwrite_warning(tmp_path: Path) -> None: def make_valid_pack(src: Path):
src = tmp_path / "srcpack"
src.mkdir() src.mkdir()
(src / "pack.yaml").write_text("name: x\ndisplay_name: X\nversion: 0.1.0\n", encoding="utf-8") (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 = WorkspaceManager(tmp_path / "registry.json", tmp_path / "roots")
mgr.create_workspace("ws2", "Workspace Two") mgr.create_workspace("ws2", "Workspace Two")
preview = mgr.preview_import(src, "ws2") 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: def test_import_draft_pack_requires_overwrite_flag(tmp_path: Path) -> None:
src = tmp_path / "srcpack" src = tmp_path / "srcpack"
src.mkdir() make_valid_pack(src)
(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")
mgr = WorkspaceManager(tmp_path / "registry.json", tmp_path / "roots") mgr = WorkspaceManager(tmp_path / "registry.json", tmp_path / "roots")
mgr.create_workspace("ws2", "Workspace Two") mgr.create_workspace("ws2", "Workspace Two")
try: try:

View File

@ -46,11 +46,7 @@ export default function App() {
}); });
const data = await res.json(); const data = await res.json();
setImportPreview(data); setImportPreview(data);
if (data.ok) { setMessage(data.ok ? "Import preview ready." : "Import preview found blocking errors.");
setMessage("Import preview ready.");
} else {
setMessage("Import preview found blocking errors.");
}
} }
async function importWorkspace() { async function importWorkspace() {
@ -157,10 +153,10 @@ export default function App() {
<div className="page"> <div className="page">
<header className="hero"> <header className="hero">
<div> <div>
<h1>Didactopus Import Validation</h1> <h1>Didactopus Full Pack Validation</h1>
<p> <p>
Reduce the activation-energy hump from generated draft packs to curated review workspaces 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.
</p> </p>
<div className="small">{message}</div> <div className="small">{message}</div>
</div> </div>
@ -205,14 +201,17 @@ export default function App() {
<div><strong>Overwrite Required:</strong> {String(importPreview.overwrite_required)}</div> <div><strong>Overwrite Required:</strong> {String(importPreview.overwrite_required)}</div>
<div><strong>Pack:</strong> {importPreview.summary?.display_name || importPreview.summary?.pack_name || "-"}</div> <div><strong>Pack:</strong> {importPreview.summary?.display_name || importPreview.summary?.pack_name || "-"}</div>
<div><strong>Version:</strong> {importPreview.summary?.version || "-"}</div> <div><strong>Version:</strong> {importPreview.summary?.version || "-"}</div>
<div><strong>Concept Count:</strong> {importPreview.summary?.concept_count ?? "-"}</div> <div><strong>Concepts:</strong> {importPreview.summary?.concept_count ?? "-"}</div>
<div><strong>Roadmap Stages:</strong> {importPreview.summary?.roadmap_stage_count ?? "-"}</div>
<div><strong>Projects:</strong> {importPreview.summary?.project_count ?? "-"}</div>
<div><strong>Rubrics:</strong> {importPreview.summary?.rubric_count ?? "-"}</div>
</div> </div>
<div className="card"> <div className="card">
<h2>Errors</h2> <h2>Validation Errors</h2>
<ul>{(importPreview.errors || []).length ? importPreview.errors.map((x, i) => <li key={i}>{x}</li>) : <li>none</li>}</ul> <ul>{(importPreview.errors || []).length ? importPreview.errors.map((x, i) => <li key={i}>{x}</li>) : <li>none</li>}</ul>
</div> </div>
<div className="card"> <div className="card">
<h2>Warnings</h2> <h2>Validation Warnings</h2>
<ul>{(importPreview.warnings || []).length ? importPreview.warnings.map((x, i) => <li key={i}>{x}</li>) : <li>none</li>}</ul> <ul>{(importPreview.warnings || []).length ? importPreview.warnings.map((x, i) => <li key={i}>{x}</li>) : <li>none</li>}</ul>
</div> </div>
</section> </section>