diff --git a/src/didactopus/api.py b/src/didactopus/api.py
index 64fd3f3..0942b91 100644
--- a/src/didactopus/api.py
+++ b/src/didactopus/api.py
@@ -5,10 +5,10 @@ from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from .config import load_settings
from .db import Base, engine
-from .models import LoginRequest, RefreshRequest, TokenPair, CreateLearnerRequest, LearnerState, EvidenceEvent, EvaluatorSubmission, EvaluatorJobStatus, CreatePackRequest, GovernanceAction, ReviewCommentCreate, ContributionSubmissionCreate, AgentCapabilityManifest, AgentLearnerPlanRequest, AgentLearnerPlanResponse
+from .models import LoginRequest, RefreshRequest, TokenPair, CreateLearnerRequest, LearnerState, EvidenceEvent, EvaluatorSubmission, EvaluatorJobStatus, CreatePackRequest, GovernanceAction, ReviewCommentCreate, ContributionSubmissionCreate
from .repository import (
authenticate_user, get_user_by_id, store_refresh_token, refresh_token_active, revoke_refresh_token,
- deployment_policy_profile, list_packs_for_user, list_pack_admin_rows, get_pack, get_pack_row, get_pack_validation, get_pack_provenance,
+ list_packs_for_user, list_pack_admin_rows, get_pack, get_pack_row, get_pack_validation, get_pack_provenance,
upsert_pack, create_submission, list_submissions, get_submission_diff, get_submission_gates, list_review_tasks,
set_pack_publication, can_publish_pack, set_governance_state, list_pack_versions, add_review_comment, list_review_comments,
create_learner, list_learners_for_user, learner_owned_by_user, load_learner_state,
@@ -57,35 +57,6 @@ def ensure_pack_access(user, pack_id: str):
return row
raise HTTPException(status_code=403, detail="Pack not accessible by this user")
-@app.get("/api/deployment-policy")
-def api_deployment_policy(user = Depends(current_user)):
- return deployment_policy_profile().model_dump()
-
-@app.get("/api/agent/capabilities", response_model=AgentCapabilityManifest)
-def api_agent_capabilities(user = Depends(current_user)):
- return AgentCapabilityManifest()
-
-@app.post("/api/agent/learner-plan", response_model=AgentLearnerPlanResponse)
-def api_agent_learner_plan(payload: AgentLearnerPlanRequest, user = Depends(current_user)):
- ensure_learner_access(user, payload.learner_id)
- ensure_pack_access(user, payload.pack_id)
- state = load_learner_state(payload.learner_id)
- pack = get_pack(payload.pack_id)
- if pack is None:
- raise HTTPException(status_code=404, detail="Pack not found")
- cards = recommend_next(state, pack)
- return AgentLearnerPlanResponse(
- learner_id=payload.learner_id,
- pack_id=payload.pack_id,
- next_cards=cards,
- suggested_actions=[
- "Read current learner state",
- "Select the highest-priority next card",
- "Attempt the exercise or checkpoint",
- "Post evidence and refresh recommendations",
- ],
- )
-
@app.post("/api/login", response_model=TokenPair)
def login(payload: LoginRequest):
user = authenticate_user(payload.username, payload.password)
diff --git a/src/didactopus/config.py b/src/didactopus/config.py
index 0a1ffa3..6db28e1 100644
--- a/src/didactopus/config.py
+++ b/src/didactopus/config.py
@@ -8,7 +8,6 @@ class Settings(BaseModel):
port: int = int(os.getenv("DIDACTOPUS_PORT", "8011"))
jwt_secret: str = os.getenv("DIDACTOPUS_JWT_SECRET", "change-me")
jwt_algorithm: str = "HS256"
- deployment_policy_profile: str = os.getenv("DIDACTOPUS_POLICY_PROFILE", "single_user")
def load_settings() -> Settings:
return Settings()
diff --git a/src/didactopus/models.py b/src/didactopus/models.py
index 99ca091..5cd5a5f 100644
--- a/src/didactopus/models.py
+++ b/src/didactopus/models.py
@@ -19,26 +19,6 @@ class LoginRequest(BaseModel):
class RefreshRequest(BaseModel):
refresh_token: str
-class DeploymentPolicyProfile(BaseModel):
- profile_name: str
- default_personal_lane_enabled: bool = True
- default_community_lane_enabled: bool = True
- community_publish_requires_approval: bool = True
- personal_publish_direct: bool = True
- reviewer_assignment_required: bool = False
- description: str = ""
-
-class AgentCapabilityManifest(BaseModel):
- supports_pack_listing: bool = True
- supports_pack_write_personal: bool = True
- supports_pack_submit_community: bool = True
- supports_recommendations: bool = True
- supports_learner_state_read: bool = True
- supports_learner_state_write: bool = True
- supports_evaluator_jobs: bool = True
- supports_governance_endpoints: bool = True
- supports_review_queue: bool = True
-
class PackConcept(BaseModel):
id: str
title: str
@@ -118,13 +98,3 @@ class EvaluatorJobStatus(BaseModel):
result_score: float | None = None
result_confidence_hint: float | None = None
result_notes: str = ""
-
-class AgentLearnerPlanRequest(BaseModel):
- learner_id: str
- pack_id: str
-
-class AgentLearnerPlanResponse(BaseModel):
- learner_id: str
- pack_id: str
- next_cards: list[dict] = Field(default_factory=list)
- suggested_actions: list[str] = Field(default_factory=list)
diff --git a/src/didactopus/orm.py b/src/didactopus/orm.py
index e953eb4..1a49273 100644
--- a/src/didactopus/orm.py
+++ b/src/didactopus/orm.py
@@ -21,7 +21,7 @@ class PackORM(Base):
__tablename__ = "packs"
id: Mapped[str] = mapped_column(String(100), primary_key=True)
owner_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
- policy_lane: Mapped[str] = mapped_column(String(50), default="personal")
+ policy_lane: Mapped[str] = mapped_column(String(50), default="personal") # personal | community
title: Mapped[str] = mapped_column(String(255))
subtitle: Mapped[str] = mapped_column(Text, default="")
level: Mapped[str] = mapped_column(String(100), default="novice-friendly")
diff --git a/src/didactopus/repository.py b/src/didactopus/repository.py
index b2258ed..576479f 100644
--- a/src/didactopus/repository.py
+++ b/src/didactopus/repository.py
@@ -4,47 +4,12 @@ from datetime import datetime, timezone
from sqlalchemy import select
from .db import SessionLocal
from .orm import UserORM, RefreshTokenORM, PackORM, PackVersionORM, ReviewCommentORM, ContributionSubmissionORM, ReviewTaskORM, LearnerORM, MasteryRecordORM, EvidenceEventORM, EvaluatorJobORM
-from .models import PackData, LearnerState, MasteryRecord, EvidenceEvent, DeploymentPolicyProfile
+from .models import PackData, LearnerState, MasteryRecord, EvidenceEvent
from .auth import verify_password
-from .config import load_settings
-
-settings = load_settings()
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
-def deployment_policy_profile() -> DeploymentPolicyProfile:
- profile = settings.deployment_policy_profile
- if profile == "community_repo":
- return DeploymentPolicyProfile(
- profile_name="community_repo",
- default_personal_lane_enabled=True,
- default_community_lane_enabled=True,
- community_publish_requires_approval=True,
- personal_publish_direct=True,
- reviewer_assignment_required=True,
- description="Shared repository deployment with stronger community controls."
- )
- if profile == "team_lab":
- return DeploymentPolicyProfile(
- profile_name="team_lab",
- default_personal_lane_enabled=True,
- default_community_lane_enabled=True,
- community_publish_requires_approval=True,
- personal_publish_direct=True,
- reviewer_assignment_required=False,
- description="Team deployment with shared review but moderate overhead."
- )
- return DeploymentPolicyProfile(
- profile_name="single_user",
- default_personal_lane_enabled=True,
- default_community_lane_enabled=True,
- community_publish_requires_approval=True,
- personal_publish_direct=True,
- reviewer_assignment_required=False,
- description="Single-user/private-first deployment."
- )
-
def pack_diff(old_pack: dict | None, new_pack: dict) -> dict:
old_pack = old_pack or {}
old_concepts = {c.get("id"): c for c in old_pack.get("concepts", [])}
@@ -219,9 +184,6 @@ def create_submission(pack: PackData, contributor_user_id: int, submission_summa
proposed_payload = pack.model_dump()
diff = pack_diff(current_payload, proposed_payload)
gates = gate_summary(validation, provenance)
- task_note = "Community submission awaiting reviewer attention"
- if deployment_policy_profile().reviewer_assignment_required:
- task_note += " (reviewer assignment required by deployment policy)"
sub = ContributionSubmissionORM(
pack_id=pack.id,
policy_lane="community",
@@ -240,7 +202,7 @@ def create_submission(pack: PackData, contributor_user_id: int, submission_summa
submission_id=sub.id,
reviewer_user_id=None,
task_status="open",
- task_note=task_note,
+ task_note="Community submission awaiting reviewer attention",
created_at=now_iso(),
))
db.commit()
@@ -289,7 +251,7 @@ def can_publish_pack(pack_id: str) -> tuple[bool, str]:
return False, "Pack not found"
if row.policy_lane == "personal":
return True, "Personal lane pack may publish directly"
- if deployment_policy_profile().community_publish_requires_approval and row.governance_state != "approved":
+ if row.governance_state != "approved":
return False, "Community lane pack must be approved before publication"
validation = json.loads(row.validation_json or "{}")
provenance = json.loads(row.provenance_json or "{}")
@@ -304,9 +266,14 @@ def set_pack_publication(pack_id: str, is_published: bool):
if row is None:
return False, "Pack not found"
if is_published:
- ok, reason = can_publish_pack(pack_id)
- if not ok:
- return False, reason
+ validation = json.loads(row.validation_json or "{}")
+ provenance = json.loads(row.provenance_json or "{}")
+ gates = gate_summary(validation, provenance)
+ if row.policy_lane == "community":
+ if row.governance_state != "approved":
+ return False, "Community lane pack must be approved before publication"
+ if not gates.get("ready_for_review", False):
+ return False, "Community lane gates not satisfied"
row.is_published = is_published
db.commit()
return True, "Updated"
diff --git a/src/didactopus/seed.py b/src/didactopus/seed.py
index 945e51f..14b09e5 100644
--- a/src/didactopus/seed.py
+++ b/src/didactopus/seed.py
@@ -38,7 +38,9 @@ def main():
title="Wesley Private Pack",
subtitle="Personal pack example without community friction.",
level="novice-friendly",
- concepts=[PackConcept(id="intro", title="Intro", prerequisites=[], masteryDimension="mastery", exerciseReward="Intro marker")],
+ concepts=[
+ PackConcept(id="intro", title="Intro", prerequisites=[], masteryDimension="mastery", exerciseReward="Intro marker"),
+ ],
onboarding={"headline":"Start privately","body":"Personal pack lane.","checklist":["Create pack","Use pack"]},
compliance=PackCompliance(sources=0, attributionRequired=False, shareAlikeRequired=False, noncommercialOnly=False, flags=[])
),
diff --git a/webui/index.html b/webui/index.html
index 7cbbc61..83e5a74 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -3,7 +3,7 @@
- Didactopus Deployment Policy + Agent Hooks
+ Didactopus Dual-Lane Policy Layer
diff --git a/webui/package.json b/webui/package.json
index fa83249..45e8f5a 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -1,5 +1,5 @@
{
- "name": "didactopus-deployment-policy-ui",
+ "name": "didactopus-dual-lane-ui",
"private": true,
"version": "0.1.0",
"type": "module",
diff --git a/webui/src/App.jsx b/webui/src/App.jsx
index 3c39a92..48c490a 100644
--- a/webui/src/App.jsx
+++ b/webui/src/App.jsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
-import { login, refresh, fetchDeploymentPolicy, fetchAgentCapabilities, fetchPacks, upsertPack, createContribution, fetchAdminPacks, fetchPackValidation, fetchPackProvenance, fetchPackVersions, fetchPackComments, fetchSubmissions, fetchSubmissionDiff, fetchSubmissionGates, fetchReviewTasks, publishPack, fetchPublishability } from "./api";
+import { login, refresh, fetchPacks, upsertPack, createContribution, fetchAdminPacks, fetchPackValidation, fetchPackProvenance, fetchPackVersions, fetchPackComments, fetchSubmissions, fetchSubmissionDiff, fetchSubmissionGates, fetchReviewTasks, publishPack, fetchPublishability, governanceAction, addReviewComment } from "./api";
import { loadAuth, saveAuth, clearAuth } from "./authStore";
function LoginView({ onAuth }) {
@@ -29,7 +29,6 @@ function LoginView({ onAuth }) {
function NavTabs({ tab, setTab, role }) {
return (
-
{role === "admin" ? <>
@@ -42,9 +41,7 @@ function NavTabs({ tab, setTab, role }) {
export default function App() {
const [auth, setAuth] = useState(loadAuth());
- const [tab, setTab] = useState("policy");
- const [deploymentPolicy, setDeploymentPolicy] = useState(null);
- const [agentCapabilities, setAgentCapabilities] = useState(null);
+ const [tab, setTab] = useState("personal");
const [packs, setPacks] = useState([]);
const [adminPacks, setAdminPacks] = useState([]);
const [selectedPackId, setSelectedPackId] = useState("");
@@ -58,6 +55,8 @@ export default function App() {
const [submissionGates, setSubmissionGates] = useState(null);
const [publishability, setPublishability] = useState(null);
const [reviewTasks, setReviewTasks] = useState([]);
+ const [commentText, setCommentText] = useState("Looks structurally plausible.");
+ const [reviewSummary, setReviewSummary] = useState("Reviewed and ready for next stage.");
const [message, setMessage] = useState("");
const [personalPack, setPersonalPack] = useState({
id: "my-private-pack",
@@ -104,14 +103,11 @@ export default function App() {
useEffect(() => {
if (!auth) return;
async function load() {
- setDeploymentPolicy(await guarded((token) => fetchDeploymentPolicy(token)));
- setAgentCapabilities(await guarded((token) => fetchAgentCapabilities(token)));
const p = await guarded((token) => fetchPacks(token));
setPacks(p);
+ setSelectedPackId((prev) => prev || p[0]?.id || "");
if (auth.role === "admin") {
- const ap = await guarded((token) => fetchAdminPacks(token));
- setAdminPacks(ap);
- setSelectedPackId((prev) => prev || ap[0]?.id || "");
+ setAdminPacks(await guarded((token) => fetchAdminPacks(token)));
setSubmissions(await guarded((token) => fetchSubmissions(token)));
setReviewTasks(await guarded((token) => fetchReviewTasks(token)));
}
@@ -151,6 +147,21 @@ export default function App() {
setMessage(`Community submission created: ${result.submission_id}`);
}
+ async function doGovernance(status) {
+ await guarded((token) => governanceAction(token, selectedPackId, { status, review_summary: reviewSummary }));
+ setAdminPacks(await guarded((token) => fetchAdminPacks(token)));
+ setVersions(await guarded((token) => fetchPackVersions(token, selectedPackId)));
+ setPublishability(await guarded((token) => fetchPublishability(token, selectedPackId)));
+ setMessage(`Pack moved to ${status}`);
+ }
+
+ async function addCommentNow() {
+ const versionNumber = versions[0]?.version_number || 1;
+ await guarded((token) => addReviewComment(token, selectedPackId, versionNumber, { comment_text: commentText, disposition: "comment" }));
+ setComments(await guarded((token) => fetchPackComments(token, selectedPackId)));
+ setMessage("Review comment added");
+ }
+
async function publishSelected() {
const result = await guarded((token) => publishPack(token, selectedPackId, true));
setMessage(result.reason || "Publish updated");
@@ -164,8 +175,8 @@ export default function App() {
-
Didactopus deployment policy + agent hooks
-
Policy profiles plus explicit API parity for human UI use and AI learner use.
+
Didactopus dual-lane policy layer
+
Personal packs stay low-friction. Community packs keep gates, review, and approval workflows.
Signed in as {auth.username} ({auth.role})
{message ?
{message}
: null}
@@ -179,20 +190,6 @@ export default function App() {
- {tab === "policy" && (
-
-
- Deployment policy profile
- {JSON.stringify(deploymentPolicy, null, 2)}
-
-
- AI learner capability hooks
- This installation exposes direct API hooks for an AI learner instead of requiring UI mediation.
- {JSON.stringify(agentCapabilities, null, 2)}
-
-
- )}
-
{tab === "personal" && (
@@ -257,7 +254,13 @@ export default function App() {
Governance and publishability
-
+
+
+
+
+
+
+
Publishability
{JSON.stringify(publishability, null, 2)}
Validation
@@ -269,6 +272,8 @@ export default function App() {
Versions and comments
Versions
{JSON.stringify(versions, null, 2)}
+
+
Comments
{JSON.stringify(comments, null, 2)}
diff --git a/webui/src/api.js b/webui/src/api.js
index af39326..ae687ea 100644
--- a/webui/src/api.js
+++ b/webui/src/api.js
@@ -16,8 +16,6 @@ export async function refresh(refreshToken) {
if (!res.ok) throw new Error("refresh failed");
return await res.json();
}
-export async function fetchDeploymentPolicy(token) { const res = await fetch(`${API}/deployment-policy`, { headers: authHeaders(token, false) }); if (!res.ok) throw new Error("fetchDeploymentPolicy failed"); return await res.json(); }
-export async function fetchAgentCapabilities(token) { const res = await fetch(`${API}/agent/capabilities`, { headers: authHeaders(token, false) }); if (!res.ok) throw new Error("fetchAgentCapabilities failed"); return await res.json(); }
export async function fetchPacks(token) { const res = await fetch(`${API}/packs`, { headers: authHeaders(token, false) }); if (!res.ok) throw new Error("fetchPacks failed"); return await res.json(); }
export async function upsertPack(token, payload) { const res = await fetch(`${API}/packs`, { method: "POST", headers: authHeaders(token), body: JSON.stringify(payload) }); if (!res.ok) throw new Error("upsertPack failed"); return await res.json(); }
export async function createContribution(token, payload) { const res = await fetch(`${API}/contributions`, { method: "POST", headers: authHeaders(token), body: JSON.stringify(payload) }); if (!res.ok) throw new Error("createContribution failed"); return await res.json(); }
@@ -32,3 +30,5 @@ export async function fetchSubmissionGates(token, submissionId) { const res = aw
export async function fetchReviewTasks(token) { const res = await fetch(`${API}/admin/review-tasks`, { headers: authHeaders(token, false) }); if (!res.ok) throw new Error("fetchReviewTasks failed"); return await res.json(); }
export async function publishPack(token, packId, isPublished) { const res = await fetch(`${API}/admin/packs/${packId}/publish?is_published=${isPublished}`, { method: "POST", headers: authHeaders(token, false) }); if (!res.ok) throw new Error("publishPack failed"); return await res.json(); }
export async function fetchPublishability(token, packId) { const res = await fetch(`${API}/admin/packs/${packId}/publishability`, { headers: authHeaders(token, false) }); if (!res.ok) throw new Error("fetchPublishability failed"); return await res.json(); }
+export async function governanceAction(token, packId, payload) { const res = await fetch(`${API}/admin/packs/${packId}/governance`, { method: "POST", headers: authHeaders(token), body: JSON.stringify(payload) }); if (!res.ok) throw new Error("governanceAction failed"); return await res.json(); }
+export async function addReviewComment(token, packId, versionNumber, payload) { const res = await fetch(`${API}/admin/packs/${packId}/comments?version_number=${versionNumber}`, { method: "POST", headers: authHeaders(token), body: JSON.stringify(payload) }); if (!res.ok) throw new Error("addReviewComment failed"); return await res.json(); }
diff --git a/webui/src/styles.css b/webui/src/styles.css
index 9919182..c284650 100644
--- a/webui/src/styles.css
+++ b/webui/src/styles.css
@@ -23,6 +23,7 @@ input, select, textarea { width:100%; margin-top:6px; border:1px solid var(--bor
.table { width:100%; border-collapse:collapse; }
.table th, .table td { border-bottom:1px solid var(--border); text-align:left; padding:8px; vertical-align:top; }
.prebox { background:#f7f8fa; border:1px solid var(--border); border-radius:12px; padding:12px; overflow:auto; max-height:320px; }
+.button-row { display:flex; gap:10px; flex-wrap:wrap; margin-bottom:12px; }
@media (max-width:1100px) {
.hero { flex-direction:column; }
.twocol { grid-template-columns:1fr; }