# Plan: Gated Rubric-Creation Script + canvas_api rubric fixes

## Motivation

This session exposed that the plugin has a `rubric-creator` SKILL but **no enforcing
script**, so every rubric build is hand-rolled — and hand-rolling skipped the Write
Safety Guard twice (a Page was created instead of a rubric; the staging gate was
bypassed). Three Canvas-API gotchas were also discovered the hard way against
`canvas.asu.edu` (course 256173) and must be encoded once, in code, so no future
session rediscovers them.

## Confirmed Canvas-API facts (verified live, 2026-06-15, course 256173)

1. **Rating `long_description` does not persist via the REST API.** Sending
   `rubric[criteria][i][ratings][j][long_description]` (JSON-nested *or*
   form-encoded) returns HTTP 200 but stores `""`. The UI-built STP530 rubric
   (course 256162, rubric 1338640) confirms the convention: it stores the full
   cell text in the rating **`description`** field. → Bake cell text into
   `description`; do not rely on rating `long_description`.
2. **Criteria edits on an already-associated rubric do not persist.** `PUT
   /courses/:id/rubrics/:id` returns 200 and echoes the payload, but a fresh GET
   shows the original criteria unchanged once the rubric has rubric_associations.
   → To change an existing rubric's criteria, delete + recreate, then re-associate.
3. **Equal-point ratings render in alphabetical order.** Canvas sorts a
   criterion's ratings by points descending; ties sort alphabetically by
   description. A pure 0-point ("qualitative") rubric therefore renders columns
   scrambled (Developing before Exemplary). → Use descending points for column
   order. When the rubric must not affect grades, attach with
   `use_for_grading=false` and/or place it on a `not_graded` assignment.

## Deliverables

### A. `scripts/canvas_api.py` fixes
- `create_rubric`: keep serializing rating `description` + `points` (works). Add a
  module-level docstring note that rating `long_description` is dropped by Canvas
  and callers must merge cell text into `description`. Do **not** silently send a
  rating `long_description` that will be lost.
- Add `update_rubric(config, rubric_id, ...)` that performs the **delete +
  recreate + re-associate** dance safely: read existing associations, delete the
  old rubric, create the new one, re-point every prior association, return the new
  id. Must preserve `use_for_grading` per association.
- Retry/﻿tolerate the Course-bookmark 500 (it is transient and non-fatal; the
  rubric is visible through assignment associations). Downgrade to a warning, never
  fail the whole create on a bookmark error (already the behavior — keep it).

### B. `scripts/create_rubric_gated.py` (new, the enforcing entry point)
CLI that the `rubric-creator` skill routes through. Steps, in order:
1. Accept a rubric spec (JSON file or stdin): title, criteria[], each criterion
   with `description`, optional `long_description`, and `levels[]` of
   `{name, text}`; plus a points mode (`descending` default, or explicit points).
2. Build ratings with cell text merged into `description` and descending points
   for ordering.
3. Render an **HTML preview** of the rubric as Canvas will draw it (table:
   criteria rows x level columns) to
   `courses/{id}/staging/rubrics/{slug}.html`.
4. Run `scripts/ensure_staging_server.py`; print the canonical
   `http://127.0.0.1:8111/courses/{id}/preview/rubrics/{slug}` URL on its own line.
5. Require `--confirm-verb {push,approved,deploy}` for the live write; reject
   ambiguous verbs (`ok`/`yes`/`confirm`). Mirror `deploy_interactives.py`.
6. On confirm: create the rubric (or `update_rubric` when `--replace <id>` is
   given), associate to `--assignment-id` (repeatable), clear the staged preview
   on success.
7. `--dry-run` renders the preview + prints what would be created, no write.

### C. `rubric-creator/SKILL.md`
Point the skill at `create_rubric_gated.py` as the required path. Record the three
Canvas-API facts above so the agent does not rediscover them.

### D. Tests (`tests/`)
- `create_rubric` merges/serializes ratings correctly (mock the POST; assert the
  flattened payload carries cell text in `description`, not `long_description`).
- `update_rubric` deletes, recreates, and re-associates (mock; assert old id
  deleted, new associations cover the same assignment ids + grading flags).
- Gated script refuses to write without a valid confirm verb (assert no POST).
- Gated script `--dry-run` writes a preview file and performs no Canvas write.
- Preview HTML renders one column per level in descending-points order.
- Mock `get_config`/`_get_config` so the suite passes token-less in CI.

## Acceptance criteria
- `pytest tests/ -q` green on a clean checkout.
- Gated script, run against a sandbox course, produces a rubric whose cells are
  populated and whose columns read Exemplary -> Insufficient.
- No raw `create_rubric`/`associate_rubric` call path remains in the skill docs;
  the gated script is the only documented entry.
- Write Safety Guard satisfied: stage preview -> ensure server -> 127.0.0.1 URL ->
  explicit confirm verb -> write -> clear staging.

## Out of scope (separate follow-ups)
- `hide_points` support (user chose visible descending points; revisit only if a
  true no-points-but-ordered rubric is needed).
- Gated batch object-creation (`batch_create.py`) — recommendation #2, own PR.
- Cleanup of session debris scripts in `scripts/` (recommendation #3).
