What a host inspects
This is an audit of the smallest surface a custom application has to speak so a host of WordPress sites accepts the webroot. It is not a guide to forking WordPress software, and it is not a copy of that software. It is a map of the contracts hosts, backup tools, monitors, and WP-CLI actually inspect.
The point of writing it down is that the map is much smaller than the tree. Once the map exists, the same application can be written in PHP, Go, Rust, or anything else, as long as it still presents this shape.
WordPress 7.1 is the release whose contracts this page names.
The three layers
The webroot is one directory a host of WordPress sites already knows how to operate. Three things occupy it. They are not three packages. Data sits inside adaptor folders because that is where backups already look. Minn is the exception: one folder of its own.
Three things occupy one webroot. They are not three packages. Data sits inside adaptor folders because that is where backups already look.
The dump. Twelve wp_* tables.
Fork vs from scratch
Forking WordPress and building something WordPress-compatible are different jobs. They get mixed up because both produce a directory that a host will call "a WordPress site."
A fork keeps the tree. ClassicPress is the clean example: it kept the contracts and stayed a fork, so the license came with the files. Every wp-includes/.php file is still WordPress's file, or a descendant of one. The host is happy because the files are there. The GPL is not optional, because the implementation is* the files.
A from-scratch application keeps the contracts and throws the tree away. Nginx did this to Apache's HTTP. Samba did this to SMB. Wine did this to Win32. The surrounding world does not move. Compatibility is the product, and the implementation is a new work.
WordPress makes the second job look harder than it is, because what people call "the WordPress standard" is not a published protocol. It is a pile of PHP files. Hosts, WP-CLI, backup plugins, and migration tools do not read a spec. They file_exists() a path, regex a config, parse $wp_version out of wp-includes/version.php, and run wp option get home. If the file is missing, the host does not treat the directory as a WordPress site. If the file is present and the output shape matches, it does.
That is the trap. Meeting the standard looks like "ship all of those files." Shipping all of those files is a fork. The files are the interface. Their contents, except for a handful of parseable assignments and a boot chain that a regex can follow, are the implementation. A compatible application has to put the interface on disk. It does not have to put WordPress inside it.
Including WordPress GPL code is how you fork. Matching the file names, the table names, the HTTP paths, and the CLI verbs is how a host of WordPress sites accepts the webroot.
Minn Engine is one implementation of this map. The map is the point of the page. Another runtime in another language can follow it without carrying any of Minn, and without carrying any of WordPress.
The standard is a pile of files
WordPress 7.1 ships about 1,315 PHP files and 2,130 files in total, 1.8 million lines, once the bundled themes and plugins are left out. Roughly a thousand of those PHP files live under wp-includes/. Another 242 live under wp-admin/. Fourteen PHP files sit at the webroot.
There is no document that says "these fourteen are the host contract, and these thousand are the runtime." The installer drops the whole tree. Hosting panels treat the whole tree as the install. wp core verify-checksums hashes thousands of those files against wordpress.org. A missing wp-includes/functions.php is, to that world, a broken WordPress site.
For a host that is trying to operate the site, almost none of that tree is load-bearing. The operator needs to:
- Detect that the webroot is a WordPress site.
- Read database credentials without running the application.
- Open a MySQL database whose tables match the
wp_*schema. - Hit a few well-known URLs and not get 404s.
- Run a handful of WP-CLI verbs whose stdout they already parse.
- Back up files plus a database dump, and restore them somewhere else.
Those six jobs are the adapter. Everything else (the admin screens, the block editor packages, TinyMCE, the Customizer, the PHP functions a plugin would call) is software that happens to live in the same tree. A from-scratch application that wants a web host to operate it the way it operates a WordPress site implements the adapter. Looking like WordPress software to a plugin is a different, much larger contract, and it is not what this page is for.
What a host is
In this document, "host" means the operational layer around the site, not a person in a browser:
- Platforms that host WordPress sites (Kinsta, WP Engine, and the rest) that detect the install, run WP-CLI, probe
/wp-login.phpand/wp-admin/, and inject drop-ins. - Fleet tools (CaptainCore, Disembark) that inventory, back up, migrate, and search-replace.
- Backup plugins and migrators that regex
wp-config.phpand dumpwp-content/plus the database. - Uptime monitors that GET
/and a short list of well-known PHP paths. - WP-CLI itself, which decides whether the working directory "seems to be a WordPress installation."
Humans using /wp-admin/ are not this audience. Minn answers that audience with Minn Admin. A from-scratch app can answer it however it wants.
Files on disk
This is the layer that makes wp core version and a hosting panel treat the directory as a WordPress site. PHP-FPM and nginx decide it before any application code runs: a missing .php file is a 404, and a directory without an index is often a 403. The files have to exist as files.
The boot chain
An unmodified wp-config.php is the whole trick. Backup tools, WP-CLI config and db commands, and host panels parse that file as text. They must not have to edit it. The stock file ends with require_once ABSPATH . 'wp-settings.php'. Whatever sits at wp-settings.php is the application.
index.php
defines ABSPATH as this directory
requires wp-config.php
wp-config.php (the file WordPress generated; never rewrite it)
defines DB_* and the eight KEY/SALT constants
sets $table_prefix
requires wp-settings.php
wp-settings.php
boots your application
index.php must define ABSPATH before loading the config. WordPress itself does this in wp-load.php. Custom configs test defined('ABSPATH'). If the constant is missing, those configs fatally assume they are being included the wrong way.
Shape files a host needs
| Path | Who reads it | What it must do |
|---|---|---|
index.php | The web server, WP-CLI (extract_subdir_path) | Define ABSPATH, require wp-config.php. |
wp-config.php | Every backup and migration tool (regex), WP-CLI config and db | Exist, in the generated shape. Do not ship it. Do not rewrite it. |
wp-settings.php | The last line of wp-config.php | Boot the application. Two lines is enough. |
wp-includes/version.php | WP-CLI (wp_exists() is file_exists on this path; core version parses assignments by string search), hosting panels, backup tools | Assign $wp_version, $wp_db_version, $tinymce_version. The values name the release whose contracts you speak. The file contains none of that release's code. |
wp-login.php | nginx (a missing .php is a 404 before PHP runs), hide-login plugins that require ABSPATH . 'wp-login.php', monitors | Exist. Answer GET with a sign-in form or a 302. Never 404. |
wp-admin/index.php | nginx (a directory without an index is a 403), monitors, panels | The directory and this file must exist on disk. A 302 away from /wp-admin/ is fine. A 404 is the one status that tells a panel the directory is not a WordPress site. |
wp-cli.yml | WP-CLI, if you intercept verbs before WordPress would load | Optional. The way a from-scratch app registers commands without shipping wp-load.php. |
wp core version does not boot the application. It includes wp-includes/version.php and prints $wp_version. A one-assignment file is a complete implementation of that verb:
<?php
$wp_version = '7.1';
$wp_db_version = 61833;
$tinymce_version = '49110-20250317';
$required_php_version = '7.4';
$required_mysql_version = '5.5.5';
WP-CLI also wants $wp_version >= 3.7 after including the file. Hosting panels string-search the same assignments. Matching the format ($wp_version = '7.1'; on its own line) matters more than matching comments.
wp-content/
| Path | Role |
|---|---|
wp-content/plugins/ | Plugin folders, or a single-file slug.php. Headers in the plugin file are how wp plugin list gets name, title, and version. |
wp-content/themes/ | Theme folders. style.css headers are how wp theme list gets name and version. Template: names the parent. |
wp-content/uploads/ | Media. Convention is YYYY/MM/filename. The web server serves these as static files. |
wp-content/mu-plugins/ | Must-use plugins. Hosts drop their own helpers here. They must be allowed to exist. |
wp-content/languages/ | Translation packs. Optional for detection. |
Drop-ins (object-cache.php, advanced-cache.php, db.php, maintenance.php, sunrise.php, …) | Hosts inject these. Redis object cache is object-cache.php. They must be allowed to sit in wp-content/. Loading them is a runtime concern; forbidding the filename is how you get caught. |
Plugin file header, first docblock in the main PHP file:
Plugin Name: Example
Version: 1.0.0
Theme header, top of style.css:
Theme Name: Example
Version: 1.0.0
Template: parent-slug
wp plugin list --format=json --fields=name,title,status,version (CaptainCore's inventory) is a directory walk plus the active_plugins option. You do not have to run the plugin. You have to report it.
Files that are deliberately not required
These exist in a WordPress tree and nothing in the host layer fails if your application answers them another way, or not as files:
| Path | Why it is in the tree | Compatible stand-in |
|---|---|---|
wp-load.php, wp-blog-header.php | WordPress's own include chain | Not needed if index.php loads the config. WP-CLI finds the root through index.php or the working directory. |
xmlrpc.php | XML-RPC | The router can answer /xmlrpc.php. A file is nicer for nginx, not required if the host sends unknown .php through the front controller (Cove does; Kinsta does not). |
wp-cron.php | Pseudo-cron | Same as xmlrpc: a file on hosts that 404 missing .php, or a route on hosts that do not. GET/POST should return 200 with an empty body. |
wp-comments-post.php, wp-mail.php, wp-links-opml.php, wp-trackback.php, wp-activate.php, wp-signup.php | Legacy endpoints | Not what a host probes. |
readme.html, license.txt | Version fingerprinting in the browser | Optional. Some scanners use them. They are not an operational surface. |
The rest of wp-includes/ and wp-admin/ | The implementation | Placeholders so require ABSPATH . 'wp-admin/includes/plugin.php' resolves. Empty PHP files. Not WordPress code. |
The last row is the whole argument in miniature. Plugin code and host mu-plugins write require ABSPATH . 'wp-includes/plugin.php'. On a WordPress host that request is a file read, not a protocol. The compatible answer is an empty file at that path, with the symbols provided by your runtime (or not, if you are not running plugin PHP). The compatible answer is not "copy plugin.php out of WordPress." Copying it is a fork.
wp-config.php
Treat this file as data. Hosts and tools read it with regexes. They do not want to run it, and a from-scratch installer must not rewrite it.
What the regexes look for
define( 'DB_NAME', 'database_name_here' );
define( 'DB_USER', 'username_here' );
define( 'DB_PASSWORD', 'password_here' );
define( 'DB_HOST', 'localhost' );
define( 'DB_CHARSET', 'utf8mb4' );
define( 'DB_COLLATE', '' );
define( 'AUTH_KEY', 'put your unique phrase here' );
define( 'SECURE_AUTH_KEY', 'put your unique phrase here' );
define( 'LOGGED_IN_KEY', 'put your unique phrase here' );
define( 'NONCE_KEY', 'put your unique phrase here' );
define( 'AUTH_SALT', 'put your unique phrase here' );
define( 'SECURE_AUTH_SALT', 'put your unique phrase here' );
define( 'LOGGED_IN_SALT', 'put your unique phrase here' );
define( 'NONCE_SALT', 'put your unique phrase here' );
$table_prefix = 'wp_';
require_once ABSPATH . 'wp-settings.php';
A parser that only handles quoted string literals will miss real configs. Tolerate at least:
define('DB_NAME', getenv('WORDPRESS_DB_NAME'))- the official Docker image's
getenv_docker('WORDPRESS_DB_NAME', 'wordpress')(env, then{NAME}_FILE, then the fallback) - a
$table_prefixthat is notwp_(random prefixes are common; the allowed character class is[A-Za-z0-9_])
Hosts also inject extra define()s (DISABLE_WP_CRON, FS_METHOD, DISALLOW_FILE_EDIT, AUTOMATIC_UPDATER_DISABLED, WP_CACHE, WP_CONTENT_DIR). Ignore unknown constants. Do not round-trip the file.
WP-CLI's config get and db * (the ones that run after_wp_config_load) strip the trailing require, eval the rest, and talk to MySQL with those credentials. If wp-includes/version.php exists and wp-config.php is parseable, wp db export works without your application existing at all.
Twelve tables
The portable unit of a WordPress site is files plus a database dump. The dump is MySQL/MariaDB. The table prefix comes from $table_prefix. The twelve core tables, with the prefix stripped:
| Table | What lives there |
|---|---|
posts | Posts, pages, attachments, revisions, navigation, templates, and every other post type. One table, discriminated by post_type and post_status. |
postmeta | Per-post key/value. meta_value is often a serialized PHP blob. |
options | Site options. option_name is unique. option_value is often a serialized PHP blob. autoload is yes/on/auto-on or no/off/auto-off. |
users | Accounts. user_pass holds the password hash. |
usermeta | Per-user key/value. Primary key column is umeta_id, not meta_id. Session tokens, capabilities, and locale live here. |
terms | Term names and slugs. |
term_taxonomy | Term plus taxonomy, description, parent, and count. Reads trust the stored count. |
term_relationships | Object to term_taxonomy_id. |
termmeta | Per-term key/value. |
comments | Comments and the comment-shaped rows (comment_type). |
commentmeta | Per-comment key/value. |
links | The old blogroll. Empty on modern sites. Still there. |
Anything else (wp_woocommerce_, wp_wf, …) is plugin data. A host dump includes it. An adapter that only speaks core can ignore extra tables and must not drop them.
Columns that tooling actually uses
posts
| Column | Notes |
|---|---|
ID | Unsigned bigint, primary key. |
post_author | User ID; 0 is authorless (wp-cli's default). |
post_date, post_date_gmt | Local and GMT. 0000-00-00 00:00:00 is a real stored value. |
post_content | The field. Blocks, classic HTML, or anything else. |
post_title, post_excerpt | Text. |
post_status | publish, draft, future, pending, private, trash, auto-draft, inherit. |
post_name | Slug. |
post_modified, post_modified_gmt | |
post_parent | Pages, attachments, revisions. |
guid | Stored as written. Search-replace tools skip this column on purpose. |
menu_order | Pages. |
post_type | post, page, attachment, revision, plus the rest. |
post_mime_type | Attachments. |
comment_count | Stored. Writers recount. |
options, the keys fleet tooling reads first: home, siteurl, blogname, blogdescription, admin_email, template, stylesheet, active_plugins, permalink_structure, cron, site_icon, WPLANG. home and siteurl are how a host knows the public URL. template / stylesheet are the active theme folder (and its parent). active_plugins is a serialized PHP array of folder/file.php paths.
users.user_pass, modern scheme, observed from running WordPress: a $wp$ prefix, then bcrypt over base64(hmac_sha384(password, "wp-sha384")). Legacy $P$ (phpass) still exists on old rows. A host does not hash passwords, but a tool that logs in (or that copies users between sites) needs rows in this shape.
Serialized PHP is forever
option_value and the _meta value columns store PHP's serialize() output: a:1:{i:0;i:5;} for sticky_posts, similarly for active_plugins, role objects, and a long tail of plugin data. Search-replace tools (including wp search-replace) walk those blobs without* a full unserialize, because a naive byte replace breaks the length prefixes.
A from-scratch application has to:
- Read the blobs it cares about with a tolerant parser, not
unserialize()of untrusted data. - Leave blobs it does not understand untouched.
- When it writes an array that WordPress would serialize, write the same format, so a later dump still loads in WordPress after an eject.
This is the one encoding in the adapter that is PHP-shaped even if the application is not PHP. The dump is the portable unit. The dump talks this encoding.
HTTP probes
Monitors and panels request a short list of URLs that are not "a page on the site." Status codes matter more than bodies. A 404 on these paths is how you fail the Turing test.
| URL | Expected | Why |
|---|---|---|
GET / | 200 | The site is up. |
GET /wp-login.php | 200 (form) or 302 (to your own sign-in) | Monitors, wp user login links, scripted sign-in. A 404 is fatal. |
GET /wp-admin/ and anything under it | 302 (login, or your admin) | Same. The directory must exist on disk so nginx does not 403 it first. |
GET /wp-cron.php | 200, empty body | Hosts and plugins poke this. Honour DISABLE_WP_CRON if you do work here. |
GET /xmlrpc.php | 405, Allow: POST | A 404 is a tell. POST can refuse (many hosts disable XML-RPC themselves). |
GET /robots.txt | 200, four lines including the sitemap URL; or Disallow: / when the site is not public | Crawlers. Cheap to match. |
GET /wp-json/ or GET /?rest_route=/ | 200, JSON index | The REST discovery document. |
Link: <{home}/wp-json/>; rel="https://api.w.org/" | Response header on HTML pages | Discovery without fetching the index. |
GET /feed/ | 200, RSS 2.0 | Optional for a host, expected by feed readers. <generator> carries the recorded core version. |
GET /wp-sitemap.xml | 200 | Optional for a host. |
GET /favicon.ico | 302 to the site icon, or 404 | Optional. |
Pretty permalinks (/hello-world/) are a public-site concern. Hosts with nginx/Caddy already send unknown paths to index.php. .htaccess rewrite rules are for Apache; keep them if you find them, do not require them.
REST, as a host sees it
The adapter does not need the full wp/v2 surface for a host to be happy. It needs:
- The index at
/wp-json/(name,description,url,home,namespaces,routes). - Error objects of the form
{ "code": "rest_no_route", "message": "…", "data": { "status": 404 } }. GET /wp-json/wp/v2/settingsgated onmanage_options, if anything in the fleet reads site title through REST.- Cookie plus
X-WP-Nonceauthentication, if anything in the fleet logs in through REST.
The rest of wp/v2 (posts, media, users, comments) is how an admin application speaks to the site. Minn Admin needs it. A host's backup job does not.
WP-CLI
WP-CLI is MIT. The wp binary runs unchanged. Detection is:
- Find a webroot (working directory, or
index.phpin a parent). file_exists( wp-includes/version.php ). If missing:Error: This does not seem to be a WordPress installation.- Phase
before_wp_load: commands that opted in. This is where a from-scratch app registers verbs, viawp-cli.ymlrequire:. - Phase
after_wp_config_load:wp dbandwp config, using the parsed config. Needsversion.phppluswp-config.php. Does not need your application. - Phase
after_wp_load: everything else. On WordPress this loads the full runtime. On a from-scratch app that does not ship that runtime, refuse cleanly rather than fatal.
Verbs fleet tooling actually runs
CaptainCore, Disembark, and typical host panels have been observed to call:
| Verb | Why |
|---|---|
wp core version | Inventory. Parsed from version.php. |
wp option get home / option get siteurl | Public URL. --skip-plugins --skip-themes and --url= are common. |
wp option update / option set | Launch and migrate scripts. |
wp plugin list --format=json --fields=name,title,status,version | Inventory. |
wp theme list | Inventory. |
wp plugin install / theme install / activate / deactivate | Updates and provisioning. |
wp user list / user get / user create | Provisioning. |
wp user login <user> | Magic-link sign-in (CaptainCore helper contract: a short token on wp-login.php). |
wp search-replace <old> <new> --all-tables --report-changed-only | Domain changes on launch and migrate. Must walk serialized PHP arrays without breaking prefixes. Skip guid. |
wp db export / db query / db check | Backups and health. WP-CLI's own, after config load. |
wp cache flush | After writes. "Success: The cache was flushed." even when there is no object cache, so scripts do not abort. |
wp rewrite flush | After permalink changes. |
wp maintenance-mode (activate / deactivate) | Bookends updates. Implemented as {ABSPATH}/.maintenance. |
Stdout wording is part of the contract. Callers match Success: and Error: lines. Exit codes too: missing option is exit 1, missing plugin on is-installed is exit 1 with no output.
You do not need wp post list for a host. You do not need wp core update until you have a release of your own; that command becomes your updater, not a rewrite of WordPress's. wp core verify-checksums will fail against wordpress.org, because you do not ship those files. That failure is the truthful signal. Do not fake the hashes.
Shared auth
A session is shared between the browser, the application, and whatever WordPress tooling still touches the site. Hosts that open the admin, magic-link logins, and REST clients all depend on the same cookies.
Cookie name: wordpress_logged_in_ plus md5(siteurl). Over HTTPS an additional wordpress_sec_{hash} cookie is set on /wp-admin and /wp-content/plugins; over HTTP it is wordpress_{hash}. The logged-in cookie is the one REST reads, path /.
Cookie value: username|expiration|token|hmac.
The HMAC key is derived from a four-character fragment of user_pass. Observed rule: for a $wp$-prefixed hash, the fragment is the last four characters; for legacy phpass $P$, it is substr(hash, 8, 4). Getting this wrong produces a cookie that parses, finds the user, finds a live session, and still fails the HMAC, which looks like a salt mismatch.
The token's sha256 must be a live key in the user's session_tokens usermeta.
REST nonce (wp_rest action): substr(HMAC-md5(tick|wp_rest|uid|token, NONCE_KEY . NONCE_SALT), -12, 10), with tick = ceil(time / 43200). Current and previous tick both accepted.
Status split, observed: cookie failure is 401 rest_not_logged_in; nonce failure is 403 rest_cookie_invalid_nonce. Clients use that split to decide between re-authenticating and refetching a nonce.
None of this requires WordPress source. It is a keyed-hash protocol over values in wp-config.php and wp_usermeta. Any language with HMAC-MD5, HMAC-SHA256, and bcrypt can implement it. Cross-acceptance (a cookie minted by the application authenticates against WordPress, and the other way around) is the proof the protocol was matched.
The portable unit
Backup, migrate, staging, and "download a copy of my site" all assume the same bundle:
- The files under the webroot, especially
wp-content/andwp-config.php. - A SQL dump of every table with the site's prefix.
Restore is: put the files on a PHP host, import the dump, wp search-replace old-host new-host --all-tables, and visit /wp-login.php. A from-scratch application that can ingest that bundle and emit that bundle is operated the same way a WordPress site is, even if not one line of WordPress software runs.
Disembark's phar does not even read the shape files by name. It works from the database credentials it is given. UpdraftPlus reads wp-config.php and dumps wp-content/ plus the database. CaptainCore's file hashes (component-hashes, quicksave-fingerprint) are bash over the filesystem.
The adapter's job on this layer is: do not invent a new layout, do not invent a new schema, do not invent a new encoding for options.
What is not required
A host that can detect, back up, migrate, monitor, and run WP-CLI against the site does not need:
- The WordPress admin screens, or any file under
wp-admin/exceptindex.php(and empty placeholders if plugin PHP willrequirethem). - The block editor JavaScript packages, TinyMCE, the Customizer, Dashicons.
- The PHP function surface (
add_action,WP_Query, …). That is how a plugin sees WordPress, not how a host sees it. - XML-RPC methods. Many hosts disable them.
- Multisite tables (
blogs,site,sitemeta,blogmeta,signups). readme.htmlmatching wordpress.org.- Byte-identical theme rendering.
- Core checksums against wordpress.org.
Those absences are how you tell a from-scratch application from a fork. A fork has the files because it is the files. A compatible application has the handful of paths the host reads, and is honest about the rest.
What gives you away, if you care
| Signal | Who looks | Honest response |
|---|---|---|
wp core verify-checksums fails | Host integrity scans, CaptainCore | Correct: you do not ship WordPress. Do not impersonate the hashes. |
Missing wp-includes/functions.php as a real implementation | Plugin PHP, some mu-plugins | Empty placeholder if you load plugin PHP; omit if you do not. |
GET /wp-admin/ is 404 | Monitors, panels | Always 302. |
GET /wp-login.php is 404 | Monitors, magic links | Always exist as a file, then 200 or 302. |
wp-includes/version.php missing | WP-CLI, panels | Always exist, with parseable assignments. |
Rewritten wp-config.php | Backup tools | Never. |
| Extra tables dropped on migrate | Any dump/restore | Never. |
| Serialized PHP rewritten as JSON | Search-replace, eject back to WordPress | Never, for stored options and meta. |
Being operated like a WordPress site is not the same as hiding that you are something else. The truthful tells (checksums, wp minn version, a generator string that names your engine) can stay. The operational tells (404s, missing version.php, a rewritten config) cannot.
Another language
The contracts above are file names, SQL, HTTP, and CLI stdout. None of them require the application to be PHP. Two constraints still bind.
The host runs PHP. On a platform that hosts WordPress sites, nginx sends *.php to PHP-FPM. A Go binary sitting at the webroot will not be executed. The compatible pattern is thin PHP at the well-known paths (index.php, wp-settings.php, wp-login.php, wp-admin/index.php, wp-includes/version.php) that boots, or proxies to, your runtime. Those stubs are original work: define a constant, require a config, hand off. They are not a fork.
WP-CLI is a PHP phar. Verbs you need either register at before_wp_load through wp-cli.yml, or they are WP-CLI's own (config, db) and only need the config file. You do not have to load a WordPress runtime to export a database.
The database is MySQL. The option encoding is serialized PHP. The auth scheme is HMAC plus bcrypt. HTTP is HTTP. A Rust service that speaks those, with five PHP stubs at the webroot, is operated by Kinsta the way Kinsta operates a WordPress site. A Rust service with none of the stubs is a different kind of host.
That is the whole license argument from the other direction. The stubs, the table names, the route paths, and the CLI verbs are the interface. Interfaces are not copyrightable expression. The thousand PHP files under wp-includes/ are expression. A from-scratch application implements the first and does not copy the second. A fork copies the second and inherits the license. The reason WordPress feels like it can only be forked is that the interface and the expression are shipped in the same pile of files, and nobody wrote the interface down.
This page is that interface.
How this was written
Captured from a running WordPress 7.1 (the "oracle") and from the tools that operate real sites: WP-CLI, CaptainCore, Disembark, Kinsta's file-on-disk behaviour, UpdraftPlus-style backup. Observed behaviour, not WordPress source. Comparing outputs is always legal. Copying wp-includes/functions.php is how you accidentally fork.
Minn Engine is one implementation of these contracts, in PHP, MIT licensed, with fixture suites that pin the shapes. The contracts live next to that engine. They are not the engine. If this page is useful, it is because someone can follow it in a language Minn does not speak.