# piplet: When Program and Data Live in the Same File

> WordPress' piplet stores its notes behind __halt_compiler() inside its own PHP file. What the source teaches about atomic writes, inode races, validating before parsing and optimistic concurrency — and which of it belongs in your own projects.

Source: https://www.jpkc.com/db/en/blog/piplet-selbstmodifizierendes-php/

Today I stumbled across a repository I have not been able to get out of my head since: [`WordPress/piplets`](https://github.com/WordPress/piplets). A single PHP file, 175,162 bytes. Inside it sits a complete wiki: PHP backend, HTTP API, HTML, CSS, JavaScript — and the notes. Not in a database next to it, not in a `data/` directory. **In the same file, right behind the program code.**

With something like this I am rarely interested in the question "should I deploy it". I am interested in: how is it built? Which problems appear once you tear that boundary down? And which of the answers are good enough that I can reuse them in projects that have nothing to do with self-modifying PHP?

This article is my answer to those three questions. It is not a review and not a recommendation. It takes the source apart, places it in a tradition that reaches back to Perl, and distils at the end what is transferable. Where I quote numbers, I measured them on a running system; where I could not verify something, I say so.

## The trick, in four lines

The repository contains two programs. The small one is called `wiki-piplet-unsafe.php`, is 78 lines long and exists purely to show the core. The decisive lines are numbers 3 and 4:

```php
$raw = file_get_contents(__FILE__);
$pages = json_decode(substr($raw, __COMPILER_HALT_OFFSET__), true);
```

The program reads itself. And at the end of the very same file:

```php
<?php __halt_compiler();
{"welcome":{"title":"Hello, piplet","body":"This is a deliberately tiny, unsafe piplet."}}
```

`__halt_compiler()` is not an ordinary function but a rule in PHP's grammar. It does not stop execution, it stops **lexical analysis**: the compiler stops reading the file at that point. Everything after it never becomes tokens, never becomes opcodes, never becomes anything executable. They are simply bytes at the end of a file. The PHP manual describes the purpose itself:

> "Halts the execution of the compiler. This can be useful to embed data in PHP scripts, like the installation files."

So that you can reach those bytes, PHP defines a constant at compile time: `__COMPILER_HALT_OFFSET__`, holding the byte position where the data starts. Both have existed since **PHP 5.1.0**, so since 2005.

One detail I measured myself, because explanations online get it wrong regularly: the offset points at the byte **immediately after the semicolon**. The following newline is *not* skipped, it already belongs to the data. If you assume while serialising that PHP swallows "that one newline", you shift by exactly one byte — and write garbage on the next save.

Writing back is a single line in the small version:

```php
file_put_contents(__FILE__, substr($raw, 0, __COMPILER_HALT_OFFSET__) . "\n$json\n", LOCK_EX);
```

The program part is taken from the buffer read at request start, prepended byte for byte, and fresh JSON follows. I ran three consecutive saves: the file size stays stable across all three — 7,066 bytes in my test, though the number depends on the text you save — with exactly one `\n` between `__halt_compiler();` and `{`. The executable part reproduces itself byte for byte.

That is all of it. No database handle, no migration, no schema. And in that nakedness you can immediately see why the file is called "unsafe" — more on that later.

**Interim conclusion:** the core of piplet is a 20-year-old PHP mechanism almost every developer has used without noticing. The mechanism is not what is new. What is new is the direction it is used in.

## Where this pattern comes from — and where it ends everywhere else

Because "data behind the code" is one of the oldest ideas in the craft. I looked at how other languages solve it, and a pattern emerged that I had not had on my radar in this form.

| Language | Idiomatic equivalent | Since | Writable at runtime? |
|---|---|---|---|
| Perl | `__END__` / `__DATA__` plus the `DATA` filehandle | `__END__` since Perl 4, `__DATA__` since Perl 5 | no, read-only handle |
| PHP | `__halt_compiler()`, and phar on top of it | 5.1.0 / 5.3 | phar locked by default |
| Python | no language feature; `zipapp` (shebang + ZIP) | 3.5 | **yes**, see below |
| Go | `//go:embed` | 1.16 (2021) | no, explicitly read-only |
| Java | JAR = ZIP; data before it unbounded, after it only ~64 KiB | — | practically no |
| Shell | marker + `tail`; makeself: byte offset + `dd` | 1998 | no |

[Perl's documentation](https://perldoc.perl.org/perldata) is unambiguous: "The filehandle is left open pointing to the line after `__DATA__`." A **read** handle. A documented way back does not exist. Go's [`embed` package](https://pkg.go.dev/embed) is even clearer: "An FS is a **read-only** value" — and it acts at compile time anyway, so it could not write back if it wanted to. And makeself, the tool behind countless self-extracting installers, does compute a byte offset into its own file, but only reads it with `dd`.

Two exceptions turned up when I tried to refute this, and both are explicitly documented. **Python** can write back: `zipfile.ZipFile(sys.argv[0], 'a')` appends to an existing archive, and the manual names exactly that case — "This is meant for adding a ZIP archive to another file (such as `python.exe`)." The shebang prefix stays byte-identical and the `.pyz` keeps running. And **SQLite** ships a VFS for it: `sqlite3 --append` puts a fully writable database behind an arbitrary file, per the source comment "such as an executable". What I found nowhere is a **language** that provides a way back into its own `__DATA__` section.

The most interesting case is PHP's own descendant. **phar archives use exactly the same separator marker.** The [smallest possible phar stub](https://www.php.net/manual/en/phar.fileformat.stub.php), per the manual, is:

```php
<?php __HALT_COMPILER();
```

So PHP has known "program plus appended data" as an official package format since 2005. And it has **locked** the write channel by default, with a justification I have rarely seen in that form in a language reference:

> "This option disables creation or modification of Phar archives using the `phar` stream or Phar object's write support. This setting should always be enabled on production machines, as the phar extension's convenient write support could allow straightforward creation of a php-based virus when coupled with other common security vulnerabilities."

[`phar.readonly`](https://www.php.net/manual/en/phar.configuration.php) defaults to `1` and cannot be switched off from inside a script, only in `php.ini`. piplet does not circumvent that lock, it never touches it: it does not write through the `phar://` stream but with ordinary file functions.

### The wiki line: TiddlyWiki and the browser's prohibition

There is a second tradition, and it is even older. **TiddlyWiki**, first released in September 2004, is the original self-saving single-file wiki. Wikipedia calls it "an unusual example of a practical quine" — the ability to produce a copy of its own source lies at the heart of its saving feature.

Except: TiddlyWiki *cannot* write itself. The browser forbids it. The project's 21-year history is a chain of detours around exactly that prohibition: in the Classic era it was ActiveX `FileSystemObject` in Internet Explorer, a Java applet and Mozilla's `UniversalXPConnect` — the last of which Firefox later removed. The core of TiddlyWiki 5 today holds **15 saver modules**, among them TiddlyFox (which stopped working with Firefox 57 in 2017), HTTP PUT, GitHub/GitLab/Gitea uploads and savers for the wrapper apps AndTidWiki and TWEdit. **Feather Wiki**, the modern relative at around 57 kB, does the same: download, or an HTTP PUT to a server you have to provide yourself — the save button only appears if the far end returns a `dav` header.

That is the real point about piplet's shape: it does not solve TiddlyWiki's problem. It runs server-side, and there a process may replace its own file. The browser sandbox as an adversary disappears — in exchange it inherits a different one: hardening policies.

### And the PHP single-file apps?

Here I wanted certainty, because the comparison "that's just Adminer" is the obvious one. I downloaded the sources instead of trusting project descriptions.

| Project | Single file? | Writes into its own file? | Where does the data go? |
|---|---|---|---|
| Adminer 6.0.1 | yes, 519,492 B | **no** | into the managed database |
| tinyfilemanager 2.6 | yes | **yes — configuration only** | own file or `config.php` |
| WonderCMS 3.6.0 | almost | **yes — self-update only** | `database.js` |
| Bludit 3.22 | no | no | metadata in `.php` files with a JSON body, page text in `index.txt` |
| Pico 2.1.4 | no | **no — not a single write call** | `content/*.md` |
| antonmedv/wiki | yes | no | `db/wiki.db` (SQLite) |

**Adminer is the most important negative finding.** In the shipped file `file_put_contents` appears zero times, there is no write to `__FILE__` and no `__halt_compiler`. Adminer is a single-file *delivery*, not a self-saving program.

The closest match is **tinyfilemanager**: line 3 of the shipped file is a `$CONFIG = '{"lang":"en",…}';`, and a method writes exactly that line back into `__FILE__` as soon as you change language or theme. But those are settings, not content — and as soon as a `config.php` sits next to it, it moves there. **WonderCMS** also overwrites its `index.php`, but with *new code* during self-update; the data goes to `database.js`.

The most interesting borderline case is **Bludit**: its database files *are* PHP files. They start with `<?php defined('BLUDIT') or die('Bludit CMS.'); ?>`, with JSON underneath. But that prologue is direct-access protection, not a program — and the program file itself stays untouched.

**Interim conclusion:** "data behind the code" is almost everywhere a **read** pattern — for installers, archives, embedded assets. The way back is rare and is a language feature nowhere; where it exists, it is a library or tooling route, as with Python's `zipfile` and SQLite's `appendvfs`. And of the six production single-file and flat-file PHP projects in the table, **none** writes its payload into its own program file. piplet stands at the end of that line, not in the middle of it. I found no older, named PHP project that stores content behind `__halt_compiler()` in its own file — which does not mean none exists, only that I could not find one.

## The large edition: 3,584 lines

Next to the 78-line demo sits `wiki-piplet.php`. Same idea, built out:

| | `wiki-piplet-unsafe.php` | `wiki-piplet.php` |
|---|---|---|
| Lines | 78 (29 of them PHP) | 3,584 |
| Size | 7,209 B | 175,162 B |
| Split | almost all markup | PHP 1,541, JS 1,597, CSS 362, HTML 80 lines (rest: data trailer) |
| Protection | none | Basic auth (mandatory), CSRF, CSP with nonce, Fetch Metadata |
| Write path | `file_put_contents(__FILE__)` | temp file + `fsync` + `rename` |
| Tests | 64 assertions | 433 assertions |

The layout of the large file:

```text
wiki-piplet.php
├── PHP persistence and HTTP API
├── HTML, editable CSS, browser UI
├── __halt_compiler();
├── PIPLET-DATA/2
└── { generation, versioned JSON notes, appearance }
```

Before I go into the code, a word on provenance — it belongs to the picture.

Matt Mullenweg presented piplets on **19 August 2026** in the closing fireside chat of WordCamp US in Phoenix, in conversation with Robert Jacobi. The official recap on wordpress.org, written by Nicholas Garofalo, sums it up in exactly one paragraph:

> "After a detour through why AI models now prefer simple, self-contained HTML files, the argument became tangible with WordPress Piplets, a 2007 idea built around a single self-modifying PHP file that stores its own data, with no database and no third-party packages. One file can hold up to 25 megabytes, about four million words, and load in about 50 milliseconds. For Mullenweg, it extends the experiments that took WordPress from MySQL to SQLite to Playground, and he floated the idea of a WordPress.org directory where people could publish, fork, and remix."

And the only quote the recap attributes to Mullenweg verbatim:

> "I want WordPress to be known for simplicity, not just complexity." — Matt Mullenweg

Three observations I made while checking, all useful when reading the code.

**First: the number in the recap and the number in the code diverge.** The source caps the file at `PIPLET_MAX_FILE_BYTES = 8 * 1024 * 1024`, so 8 MiB, and notes at 2,000. The README says it just as plainly: "The configured file ceiling is 8 MiB; request JSON is capped at 5 MiB, stored notes at 2,000, and tag references at 24,000." That is roughly a third of the 25 MB quoted. And the value is not a later hardening artefact — it is already in the very first commit, then named `PHPLET_MAX_FILE_BYTES`, a good three days before the stage. Neither "25" nor "million" nor "millisecond" appears in the README even once. And for attribution this matters: the three numbers sit in Garofalo's prose in the recap, not inside a marked Mullenweg quote. If you quote them, put the 8 MiB next to them.

**Second: the commit history is its own story.** Fourteen commits, all with author *and* committer `Codex <codex@openai.com>` — none of them cryptographically signed, so the attribution rests on the Git author field. No human commit, no `Co-authored-by`. Commit `f928aca` is called "Rename: phplet -> piplet" and carries the timestamp 2026-08-19, 21:17 UTC — that is **1 hour and 43 minutes before** the session in which the name was mentioned. Before that, the file, the README title and the constant prefix all read `phplet` — with one telling exception: the format marker in the data already read `PIPLET-DATA/1` in the very first commit. The name existed three days before the rename, just not as the project name. And the repository itself was only created on 21 August at 03:31 UTC, roughly 28 hours after the announcement; the first push followed 69 seconds later. That is the pattern of a one-off import.

**Third: I could not substantiate the 2007 origin.** I went through the blog search on ma.tt, the wp-hackers mailing list archive for July 2007 and the usual search routes — no occurrence of "piplet" or "phplet" before August 2026. The attribution "2007 idea" rests solely on Mullenweg's own spoken statement, relayed in the official recap. A recording does exist: WordPress published the livestream as [Closing Keynote — Fireside Chat with Matt Mullenweg and Robert Jacobi](https://www.youtube.com/live/notaTgEZByo), 1:07:48 long, with auto-generated English captions. There is no edited transcript, and I did not evaluate the recording — if you need the exact wording on the 25 MB, that is where it is. The missing 2007 evidence is not a refutation, just a gap you should know about before passing the year along.

**Interim conclusion:** piplet was, per its commit metadata, written in a few days by an AI agent, published a good day after the announcement, and has carried its project name since 103 minutes before the talk. None of that says anything about the quality of the code — and as you will see, that quality is remarkable in several places. It says something about the half-life of conference numbers.

## What a save actually does

This is where it gets interesting, because this part is instructive independently of PHP, wikis and WordPress.

The first surprise: **piplet does not overwrite itself at all.** The phrase "self-modifying file" describes the result, not the route. In the entire program `file_put_contents` appears zero times. Both write paths open the live file read-only (`@fopen($path, 'rb')`), the only write handle points at a temporary file, and the canonical path is changed by exactly one `rename()` call.

Two consequences follow that are easy to miss:

- **Reading needs no lock.** Anyone who can open the file holds a descriptor on an inode that is never mutated again. They are guaranteed to see a complete old or a complete new file — never half of one.
- **The PHP process needs no write permission on the file**, but on the **directory**. That is exactly what the UI checks: `is_readable($path) && is_writable(dirname($path))`.

The sequence in its eleven core steps, in the order of the code:

1. Check whether `fsync()` exists at all — if not, HTTP 503, **before** anything is locked.
2. `fopen($path, 'rb')`, then `flock(LOCK_EX | LOCK_NB)`. If the lock blocks, sleep 5–20 ms and retry.
3. Compare `fstat()` of the descriptor against `stat()` of the path. If they differ, release and start over (more on this in a moment).
4. Reject hard links: `nlink !== 1` → abort.
5. Read the file, decode the trailer, apply the mutation, increment the document revision.
6. `$prefix = substr($raw, 0, __COMPILER_HALT_OFFSET__);`
7. Compute the exact output size **before any file exists**.
8. Create a private temp file next to it, fill it in 64 KiB chunks, `fflush` + `fsync`.
9. Adopt the live file's permission bits, `fsync` **again**.
10. Check one last time that the target path is still the same inode.
11. `rename($temp, $path)`.

Step 6 is my favourite line in the whole file. The **offset** comes from the running compilation unit — PHP baked it in as a literal while compiling. The **bytes** come from the file just read under lock. There is no template, no regeneration, no `strpos('__halt_compiler')`, nothing that data could influence. The prefix is copied, never produced.

### The inode retry: why `flock(__FILE__)` is wrong

This is the point where I had to stop and reread. The comment in the code names it itself:

> "`rename()` swaps inodes, so locking the first file we open is not enough: a waiter may have opened the old inode. We lock, compare the descriptor's device/inode with the current path, and retry until we own the live file."

The sequence it hangs on:

1. **A** opens the file → descriptor on inode 1, `flock` succeeds.
2. **B** opens the same file → also **inode 1**, `flock` blocks, B waits.
3. **A** writes its temp file and calls `rename()`. The path now points at **inode 2**. Inode 1 is unlinked but stays alive because B holds it open.
4. **A** releases the lock.

And here naive and correct implementations part ways. Naively, B now gets the lock — on an **orphaned file**. `flock` on an unlinked inode succeeds perfectly well and excludes nobody, because the next writer locks inode 2. B reads the state *before* A, applies its change and renames its result over the path. **A's save is gone without a trace** — and the mutual exclusion everyone is relying on simply does not exist.

The Linux man page says precisely that: "Locks created by `flock()` are associated with an open file description." The lock hangs on the inode, not on the path name. After a `rename()` you are locking the wrong thing.

The fix is unspectacular and that is what makes it good:

```php
$lockedStat = fstat($handle);
clearstatcache(true, $path);
if (!piplet_same_inode($lockedStat, @stat($path))) {
    @flock($handle, LOCK_UN); @fclose($handle); $handle = null;
    usleep(random_int(5000, 20000));
    continue;
}
```

Compare `dev` and `ino` together, start over on a mismatch. The `clearstatcache()` before it is mandatory, not decoration: PHP caches `stat` results per request, and an uncleared cache would never see the swap — a completely silent failure.

Two more subtleties. The comparison is **ABA-free**, because a filesystem may not reuse an inode number while a process holds a descriptor on it. Comparing two `stat(path)` calls at different times would not have that property. And the backoff is **randomised** with `random_int(5000, 20000)` — against lockstep when several writers wait at once.

All retries share a single monotonic deadline of two seconds (`hrtime(true) + 2000000000`). When it expires, you get HTTP 503 with `Retry-After: 1`. Worth being precise about: that deadline bounds **only the retries**. Once the lock is held, reading, mutating and the entire write run with no time limit at all. Two seconds are not a budget for the request.

### Compute the size before a file exists

`piplet_json_encoded_length()` reproduces the JSON grammar without whitespace: 2 for the bracket pair, +1 per comma, for objects the quoted key plus 1 for the colon, scalars encoded individually with only their length kept. The docblock says why: "Exact length under `PIPLET_JSON_FLAGS`, without allocating the full JSON string."

Three reasons, all visible in the code:

- **No artefact on oversize.** The 413 throw sits before the temp file is created. An oversized save never produces a file that would need cleaning up.
- **Memory protection.** On oversize, `json_encode` is never called on the whole document — peak usage stays small instead of allocating several MiB while the old document is still in memory.
- **Exact byte budget.** The cap is written overflow-safe: `$jsonLength > MAX - $fixed` rather than `$fixed + $jsonLength > MAX`.

And then comes the line that makes the whole approach viable:

```php
if (strlen($json) !== $jsonLength) {
    throw new RuntimeException('Snapshot size projection failed.');
}
```

A **self-audit**. The model — the hand-written length calculation — is checked against reality after the real `json_encode`. Should PHP's encoder ever deviate, because escaping semantics change or a type is handled wrongly, the save aborts instead of writing a file that exceeds the cap. An optimisation that checks itself.

### The temp file

`tempnam()` has a trap I have overlooked myself once: if the given directory is not writable, the function **silently falls back to `sys_get_temp_dir()`**. But a temp file in `/tmp` may live on a different filesystem — and `rename()` is only atomic *within* one filesystem. Worse, PHP's `rename()` falls back internally to copy-plus-delete on `EXDEV` and **still reports success**. You lose atomicity without seeing an error.

piplet catches this before a single byte is written:

```php
$created = @tempnam($directory, '.piplet-tmp-');
if (!is_string($created) || realpath(dirname($created)) !== realpath($directory)) {
    if (is_string($created)) { @unlink($created); }
    throw new RuntimeException('Cannot create a snapshot beside the piplet.');
}
```

The name then gets 16 hex characters from `random_bytes(8)` and the extension **`.php`**. That extension is deliberate: the file briefly sits in a directory a web server serves. Without `.php` it would be delivered as plain text — source and every note. With `.php` it is executed, and then the guard at the very top of the file kicks in, before any class definition:

```php
// A half-written temporary copy must never behave as the live application.
if (str_contains(basename(__FILE__), '.piplet-tmp-')) {
    http_response_code(503);
    exit('Save in progress.');
}
```

**An artefact that recognises itself.** That is the pattern I noted down.

On creation piplet checks four properties: regular file, no group/other bits, `nlink === 1`, and `fstat(fd)` against `lstat(path)` — the last of which rules out a swapped-in symlink, because `lstat` follows no link. Two of them, inode identity and `nlink`, are checked again after the write and once more after the `chmod`.

Even the `chmod(0600)` right after creation has a non-obvious reason, explained in the comment: it **repairs** rather than narrows. `tempnam()` creates with 0600, but the umask applies — under `umask 0777` you get mode `0000`, and the owner could no longer open the file through its path. The target permissions are only set once content and `fsync` are done: "it never widens an exposed group/other-readable interval."

### What is guaranteed — and what is not

The cleanup path in `finally` deletes **only if** `lstat` still shows the same inode and `nlink === 1`. If the path now points elsewhere, nothing is deleted, only logged: `'piplet left an unrecognized temporary path untouched'`. A recursive delete that goes purely by path name is a footgun — here it is cleanly avoided.

What a hard kill leaves behind is tested and documented: if the process is SIGKILLed between `fsync` and `rename`, the canonical file is **bit-for-bit unchanged**, and next to it sits exactly one 0600 orphan — which is not a fragment but a complete, valid piplet, merely unpublished.

And now the gap the README names itself, which applies to anyone rebuilding this pattern. The authoritative reference for safe replacement is Jeff Moyer's LWN article "Ensuring data reaches disk". It lists five steps:

> 1. "create a new temp file (on the same file system!)"
> 2. "write data to the temp file"
> 3. "fsync() the temp file"
> 4. "rename the temp file to the appropriate name"
> 5. "fsync() the containing directory"

piplet does steps 1 through 4. **Step 5 is missing.** `fsync` on the file secures content and metadata of that inode — but not the directory entry pointing at it. If power fails after the `rename` but before the directory block is written back, the last save can be lost. The result is still never torn: either completely old or completely new.

Interestingly, the common justification — "portable PHP cannot fsync directories" — is not quite right. I measured it: `fopen($dir, 'r')` followed by `fsync($handle)` returns `bool(true)`, and `strace` shows the real syscall:

```text
openat(AT_FDCWD, "/tmp/dsynctest", O_RDONLY) = 4
fsync(4)                                     = 0
```

So on POSIX systems it works. Not on Windows, and PHP's documentation never mentions the case — which makes the caution understandable. But if you rebuild the pattern in a Linux-only context, you can have step five.

Also **not** guaranteed, because `rename()` publishes a new inode: ACLs, extended attributes, setuid/setgid/sticky bits (the mask is `& 0777`) and ownership. A piplet deployed as `root:root` belongs to the PHP process user after the first save.

**Interim conclusion:** the write path is the strongest part of the program, and it has nothing to do with wikis. Four interlocking ideas carry it: never write in place; validate the lock against inode identity rather than the path name; know the cost before you incur it; and build every intermediate artefact so it recognises itself as one.

### Takeaway: atomic writes, in any language

The pattern is language-independent and you need it more often than you think — every time a config file, a cache index, an export or a state document is replaced in production:

1. Create the temp file **in the same directory** (not in the system temp — otherwise you cross a filesystem boundary).
2. Write it fully, handling short writes in a loop.
3. `fsync` the file.
4. Set permissions, `fsync` again if needed.
5. `rename` over the target.
6. If possible: `fsync` the directory.

In Node.js that is `fs.writeFileSync` into a temp file, `fs.fsyncSync(fd)`, then `fs.renameSync`. In Python `os.fsync(f.fileno())` and `os.replace()`. What you should do in **no** language is the obvious one-liner: `file_put_contents($f, $data, LOCK_EX)` looks safe but is not. I looked at what actually happens:

```text
openat(AT_FDCWD, "/tmp/lockx/t.txt", O_WRONLY|O_CREAT, 0666) = 4
flock(4, LOCK_EX)                                            = 0
ftruncate(4, 0)                                              = 0
```

Credit where it is due: the frequently claimed "truncate before lock" race does not exist in current PHP — the lock comes before the truncation. It is still not atomic, for two other reasons. First, the lock is **advisory**: any reader that does not call `flock` — and that is practically every `include`, every `file_get_contents`, every web server — sees the window between `ftruncate()` and the final `write()`. In that window the file is empty or half written. Second, there is no all-or-nothing promise: if the process dies in between, a truncated file remains. For a program that writes its own source, that means a parse error and total loss.

## Validate before you parse

The second idea I am taking away concerns something most of us treat far too casually: `json_decode`.

piplet runs three of its own byte scanners **before** the parser sees the embedded data. All three work on the raw string without allocating. Incoming requests get two of them — the structure budget and the member names; the number scanner guards the trailer only. The comment above the first names the division of labour precisely:

```php
/** Cheap allocation guard; json_decode remains the JSON grammar authority. */
```

### First: the structure budget

`piplet_json_within_budget()` counts three quantities during a character-by-character scan — structural characters, opened containers, nesting depth — while correctly skipping strings and escapes.

Why? Because `json_decode`'s `depth` parameter bounds only *nesting*, not *quantity*. A body of `[1,1,1,…]` has depth 2 and looks entirely harmless. I measured it:

| | Value |
|---|---|
| Payload `[1,1,…]` at the request limit | 5,242,879 bytes, 2,621,439 elements |
| `json_decode()` memory increase | **91 MiB** |
| The pre-scan | `false` in **0.05 ms**, no increase |

With the `memory_limit=128M` the README requires, this particular body still fits — the peak lands around 105 MiB. Swap the ones for `[1]` and the same 5 MiB hold 1.3 million nested arrays: then the request dies on a fatal error instead of a clean HTTP 413. That is exactly the kind of payload the pre-scan catches before anything is allocated.

The container counter is the genuinely interesting quantity here: it is **cumulative, not maximal**. 8,192 flat objects side by side are bounded just as 8,192 nested ones are.

### Second: duplicate member names

`json_decode('{"a":1,"a":2}')` throws no error. The last value wins, the first disappears silently. Injecting a record with `{"version":"<real>","version":"<forged>"}` lets a validation and the later use drift apart.

The scanner keeps a small stack and checks every member name for uniqueness. The trick is that it **decodes** the name instead of comparing bytes:

```php
$member = json_decode(substr($json, $index, $end - $index + 1), false, 2, JSON_THROW_ON_ERROR);
$lookup = "\0" . $member; // Prevent numeric-string conversion by PHP arrays.
if (isset($stack[$slot][$lookup])) return false;
```

Why that matters shows up in one line of my measurement series: in `{"a":1,"\u0061":2}` two different byte sequences denote the same member name. A byte comparison would have waved it through; `json_decode` collapses it. And the `"\0"` prefix is no decoration: PHP arrays cast decimal string keys to integers. `array_keys(get_object_vars(json_decode('{"123":"x"}')))` returns `[int(123)]`. Precisely for that reason — and only for it — piplet's ID rule excludes pure digit strings (`!ctype_digit($value)`).

### Third: lossless numbers

The third scanner isolates every number outside strings and checks a **round trip against its own encoder flags**:

```php
if (json_encode($decoded, PIPLET_JSON_FLAGS) !== $number) return false;
```

Decode, re-encode, compare byte for byte. Whatever does not come back identical is rejected. An extract from the measurement:

| Number | Lossless? | Reason |
|---|---|---|
| `1` | yes | — |
| `1.0` | yes | only thanks to `JSON_PRESERVE_ZERO_FRACTION` |
| `0.30000000000000004` | yes | float round trip holds |
| `9007199254740993` | yes | PHP integer, exact |
| `1e2` | **no** | becomes `100.0` |
| `-0` | **no** | becomes `0` |
| `12345678901234567890` | **no** | becomes `1.2345678901234567e+19` |
| `1e999` | **no** | becomes `INF`, `json_encode` throws |

The reason is not pedantry but the architecture: **every save reads the whole document, decodes it and re-encodes it completely.** Any number PHP does not reproduce identically would be silently rewritten on the next foreign save — including in fields piplet does not know about, since unknown keys survive the mutation. Without the check, piplet would have documents it can read but never write again.

The calibration is noteworthy: it checks **PHP's** round trip, not JavaScript's. `9007199254740993` passes, even though the browser would lose the number. For the fields that actually travel to the browser there is a separate limit: `PIPLET_MAX_REVISION = 9007199254740991` — exactly `Number.MAX_SAFE_INTEGER`.

**Interim conclusion:** together the three scanners are under 130 lines and cover three classes a parser cannot cover in principle: cost *before* allocation, silent collapse semantics and silent precision loss. That is the figure of thought behind it — **cheap, allocation-free pre-checks; the parser still owns the grammar.** Transferable to every place where you hand foreign data to a parser: XML, YAML, CSV, image formats.

## Two editors, no database

The third idea addresses a problem you would normally have a database for: what happens when two people write at the same time?

piplet layers three quantities:

| Quantity | Type | Role |
|---|---|---|
| `generation` | 32 hex characters (128 bit) | identity of the **file's lineage** |
| `version` | 32 hex characters, **per record** | identity of **this revision** of this record |
| `revision` | integer, document-wide plus per record | display counter and ordering relation |

Every mutation sends all three as preconditions; the two hex values are compared with `hash_equals()`, the revision as an integer with `!==`. And here sits the decision that makes the whole model usable: **the document revision is never a precondition.** What is compared is always the affected record's version. That is why the promise "two editors can safely change different notes" holds — every save bumps the global counter, but that does not invalidate anyone else's preconditions.

If one of the three base values is missing, the server answers not with 422 but with **HTTP 428 Precondition Required**. That is the status code it exists for, and I almost never see it in the wild.

### The ABA problem

Classic shape: a second client deletes note `foo` and recreates it under the same name. It *looks* like before. The first client still holds `id=foo` and would overwrite blindly.

The slug really is reused — after deletion it is free again. The case is detected via the **version**, not the ID. A 128-bit random value per revision cannot return through delete-and-recreate. The sequence, played through against a working copy:

```text
create#1                      => OK   id=idem revision=8  version=cee493c2…
delete                        => OK   docRev=9
recreate same title           => OK   id=idem revision=10 version=faddd3dd…   (same slug!)
old client saves              => 409  This note changed after you opened it.
```

The revision alone would notice here too (10 ≠ 8) — but not after a restore from backup. That is what the generation is for. And the README is honest about the limit:

> "They cannot detect an exact rollback to the same generation and versions a client already loaded; that requires trusted state outside this one file, such as an append-only log or database."

So against an exact rollback the remedy is: a database. That is a remarkably open sentence for a project whose selling point is "no database".

### Idempotency without state

The other classic: the client sends "create a new note", the response is lost, the client retries. Without precautions you now have two notes.

Every editor generates a `createToken` when it opens — 128 bit from `crypto.getRandomValues`, **stable** across retries and stored alongside the draft in `sessionStorage`. Server-side there are three outcomes:

```text
create#1                              => OK   id=idem revision=8 docRev=8
create#2 identical retry              => OK   id=idem revision=8 docRev=8   (unchanged!)
create#3 same token, different text   => 409  This new note was already saved with different content.
```

The second request returns 200 with the same note **and an unchanged document revision**. Not a single byte is written: a dedicated return type short-circuits before the increment and before persisting. The third case is the genuinely interesting design decision — with the same token and differing content, piplet neither silently creates a second note nor overwrites; it returns 409 with the existing note and lets the human decide.

And the token stays attached to the note for life. The dedupe window is therefore **unbounded as long as the note exists** — not time-based, as idempotency keys in APIs usually are.

**Interim conclusion:** optimistic concurrency across three levels — lineage, record revision, display counter — replaces a transaction here. That is not an exotic construct but exactly what an HTTP `ETag` and `If-Match` do, only per record instead of per resource. If you are building an API where two clients can edit the same object: **a random version per revision beats a timestamp, and a stable idempotency key belongs to client state, not to the request.**

## Everything in the browser, everything from one file

The largest single block of the file is not the PHP but the JavaScript: 1,597 lines, 81 KB. There is no build, no module, no framework — one IIFE, 14 `let` variables as a single block of state, no diffing, no router.

Three details I found instructive.

**The boot transport.** The entire dataset travels inline with the first response. There are no read endpoints, only three write actions. And the data enters the page like this:

```html
<script type="application/octet-stream" id="piplet-state" nonce="…">…base64…</script>
```

Two properties interlock. `type="application/octet-stream"` is not a JavaScript MIME type — the browser parses the content as a data block, executes nothing, but exposes it through `.textContent`. And **base64 contains no `<`**. The alphabet is `A–Z a–z 0–9 + / =`. That makes it mathematically impossible, regardless of note content, to form `</script>` or any other HTML end tag out of the boot state. Not "unlikely" — impossible. That is a far stronger statement than any escaping.

**Note text never reaches the rendered HTML.** No note title, no body, no tag appears in the server-generated markup; the `<title>` is static. The only user input that shows up there at all is the custom CSS — and it travels as a JSON string with `JSON_HEX_TAG` and its siblings into a pure `textContent` assignment. In the client everything goes through a helper that only ever sets `node.textContent`. There is exactly **one** `innerHTML` access in the whole program, and both callers pass literal SVG paths. `eval`, `new Function`, `insertAdjacentHTML` and `document.write` do not appear.

The CSP is correspondingly terse:

```text
default-src 'none'; style-src 'nonce-…'; script-src 'nonce-…'; connect-src 'self';
img-src data:; base-uri 'none'; form-action 'self'; frame-ancestors 'none'
```

`img-src data:` — **no `'self'`**. Even the app's own image files would be blocked. Side effect: the classic CSS exfiltration through `background: url(https://…)` in attribute selectors dies with it. And because the custom CSS feature is deliberately unbounded (32 KiB of complete stylesheet, no parser, no selector allowlist), that is exactly the containment: CSS can wreck the interface, but it cannot load anything and cannot become script. For the case where you do wreck your interface there is `?safe=1` — the parameter changes nothing about headers or permissions; it blanks the custom CSS on both server and client and shows a notice with a way back.

**The draft rescuer.** Around 250 lines exist solely so that text the server has never seen survives. What convinced me is not the mechanism but the stance: `sessionStorage` is treated as **hostile input**. Every recovered draft passes a structure budget of its own (256 structural characters, depth 8 — the JS mirror of the server scanner), schema validation, a key-shape check, a Unicode repair for lone surrogates, and a **read-back verification**:

```js
if (raw.length > 512 * 1024 || !sessionWrite(source.recoveryKey, raw)
    || sessionRead(source.recoveryKey) !== raw) {
    source.recoveryWarning = 'This browser could not store the latest draft. …';
    return false;
```

The success of `setItem` is not believed; the value is read back and compared. That catches browsers which silently swallow or truncate.

And the consequence is thought through to the end: if the draft cannot be secured, the application **refuses** the state change instead of carrying on optimistically. The editor stays open and says why. Fittingly, `beforeunload` is registered only in exactly that case — a normal editing session never shows a "leave page?" dialog. That is the inverse of what most applications do, and I think it is right: if the dialog is an exception, users take it seriously.

**Interim conclusion:** when you need to get data from server into page, base64 in an inert element is more robust than any escaping, because it excludes the attack class through the alphabet rather than a filter rule. And: treat your own client storage as foreign input. `JSON.parse(localStorage.getItem(…))` followed by trust is where many SPAs are surprisingly careless.

## How you even test something like this

The test suite is 3,614 lines and has no dependencies. No framework, no autoloader, no class hierarchy. The complete assertion library is eight lines:

```php
function check(bool $condition, string $message): void
{
    global $assertions;
    $assertions++;
    if (!$condition) throw new RuntimeException($message);
}
```

I ran it, on PHP 8.4.24:

```text
ok — 433 assertions; source file untouched; 7+ MiB cycle 10.86s (worker peak 27.6 MiB)
```

Those 433 are the count of a run **with** the browser regressions. My first attempt did fail — the Snap Chromium under WSL got stuck on its mount-namespace construction — but the second went through. When the browser really is missing, the suite says so itself and counts differently: you get `skip — Chrome unavailable; dynamic browser regressions were not run` above the summary and 436 assertions below it. The README is strict here — "a skip is not evidence that the browser boundary passed" — and for release validation there is `PIPLET_REQUIRE_CHROME=1`, which turns a missing browser into a failure.

What is interesting is how the hard cases are made testable at all. The consistent method is **source instrumentation of a throwaway copy**, never mocking:

1. Create an isolated copy.
2. Define an exact, multi-line needle.
3. `check(substr_count($source, $needle) === 1, 'Could not locate …')` — **exactly one** hit, or the test aborts.
4. Insert an environment-driven checkpoint via `str_replace`.

Step 3 is the clever part: if the application is refactored and the needle disappears or doubles, the test fails **loudly** instead of quietly doing nothing.

That makes things provable that would otherwise only be reproducible by chance:

- **The inode race, deterministically.** A file-based barrier right after the `fopen`: the child writes `<barrier>.opened`, the runner checks the inode number, replaces the file, writes `<barrier>.release`. A `.passed` marker makes the barrier **one-shot**, so the retry pass does not block again, and the injected wait loop has its own deadline and throws.
- **Crash consistency with a real SIGKILL.** The checkpoint signals and deliberately hangs (`while (true) usleep(10000);`), then `proc_terminate($process, 9)`. No `finally` runs — that is the entire point. Afterwards: canonical file unchanged by SHA-256, exactly one orphan at mode 0600, and the orphan is **executed** and must answer `Save in progress.`
- **Partial writes without an OS fault injector.** A custom stream wrapper whose `stream_write` never accepts more than 7 bytes. 120,000 bytes pushed through, result must be byte-identical.
- **Missing `fsync`** via `php -d disable_functions=fsync` — expecting an abort with an **unchanged** file hash.
- **Lost responses** with no network fault at all: send the same payload twice, then compare not just the content but the **inode number** — proof that no `rename` happened.

And the harness tests its own primitives before using them: that the command deadline holds, that the worker refuses `/etc/passwd`, that the server-readiness check does not accept a foreign process on the same port, that 1 MiB of worker output arrives untruncated — and that the runner itself returns 404 under a web SAPI.

**Interim conclusion:** 280 `check()` sites produce 433 assertions for 3,584 lines of application code. For comparison: 64 assertions for the 78-line demo. The size of the two suites is the most precise description available of what the small edition's missing properties cost.

## What operating it requires

It is worth looking closely here — not as a verdict, but because the conditions themselves are instructive.

**PHP 8.1 is not an arbitrary floor.** `fsync()` only exists in PHP as of 8.1 (RFC "fsync_function", accepted 30 votes to 1). And piplet makes saving hard-dependent on it:

```php
if (!function_exists('fsync')) {
    throw new PipletHttpError(503, 'Saving is unavailable because file synchronization is disabled.');
}
```

The second requirement — 64 bit — has an independent reason: `hrtime(true)` returns a float instead of an integer on 32 bit, and `PIPLET_MAX_REVISION` is 2⁵³−1 and not representable there. The code checks that separately.

**Deployment is not "upload a file".** The README demands a dedicated HTTPS origin containing only `index.php`, `PIPLET_PASSWORD` in the worker environment, every other path denied at the web server, and a backend reachable only through the TLS proxy. The requirement list for the proxy runs over ten lines: at most 5 MiB body, a 4 KiB request target, at most 64 headers, rejection of conflicting `Content-Length`/`Transfer-Encoding` combinations, rate limiting of failed authentication, Fetch Metadata checks. Unsupported are NFS/SMB, multiple hosts, serverless filesystems, hard-linked aliases and Windows — though none of that is enforced in code, there is no filesystem type check.

The password is mandatory, without exception, including on loopback. I verified that by absence: the complete list of `$_SERVER` keys read anywhere in the file has twelve entries — **no `REMOTE_ADDR`, no `HTTP_HOST`**. With no password there is HTTP 403 in plain text, deliberately without `WWW-Authenticate`, because a login dialog that can never succeed would be misleading. And the string "forward" appears **not once** in the entire program — `X-Forwarded-Proto` and `Forwarded` are not merely ignored, they are never even mentioned.

**The frame a web-writable PHP file sits in.** These rules exist regardless of how clean the code is. The [Apache Software Foundation's HTTPD wiki](https://cwiki.apache.org/confluence/spaces/HTTPD/pages/115522317/FileSystemSecurity) puts it most directly:

> "Read access only. The web server user should not own, or be able to write to, its configuration files or content."

Germany's BSI requires, as a baseline in IT-Grundschutz APP.3.2.A1 (2023 edition), that all unnecessary write permissions be removed from the web server service, and in A2 that scripts and configuration files be protected against unauthorised reading and modification. And PCI DSS 11.5.2 (v4.0.1) requires at least weekly file comparisons on "critical files", defined as files that normally do **not** change but whose modification could indicate a compromise.

Precision matters to me here: none of these sources contains a sentence banning self-modifying code by name. And the WordPress hardening handbook is more nuanced than it is usually quoted — it explicitly permits write access by the web server process for `/wp-content/`, just not for core code. What can be **inferred**, and this is my inference rather than a source statement: change detection on this one file can no longer distinguish "someone saved a note" from "someone injected code". It is the same event.

The README, incidentally, anticipates the objection itself:

> "A web-writable PHP file is intentionally unusual. Keep backups and do not deploy it where policy or hardening rules forbid self-modifying code."

**And then there is OPcache.** This is where I learned the most, because it applies without piplet too. `__COMPILER_HALT_OFFSET__` is baked into the opcodes as a literal at compile time. As long as only the data *behind* the marker changes, the prefix stays the same length and the cached offset remains correct — which is why ordinary saving is harmless, and why piplet never calls `opcache_invalidate()`. But if the **length of the code prefix** changes, the cached offset points into nothing.

I reproduced the failure path. A piplet-style file, included twice in the same request, rewritten in between with a longer prefix:

```text
1st include: off=144 data='PAYLOAD_ONE'
file rewritten. new size=189
2nd include: off=144 data='), $o)];\n__halt_compiler();PAYLOAD_TWO_LONGER'
TRUE offset on disk = 171, true payload = 'PAYLOAD_TWO_LONGER'
```

The second include reads **source code as payload**. Three findings you cannot guess:

- Without OPcache it does not happen. It is purely a cache effect.
- `opcache.revalidate_freq=0` does not help. It does make OPcache re-check the file within the same request — but `stat()` only returns whole seconds, and two writes inside the same second look identical to the cache.
- The default `opcache.file_update_protection=2` **hides** the bug, because a freshly written file is not cached at all. It only bites once the file is older than two seconds — so in production, not in a quick test.

And invalidation itself has a trap I did not know:

| Call | Return | Result |
|---|---|---|
| *(none)* | — | corrupt |
| `opcache_invalidate($f, false)` | `true` | **corrupt despite `true`** |
| `opcache_invalidate($f, true)` | `true` | correct |

The non-forced variant reports success and does nothing. The cause is mtime resolution: PHP's `stat()` returns whole seconds — two writes one millisecond apart share an mtime, so the file does not count as "newer". **After any code change to a file that is written at runtime, `force = true` is mandatory.** That holds for every template-cache and codegen scenario, not just this one.

For fairness, the comparison: frameworks write code at runtime all the time. Blade compiles templates to PHP, Symfony generates its container, WordPress has updated itself since 2013. The technical differences are nameable and all four documented: those artefacts live **outside** the document root; they are **deterministically reconstructible** from source files, so deleting them costs performance and not data; they can be **warmed at build time**, making read-only operation possible; and WordPress auto-updates only write directly when the PHP process itself owns the core files — `get_filesystem_method()` compares `fileowner(__FILE__)` with the owner of a freshly created temp file; if that does not match, WordPress falls back to FTP or SSH. With piplet the written file is the one requested by URL, it is the only copy of the content, and writing is normal operation.

**Interim conclusion:** the simplicity is in the artefact, not in the operation. Copying one file is simple; building the origin, the proxy, the permissions, the backups and the OPcache discipline around it is not. That is not a weakness of the implementation — it is the price of code and data sharing an inode.

## What of this fits into your own projects

Here is the harvest. Nine patterns that have nothing to do with self-modifying PHP, each with the question of when they pay off and when they are overkill.

**1. Temp file + `fsync` + `rename` instead of writing in place.** Worth it for every file replaced during operation whose half-state does damage: configuration, caches, exports, state documents, generated assets. Overkill for append-only logs, where a truncated last entry is survivable.

**2. Validate locks against identity, not against the path.** As soon as anything in the system replaces via `rename`, a lock on the *first opened* descriptor is worthless. Compare `fstat` against `stat` and retry. Overkill if only one process ever writes — but you rarely know that for certain.

**3. Randomised backoff with a shared, monotonic deadline.** `hrtime()`, not `time()` — NTP jumps and daylight saving must not shift a deadline. And compute the deadline once before the loop instead of per attempt, otherwise it is not one.

**4. Compute the cost before you incur it — and audit the model.** The size projection with a subsequent comparison against the real result is the most transferable detail in the whole program. Worth it wherever a hard ceiling exists and the expensive path already costs resources. Overkill for soft limits, where aborting after the fact is fine.

**5. Cheap, allocation-free pre-checks before the parser.** The parser stays the authority on grammar; the scanner only answers "am I allowed to touch this at all". Worth it at every boundary where foreign data enters a parser. Overkill for data from your own build.

**6. Optimistic concurrency with a random version per revision.** Timestamps collide, counters return after a restore, a 128-bit random value does not. Together with a stable idempotency key on the client side, that covers the two most common multi-user failures. Overkill in single-writer systems.

**7. Artefacts that recognise themselves.** `str_contains(basename(__FILE__), '.piplet-tmp-')` turns any accidentally executed intermediate copy into a 503 instead of a second application instance. Worth it for every temporary artefact in an executable path. Overkill when the artefact structurally lives outside — then location is the better answer than naming.

**8. Clean up only what you identified yourself.** Before deleting, check `dev`/`ino` against the remembered state; if it does not match, log instead of delete. That costs five lines and prevents the kind of failure people write post-mortems about.

**9. Turn failed guarantees into refusals.** If the backup cannot be confirmed, the editor stays open and says why — instead of carrying on optimistically. Worth it wherever a silent loss would go unnoticed. Overkill for actions with no risk of loss, like sorting or filtering, where refusal only creates friction.

### And where does the core idea itself hold up?

Separately, the question of the shape "program and data in one file". It holds where deployment is the actual problem and the data stays small: an internal notes tool on a machine without a database, a throwaway tool that arrives by `scp` and disappears two weeks later, a teaching example. It does not hold with multiple writers, clustered hosting, containerised filesystems, or anything that needs change detection.

There is a remarkable answer to this question from the project's own surroundings. The only fork with substance of its own — [`kasparsd/piplets`](https://github.com/kasparsd/piplets), created on the day of my research — writes in its `plan.md` under "Research findings (verified, not assumed)":

> "Does the data need to live inside the PHP file? **No.** The data section is pure JSON — the `__halt_compiler()` boundary existed only because data shared the file with executable code. Once external, it is a plain `.json` document (validated: format 2, bounded structure, lossless numbers, unique members)."

One of five stated goals of that fork, whose main goal is a bundled CLI executable: "Separates **code from data**: the engine stays immutable; the document is a portable pure-JSON file." So the first imitator with substance of its own removes precisely the property that defines piplet — and keeps everything else: the atomic persistence, the validation, the conflict handling.

I consider that the most honest summary this project could have received. The value is not in the boundary being torn down. It is in the care with which the consequences are caught — and that is fully transferable to programs which respect the boundary.

## Conclusion

I stumbled over a repository today whose core idea I took for a curiosity, and I am ending the day with a notebook full of patterns I will reuse.

The core idea is the least interesting layer. "Data behind `__halt_compiler()`" is four lines, and the 78-line demo shows all of it. Everything interesting arises only from the question of what happens when those four lines have to survive real operation: two writers at once. A power cut in the wrong millisecond. A backup two weeks old. A browser that silently swallows `setItem`. A JSON body with 2.6 million ones in it.

The 3,506 lines of difference between the small and the large edition are almost entirely answers to those questions — and they are what makes the source worth reading, whether or not you care about wikis or WordPress or PHP. It is rare to be able to read, in such concentrated form, what durability costs in detail.

What I like most about the whole thing is the honesty of the README. It names the limits of its own approach more clearly than any reporting about it — right down to the sentence that against an exact rollback only "trusted state outside this one file, such as an append-only log or database" helps. A project that starts out with "no database" and documents the one case where it would need one has my respect.

One last thought that belongs on this blog. I have often written here that [files in a directory](https://www.jpkc.com/db/en/blog/sveltia-cms/) are the most stable foundation a project can have — because you can take them with you again. piplet pushes that idea to its extreme: there is only **one** file left, and it contains everything. The fork next door draws the conclusion and separates again. Between those two points lies exactly the decision you make in every system that holds state — just rarely as visible as it is here.

## Further reading

- [Why 11ty: Six Years Static](https://www.jpkc.com/db/en/blog/warum-11ty/) — the same longing for reduction, solved from the opposite direction.
- [Sveltia CMS for 11ty](https://www.jpkc.com/db/en/blog/sveltia-cms/) — why files in a directory are the shape you can take off again.
- [Working Safely With Node.js and npm](https://www.jpkc.com/db/en/blog/nodejs-npm-absichern/) — dependency freedom as a security argument, with the costs beside it.
- [Who Checks the Checker? Adversarial Fact-Checking for AI Texts](https://www.jpkc.com/db/en/blog/faktentreue-review/) — why a project whose commits all come from an agent makes me measure twice.
- [WordPress/piplets on GitHub](https://github.com/WordPress/piplets) — source, README and test suite, the origin of every quote here.
- [WCUS 2026 recap on wordpress.org](https://wordpress.org/news/2026/08/wcus-2026-recap/) — the paragraph most of the reporting leans on.
- [Closing Keynote — Fireside Chat with Matt Mullenweg and Robert Jacobi](https://www.youtube.com/live/notaTgEZByo) — the full recording of the session, 1:07:48.
- [`__halt_compiler` in the PHP manual](https://www.php.net/manual/en/function.halt-compiler.php) — the mechanism, since PHP 5.1.0.
- [Ensuring data reaches disk](https://lwn.net/Articles/457667/) — Jeff Moyer's LWN article with the five-step reference for atomic replacement.

