Apply ZIP update: 195-didactopus-dual-lane-policy-layer.zip [2026-03-14T13:20:27]
This commit is contained in:
parent
27bc03f77c
commit
ebd754c6ba
|
|
@ -5,10 +5,10 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from .config import load_settings
|
from .config import load_settings
|
||||||
from .db import Base, engine
|
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 (
|
from .repository import (
|
||||||
authenticate_user, get_user_by_id, store_refresh_token, refresh_token_active, revoke_refresh_token,
|
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,
|
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,
|
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,
|
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
|
return row
|
||||||
raise HTTPException(status_code=403, detail="Pack not accessible by this user")
|
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)
|
@app.post("/api/login", response_model=TokenPair)
|
||||||
def login(payload: LoginRequest):
|
def login(payload: LoginRequest):
|
||||||
user = authenticate_user(payload.username, payload.password)
|
user = authenticate_user(payload.username, payload.password)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ class Settings(BaseModel):
|
||||||
port: int = int(os.getenv("DIDACTOPUS_PORT", "8011"))
|
port: int = int(os.getenv("DIDACTOPUS_PORT", "8011"))
|
||||||
jwt_secret: str = os.getenv("DIDACTOPUS_JWT_SECRET", "change-me")
|
jwt_secret: str = os.getenv("DIDACTOPUS_JWT_SECRET", "change-me")
|
||||||
jwt_algorithm: str = "HS256"
|
jwt_algorithm: str = "HS256"
|
||||||
deployment_policy_profile: str = os.getenv("DIDACTOPUS_POLICY_PROFILE", "single_user")
|
|
||||||
|
|
||||||
def load_settings() -> Settings:
|
def load_settings() -> Settings:
|
||||||
return Settings()
|
return Settings()
|
||||||
|
|
|
||||||
|
|
@ -19,26 +19,6 @@ class LoginRequest(BaseModel):
|
||||||
class RefreshRequest(BaseModel):
|
class RefreshRequest(BaseModel):
|
||||||
refresh_token: str
|
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):
|
class PackConcept(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
title: str
|
title: str
|
||||||
|
|
@ -118,13 +98,3 @@ class EvaluatorJobStatus(BaseModel):
|
||||||
result_score: float | None = None
|
result_score: float | None = None
|
||||||
result_confidence_hint: float | None = None
|
result_confidence_hint: float | None = None
|
||||||
result_notes: str = ""
|
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)
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class PackORM(Base):
|
||||||
__tablename__ = "packs"
|
__tablename__ = "packs"
|
||||||
id: Mapped[str] = mapped_column(String(100), primary_key=True)
|
id: Mapped[str] = mapped_column(String(100), primary_key=True)
|
||||||
owner_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=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))
|
title: Mapped[str] = mapped_column(String(255))
|
||||||
subtitle: Mapped[str] = mapped_column(Text, default="")
|
subtitle: Mapped[str] = mapped_column(Text, default="")
|
||||||
level: Mapped[str] = mapped_column(String(100), default="novice-friendly")
|
level: Mapped[str] = mapped_column(String(100), default="novice-friendly")
|
||||||
|
|
|
||||||
|
|
@ -4,47 +4,12 @@ from datetime import datetime, timezone
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from .db import SessionLocal
|
from .db import SessionLocal
|
||||||
from .orm import UserORM, RefreshTokenORM, PackORM, PackVersionORM, ReviewCommentORM, ContributionSubmissionORM, ReviewTaskORM, LearnerORM, MasteryRecordORM, EvidenceEventORM, EvaluatorJobORM
|
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 .auth import verify_password
|
||||||
from .config import load_settings
|
|
||||||
|
|
||||||
settings = load_settings()
|
|
||||||
|
|
||||||
def now_iso() -> str:
|
def now_iso() -> str:
|
||||||
return datetime.now(timezone.utc).isoformat()
|
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:
|
def pack_diff(old_pack: dict | None, new_pack: dict) -> dict:
|
||||||
old_pack = old_pack or {}
|
old_pack = old_pack or {}
|
||||||
old_concepts = {c.get("id"): c for c in old_pack.get("concepts", [])}
|
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()
|
proposed_payload = pack.model_dump()
|
||||||
diff = pack_diff(current_payload, proposed_payload)
|
diff = pack_diff(current_payload, proposed_payload)
|
||||||
gates = gate_summary(validation, provenance)
|
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(
|
sub = ContributionSubmissionORM(
|
||||||
pack_id=pack.id,
|
pack_id=pack.id,
|
||||||
policy_lane="community",
|
policy_lane="community",
|
||||||
|
|
@ -240,7 +202,7 @@ def create_submission(pack: PackData, contributor_user_id: int, submission_summa
|
||||||
submission_id=sub.id,
|
submission_id=sub.id,
|
||||||
reviewer_user_id=None,
|
reviewer_user_id=None,
|
||||||
task_status="open",
|
task_status="open",
|
||||||
task_note=task_note,
|
task_note="Community submission awaiting reviewer attention",
|
||||||
created_at=now_iso(),
|
created_at=now_iso(),
|
||||||
))
|
))
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
@ -289,7 +251,7 @@ def can_publish_pack(pack_id: str) -> tuple[bool, str]:
|
||||||
return False, "Pack not found"
|
return False, "Pack not found"
|
||||||
if row.policy_lane == "personal":
|
if row.policy_lane == "personal":
|
||||||
return True, "Personal lane pack may publish directly"
|
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"
|
return False, "Community lane pack must be approved before publication"
|
||||||
validation = json.loads(row.validation_json or "{}")
|
validation = json.loads(row.validation_json or "{}")
|
||||||
provenance = json.loads(row.provenance_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:
|
if row is None:
|
||||||
return False, "Pack not found"
|
return False, "Pack not found"
|
||||||
if is_published:
|
if is_published:
|
||||||
ok, reason = can_publish_pack(pack_id)
|
validation = json.loads(row.validation_json or "{}")
|
||||||
if not ok:
|
provenance = json.loads(row.provenance_json or "{}")
|
||||||
return False, reason
|
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
|
row.is_published = is_published
|
||||||
db.commit()
|
db.commit()
|
||||||
return True, "Updated"
|
return True, "Updated"
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,9 @@ def main():
|
||||||
title="Wesley Private Pack",
|
title="Wesley Private Pack",
|
||||||
subtitle="Personal pack example without community friction.",
|
subtitle="Personal pack example without community friction.",
|
||||||
level="novice-friendly",
|
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"]},
|
onboarding={"headline":"Start privately","body":"Personal pack lane.","checklist":["Create pack","Use pack"]},
|
||||||
compliance=PackCompliance(sources=0, attributionRequired=False, shareAlikeRequired=False, noncommercialOnly=False, flags=[])
|
compliance=PackCompliance(sources=0, attributionRequired=False, shareAlikeRequired=False, noncommercialOnly=False, flags=[])
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Didactopus Deployment Policy + Agent Hooks</title>
|
<title>Didactopus Dual-Lane Policy Layer</title>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</head>
|
</head>
|
||||||
<body><div id="root"></div></body>
|
<body><div id="root"></div></body>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "didactopus-deployment-policy-ui",
|
"name": "didactopus-dual-lane-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useEffect, useState } from "react";
|
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";
|
import { loadAuth, saveAuth, clearAuth } from "./authStore";
|
||||||
|
|
||||||
function LoginView({ onAuth }) {
|
function LoginView({ onAuth }) {
|
||||||
|
|
@ -29,7 +29,6 @@ function LoginView({ onAuth }) {
|
||||||
function NavTabs({ tab, setTab, role }) {
|
function NavTabs({ tab, setTab, role }) {
|
||||||
return (
|
return (
|
||||||
<div className="tab-row">
|
<div className="tab-row">
|
||||||
<button className={tab==="policy" ? "active-tab" : ""} onClick={() => setTab("policy")}>Policy & agent hooks</button>
|
|
||||||
<button className={tab==="personal" ? "active-tab" : ""} onClick={() => setTab("personal")}>Personal packs</button>
|
<button className={tab==="personal" ? "active-tab" : ""} onClick={() => setTab("personal")}>Personal packs</button>
|
||||||
<button className={tab==="contribute" ? "active-tab" : ""} onClick={() => setTab("contribute")}>Community contribution</button>
|
<button className={tab==="contribute" ? "active-tab" : ""} onClick={() => setTab("contribute")}>Community contribution</button>
|
||||||
{role === "admin" ? <>
|
{role === "admin" ? <>
|
||||||
|
|
@ -42,9 +41,7 @@ function NavTabs({ tab, setTab, role }) {
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [auth, setAuth] = useState(loadAuth());
|
const [auth, setAuth] = useState(loadAuth());
|
||||||
const [tab, setTab] = useState("policy");
|
const [tab, setTab] = useState("personal");
|
||||||
const [deploymentPolicy, setDeploymentPolicy] = useState(null);
|
|
||||||
const [agentCapabilities, setAgentCapabilities] = useState(null);
|
|
||||||
const [packs, setPacks] = useState([]);
|
const [packs, setPacks] = useState([]);
|
||||||
const [adminPacks, setAdminPacks] = useState([]);
|
const [adminPacks, setAdminPacks] = useState([]);
|
||||||
const [selectedPackId, setSelectedPackId] = useState("");
|
const [selectedPackId, setSelectedPackId] = useState("");
|
||||||
|
|
@ -58,6 +55,8 @@ export default function App() {
|
||||||
const [submissionGates, setSubmissionGates] = useState(null);
|
const [submissionGates, setSubmissionGates] = useState(null);
|
||||||
const [publishability, setPublishability] = useState(null);
|
const [publishability, setPublishability] = useState(null);
|
||||||
const [reviewTasks, setReviewTasks] = useState([]);
|
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 [message, setMessage] = useState("");
|
||||||
const [personalPack, setPersonalPack] = useState({
|
const [personalPack, setPersonalPack] = useState({
|
||||||
id: "my-private-pack",
|
id: "my-private-pack",
|
||||||
|
|
@ -104,14 +103,11 @@ export default function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!auth) return;
|
if (!auth) return;
|
||||||
async function load() {
|
async function load() {
|
||||||
setDeploymentPolicy(await guarded((token) => fetchDeploymentPolicy(token)));
|
|
||||||
setAgentCapabilities(await guarded((token) => fetchAgentCapabilities(token)));
|
|
||||||
const p = await guarded((token) => fetchPacks(token));
|
const p = await guarded((token) => fetchPacks(token));
|
||||||
setPacks(p);
|
setPacks(p);
|
||||||
|
setSelectedPackId((prev) => prev || p[0]?.id || "");
|
||||||
if (auth.role === "admin") {
|
if (auth.role === "admin") {
|
||||||
const ap = await guarded((token) => fetchAdminPacks(token));
|
setAdminPacks(await guarded((token) => fetchAdminPacks(token)));
|
||||||
setAdminPacks(ap);
|
|
||||||
setSelectedPackId((prev) => prev || ap[0]?.id || "");
|
|
||||||
setSubmissions(await guarded((token) => fetchSubmissions(token)));
|
setSubmissions(await guarded((token) => fetchSubmissions(token)));
|
||||||
setReviewTasks(await guarded((token) => fetchReviewTasks(token)));
|
setReviewTasks(await guarded((token) => fetchReviewTasks(token)));
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +147,21 @@ export default function App() {
|
||||||
setMessage(`Community submission created: ${result.submission_id}`);
|
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() {
|
async function publishSelected() {
|
||||||
const result = await guarded((token) => publishPack(token, selectedPackId, true));
|
const result = await guarded((token) => publishPack(token, selectedPackId, true));
|
||||||
setMessage(result.reason || "Publish updated");
|
setMessage(result.reason || "Publish updated");
|
||||||
|
|
@ -164,8 +175,8 @@ export default function App() {
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<header className="hero">
|
<header className="hero">
|
||||||
<div>
|
<div>
|
||||||
<h1>Didactopus deployment policy + agent hooks</h1>
|
<h1>Didactopus dual-lane policy layer</h1>
|
||||||
<p>Policy profiles plus explicit API parity for human UI use and AI learner use.</p>
|
<p>Personal packs stay low-friction. Community packs keep gates, review, and approval workflows.</p>
|
||||||
<div className="muted">Signed in as {auth.username} ({auth.role})</div>
|
<div className="muted">Signed in as {auth.username} ({auth.role})</div>
|
||||||
{message ? <div className="message">{message}</div> : null}
|
{message ? <div className="message">{message}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -179,20 +190,6 @@ export default function App() {
|
||||||
|
|
||||||
<NavTabs tab={tab} setTab={setTab} role={auth.role} />
|
<NavTabs tab={tab} setTab={setTab} role={auth.role} />
|
||||||
|
|
||||||
{tab === "policy" && (
|
|
||||||
<main className="layout twocol">
|
|
||||||
<section className="card">
|
|
||||||
<h2>Deployment policy profile</h2>
|
|
||||||
<pre className="prebox">{JSON.stringify(deploymentPolicy, null, 2)}</pre>
|
|
||||||
</section>
|
|
||||||
<section className="card">
|
|
||||||
<h2>AI learner capability hooks</h2>
|
|
||||||
<p className="muted">This installation exposes direct API hooks for an AI learner instead of requiring UI mediation.</p>
|
|
||||||
<pre className="prebox">{JSON.stringify(agentCapabilities, null, 2)}</pre>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tab === "personal" && (
|
{tab === "personal" && (
|
||||||
<main className="layout onecol">
|
<main className="layout onecol">
|
||||||
<section className="card">
|
<section className="card">
|
||||||
|
|
@ -257,7 +254,13 @@ export default function App() {
|
||||||
<main className="layout twocol">
|
<main className="layout twocol">
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<h2>Governance and publishability</h2>
|
<h2>Governance and publishability</h2>
|
||||||
<button onClick={publishSelected}>Publish</button>
|
<div className="button-row">
|
||||||
|
<button onClick={() => doGovernance("in_review")}>Move to in_review</button>
|
||||||
|
<button onClick={() => doGovernance("approved")}>Approve</button>
|
||||||
|
<button onClick={() => doGovernance("rejected")}>Reject</button>
|
||||||
|
<button onClick={publishSelected}>Publish</button>
|
||||||
|
</div>
|
||||||
|
<label>Review summary<textarea value={reviewSummary} onChange={(e) => setReviewSummary(e.target.value)} /></label>
|
||||||
<h3>Publishability</h3>
|
<h3>Publishability</h3>
|
||||||
<pre className="prebox">{JSON.stringify(publishability, null, 2)}</pre>
|
<pre className="prebox">{JSON.stringify(publishability, null, 2)}</pre>
|
||||||
<h3>Validation</h3>
|
<h3>Validation</h3>
|
||||||
|
|
@ -269,6 +272,8 @@ export default function App() {
|
||||||
<h2>Versions and comments</h2>
|
<h2>Versions and comments</h2>
|
||||||
<h3>Versions</h3>
|
<h3>Versions</h3>
|
||||||
<pre className="prebox">{JSON.stringify(versions, null, 2)}</pre>
|
<pre className="prebox">{JSON.stringify(versions, null, 2)}</pre>
|
||||||
|
<label>Reviewer comment<textarea value={commentText} onChange={(e) => setCommentText(e.target.value)} /></label>
|
||||||
|
<button className="primary" onClick={addCommentNow}>Add comment</button>
|
||||||
<h3>Comments</h3>
|
<h3>Comments</h3>
|
||||||
<pre className="prebox">{JSON.stringify(comments, null, 2)}</pre>
|
<pre className="prebox">{JSON.stringify(comments, null, 2)}</pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@ export async function refresh(refreshToken) {
|
||||||
if (!res.ok) throw new Error("refresh failed");
|
if (!res.ok) throw new Error("refresh failed");
|
||||||
return await res.json();
|
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 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 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(); }
|
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 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 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 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(); }
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ input, select, textarea { width:100%; margin-top:6px; border:1px solid var(--bor
|
||||||
.table { width:100%; border-collapse:collapse; }
|
.table { width:100%; border-collapse:collapse; }
|
||||||
.table th, .table td { border-bottom:1px solid var(--border); text-align:left; padding:8px; vertical-align:top; }
|
.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; }
|
.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) {
|
@media (max-width:1100px) {
|
||||||
.hero { flex-direction:column; }
|
.hero { flex-direction:column; }
|
||||||
.twocol { grid-template-columns:1fr; }
|
.twocol { grid-template-columns:1fr; }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue