# ASUCB Backlog

Tracked-but-not-blocking items. Promote to a phase if a tester / IT-review / adoption signal escalates the risk.

Format: each item carries (a) what's missing, (b) why deferred, (c) escalation trigger, (d) estimated effort, (e) cheapest viable implementation.

---

## C Margin design — port to the staging server UI

- **What's missing:** the v7.10.0 "C Margin" redesign shipped for the `course-mockup` faculty preview only. The staging server's three HTML views — index/course view (`_build_index`), single-item preview wrappers (`_wrap_html` / `_wrap_interactive_preview` / `_render_quiz`), and the unified review (`unified_staging_review.py`) — still use the old chrome.
- **Why deferred:** Phase 4 (unified review) touches the live edit-mode / Tiptap / save-back / v7.5.3 push gate. A chrome refactor on the active staging path during the pilot risks destabilizing the surface IDs/IDAs use daily. Parked until post-pilot.
- **Escalation trigger:** pilot stabilizes (no open staging incidents for ~1 week), OR a stakeholder asks why the staging UI looks unfinished next to the redesigned mockup.
- **Estimated effort:** ~2–3 days across 6 phases — see the full plan at `docs/plan-staging-server-c-margin.md` (feasibility: FEASIBLE-WITH-RISK, chrome-only swap, 8 interactive hooks to preserve).
- **Cheapest viable implementation:** execute `docs/plan-staging-server-c-margin.md` in order. Phases 1–3 (shared `c-margin-chrome.css` + index + display wrappers) are low-risk and could ship earlier if desired; Phases 3–4 (quiz editor, unified review) wait for pilot calm.

## Security — staging server hardening (v7.4.0 baseline)

Current state: 127.0.0.1 bind + path-traversal regex + `is_relative_to(COURSES_DIR)` guard + CSRF Origin allowlist on PUT/POST/DELETE. 10 regression tests in `tests/test_staging_server.py`. Good for single-user local-dev threat model. See `tests/test_staging_server.py` and `scripts/staging_server.py` for the active surface.

### Gap A — No per-session authentication
- **What's missing:** any process on the user's machine with an HTTP client can set `Origin: http://127.0.0.1:8111` and bypass the CSRF allowlist. Origin headers are forgeable from non-browser clients (curl, custom scripts, malicious npm postinstalls, rogue VSCode extensions).
- **Why deferred:** single-user dev tool, loopback-only, threat requires a separately-compromised process. Acceptable for pilot 3 testers + internal ASU Online ID team rollout.
- **Escalation trigger:** any of (1) tester reports a security review by IT, (2) plugin distributed beyond ASU Online's trusted-user perimeter, (3) shared dev machine usage emerges, (4) any allowlist-bypass incident observed.
- **Estimated effort:** ~1 hour.
- **Cheapest viable implementation:** per-session bearer token. On `staging_server.py` startup, generate a UUID, write to `.staging-token` file in plugin root (0600 perms), require `Authorization: Bearer <token>` on PUT/POST/DELETE in addition to Origin allowlist. Frontend reads token from `/api/staging/token` (loopback-allowed-without-token GET endpoint, same Origin guard). Token rotates per server restart. No persistent secrets.

### Gap B — No script-level write confirmation enforcement
- **What's missing:** Per CLAUDE.md §Write Safety Guard, Claude must verbally confirm before any Canvas write. But Python scripts (`push_to_canvas.py`, `canvas_api.create_*`, `canvas_api.update_*`, `deploy_interactives.py`) have no `input()` prompt — they execute on call. If Claude skips the confirm step (bug, forgetfulness, allowlist auto-run), the write happens silently. The interactive-content skill recently added a Desktop-preview gate (v7.4.0) — that pattern should generalize.
- **Why deferred:** the staging pipeline (preview at :8111 → backup → push → verify) provides de-facto review for pages/assignments/discussions/quizzes. Interactives now have explicit Desktop preview. Direct `canvas_api.*` write functions (used by `course-build`, `course-config`, `interactive-content`) are the residual gap.
- **Escalation trigger:** any of (1) tester reports a Canvas write that they didn't approve, (2) post-mortem on a bad push reveals the confirm step was skipped, (3) Claude Code allowlist auto-run pattern observed by user.
- **Estimated effort:** ~30 minutes for `--auto-confirm` flag pattern; ~2 hours for a `@requires_confirmation` decorator on `canvas_api.create_*` / `update_*` / `delete_*` functions with per-session approval token.
- **Cheapest viable implementation:** add `--auto-confirm` flag (default `False`) to all write-path scripts. Without it, script prints `About to write {N} items to course {ID}. Continue? [y/N]` and reads stdin. Claude must pass `--auto-confirm` after explicit user OK in chat. Trail is captured in stdout, visible to user, defends against Claude forgetting + allowlist auto-runs + Codex-dispatch race conditions.

