# Versions and updates

A person approves one exact version of your code, named by its digest. What the digest covers, the states of a plug-in, what an update may change, how one ends.

A person approves one exact set of files, named by its digest; change one and
it waits again. `version`, `major.minor.patch`, is the name people see: raise it
with every change you publish.

## The digest {#digest}

The SHA-256 of every file in your folder that runs, in a fixed order, as 64
lowercase hex characters. It covers every file except:

- at the top: `fixtures/`, `listing/`, `listing.json` and `review.json`, which
  describe your plug-in rather than run it;
- anywhere: `node_modules/`, `.git/`, `.DS_Store`, names starting `README`,
  `AGENTS.md` and `CLAUDE.md`.

A symbolic link anywhere refuses the plug-in. What it leaves out is not copied
into the locked room, so code cannot import a file nobody approved, and editing
words for people or agents never sends your code back.

```sh run
node kanboombo-plugin.mjs digest harbour-almanac --files
```

```text output
53f07aca1e4d6f74ee24c86f95d9ae3600dfd9b51e1aded934444453b2122323
  LICENSE
  csv.mjs
  fetch.mjs
  plugin.json
```

Change any of them, even only the version, and the digest changes, though the
records do not.

### The algorithm {#algorithm}

1. List the covered files by their paths relative to the folder, with `/`
   between folders.
2. Sort the paths by their UTF-8 bytes.
3. For each, feed SHA-256 its path, a zero byte, its length in bytes as decimal
   digits, a zero byte, and its bytes.
4. The digest is the SHA-256 in lowercase hexadecimal.

A test vector:

```text file=vector/plugin.json
{"contract":1}
```

```text file=vector/fetch.mjs
console.log("hello");
```

```text file=vector/README.md
Not in the digest.
```

```text file=vector/fixtures/records.jsonl
{"not":"in the digest either"}
```

```md file=vector/notes/AGENTS.md
Nor at any depth.
```

```sh run
node kanboombo-plugin.mjs digest vector --files
```

```text output
59f6124be3afabb8d74756d91613dce8db981d32f26a2b4578a3ca6f7a072110
  fetch.mjs
  plugin.json
```

If yours differs, compare the order and the lengths (22 and 15 bytes) first.

## Approval and states {#states}

A person approves at host.kanboombo.com › Plug-ins, which sets out what the
manifest claims (downloads, schedule, storage, licence, readers), the digest and
its files, the last five runs and the Node version. Approving names the digest
the page showed: if the files changed meanwhile, it is refused. An approved
plug-in runs at once, then on its schedule. A `local` one cannot be approved.

| State | Runs | Your readers see | How it gets there | What a person can do |
|---|---|---|---|---|
| Pending | No | For a first version nothing; else the approved records, then `Not updating` | It arrived, or its files changed | Approve, Remove; Pause if an earlier version was approved |
| Approved | On its schedule | Its records | Approved or resumed, or a run succeeded while Failing | Pause, Remove |
| Failing | Retried after 30 minutes, then on schedule | Its records, then `Couldn't refresh` | A run failed while Approved | Pause, Remove |
| Paused | No | Its records, then `Not updating` | A person paused it | Resume, Remove |
| Removed | No | Nothing: it leaves the app | A person removed it | Approve |

**Resume** returns a paused plug-in to Approved, or to Pending if its files
changed. **Removed** keeps the records for if it is approved again. **Held** is
not a state but a run waiting for a person
([Records](/developers/plugins/concepts/records/#held)).

**Back to Pending**: any change to a covered file, even only `version`. The
Server hashes what it is about to run; if that is not the approved digest,
nothing runs and the new one waits, while records stay. Once Pending, it stays
so until approved, even if the files change back. A version from Kanboombo's own
release stops until approved; one published through the portal keeps running
while the next is reviewed. Node's version is recorded, not approved: upgrading
it stops no plug-in. Write for Node 24, leaning on `node:` modules.

## What an update may change {#refused}

An update never removes records, and a new value for a date is a revision.
The checks refuse an update that changes what stored records mean: **the time
zone**, a series' **kind**, **unit** or **dates**. For those, add a series with a
new key, or build a new plug-in. A series removed (its records kept), a field
removed, or a new source host is allowed, with a warning for you and the
reviewer. Every update carries
[release notes](/developers/plugins/publish/review-requirements/#release-notes).

A reader installs your plug-in, not a version: a published update is live within
15 minutes, everyone moves to it at its next run, and nobody installs again or
loses a record. A newer version marks the one before superseded; the Server
keeps each published version's code by digest and hashes it before every run.

## How a plug-in ends {#ending}

Yours are under **In the store** on `kanboombo://publish/<id>`:

| Action | Who | What happens |
|---|---|---|
| Withdraw | You | A submitted version leaves review, or an approved one is not published. |
| Unpublish | You, or your agent if allowed to publish | Nobody new can install it; it keeps running for those who have it, who see it is unlisted. Records stay. |
| List again | You, or your agent if allowed to publish | Back in the store, only after Unpublish: never after a takedown, being taken off the shelf, or Retire. |
| Retire | You, typing its id | Runs stop for good; readers keep its records; its id is never used again by anyone; a version in review is withdrawn; your owners and admins are emailed. |
| Take off the shelf | Kanboombo | Like Unpublish, but only Kanboombo lists it again. You can still retire it. |
| Take down | Kanboombo | Runs stop and a write under way is refused. A reason, a private note and a public message of up to 280 characters, which its readers see, are recorded. Records are hidden, not deleted; it can be restored only for the same digest. |

Pressing Unpublish or List again twice does nothing the second time; every
move is in the history with who made it; a refusal comes with its code
([In the store](/developers/plugins/publish/submission-errors/#stage-publish)).
A taken-down plug-in's records are removed only if its licence says they may
not be kept, through the dry-run-first [purge](/developers/plugins/plan/data-rights/#purge).

## Readers' reports {#reports}

A reader can report your plug-in from its store page (wrong data, offensive,
impersonating, data it may not use, stopped working, or else) with a note and
the day. Kanboombo reads every one, and so do you, under **Reports** on your
plug-in's page, with what Kanboombo did. Never who sent it.
