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:
- 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.

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:
- id: stage-1
title: Foundations
title: Bad Stage
concepts:
- statistics-and-probability
- id: stage-2
title: Advanced Inference
concepts:
- posterior-analysis
- missing-concept

View File

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

View File

@ -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.

View File

@ -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.

View File

@ -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"}

View File

@ -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

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}")
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.")

View File

@ -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

View File

@ -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)

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")
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"])

View File

@ -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:

View File

@ -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() {
<div className="page">
<header className="hero">
<div>
<h1>Didactopus Import Validation</h1>
<h1>Didactopus Full Pack Validation</h1>
<p>
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>
<div className="small">{message}</div>
</div>
@ -205,14 +201,17 @@ export default function App() {
<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>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 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>
</div>
<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>
</div>
</section>