Add json and csv artifact kinds with interactive viewers

Gap 1 from planning/2026-09-02-artifact_kind_gaps_for_frequent_agent_output.md.
Chose new first-class kinds over riding on html_fragment, resolving that
doc's open design question in favor of filtering support, raw-source
preservation (like markdown's .md sidecar), and server-owned validation.

- json: validated with json.loads at post time (400 on invalid JSON),
  rendered with renderjson (collapsible tree), vendored locally.
- csv: parsed server-side with the stdlib csv module into columns/rows and
  handed to Grid.js as plain data - no client-side CSV-parsing plugin
  needed, keeping normalization server-owned. Vendored locally (MIT).
- Both preserve their original text as a sidecar file, generalizing what
  was previously markdown-only handling (storage.py's SIDECAR_SUFFIXES) so
  disk-size accounting, eviction cleanup, and ?raw=1 all cover json/csv too.
- Raw JSON is embedded in a <script type="application/json"> block with
  `</script` occurrences escaped (safe because JSON's \/ escape round-trips
  correctly) - verified with a live </script>-injection payload against the
  actual running server, not just unit tests.

New migration (0012) for the KIND_CHOICES change - SQLite doesn't enforce
choices at the schema level, but Django's migration state tracks it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
peter 2026-09-02 14:56:20 +01:00
parent af9ecf28a9
commit e5d2f192df
12 changed files with 476 additions and 18 deletions

View file

@ -35,6 +35,12 @@ Currently supported kinds:
| `image` | Multipart file | Browser image preview | No |
| `pdf` | Multipart file | Browser PDF preview | No |
| `binary` | Multipart file | Stored and downloadable | No |
| `json` | JSON text | Interactive collapsible tree view (must be valid JSON) and original text is preserved | Yes |
| `csv` | JSON text | Interactive sortable/searchable table (parsed server-side) and original text is preserved | Yes |
`json` and `csv` content is required to be a JSON/DRF **string** field even
though the value itself is JSON/CSV text — i.e. `{"kind": "json", "content":
"{\"a\": 1}"}`, not a nested JSON object in the request body.
## Source Fields
@ -84,15 +90,17 @@ Required fields:
| Field | Notes |
|---|---|
| `source` | Required. Max 80 characters. |
| `kind` | One of `html_fragment`, `html_full`, `markdown`, `deck`. |
| `kind` | One of `html_fragment`, `html_full`, `markdown`, `deck`, `json`, `csv`. |
| `title` | Required. Max 200 characters. |
| `content` | Required text content. Must match the shape `kind` implies — see below. |
`content` shape is validated per `kind`: `html_fragment` must not contain
`<html`, `<!doctype`, or `<body` (body-only HTML, wrapped in a theme
server-side); `html_full` must contain `<html`; `deck` must contain
`<section`; `markdown` must not be blank or whitespace-only. A mismatch
returns `400`.
`<section`; `markdown` must not be blank or whitespace-only; `json` must be
valid JSON (`json.loads` must succeed). `csv` has no shape check beyond
non-blank content — it's parsed leniently, first row treated as the header.
A mismatch returns `400`.
Optional fields: