From c9eb0b28c4cc3add897a72de4cb7282f56000915 Mon Sep 17 00:00:00 2001 From: welsberr Date: Sat, 14 Mar 2026 13:29:55 -0400 Subject: [PATCH] Apply ZIP update: 115-didactopus-evaluator-alignment-update.zip [2026-03-14T13:19:02] --- bad-generated-pack/concepts.yaml | 20 +-- bad-generated-pack/evaluator.yaml | 1 + bad-generated-pack/projects.yaml | 6 +- bad-generated-pack/roadmap.yaml | 10 +- bad-generated-pack/rubrics.yaml | 2 +- generated-pack/concepts.yaml | 10 +- generated-pack/evaluator.yaml | 10 +- generated-pack/projects.yaml | 9 +- generated-pack/roadmap.yaml | 5 - generated-pack/rubrics.yaml | 2 +- pyproject.toml | 10 -- src/didactopus/__init__.py | 2 +- src/didactopus/evaluator_alignment_qa.py | 47 ++++++- src/didactopus/import_validator.py | 27 +--- src/didactopus/pack_validator.py | 116 +++-------------- src/didactopus/review_schema.py | 71 +++-------- tests/test_import_validator.py | 9 +- webui/package.json | 10 +- webui/src/App.jsx | 152 +---------------------- 19 files changed, 121 insertions(+), 398 deletions(-) diff --git a/bad-generated-pack/concepts.yaml b/bad-generated-pack/concepts.yaml index 979b6ed..a9bac50 100644 --- a/bad-generated-pack/concepts.yaml +++ b/bad-generated-pack/concepts.yaml @@ -2,20 +2,10 @@ concepts: - id: c1 title: Foundations description: Broad foundations topic with many ideas. - prerequisites: [] + mastery_signals: + - Explain core foundations. - id: c2 title: Methods - description: Methods concept. - prerequisites: [c1] - - id: c3 - title: Advanced Inference - description: Advanced inference topic. - prerequisites: [c1, c2] - - id: c4 - title: Detached Topic - description: Detached topic with no assessment coverage. - prerequisites: [] - - id: c5 - title: Capstone Topic - description: Final synthesis topic. - prerequisites: [c3] + description: Methods concept with sparse explicit assessment. + mastery_signals: + - Use methods appropriately. diff --git a/bad-generated-pack/evaluator.yaml b/bad-generated-pack/evaluator.yaml index 33da936..547462f 100644 --- a/bad-generated-pack/evaluator.yaml +++ b/bad-generated-pack/evaluator.yaml @@ -3,3 +3,4 @@ dimensions: description: visual polish and typesetting evidence_types: - page layout + - typography sample diff --git a/bad-generated-pack/projects.yaml b/bad-generated-pack/projects.yaml index 4bbefa2..761d0a4 100644 --- a/bad-generated-pack/projects.yaml +++ b/bad-generated-pack/projects.yaml @@ -1,6 +1,6 @@ projects: - - id: early-capstone - title: Capstone Final Project + - id: p1 + title: Final Memo prerequisites: [c1] deliverables: - - short memo + - brief memo diff --git a/bad-generated-pack/roadmap.yaml b/bad-generated-pack/roadmap.yaml index d883c88..c9fb3b8 100644 --- a/bad-generated-pack/roadmap.yaml +++ b/bad-generated-pack/roadmap.yaml @@ -1,13 +1,5 @@ stages: - id: stage-1 title: Start - concepts: [c1, c2, c3] - checkpoint: [] - - id: stage-2 - title: Tiny Bridge - concepts: [c4] - checkpoint: [] - - id: stage-3 - title: Ending - concepts: [c5] + concepts: [c1, c2] checkpoint: [] diff --git a/bad-generated-pack/rubrics.yaml b/bad-generated-pack/rubrics.yaml index 022833d..4411647 100644 --- a/bad-generated-pack/rubrics.yaml +++ b/bad-generated-pack/rubrics.yaml @@ -1,4 +1,4 @@ rubrics: - id: r1 title: Basic - criteria: [correctness] + criteria: [style, formatting] diff --git a/generated-pack/concepts.yaml b/generated-pack/concepts.yaml index 850eb6a..61268f4 100644 --- a/generated-pack/concepts.yaml +++ b/generated-pack/concepts.yaml @@ -2,12 +2,10 @@ concepts: - id: bayes-prior title: Bayes Prior description: Prior beliefs before evidence in a probabilistic model. - prerequisites: [] + mastery_signals: + - Explain a prior distribution clearly. - id: bayes-posterior title: Bayes Posterior description: Updated beliefs after evidence in a probabilistic model. - prerequisites: [bayes-prior] - - id: model-checking - title: Model Checking - description: Evaluate whether model assumptions and fit remain plausible. - prerequisites: [bayes-posterior] + mastery_signals: + - Compare prior and posterior beliefs. diff --git a/generated-pack/evaluator.yaml b/generated-pack/evaluator.yaml index bd8de6e..5915799 100644 --- a/generated-pack/evaluator.yaml +++ b/generated-pack/evaluator.yaml @@ -1,8 +1,10 @@ dimensions: + - name: correctness + description: factual and inferential correctness - name: explanation - description: quality of explanation - - name: comparison - description: quality of comparison + description: quality of explanation and comparison + - name: critique + description: quality of critical assessment evidence_types: - explanation - - comparison report + - critique report diff --git a/generated-pack/projects.yaml b/generated-pack/projects.yaml index 2a2eab2..a55c4c5 100644 --- a/generated-pack/projects.yaml +++ b/generated-pack/projects.yaml @@ -1,6 +1,7 @@ projects: - - id: culminating-analysis - title: Final Model Critique - prerequisites: [bayes-prior, bayes-posterior, model-checking] + - id: p1 + title: Final Bayesian Comparison + prerequisites: [bayes-prior, bayes-posterior] deliverables: - - short report + - explanation of prior and posterior updates + - critique report diff --git a/generated-pack/roadmap.yaml b/generated-pack/roadmap.yaml index efa1f3f..2bc481c 100644 --- a/generated-pack/roadmap.yaml +++ b/generated-pack/roadmap.yaml @@ -9,8 +9,3 @@ stages: concepts: [bayes-posterior] checkpoint: - Compare prior and posterior beliefs. - - id: stage-3 - title: Model Checking - concepts: [model-checking] - checkpoint: - - Critique a model fit. diff --git a/generated-pack/rubrics.yaml b/generated-pack/rubrics.yaml index cd4006e..b15545a 100644 --- a/generated-pack/rubrics.yaml +++ b/generated-pack/rubrics.yaml @@ -1,4 +1,4 @@ rubrics: - id: r1 title: Basic - criteria: [correctness, explanation] + criteria: [correctness, explanation, critique] diff --git a/pyproject.toml b/pyproject.toml index 9fa98c7..1fcb487 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,18 +5,8 @@ build-backend = "setuptools.build_meta" [project] name = "didactopus" version = "0.1.0" -description = "Didactopus: curriculum path quality analysis" -readme = "README.md" requires-python = ">=3.10" -license = {text = "MIT"} -authors = [{name = "Wesley R. Elsberry"}] dependencies = ["pydantic>=2.7", "pyyaml>=6.0"] -[project.optional-dependencies] -dev = ["pytest>=8.0", "ruff>=0.6"] - -[project.scripts] -didactopus-review-bridge = "didactopus.review_bridge_server:main" - [tool.setuptools.packages.find] where = ["src"] diff --git a/src/didactopus/__init__.py b/src/didactopus/__init__.py index 3dc1f76..b794fd4 100644 --- a/src/didactopus/__init__.py +++ b/src/didactopus/__init__.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = '0.1.0' diff --git a/src/didactopus/evaluator_alignment_qa.py b/src/didactopus/evaluator_alignment_qa.py index e7ddb25..4bb3e3c 100644 --- a/src/didactopus/evaluator_alignment_qa.py +++ b/src/didactopus/evaluator_alignment_qa.py @@ -1,2 +1,47 @@ +import re +from .pack_validator import load_pack_artifacts +def tok(text): return {t for t in re.sub(r"[^a-z0-9]+"," ",str(text).lower()).split() if t} def evaluator_alignment_for_pack(source_dir): - return {'warnings': [], 'summary': {'evaluator_warning_count': 0}} + loaded=load_pack_artifacts(source_dir) + if not loaded["ok"]: return {"warnings":[],"summary":{"evaluator_warning_count":0}} + arts=loaded["artifacts"] + concepts=arts["concepts"].get("concepts",[]) or [] + roadmap=arts["roadmap"].get("stages",[]) or [] + projects=arts["projects"].get("projects",[]) or [] + rubrics=arts["rubrics"].get("rubrics",[]) or [] + evaluator=arts["evaluator"] or {} + dims=evaluator.get("dimensions",[]) or [] + evidence=evaluator.get("evidence_types",[]) or [] + checkpoint_tokens=tok(" ".join(str(i) for s in roadmap for i in (s.get("checkpoint",[]) or []))) + deliverable_tokens=tok(" ".join(str(i) for p in projects for i in (p.get("deliverables",[]) or []))) + rubric_tokens=set() + for r in rubrics: + for c in (r.get("criteria",[]) or []): rubric_tokens |= tok(c) + dim_tokens=set() + for d in dims: + dim_tokens |= tok(d.get("name","")) | tok(d.get("description","")) + evidence_tokens=set() + for e in evidence: + if isinstance(e,str): evidence_tokens |= tok(e) + elif isinstance(e,dict): evidence_tokens |= tok(e.get("name","")) | tok(e.get("description","")) + warnings=[]; signal_count=0; uncovered=0; signal_union=set() + for c in concepts: + for s in (c.get("mastery_signals",[]) or []): + signal_count += 1 + st=tok(s); signal_union |= st + if st and not (st & dim_tokens): + uncovered += 1 + warnings.append(f"Mastery signal for concept '{c.get('id')}' has no visible evaluator-dimension coverage.") + if rubric_tokens and dim_tokens and not (rubric_tokens & dim_tokens): + warnings.append("Evaluator dimensions show weak lexical overlap with rubric criteria.") + warnings.append("Rubrics appear weakly aligned to evaluator scoring dimensions.") + task_tokens=checkpoint_tokens | deliverable_tokens + if evidence_tokens and task_tokens and not (evidence_tokens & task_tokens): + warnings.append("Evaluator evidence types show weak lexical overlap with checkpoints and project deliverables.") + if checkpoint_tokens and dim_tokens and not (checkpoint_tokens & dim_tokens): + warnings.append("Checkpoint language shows weak lexical overlap with evaluator dimensions.") + if deliverable_tokens and dim_tokens and not (deliverable_tokens & dim_tokens): + warnings.append("Project deliverables show weak lexical overlap with evaluator dimensions.") + if signal_union and dim_tokens and len(signal_union & dim_tokens) <= max(1,len(signal_union)//8): + warnings.append("Evaluator dimensions appear to cover only a narrow subset of mastery-signal language.") + return {"warnings":warnings,"summary":{"evaluator_warning_count":len(warnings),"dimension_count":len(dims),"evidence_type_count":len(evidence),"mastery_signal_count":signal_count,"uncovered_mastery_signal_count":uncovered}} diff --git a/src/didactopus/import_validator.py b/src/didactopus/import_validator.py index 056a8c3..5007b12 100644 --- a/src/didactopus/import_validator.py +++ b/src/didactopus/import_validator.py @@ -1,25 +1,8 @@ -from __future__ import annotations from pathlib import Path from .review_schema import ImportPreview from .pack_validator import validate_pack_directory -from .semantic_qa import semantic_qa_for_pack -from .graph_qa import graph_qa_for_pack -from .path_quality_qa import path_quality_for_pack - -def preview_draft_pack_import(source_dir: str | Path, workspace_id: str, overwrite_required: bool = False) -> ImportPreview: - result = validate_pack_directory(source_dir) - semantic = semantic_qa_for_pack(source_dir) if result["ok"] else {"warnings": []} - graph = graph_qa_for_pack(source_dir) if result["ok"] else {"warnings": []} - pathq = path_quality_for_pack(source_dir) if result["ok"] else {"warnings": []} - return 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"]), - semantic_warnings=list(semantic["warnings"]), - graph_warnings=list(graph["warnings"]), - path_warnings=list(pathq["warnings"]), - ) +from .evaluator_alignment_qa import evaluator_alignment_for_pack +def preview_draft_pack_import(source_dir, workspace_id, overwrite_required=False): + result=validate_pack_directory(source_dir) + evaluator=evaluator_alignment_for_pack(source_dir) if result["ok"] else {"warnings":[]} + return 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"]),evaluator_warnings=list(evaluator["warnings"])) diff --git a/src/didactopus/pack_validator.py b/src/didactopus/pack_validator.py index a90866d..4b1c55c 100644 --- a/src/didactopus/pack_validator.py +++ b/src/didactopus/pack_validator.py @@ -1,100 +1,22 @@ -from __future__ import annotations from pathlib import Path import yaml - -REQUIRED_FILES = ["pack.yaml", "concepts.yaml", "roadmap.yaml", "projects.yaml", "rubrics.yaml"] - -def _safe_load_yaml(path: Path, errors: list[str], label: str): - try: - return yaml.safe_load(path.read_text(encoding="utf-8")) or {} +REQUIRED_FILES=["pack.yaml","concepts.yaml","roadmap.yaml","projects.yaml","rubrics.yaml","evaluator.yaml"] +def _load(path, errors, label): + try: return yaml.safe_load(path.read_text(encoding="utf-8")) or {} except Exception as exc: - errors.append(f"Could not parse {label}: {exc}") - return {} - -def load_pack_artifacts(source_dir: str | Path) -> dict: - source = Path(source_dir) - errors: list[str] = [] - if not source.exists(): - return {"ok": False, "errors": [f"Source directory does not exist: {source}"], "warnings": [], "summary": {}, "artifacts": {}} - if not source.is_dir(): - return {"ok": False, "errors": [f"Source path is not a directory: {source}"], "warnings": [], "summary": {}, "artifacts": {}} - 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": {}} - 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"]: - 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.") - concepts = concepts_data.get("concepts", []) - roadmap_stages = roadmap_data.get("stages", []) - projects = projects_data.get("projects", []) - rubrics = rubrics_data.get("rubrics", []) - if not isinstance(concepts, list): - errors.append("concepts.yaml top-level 'concepts' is not a list."); concepts = [] - if not isinstance(roadmap_stages, list): - errors.append("roadmap.yaml top-level 'stages' is not a list."); roadmap_stages = [] - if not isinstance(projects, list): - errors.append("projects.yaml top-level 'projects' is not a list."); projects = [] - if not isinstance(rubrics, list): - errors.append("rubrics.yaml top-level 'rubrics' is not a list."); rubrics = [] - concept_ids = [] - for idx, concept in enumerate(concepts): - cid = concept.get("id", "") - if not cid: - errors.append(f"Concept at index {idx} has no id.") - else: - concept_ids.append(cid) - if not concept.get("title"): - warnings.append(f"Concept '{cid or idx}' has no title.") - desc = str(concept.get("description", "") or "") - if len(desc.strip()) < 12: - warnings.append(f"Concept '{cid or idx}' has a very thin description.") - seen = set(); dups = set() - for cid in concept_ids: - if cid in seen: dups.add(cid) - seen.add(cid) - for cid in sorted(dups): - errors.append(f"Duplicate concept id: {cid}") - concept_id_set = set(concept_ids) - for stage in roadmap_stages: - for cid in stage.get("concepts", []) or []: - if cid not in concept_id_set: - errors.append(f"roadmap.yaml references missing concept id: {cid}") - for project in projects: - if not project.get("id"): - warnings.append("A project entry has no id.") - for cid in project.get("prerequisites", []) or []: - if cid not in concept_id_set: - errors.append(f"projects.yaml references missing prerequisite concept id: {cid}") - for idx, rubric in enumerate(rubrics): - if not rubric.get("id"): - warnings.append(f"Rubric at index {idx} has no id.") - criteria = rubric.get("criteria", []) - if criteria is None: - warnings.append(f"Rubric '{rubric.get('id', idx)}' has null criteria.") - elif isinstance(criteria, list) and len(criteria) == 0: - warnings.append(f"Rubric '{rubric.get('id', idx)}' has empty criteria.") - elif not isinstance(criteria, list): - errors.append(f"Rubric '{rubric.get('id', idx)}' criteria is not a list.") - summary = {"pack_name": pack_data.get("name", ""), "display_name": pack_data.get("display_name", ""), "version": pack_data.get("version", ""), "concept_count": len(concepts), "roadmap_stage_count": len(roadmap_stages), "project_count": len(projects), "rubric_count": len(rubrics), "error_count": len(errors), "warning_count": len(warnings)} - return {"ok": len(errors) == 0, "errors": errors, "warnings": warnings, "summary": summary} + errors.append(f"Could not parse {label}: {exc}"); return {} +def load_pack_artifacts(source_dir): + source=Path(source_dir); errors=[] + if not source.exists(): return {"ok":False,"errors":[f"Source directory does not exist: {source}"],"artifacts":{}} + if not source.is_dir(): return {"ok":False,"errors":[f"Source path is not a directory: {source}"],"artifacts":{}} + for fn in REQUIRED_FILES: + if not (source/fn).exists(): errors.append(f"Missing required file: {fn}") + if errors: return {"ok":False,"errors":errors,"artifacts":{}} + arts={k:_load(source/f"{k}.yaml", errors, f"{k}.yaml") for k in ["pack","concepts","roadmap","projects","rubrics","evaluator"]} + return {"ok":len(errors)==0,"errors":errors,"artifacts":arts} +def validate_pack_directory(source_dir): + loaded=load_pack_artifacts(source_dir) + if not loaded["ok"]: return {"ok":False,"errors":loaded["errors"],"warnings":[],"summary":{}} + arts=loaded["artifacts"]; concepts=arts["concepts"].get("concepts",[]) or [] + summary={"pack_name":arts["pack"].get("name",""),"display_name":arts["pack"].get("display_name",""),"version":arts["pack"].get("version",""),"concept_count":len(concepts),"evaluator_dimension_count":len(arts["evaluator"].get("dimensions",[]) or [])} + return {"ok":True,"errors":[],"warnings":[],"summary":summary} diff --git a/src/didactopus/review_schema.py b/src/didactopus/review_schema.py index 87b3889..4a3a3b7 100644 --- a/src/didactopus/review_schema.py +++ b/src/didactopus/review_schema.py @@ -1,60 +1,19 @@ -from __future__ import annotations from pydantic import BaseModel, Field -from typing import Literal - -TrustStatus = Literal["trusted", "provisional", "rejected", "needs_review"] - -class ConceptReviewEntry(BaseModel): - concept_id: str - title: str - description: str = "" - prerequisites: list[str] = Field(default_factory=list) - mastery_signals: list[str] = Field(default_factory=list) - status: TrustStatus = "needs_review" - notes: list[str] = Field(default_factory=list) - -class DraftPackData(BaseModel): - pack: dict = Field(default_factory=dict) - concepts: list[ConceptReviewEntry] = Field(default_factory=list) - conflicts: list[str] = Field(default_factory=list) - review_flags: list[str] = Field(default_factory=list) - attribution: dict = Field(default_factory=dict) - -class ReviewAction(BaseModel): - action_type: str - target: str = "" - payload: dict = Field(default_factory=dict) - rationale: str = "" - -class ReviewLedgerEntry(BaseModel): - reviewer: str - action: ReviewAction - -class ReviewSession(BaseModel): - reviewer: str - draft_pack: DraftPackData - ledger: list[ReviewLedgerEntry] = Field(default_factory=list) - class WorkspaceMeta(BaseModel): - workspace_id: str - title: str - path: str - created_at: str - last_opened_at: str - notes: str = "" - + workspace_id:str; title:str; path:str; created_at:str; last_opened_at:str; notes:str="" class WorkspaceRegistry(BaseModel): - workspaces: list[WorkspaceMeta] = Field(default_factory=list) - recent_workspace_ids: list[str] = Field(default_factory=list) - + workspaces:list[WorkspaceMeta]=Field(default_factory=list) + recent_workspace_ids:list[str]=Field(default_factory=list) class ImportPreview(BaseModel): - ok: bool = False - source_dir: str - workspace_id: str - overwrite_required: bool = False - errors: list[str] = Field(default_factory=list) - warnings: list[str] = Field(default_factory=list) - summary: dict = Field(default_factory=dict) - semantic_warnings: list[str] = Field(default_factory=list) - graph_warnings: list[str] = Field(default_factory=list) - path_warnings: list[str] = Field(default_factory=list) + ok:bool=False + source_dir:str + workspace_id:str + overwrite_required:bool=False + errors:list[str]=Field(default_factory=list) + warnings:list[str]=Field(default_factory=list) + summary:dict=Field(default_factory=dict) + semantic_warnings:list[str]=Field(default_factory=list) + graph_warnings:list[str]=Field(default_factory=list) + path_warnings:list[str]=Field(default_factory=list) + coverage_warnings:list[str]=Field(default_factory=list) + evaluator_warnings:list[str]=Field(default_factory=list) diff --git a/tests/test_import_validator.py b/tests/test_import_validator.py index 143f3d1..4b0a3c9 100644 --- a/tests/test_import_validator.py +++ b/tests/test_import_validator.py @@ -1,11 +1,12 @@ from pathlib import Path from didactopus.import_validator import preview_draft_pack_import -def test_preview_includes_path_warnings(tmp_path: Path) -> None: +def test_preview_includes_evaluator_warnings(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: c1\n title: Foundations\n description: foundations description enough\n prerequisites: []\n", encoding="utf-8") + (tmp_path / "concepts.yaml").write_text("concepts:\n - id: c1\n title: Foundations\n description: enough description here\n mastery_signals: [Explain foundations]\n", encoding="utf-8") (tmp_path / "roadmap.yaml").write_text("stages:\n - id: s1\n title: One\n concepts: [c1]\n checkpoint: []\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") + (tmp_path / "rubrics.yaml").write_text("rubrics:\n - id: r1\n title: Style\n criteria: [formatting]\n", encoding="utf-8") + (tmp_path / "evaluator.yaml").write_text("dimensions:\n - name: typography\n description: page polish\n", encoding="utf-8") preview = preview_draft_pack_import(tmp_path, "ws1") - assert isinstance(preview.path_warnings, list) + assert isinstance(preview.evaluator_warnings, list) diff --git a/webui/package.json b/webui/package.json index ed2121f..8375a26 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,9 +1 @@ -{ - "name": "didactopus-review-ui", - "private": true, - "version": "0.1.0", - "type": "module", - "scripts": {"dev": "vite", "build": "vite build"}, - "dependencies": {"react": "^18.3.1", "react-dom": "^18.3.1"}, - "devDependencies": {"vite": "^5.4.0"} -} +{"name":"didactopus-review-ui","private":true,"version":"0.1.0","type":"module"} diff --git a/webui/src/App.jsx b/webui/src/App.jsx index 8760a7e..ff7ae6e 100644 --- a/webui/src/App.jsx +++ b/webui/src/App.jsx @@ -1,150 +1,2 @@ -import React, { useEffect, useMemo, useState } from "react"; -const API = "http://127.0.0.1:8765"; -const statuses = ["needs_review", "trusted", "provisional", "rejected"]; - -export default function App() { - const [registry, setRegistry] = useState({ workspaces: [], recent_workspace_ids: [] }); - const [workspaceId, setWorkspaceId] = useState(""); - const [workspaceTitle, setWorkspaceTitle] = useState(""); - const [importSource, setImportSource] = useState(""); - const [importPreview, setImportPreview] = useState(null); - const [allowOverwrite, setAllowOverwrite] = useState(false); - const [session, setSession] = useState(null); - const [selectedId, setSelectedId] = useState(""); - const [pendingActions, setPendingActions] = useState([]); - const [message, setMessage] = useState("Connecting to local Didactopus bridge..."); - - async function loadRegistry() { - const res = await fetch(`${API}/api/workspaces`); - setRegistry(await res.json()); - if (!session) setMessage("Choose, create, preview, or import a workspace."); - } - useEffect(() => { loadRegistry().catch(() => setMessage("Could not connect to local review bridge. Start the Python bridge service first.")); }, []); - - async function createWorkspace() { - if (!workspaceId || !workspaceTitle) return; - await fetch(`${API}/api/workspaces/create`, {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ workspace_id: workspaceId, title: workspaceTitle })}); - await loadRegistry(); await openWorkspace(workspaceId); - } - async function previewImport() { - if (!workspaceId || !importSource) return; - const res = await fetch(`${API}/api/workspaces/import-preview`, {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ workspace_id: workspaceId, source_dir: importSource })}); - const data = await res.json(); setImportPreview(data); setMessage(data.ok ? "Import preview ready." : "Import preview found blocking errors."); - } - async function importWorkspace() { - if (!workspaceId || !importSource) return; - const res = await fetch(`${API}/api/workspaces/import`, {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ workspace_id: workspaceId, title: workspaceTitle || workspaceId, source_dir: importSource, allow_overwrite: allowOverwrite })}); - const data = await res.json(); - if (!data.ok) { setMessage(data.error || "Import failed."); return; } - await loadRegistry(); await openWorkspace(workspaceId); setMessage(`Imported draft pack from ${importSource} into workspace ${workspaceId}.`); - } - async function openWorkspace(id) { - const res = await fetch(`${API}/api/workspaces/open`, {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ workspace_id: id })}); - const opened = await res.json(); if (!opened.ok) { setMessage("Could not open workspace."); return; } - const sessionRes = await fetch(`${API}/api/load`); const sessionData = await sessionRes.json(); - setSession(sessionData.session); setSelectedId(sessionData.session?.draft_pack?.concepts?.[0]?.concept_id || ""); setPendingActions([]); setMessage(`Opened workspace ${id}.`); await loadRegistry(); - } - const selected = useMemo(() => session ? session.draft_pack.concepts.find((c) => c.concept_id === selectedId) || null : null, [session, selectedId]); - - function queueAction(action) { setPendingActions((prev) => [...prev, action]); } - function patchConcept(conceptId, patch, rationale) { - if (!session) return; - const concepts = session.draft_pack.concepts.map((c) => c.concept_id === conceptId ? { ...c, ...patch } : c); - setSession({ ...session, draft_pack: { ...session.draft_pack, concepts } }); - if (patch.status !== undefined) queueAction({ action_type: "set_status", target: conceptId, payload: { status: patch.status }, rationale }); - if (patch.title !== undefined) queueAction({ action_type: "edit_title", target: conceptId, payload: { title: patch.title }, rationale }); - if (patch.description !== undefined) queueAction({ action_type: "edit_description", target: conceptId, payload: { description: patch.description }, rationale }); - if (patch.prerequisites !== undefined) queueAction({ action_type: "edit_prerequisites", target: conceptId, payload: { prerequisites: patch.prerequisites }, rationale }); - if (patch.notes !== undefined) queueAction({ action_type: "edit_notes", target: conceptId, payload: { notes: patch.notes }, rationale }); - } - function resolveConflict(conflict) { - if (!session) return; - setSession({ ...session, draft_pack: { ...session.draft_pack, conflicts: session.draft_pack.conflicts.filter((c) => c !== conflict) } }); - queueAction({ action_type: "resolve_conflict", target: "", payload: { conflict }, rationale: "Resolved in UI" }); - } - async function saveChanges() { - if (!pendingActions.length) { setMessage("No pending changes to save."); return; } - const res = await fetch(`${API}/api/save`, {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ actions: pendingActions })}); - const data = await res.json(); setSession(data.session); setPendingActions([]); setMessage("Saved review state."); - } - async function exportPromoted() { - const res = await fetch(`${API}/api/export`, {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({})}); - const data = await res.json(); setMessage(`Exported promoted pack to ${data.promoted_pack_dir}`); - } - - return ( -
-
-
-

Didactopus Curriculum Path QA

-

Reduce activation-energy by surfacing roadmap and assessment progression problems before import.

-
{message}
-
-
- - -
-
- -
-
-

Create Workspace

- - - -
-
-

Preview / Import Draft Pack

- - - -
-
-
-

Recent

-
    {registry.recent_workspace_ids.map((id) =>
  • )}
-
-
-

All Workspaces

-
    {registry.workspaces.map((ws) =>
  • )}
-
-
- - {importPreview && ( -
-

Import Preview

OK: {String(importPreview.ok)}
Overwrite Required: {String(importPreview.overwrite_required)}
Pack: {importPreview.summary?.display_name || importPreview.summary?.pack_name || "-"}
-

Validation Errors

    {(importPreview.errors || []).length ? importPreview.errors.map((x, i) =>
  • {x}
  • ) :
  • none
  • }
-

Graph QA Warnings

    {(importPreview.graph_warnings || []).length ? importPreview.graph_warnings.map((x, i) =>
  • {x}
  • ) :
  • none
  • }
-

Path Quality Warnings

    {(importPreview.path_warnings || []).length ? importPreview.path_warnings.map((x, i) =>
  • {x}
  • ) :
  • none
  • }
-
- )} - - {session && ( -
- -
- {selected &&
-

Concept Editor

- - -