--- name: autokap-world description: Create and maintain the demo world that screenshots are taken of — write idempotent seed files under captures/world/seed/, keep captures/world/world.md in step with them, and apply changes only through the autokap guard engine after explicit human consent. Use when a shot needs data that does not exist, when the user says "put this data here instead", "add a few more tickets", "the demo account needs X", or when a run fails on an assertion like "0 rows, expected at least 8". --- # autokap-world — building the world the pictures are taken of **The demo world is the product.** The capture scripts are only the visible part. A screenshot is easy to take; what is hard is that a useful one needs controlled data, and that data has to survive six months of code changes. This skill writes data. It never takes a picture — that is [autokap-shot](../autokap-shot/SKILL.md). ## Before writing anything 1. Read `captures/world/world.md`. It says who exists and what they own. 2. Read `captures/scope.mjs`. It says what may be written at all. 3. Read the existing seeds in `captures/world/seed/`. The data you need may already be there, and a seed that duplicates another is a seed that breaks idempotency. 4. In direct mode, **read the database** before assuming. `world.md` can lie — it is a document, not a query. If the perimeter is still the empty template, stop and run [autokap-init](../autokap-init/SKILL.md) first. ## A seed file One file per addition, numbered, and **idempotent**: run again, it duplicates nothing — it reads what exists and creates only what is missing. ```js // captures/world/seed/004-aurora-board.mjs import { openWorld, createPlan } from "autokap/guard"; const world = await openWorld(); const plan = createPlan(world); const existing = await world.adapter.count("issues", { where: { project_id: AURORA } }); if (existing >= 12) { console.log("The Aurora board already has its tickets. Nothing to do."); process.exit(0); } plan.insert("issues", rows, "the tickets on Aurora's board, 4 columns filled"); plan.update("issues", { id }, { status: "in_review" }, "one ticket in review, for the badge"); plan.remove("issues", { id }, "one ticket too many, it unbalances the column"); console.log(plan.describe()); // shown to the user const result = await plan.apply({ confirmed: true }); // only after a real answer if (result.report) console.log(result.report); // see below ``` Number files in order. Never renumber an existing one. `apply()` returns `{ steps, blast, changed, report }`. **Print `report` and pass it on to the user.** It is the other half of the blast radius: rows *outside* the demo world that appeared while the seed ran. That is not an error — it is someone real using the product at that moment — but it is the one line that says the count moved, and a count that moves unseen is a count nobody measured. A disappearance never reaches `report`: it throws. ## Consent is not a formality `confirmed: true` is the trace of a human decision. It is written **only after**: 1. you have shown `plan.describe()` to the user, in the conversation, 2. and they have answered clearly about **that plan**. Silence is not consent. An "ok" about something else is not consent. Approval of an earlier, different plan is not consent. If you changed the plan after they agreed, ask again. Write the file with `apply()` in it, show the description, and only run it once they have said yes. ## Entitlements An account on the free plan photographs paywalls. `ENTITLEMENTS` in `captures/scope.mjs` declares what the demo account must own and which product lever grants it. Turn it into steps on an ordinary plan: ```js import { openWorld, createPlan, planEntitlements } from "autokap/guard"; const world = await openWorld(); const plan = createPlan(world); const granted = await planEntitlements(world, plan); // one step per right still missing console.log(plan.describe()); await plan.apply({ confirmed: true }); ``` There is no `applyEntitlements()` that writes on its own, and there will not be one: a right is a write like any other, so it takes the same road — same description, same consent, same refusals. It is idempotent, so a right already granted produces an empty plan and `granted[i].alreadyGranted === true`; say so rather than running an apply with nothing in it. The table has to be in `SCOPE` and `writable: true`, or the plan refuses. If the demo account has no row to grant the right on, the engine refuses too — **never invent one, and never a simulated payment state.** If the only way to grant the right is to fake a payment, stop and say so: that is a product decision. ## The parent rule **A child can only be inserted after its parent has been applied.** An `apply()` refreshes the world; before that, the parent row does not exist and cannot anchor anything. Inserting a post and its votes in the same plan fails, and it is meant to. That is what makes verification possible: every row is checked against parents that are already proven to be ours. So: one plan per level. Projects, apply. Then issues, apply. Then labels, apply. ## When the engine refuses The engine refuses with a named code and a sentence. **Read the sentence; it says what is actually wrong.** The refusal is almost never a bug in the engine. | Code | What it means | What to do | |---|---|---| | `TABLE_NOT_IN_SCOPE` | the table is not declared | ask the user whether to widen `scope.mjs`, and show them the diff. Never widen silently | | `ANCHOR_NOT_PROVEN` | a row points at something that is not demo | fix the row. If it points at a parent you just planned, split the plan | | `INERT_VIOLATION` | the row would be claimed by a cron or worker | read the `why`. Pick a status that is inert; do not remove the constraint | | `TARGET_NOT_PROVEN` | the filter also catches rows that are not ours | narrow the filter. Never make it broader to "make it work" | | `BLAST_RADIUS` | rows outside the world disappeared | **stop.** Report exactly what the engine said, attempt no correction, and let the user look at the database. Diagnosis before gesture | | `NOT_CONFIRMED` | you called `apply()` too early | go and ask | | `IDENTITY_EMPTY` | no demo account is known at all | in direct mode, nothing matches `IDENTITY.emailPattern` — create the account. In indirect mode, `IDENTITY.knownIds` is empty: have the user create it through their own signup screen and paste the id back | | `ENTITLEMENT_WITHOUT_ROW` | the account owns no row to grant the right on | create it the way the product creates it. Never invent one | Widening the perimeter to make a refusal go away is the one move that turns this whole thing back into a liability. If the perimeter is genuinely too narrow, say so, propose the exact edit to `scope.mjs`, and let the user decide. ## Indirect mode If `adapter.mode === "indirect"`, `plan.apply()` refuses. Use: ```js const { file } = await plan.emit(); console.log(`Written: ${file} — read it, then run it.`); ``` The engine still validates the plan — perimeter, anchors, inertia — before emitting: better to refuse to write a file than to hand the user one to run. Say plainly that anchors are *declared*, not *proven*, and that their reading is the only safety there is. ## `world.md` is not documentation It is the registry, and it is kept in step **in the same commit as the seed that changes it**. Sections that must stay accurate: - the demo account and its family, with ids - the main entities, with the stable identifier that shots point at (`project:aurora`) — these are what a shot's `WORLD` declaration names - what the account is entitled to, and via which product lever - the seed log: date, what was added, for which shot When you change a piece of data, **replay every shot whose `WORLD` names the entity you touched**. Grep for it: ``` grep -rl "project:aurora" captures/shots/*/shot.mjs npx autokap run ``` Without that step, improving one screenshot silently breaks three. ## Choosing the data Data is chosen because it photographs well. - Names that read like people, not `test1` / `foo` / `asdf`. - Enough rows that a list looks alive, few enough that nothing overflows. - Dates that sit sensibly around the frozen clock in `captures/config.mjs`, so "2 days ago" says the same thing in June and in December. - Text long enough to wrap once, never long enough to be cut off. - Real variety in whatever the screen is meant to show: statuses, priorities, avatars. A board where every ticket is identical demonstrates nothing. ## Never - Never `reset`, `truncate`, re-run migrations, or reseed globally. There is no such command, and there will not be one. - Never write through the application's HTTP API. The routes fire assignment, notifications, analytics, emails and billing. Write to the database. - Never run raw SQL through the adapter. It does not have that method on purpose. - Never delete the demo account casually. `deleteWorld()` exists, it refuses any account outside the identity pattern, and it requires the user to type the confirmation sentence in the current message.