# Mangkun Portfolio API — AI Agent Guide

This guide is written for **external AI agents** that programmatically write
to the Mangkun portfolio graph (posts and roadmaps). Read it end-to-end
before making any write call. Side effects (embedding, edge regeneration,
backup snapshots) are not optional.

> **Discovery for headless agents** — `/doc` is JS-rendered. If your tool
> cannot execute JavaScript, fetch one of these instead (all served as
> plain HTTP responses on the public domain, no JS required):
>
> | URL | Content-Type | Purpose |
> |---|---|---|
> | `https://www.mangkun.work/openapi.json` | `application/json` | OpenAPI 3.x spec |
> | `https://www.mangkun.work/api.md` | `text/markdown` | This guide as raw markdown |
> | `https://www.mangkun.work/llms.txt` | `text/plain` | Resource index (llmstxt.org format) |
>
> All `/api/...` paths are also reachable on `www.mangkun.work` (proxied to
> the backend origin), so you can call endpoints from either host.

---

## 1. Connection

| Setting | Value |
|---|---|
| Production base URL | `https://mangkun-data.org` |
| Local dev base URL | `http://localhost:8022` |
| Auth | JWT Bearer in `Authorization: Bearer <access>` header |
| Content-Type | `application/json` for write calls |
| Public docs UI | `https://www.mangkun.work/doc` |
| Timeout | **≥ 30 s** for `POST /api/admin/works` (sync re-linking) |

Get an account from the operator. Never hardcode the password — use env vars.

---

## 2. Authentication flow

### 2.1 Log in once → get access + refresh tokens

```bash
curl -sS -X POST $BASE/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"…"}'
```

Response:
```json
{
  "access_token": "eyJhbGciOi…",
  "refresh_token": "eyJhbGciOi…",
  "token_type": "bearer"
}
```

The access token lasts ~30 minutes; the refresh token ~14 days.

### 2.2 Attach the access token to every admin call

```
Authorization: Bearer <access_token>
```

### 2.3 When you get `401 Token expired`, refresh — don't re-login

```bash
curl -sS -X POST $BASE/api/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token":"<refresh>"}'
```

Returns a new `{access_token, refresh_token}` pair. Replace both.

### 2.4 Identity / role

```
GET /api/auth/me        →  {id, email, role, created_at}
```

Roles: `admin` (everything), `editor` (writes), `viewer` (read-only).
Posting requires `editor` or `admin`.

---

## 3. Writing a post (Work)

A **Work** is one portfolio item — a project, a tool, a research note, a lecture.

### 3.1 Schema

| Field | Type | Required | Notes |
|---|---|---|---|
| `id` | int | server-assigned | Do not send on create. |
| `title` | string | ✅ | Short, human-readable. |
| `description` | string | ✅ | 1–2 sentences. **Used for embedding.** |
| `category` | string | ✅ | `development`, `ai`, `research`, `design`, `tools`, `lecture`, `game`, `other`, or new. |
| `date` | string | ✅ | Format `YYYY.MM`. |
| `label` | string | optional | Defaults to `title`. |
| `tags` | string[] | optional | **Strongest linking signal.** 4–10 ideal. Mix Korean + English. |
| `thumbnail` | string | optional | URL or `/static/uploads/…`. |
| `externalUrl` | string | optional | Outbound link. |
| `content` | string | optional | Long-form Markdown. **Embedded.** |
| `published` | bool | optional | Default `true` for legacy; set `false` to keep as draft. |

Server-managed (do not set):
- `embedding` — 1024-dim, regenerated from `title + description + tags + content[:2000]` on every write.
- Outgoing `RELATED_TO` edges — wiped + regenerated per write.

### 3.2 End-to-end example (Python, stdlib only)

```python
import os, json, urllib.request, urllib.error

BASE  = os.environ.get("PORTFOLIO_API", "https://mangkun-data.org")
EMAIL = os.environ["MANGKUN_EMAIL"]
PWORD = os.environ["MANGKUN_PASSWORD"]

def http(method, path, body=None, token=None, timeout=30):
    headers = {"Content-Type": "application/json"}
    if token:
        headers["Authorization"] = f"Bearer {token}"
    req = urllib.request.Request(
        BASE + path,
        data=json.dumps(body).encode() if body else None,
        headers=headers, method=method,
    )
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.loads(r.read())

# 1. Log in once
tok = http("POST", "/api/auth/login", {"email": EMAIL, "password": PWORD})
access = tok["access_token"]

# 2. Dedupe before posting
existing = {w["title"] for w in http("GET", "/api/admin/works", token=access)}

post = {
    "title": "Houdini",
    "description": "Procedural 3D tool. VFX, simulation, 절차적 모델링.",
    "category": "tools",
    "date": "2026.05",
    "tags": ["Houdini", "Procedural", "VFX", "Simulation", "3D", "절차적"],
    "content": "## Houdini\n\nProcedural workflows…",
    "published": False,      # 초안으로 만들고 나중에 공개 토글
}

if post["title"] in existing:
    print("skip — already exists")
else:
    res = http("POST", "/api/admin/works", post, token=access)
    print(f"created id={res['id']}  edges={res['edges_created']}")
```

