Apply ZIP update: 090-didactopus-draft-pack-import-workflow-update.zip [2026-03-14T13:18:46]

This commit is contained in:
welsberr 2026-03-14 13:29:55 -04:00
parent 55b170f918
commit 6a983676b4
10 changed files with 105 additions and 53 deletions

View File

@ -1,19 +1,24 @@
# FAQ
## Why add a workspace manager?
## Why add a draft-pack import feature?
Because the activation-energy problem is not just parsing content. It is also
staying organized once several candidate domains and draft packs exist.
Because the transition from generated draft pack to curated workspace is one of the
places where users can lose momentum.
## What problem does this solve?
## How does this relate to the activation-energy goal?
It reduces the friction of:
- tracking multiple projects
- reopening previous review sessions
- switching among draft packs
- treating Didactopus like an actual working environment
Even when online course contents can be ingested, people may still stall if the next
steps are awkward. Importing a draft pack into a workspace should feel like one
smooth continuation, not a separate manual task.
## Does this help with online course ingestion?
## What does import do?
Yes. One of the barriers to using online course contents is that the setup work
quickly becomes messy. A workspace manager helps turn that mess into a manageable process.
In this scaffold it:
- creates or updates a workspace
- copies a source draft-pack directory into that workspace
- makes it ready to open in the review UI
## Is the import workflow validated?
Only lightly in this scaffold. A future revision should add stronger schema checks,
collision handling, and overwrite safeguards.

View File

@ -1,21 +1,14 @@
concepts:
- id: bayes-prior
title: Bayes Prior
description: Prior beliefs before evidence in a probabilistic model.
description: Prior beliefs before evidence.
prerequisites: []
mastery_signals:
- Explain a prior distribution.
- id: bayes-posterior
title: Bayes Posterior
description: Updated beliefs after evidence in a probabilistic model.
description: Updated beliefs after evidence.
prerequisites:
- bayes-prior
mastery_signals:
- Compare prior and posterior beliefs.
- id: model-checking
title: Model Checking
description: Evaluate whether model assumptions and fit remain plausible.
prerequisites:
- bayes-posterior
mastery_signals:
- Critique a model fit.

View File

@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "didactopus"
version = "0.1.0"
description = "Didactopus: workspace manager for local review UI"
description = "Didactopus: draft-pack import workflow for workspaces"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}

View File

@ -73,6 +73,21 @@ class ReviewBridgeHandler(BaseHTTPRequestHandler):
json_response(self, 200, {"ok": True, "workspace_id": self.active_workspace_id})
return
if self.path == "/api/workspaces/import":
try:
meta = self.workspace_manager.import_draft_pack(
source_dir=payload["source_dir"],
workspace_id=payload["workspace_id"],
title=payload.get("title"),
notes=payload.get("notes", "")
)
except FileNotFoundError as exc:
json_response(self, 404, {"error": str(exc)})
return
self.set_active_workspace(meta.workspace_id)
json_response(self, 200, {"ok": True, "workspace": meta.model_dump()})
return
if self.active_bridge is None:
json_response(self, 400, {"error": "no active workspace"})
return

View File

@ -1,7 +1,7 @@
from __future__ import annotations
from pathlib import Path
from datetime import datetime, UTC
import json
import json, shutil
from .review_schema import WorkspaceMeta, WorkspaceRegistry
def utc_now() -> str:
@ -70,3 +70,34 @@ class WorkspaceManager:
if ws.workspace_id == workspace_id:
return ws
return None
def import_draft_pack(self, source_dir: str | Path, workspace_id: str, title: str | None = None, notes: str = "") -> WorkspaceMeta:
source_dir = Path(source_dir)
if not source_dir.exists():
raise FileNotFoundError(f"Draft pack source does not exist: {source_dir}")
meta = self.get_workspace(workspace_id)
if meta is None:
meta = self.create_workspace(workspace_id, title or workspace_id, notes=notes)
else:
self.touch_recent(workspace_id)
workspace_dir = Path(meta.path)
target_draft = workspace_dir / "draft_pack"
if target_draft.exists():
shutil.rmtree(target_draft)
shutil.copytree(source_dir, target_draft)
registry = self.load_registry()
for ws in registry.workspaces:
if ws.workspace_id == workspace_id:
ws.last_opened_at = utc_now()
if title:
ws.title = title
if notes:
ws.notes = notes
meta = ws
break
registry.recent_workspace_ids = [workspace_id] + [w for w in registry.recent_workspace_ids if w != workspace_id]
self.save_registry(registry)
return meta

View File

@ -9,9 +9,12 @@ def test_create_and_get_workspace(tmp_path: Path) -> None:
assert got is not None
assert got.title == "Workspace One"
def test_recent_tracking(tmp_path: Path) -> None:
def test_import_draft_pack(tmp_path: Path) -> None:
src = tmp_path / "srcpack"
src.mkdir()
(src / "pack.yaml").write_text("name: x\n", encoding="utf-8")
(src / "concepts.yaml").write_text("concepts: []\n", encoding="utf-8")
mgr = WorkspaceManager(tmp_path / "registry.json", tmp_path / "roots")
mgr.create_workspace("ws1", "Workspace One")
mgr.touch_recent("ws1")
reg = mgr.list_workspaces()
assert reg.recent_workspace_ids[0] == "ws1"
meta = mgr.import_draft_pack(src, "ws2", title="Workspace Two")
assert meta.workspace_id == "ws2"
assert (tmp_path / "roots" / "ws2" / "draft_pack" / "pack.yaml").exists()

View File

@ -7,6 +7,7 @@ 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 [session, setSession] = useState(null);
const [selectedId, setSelectedId] = useState("");
const [pendingActions, setPendingActions] = useState([]);
@ -16,9 +17,7 @@ export default function App() {
const res = await fetch(`${API}/api/workspaces`);
const data = await res.json();
setRegistry(data);
if (data.recent_workspace_ids?.length && !session) {
setMessage("Choose or reopen a workspace.");
}
if (!session) setMessage("Choose, create, or import a workspace.");
}
useEffect(() => {
@ -36,6 +35,27 @@ export default function App() {
await openWorkspace(workspaceId);
}
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
})
});
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",
@ -138,6 +158,12 @@ export default function App() {
<label>Title<input value={workspaceTitle} onChange={(e) => setWorkspaceTitle(e.target.value)} /></label>
<button onClick={createWorkspace}>Create</button>
</div>
<div className="card">
<h2>Import Draft Pack</h2>
<label>Workspace ID<input value={workspaceId} onChange={(e) => setWorkspaceId(e.target.value)} /></label>
<label>Draft Pack Source Directory<input value={importSource} onChange={(e) => setImportSource(e.target.value)} placeholder="e.g. generated-pack" /></label>
<button onClick={importWorkspace}>Import into Workspace</button>
</div>
<div className="card">
<h2>Recent</h2>
<ul>{registry.recent_workspace_ids.map((id) => <li key={id}><button onClick={() => openWorkspace(id)}>{id}</button></li>)}</ul>
@ -146,10 +172,6 @@ export default function App() {
<h2>All Workspaces</h2>
<ul>{registry.workspaces.map((ws) => <li key={ws.workspace_id}><button onClick={() => openWorkspace(ws.workspace_id)}>{ws.title} ({ws.workspace_id})</button></li>)}</ul>
</div>
<div className="card">
<h2>Pending Actions</h2>
<div className="big">{pendingActions.length}</div>
</div>
</section>
{session && (

View File

@ -28,7 +28,6 @@ button:hover { border-color: var(--accent); }
label { display: block; font-weight: 600; margin-bottom: 12px; }
input, textarea, select { width: 100%; margin-top: 6px; border: 1px solid var(--border); border-radius: 10px; padding: 10px; font: inherit; background: white; }
.small { color: var(--muted); }
.big { font-size: 34px; font-weight: 700; }
.conflict { border-top: 1px solid var(--border); padding-top: 12px; margin-top: 12px; }
ul { padding-left: 18px; }
@media (max-width: 1100px) {

View File

@ -7,18 +7,9 @@
"created_at": "2026-03-13T12:00:00+00:00",
"last_opened_at": "2026-03-13T12:30:00+00:00",
"notes": "Initial imported course pack"
},
{
"workspace_id": "stats-foundations",
"title": "Statistics Foundations",
"path": "workspaces/stats-foundations",
"created_at": "2026-03-13T12:05:00+00:00",
"last_opened_at": "2026-03-13T12:20:00+00:00",
"notes": "Secondary draft pack"
}
],
"recent_workspace_ids": [
"bayes-intro",
"stats-foundations"
"bayes-intro"
]
}

View File

@ -5,10 +5,3 @@ concepts:
prerequisites: []
mastery_signals:
- Explain mean, median, and variance.
- id: probability-basics
title: Probability Basics
description: Basic event probability and conditional probability.
prerequisites:
- descriptive-statistics
mastery_signals:
- Compute a simple conditional probability.