Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/iniznet/kumki/archive/refs/heads/main.zipKumki — a mahout starter plugin
The durable half of a WordPress site, as a starter. Content types, field groups, custom tables, admin screens, REST routes and the migrations that build the schema — installed once, and still there after a theme switch.
Stack: WordPress 7.1+ · PHP 8.4+ · Composer · PHPUnit · PHPStan max · Psalm (taint) · Rector · PHP-CS-Fixer · Vite (build only)
Generated by mahout-scaffold (mahout new <slug> --host=plugin). AGENTS.md is the
working contract; it is the complete public one, and every rule in it is enforced by a
gate in this repository.
Install
composer install # the family resolves over VCS; no Packagist account needed
npm install && npm run build
Then activate the plugin. Activation runs the migrations that create the field tables,
which is the plugin's equivalent of the theme's after_switch_theme path — a plugin has
an activation hook, and mahout-db attaches its run to whichever one the host owns.
Before any gate will run, copy the test configuration:
cp tests/wp-tests-config.php.dist tests/wp-tests-config.php
What lives here
| Path | Owns |
|---|---|
app/Bootstrap.php |
the composition root: every provider, named once, and the claim on the process |
app/Providers/ |
hook attachment, assets, content registration, the field seam, the admin seam |
app/Features/<Name>/ |
one vertical slice: schema, module, repository, mapper, data |
app/Components/ |
typed props to markup — a block's render target, nothing else |
config/content-types.php |
the post types, taxonomies and REST routes this plugin registers |
config/fields.php |
the field groups and option screens, with a StorageTarget per field |
config/entries.php |
the Vite entry list |
uninstall.php |
the deletion policy, which is deliberately not a guess |
The worked example
Offices is the feature this starter exists to demonstrate. Read it in this order,
because each file answers a different question a new host asks.
| File | The question it answers |
|---|---|
config/content-types.php |
why a plugin rather than a theme: a post type declared here survives switching the theme |
config/fields.php |
when a field is Meta and when it is Table — the same group carries both, plus a queried repeater, and the file says why each one is where it is |
app/Features/Offices/OfficesRepository.php |
the read path: FieldReader::prime() filed before mapping, the two-phase filtered read, and the aggregate that saturates at its ceiling |
app/Features/Offices/OfficeMapper.php + OfficeData.php |
where a WP_Post stops existing and a typed value starts |
app/Features/Offices/OfficesModule.php |
how a feature declares its collaborators by contract id and never reaches for a container |
app/Providers/EditorProvider.php |
the field seam: panels and screens read from config, bound under the package's contracts, groups registered on the package's own hook |
tests/Integration/OfficesTest.php |
the claims above, executed — including the boundary a declaration moves: a repeater that states its maximum is primed with the rest of the page, and one that states nothing pays per object |
Delete the two config entries and the feature stops existing: nothing else in the
starter refers to it except the module line in app/Bootstrap.php. That is the test of
whether the example is a worked example or a fixture.
Gates
composer check # format, stan, psalm taint, arch, rector, test, hooks, i18n, config, doctor
composer check must pass before every commit. composer doctor asserts the
installation around this plugin as well as its own assembly, and it fails when a second
host on the same site also carries the packages: one process has one composition root
(mahout-kernel ADR-0007), so a theme that installs the family and a plugin that installs
the family are alternative assemblies, not two layers of one site.
One finding is a date rather than a defect. config:check reports that the family is
locked to a path repository, and it is right to: the lock this working copy resolves is
produced by composer.dev.json, whose repositories are directories on this machine, and
a committed lock must resolve over a committed repository. The lock cannot be produced
until the packages carry their first 1.0 tags, because the manifests require ^1.0
over VCS and no such ref exists yet. So: develop with COMPOSER=composer.dev.json, never
commit the lock that resolves, and expect this one line to go away at the first tag —
which is also the moment the generated starters become installable.
Choosing between this and the theme starter
The theme owns presentation: the template hierarchy, theme.json, design tokens,
navigation, and the Surfaces a request resolves to. This plugin owns what must survive
that being swapped. A site installs the packages in exactly one of them.