---
{
  "n": 13,
  "title": "A feature is a resource",
  "abstract": "Features become rows the engine creates on boot, a missing row means the feature is off, and any resource carries arbitrary state for a feature's own bookkeeping or another's",
  "refs": [],
  "seen": [
    "agent"
  ],
  "data": {
    "status": "final"
  },
  "created": 1789932831.381688,
  "updated": 1789933395.4796228,
  "deleted": 0.0,
  "completed": 1789932868.718244,
  "outcome": "final",
  "type": "doc"
}
---


## What you said, in short
A feature is a resource like any other. The engine writes the row for every feature it finds when it boots, and a migration backfills the rows for journals that already exist. A feature whose row is missing is off: delete the file and the feature is gone from that journal; write the row again and it is back. The row carries the feature's state, so the state lives where the feature lives rather than in a settings dictionary beside it. And any resource holds any value under any key, so a feature can keep its own bookkeeping on its row, and one feature can write state on another's resource to change how it behaves.

## What is already true
A resource holds arbitrary state today. A declared field is checked against its kind; every other key passes through into the row's data untouched and comes back as it went in. So the last part of your design needs no work at all — journal <type> set <n> <key> <value> and --set key=value already write anything onto anything, and a feature reading its own key off a row is a read away. The generic command surface is there too: one Controller carries every verb, one Resource carries title, abstract, brief, sections, links, seen and outcome, so a new type gets the whole surface without writing any of it.

## What has to change
A feature is a class today, found by scanning features/ for a feature.py, and whether it is on is a key in the environment's features setting. To make it a resource: a feature type with the fields it needs — enabled, trigger, and whatever state it keeps; the engine writing a row on boot for every feature class it finds; a migration writing the rows for existing journals from the settings dictionary, so nothing switched off comes back on; the registry reading rows rather than the settings dictionary; and the Settings page writing to the row instead.

## Where the design pulls against itself
Two rules in the description fight: the engine writes a missing row on boot, and deleting the file removes the feature. Delete the file and the next boot writes it back, so the deletion would last only until the engine restarts. Settled by question 28: deleting the file means off, not gone. Boot writes the row back with enabled false, so the file is a switch and never a grave, and what comes back says plainly that it is off. The two alternatives — boot filling in only what has never existed, and a list of removed features that boot skips — were declined: the first leaves a journal that misses a boot never learning about a new feature, the second leaves a tombstone behind, which is what deleting a file was meant to avoid.

## What it buys
A feature stops being special. It is listed, read, commented on, linked and searched like anything else; its state is on its row where a later reader will find it; and a plugin can reach a feature the same way it reaches a to-do, over the bus, rather than needing a second mechanism. It also gives the Settings page nothing to invent: it edits a row.
