Search by

droost / engine

chadmandoo

Framework-free engine behind Droost: QA verify loop, AI-harness installers, skill emitters, scaffold blueprints, OKF wiki core, and code search/graph core for Drupal projects.

0.7.6 2026-09-23 17:36 UTC

This package is auto-updated.

Last update: 2026-09-23 17:44:41 UTC


README

The framework-free engine behind Droost.

Droost is a developer-acceleration toolkit for AI coding agents working on Drupal. Most of what it does is not actually Drupal-specific: running a QA verify loop, writing AI-harness config files, generating scaffolds, composing wiki pages, indexing code. This package is that half — plain PHP with no Drupal dependency — so the same logic can serve the Drupal module, a standalone CLI, and repo-only tooling that has no installed site to boot.

Areas (each depends only on Support and Site, never on a sibling):

Area What it holds Landed
Support Shared primitives: project-root discovery, path guards, secret redaction, git HEAD, clock, state store partly
Site What the engine may know about the site it runs against: the extension-locator port and the no-site implementation yes
Verify The QA verify loop (lint, static analysis, tests) and its leg results yes
Skills Skill emitters: droost's own skill files rendered as SKILL.md yes
Harness Installers that write AI-harness files (AGENTS.md, SKILL.md, and friends) yes
Scaffold Blueprint registry and the framework-free code blueprints partly — see below
Wiki OKF wiki core: frontmatter, provenance, page composition, bundle reading yes
Search Code search and graph core: chunkers, extractors, index diffing partly — stores and indexer still to come

Why Scaffold is a partial lift

The scaffold area splits along a line that is not negotiable. Droost composes Drush's ~70 code generators rather than reinventing them, and that runner needs the Drush runtime container — Drush::hasContainer(), and Drush's own ApplicationFactory. Seven blueprints (service, content-entity, config-entity, event-subscriber, block, form, plugin) are built on it, and an eighth reflects over DrushCommands to compute its reserved method list. None of those can live in a package that does not depend on Drush, so they stay in the module.

What lives here is the spine — BlueprintInterface, AbstractBlueprint, BlueprintRegistry, ScaffoldContext, ScaffoldResult — plus the eight blueprints that emit from templates rather than delegating: access-handler, config-schema, hook, kernel-test, mcp-tool, route-subscriber, sdc, views-handler. The registry accepts both halves, so a consumer sees one set.

That the two halves are wired together at the container rather than merged is the point: the contract is framework-free even where an implementation is not.

The Site port, and why it is three-valued

Site\ExtensionLocatorInterface is how everything else asks what the project has. Its isInstalled() returns true, false, or null — because the engine runs both inside a booted site, where absence is a fact, and against a bare checkout, where it is not knowable.

That distinction is load-bearing rather than fussy: letting null collapse into false makes a plain checkout report as missing what is merely unknowable, and a missing answer looks exactly like a negative one. Site\UnknownSite answers null to everything, and callers are contracted to read that as "cannot tell", never as "not installed". Since 0.7.0 the engine itself reads only coreVersion() from the port; the guideline catalogue that pruned on isInstalled() is gone.

Status

0.x — extraction in progress. Code is being lifted out of the droost module area by area (phases B1 through B5 of the extraction plan); each tranche lands behind a tagged release before the module switches over to it. Treat the API as unstable until 1.0: breaking changes bump the minor, and consumers should pin per minor (~0.1.0).

0.1.1 landed the pilot tranche — Support\ProjectRoot, Support\PathGuard, Support\SecretRedactor, Support\GitHead, Verify\VerifyRunner, Verify\LegResult — chosen because it has zero coupling to Drupal and therefore proved the packaging pipeline on the smallest possible surface.

0.2.0 adds the guidance tranche (B2): Site\ExtensionLocatorInterface and Site\UnknownSite, Guidelines\GuidelineProvider, Skills\Skill, Skills\SkillProvider, Skills\SkillMdWriter, and the whole Harness area — fifteen classes that write AGENTS.md, SKILL.md, and the per-harness config files for Claude, Codex, Gemini, opencode and Qwen. The minor bumps because consumers pinned to ~0.1.0 must opt in; nothing in 0.1.1 changed.

0.3.0 adds the scaffold spine and the eight template-only blueprints (B3), for the reasons in "Why Scaffold is a partial lift" above.

0.4.0 adds the OKF wiki core (B4) — frontmatter parsing, provenance, page composition, bundle reading — plus Support\Yaml, which reproduces Drupal's YAML settings exactly (Dumper(2), inline level PHP_INT_MAX, the same flag pairs) so a regenerated page is byte-identical to the last one. That was verified against core's own serializer on a booted site, not assumed; Wiki\BundleLocatorInterface is the one port it needs, and the module's existing WikiSettings already satisfies it unchanged.