---

## Docs — DOCX setup guide drift

- **What's missing:** `docs/ASUCB-Setup-Guide.docx` (and sibling `ASUCB-How-to-Use-Guide.docx`, `ASUCB-Quick-Reference.docx`, `ASUCB-FAQ-Troubleshooting.docx`, `ASUCB-Admin-Guide.docx`) are binary Word documents from the pre-v7.4.0 era. They likely contain the same stale "Create a Course Folder" / `mkdir ~/Documents/bio-101` / `localhost:8111` instructions that we already removed from the Coda Getting Started page and the plugin README. No automated drift detection covers them.
- **Why deferred:** binary `.docx` is edited via `python-docx` (the `docx` skill); no external office suite is required. First-touch surface for pilot users now points to Coda + README, which are both clean as of v7.5.1.
- **Escalation trigger:** any (1) ID/IDA pilot user reports the DOCX got handed to them, (2) admin asks for an updated DOCX bundle, (3) DOCX gets referenced from any onboarding email or LMS announcement.
- **Estimated effort:** ~1 hour with `python-docx` to regenerate from the Coda page content, or ~15 min to delete the stale DOCX files and add a one-line `docs/README.md` pointing readers to Coda + plugin README.
- **Cheapest viable implementation:** delete the five DOCX files (`git rm docs/ASUCB-*.docx`), add `docs/README.md` saying "Setup and how-to docs live in Coda (Canvas + Claude Guide) and in the plugin README." Single source of truth, zero drift.

---

## Alignment preview d3 tooltip — unescaped HTML interpolation in graph view

- **What's missing:** `scripts/alignment_preview_html.py:render_alignment_preview` inline JS (~lines 581–605, shipped in v7.16.0 PR #27 Graphify viz) uses d3's `.html(...)` to render hover tooltips and interpolates `d.evidence`, `d.label`, `d.text`, and `d.canvas_url` from the graph payload without `_esc()`. A maliciously crafted Canvas page title, MLO text, or edge evidence string could inject script tags into the tooltip.
- **Why deferred:** reports are generated locally and rendered offline in the ID's browser — not internet-facing, not served by a public web app. Exposure is low. The other 5 sections of the renderer (Course Summary, Filters, Alignment Paths, Possible Gaps, Assessment Detail) all escape via `_esc`. Only the tooltip surface drifted because d3 made `.html()` more convenient than building `<div>` nodes.
- **Escalation trigger:** any of (1) report is shared via public URL or hosted on a server, (2) hostile content discovered in a Canvas course used for a real audit, (3) plugin consumer reports a working XSS payload in any LO/edge text.
- **Estimated effort:** ~20 minutes. Replace `.html(...)` calls with `.text(...)` for label/text/evidence; build the Canvas-URL anchor element programmatically via `d3.create('a').attr('href', d.canvas_url)`. Add one renderer test that round-trips a hostile fixture (e.g. `<script>alert(1)</script>` in a node text) and asserts the script tag does NOT appear in the rendered HTML.
- **Cheapest viable implementation:** swap two `.html()` callsites for `.text()` + `.append("a").attr("href", ...)`. Update inline tooltip on lines ~581 and ~605. One regression test.

---

## Staging editor — deferred WYSIWYG bugs (from image-percentage deep dive)

Spun out of the percentage-width fix (PR `fix/staging-editor-percentage-img-width`). The bug-3 and bugs-5..7 below were surfaced during that investigation but were out of scope for the single-PR fix.

### Bug 3 — Source view ↔ WYSIWYG autosave race
- **What's missing:** toggling between Tiptap WYSIWYG and the raw-HTML source view in the staging editor can lose pending edits when the autosave debounce fires after the toggle but before the source-view textarea has flushed back to the editor model. Symptom: user pastes HTML into source view, switches back to WYSIWYG, edits land but then revert on the next autosave tick.
- **Why deferred:** orthogonal to the image-attr fix; needs its own reproduction + targeted patch in `scripts/unified_staging_review.py` save flow.
- **Escalation trigger:** any tester reports losing edits made via the source view.
- **Estimated effort:** ~1–2 hours — flush source-view content into the editor model synchronously on toggle; cancel pending autosave timer on toggle.
- **Cheapest viable implementation:** in the source-view toggle handler, clear `saveTimers[slug]` and call `editor.commands.setContent(textarea.value, true)` before swapping displays. Ship under branch `fix/staging-editor-source-toggle-race`.