### 3.3 Publish toggle

A new post defaults visible. To keep it draft and publish later:

```bash
# 초안으로 저장
POST /api/admin/works              { … , "published": false }

# 나중에 공개
PUT  /api/admin/publish/work/95    { "published": true }
```

Public routes (`/api/graph`, `/api/works`, `/api/search`) only return
`published=true` (or unset) items.

---

## 4. Writing a roadmap with a todo list

A **Project** groups related work and has a status + dates. Inside each
project you keep a **todo list** of `Task`s, optionally grouped under
`Milestone`s. When a project nears completion, you write `history_md` —
a curated markdown narrative shown to customers at
`https://www.mangkun.work/roadmap/{project_id}`.

### 4.1 Object shapes

```
Project    { id, name, description, status, start_date, target_date,
             tags[], color, published, history_md? }
Milestone  { id, project_id, title, description, due_date, status }
Task       { id, project_id, milestone_id?, title, description,
             status, due_date, completed_at? }
```

`completed_at` is set automatically when a Task's `status` transitions to
`done`, and cleared if you move it back. You don't write it directly.
`history_md` is the only customer-facing text — keep tasks technical and
write the customer-facing story in `history_md`.

`status` values:

| Object | Allowed values |
|---|---|
| Project | `idea`, `planning`, `in_progress`, `blocked`, `done`, `archived` |
| Milestone, Task | `todo`, `in_progress`, `done`, `cancelled` |

Dates are ISO `YYYY-MM-DD` (different from Work's `YYYY.MM`).

### 4.2 Build a roadmap step-by-step

```python
# (continued from §3.2 — `access` is your bearer token)

# 1. Create a project
proj = http("POST", "/api/admin/projects", {
    "name": "Mangkun Agent v2",
    "description": "외부 에이전트가 작업물을 자동으로 큐레이션하는 시스템",
    "status": "in_progress",
    "start_date": "2026-05-01",
    "target_date": "2026-08-01",
    "tags": ["agent", "automation"],
    "color": "#3b82f6",
    "published": False,
}, token=access)
pid = proj["id"]

# 2. Add a milestone
ms = http("POST", f"/api/admin/projects/{pid}/milestones", {
    "title": "Alpha release",
    "due_date": "2026-06-15",
    "status": "todo",
}, token=access)
mid = ms["id"]

# 3. Add tasks (some grouped under the milestone, some not)
todos = [
    {"title": "Define agent SOP",      "status": "in_progress", "milestone_id": mid},
    {"title": "Implement scheduler",   "status": "todo",        "milestone_id": mid},
    {"title": "Hook up to Slack",      "status": "todo",        "due_date": "2026-06-01"},
    {"title": "Write integration tests", "status": "todo"},
]
for t in todos:
    http("POST", f"/api/admin/projects/{pid}/tasks", t, token=access)

# 4. Declare a dependency on another project
http("POST", f"/api/admin/projects/{pid}/depends-on", {
    "target_project_id": "<other-project-uuid>",
    "note": "needs auth scaffold",
}, token=access)

# 5. Link an API endpoint this project uses (optional)
http("POST", "/api/admin/registry/endpoints/<endpoint-uuid>/used-by", {
    "project_id": pid,
}, token=access)

# 6. Fetch the unified roadmap (single call powers the /admin/roadmap UI)
road = http("GET", "/api/admin/roadmap", token=access)
print(road["projects"][0])  # progress = task_done_count / task_count
```

### 4.3 Updating task status

```bash
PUT /api/admin/tasks/{task_id}      { "status": "done" }
```

Re-fetch `/api/admin/roadmap` after each batch — `task_done_count` and
overall progress recompute automatically.

### 4.4 Closing a project + writing the customer history

```python
# 1) Mark all delivered tasks done — completed_at is set automatically
for t in http("GET", f"/api/admin/projects/{pid}/tasks", token=access):
    if t["title"] in {"Implement scheduler", "Hook up to Slack"}:
        http("PUT", f"/api/admin/tasks/{t['id']}", {"status": "done"}, token=access)

# 2) Write the customer-facing narrative (Markdown)
history = """# Mangkun Agent v2 — Phase 1 shipped

A small agent that posts portfolio works on a schedule.

## What shipped
- **2026-06-12** — Authentication + token refresh
- **2026-06-15** — Scheduler runs every 6 hours

## What's next
Phase 2 will add Slack notifications and dry-run mode.
"""
http("PUT", f"/api/admin/projects/{pid}", {
    "status": "done",
    "history_md": history,
}, token=access)

# 3) Make it public
http("PUT", f"/api/admin/publish/project/{pid}", {"published": True}, token=access)
```

After publish, customers see the project at
`https://www.mangkun.work/roadmap/{pid}`: the `history_md` is rendered
at the top, followed by milestones, then a timeline of every `done` task
in chronological order (using `completed_at`).

**Tip**: the admin UI has a "Generate from done tasks" button that
pre-fills `history_md` with a chronological list. The agent equivalent
is to read `GET /api/admin/projects/{pid}/tasks`, filter `status=='done'`,
sort by `completed_at`, and assemble a markdown draft — then edit for
voice/tone before `PUT`-ing.

---

### 4.5 Reading public roadmap data (no auth)

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/api/projects` | List published projects (summary cards) |
| `GET` | `/api/projects/{id}` | Detail: `history_md` + milestones + done-tasks timeline |

Public routes only return `published=true` projects, never drafts.

---

## 5. API key vault (using your stored secrets from another project)

If your agent needs an API key registered in the vault (e.g., to call an
endpoint declared in the registry):

```bash
GET  /api/admin/vault/keys                       →  list metadata (preview only)
GET  /api/admin/vault/keys/{id}/reveal           →  decrypted value (owner or admin)
```

Reveal is logged. Only call it at the moment of use; never persist
the plaintext to disk.

```python
key = http("GET", f"/api/admin/vault/keys/{kid}/reveal", token=access)["value"]
# use `key` for the outbound call, then forget it
```

---

## 6. Auto-linking semantics for Works

When you `POST` or `PUT` a Work, the server regenerates outgoing edges:

```
score = 0.70 · cosine(embedding_a, embedding_b)
      + 0.20 · 1{same_category}
      + 0.10 · jaccard(tags_a, tags_b)
```

Edge created when `score ≥ 0.42`. **Tag override**: if at least one tag
matches another work, an edge is always created — independent of
category or semantic distance. Weight floor scales with shared tag count
(0.45 + 0.05·n, capped at 0.60).

Implication for an agent: **tagging well is the single highest-leverage
action you take**.

---

## 7. Error contract

| Status | Cause | Agent action |
|---|---|---|
| `401 Token expired` | Access token aged out. | Call `/api/auth/refresh`. |
| `401 Invalid token` | Tampered or wrong secret. | Re-login. |
| `403 Insufficient role` | Account is `viewer`. | Stop. Ask operator to elevate. |
| `404 Not found` | Wrong id on PUT/DELETE. | Re-list and retry. |
| `409 Already exists` | Duplicate email on register. | Use a different email. |
| `422` | Validation error — missing field or wrong type. | Inspect `detail`, fix payload. |
| `503` | Server unhealthy (Vault/JWT misconfigured). | Stop. Notify operator. |
| `500` | Embedding service or Neo4j unreachable. | Backoff, retry once. |

---

## 8. Best practices

1. **Log in once per session.** Cache tokens in memory; refresh, don't re-login.
2. **Dedupe Works by title** before `POST` — pull `GET /api/admin/works` first.
3. **Make `description` count.** It feeds the embedding directly. Two specific
   sentences beat a one-line label.
4. **Tag bilingually** (Korean + English). Both enter the embedding and the
   tag-override path.
5. **Use drafts** (`published: false`) for staged content; flip later via
   `/admin/publish/{kind}/{id}`.
6. **Coalesce edits.** Every `PUT /admin/works/{id}` re-embeds + relinks.
7. **For roadmaps, fetch `/admin/roadmap` once** to get projects + tasks +
   edges in a single payload — don't N+1.
8. **Handle 401 with refresh, not re-login.** Re-login wastes the
   audit trail and may rate-limit you.
9. **Respect timeouts.** A successful Work POST can legitimately take
   2–4 s (embedding + relinking are synchronous).
10. **Do not store revealed vault values.** Fetch at the moment of use.

---

## 9. Companion files

- `backend/.env.example` — environment template.
- `backend/scripts/gen_sdk.sh` — generates TypeScript + Python clients
  from this OpenAPI spec; install once and import the typed client from
  your other projects.
- `backend/scripts/embed_existing.py` — full graph rebuild (operator-only).
- `backend/scripts/backup_db.py` — manual snapshot.
