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 # 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 Because the transition from generated draft pack to curated workspace is one of the
staying organized once several candidate domains and draft packs exist. 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: Even when online course contents can be ingested, people may still stall if the next
- tracking multiple projects steps are awkward. Importing a draft pack into a workspace should feel like one
- reopening previous review sessions smooth continuation, not a separate manual task.
- switching among draft packs
- treating Didactopus like an actual working environment
## 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 In this scaffold it:
quickly becomes messy. A workspace manager helps turn that mess into a manageable process. - 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: concepts:
- id: bayes-prior - id: bayes-prior
title: Bayes Prior title: Bayes Prior
description: Prior beliefs before evidence in a probabilistic model. description: Prior beliefs before evidence.
prerequisites: [] prerequisites: []
mastery_signals: mastery_signals:
- Explain a prior distribution. - Explain a prior distribution.
- id: bayes-posterior - id: bayes-posterior
title: Bayes Posterior title: Bayes Posterior
description: Updated beliefs after evidence in a probabilistic model. description: Updated beliefs after evidence.
prerequisites: prerequisites:
- bayes-prior - bayes-prior
mastery_signals: mastery_signals:
- Compare prior and posterior beliefs. - 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] [project]
name = "didactopus" name = "didactopus"
version = "0.1.0" version = "0.1.0"
description = "Didactopus: workspace manager for local review UI" description = "Didactopus: draft-pack import workflow for workspaces"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
license = {text = "MIT"} license = {text = "MIT"}

View File

@ -73,6 +73,21 @@ class ReviewBridgeHandler(BaseHTTPRequestHandler):
json_response(self, 200, {"ok": True, "workspace_id": self.active_workspace_id}) json_response(self, 200, {"ok": True, "workspace_id": self.active_workspace_id})
return 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: if self.active_bridge is None:
json_response(self, 400, {"error": "no active workspace"}) json_response(self, 400, {"error": "no active workspace"})
return return

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from datetime import datetime, UTC from datetime import datetime, UTC
import json import json, shutil
from .review_schema import WorkspaceMeta, WorkspaceRegistry from .review_schema import WorkspaceMeta, WorkspaceRegistry
def utc_now() -> str: def utc_now() -> str:
@ -70,3 +70,34 @@ class WorkspaceManager:
if ws.workspace_id == workspace_id: if ws.workspace_id == workspace_id:
return ws return ws
return None 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 is not None
assert got.title == "Workspace One" 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 = WorkspaceManager(tmp_path / "registry.json", tmp_path / "roots")
mgr.create_workspace("ws1", "Workspace One") meta = mgr.import_draft_pack(src, "ws2", title="Workspace Two")
mgr.touch_recent("ws1") assert meta.workspace_id == "ws2"
reg = mgr.list_workspaces() assert (tmp_path / "roots" / "ws2" / "draft_pack" / "pack.yaml").exists()
assert reg.recent_workspace_ids[0] == "ws1"

View File

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

View File

@ -28,7 +28,6 @@ button:hover { border-color: var(--accent); }
label { display: block; font-weight: 600; margin-bottom: 12px; } 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; } 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); } .small { color: var(--muted); }
.big { font-size: 34px; font-weight: 700; }
.conflict { border-top: 1px solid var(--border); padding-top: 12px; margin-top: 12px; } .conflict { border-top: 1px solid var(--border); padding-top: 12px; margin-top: 12px; }
ul { padding-left: 18px; } ul { padding-left: 18px; }
@media (max-width: 1100px) { @media (max-width: 1100px) {

View File

@ -7,18 +7,9 @@
"created_at": "2026-03-13T12:00:00+00:00", "created_at": "2026-03-13T12:00:00+00:00",
"last_opened_at": "2026-03-13T12:30:00+00:00", "last_opened_at": "2026-03-13T12:30:00+00:00",
"notes": "Initial imported course pack" "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": [ "recent_workspace_ids": [
"bayes-intro", "bayes-intro"
"stats-foundations"
] ]
} }

View File

@ -5,10 +5,3 @@ concepts:
prerequisites: [] prerequisites: []
mastery_signals: mastery_signals:
- Explain mean, median, and variance. - 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.