### Bug 5 — Highlight color palette mismatches callout convention colors
- **What's missing:** Tiptap Highlight extension ships a palette (e.g. `#fff2a8` yellow) that does not match the ASU callout convention colors used in `page-design.md` (e.g. `#FFC627` gold). IDs hand-rolling a callout via the highlight tool produce off-brand color.
- **Why deferred:** cosmetic; the design system covers the canonical callout components which IDs should be using anyway.
- **Escalation trigger:** any ID reports using the highlight tool produced an off-brand page that shipped to a faculty review.
- **Estimated effort:** ~30 minutes — configure Highlight with `multicolor: true` and a custom palette aligned to `page-design.md`.
- **Cheapest viable implementation:** pass a `colors` array to `Highlight.configure({})` in `scripts/unified_staging_review.py` containing the ASU palette + neutral options. One regression test asserts the rendered JS includes the palette config.

### Bug 6 — `clearFormat` scope flattens nested lists
- **What's missing:** the Clear Format toolbar command in the staging editor calls Tiptap's `unsetAllMarks` + `clearNodes`, which flattens nested `<ul>`/`<ol>` structures along with the inline formatting. Users expect inline formatting cleared and list structure preserved.
- **Why deferred:** workaround exists (undo + re-apply). Not blocking pilot.
- **Escalation trigger:** any tester reports losing list structure unexpectedly when using Clear Format.
- **Estimated effort:** ~1 hour — wire a custom command that runs `unsetAllMarks` only, skipping `clearNodes`, with a separate "Clear Block" command for users who actually want structure flattened.
- **Cheapest viable implementation:** replace the single Clear Format button with two: "Clear Formatting" (marks only) and "Clear Block" (nodes). Update tests for toolbar HTML.

### Bug 7 — IframeNode YouTube round-trip verification
- **What's missing:** the `IframeNode` Tiptap extension (lines ~1377–1390 of `scripts/unified_staging_review.py`) declares `allowfullscreen` and `allow` as attributes but no test verifies that a YouTube embed with `allowfullscreen` and `allow="accelerometer; autoplay; ..."` round-trips through parse → render without dropping or rewriting those attributes. ProseMirror's HTML attribute handling can silently strip boolean attrs that aren't declared as `default: ''`.
- **Why deferred:** YouTube embeds appear in pilot courses but no failure has been reported. Risk: silent regression where a future Tiptap upgrade drops the attribute and embeds break.
- **Escalation trigger:** any (1) tester reports a YouTube embed losing fullscreen capability after editing, (2) Tiptap dependency bumped to a new major version.
- **Estimated effort:** ~30 minutes — write a parse-then-render fixture test in `tests/test_staging_editor_iframe.py`.
- **Cheapest viable implementation:** add a test that asserts the rendered JS module declares `allowfullscreen: { default: null, parseHTML: el => el.getAttribute('allowfullscreen') }` (or equivalent preserving handler) and that the IframeNode attribute set matches the documented allowlist. If the round-trip is broken, fix in the same PR by adding explicit parseHTML/renderHTML handlers.

Note — bug 4 (H1 toolbar omission) was investigated alongside these and is documented as intentional, not a bug: H1 is reserved for the Canvas page title surface; in-body H1s break the page hierarchy.

---

## Feature ideas — STP533 in-person→online conversion session (2026-05-28)

Reusable capabilities surfaced while converting STP 533 (course 261470) to ASU Online. Verified absent from the plugin at log time.

**Decision (2026-05-28):** build **#1 → #4 → #6 → #2 → #3** in that order; **#5 declined.** Rationale: deterministic + high-frequency + testable first (#1 conversion intake + #4 scope triage pair, sharing one session-calendar util), then the cheap accuracy win (#6), then structural reuse (#2), then the design-y proposal deliverable (#3). New code follows feature-branch PR + full pytest before merge (Opus specs+reviews, Sonnet codes).

