# 004 — `html_fragment`/`deck`/`html_full` content-shape rules exist only in the client helper, not the server **Status:** Fixed ## Description `docs/api.md` and `skills/daybook-posting/SKILL.md` both describe a content-shape contract per text `kind`: - `html_fragment` — "Wrapped with a Daybook theme" (body-only HTML, no `doctype`/`html`/`body`). - `html_full` — complete document, must contain ``. - `deck` — top-level `
` blocks. `skills/daybook-posting/scripts/daybook_post.py`'s `validate_payload()` enforces all three of these client-side before ever sending the request. **`artifacts/views.py`'s `ArtifactListCreateView.post` / `create_text_artifact` enforce none of them** — the only server-side checks are "kind is one of the 4 text kinds" and "required fields present." Any client that talks to `POST /api/artifacts` directly (curl, a different language, a misconfigured agent) bypasses the contract entirely, and the server accepts and stores whatever is sent. Confirmed: posting `kind: "html_fragment"` with a full `......` as `content` returns `201`, and the stored page (`GET /a/{id}`) is: ```html ... nested html fragment Injected

Nested doc

``` i.e. a doctype and a full `` nested inside the theme wrapper's own `` — invalid markup that browsers will silently "fix up" in unpredictable ways (duplicate/conflicting ``, nested `<head>`, etc.), rather than the clean, themed fragment the kind is supposed to guarantee. This is the same class of gap as #003: validation the docs promise is implemented only in the optional client helper, not the server that's supposed to be the actual contract enforcement point. Related to (but distinct from) issue #003, which is about unenforced *length* limits rather than unenforced *content shape*. ## Suggested Fix Port `daybook_post.py`'s `validate_payload()` checks (or equivalent) into `create_text_artifact`/`ArtifactListCreateView.post` server-side, returning `400` with the same messages the helper already uses. This also closes the gap for any future non-Python client. ## Resolution Added `validate_content_shape(kind, content)` in `artifacts/views.py`, ported directly from `daybook_post.py`'s `validate_payload()` (same rules, same case-insensitive substring checks) so client and server agree: `deck` requires `<section`, `html_fragment` rejects `<html`/`<!doctype`/`<body`, `html_full` requires `<html`, and `markdown` rejects whitespace-only content (which the old required-field check missed, since a non-empty whitespace-only string is truthy). Called from `ArtifactListCreateView.post` after the existing field/source validation. Tests added covering each rejection case plus a well-formed `html_fragment` still succeeding.