From ebd754c6bac3e80eb2ff5c5eb156731da2bde5a6 Mon Sep 17 00:00:00 2001 From: welsberr Date: Sat, 14 Mar 2026 13:29:55 -0400 Subject: [PATCH] Apply ZIP update: 195-didactopus-dual-lane-policy-layer.zip [2026-03-14T13:20:27] --- src/didactopus/api.py | 33 ++------------------ src/didactopus/config.py | 1 - src/didactopus/models.py | 30 ------------------ src/didactopus/orm.py | 2 +- src/didactopus/repository.py | 55 +++++++-------------------------- src/didactopus/seed.py | 4 ++- webui/index.html | 2 +- webui/package.json | 2 +- webui/src/App.jsx | 59 +++++++++++++++++++----------------- webui/src/api.js | 4 +-- webui/src/styles.css | 1 + 11 files changed, 54 insertions(+), 139 deletions(-) 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

- +
+ + + + +
+