**Status (2026-05-28):** ALL FIVE MERGED — #1 (#53 @ df396aa), #4 (#54 @ 9677c30), #6 (#55 @ 2d75868), #2 (#56 @ d6557c5), #3 (#57 @ 9e05169). #5 declined. **Batch complete.** **RELEASE NOW DUE** — `git describe` = `v7.19.4-6` (5 `feat`s + 1 chore ahead of `v7.19.4`); `feat` → minor bump → **v7.20.0**. Release pass: bump `.claude-plugin/plugin.json` + `.codex-plugin/plugin.json` (synced) + `VERSION` + `## 7.20.0` CHANGELOG section + tag `v7.20.0` + push → Coda sync → engineer attaches release `.zip` to Coda Download page.

### 1. Conversion-Readiness Audit mode (course-audit) — MERGED (PR #53, 2026-05-28)
- **What's missing:** no audit mode that scores an in-person course's readiness to become an online course. Today's audit was done by hand.
- **Why deferred:** awaiting a decision on which of these five to build first.
- **Escalation trigger:** any second in-person→online conversion request (ASU Online is converting many).
- **Estimated effort:** ~1 day. New `course-audit` mode + `scripts/audit_conversion_readiness.py`.
- **Cheapest viable implementation:** deterministic detectors → backlog findings: bare single-iframe pages (no text), video embeds off the approved host (Google Drive / non-MediaPlus), caption presence, missing course/module objectives, empty syllabus body, discussion count = 0, junk gradebook items (0-pt placeholders, empty quizzes). Emit via `backlog_manager.py` like the other audits.

### 2. Clone module layout from a reference course — MERGED (PR #56, 2026-05-28)
- **What's missing:** `canvas-templates` are static spec docs (QM, SDO-JDO). No way to learn a layout from a live Canvas course ID ("build it like course X").
- **Why deferred:** same decision gate.
- **Escalation trigger:** any "structure it like my other course" request (common ID ask).
- **Estimated effort:** ~0.5–1 day. `scripts/extract_course_layout.py`.
- **Cheapest viable implementation:** given a source course ID, dump module order + SubHeader pattern + per-module page-type sequence + sample page signatures into a reusable template dict the `course-mockup`/`course-build` skills consume. Today this was done ad hoc against STP 530 (256162).
- **Extension (syllabus clone):** the same idea applies to the syllabus. Today, STP 533's mockup syllabus was built by pulling STP 530's live `syllabus_body` (via `canvas_api.get_syllabus` → key `html`), keeping section order + ASU policy boilerplate verbatim, and swapping only the course-specific sections (description, overview, objectives, outcomes, textbook, grading table). Worth wiring into `syllabus-generator`'s "Importing an Existing Syllabus" path as "clone from reference course ID."

### 3. Proposal / blueprint deliverable (course-mockup new mode) — MERGED (PR #57, 2026-05-28)
- **What's missing:** `course-mockup` produces a faculty-review preview only — no neutral, presenter-facing proposal/pitch deck (rationale, CLOs, scope, options, decision points, timeline).
- **Why deferred:** same decision gate.
- **Escalation trigger:** any ID pitching a redesign to faculty/leadership.
- **Estimated effort:** ~1 day. New `course-mockup` output mode "proposal".
- **Cheapest viable implementation:** warm-neutral editorial shell + section scaffold (audit → CLOs → scope → assessment → module map → decision points → timeline), neutral business-proposal tone (no faculty-review "your draft" framing, no AI badge). Template the structure built today at `courses/261470/mockup/Applied Multivariate Analysis (STP 533) Online Blueprint.html`.

### 4. Accelerated-session scope triage + session-calendar awareness  — MERGED (PR #54, 2026-05-28)
- **What's missing:** `needs-analysis` mentions session length but has no triage logic. No encoding of ASU session structure (A/B/C, 7.5 vs 15 wk) or the build-complete window math.
- **Why keep (2026-05-28):** converting on-campus **15-week courses → accelerated 7.5-week online** (Fall/Spring A or B) is common at ASU, so the over-scope flag + build-window math earn their keep even without peer benchmarking. Pairs with #1 as the conversion intake; shares one `session_calendar` util. Boundary: the tool flags over-scope and proposes cut *candidates* — the "what to cut" call stays human/faculty judgment, not auto-cut.
- **Escalation trigger:** any accelerated-session (Fall/Spring A or B) build.
- **Estimated effort:** ~0.5 day. `scripts/session_calendar.py` + a triage step in `course-build`/`needs-analysis`.
- **Cheapest viable implementation:** encode session lengths + `build_complete = start − 28d` (target) / `− 14d` (floor); given a topic inventory + target session, flag over-scope and suggest cut/demote (today: 17 topics → 7-week core, peer-justified).