0.5.0 adds the pure search core (B5a): the chunkers, Graph\GraphVisitor and Graph\YamlGraphExtractor, index diffing, and the embedding and vector-store interfaces. The storage layer stays in the module for now — four classes built on Drupal's database API, which need a port of their own before they can move (B5b).

0.6.x lands B5b: the three storage ports (file manifest, code graph, vector store), each with a SQLite implementation. They are what bin/droost-index runs on, so it can index a repository and answer code-graph questions with no Drupal site at all. The Drupal indexer stays in the module, because it reads site configuration a bare checkout does not have. The line also added:

  • the recipe, migrate, plugin-deriver, media-source, ckeditor5-plugin and functional-test blueprints;
  • metadata filters on the vector-store port, applied before the k-limit;
  • slash-command materialization for the Claude installer (CommandProvider, with nested command names).

It also let a guideline topic name the module it describes, which 0.7.0 took out again with the rest.

0.7.0 removes the guidance tranche. Guidelines\GuidelineProvider is gone — its one non-guidance job, deriveMajor(), is now Support\CoreVersion::major() — and Skills\SkillProvider lists only the consumer's own skill files instead of rendering every guideline topic as a drupal-* skill. The harness writes the AGENTS.md block (Harness\DroostBlock, what droost is) and every editor's pointer to it unconditionally: InstallContext loses guidelinesMode, because those files are also how a build pipeline's own AGENTS.md block reaches the agent, and a switch that could cut that road silently is not one worth keeping. The Claude installer now sweeps retired droost skills — only directories it provably wrote and nobody has touched since — and its sentinel records the SKILL.md hash so later sweeps can keep an edited skill. Breaking, hence the minor.

0.7.2 records only unconditional declarations as code-graph symbols. A class or function declared inside an if, a loop or a function body exists only once that code has run, and is usually a fallback for the real one: project_browser's fixture script declares class Drupal when !class_exists('Drupal'). With core unindexed, that stand-in owned every \Drupal:: call in a codebase, so the wiki put project_browser in scope for every custom module that calls \Drupal. PhpGraphExtractor now runs ParentConnectingVisitor, which GraphVisitor expects.

0.7.3 narrows that rule to what it was for. Only a declaration guarded by its own absence, if (!class_exists('X')) { class X … } (or the interface, trait, enum or function form), is skipped. 0.7.2 skipped every conditional declaration, and a full rebuild of a real site showed the cost: webform's WebformManagedFileBase, declared in both branches of a feature check, and a theme's preprocess hook, declared only while a module is on, dropped out of the graph although each is the only declaration of its name. RepoIndexer::INDEXER_VERSION is 2, so a repository index rebuilds itself.

0.7.4 fixes two blueprints. The CKEditor 5 plugin's declaration no longer names a library or admin_library the blueprint never writes, and the MCP tool blueprint implements the doExecute() hook its base class calls rather than overriding the final execute().

0.7.5 adds Site\ScopeClassifier and Site\Provenance: one answer to who owns an extension's code, where droost had five that disagreed. Composer's runtime record decides first (drupal-core is core, drupal-custom-* custom, any other drupal-* package contrib, the deepest install directory winning), then the first path segment named contrib or custom, and code placed by hand is the project's own. A package installed at or above the docroot owns nothing, so a module's own CI checkout does not make every extension contrib. Paths are compared as written, never resolved, so a package composer linked in from elsewhere is still found at its link.

0.7.6 is the wiki at scale:

  • Wiki\PageRenderer writes a page body from a factsheet alone, with no model. The same factsheet renders the same bytes, and a section nobody could measure says why.
  • GenerationRow::unchanged() reports a regeneration that would write what a page already says.
  • PageComposer::compose() records which writer made the body (droost:wiki:generate, droost:wiki:write or droost:wiki:render) and types a theme's page as a Drupal Theme.

It also has two fixes:

  • A hook an api.php documents is a hook:NAME symbol owned by its module, so an implementation's edge reaches a module rather than nothing. RepoIndexer::INDEXER_VERSION is 3.
  • The kernel-test and functional-test blueprints keep a class name that already ends in Test.

The rest of Support (clock, state store) arrives with the areas that need it.

Install

composer require droost/engine

Requires PHP 8.3 or newer. ext-pdo_sqlite is suggested — it backs the SQLite code-graph and vector stores used for repo-only (no site) operation.

Development

composer install
vendor/bin/phpcs
vendor/bin/phpstan analyse
vendor/bin/phpunit

Coding standards are Drupal + DrupalPractice (phpcs.xml.dist), matching the module so lifted files diff to near-zero. PHPStan runs at level max with an empty baseline. CI runs all four on PHP 8.3 and 8.4.

License

GPL-2.0-or-later. See LICENSE.