{
  "name": "article-editing",
  "family": "the loop",
  "description": "Use when creating, editing, sourcing, illustrating, exporting, or deleting any article on miscsubjects.com — from the admin studio, curl, a coding agent, a Cloudflare Gateway model, or Google Sheets. One REST contract for all of them.",
  "body": "\n# Article editing — the one contract\n\nEvery article on miscsubjects.com is one row in one table, reachable through one REST API.\nThe admin studio (`/admin/articles`), the public page's admin bar, curl, coding agents,\nGateway models, and the Google Sheets poller are all clients of the same verbs. There is\nno second write path. If you are about to write an article any other way, stop and use this.\n\n## The verbs\n\n| Action | Call |\n|---|---|\n| List + filter | `GET /api/articles?q=&tag=&category=&status=&model=&has_hero=1&register=all&limit=250` |\n| Read | `GET /api/articles/<slug>` — full object; `?format=post` = the exact re-postable body; `?rev=N` = an old revision |\n| Create / replace | `PUT /api/articles/<slug>` body `{\"title\",\"body\",\"tags\":[],\"category\",\"register\",\"status\",\"hero\",\"claims\":[],\"sources\":[],\"replace\":true,\"prov\":{\"model\":\"<you>\",\"action\":\"write\"}}` |\n| Surgical edit | `PATCH /api/articles/<slug>` body `{\"find\":\"<exact>\",\"replace\":\"<new>\",\"expected_hash\":\"<sha256 of the body you read>\"}` — 409 on concurrent edit, 422 on 0 or ambiguous matches |\n| Append a source | `POST /api/articles/<slug>/webhook` body `{\"kind\":\"source\",\"data\":{\"url\",\"title\",\"quote\",\"claim_ids\":[]}}` (also kinds: claim, widget, provenance, contribution, review) |\n| Model rewrite | `POST /api/articles/<slug>/rewrite` body `{\"find\",\"instruction\",\"model\":\"workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast\"}` — returns the proposal + the exact PATCH that applies it; nothing is applied until you send that PATCH |\n| Delete | `DELETE /api/articles/<slug>` (immutable slugs refuse; retract via `status`) |\n| Download one | `GET /api/articles/export?slug=<slug>` (.md attachment) |\n| Download a set | `GET /api/articles/export?tag=<tag>` or `?category=<cat>` or `?all=1` |\n| Object folder | `GET /api/articles/<slug>/bundle?format=zip` (markdown + json + skill + manifest) |\n| Revisions | `GET /api/articles/<slug>/revisions`; restore = `GET ?rev=N` then `PUT` its fields back |\n\n## Auth — one token, every transport\n\nThere is ONE token format. Present it any of three ways — they are interchangeable by design,\nso a browser-based model and its curl fallback never diverge:\n\n- browser URL: append `?share=<token>`\n- curl: header `Authorization: Bearer <token>`\n- legacy header: `x-write-token: <token>`\n\nValidate any token, either transport: `GET /api/token/validate?share=<token>` or\n`curl /api/token/validate -H 'Authorization: Bearer <token>'` — returns scope, expiry,\nexactly what it permits, and its revoke + ledger links.\n\nScopes: **act** = read + write (search the directory, read any article, PUT/PATCH/DELETE it,\nappend sources, invoke any capability — the write gate is satisfied). **row:/rows:/pfx:** =\nthe same token format attenuated to named capabilities. Owner mints:\n`GET /api/dispatch?mint_share=1&scope=act` (with the owner key or admin cookie).\n\nOther lanes that still work: the owner's `/admin/login` cookie (studio + admin bar);\n`x-terminal-key` for the owner's own terminal; and a model holding NOTHING can still earn a\nprose write by reading the live law — `GET /api/write-gate/challenge` → answer → token.\n\n## Writing a new article — the short path\n\n1. `GET /api/articles/writing-law/skill` — the live writing law. Follow it.\n2. Draft first: `PUT /api/articles/<slug>` with `\"draft\": true` — invisible everywhere public until you publish.\n3. Sources as you go: one `POST /webhook {\"kind\":\"source\"}` per source. Embed any of them inline in the body with `[[embed:source:sN]]` — the page renders a full source card at that spot. Other inline shortcodes: `[[embed:<other-article-slug>]]` embeds an article card.\n4. Claims: include `claims:[{id,text,section,tier,source_ids}]` in the PUT — claims are the atomized, checkable objects; tiers and source links render on the page.\n5. Hero image: generate through the build's image pipeline (`/admin/generate`, or the image-to-R2 capability in the directory), take the returned `https://miscsubjects.com/img/gen/...` URL, set it as `hero`.\n6. Publish: `PUT` again with `\"draft\": false` (or just without draft). Sign your provenance: `prov.model` is never omitted.\n7. Verify: open `https://miscsubjects.com/a/<slug>` and `GET /api/articles/<slug>/health`.\n\n## Guardrails you will hit (they are features)\n\n- `shrink_guard` 409 — you replaced a 2k+ body with something under 40% its size without `\"replace\":true`.\n- `write gate` 428 — you wrote prose without a token; follow the challenge in the response.\n- `find_not_present` / `find_ambiguous` 422 — your PATCH `find` must be an exact, unique substring.\n- `hash_mismatch` 409 — someone edited between your read and write; re-read and re-target.\n- `register_refused` 422 — test titles, model self-introductions, and hashtag blocks never publish.\n- `corpus_freeze` 423 — canonical corpus pages are owner-locked; contribute claims/sources/objections instead.\n\n## Why some articles have DIVs and some don't\n\nDIV/voxel structure (`GET /api/articles/<slug>/voxels`) is generated from `claims` in meta.\nAn article with atomized claims has addressable DIVs, hashes, and a challengeable surface;\nan article that is only prose does not. To give a prose article DIVs, add claims\n(`POST /webhook {\"kind\":\"claim\"}` or `claims:[]` on PUT) — never a parallel format.\n\n## Interfaces over this contract\n\n- **Directory**: every article is simultaneously a directory object. Search: `GET /api/directory/search?q=<words>` returns `article:<slug>` projections; `GET /api/directory/article:<slug>` resolves to the canonical article with its verbs (no content is duplicated); opening `article:<slug>` in the admin directory lands on the article editor.\n- **Admin studio**: `/admin/articles` (list, filter, create) and `/admin/articles/<slug>` (edit everything; every button prints its REST + curl).\n- **On-page admin bar**: when logged in, every `/a/<slug>` page shows Edit / Admin / View as guest / Log out, bottom-right.\n- **Google Sheets**: `operator-console/Articles.js` polls AND pushes through this same API — but its push lane truncates bodies at 45,000 chars before a full PUT. Do not bulk-push from Sheets until the truncation guard lands.\n- **Everything downloadable**: article, tag, category, or whole library as one .md; any object as a .zip folder.\n\nUpdate this skill whenever the API changes: edit `.claude/skills/article-editing/SKILL.md`\nand its sibling in `.agents/skills/`, then run `node scripts/sync_skill_pages.mjs` — the\nlive page at `/skills/article-editing` regenerates from these files.\n",
  "raw": "---\nname: article-editing\ndescription: Use when creating, editing, sourcing, illustrating, exporting, or deleting any article on miscsubjects.com — from the admin studio, curl, a coding agent, a Cloudflare Gateway model, or Google Sheets. One REST contract for all of them.\n---\n\n# Article editing — the one contract\n\nEvery article on miscsubjects.com is one row in one table, reachable through one REST API.\nThe admin studio (`/admin/articles`), the public page's admin bar, curl, coding agents,\nGateway models, and the Google Sheets poller are all clients of the same verbs. There is\nno second write path. If you are about to write an article any other way, stop and use this.\n\n## The verbs\n\n| Action | Call |\n|---|---|\n| List + filter | `GET /api/articles?q=&tag=&category=&status=&model=&has_hero=1&register=all&limit=250` |\n| Read | `GET /api/articles/<slug>` — full object; `?format=post` = the exact re-postable body; `?rev=N` = an old revision |\n| Create / replace | `PUT /api/articles/<slug>` body `{\"title\",\"body\",\"tags\":[],\"category\",\"register\",\"status\",\"hero\",\"claims\":[],\"sources\":[],\"replace\":true,\"prov\":{\"model\":\"<you>\",\"action\":\"write\"}}` |\n| Surgical edit | `PATCH /api/articles/<slug>` body `{\"find\":\"<exact>\",\"replace\":\"<new>\",\"expected_hash\":\"<sha256 of the body you read>\"}` — 409 on concurrent edit, 422 on 0 or ambiguous matches |\n| Append a source | `POST /api/articles/<slug>/webhook` body `{\"kind\":\"source\",\"data\":{\"url\",\"title\",\"quote\",\"claim_ids\":[]}}` (also kinds: claim, widget, provenance, contribution, review) |\n| Model rewrite | `POST /api/articles/<slug>/rewrite` body `{\"find\",\"instruction\",\"model\":\"workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast\"}` — returns the proposal + the exact PATCH that applies it; nothing is applied until you send that PATCH |\n| Delete | `DELETE /api/articles/<slug>` (immutable slugs refuse; retract via `status`) |\n| Download one | `GET /api/articles/export?slug=<slug>` (.md attachment) |\n| Download a set | `GET /api/articles/export?tag=<tag>` or `?category=<cat>` or `?all=1` |\n| Object folder | `GET /api/articles/<slug>/bundle?format=zip` (markdown + json + skill + manifest) |\n| Revisions | `GET /api/articles/<slug>/revisions`; restore = `GET ?rev=N` then `PUT` its fields back |\n\n## Auth — one token, every transport\n\nThere is ONE token format. Present it any of three ways — they are interchangeable by design,\nso a browser-based model and its curl fallback never diverge:\n\n- browser URL: append `?share=<token>`\n- curl: header `Authorization: Bearer <token>`\n- legacy header: `x-write-token: <token>`\n\nValidate any token, either transport: `GET /api/token/validate?share=<token>` or\n`curl /api/token/validate -H 'Authorization: Bearer <token>'` — returns scope, expiry,\nexactly what it permits, and its revoke + ledger links.\n\nScopes: **act** = read + write (search the directory, read any article, PUT/PATCH/DELETE it,\nappend sources, invoke any capability — the write gate is satisfied). **row:/rows:/pfx:** =\nthe same token format attenuated to named capabilities. Owner mints:\n`GET /api/dispatch?mint_share=1&scope=act` (with the owner key or admin cookie).\n\nOther lanes that still work: the owner's `/admin/login` cookie (studio + admin bar);\n`x-terminal-key` for the owner's own terminal; and a model holding NOTHING can still earn a\nprose write by reading the live law — `GET /api/write-gate/challenge` → answer → token.\n\n## Writing a new article — the short path\n\n1. `GET /api/articles/writing-law/skill` — the live writing law. Follow it.\n2. Draft first: `PUT /api/articles/<slug>` with `\"draft\": true` — invisible everywhere public until you publish.\n3. Sources as you go: one `POST /webhook {\"kind\":\"source\"}` per source. Embed any of them inline in the body with `[[embed:source:sN]]` — the page renders a full source card at that spot. Other inline shortcodes: `[[embed:<other-article-slug>]]` embeds an article card.\n4. Claims: include `claims:[{id,text,section,tier,source_ids}]` in the PUT — claims are the atomized, checkable objects; tiers and source links render on the page.\n5. Hero image: generate through the build's image pipeline (`/admin/generate`, or the image-to-R2 capability in the directory), take the returned `https://miscsubjects.com/img/gen/...` URL, set it as `hero`.\n6. Publish: `PUT` again with `\"draft\": false` (or just without draft). Sign your provenance: `prov.model` is never omitted.\n7. Verify: open `https://miscsubjects.com/a/<slug>` and `GET /api/articles/<slug>/health`.\n\n## Guardrails you will hit (they are features)\n\n- `shrink_guard` 409 — you replaced a 2k+ body with something under 40% its size without `\"replace\":true`.\n- `write gate` 428 — you wrote prose without a token; follow the challenge in the response.\n- `find_not_present` / `find_ambiguous` 422 — your PATCH `find` must be an exact, unique substring.\n- `hash_mismatch` 409 — someone edited between your read and write; re-read and re-target.\n- `register_refused` 422 — test titles, model self-introductions, and hashtag blocks never publish.\n- `corpus_freeze` 423 — canonical corpus pages are owner-locked; contribute claims/sources/objections instead.\n\n## Why some articles have DIVs and some don't\n\nDIV/voxel structure (`GET /api/articles/<slug>/voxels`) is generated from `claims` in meta.\nAn article with atomized claims has addressable DIVs, hashes, and a challengeable surface;\nan article that is only prose does not. To give a prose article DIVs, add claims\n(`POST /webhook {\"kind\":\"claim\"}` or `claims:[]` on PUT) — never a parallel format.\n\n## Interfaces over this contract\n\n- **Directory**: every article is simultaneously a directory object. Search: `GET /api/directory/search?q=<words>` returns `article:<slug>` projections; `GET /api/directory/article:<slug>` resolves to the canonical article with its verbs (no content is duplicated); opening `article:<slug>` in the admin directory lands on the article editor.\n- **Admin studio**: `/admin/articles` (list, filter, create) and `/admin/articles/<slug>` (edit everything; every button prints its REST + curl).\n- **On-page admin bar**: when logged in, every `/a/<slug>` page shows Edit / Admin / View as guest / Log out, bottom-right.\n- **Google Sheets**: `operator-console/Articles.js` polls AND pushes through this same API — but its push lane truncates bodies at 45,000 chars before a full PUT. Do not bulk-push from Sheets until the truncation guard lands.\n- **Everything downloadable**: article, tag, category, or whole library as one .md; any object as a .zip folder.\n\nUpdate this skill whenever the API changes: edit `.claude/skills/article-editing/SKILL.md`\nand its sibling in `.agents/skills/`, then run `node scripts/sync_skill_pages.mjs` — the\nlive page at `/skills/article-editing` regenerates from these files.\n",
  "files": [
    {
      "path": "SKILL.md",
      "bytes": 6834
    }
  ],
  "has_license_file": false,
  "source": {
    "repo": "this build",
    "url": "/a/writing-law",
    "license": "site"
  },
  "prevents": [
    {
      "date": "2026-07-29",
      "failure": "Models asked to write or edit one article burned most of their turn discovering tools, repairing formats, and choosing between competing write paths; the owner ordered one documented REST contract for every client — studio, curl, agents, Gateway models, Sheets."
    }
  ],
  "canonical_source": ".claude/skills/article-editing/SKILL.md",
  "sibling": ".agents/skills/article-editing/SKILL.md"
}