### 5. Peer-benchmarking workflow — DECLINED (2026-05-28)
- **Decision:** not implementing. Judgment-heavy, low reuse, hard to make deterministic; an ID does this manually on the rare occasion it's needed. Kept here as a record of the decision.
- **What's missing:** no capability to benchmark a course against peer-program courses.
- **Why deferred:** judgment-heavy, less deterministic than 1–4.
- **Escalation trigger:** a redesign needing external justification (today: Penn State STAT 505 + OSU STAT 6560 pulled to justify scope cuts).
- **Estimated effort:** ~0.5 day. A `needs-analysis` benchmarking sub-step.
- **Cheapest viable implementation:** fetch peer syllabi/course pages, extract topic list + assessment model + software, produce a comparison table + evidence-cited recommendations.

### 6. Ship verbatim ASU policy boilerplate as an asset (syllabus-generator) — MERGED (PR #55, 2026-05-28)
- **What's missing:** `skills/syllabus-generator/SKILL.md` (line ~300) names "ASU Institutional Policy Sections (Boilerplate — 16 Elements)" but ships **no stored text** — it instructs "use the standard ASU language," so the model improvises institutional policy wording each generation. Policy text (Academic Integrity, Disability Accommodations, Title IX / Discrimination, Copyright, Counseling, Disruptive Behavior, Generative AI, Photo Requirement, Syllabus Disclaimer, etc.) should be verbatim institutional language, not model-generated.
- **Why deferred:** prioritization gate with items 1–5; no incident yet.
- **Escalation trigger:** any (1) generated syllabus ships inaccurate/outdated ASU policy text, (2) compliance/legal review flags syllabus policy wording, (3) an ID reports having to hand-fix the policy block.
- **Estimated effort:** ~0.5 day. Capture canonical text once + wire injection.
- **Cheapest viable implementation:** store the 16 sections verbatim in `standards/asu-syllabus-boilerplate.html` (sourced from a current ASU Online syllabus, e.g. STP 530 / 256162's `syllabus_body`), and have `syllabus-generator` inject that asset instead of regenerating prose. Add a "verify against the current ASU template each term" note + a resync date, mirroring the QM-template provenance pattern.

---

## Provenance-label codification in standards + skill docs

- **What's missing:** the AI-Generated vs ID-Owned provenance convention currently lives in personal memory + one section of `skills/course-mockup/SKILL.md` only. Other content-gen skills (`assignment-generator`, `course-build`, `quiz-generator`, `syllabus-generator`, `visual-builder`, `images`, `interactive-content`) have zero documented provenance requirement.
- **Why deferred:** doc-only PR, low risk, low urgency. Discipline + faculty review currently catch fabrications. Promote to ship when a second AI-fabrication slips past discipline (especially undetected by faculty), OR when scale forces it (multiple IDs producing courses without tight faculty review).
- **Escalation trigger:** any second AI-content fabrication that ships to live Canvas without being caught by faculty before students see it.
- **Estimated effort:** ~30 minutes (Sonnet PR). Add a "Provenance Labels" subsection to `standards/page-design.md` (the canonical doc all generators cite) + a one-line cross-reference in 7 SKILL.md frontmatters. No code, no preflight.
- **Cheapest viable implementation:** doc-only. Convention reads: AI-Generated content carries a maroon badge `<strong style="color:#8C1D40;">AI-Generated by [Model + Version]. Validate before using.</strong>` placed inline before the content. ID-Owned content (default) carries no label. Labels persist on mockup AND live student-facing content per the established disclosure-as-feature principle. Trichotomy ID/ID+AI/AI collapsed to dichotomy ID-Owned/AI-Generated (ID+AI collab counted as ID-Owned because the ID curates the result through staging review).

---

## Adding to this backlog

When you see a deferred-but-tracked item during work, add it here with the 5-field format above. Don't put backlog items in MEMORY.md (user memory dir) — those are for behavioral rules and operational lessons, not pending repo work.

Promotion path: when an escalation trigger fires, lift the item into a numbered Phase in a `docs/plan-*.md` file, mirror the phase pattern from v7.4.0 cleanup (Phase 0 inventory → security fixes → tests → ship).
