
Akashi
Probatio Verborum Viventium『証』〜AKASHI〜
Akashi turns PHP examples in Markdown and PHPDoc into tests. A project discovers its examples once, executes them through PHPUnit, and can reuse a selected part of the same corpus for PHPStan verification or named-example extraction. In-process execution is the normal runtime path; examples that need process isolation can opt into a child process.
See It Work
Write ordinary PHP in a Markdown fence:
$result = strtoupper('akashi');
assert($result === 'AKASHI');
Connect the containing document through Akashi’s PHPUnit trait, then run vendor/bin/phpunit. Akashi makes the native
assertion unconditional, isolates the example’s variables and declarations, and reports a failure against this
documentation location. This page and the root README are verified through that same path in Akashi’s own test suite.
Follow the Quick Start for the complete working test class.
One Corpus, Several Uses
README.md / docs/ / src PHPDoc
│
▼
Akashi examples
┌──────┼───────────┐
▼ ▼ ▼
PHPUnit PHPStan extraction
runtime analysis / consumers
PHPUnit is the usual runtime integration. PHPStan support is optional and project-configured: it lets a rule test analyze documentation examples independently of executing them. Extraction is a separate CLI workflow for cases where a stable named example must also become a consumer fixture.
Choose Your Next Step
- Quick Start: install Akashi and run the first documentation test.
- Authoring Examples: choose documents, write fences, and understand labels.
- PHPUnit: configure runtime execution and PHPUnit reporting.
- PHPStan: reuse documentation as static-analysis fixtures.
- Extracting Named Examples: emit one stable example for another consumer.
- Compatibility and Safety: supported versions, limitations, and trust boundaries.
Project Status
The Markdown and PHPDoc workflows, in-process and separate-process execution, PHPUnit integration, PHPStan verification, marked extraction, check/write synchronization, and optional PHP-CS-Fixer checks for inline examples are implemented, and both recorded consumer migrations are complete. Akashi is pre-1.0, and its categorized public API is usable but may change between minor releases before 1.0. Deferred work is listed separately in the Roadmap; it is not required for the workflow shown above.
Quick Start
Above the unformed marsh, thunder wandered without echo until it entered a hollow bone. The bone answered, and reeds lifted from the mud to hear. Thereafter every creature carried an emptiness by which the world might speak. Guard the hollow within thee; abundance is not its only purpose.
— Ordinances of the Synthetic Dawn 18:2
This tutorial takes one Markdown example from source text to a named PHPUnit test. In-process execution is the default; you do not need to configure an execution backend for this path.
1. Install Akashi and PHPUnit
Akashi requires PHP 8.1 or later. Install it with a compatible PHPUnit release:
composer require --dev "jbboehr/akashi:^0.2" "phpunit/phpunit:^10.5 || ^11.5"
Akashi supports PHPUnit 10.5 and 11.5. Composer selects PHPUnit 10.5 on PHP 8.1 and the newest compatible release on later PHP versions; this tutorial works with either line.
2. Write an Example
Add a PHP fence to README.md:
$result = strtoupper('akashi');
assert($result === 'AKASHI');
An opening <?php tag is optional. Every fence whose first info-string word is php, compared case-insensitively,
enters the selected corpus.
3. Connect the Document to PHPUnit
Create tests/DocumentationExamplesTest.php:
<?php
use jbboehr\Akashi\ExampleCorpus;
use jbboehr\Akashi\Integration\PhpUnit\VerifiesPhpUnitExamples;
use jbboehr\Akashi\Source\DocumentationSource;
use PHPUnit\Framework\TestCase;
final class DocumentationExamplesTest extends TestCase
{
use VerifiesPhpUnitExamples;
protected static function akashiExampleCorpus(): ExampleCorpus
{
return DocumentationSource::forProject(dirname(__DIR__))
->includeFile('README.md')
->load();
}
}
For a test class directly inside tests/, dirname(__DIR__) resolves the project root independently of PHPUnit’s
working directory. The trait supplies the PHPUnit data provider and test method. Your test class supplies the corpus.
4. Run It
vendor/bin/phpunit
Akashi discovers the fence and gives it a deterministic, readable data-set label. The trait delegates to
PhpUnitRuntime, which transforms and executes the example in-process. The PHPUnit process’s existing Composer
autoloader remains available to the example.
5. Break It Deliberately
Change the expected value to Akashi and run PHPUnit again. The test fails with the example ID, label, and originating
README.md line. Restore AKASHI to make it pass.
Akashi rewrites supported native assert() calls to PHPUnit assertions, so the check still runs when the host has
zend.assertions=-1. Details and edge cases are in PHPUnit.
Where Next?
- Authoring Examples covers directories, exclusions, fence rules, and labels.
- PHPUnit covers runtime configuration and result reporting.
- PHPStan adds optional static-analysis verification of the same corpus.
- Separate-Process Execution handles examples that cannot run safely in-process.
- Extracting Named Examples turns a marked fence into a stable consumer fixture.
- Compatibility and Safety records supported versions and exact limitations.
The documentation example in this tutorial executes through Akashi. The PHPUnit integration snippet receives
compile-only validation because its __DIR__ is meaningful after copying it into the project’s tests/ directory.
Using Akashi
Before the oceans knew motion, they lay heavy and still beneath a copper sky. A flock of black swans beat their wings across the surface, raising the first waves and teaching depth to travel without departure. Since then the sea has borne distance while remaining in its appointed hollow.
— Ordinances of the Synthetic Dawn 13:44
Akashi separates discovering documentation examples from deciding what to do with them. Build one ExampleCorpus, then
hand it to the integration needed by the project:
- PHPUnit executes examples as named data sets.
- PHPStan analyzes a selected subcorpus and checks expected diagnostics.
- Extracting Named Examples emits one author-marked fence as a consumer fixture.
Most projects begin with the in-process PHPUnit path from the Quick Start. Add separate-process execution only to examples that require it, and add PHPStan only when the project has a rule or analysis behavior worth demonstrating.
Authoring Examples describes the shared Markdown/PHPDoc corpus used by all three workflows.
Authoring Examples
Receive the stranger who beareth one seed as gladly as the caravan bearing a thousand jars; harvest judgeth the gift by what awakeneth, not by the noise of its arrival.
— Ordinances of the Synthetic Dawn 30:27
Akashi discovers Markdown documents and PHP source files, extracts PHP fenced blocks and PHPDoc references to canonical PHP files, and preserves their maintained source locations. Corpus selection controls which documentation files participate; example metadata adds identity and runtime behavior but does not make unmarked PHP fences disappear.
Build a Corpus
Create a source configuration from an absolute project root, then add project-relative files and directories:
<?php
use jbboehr\Akashi\Source\DocumentationSource;
$corpus = DocumentationSource::forProject(dirname(__DIR__))
->includeFile('README.md')
->includeDirectory('docs')
->includeDirectory('src')
->exclude('docs/archive')
->load();
Each configuration method returns a new immutable value. Scalar path syntax is checked immediately; filesystem paths,
readability, and document identity are checked by load(). DocumentationSource selects case-sensitive .md and
.php files and dispatches each format to its corresponding extractor.
includeFiles() accepts an array, generator, or iterator of project-relative strings, ProjectPath values, or
SplFileInfo objects. A Symfony Finder configured with files() can therefore be passed directly without adding
Symfony Finder as an Akashi dependency. Directory includes remain available for the zero-dependency common case.
Exclusions match an exact file or a complete directory subtree. See Configuration for
ordering, symlink, duplicate-document, and failure behavior. MarkdownSource remains available when a project wants an
explicitly Markdown-only manifest.
Choose documents whose PHP fences are meant to participate in at least one configured workflow. Use compile-only for
valid PHP that PHPUnit should parse without executing, and runtime skip when PHPUnit should report a skipped data set.
Akashi has no global ignore directive; use another fence language for fragments that should not enter the corpus, or
exclude their containing document.
Write PHP Fences
Akashi selects a fenced block when the first word of its info string is php, compared case-insensitively. An opening
PHP tag is optional:
```php
$message = sprintf('Hello, %s!', 'Akashi');
assert($message === 'Hello, Akashi!');
```
Backtick and tilde fences, longer fences, indentation, block quotes, and other CommonMark structure are handled by the CommonMark parser. Additional info-string words are retained as metadata but do not currently change Akashi behavior.
Every inline example retains the original code, document, fence metadata, line and byte spans, and its ordinal in the document. Generated inline IDs combine a hash of the project-relative path with that ordinal. Moving the document or inserting an earlier PHP fence therefore changes the generated ID. Referenced examples instead derive stable IDs from their canonical project-relative path and optional region name.
The exact generated form is example-{first 12 hexadecimal characters of sha1(project-relative path)}-{ordinal}, with
the ordinal padded to at least two digits. Use an explicit example ID when another tool needs an identity that survives
reordering.
Write PHPDoc Fences
Every php fence on the interior lines of a selected /** ... */ comment enters the corpus, whether or not the comment
is attached to a named declaration:
<?php
namespace Acme;
/**
* Return a stable display name.
*
* ```php
* $name = \Acme\Text::displayName('akashi');
*
* assert($name === 'AKASHI');
* ```
*/
final class Text
{
public static function displayName(string $name): string
{
return strtoupper($name);
}
}
Akashi removes conventional docblock indentation, the leading *, and one following space before CommonMark parsing.
The opening /** and closing */ lines are delimiters rather than Markdown content, so put fences and prose on the
interior lines. An opening <?php tag inside the fence remains optional.
The extracted code is prefix-free, while failures refer to the original .php path and PHPDoc line. Each docblock is
parsed independently, so metadata cannot associate with a fence in a later comment. Use another fence language for a PHP
fragment that should not enter any workflow.
Akashi extracts the fence, not its surrounding declaration. The example above therefore calls the project class through its fully qualified, Composer-autoloadable name. Supporting declarations must already be available through normal project bootstrap or autoloading, or be written inside the fence.
Reference Canonical PHP Examples
Use an inline PHPDoc fence for a short demonstration tied closely to one symbol. For a substantial or reused example, keep ordinary PHP as the source of truth and reference it from PHPDoc:
/**
* @akashi-example examples/conversion.php#basic-conversion
*/
The target is relative to the configured project root. It names either a whole case-sensitive .php file or one stable
named region. A canonical region file remains ordinary valid PHP:
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
// akashi-region: basic-conversion
$result = convert(1, 'meter', 'centimeter');
assert($result === 100);
// akashi-region-end: basic-conversion
Akashi executes and analyzes only the bytes between the named marker lines. The surrounding opening tag, bootstrap, and
other regions keep the complete file directly executable and friendly to IDEs, formatters, and static-analysis tools.
Whole-file references use the complete file instead. A named region must not rely on a surrounding require, use, or
other file-level setup being copied into the example; keep required source inside the region or provide project setup
through the normal runtime and PHPStan configuration.
Region markers must be standalone PHP line comments with matching lowercase kebab-case names. Missing, malformed,
orphaned, mismatched, nested, duplicate, and empty regions fail during load() instead of being guessed at. Stable
names are deliberately used instead of line-number ranges, which unrelated edits would shift.
The default reference tag is @akashi-example. To consume another public tag convention, replace the accepted set:
$source = DocumentationSource::forProject(dirname(__DIR__))
->includeDirectory('src')
->withPhpDocReferenceTags('example');
Pass more than one name to accept a migration overlap, for example
withPhpDocReferenceTags('akashi-example', 'example'). Akashi’s model remains independent of PHPDocumentor; configuring
example does not adopt PHPDocumentor’s line-range behavior or trailing-description syntax.
The canonical PHP file need not also be in the include manifest. It must resolve to a readable .php file inside the
same canonical project root. Multiple PHPDoc sites may reference the same whole file or region; Akashi creates one
example and retains every presentation site for tooling and future renderer integrations. Failures point to the
canonical PHP code line, while ReferencedExampleSource::$references preserves the referring PHPDoc locations.
Resolution is not recursive: PHPDoc inside a referenced file is scanned only when that file is also selected by the
source manifest.
External references are currently a PHPDoc authoring mode. Markdown continues to use physically embedded fences.
Synchronized Presentations
Some documentation renderers cannot include an external file directly. Akashi can inspect an inline presentation whose canonical source remains an ordinary PHP file or named region:
<!-- akashi-sync: examples/conversion.php#basic-conversion -->
```php
$result = convert(1, 'meter', 'centimeter');
assert($result === 100);
```
<!-- akashi-sync-end -->
The start comment, PHP fence, and end comment must remain consecutive Markdown blocks, with only optional blank lines
between them. This form is stable under formatters such as Prettier, which insert those blank lines. The fence must be
explicitly closed and labelled php. The target follows the same project-relative .php path and optional lowercase
named-region rules as a PHPDoc external reference. The same canonical target may be presented in several documentation
locations. Directive names are case-sensitive; a case variant that resembles an Akashi directive is rejected as
malformed rather than silently ignored.
SynchronizationChecker parses this form in Markdown and conventional multiline PHPDoc comments and returns typed
SynchronizationMismatch values without modifying the document. Its library-only rewrite seam returns a new Document
containing the canonical code without writing it to disk:
$checker = SynchronizationChecker::forProject($projectRoot);
$updated = $checker->rewrite($document);
$updated->contents; // Corrected document bytes.
The rewrite changes only recorded code spans. It retains directives, fences, prose, Markdown or PHPDoc container prefixes, and the local line-ending convention, and it rejects canonical code that would break the surrounding fence or PHPDoc structure. Rewriting the returned document again is a no-op.
To check explicit files in CI without writing them, run:
vendor/bin/akashi sync --check --project-root=. README.md docs/examples.md src/Example.php
The command is silent when every presentation is current. Stale presentations receive a source-labelled unified diff
from their embedded code to the canonical replacement. Stale or invalid presentations are reported on stderr and exit
with status 1. To apply the same validated replacements, select write mode explicitly:
vendor/bin/akashi sync --write --project-root=. README.md docs/examples.md src/Example.php
Akashi validates every selected document before the first write, rejects documents changed since loading, and atomically replaces each changed file through a temporary sibling. See the CLI reference for the exact path, stream, write-safety, diff, and exit-status contract.
SynchronizationRegion::$embeddedCode contains the logical, undecorated PHP seen by CommonMark. Its location and
regionSpan point into the original maintained document, so slicing those spans returns raw Markdown indentation or
PHPDoc leading * decoration. This distinction preserves both comparison-ready code and the exact authored bytes that a
future writer would need.
Comparison is intentionally narrow and deterministic:
- CRLF and CR line endings compare as LF;
- a missing final newline is treated as one final LF, while additional trailing blank lines remain significant;
- Markdown fence indentation and conventional PHPDoc indentation and leading
*decoration are containers, not code; - indentation inside the logical PHP fence remains significant; and
- PHP opening tags are canonical code and are not inserted, removed, or case-normalized. A synchronized whole-file presentation therefore includes its opening tag when the canonical file does.
Malformed, orphaned, nested, overlapping, or incomplete synchronization structures fail rather than being guessed at. Canonical named-region validation continues to reject missing, malformed, nested, mismatched, empty, or duplicate regions. Synchronization is independent of the optional formatter check described below.
Check or Write Inline Formatting
Ordinary external PHP files and named regions should be formatted directly by the project’s normal PHP tooling. PHP embedded in Markdown or PHPDoc is harder for those tools to reach, so Akashi can optionally present each inline example to a project-installed PHP-CS-Fixer:
vendor/bin/akashi format --check --project-root=. README.md docs/examples.md src/Example.php
The command defaults to vendor/bin/php-cs-fixer and PHP-CS-Fixer’s normal project-root configuration discovery. Pass
--php-cs-fixer=PATH or --config=PATH to select other project-relative files. PHP-CS-Fixer is optional and never runs
during discovery, PHPUnit, PHPStan, extraction, synchronization, or the normal Akashi library workflow.
Akashi checks only physically embedded Markdown and PHPDoc fences from the selected documentation files. It deliberately
skips referenced whole files and named regions because ordinary formatter commands already cover them. Each inline body
is placed in a private temporary PHP file, PHP-CS-Fixer runs through an explicit argument vector without constructing a
shell command or using a cache, and Akashi compares only the body after a protected boundary. File-level additions such
as a configured license header do not enter the fence. An authored opening <?php tag and its separator are preserved
outside the comparison; body line endings and final-newline changes remain significant.
Check mode never changes maintained documentation. A clean check is silent. A mismatch produces a source-labelled
unified diff on stderr and status 1. Formatter launch, timeout, invalid output, and cleanup failures identify the
maintained example rather than only the temporary file. Closing tags, inline HTML, and __halt_compiler() are rejected
by this initial adapter because they cannot be safely enclosed.
For applications that need to inspect a proposed update without writing it, FormattingRewriter::rewrite() can apply
the checked mismatches for one loaded document and return a new immutable Document. It changes only the inline code
spans and re-extracts the result before returning it; stale inputs, mismatches from another document, and output that
would damage a fence or PHPDoc comment are rejected.
To apply the same validated changes to the selected inline examples, use write mode:
vendor/bin/akashi format --write --project-root=. README.md docs/examples.md src/Example.php
Before writing, Akashi reloads the complete source and requires a second formatter pass to produce the same set of changes. It then rejects stale bytes and symbolic-link paths and atomically replaces each changed document from a same-directory temporary file. Write mode reports each updated project path on stderr; a current set is silent. External canonical PHP remains the responsibility of ordinary formatter commands. Hidden support code and renderer inclusion remain deferred.
Labels and PHPUnit Data Sets
The human-readable example label is derived from its source location and becomes the PHPUnit data-set name.
PhpUnitExampleDataSets::fromCorpus() rejects duplicate labels before yielding the first data set, which keeps PHPUnit
filters and reports unambiguous.
Use ordinary prose immediately around a fence to explain the example. Akashi does not require each example to carry a special name unless another consumer needs a durable identity.
Add a Stable Example ID
For consumer extraction, assign an example property in an Akashi metadata comment:
<!-- akashi: example=conversion-basic -->
```php
$result = convert(1, 'meter', 'centimeter');
```
Example IDs use lowercase kebab-case and must be unique across the corpus. Identity is optional metadata: load() still
returns every PHP fence. The same property may appear as // akashi: example=conversion-basic inside fenced or
referenced canonical PHP. Continue to Extracting Named Examples when a consumer needs one named
example.
Projects retaining an older marker comment such as <!-- yumemi-example: conversion-basic --> can add that dialect with
withMarkerName('yumemi-example'). Canonical akashi: metadata remains recognized alongside it.
Add a Runtime Directive
Akashi currently recognizes skip, compile-only, and separate-process. Place metadata immediately before the PHP
fence; adjacent comments and blank lines may be stacked together. Prose or an unrelated block breaks the association.
<!-- akashi: separate-process -->
```php
exit(0);
```
Unknown, duplicated, orphaned, or non-PHP metadata fails during extraction. See Example Metadata for grammar and precedence and Separate-Process Execution for backend configuration.
PHPUnit
The judge received the common stone without asking whether the quarry had named it white; he weighed it once beneath the lamp, and the court recorded the measure even when no accusation followed.
— Ordinances of the Synthetic Dawn 59:1
PHPUnit is Akashi’s normal runtime integration. VerifiesPhpUnitExamples exposes each documentation example as an
independently named test case, while PhpUnitRuntime selects the runtime backend and reports its result. The public
integration supports the PHPUnit 10.5 and 11.5 release lines.
Connect a Corpus
The Quick Start contains the smallest complete test class. Use the trait and return the corpus from
one protected hook. This example uses the project-owned DocumentationCorpus helper defined in
Test a README and docs/:
use jbboehr\Akashi\ExampleCorpus;
use jbboehr\Akashi\Integration\PhpUnit\VerifiesPhpUnitExamples;
use PHPUnit\Framework\TestCase;
final class DocumentationExamplesTest extends TestCase
{
use VerifiesPhpUnitExamples;
protected static function akashiExampleCorpus(): ExampleCorpus
{
return DocumentationCorpus::load();
}
}
The trait owns the provider and test method so every example retains its deterministic data-set label. The consuming
project remains responsible for source selection and can share the same ExampleCorpus with other integrations.
Configure Runtime Execution
Without an override, the trait selects the in-process defaults. Override its second hook when examples need an explicit project working directory, a bootstrap, or child-process execution:
<?php
use jbboehr\Akashi\ExampleCorpus;
use jbboehr\Akashi\Execution\RuntimeConfiguration;
use jbboehr\Akashi\Integration\PhpUnit\VerifiesPhpUnitExamples;
use PHPUnit\Framework\TestCase;
final class ConfiguredDocumentationExamplesTest extends TestCase
{
use VerifiesPhpUnitExamples;
protected static function akashiExampleCorpus(): ExampleCorpus
{
return DocumentationCorpus::load();
}
protected static function akashiRuntimeConfiguration(): RuntimeConfiguration
{
return RuntimeConfiguration::forProject(dirname(__DIR__))
->withBootstrap('vendor/autoload.php');
}
}
The runtime configuration is immutable. Its project root is canonicalized immediately, and its bootstrap must be a readable file that resolves inside that root.
Customize the PHPUnit Test
Projects that need a custom test name, additional data-set arguments, filtering, or per-example setup can use the lower-level adapter and facade directly:
<?php
use jbboehr\Akashi\Example;
use jbboehr\Akashi\Integration\PhpUnit\PhpUnitExampleDataSets;
use jbboehr\Akashi\Integration\PhpUnit\PhpUnitRuntime;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
final class CustomDocumentationExamplesTest extends TestCase
{
public static function examples(): iterable
{
yield from PhpUnitExampleDataSets::fromCorpus(DocumentationCorpus::load());
}
#[DataProvider('examples')]
public function testExample(Example $example): void
{
PhpUnitRuntime::assertExample($example);
}
}
This is the same path used by the trait; it does not change execution semantics.
What In-Process Execution Does
For the default path, Akashi:
- parses the example as PHP and retains its documentation line mapping;
- rejects syntax that cannot be isolated soundly in the hosting process;
- rewrites supported native assertions;
- places declarations in a generated namespace;
- evaluates the code with an empty local variable scope;
- captures output and restores the working directory, error-reporting level, and output-buffer depth;
- reports the result through PHPUnit.
The example can normally use the Composer autoloader already loaded by PHPUnit. An explicitly configured in-process
bootstrap is loaded with require_once, once per PHPUnit process. Use it for persistent setup such as autoloaders and
declarations. Akashi restores its reversible changes to the working directory, error-reporting level, and output-buffer
stack, so a bootstrap should not rely on those top-level changes remaining in effect for later examples.
Assertion Behavior
PHP may compile native assert() calls out when zend.assertions=-1. Documentation tests must not disappear with host
configuration, so Akashi rewrites calls that resolve to PHP’s native assert() into unconditional PHPUnit assertions.
Supported calls provide one assertion value and at most one string, Throwable, or null description. Positional and
valid named arguments are accepted:
assert($value > 0);
assert($value > 0, 'The value must be positive.');
assert(assertion: $value > 0, description: 'The value must be positive.');
Unsupported argument names, missing or duplicated assertion values, more than two arguments, argument unpacking, and
first-class-callable syntax are rejected with the documentation location. Non-native functions or methods named assert
are left alone.
The rewritten call always evaluates both its assertion and description. That differs from a native assertion compiled
out by PHP, so examples must not rely on either argument being a production no-op. A false assertion with a Throwable
description throws that object; a string becomes the PHPUnit failure message; otherwise Akashi reports the original
expression and source line.
Separate-process examples are not rewritten. The child PHP process enables native assertion exceptions explicitly.
Expected Exceptions
For an example whose intended result is a thrown exception, place expect-exception metadata immediately inside its PHP
fence:
// akashi: expect-exception=DomainException
// akashi: expect-exception-message="Invalid documentation input", expect-exception-code=73
throw new DomainException('Invalid documentation input.', 73);
The visible comment may appear anywhere and applies to the whole example. Prefer placing it immediately before the
operation expected to throw; Akashi does not attempt to infer control flow or enforce that order. An equivalent
<!-- akashi: expect-exception=DomainException --> comment may instead precede the fence when surrounding prose makes
the failure clear or extracted PHP should not contain Akashi metadata. Its optional message constraint uses a second
<!-- akashi: expect-exception-message="Invalid documentation input" --> comment, and its optional code constraint uses
<!-- akashi: expect-exception-code=73 -->. Adjacent HTML and inline properties are merged, but each property may occur
only once.
The type name is interpreted globally, and a subclass satisfies a parent-class or interface expectation. Akashi checks
the type in the selected runtime, so application exception classes may come from its configured bootstrap or Composer
autoloader; a separate-process type may exist only inside the child. A missing throwable, a different throwable type, or
cleanup failure fails the PHPUnit data set with the maintained documentation location. When present,
expect-exception-message requires a nonempty, case-sensitive substring in the actual message, matching PHPUnit’s
expectExceptionMessage() semantics. expect-exception-code accepts a signed base-10 integer in PHP’s integer range
and requires exact equality with the actual exception code. A runtime string code, such as a PDO SQLSTATE, remains
available for type and message matching but is reported as a mismatch when an integer code was expected.
The contract is not a general “any failure is success” mode. In particular, a child exit, signal, timeout, startup failure, or malformed exception report remains a process or infrastructure failure rather than an expected exception.
Exact Output
Use expect-output when stdout itself is part of the example’s contract:
// akashi: expect-output="Hello, Akashi!\n"
echo "Hello, Akashi!\n";
The quoted value uses JSON string escaping. Akashi compares the captured stdout bytes exactly: it does not trim output,
normalize line endings, or perform pattern matching. expect-output="" explicitly requires no stdout. Expected output
also works with expect-exception, covering bytes emitted before the matching throwable. Akashi checks execution and
exception semantics first so an output mismatch does not hide a more fundamental runtime failure. Stderr continues to
appear in failure diagnostics but cannot be asserted in this release.
Skips and Failures
An authored <!-- akashi: skip --> directive remains a named data set, but PHPUnit reports it as skipped before Akashi
configures, transforms, bootstraps, or executes the example. It does not remove the example from PHPStan or extraction.
An authored compile-only directive also remains a named data set. Akashi validates its PHP syntax against the running
host version and records one assertion without applying runtime transforms, loading a bootstrap, or executing the code.
This is useful for valid illustrative fragments that should remain available to PHPStan and extraction. It cannot be
combined with separate-process, an expected exception, or expected output; skip takes precedence when both
dispositions are present. Compile-only governs this PHPUnit path only. PHPStan verification requires selected files and
executes their top-level code, so exclude unsafe compile-only fragments from the PHPStan subcorpus.
Successful examples record one completion assertion even when they contain no native assertion. Failures report the example ID, label, maintained documentation location when available, failure phase, cause, captured stdout and stderr, and cleanup problems. The original exception remains in the exception chain.
In-process execution is trusted-code isolation, not a sandbox. Read Compatibility and Safety before running generated, third-party, or otherwise untrusted documentation.
PHPStan
Above the city of glass there appeared seven dim stars, each reflected in a different well. The priests drew no water until every reflection had been compared with its appointed star, and dawn found the vessels empty but the heavens rightly named.
— Revelation of the Artificial Sun 66:9
A documentation example can be executed at runtime and independently checked as a static-analysis fixture. PHPStan is an optional, first-class integration: projects that do not need it can use the documentation and PHPUnit workflow without installing or configuring PHPStan.
The consumer supplies its PHPStan rule and extension configuration. Akashi supplies corpus selection, expectation
parsing, temporary analysis files, diagnostic matching, source-line mapping, and PHPUnit reporting through PHPStan’s
RuleTestCase.
Express an Expected Diagnostic
Prefer a standalone identifier expectation immediately before the PHP statement that should produce the diagnostic:
// @akashi-phpstan-error argument.type
operationThatPHPStanShouldReject();
The identifier is matched exactly and case-sensitively. An optional nonempty text constraint can also match a case-sensitive substring in PHPStan’s message plus optional tip:
// @akashi-phpstan-error argument.type: incompatible unit
operationThatPHPStanShouldReject();
Repeated directives may describe several diagnostics for the same next statement. Blank lines may separate the directives from that statement, but other comments or code may not intervene. The reported diagnostic line must fall within the statement’s maintained source span. Malformed or misplaced identifier directives fail as authoring errors.
Akashi also retains the standalone message-only form for existing consumers:
//! argument has an incompatible unit
operationThatPHPStanShouldReject();
The //! text must be nonempty. It matches message and tip text across the selected example without constraining a
PHPStan identifier or statement line. A trailing marker on the same line as PHP code is not recognized.
For both forms, Akashi requires actual and expected diagnostic counts to match and assigns every expectation to a distinct diagnostic. A selected example with no expectations must analyze cleanly. Assignment considers the complete expectation/diagnostic set rather than committing to the first greedy match, so overlapping broad and narrow expectations remain deterministic.
Select Relevant Examples
Select with any project-owned predicate:
<?php
use jbboehr\Akashi\Example;
use jbboehr\Akashi\Integration\PHPStan\PhpStanExampleConfiguration;
$configuration = PhpStanExampleConfiguration::forProject(
$projectRoot,
static fn (Example $example): bool => str_contains($example->code->source, '@analyze-example'),
);
For a list of case-sensitive source tokens, use the convenience constructor:
<?php
use jbboehr\Akashi\Integration\PHPStan\PhpStanExampleConfiguration;
$configuration = PhpStanExampleConfiguration::forTokens(
$projectRoot,
'@akashi-phpstan-error',
'//!',
'@analyze-example',
);
Token names are project policy, not Akashi directives. Blank or duplicate tokens are rejected, and the selected subcorpus must not be empty.
Connect a RuleTestCase
Add VerifiesPhpStanExamples to the consumer’s RuleTestCase, build the same corpus used for runtime tests, and call:
$this->assertPhpStanExamples($corpus, $configuration);
The consumer still implements getRule() and, when needed, getAdditionalConfigFiles() in the normal PHPStan way. See
Reuse Examples for Runtime and PHPStan for a complete combined pattern and a clear
division between Akashi, PHPStan, and project-owned setup.
PHPStan 1.12
Akashi supports both PHPStan 1.12 and PHPStan 2.x. PHPStan 2.x is the normal development and recommended integration line. A PHPStan 1.12 project must explicitly select PHP-Parser 4 alongside Akashi:
composer require --dev "phpstan/phpstan:^1.12" "nikic/php-parser:^4.19.5"
PHPStan 1 embeds APIs built against PHP-Parser 4, while Akashi’s own parser integration supports PHP-Parser 4.19.5 and
5.x. The explicit pin prevents Composer from selecting PHP-Parser 5 for a PHPStan 1 process. PHPStan 2 projects need no
special parser pin during normal dependency resolution. A PHPStan 2 project that deliberately resolves with
--prefer-lowest should explicitly require nikic/php-parser:^5.8; otherwise Composer may select Parser 4 from
Akashi’s dual compatibility range even though PHPStan 2 expects Parser 5 APIs in the shared process.
Akashi preserves RuleTestCase semantics: the diagnostics under test come from the rule returned by the consumer’s
getRule(). Additional configuration can register extensions that participate in parsing, reflection, or type
inference, but it does not turn the test into a complete phpstan analyse run or automatically execute every configured
PHPStan rule. If an example expects a diagnostic, the consumer-provided rule must report it.
Verify an External PHPStan Run
For end-to-end consumer fixtures, a project can run the installed phpstan analyse --error-format=json command and
verify its output without loading PHPStan or PHPUnit classes. The consumer remains responsible for preparing the
disposable Composer project and installing the packages under test.
Keep the analyzed fixture as an ordinary PHP file. The selection token opts it into PHPStan verification, while each identifier directive records an expected diagnostic immediately before the relevant statement:
<?php
// @akashi-phpstan-example
// @akashi-phpstan-error method.notFound: Call to an undefined method Demo::missing()
(new Demo())->missing();
Reference that canonical file from a PHPDoc location included in the documentation corpus:
<?php
/**
* @akashi-example fixtures/demo.php
*/
final class DemoDocumentation
{
}
The planner selects the referenced external example, validates that its maintained bytes still match the loaded corpus, and produces the analysis paths and exact expectation map consumed by the command verifier:
<?php
use jbboehr\Akashi\Integration\PHPStan\PhpStanCommandNotCompleted;
use jbboehr\Akashi\Integration\PHPStan\PhpStanCommandOutputRejected;
use jbboehr\Akashi\Integration\PHPStan\PhpStanCommandVerified;
use jbboehr\Akashi\Integration\PHPStan\PhpStanCommandVerifier;
use jbboehr\Akashi\Integration\PHPStan\PhpStanExampleConfiguration;
use jbboehr\Akashi\Integration\PHPStan\PhpStanExternalFixturePlanner;
use jbboehr\Akashi\Source\DocumentationSource;
$canonicalProjectRoot = realpath($projectRoot);
if ($canonicalProjectRoot === false) {
throw new RuntimeException('The PHPStan project root is unavailable.');
}
$corpus = DocumentationSource::forProject($canonicalProjectRoot)
->includeFile('src/DemoDocumentation.php')
->load();
$fixtures = (new PhpStanExternalFixturePlanner())->plan(
$corpus,
PhpStanExampleConfiguration::forTokens(
$canonicalProjectRoot,
'@akashi-phpstan-example',
),
);
$outcome = (new PhpStanCommandVerifier())->verify(
projectRoot: $canonicalProjectRoot,
executable: PHP_BINARY,
arguments: [
$canonicalProjectRoot . '/vendor/bin/phpstan',
'analyse',
'--error-format=json',
'--no-progress',
'--',
...$fixtures->analysisPaths,
],
expectationsByFile: $fixtures->expectationsByFile,
timeoutSeconds: 60.0,
);
if ($outcome instanceof PhpStanCommandNotCompleted) {
throw new RuntimeException($outcome->commandResult->failureMessage ?? 'PHPStan did not complete normally.');
}
if ($outcome instanceof PhpStanCommandOutputRejected) {
throw new RuntimeException('PHPStan returned unsupported output.', 0, $outcome->cause);
}
if (!$outcome instanceof PhpStanCommandVerified) {
throw new LogicException('Unknown PHPStan command verification outcome.');
}
$verification = $outcome->verificationResult;
The plan analyzes each selected canonical PHP file once. Whole-file and named-region references to the same file are
grouped, and hard-link aliases are also grouped when the filesystem reports a stable device/inode identity. Duplicate
expectations from overlapping references are removed. Selection is intentionally limited to referenced external
examples: inline Markdown and PHPDoc examples still use the generated-source RuleTestCase path. Because PHPStan
analyzes the complete physical file, a diagnostic outside a selected named region is unexpected and causes a mismatch.
Callers that already own an expectation map may use PhpStanCommandVerifier directly without the planner. A project
using both verification paths should give them distinct selection tokens, or use a custom forProject() predicate that
selects only referenced sources. Keep the -- argument before the planned paths so a valid project filename beginning
with - cannot be parsed as a PHPStan option.
The three result variants distinguish a command that did not complete, completed command output that could not be
decoded, and a completed verification. PhpStanCommandVerified means that verification ran; inspect
verificationResult->isSuccessful() to determine whether diagnostics matched. A nonzero PHPStan exit status remains raw
command evidence and does not by itself fail a verification, because expected diagnostics commonly produce a nonzero
status.
PhpStanJsonResult keeps analyzer-wide errors separate from diagnostics associated with files. Each
AnalyzerDiagnostic retains its message, optional line, optional identifier and tip, and PHPStan’s ignorable flag.
The decoder accepts the documented PHPStan 1.12 and 2.x JSON shape, including PHPStan 1.12’s empty files list, ignores
unknown fields for forward compatibility, and rejects malformed or internally inconsistent results.
PhpStanCommandVerifier validates the complete expectation map before launching the process, then composes
PhpStanCommandRunner, PhpStanJsonDecoder, and PhpStanResultVerifier. The lower-level classes remain available when
a consumer needs to apply its own command-status or decoding policy. The command timeout defaults to 60 seconds and may
be replaced with another finite positive duration, as shown explicitly above.
PhpStanCommandRunner executes an explicit executable and argument list from an explicit project root. Akashi never
constructs a command string or interpolates caller values into one, so arguments do not undergo shell word splitting,
globbing, or command substitution. The example uses the current PHP binary to run the project-installed PHPStan proxy,
which is portable across the supported operating-system runners. The immutable result preserves the termination kind,
exit status, standard streams, elapsed time, and any applicable timeout, signal, or infrastructure-failure message. A
nonzero exit status is still a completed invocation because PHPStan may return diagnostics with that status; decoding
and later verification decide what the output means.
The runner canonicalizes the project root and executable with realpath() and inherits the caller’s environment. The
fixture planner derives exact expectation paths from that same canonical root. The runner neither installs PHPStan nor
chooses analysis paths or arguments, and it does not decode output automatically. Malformed arguments and timeout values
are programmer errors; unavailable paths, local instrumentation failures, and process failures surfaced as exceptions
are returned as typed infrastructure evidence.
Symfony Process may retry a failed direct POSIX launch through an escaped shell command line. Because it does not expose
whether that fallback occurred, a resulting status such as 126 or 127 remains raw Completed evidence rather than
being guessed to be an infrastructure failure. This boundary preserves caller-supplied argument boundaries, but it is
not a security sandbox: the configured executable runs with the caller’s operating-system permissions and should be
treated as trusted project tooling.
PhpStanResultVerifier compares each expected file with the corresponding decoded diagnostics through the same
deterministic one-to-one matcher used by the RuleTestCase integration. The returned PhpStanVerificationResult keeps
successful file matches, file mismatches, and analyzer-wide errors separate. An expectation can require an exact
identifier, a case-sensitive message-or-tip substring, or both. When it carries a sourceLineRange, the diagnostic’s
maintained sourceLine, or its analyzer line when no maintained mapping exists, must fall within that range. Matching
also requires equal counts and assigns every expectation to a distinct diagnostic. DiagnosticExpectation::$sourceLine
identifies the authored expectation for reporting and is not itself a matching constraint.
A missing expected file with at least one expectation and an unexpected diagnostic file become ordinary count mismatches
with their complete evidence. An expected path with an empty expectation list matches an absent analyzer entry, because
the decoded result is not a complete manifest of every analyzed file. Verification therefore cannot by itself prove that
a clean file was analyzed; callers that need that guarantee must cross-check their configured or invoked analysis paths.
Analyzer-wide errors make the result unsuccessful without discarding otherwise successful file matches. Use
isSuccessful() for disposition and inspect matchesByFile, mismatchesByFile, and globalErrors for reporting.
Paths are compared exactly. A caller that analyzes generated or relocated files remains responsible for associating the analyzer paths with the maintained expectation paths before verification.
Command execution, JSON decoding, and expectation verification remain separate public stages beneath the convenience orchestrator. Akashi does not create Composer projects, install packages, generate PHPStan configuration, or choose a consumer’s compatibility matrix.
Analysis Lifecycle and Trust
Akashi parses all selected examples and validates their declarations before loading any of them. It rejects direct
exit or die, __halt_compiler(), built-in define(), duplicate class-like, function, or global-constant
declarations, and declarations already present in the hosting process. It then writes private temporary PHP files,
requires every selected file once so declarations are visible to reflection, and analyzes each file independently via
PHPStan’s public gatherAnalyserErrors() API.
Requiring the files executes their top-level code. PHPStan verification is therefore for trusted, runtime-safe project documentation. Akashi captures output, restores the working directory, error-reporting level, and output-buffer stack, and removes temporary artifacts, but it is not a sandbox.
Analyzer lines are translated back to maintained Markdown, inline PHPDoc, or canonical external PHP lines when the current mapping supports them. Low-level diagnostic metadata may retain a temporary path, while the user-facing failure report prefers the canonical maintained document. This means one ordinary named-region file can serve direct tooling, runtime verification, and PHPStan verification without copying its code into every PHPDoc presentation site.
Separate-Process Execution
In the year of the divided tribunal, each witness crossed alone into a chamber beyond the city; the wardens returned voice, alarm, sentence, and elapsed hour, then erased the borrowed threshold behind him.
— Acts of the Western Court 62:10
In-process execution is Akashi’s default because it is fast, shares PHPUnit’s loaded project environment, and reports through normal PHPUnit assertions. Select a child process for an example whose behavior cannot be isolated in the host.
Choose It for One Example
Place the directive immediately before the PHP fence:
<!-- akashi: separate-process -->
```php
exit(0);
```
In the ordinary trait-based integration, provide explicit runtime configuration through its optional hook. The
project-owned DocumentationCorpus helper is described in
Test a README and docs/.
<?php
use jbboehr\Akashi\ExampleCorpus;
use jbboehr\Akashi\Execution\RuntimeConfiguration;
use jbboehr\Akashi\Integration\PhpUnit\VerifiesPhpUnitExamples;
use PHPUnit\Framework\TestCase;
final class DocumentationExamplesTest extends TestCase
{
use VerifiesPhpUnitExamples;
protected static function akashiExampleCorpus(): ExampleCorpus
{
return DocumentationCorpus::load();
}
protected static function akashiRuntimeConfiguration(): RuntimeConfiguration
{
return RuntimeConfiguration::forProject(dirname(__DIR__))
->withBootstrap('vendor/autoload.php');
}
}
A separate-process directive without RuntimeConfiguration is rejected; Akashi never weakens requested isolation by
running the example in-process.
Projects using a custom PHPUnit method can pass the same configuration directly to the lower-level facade:
<?php
use jbboehr\Akashi\Execution\RuntimeConfiguration;
use jbboehr\Akashi\Integration\PhpUnit\PhpUnitRuntime;
$runtime = RuntimeConfiguration::forProject(dirname(__DIR__))
->withBootstrap('vendor/autoload.php');
PhpUnitRuntime::assertExample($example, $runtime);
Use this backend for authored namespaces, closing tags or inline HTML, relocation-sensitive magic constants, direct
exit() or die(), and examples that intentionally alter process-global state. It also prevents ordinary parse errors,
runtime exceptions, signals, and nonzero exits from terminating the hosting PHPUnit process.
Expected exceptions work across this boundary, including types declared only inside the child:
namespace AkashiDocs\SeparateProcess;
// akashi: separate-process, expect-exception=AkashiDocs\SeparateProcess\ImportFailure
// akashi: expect-exception-message="Import rejected", expect-exception-code=73
final class ImportFailure extends \RuntimeException
{
}
throw new ImportFailure('Import rejected by the isolated example.', 73);
Make It the Default
Projects may select child execution for every unmarked example by changing the trait’s configuration hook:
<?php
protected static function akashiRuntimeConfiguration(): RuntimeConfiguration
{
return RuntimeConfiguration::forProject(dirname(__DIR__))
->withDefaultExecutionMode(ExecutionMode::SeparateProcess);
}
An authored skip directive still takes precedence, followed by compile-only, an authored separate-process
directive, the configured default, and finally the in-process fallback. Compile-only selects no backend and cannot be
combined with the separate-process directive.
Child-Process Boundary
Akashi writes a private temporary PHP file and invokes the current PHP_BINARY with an argument list rather than a
shell command. The child runs from the configured project root. A configured bootstrap is loaded independently for each
child through auto_prepend_file. Akashi enables assertion exceptions, captures stdout and stderr separately, applies a
fixed 60-second emergency timeout, and removes temporary files in finally.
When an exception expectation is present, a private launcher catches Throwable around the authored file and records
token-bound, base64-safe typed evidence in a separate private file. Stdout and stderr remain user streams and are never
parsed as the protocol. The parent verifies the exception type, message, code, and mapped source line from that
evidence. Nonzero exits take precedence over any report; after a clean child exit, malformed changed evidence is an
infrastructure failure.
A zero exit status is success, including exit(0). Nonzero exits, signals, timeouts, startup failures, and cleanup
failures become typed execution failures. Where PHP reports a usable line, Akashi maps it back to the maintained
Markdown or PHPDoc source.
Separate process means failure containment, not security isolation. The child inherits the parent’s environment, filesystem and network permissions, PHP binary, and fixed Akashi INI profile. Use an operating-system sandbox or a dedicated CI boundary for untrusted code.
Extracting Named Examples
A white horse returned each spring to the abandoned mill and waited beside the motionless wheel. In the twelfth year, a child tied no bridle upon it but cleared the channel. Water arrived before noon, and the horse departed while grain still fell warm from the stones.
— Scholia of the Fifth Archive 48:40
Sometimes a documentation example should also become a real consumer fixture. Akashi can extract one stable named example without making a second copy the source of truth. Extraction does not execute or transform the PHP.
Mark the Example
Assign a stable example ID in the Akashi metadata associated with a PHP fence:
<!-- akashi: example=greeting -->
```php
<?php
echo "Hello from Akashi!\n";
```
The canonical example property is built in. It is distinct from the default PHPDoc @akashi-example
external-reference tag: a reference adds its canonical file or region to the corpus, while example=greeting assigns an
identity that the extraction command can select.
The same identity metadata can live inside PHPDoc and applies only to a fence in that comment:
/**
* <!-- akashi: example=greeting -->
*
* ```php
* echo "Hello from Akashi!\n";
* ```
*/
Extract It
vendor/bin/akashi extract \
docs/examples.md \
greeting
On success, stdout contains only the authored PHP source with one final LF. This makes shell redirection and byte-for-byte consumer fixtures predictable. Diagnostics go to stderr.
When the input is below the project root and its PHPDoc uses external references, add
--project-root=/absolute/project/path so those project-relative targets resolve against the intended boundary.
Example IDs use lowercase kebab-case and must be unique across the loaded corpus. Invalid, missing, duplicate, orphaned, or non-PHP identity metadata fails explicitly. See the CLI reference for the complete stream and exit-status contract.
Select It in PHP
The same operation is available without the CLI:
<?php
use jbboehr\Akashi\Source\DocumentationSource;
use jbboehr\Akashi\Source\MarkedExampleSelector;
$corpus = DocumentationSource::forProject(dirname(__DIR__))
->includeFile('docs/examples.md')
->load();
$example = (new MarkedExampleSelector())->select($corpus, 'greeting');
Use ordinary corpus loading for PHPUnit and PHPStan. An example property adds a stable author-assigned identity; it
does not filter unnamed examples from either workflow.
For compatibility, a project may retain an existing marker such as <!-- yumemi-example: greeting -->. Add that dialect
with withMarkerName('yumemi-example') when loading in PHP, and pass --marker-name=yumemi-example to extract.
Canonical akashi: metadata remains recognized at the same time. Duplicate identities across canonical and legacy forms
fail rather than one taking precedence.
Guides
A midwife carried a silk cloth and a rough linen cloth. The silk adorned the cradle; the linen gripped the newborn when her hands were wet. She taught her daughters to honor what serves before what is displayed. Welcome arrives safely by the humbler fabric.
— Acts of the Western Court 42:3
These guides start from a concrete project task rather than an Akashi subsystem:
- Test a README and docs/ defines a practical recursive corpus with exclusions.
- Reuse Examples for Runtime and PHPStan gives one corpus definition to both verifiers.
- Diagnose Failures traces extraction, transformation, execution, and analysis reports back to their maintained source.
For a linear first use, begin with the Quick Start. For exact contracts, use the Reference.
Test a README and docs/
At midnight the snow upon the observatory dome rose into the air and revealed old repairs in the copper. The astronomers beheld no star; they saw instead the patient hands that had preserved their sight, and kept vigil until the snow descended again.
— Revelation of the Artificial Sun 47:26
Most projects want the root README plus a recursive documentation directory, while excluding generated books, archives, or prose pages whose PHP fences are illustrative rather than executable.
Define the Source Set
<?php
use jbboehr\Akashi\ExampleCorpus;
use jbboehr\Akashi\Source\MarkdownSource;
final class DocumentationCorpus
{
public static function load(): ExampleCorpus
{
return MarkdownSource::forProject(dirname(__DIR__))
->includeFile('README.md')
->includeDirectory('docs')
->exclude('docs/archive')
->exclude('docs/generated')
->load();
}
}
All configured paths are relative to the project root. Directory includes recurse, and a directory exclusion removes its whole subtree. Include and exclusion paths must exist when the corpus loads; a stale path is an error rather than a silent coverage change.
includeDirectory('docs') selects every case-sensitive .md file below docs. Each php fence must be intended for
at least one workflow. Mark valid PHP that should be parsed by PHPUnit without execution as compile-only. For
fragments that should enter no workflow, use another language label such as php.ini or text, keep the document
outside this source set, or narrow the manifest; Akashi does not yet provide a global ignore directive.
Compile-only changes PHPUnit behavior only. If the corpus also feeds PHPStan, exclude compile-only fragments with unsafe top-level code from PHPStan selection because that workflow requires selected analysis files.
Use DocumentationSource instead when the same corpus should also include .php files containing inline PHPDoc fences
or references to canonical PHP examples. It has the same file, directory, and exclusion model and dispatches selected
files by extension.
Use It in PHPUnit
Return the corpus from Akashi’s PHPUnit trait hook:
use jbboehr\Akashi\ExampleCorpus;
use jbboehr\Akashi\Integration\PhpUnit\VerifiesPhpUnitExamples;
use PHPUnit\Framework\TestCase;
final class DocumentationExamplesTest extends TestCase
{
use VerifiesPhpUnitExamples;
protected static function akashiExampleCorpus(): ExampleCorpus
{
return DocumentationCorpus::load();
}
}
Each PHP fence becomes one independently reported PHPUnit data set. A runtime skip directive keeps its data-set entry
visible rather than removing it from discovery. A compile-only example instead passes after source-aware parsing and
never executes its code.
Keep the Set Deliberate
Prefer a short, explicit list of source roots over including the repository root. Akashi rejects duplicate physical documents reached through overlapping includes, symbolic-link directory traversal, and documents resolving outside the project root. Those checks keep a corpus reproducible, but they cannot decide whether an illustrative snippet is a good test.
If several test classes need the same selection, put this source configuration in a small project-owned helper. Akashi does not maintain a mutable global corpus registry.
Reuse Examples for Runtime and PHPStan
Beneath the glass mountain two processions appeared, one ascending and one descending, yet every pilgrim bore the same wound upon the left hand.
— Revelation of the Artificial Sun 31:24
Runtime behavior and static-analysis behavior answer different questions, but they can begin from the same maintained
documentation. Define the corpus once in project code, then let PHPUnit execute all examples and a PHPStan
RuleTestCase select the relevant subset.
Define a Project Corpus
This helper belongs to the consuming project:
<?php
use jbboehr\Akashi\ExampleCorpus;
use jbboehr\Akashi\Source\DocumentationSource;
final class DocumentationCorpus
{
public static function load(): ExampleCorpus
{
return DocumentationSource::forProject(dirname(__DIR__))
->includeFile('README.md')
->includeDirectory('docs/examples')
->includeDirectory('src')
->load();
}
}
This is an ordinary project helper, not an Akashi requirement. It keeps Markdown and PHPDoc source selection consistent between tests.
Execute It with PHPUnit
<?php
use jbboehr\Akashi\ExampleCorpus;
use jbboehr\Akashi\Integration\PhpUnit\VerifiesPhpUnitExamples;
use PHPUnit\Framework\TestCase;
final class DocumentationRuntimeTest extends TestCase
{
use VerifiesPhpUnitExamples;
protected static function akashiExampleCorpus(): ExampleCorpus
{
return DocumentationCorpus::load();
}
}
Akashi’s trait provides the data provider and runtime test. The consuming project owns the corpus definition and PHPUnit test class.
Analyze a Relevant Subcorpus
The following is a template: replace YourRule and extension.neon with the consumer’s real PHPStan rule and
configuration.
<?php
use jbboehr\Akashi\Integration\PHPStan\PhpStanExampleConfiguration;
use jbboehr\Akashi\Integration\PHPStan\VerifiesPhpStanExamples;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
/** @extends RuleTestCase<YourRule> */
final class DocumentationPhpStanTest extends RuleTestCase
{
use VerifiesPhpStanExamples;
protected function getRule(): Rule
{
return self::getContainer()->getByType(YourRule::class);
}
public static function getAdditionalConfigFiles(): array
{
return [dirname(__DIR__) . '/extension.neon'];
}
public function testExamples(): void
{
$projectRoot = dirname(__DIR__);
$configuration = PhpStanExampleConfiguration::forTokens(
$projectRoot,
'@akashi-phpstan-error',
'//!',
'@analyze-example',
);
$this->assertPhpStanExamples(DocumentationCorpus::load(), $configuration);
}
}
PHPStan owns RuleTestCase, the container, rule construction, and extension configuration. Akashi owns selection from
the supplied corpus, identifier and legacy //! expectation parsing, analysis-file preparation, diagnostic matching,
source mapping, and reporting through PHPUnit. @akashi-phpstan-error is Akashi’s preferred expectation directive;
tokens such as @analyze-example are chosen by the consuming project.
Decide Which Workflow Sees an Example
- Every selected PHP fence enters the shared corpus.
- PHPUnit sees each example as a data set;
<!-- akashi: skip -->reports one as skipped, whilecompile-onlyparses one without runtime execution. - PHPStan sees only examples accepted by
PhpStanExampleConfiguration; runtime skip and compile-only do not affect that selection. - Marked extraction can select one example independently of both verifiers.
PHPStan requires selected files to make declarations available, which executes their top-level code. Its subcorpus must therefore contain trusted, runtime-safe top-level code even when PHPUnit skips a fence or validates it as compile-only. Compile-only changes PHPUnit behavior only; exclude an unsafe non-running fragment from PHPStan selection.
Diagnose Failures
The physician marked whether the wound arose beneath the blade or beneath the bandage, for one grief may require two remedies and neither is served by an unnamed hour.
— Acts of the Western Court 57:15
Akashi reports failures at the maintained documentation source whenever the current transformation has enough mapping information. Start with the phase and source path in the message, then inspect generated or temporary details only when the report says an exact maintained line is unavailable.
Discovery and Metadata Failures
Corpus loading fails before PHPUnit yields data sets when an include or exclusion is missing, a path escapes the project root, the same physical document is reached twice, no documents or PHP examples are found, or example metadata is malformed. These messages name the configured path or maintained documentation line responsible.
Fix the source set or comment placement; rerunning individual data sets cannot bypass a corpus-level discovery error.
Parse and Transform Failures
PHP syntax errors and unsupported in-process constructs report the example ID and maintained Markdown or PHPDoc
location. Unsupported examples commonly need either a source change or an explicit <!-- akashi: separate-process -->
directive. Akashi does not silently reroute them, because doing so would hide a change in execution semantics.
An assertion-transform error usually means the native assert() call uses unsupported argument syntax. See
Assertion Behavior for the accepted forms.
Runtime Failures
The PHPUnit report includes:
- the example ID and human-readable data-set label;
- the failure phase;
- the maintained document and line, or the example start when an exact line is unavailable;
- the original exception or child-process outcome;
- captured stdout and stderr when nonempty;
- state-restoration or temporary-file cleanup failures.
For in-process examples, rewritten assertion messages identify the expression and source line unless the author supplied a custom message. For child examples, PHP parse and runtime lines are translated when PHP reports the temporary-file location in a recognized form. The original cause remains in the exception chain for debugger inspection.
PHPStan Failures
PHPStan verification distinguishes configuration and preflight failures from diagnostic mismatches. A mismatch report shows authored expectations, analyzer diagnostics, and their maintained documentation locations. Common causes are:
- a diagnostic identifier changed or its optional text no longer matches the message or tip;
- a diagnostic is reported outside the statement associated with its identifier expectation;
- a legacy diagnostic changed wording and no longer contains the
//!substring; - the actual and expected diagnostic counts differ;
- two expectations can match only one diagnostic;
- an example selected by a relevance token unexpectedly reports a diagnostic;
- declarations collide within the selected corpus or with the hosting process.
Identifier expectations display their exact identifier, associated statement span, and optional text constraint. Legacy
//! expectations display their message-and-tip substring.
Temporary Locations
Akashi keeps original example code separate from prepared execution or analysis source. Current source maps translate generated PHP lines back to Markdown or PHP for parse errors, runtime failures, rewritten assertions, and PHPStan diagnostics where the underlying tool exposes a usable line. A low-level exception may still mention an Akashi temporary file; the user-facing report should prefer the original document when a mapping exists.
If a failure reports only an opaque temporary location even though PHP or PHPStan supplied a clear generated line, treat that as a reporting defect and include both locations when filing an issue.
Reference
An old scholar kept a basket of walnuts beside his books. For every answer he gave, he cracked one; for every question he could not answer, he planted one. His garden outlived his library. Let uncertainty take root before certainty has consumed the whole harvest.
— Acts of the Western Court 2:31
This section records exact implemented contracts rather than teaching the first workflow.
- Configuration: source and runtime configuration values.
- Example Metadata: identity and runtime metadata grammar, association, and precedence.
- CLI: command syntax, streams, output, and exit statuses.
- Public API: supported entry points and dependency boundaries.
- Compatibility and Safety: versions, limitations, and trust model.
For task-oriented examples, return to Using Akashi or the Guides.
Configuration
The steward kept one immutable chart naming the court, the optional scroll read before testimony, and the ordinary road; each revision produced a new chart while the former hearing retained its own.
— Revelation of the Artificial Sun 61:9
Akashi uses immutable configuration objects. There is no global Akashi registry or configuration file in the current API. Optional PHP-CS-Fixer checks may point to that tool’s existing project configuration.
Documentation Sources
Start with an absolute project root:
$source = DocumentationSource::forProject($projectRoot);
The fluent methods are:
| Method | Contract |
|---|---|
includeFile($path) | Add one project-relative file with the case-sensitive .md or .php extension. |
includeFiles($paths) | Add files from an array or iterator of strings, ProjectPath, or SplFileInfo values. |
includeDirectory($path) | Recursively add .md and .php files below one project-relative directory. |
exclude($path) | Exclude an exact path and, for a directory, its complete subtree. |
withMarkerName($name) | Add one lowercase kebab-case legacy marker-comment dialect across both source formats. |
withPhpDocReferenceTags(...$tags) | Replace the default @akashi-example external-reference tag with one or more accepted names. |
load() | Read selected sources and return one nonempty, deterministically ordered ExampleCorpus. |
includeFiles() consumes its iterable immediately to preserve immutable configuration. Symfony Finder entries extend
SplFileInfo, so a Finder restricted to files can be passed directly. A SplFileInfo may carry an absolute pathname,
but the resolved file must remain inside the configured project root. Strings and ProjectPath values remain
project-relative.
MarkdownSource retains the Markdown-only API, including loadDocuments(), and now also accepts includeFiles(). Its
explicit files and recursive directories continue to select only the case-sensitive .md extension.
Canonical akashi: metadata, including example=ID, requires no source configuration. withMarkerName() is additive:
it preserves a project-specific comment such as <!-- yumemi-example: ID --> while canonical metadata remains active.
IDs from both forms share one corpus-wide uniqueness check.
Includes and exclusions are evaluated when loading. Configured paths must exist, documents must be readable, and resolved documents must remain inside the project root. Symlinked directories are not traversed. Reaching one physical document through multiple includes is an error. Documents are ordered by slash-normalized project-relative path using bytewise lexical comparison. The final mixed corpus is ordered by canonical code path, first code line, and stable example ID, so inline and referenced examples remain deterministic together.
External PHP files referenced by selected PHPDoc do not also need to appear in the include manifest. They must resolve
to readable case-sensitive .php files inside the same canonical project root. Repeated references to the same whole
file or named region produce one example with every PHPDoc presentation location retained. See
Authoring Examples.
An empty include set, an include/exclusion that does not exist, a selected set with no supported documents, and a corpus with no PHP fences or external references are distinct failures. See Authoring Examples for the common setup.
PHP-CS-Fixer Configuration
Formatting\PhpCsFixerConfiguration::forProject() validates one canonical project root, one project-relative
PHP-CS-Fixer executable, and an optional project-relative PHP-CS-Fixer configuration:
use jbboehr\Akashi\Formatting\PhpCsFixerConfiguration;
$formatting = PhpCsFixerConfiguration::forProject(
projectRoot: dirname(__DIR__),
executable: 'vendor/bin/php-cs-fixer',
config: '.php-cs-fixer.dist.php',
);
The executable defaults to vendor/bin/php-cs-fixer; the config defaults to null, allowing PHP-CS-Fixer to discover
its usual project configuration from the project root. Configured files must exist, be readable regular files, and
resolve inside the canonical root. The executable is invoked through the current PHP_BINARY, so it is expected to be
the PHP Composer binary proxy rather than an arbitrary native executable.
Pass this immutable configuration to Formatting\FormattingChecker. Constructing it does not run the formatter, and no
other Akashi workflow reads it implicitly.
Runtime Configuration
RuntimeConfiguration is optional for in-process execution and required for separate-process execution:
<?php
use jbboehr\Akashi\Execution\ExecutionMode;
use jbboehr\Akashi\Execution\RuntimeConfiguration;
$runtime = RuntimeConfiguration::forProject($projectRoot)
->withBootstrap('vendor/autoload.php')
->withDefaultExecutionMode(ExecutionMode::InProcess);
forProject() resolves a readable directory to its canonical path. withBootstrap() accepts a project-relative,
readable file that resolves within that root. withDefaultExecutionMode() accepts ExecutionMode::InProcess or
ExecutionMode::SeparateProcess.
Runtime processing precedence is:
- authored
skipdisposition; - authored
compile-onlydisposition; - authored
separate-processselection; - configured default execution mode;
- in-process fallback.
Compile-only validation selects no execution mode. Combining its directive with an authored separate-process directive
or expected exception is invalid.
An in-process example with configuration runs from the configured project root. Without configuration, it runs from the caller’s current working directory. A separate-process example without configuration is rejected.
In-process bootstraps use require_once and are loaded once per PHPUnit process. Separate-process bootstraps use
auto_prepend_file in every child.
PHPStan Configuration
PhpStanExampleConfiguration::forProject($projectRoot, $predicate) accepts a callable from Example to bool.
PhpStanExampleConfiguration::forTokens($projectRoot, ...$tokens) creates a predicate that selects code containing any
supplied case-sensitive token. At least one nonblank, unique token is required.
The project root is canonicalized and must be a readable directory. Selection preserves corpus order and must produce a nonempty relevant subcorpus.
Example Metadata
A dancer rehearsed her falls as carefully as her leaps. When a stage board split, she descended without injury and guided another performer down. The audience praised her grace; she thanked the hours spent learning the ground. Wisdom prepares dignity for the moment it cannot remain upright.
— Acts of the Western Court 4:37
Akashi uses one small metadata grammar for an example’s stable identity, runtime disposition, execution mode, and expected runtime behavior. Write it in an HTML comment associated with the next Markdown or PHPDoc fence:
<!-- akashi: example=isolated-greeting, separate-process -->
```php
echo "Hello!\n";
```
The same grammar works as a tokenized PHP line comment inside a fence or referenced canonical PHP file:
// akashi: example=invalid-input, expect-exception=RuntimeException
// akashi: expect-exception-message="invalid documentation input", expect-exception-code=73
throw new RuntimeException('invalid documentation input', 73);
Grammar
Each comment contains a comma-separated list of flags and keyed properties:
<!-- akashi: flag, key=value, key="quoted value" -->
// akashi: flag, key=value, key="quoted value"
Whitespace around commas and = is ignored. Unquoted values are nonempty single tokens. Double-quoted values use JSON
string escaping and may contain spaces, commas, or =. Property names and flags are case-sensitive lowercase
kebab-case. Empty clauses, unknown properties, values on flags, and missing keyed values fail with the maintained source
line. Keyed values must be nonempty except that expect-output="" explicitly requires silent stdout.
The flags are:
| Flag | Meaning |
|---|---|
skip | Ask PHPUnit to report the example as skipped. |
compile-only | Parse the example for PHPUnit without executing it. |
separate-process | Select child-process execution instead of the default in-process backend. |
The keyed properties are:
| Property | Value |
|---|---|
example | A lowercase kebab-case identity used by selection and extraction. |
expect-exception | A global PHP throwable class or interface name. |
expect-exception-message | A nonempty, case-sensitive message substring. |
expect-exception-code | A signed base-10 integer in the running PHP build’s integer range. |
expect-output | The exact stdout bytes required from runtime execution. |
Adjacent HTML comments are merged, so these forms are equivalent:
<!-- akashi: example=conversion-basic, separate-process -->
<!-- akashi: example=conversion-basic -->
<!-- akashi: separate-process -->
HTML and inline PHP metadata associated with the same fence are also merged. Every property may appear at most once
across the complete example; even repeated flags fail rather than relying on precedence. Message and code constraints
require expect-exception on that example.
Association Rules
Place HTML metadata immediately before a fenced PHP block. Blank lines and adjacent Akashi metadata comments are allowed:
<!-- akashi: example=isolated-greeting, separate-process -->
```php
namespace DocumentationExample;
echo "Hello!\n";
```
Prose or an unrelated CommonMark block breaks the association. Orphaned metadata and metadata targeting non-PHP fences fail during extraction with the comment’s source location.
Inside PHPDoc, retain the normal leading * on each authored line. Akashi removes the docblock decoration before
applying the same association rules, and metadata never crosses from one PHPDoc comment into another:
/**
* <!-- akashi: separate-process -->
*
* ```php
* exit(0);
* ```
*/
Metadata is deliberately not encoded in the fence info string; ordinary php language tags remain readable to renderers
and syntax highlighters.
Any inline metadata comment may appear anywhere as an actual PHP line comment and applies to the whole example. Place an expected-exception comment immediately before the operation expected to throw when that makes the example easier to read; Akashi does not infer or enforce control-flow order. Recognition uses PHP comment tokens, so matching text inside strings or heredocs is not metadata. The comment remains part of the ordinary PHP source, so readers, IDEs, formatters, static analyzers, direct execution, and marked extraction all see it unchanged.
Use the HTML form for documentation fences when surrounding prose already establishes the behavior or an extracted consumer fixture should not contain Akashi metadata. External whole-file and named-region examples use inline comments because their canonical code is not physically adjacent to the PHPDoc reference.
Compatibility and Structural Syntax
Legacy one-property directives remain accepted, including <!-- akashi: expect-exception RuntimeException --> and
// akashi: expect-exception-message invalid documentation input. Projects may additionally recognize one legacy
identity comment such as <!-- yumemi-example: conversion-basic --> through withMarkerName('yumemi-example') or the
CLI’s --marker-name option. Canonical and legacy metadata share the same typed result and duplicate-property checks.
The legacy space form consumes the rest of its comment, including commas and =. Use canonical key=value syntax or an
adjacent metadata comment when combining an exception constraint with another property.
Structural constructs remain separate because they delimit or reference source rather than describe one example:
akashi-region and akashi-region-end delimit canonical named regions, akashi-sync and akashi-sync-end delimit
synchronized presentations, and PHPDoc @akashi-example references an external canonical source.
Runtime Semantics
skip keeps the example in its corpus and named PHPUnit data set, but PhpUnitRuntime asks PHPUnit to mark it skipped
before configuration, transformation, bootstrap loading, or execution. Skip affects runtime only. PHPStan may still
select the example, and marked extraction still returns its authored source.
compile-only keeps the example in the same corpus and named PHPUnit data set. PHPUnit asks Akashi’s host-version PHP
parser to validate it and records one successful assertion without selecting an execution backend, applying runtime
transforms, loading the configured bootstrap, or executing authored code:
// akashi: compile-only
throw new RuntimeException('PHPUnit compile-only validation does not execute this.');
Parse failures retain the maintained documentation path and line. PHPStan and marked extraction may still select the
same example independently. PHPStan verification requires selected files and therefore executes their top-level code; do
not select a compile-only fragment whose top-level code is unsafe to run. compile-only cannot be combined with
separate-process, an expected-exception contract, or expected output, because backend selection and runtime evidence
have no meaning when PHPUnit execution is disabled.
separate-process selects child-process execution. It overrides an in-process configured default and requires
RuntimeConfiguration with an explicit project root. Akashi rejects missing configuration rather than silently running
the example in-process.
expect-exception uses PHPUnit-familiar type semantics. Its argument is a global PHP class or interface name; a leading
\ is accepted but normalized away. In the selected runtime, that name must identify an available class or interface
compatible with Throwable. A subtype satisfies an expectation for its parent type:
```php
// akashi: expect-exception=DomainException
// akashi: expect-exception-message="Invalid documentation input", expect-exception-code=73
throw new DomainException('Invalid documentation input.', 73);
```
expect-exception-message requires a nonempty, case-sensitive substring in the actual exception message. This follows
PHPUnit’s expectExceptionMessage() behavior rather than requiring an exact string. expect-exception-code accepts a
signed base-10 integer within the running PHP build’s integer range and compares it exactly with Throwable::getCode().
A runtime string code, such as a PDO SQLSTATE, is preserved but cannot match that integer constraint. The example fails
if it completes normally, throws an incompatible type, has a mismatched message or code, or cannot complete its backend
cleanup. In-process mismatches preserve the actual throwable as the previous exception. Separate-process mismatches
preserve a typed parent-side representation of the child evidence; the expected type may be defined only inside the
child. Process exits, signals, timeouts, and infrastructure failures never satisfy an exception expectation. When skip
is also present, skip takes precedence over compile-only validation, configuration, transformation, and expectation
handling.
expect-output compares captured stdout exactly after the ordinary execution contract succeeds. Double-quoted JSON
escapes make line endings and other control characters explicit:
// akashi: expect-output="Hello, Akashi!\n"
echo "Hello, Akashi!\n";
Akashi does not normalize line endings, trim whitespace, or interpret patterns. expect-output="" asserts that stdout
is empty. The property may accompany expect-exception; in that case Akashi first requires the expected throwable,
message, and code contract to succeed, then compares output emitted before the throwable. Execution, cleanup, or
exception mismatches remain the primary failure instead of being masked by an output mismatch. Stderr remains diagnostic
evidence and has no expectation property in this release.
Not Implemented
Akashi does not currently implement a global ignore directive, expected compilation failure, general expected runtime failure, expected stderr, conditional or platform-specific skip, custom skip reasons, or hidden support-code syntax. These remain roadmap items and must not be inferred from Rust or PHPUnit terminology.
CLI
Do not pity the silver mask when the actor departeth. It was fashioned to bear one sorrow before the multitude, and fulfillment is not diminished because the face beneath it hath returned to ordinary joy.
— Scholia of the Fifth Archive 52:45
The Composer executable is vendor/bin/akashi. It provides marked-example extraction, optional inline formatting checks
or writes, and explicit synchronization checks or writes; it is not a standalone documentation-test runner. Runtime
examples are normally run through PHPUnit.
Usage
vendor/bin/akashi extract [--marker-name=NAME] [--project-root=PATH] FILE MARKER-ID
vendor/bin/akashi format (--check|--write) [--project-root=PATH] [--php-cs-fixer=PATH] [--config=PATH] FILE [FILE ...]
vendor/bin/akashi sync (--check|--write) [--project-root=PATH] FILE [FILE ...]
vendor/bin/akashi --help
vendor/bin/akashi --version
Akashi uses Symfony Console for command discovery, argument parsing, generated help, and shell completion. Run the
executable without arguments to list commands, or inspect one command with vendor/bin/akashi COMMAND --help. Command
names are exact: abbreviations such as ext are rejected rather than guessed.
Generate a completion script for Bash, Fish, or Zsh with the built-in command, for example:
vendor/bin/akashi completion bash
Symfony’s standard --quiet, verbosity, ANSI, and non-interaction options are available. Quiet mode suppresses
successful command output but retains failure diagnostics. Akashi deliberately rejects --silent because its stable CLI
contract requires failures to remain visible.
Extract a Named Example
FILE must use the case-sensitive .md or .php extension and may be absolute or relative to the current working
directory. Canonical example=MARKER-ID metadata may precede its fence in Markdown or PHPDoc, or appear as an actual
PHP line comment inside fenced or referenced canonical code. MARKER-ID uses lowercase kebab-case.
--marker-name=NAME optionally adds one lowercase kebab-case legacy marker-comment dialect, such as
<!-- yumemi-example: chosen -->. It may appear before or after the positional arguments and accepts either
--marker-name=NAME or --marker-name NAME. Canonical akashi: metadata remains recognized when this compatibility
option is present. The option may be specified at most once.
By default, Akashi treats FILE’s containing directory as the project root. Pass --project-root=PATH when FILE
lives deeper in the project or its PHPDoc contains project-relative external-example references. The path may be
absolute or relative to the current working directory, must contain FILE, and may be specified at most once.
On successful extraction, stdout contains only the authored PHP fence source. Akashi removes an authored final line ending, if present, and appends exactly one LF for compatibility with its recorded consumer. It does not add headings, metadata, source comments, or transformation output, and it preserves an authored opening PHP tag. Successful help and version output also use stdout.
Usage, extraction, and unexpected-failure diagnostics use stderr, including under --quiet.
Check or Write Inline Formatting
Check the inline PHP fences in explicitly selected Markdown or PHP documentation files:
vendor/bin/akashi format --check --project-root=. README.md docs/examples.md src/Example.php
Select exactly one of --check or --write. FILE and --project-root follow the same current-working-directory,
project-containment, case-sensitive extension, duplicate-document, and symbolic-link discovery rules as the other
explicit-file commands. At least one .md or .php file is required.
The PHP-CS-Fixer executable defaults to the project-relative vendor/bin/php-cs-fixer. Override it with
--php-cs-fixer=PATH; select a project-relative configuration explicitly with --config=PATH. Both files must resolve
to readable regular files inside the canonical project root. Without --config, PHP-CS-Fixer performs its ordinary
configuration discovery from that root.
Akashi checks only inline Markdown and PHPDoc fences. PHPDoc references to whole external files or named regions are loaded and validated but not sent through this adapter; run the project’s ordinary formatter directly on those PHP files. Each checked body is written to a private temporary PHP file, PHP-CS-Fixer runs through an explicit argument vector without constructing a shell command, caching, or parallel execution, and a 60-second infrastructure timeout applies.
A current set exits successfully without output. Each stale inline example produces a source-labelled unified diff on
stderr from the authored fence to the formatter result, followed by a deterministic count, and exits with status 1. An
authored opening tag is preserved, while formatter changes to body line endings and the final newline remain visible.
Project-level material outside Akashi’s protected body boundary, such as an inserted license header, is ignored.
Malformed formatter output, unsupported closing tags or inline HTML, configuration errors, process failures, timeouts,
and cleanup failures are command failures.
Write mode applies those formatter results to the inline examples in the selected documents:
vendor/bin/akashi format --write --project-root=. README.md docs/examples.md src/Example.php
Before changing the first file, Akashi renders every proposed document in memory, reloads the complete selected source, and repeats every formatter invocation. The maintained bytes and formatter results must match the first pass. Akashi then uses stale-byte protection and same-directory atomic replacement for each changed document. Direct symbolic-link files and paths through symbolic-link directories are rejected. Successful writes are reported on stderr in deterministic project-path order; an entirely current set is silent.
Validation and formatter failures leave the selected set unchanged. A later filesystem error can occur after earlier documents in a validated batch were replaced, but each individual document remains an all-or-nothing replacement. The writer preserves permission bits, but not ownership, ACLs, extended attributes, or hard-link identity. Because replacement is a directory-level rename, a read-only file can still be replaced when its containing directory is writable; its read-only permission bits are retained on the replacement.
Synchronize Presentations
Use check mode to compare one or more explicitly selected Markdown or PHP files with their canonical PHP sources:
vendor/bin/akashi sync --check --project-root=. README.md docs/examples.md src/Example.php
Select exactly one of --check or --write. Files and --project-root may be absolute or relative to the current
working directory. The project root defaults to the current working directory, must contain every selected document, and
provides the containment boundary for each project-relative synchronization target. Only case-sensitive .md and .php
files are accepted.
A current set of presentations exits successfully without output. For each stale presentation, stderr identifies the
start directive’s maintained document and line, its authored canonical target, and the resolved canonical code location.
It then prints a unified diff from the stale presentation (-) to the canonical replacement (+); both diff headers
carry their maintained path and first code line. Diff input uses the same narrow line-ending and final-newline
normalization as synchronization comparison. Akashi finally prints a mismatch count and exits with status 1.
Write mode uses the same parser and in-memory renderer:
vendor/bin/akashi sync --write --project-root=. README.md docs/examples.md src/Example.php
Before changing any file, Akashi renders and validates the complete selected set, reloads every maintained document, and verifies the canonical snapshots again. It refuses to overwrite a document whose bytes changed after loading. Each changed document is written to a temporary sibling, flushed, assigned the original file’s permission bits, and atomically renamed over the original. Direct symbolic-link files and paths through symbolic-link directories are rejected. Unchanged files are not replaced. Successful write reports use stderr in deterministic project-path order; an entirely current set is silent.
Akashi rejects a selected batch when one selected PHP document would be rewritten while another selected presentation uses that PHP document as its whole-file canonical source. Otherwise the dependent replacement would be calculated from bytes that the same batch is about to change. Write the canonical document separately and rerun the dependent synchronization. Named-region dependencies remain supported because PHPDoc presentation rewriting does not change the executable named region.
Validation errors leave the selected set unchanged. A later filesystem error can occur after earlier documents in a validated batch were replaced, but each individual document remains an all-or-nothing replacement; rerunning the command finishes any remaining current-safe work. The writer preserves permission bits, but not ownership, ACLs, extended attributes, or hard-link identity. Because replacement is a directory-level rename, a read-only file can still be replaced when its containing directory is writable; its read-only permission bits are retained on the replacement.
Malformed regions, unresolved targets, duplicate input files, unreadable files, stale document snapshots, and paths
outside the project root use status 1. Options may appear before or after file arguments, but the selected mode and
--project-root may each be specified at most once. Valued options accept either --name=value or --name value.
Exit Statuses
| Status | Meaning |
|---|---|
0 | Successful extraction, formatting/synchronization check or write, help, or version output. |
1 | A command failed, or a check found stale formatting or synchronized code. |
2 | Invalid command or command arguments. |
70 | Unexpected internal software failure. |
Invalid, missing, duplicate, orphaned, and non-PHP example identities are extraction failures. Unknown commands or
options and missing required arguments are usage failures. The extraction command selects an explicit example
identity; a PHPDoc external reference is only a corpus source unless its resolved canonical code declares that metadata.
PHP-CS-Fixer is optional and is required only when the formatting command is invoked. Generated help, command listing,
and shell completion are supplied by Symfony Console; Akashi retains its own exact-command, duplicate-option, stream,
and exit-status contracts around that router.
Public API
High above the world, the wandering lights crowded one another until the outermost loosened its circle and moved away. Silence widened behind it, giving each light room to burn. Space began as permission granted to departure. Bless what releases without cursing the one who leaves.
— Ordinances of the Synthetic Dawn 6:14
Akashi is pre-1.0, so these APIs are usable but may change between minor releases before 1.0. Architecture tests classify every autoloadable Akashi declaration as an entry point, canonical model type, PHPStan diagnostic model type, exception, or explicitly internal declaration. This reference groups the public types by consumer workflow; autoloadability alone does not create an extension point.
Migrating from 0.1
Projects that build a corpus through MarkdownSource and execute or analyze it through the PHPUnit and PHPStan traits
do not need code changes for 0.2. Existing Markdown fences, explicitly configured legacy marker comments, runtime
directives, and text-based PHPStan expectations remain accepted.
Direct consumers of the canonical model need the following changes.
Example sources
Example now distinguishes inline documentation fences from canonical external PHP sources. Replace the removed
properties as follows:
| 0.1 access | 0.2 replacement |
|---|---|
$example->document | $example->codeOrigin()->document |
$example->location | $example->source->location after confirming source is an InlineExampleSource |
$example->fence | $example->source->fence after confirming source is an InlineExampleSource |
Use codeOrigin() whenever the maintained code location is sufficient. Inspect Example::$source only when the
presentation distinction matters: InlineExampleSource carries the former fence location and metadata, while
ReferencedExampleSource carries a canonical origin, optional named region, and one or more PHPDoc reference locations.
Code that directly constructed a 0.1 inline example should use Example::fromInline() with the former constructor
arguments. The 0.2 constructor instead accepts an InlineExampleSource|ReferencedExampleSource and is intended for
integrations that already model that distinction.
Manually constructed ExampleCorpus values must be ordered by canonical source path, first code line, and example ID.
Corpora returned by MarkdownSource or DocumentationSource already satisfy this invariant.
PHPStan diagnostic expectations
DiagnosticExpectation::$text is now nullable because an expectation may constrain only a stable PHPStan identifier.
Consumers reading the property must handle null. Existing new DiagnosticExpectation($text, $sourceLine) calls remain
valid; the optional identifier and source-line range parameters are additive.
Directive::CompileOnly is a new enum case. Consumers using an exhaustive match over Directive must handle it.
Trailing optional parameters added to AnalyzerDiagnostic, ExpectedException, and MetadataLocation do not require
changes to existing calls.
No other 0.1 public type or member was removed. The change from native readonly classes to final classes composed only of readonly properties enables PHP 8.1 support without making model state mutable.
Source and Corpus
| Type | Purpose |
|---|---|
jbboehr\Akashi\Source\DocumentationSource | Immutable mixed Markdown/PHPDoc discovery and extraction. |
jbboehr\Akashi\Source\MarkdownSource | Immutable file/directory discovery and CommonMark PHP-fence extraction. |
jbboehr\Akashi\Source\MarkedExampleSelector | Select exactly one example by an author-assigned example identity. |
jbboehr\Akashi\Document | One maintained Markdown or PHP source document and its line index. |
jbboehr\Akashi\Example | Canonical example with typed source, code, directives, and runtime expectations. |
jbboehr\Akashi\ExampleCorpus | Ordered, nonempty, unique collection of examples. |
DocumentationSource is the ordinary entry point for mixed corpora; MarkdownSource remains the explicit Markdown-only
entry point and exposes loadDocuments() for consumers that need the selected documents themselves.
Document, Example, and ExampleCorpus form the canonical public model. Example::$source is either
Model\InlineExampleSource for a Markdown/PHPDoc fence or Model\ReferencedExampleSource for a canonical external PHP
file or named region. Example::codeOrigin() returns the maintained code location without requiring callers to switch
on that presentation distinction. A referenced source also retains each Model\ReferenceLocation where PHPDoc presents
the canonical example. Typed integrations constructing inline examples can use Example::fromInline() to derive a
matching CodeOrigin from one fenced SourceLocation.
Path, identifier, language, fence, directive, and source-coordinate values under jbboehr\Akashi\Model are also public
because the canonical model and configuration objects expose them as typed state. That includes
Model\ExpectedException, which carries a normalized authored throwable class name, optional nonempty case-sensitive
message substring, and optional integer code without requiring the class to exist before runtime setup.
Example::$expectedOutput carries an optional exact stdout byte string; null means no output contract, while an empty
string explicitly requires silence. Public model constructors enforce the same invariants used by source discovery;
these are data contracts, not subclassing or service-replacement seams.
The supporting value types are grouped by what they preserve:
| Concern | Types |
|---|---|
| Project paths | Model\ProjectRoot, Model\ProjectPath, Model\AbsoluteFilePath, Model\DocumentPath |
| Example identity | Model\ExampleId, Model\MarkerId, Model\MarkerName, Model\RegionName, Model\PhpDocTagName |
| Authored source | Model\ExampleCode, Model\Language, Model\LineIndex, Model\CodeOrigin, Model\SourceSpan, Model\SourceLocation, Model\MetadataLocation, Model\InlineExampleSource, Model\ReferencedExampleSource, Model\ReferenceLocation |
| Fence metadata | Model\FenceCharacter, Model\FenceMetadata |
| Runtime metadata | Model\Directive, Model\DirectiveSet, Model\ExpectedException |
Most consumers receive and inspect these values through the canonical model rather than constructing them directly. Direct construction remains supported for typed integrations that create documents or examples without weakening the model to raw arrays or ambiguous strings.
Synchronization
| Type | Purpose |
|---|---|
Synchronization\SynchronizationChecker | Inspect presentations and render corrected immutable documents. |
Synchronization\SynchronizationWriter | Atomically persist one corrected document when its maintained bytes are still current. |
Synchronization\SynchronizationRegion | Preserve one presentation, its canonical target, fence metadata, and exact source spans. |
Synchronization\SynchronizationMismatch | Pair a stale presentation with its canonical origin and expected normalized PHP code. |
SynchronizationChecker::rewrite() replaces only stale code spans in memory. It preserves directives, fences, prose,
Markdown or PHPDoc container prefixes, and the presentation’s line-ending convention, then re-parses and verifies the
result against the already resolved canonical snapshot. The method performs no filesystem writes.
SynchronizationWriter::write() accepts the original loaded Document and its rendered replacement. It verifies the
maintained bytes, rejects symbolic-link paths, and uses a flushed same-directory temporary file plus atomic rename. The
writer preserves permission bits; callers that need validation across several documents should render the complete set
before invoking it, as the CLI does.
Formatting
| Type | Purpose |
|---|---|
Formatting\PhpCsFixerConfiguration | Canonical project, PHP-CS-Fixer executable, and optional config paths. |
Formatting\FormattingChecker | Check inline examples without modifying maintained documents. |
Formatting\FormattingMismatch | Pair an inline example with the formatter-proposed replacement code. |
Formatting\FormattingRewriter | Apply checked mismatches to one immutable document after structural validation. |
FormattingChecker::check() accepts an ExampleCorpus, skips every ReferencedExampleSource, and returns ordered
mismatches for inline Markdown and PHPDoc fences. The checker runs a project-installed PHP-CS-Fixer against private
temporary files through an argument vector. It ignores formatter-added file-level material outside the protected body,
preserves authored opening tags, and does not write maintained source.
FormattingRewriter::rewrite() accepts the exact loaded Document and the mismatches belonging to it. It rejects
referenced examples, stale or cross-document inputs, duplicate replacements, and formatter output that would terminate a
Markdown fence or PHPDoc comment, change directive semantics, or otherwise fail re-extraction. A successful rewrite
changes only the original code spans, restores their authored Markdown or PHPDoc prefixes, retains formatter-proposed
line endings, and returns a new immutable Document; it performs no filesystem writes. Group mismatches by maintained
document before calling the rewriter when a corpus spans several files.
The library operation remains separate from persistence. The CLI’s format --write mode groups mismatches by document,
repeats the complete formatter pass to reject changed inputs or results, and then persists through the same
stale-byte-protected atomic writer used by synchronization.
This is a concrete PHP-CS-Fixer integration, not a generic formatter extension point. External canonical examples remain ordinary PHP files and should use normal project formatter commands directly.
PHPUnit Runtime
| Type | Purpose |
|---|---|
Integration\PhpUnit\VerifiesPhpUnitExamples | Provide a named PHPUnit test for every example in a consumer corpus. |
Integration\PhpUnit\PhpUnitExampleDataSets | Convert a corpus to uniquely labeled PHPUnit data-provider arguments. |
Integration\PhpUnit\PhpUnitRuntime | Apply runtime disposition, then validate or execute and assert one example. |
Execution\RuntimeConfiguration | Canonical project root, optional bootstrap, and default execution mode. |
Execution\ExecutionMode | InProcess or SeparateProcess. |
Prepared-source, transform, executor, result, and failure types describe Akashi’s internal implementation boundary. They
are not public extension points. Most runtime consumers should use VerifiesPhpUnitExamples; projects that need a
custom PHPUnit method can use PhpUnitExampleDataSets and PhpUnitRuntime::assertExample() directly. Both paths keep
skip and compile-only disposition, backend selection, preparation, execution, cleanup, and PHPUnit reporting within the
supported facade.
PHPStan
| Type | Purpose |
|---|---|
Integration\PHPStan\PhpStanExampleConfiguration | Canonical project root and relevance predicate. |
Integration\PHPStan\VerifiesPhpStanExamples | RuleTestCase trait that verifies a selected corpus. |
Integration\PHPStan\ExpectationParser | Parse identifier and legacy text expectations. |
Integration\PHPStan\DiagnosticMatcher | Match framework-neutral diagnostics to expectations. |
Integration\PHPStan\PhpStanCommandRunner | Execute an explicit, boundary-preserving argument vector. |
Integration\PHPStan\PhpStanCommandVerifier | Run, decode, and verify an external PHPStan command. |
Integration\PHPStan\PhpStanExternalFixturePlanner | Project canonical PHP examples into direct analyzer fixtures. |
Integration\PHPStan\PhpStanJsonDecoder | Decode PHPStan 1.12/2.x JSON without loading PHPStan. |
Integration\PHPStan\PhpStanResultVerifier | Verify decoded per-file diagnostics without PHPUnit or PHPStan. |
AnalyzerDiagnostic, DiagnosticExpectation, DiagnosticAssignment, DiagnosticMatchResult,
DiagnosticMismatchKind, DiagnosticsMatched, DiagnosticsMismatched, PhpStanJsonResult, and
PhpStanVerificationResult form the public analyzer-independent result and matching model.
AnalyzerDiagnostic::$ignorable is nullable because diagnostics built outside the JSON decoder may not carry that
PHPStan-specific evidence. PhpStanVerificationResult partitions deterministic file results into typed matched and
mismatched maps while preserving analyzer-wide errors; isSuccessful() requires no global errors and no file
mismatches. DiagnosticExpectation can constrain an exact identifier, a message-or-tip substring, or both. Identifier
directives also carry the maintained span of their associated PHP statement, which constrains the diagnostic line;
legacy expectation source lines remain reporting metadata. An absent analyzer entry satisfies an explicit empty
expectation list, so this result alone does not prove that a clean file was analyzed. Direct consumers may use these
typed models with ExpectationParser, DiagnosticMatcher, PhpStanJsonDecoder, and PhpStanResultVerifier; the
VerifiesPhpStanExamples trait remains the supported integration path for PHPStan’s runtime objects.
PhpStanCommandResult and PhpStanCommandTermination form the framework-neutral process model. A completed result
always contains the raw exit status, including nonzero statuses; timeout, signal, and infrastructure failure carry only
the evidence valid for that termination. A signaled result requires a positive signal and permits either no exit status
or a nonzero platform-specific status; a successful zero status is rejected as contradictory. Standard output, standard
error, and nonnegative elapsed nanoseconds are preserved for every result.
PhpStanCommandRunner accepts an explicit project root, executable, argument list, and finite positive timeout, which
defaults to 60 seconds. It canonicalizes the root and executable with realpath(), inherits the caller’s environment,
and never constructs or interpolates a caller-controlled command string. Symfony Process may retry a failed direct POSIX
launch through an escaped shell command line without exposing that fact. A resulting 126 or 127 therefore remains a
raw completed status; unavailable paths, local instrumentation failures, and process failures surfaced as exceptions
become typed infrastructure evidence.
PhpStanCommandVerificationResult has three public variants. PhpStanCommandNotCompleted carries timeout, signal, or
infrastructure evidence. PhpStanCommandOutputRejected carries completed command evidence and the JSON decode failure.
PhpStanCommandVerified carries the completed command, decoded analyzer result, and diagnostic verification result; its
name means verification completed, not that expectations matched. PhpStanCommandVerifier validates expectations before
launching the command and returns one of these variants. It applies the same 60-second default timeout as the runner;
callers may provide another finite positive duration. Nonzero completed statuses remain evidence rather than an
automatic failure.
PhpStanExternalFixturePlan contains one canonical project root, a sorted nonempty list of project-relative .php
analysis paths, and exactly one platform-native canonical absolute expectation-map entry for each path.
PhpStanExternalFixturePlanner builds this model from examples selected by PhpStanExampleConfiguration. The corpus
and configuration must describe the same canonical project root. The planner accepts referenced whole files and named
regions, parses region expectations against the complete canonical PHP file while restricting directives and associated
statement spans to the selected region, groups aliases of each physical file when the filesystem reports a stable
device/inode identity, chooses one deterministic analysis path, and deduplicates overlapping expectations. It rejects
selected inline examples, missing selections, empty expectation markers, or canonical files changed since corpus
loading.
Exceptions
Source-loading failures, including malformed identity, runtime, reference, and named-region metadata, share
Source\Exception\SourceException; transformation failures share Transform\Exception\TransformException; execution
failures share Execution\Exception\ExecutionException; and PHPStan integration failures share
Integration\PHPStan\Exception\PhpStanException. Formatting configuration, execution, output, rewrite,
unsupported-input, and cleanup failures share Formatting\Exception\FormattingException. Synchronization structure,
resolution, and persistence failures share Synchronization\Exception\SynchronizationException, which is also a
source-exception subtype. The format --write CLI reuses SynchronizationWriter, so failures from its persistence
boundary retain the synchronization exception family even though the application reports them as formatting command
failures. Specific subclasses preserve distinctions such as missing paths, unsupported examples, runtime configuration,
empty PHPStan selection, and verification infrastructure.
These exception families and their documented leaf classes are public machine-readable failure categories. Consumers should catch the narrowest meaningful type or its family base instead of parsing exception messages.
Malformed inputs passed directly to the analyzer-independent PHPStan model, PhpStanResultVerifier, or
PhpStanCommandVerifier are programmer errors reported as \InvalidArgumentException, not PhpStanException
instances. The same applies to malformed command paths, argument vectors, and timeout values; supported operational
command failures are returned as PhpStanCommandResult or PhpStanCommandVerificationResult evidence instead of
exceptions.
PhpUnitRuntime::assertExample() can also raise PHPUnit’s ordinary expectation-failure or skipped-test control flow.
Optional Dependencies
Core discovery, the domain model, transformation, execution, extraction, and synchronization do not require PHPUnit,
PHPStan, or PHP-CS-Fixer to autoload. The Integration\PhpUnit namespace requires a compatible PHPUnit installation
when used. Command execution, JSON decoding, and framework-neutral result verification need neither optional dependency;
the caller supplies the executable it wants to run. PHPStan RuleTestCase verification requires both PHPUnit and
PHPStan because it reports through PHPUnit. Formatting checks require a compatible project-installed PHP-CS-Fixer
executable only when invoked; Akashi does not load its PHP classes.
See Compatibility and Safety for the targeted versions.
Compatibility and Safety
The wardens walked the whole circumference before admitting the procession, marking each broken hinge and hidden passage; the singers waited without complaint, for ceremony cannot restore a gate while passing through it.
— Acts of the Western Court 55:2
Akashi is a reusable documentation-example library for PHP projects. Its Markdown/PHPDoc, runtime, PHPUnit, and PHPStan workflows are usable outside its original consumers. The 0.2 release remains under active pre-1.0 development, so its public API may change between minor releases before 1.0.
Supported Platforms and Integrations
| Component | Current boundary |
|---|---|
| PHP | 8.1 and later |
| Composer | Runtime API 2.2 and later |
| Documentation | CommonMark PHP fences in Markdown/PHPDoc; PHPDoc references to canonical PHP files or regions |
| PHPUnit | Optional consumer integration supporting the PHPUnit 10.5 and 11.5 release lines |
| PHPStan | Optional integration supporting 1.12 with a PHP-Parser 4 pin, and 2.x by default |
| PHP-CS-Fixer | Optional executable adapter, tested with the repository’s installed 3.x development release |
| ParaTest | Development-only verified runner; not required by consumers |
| Operating systems | Linux is primary and gating; macOS and Windows have advisory PHP 8.2 CI |
Akashi’s core model, Markdown/PHPDoc discovery and extraction, transformation, execution, and CLI do not require PHPUnit, PHPStan, or PHP-CS-Fixer to autoload. Integration namespaces require the corresponding optional packages when used; the formatter adapter invokes the configured Composer binary proxy without loading PHP-CS-Fixer classes.
PHP 8.1 no longer receives upstream security fixes. Akashi verifies compatibility for maintained downstream runtimes and legacy development environments; this compatibility statement does not make an unpatched PHP 8.1 runtime suitable for a public-facing service.
Linux has two deliberately overlapping CI paths. A small conventional PHP 8.2 matrix uses setup-php, Composer, and the
locked vendor/ to run PHPUnit, PHPStan, and PHP-CS-Fixer independently of Nix. The exhaustive generated Nix matrix
repeats those three checks and adds PHP 8.1 through 8.5 runtime coverage, consumer fixtures, package and documentation
checks, both ParaTest modes, repository checks, and explicit mutation testing. On pushes to master, separate advisory
macOS and Windows jobs run Composer validation, PHPStan, PHPUnit, package validation, and a CLI smoke test.
Akashi develops against PHPUnit 11.5 on PHP 8.2 and later. Its Nix PHP 8.1 closure selects PHPUnit 10.5 and runs the
full suite. composer test:phpunit10 additionally builds the current Composer archive, installs it into an isolated
consumer project, and exercises runtime assertions, authored skips, both execution backends, and the PHPStan
RuleTestCase adapter on PHP 8.1. The packaged library rewrites a synchronized consumer document in memory, and the
packaged CLI checks that document against a canonical named region and exercises the optional formatter process boundary
through a consumer-provided executable.
Akashi develops and performs its normal static analysis with PHPStan 2.x. composer test:phpstan1 builds the current
Composer archive and verifies the same consumer integration independently with PHPStan 1.12, PHPUnit 10.5, and
PHP-Parser 4.19.5. That gate runs a real PHPStan 1.12 command through Akashi’s composed command verifier and checks its
structured result. Its analysis path and expectations come from an external canonical PHP file referenced through
PHPDoc. The normal unit suite covers the documented PHPStan 2.x JSON shape. PHPStan 1 consumers must explicitly require
nikic/php-parser:^4.19.5; PHPStan 2 consumers use the normal PHP-Parser 5 dependency resolution. PHPStan 2 projects
using composer update --prefer-lowest must explicitly require nikic/php-parser:^5.8 so the dual Akashi constraint
does not select Parser 4 for PHPStan 2’s process.
Authoring Boundary
- Markdown and PHPDoc fences plus PHPDoc references to external canonical PHP files and named regions are implemented. Strict synchronized presentations can be compared or corrected in memory through the library API and checked or atomically updated through the CLI. The filesystem writer rejects stale maintained bytes and symbolic-link paths.
- Optional PHP-CS-Fixer checks cover inline Markdown and PHPDoc examples without modifying maintained documents. The
library can apply checked mismatches to a validated immutable
Document;format --writerepeats the complete formatter pass before atomically updating current, nonsymlink documents. Referenced external PHP remains the responsibility of ordinary project formatter commands. The formatter process and any selected PHP-CS-Fixer configuration execute as trusted project tooling; this boundary is not a sandbox for untrusted configuration. - Every fence whose first info-string word is
phpenters the corpus. General language inference and “all code blocks” modes are not implemented. - PHPDoc extraction inspects every
T_DOC_COMMENTin selected.phpfiles. Only interior docblock lines participate; content beside/**or*/is not interpreted as Markdown, and symbol attachment is not exposed as model metadata. - Canonical example metadata uses comma-separated flags and
key=valueproperties in associated<!-- akashi: ... -->comments or token-aware// akashi: ...PHP comments. It coversexample,skip,compile-only,separate-process, typedexpect-exception, and optional message and integer-code constraints. Adjacent HTML and inline properties merge, but every property may occur at most once. Legacy one-property directives and one explicitly configured marker-comment dialect remain accepted for compatibility. - Exact
expect-outputmetadata compares captured stdout bytes after successful execution or a satisfied expected exception contract. Akashi does not normalize line endings, trim whitespace, or match patterns. Expected stderr is not implemented. - Global ignore, expected compilation failure, general expected runtime failure, platform conditions, custom skip reasons, and hidden support code are deferred.
- Expected exceptions match an available
Throwabletype and its subtypes. An optional message constraint uses a case-sensitive substring, and an optional signed base-10 integer code uses exact comparison. Both execution backends support this contract. A runtime string code, such as a PDO SQLSTATE, remains valid for type or message matching but cannot satisfy the integer code constraint.
A runtime-skipped or compile-only fence remains in the corpus and may still participate in PHPStan or extraction. Compile-only validates host-version PHP syntax but does not execute, bootstrap, transform, or select a backend. For a fragment that should enter no workflow, select a narrower document set or use another fence language.
In-Process Execution Model
In-process is the default because it avoids process startup, uses PHPUnit’s already-loaded project environment, and turns rewritten native assertions into ordinary PHPUnit assertions. Akashi gives declarations a generated namespace, evaluates code without caller local variables, captures output, and restores the working directory, error-reporting level, and output-buffer depth.
The validator rejects known persistent-state and relocation hazards, including direct process termination, authored
namespaces, global-variable statements, writes through $GLOBALS or superglobals, persistent handler, environment,
locale, INI, autoloader, and shutdown mutations, and ambiguous string reflection involving local declarations.
This is best-effort isolation for trusted project code. Resource exhaustion, native-extension crashes, dynamically
reached exit or die, filesystem and network effects, and other fatal or external behavior can escape the guard.
Separate-Process Boundary
Child execution protects the hosting PHPUnit process from ordinary parse errors, fatal behavior, signals, and nonzero
exits. It does not restrict the child’s operating-system permissions and is not a security sandbox. The child inherits
the parent environment, filesystem and network access, and uses the runner’s PHP_BINARY, a fixed Akashi INI profile,
and a fixed 60-second timeout.
Alternate PHP binaries, custom INI profiles, environment filtering, operating-system sandboxing, and configurable timeouts are deferred. Authored namespaces, closing tags, inline HTML, and relocation-sensitive constants require this backend and are rejected in-process rather than silently rerouted.
Assertion and Source-Location Boundary
In-process native assert() calls are rewritten and always evaluate their arguments. They are not affected by
zend.assertions. Separate-process assertions use child PHP’s enabled assertion exceptions. The supported call forms
and semantic differences are documented under Assertion Behavior.
Akashi keeps original example code separate from prepared code and retains line mappings through its implemented transforms. Parse, assertion, runtime, and PHPStan reports prefer a maintained Markdown, PHPDoc, or canonical external PHP source line when the underlying tool supplies a usable generated line. Referenced examples separately retain all PHPDoc presentation locations. When Akashi cannot establish an exact mapping, it reports the canonical example start explicitly; low-level metadata may still contain a temporary-file path.
An expected exception changes only the interpretation of a clean execution result. A matching authored exception passes;
normal completion, a mismatched type, optional message substring, or optional integer code, an unavailable or
non-Throwable type, and any cleanup failure fail. For child execution, Akashi records typed exception evidence through
a private file rather than scraping stderr. A nonzero exit, signal, timeout, startup failure, malformed evidence, or
other infrastructure failure remains a failure and cannot satisfy the expectation.
PHPStan Boundary
The preferred // @akashi-phpstan-error IDENTIFIER[: optional text] syntax matches the identifier exactly, optionally
matches message or tip text, and requires the diagnostic line to fall within the next PHP statement. Repeated directives
may target that statement. The legacy standalone //! syntax remains available for current consumer compatibility; it
matches mutable message and tip text across the example without constraining an identifier or statement line. Akashi
requires exact diagnostic counts and a deterministic one-to-one assignment for both forms.
PHPStan verification loads every relevant example into the hosting test process before analysis. Persistent declarations
cannot be unloaded, so preflight rejects collisions and built-in define(). Use one corpus-level verification test per
declaration set and provide only trusted, runtime-safe top-level code.
ParaTest and Platform Notes
In this repository’s PHP 8.2 Nix checks, ParaTest runs the full suite with two workers in both default TestCase-level
mode and --functional test-level mode. The gate covers consumer-shaped data sets, both runtime backends, and the
PHPStan RuleTestCase adapter. Each PHPStan corpus assertion still runs wholly inside one worker; do not split one
declaration set across test methods or repeat it in the same worker process.
Discovery rejects symlinked directory traversal and documents resolving outside the project root. Duplicate physical
files normally use device and inode identity. On platforms reporting inode 0, Akashi falls back to canonical paths, so
distinct hard-link aliases may not be recognized as duplicates.
Recorded Consumer Acceptance
Yumemi’s migration is complete. Its 43 current PHP fences run through Akashi as named PHPUnit data sets: 41 in-process
and two authored-namespace examples in child processes. Its real PHPStan rule and configuration verify 15 relevant
examples and eight authored //! expectations. The replacement gate passed before Yumemi removed its duplicated
Markdown discovery, runtime transform, diagnostic matcher, and unused extraction helpers.
Yumemi Apocrypha’s migration is also complete. Commit
f617093 changed all
eight marked-example consumer calls to vendor/bin/akashi, retained Akashi’s byte-equivalent extraction contract, and
removed the duplicated extractor and its tests. GitHub recorded 164 successful check runs for that commit, including its
normal and isolated consumer matrices.
These two migrations complete the recorded MVP consumer acceptance gates. Akashi’s public API and documented limitations have completed their initial classification review; the API may change between minor releases before 1.0.
Project
The moon entered a deep well as a silver coin, and three merchants lowered hooks to claim it. A child drank from her hands and scattered their prize into ripples. Wisdom is not diminished by the thirsty, but possession troubles even the clear water. Receive wonder with an open palm.
— Acts of the Western Court 17:42
This section explains Akashi itself rather than a consumer workflow.
- Architecture describes the system that exists now and the boundaries between its layers.
- Invariants records the durable behavior that refactors and replacement implementations must preserve.
- Roadmap records restrained future direction without advertising deferred behavior as available.
Compatibility evidence and the runtime trust model remain in the user-facing Compatibility and Safety reference.
Architecture
Set the cedar vessel beside the bronze and fill each from the same spring; for the feast requireth both fragrance and endurance, and wisdom appointeth unlike offices without making either ashamed.
— Ordinances of the Synthetic Dawn 53:1
This document describes the architecture implemented in the current repository. Historical sequencing, migration instructions, and clean-room records live outside the public mdBook and are not part of the runtime design.
The companion Invariants chapter separates durable behavioral contracts from the replaceable mechanics described here.
Akashi is a reusable library, not a standalone test runner. It discovers examples into a framework-independent model, then lets PHPUnit, PHPStan, or the extraction CLI consume that model through explicit adapters.
Example Lifecycle
Markdown / PHPDoc files
│
▼
DocumentationSource
├── inline PHP fences ───────────────────┐
└── PHPDoc references ─► PHP files/regions
│
▼
ExampleCorpus
│
┌────────────────────┬─────┼────────────────────┬───────────────┐
▼ ▼ ▼ ▼
PHPUnit runtime PHPStan verification inline formatting marked selection
transform → execute select → analyze PHP-CS-Fixer check extract CLI
│ │ │ │
▼ ▼ ▼ ▼
PHPUnit assertions PHPUnit assertions typed diffs stdout
The Example is the shared boundary. Consumers do not need to understand parser nodes, generated namespaces, temporary
files, Symfony Process, or PHPStan diagnostics to discover a corpus.
Source Discovery
DocumentationSource is an immutable manifest of an absolute project root, file and directory includes, exclusions, an
optional legacy marker name, and configured PHPDoc reference tags. Loading resolves and validates paths, rejects
symbolic-link directory traversal and duplicate physical documents, sorts documents deterministically, and dispatches
.md and .php documents by extension. MarkdownSource retains the same Markdown-only contract. Both accept bulk file
iterables, including SplFileInfo values from Symfony Finder, without depending on Finder.
The source manifests deliberately remain concrete configuration entry points rather than implementations of a public
source interface. Extension-based dispatch is sufficient for the current formats, while ExampleCorpus is the shared
boundary consumed by PHPUnit, PHPStan, and extraction.
The CommonMark adapter selects PHP fenced code blocks and associates canonical akashi: metadata plus an optional
legacy marker dialect using document structure rather than regular expressions over the whole file. One internal typed
grammar parser merges comma-separated flags and keyed properties from adjacent HTML comments and token-aware PHP line
comments. It recognizes stable example identity, skip, compile-only, separate-process, typed expect-exception,
optional message and code constraints, and exact expected stdout anywhere in fenced or referenced canonical code. It
rejects duplicate or conflicting declarations and prevents matching text in strings and heredocs. Original source text
and exact line and byte spans remain intact; the public model continues to expose separate typed identity, directives,
and runtime expectations rather than a generic metadata map.
The PHPDoc adapter locates every T_DOC_COMMENT with PHP’s tokenizer, projects each comment’s interior lines into
CommonMark by removing conventional docblock decoration, and extracts each comment independently. It then restores the
original PHP Document, line coordinates, and raw source spans. Independent parsing prevents metadata from crossing a
docblock boundary; file-wide ordinals keep generated inline identities deterministic.
The PHPDoc reference adapter recognizes @akashi-example by default, with an explicit replacement set configurable on
DocumentationSource. References resolve from the canonical project root to ordinary .php files or token-validated
named regions. The resolver rejects project-root escapes and malformed, nested, overlapping, mismatched, duplicate, or
empty regions. Repeated references and in-project filesystem aliases resolve to one canonical example while retaining
every PHPDoc presentation location. Referenced files do not have to be duplicated in the discovery include manifest.
A nonempty ordered ExampleCorpus is constructed only after cross-document marker uniqueness, reference resolution,
physical-source deduplication, and corpus ordering invariants hold.
Discovery is separate from selection. Canonical example metadata or a configured legacy marker adds an explicit
identity but does not hide unnamed fences; a PHPStan relevance predicate selects a subcorpus later; runtime skip and
compile-only change PHPUnit disposition without deleting the example.
Canonical Example Model
Document owns the project-relative path, maintained Markdown or PHP source bytes, and line index. Example owns:
- generated identity and human-readable label;
- an
InlineExampleSourceorReferencedExampleSource; - one canonical
CodeOrigincontaining maintained document, line, byte, and directive locations; - fence metadata for inline examples or an optional region and all PHPDoc
ReferenceLocationvalues for referenced examples; - normalized language;
- the unmodified extracted PHP source;
- document ordinal and optional author-assigned marker ID; and
- a typed set of runtime directives and an optional typed expected-exception contract.
Small value objects validate paths, identifiers, source coordinates, languages, and directives at construction time.
ExampleCorpus enforces nonemptiness, unique generated and marker IDs, and deterministic canonical path, source-line,
and stable-ID order.
Original example code remains separate from transformed code. This is necessary for diagnostics and prevents execution preparation from silently becoming the maintained representation.
Source Locations and Prepared Code
Each execution backend produces a backend-specific PreparedExample containing the original Example, generated
PreparedCode, its ExecutionMode, and a SourceMap. Prepared code and its map must contain the same number of lines.
The current map translates generated lines to original Markdown, PHPDoc, or external PHP lines. A transform describes each output line in terms of its input map, and Akashi composes that relation back to the maintained source. Synthetic lines remain explicitly unmapped. This lets sequential transforms preserve useful locations without depending on which source adapter produced the example, allowing assertion, parse, runtime, and PHPStan reporting to prefer a maintained source location. Failures fall back to the example start when PHP cannot provide a reliable generated line.
The model deliberately retains original locations rather than exposing only temporary files. Canonical external code origins and separate PHPDoc presentation locations are implemented. Mapping one generated artifact to several source origins, as future synchronization write diagnostics, hidden-code, formatter, or renderer work may require, remains deferred.
In-Process Transformation
InProcessTransformer is the fixed composition root for the default backend:
PhpExampleParsersupplies a valid PHP opening tag when absent and parses withnikic/php-parser.PhpNameResolverresolves names before relocation.InProcessSafetyValidatorrejects constructs that cannot be isolated soundly.NativeAssertionRewriterchanges nativeassert()calls into fully qualified PHPUnit-backed assertions while preserving authored expressions and source coordinates.NamespaceIsolatorplaces declarations in a generated execution namespace and produces prepared code plus a line map.
These mechanics are internal. Akashi has no public transform registry; a fixed order makes the safety and mapping contract reviewable. Unsupported constructs fail explicitly and recommend the authored separate-process directive instead of changing backend semantics implicitly.
Execution Backends
Both backends implement the typed Executor contract and return ExecutionSucceeded or ExecutionFailed. Results
retain the prepared example, captured streams, duration, failure phase, original cause, and cleanup failures. Execution
and cleanup failure are separate phases so a restoration problem cannot erase the first exception.
In process
InProcessExecutor evaluates prepared source through an empty closure scope. It optionally establishes a configured
project root and require_once bootstrap, captures output, catches Throwable, and restores guarded process state in
finally. Generated namespaces isolate declarations; the evaluation closure prevents ordinary top-level variables from
entering caller scope.
This backend is the default because it avoids child startup, shares PHPUnit’s autoloaded environment, and lets rewritten assertions count as ordinary PHPUnit assertions. Its protections are deliberately best-effort: PHP cannot recover from every fatal condition or external side effect.
Separate process
SeparateProcessTransformer preserves normal-file PHP semantics and its line map. SubprocessExecutor creates a
private authored file, invokes the current PHP binary with Symfony Process through an explicit argument vector without
constructing a shell command, applies the configured project root and optional bootstrap, captures both streams,
enforces the fixed emergency timeout, classifies child outcomes, and removes temporary files in finally.
For an expected exception, a private launcher catches Throwable around the unmodified authored file and writes
token-bound JSON evidence to a private side file. Base64 fields preserve arbitrary PHP strings, while the child records
type availability, subtype matching, and integer-or-string exception codes in the environment where the throwable
exists. The parent validates that evidence and maps its generated line without treating stdout or stderr as a protocol.
Nonzero exits take precedence; after a clean child exit, malformed changed evidence is an infrastructure failure.
The child protects PHPUnit from ordinary fatal process behavior. It is not an operating-system sandbox.
Verification and Integrations
VerifiesPhpUnitExamples is the ordinary consumer integration: its corpus and optional runtime-configuration hooks
compose a project-owned PHPUnit test class without an extension or mutable registry. It delegates named provider
arguments to PhpUnitExampleDataSets, which rejects duplicate labels before yielding. PhpUnitRuntime is the runtime
facade: it applies skip and compile-only disposition before mode precedence. Compile-only parses against the host PHP
version and records one assertion without transformation, bootstrap loading, or execution. Ordinary examples are
prepared and executed through the selected backend, then their result, optional expected throwable contract, and
optional exact stdout expectation go to PhpUnitResultAsserter. A compatible authored exception is success only when
execution has no cleanup failure, its message contains the optional case-sensitive substring, and its integer code
equals the optional code constraint. Normal completion and type, message, code, or stdout mismatches fail with
maintained source context. Output comparison occurs only after the execution or exception contract succeeds. Child
exits, signals, timeouts, and infrastructure failures remain failures. The adapter and facade remain public for projects
that need a custom PHPUnit method.
PHPStan follows a separate verification path over the same Example model. PhpStanExampleConfiguration selects a
relevant ordered subcorpus. The VerifiesPhpStanExamples trait parses identifier-oriented expectations associated with
the next PHP statement and legacy message-only expectations, writes private analysis files, preloads declarations, asks
RuleTestCase for diagnostics, translates lines, and gives framework-neutral diagnostics to DiagnosticMatcher.
Matching may constrain exact identifier, message-or-tip substring, and maintained statement span. It requires exact
counts and deterministic one-to-one assignments before PHPUnit receives the result.
External analysis has a narrower implemented seam. PhpStanJsonDecoder converts PHPStan 1.12 or 2.x JSON output into a
typed PhpStanJsonResult without loading PHPStan or PHPUnit. The result keeps analyzer-wide errors, per-file
association, counts, and available diagnostic evidence distinct instead of flattening them into the matcher model. The
decoder validates the documented structure and internal counts while ignoring unknown fields. PhpStanResultVerifier
then compares an explicit expectation map across the union of expected and reported paths through DiagnosticMatcher.
Its framework-neutral PhpStanVerificationResult separates successful file assignments, complete file mismatches, and
analyzer-wide errors, so expected verification failures remain data rather than exceptions. Command execution and exit
status interpretation remain separate from matching. PhpStanCommandRunner now executes an explicit,
boundary-preserving executable and argument vector from an explicit project root without constructing a command string.
Its typed result preserves normal exit status and streams without treating a nonzero status as an infrastructure
failure, while timeout, signal, path/setup failure, local instrumentation failure, and process failures surfaced as
exceptions remain distinguishable. PhpStanCommandVerifier validates expectations before launch and composes those
three stages into typed non-completion, output-rejection, or completed-verification outcomes. It does not treat a
nonzero analysis status as an automatic verification failure. PhpStanExternalFixturePlanner selects only referenced
canonical PHP examples, groups whole-file and named-region expectations by physical identity where the filesystem
reports one, validates that their loaded bytes are still current, and returns direct project-relative analysis paths
with platform-native canonical absolute expectation keys.
Symfony Console supplies declarative command definitions, generated help, command listing, shell completion, input and
output routing, and cross-platform terminal handling. Akashi wraps that replaceable router with exact command names,
single-occurrence options, stable statuses, and explicit stdout/stderr contracts. The extraction command loads one
Markdown or PHP file, selects one author-assigned example identity, and writes the original code with its documented
final-newline contract. An optional marker-name option adds a legacy marker-comment dialect. The command does not enter
either execution pipeline. --project-root supplies the reference-resolution boundary when the selected document is
below the project root; reference targets themselves are not marker IDs.
The synchronization layer recognizes an akashi-sync comment, one closed PHP fence, and an akashi-sync-end comment as
consecutive Markdown blocks; blank separator lines are allowed so normal Markdown formatters preserve a valid structure.
SynchronizationRegion retains the presentation document, exact raw region and code spans, logical undecorated code,
fence metadata, and canonical target. SynchronizationChecker resolves that target through the same project-containment
and named-region rules as PHPDoc references, normalizes only line endings and a missing final newline, and returns typed
mismatches with both presentation and canonical origins. The sync --check CLI loads explicit Markdown or PHP files
through the shared project-containment loader and reports those mismatches on stderr with stable process statuses and
source-labelled unified diffs from presentation to canonical code. Write mode renders and validates the complete
selected set before mutation, rechecks maintained and canonical snapshots, and then reports each changed path
deterministically. It rejects a selected whole-file canonical dependency when the selected canonical PHP document would
also change; named-region dependencies remain valid because presentation edits cannot overlap their tokenized executable
regions.
The same checker can apply all nonoverlapping mismatch edits to the original byte spans and return a new immutable
Document. It derives the presentation container prefix and line ending from the authored fence, changes only code
content, and re-parses and verifies the result before returning it. Unsafe canonical content that would terminate a
fence or PHPDoc comment is rejected against its original presentation line and canonical target. Verification uses the
already resolved canonical snapshot rather than rereading source files. This pure library operation performs no
filesystem writes. SynchronizationWriter supplies the separate persistence boundary: it rejects stale bytes and
symbolic-link paths, writes and flushes a temporary sibling, preserves permission bits, and atomically replaces one
document. Batch validation belongs to the CLI; individual replacements remain atomic even if a later filesystem failure
interrupts a multi-file write.
The formatting layer addresses only code physically embedded in Markdown or PHPDoc. FormattingChecker receives the
same corpus, skips referenced external sources, and invokes one project-installed PHP-CS-Fixer process per inline
example. It separates an authored opening tag from the checked body, prefixes a harmless declare plus an
entropy-backed boundary, and writes the resulting valid PHP to a private temporary directory. PHP-CS-Fixer runs through
an explicit argument vector without constructing a shell command, cache, or parallel workers and under a fixed timeout.
The checker reads the formatter-modified temporary file, verifies the boundary, discards project-level material inserted
before it, and returns typed FormattingMismatch values without writing maintained documentation. Process evidence
replaces the temporary filename with the original document location; cleanup executes on every outcome.
FormattingRewriter is the separate pure update boundary. It accepts the exact loaded Document plus checked
mismatches for that document, rejects stale, cross-document, duplicate, referenced, or structurally unsafe inputs, and
applies only their nonoverlapping code spans. It restores the existing Markdown or PHPDoc container prefix around each
formatter-produced logical line, preserves the formatter’s body line endings, then re-extracts the complete candidate
document to ensure every fence and runtime directive retains its meaning. The result is a new immutable Document; no
filesystem access occurs.
format --check supplies explicit safe document discovery and source-labelled unified diffs over that library seam.
format --write first renders every proposed document in memory, then reloads the complete source and repeats every
formatter invocation. Source bytes and formatter results must match the first pass before any maintained file changes.
Persistence reuses the stale-byte-protected, symbolic-link-rejecting atomic writer, so each file is replaced from a
flushed sibling while retaining its permission bits. External whole files and named regions remain directly
formatter-compatible PHP and are intentionally left to normal project formatter commands. There is no generic formatter
registry.
Dependency Boundaries
league/commonmark, nikic/php-parser, sebastian/diff, symfony/console, symfony/process, and the PHP 8.2 Random
extension polyfill support core implemented behavior. Parser output is normalized with PHP’s native PhpToken, keeping
source edits independent of the token class that differs between PHP-Parser 4 and 5. sebastian/diff supplies
unified-diff formatting without making PHPUnit a CLI dependency. Symfony Console supplies the replaceable CLI router and
presentation layer; Symfony Process supplies explicit child-process execution. The Random polyfill preserves the typed
Randomizer seam on PHP 8.1 and defers to PHP’s native extension on later runtimes. PHPUnit, PHPStan, and PHP-CS-Fixer
are optional Composer suggestions. PHP-CS-Fixer is invoked as a project executable and its classes are never loaded by
Akashi. PHPUnit and PHPStan runtime types are confined to integration namespaces so core source discovery and the CLI
can autoload without them.
There is no service container, mutable global registry, plugin registry, or implicit project configuration. Projects compose source, runtime, and verifier configuration through typed immutable values and ordinary PHPUnit test classes.
Current and Deferred Architecture
Current architecture supports Markdown and inline PHPDoc fences, PHPDoc references to canonical external PHP files and
named regions, synchronized-presentation inspection and in-memory rewriting through the library, check/write sync CLI,
optional PHP-CS-Fixer checks and validated in-memory formatting rewrites for inline examples, markers, token-aware
runtime directives, both execution backends, typed exception expectations, PHPUnit, identifier- and text-oriented
PHPStan expectations through RuleTestCase, typed PHPStan command execution and composed verification, JSON decoding
and standalone result verification, and marked extraction. External canonical PHP examples can also be projected into
direct PHPStan command fixtures without generated source. Hidden support code, documentation-renderer inclusion,
generalized verifier plugins, and a standalone Akashi test runner do not exist yet.
Those directions are recorded in the Roadmap. No placeholder interfaces or registries are created solely
for them. The existing separation between original Example, prepared source, execution results, and verifier
diagnostics is the seam preserved for future work.
Invariants
The margin ended where the final lamp stood, though darkness continued beyond it; measure confesseth its own frontier and therefore remaineth trustworthy within the light.
— Scholia of the Fifth Archive 53:20
This page records behavior that must survive refactoring or replacement of Akashi’s implementation. The Architecture explains how the current code works; this page states what every compatible implementation must preserve.
Severity describes the consequence of violating an invariant:
- critical: Akashi can silently test different code from the maintained documentation or report a false result;
- high: execution can escape its documented boundary, corrupt later tests, or break a supported consumer contract;
- medium: deterministic behavior, diagnostics, or a documented integration becomes unreliable.
Discovery and Identity
| Invariant | Why it exists | Enforcement evidence | Tempting invalid alternative | Severity |
|---|---|---|---|---|
| Includes and exclusions are resolved from one canonical project root, and loaded documents are ordered by slash-normalized project-relative path. | Corpus order and generated identities must not depend on the caller’s working directory or filesystem enumeration order. | MarkdownSourceTest, DocumentationSourceTest, ExampleCorpusTest | Preserve discovery order returned by the operating system. | high |
| Directory discovery does not traverse symbolic links, and one physical document cannot enter a corpus twice. | A configured tree must not escape its boundary or execute duplicate examples through aliases. | MarkdownSourceTest, DocumentationSourceTest | Follow every readable link and deduplicate only the authored spelling. | high |
Every selected fence whose first info-string word is php, case-insensitively, enters the corpus; loading an empty document or example set fails explicitly. | Discovery must not silently hide examples or turn a broken configuration into a green test suite. | CommonMarkExampleExtractorTest, PhpDocExampleExtractorTest, MarkdownSourceTest | Select only marked or conveniently executable fences. | critical |
| PHPDoc extraction inspects every documentation comment, parses only its interior lines, and never associates metadata across comment boundaries. | Declaration attachment and neighboring comments must not silently change which examples or metadata enter the corpus. | PhpDocExampleExtractorTest | Inspect only named declarations or concatenate all comments before parsing. | critical |
PHPDoc external references resolve only to readable .php files inside the canonical project root; repeated references and physical aliases produce one example while retaining all presentation sites. | Canonical code must not escape the selected project or execute more than once because it was documented more than once. | DocumentationSourceTest | Resolve relative to the working directory and execute every reference independently. | critical |
| Named-region markers are standalone tokenized PHP comments with matched unique names; malformed, orphaned, nested, mismatched, duplicate, and empty regions fail. | Guessing region boundaries can silently execute different bytes from the canonical example an author reviewed. | DocumentationSourceTest | Search raw text for the nearest marker or use fragile line-number ranges. | critical |
Generated example IDs are unique within a corpus; explicit example IDs are valid lowercase kebab-case and unique across canonical and configured legacy forms and source formats. | Reports and external extraction must identify exactly one example. | IdentifierTest, ExampleCorpusTest, CommonMarkMetadataTest, DocumentationSourceTest | Let the last duplicate identity win. | high |
| External metadata is associated through CommonMark adjacency; inline metadata is recognized only as PHP comment tokens inside its own code, and every property occurs at most once per example. | Prose, strings, heredocs, unrelated comments, and malformed or conflicting metadata must not change another example. | CommonMarkMetadataTest, PhpDocExampleExtractorTest, DocumentationSourceTest | Search backward or through raw source text with one file-wide regular expression. | high |
Source Fidelity and Transformation
| Invariant | Why it exists | Enforcement evidence | Tempting invalid alternative | Severity |
|---|---|---|---|---|
Example retains unmodified code, one canonical origin, and typed inline or referenced source metadata separately from generated source. | Consumers and diagnostics need the source the author actually maintains and, for references, every distinct presentation site. | CommonMarkExampleExtractorTest, PhpDocExampleExtractorTest, ExampleSourceTest, DocumentationSourceTest | Treat the nearest documentation comment as the maintained code location. | critical |
| Prepared code and its source map have equal line counts; a generated line maps to a maintained documentation line or explicitly has no exact mapping. | Failures must not report a plausible but incorrect location. | TransformValueTest, PhpUnitResultAsserterTest, RuntimeConformanceTest | Apply text edits without updating locations. | critical |
| In-process preparation uses the fixed parse → name-resolution → safety-validation → assertion-rewrite → namespace-isolation order. | Reordering changes name meaning, safety decisions, and diagnostic locations. | InProcessTransformerTest, NativeAssertionRewriteTest, InProcessSafetyValidatorTest | Expose independent transforms that callers may reorder. | critical |
| Unsupported relocation or process-state behavior is rejected explicitly; Akashi never silently changes an example’s requested backend. | A passing result under weaker or different semantics would be false evidence. | InProcessSafetyValidatorTest, PhpUnitRuntimeTest | Automatically reroute anything difficult or ignore unsafe syntax. | critical |
Supported in-process native assert() calls always execute and preserve the authored expression and optional message. | Documentation assertions must not disappear under zend.assertions=-1. | NativeAssertionRewriteTest, NativeAssertionTest | Delegate to native production assertion configuration. | high |
Execution
| Invariant | Why it exists | Enforcement evidence | Tempting invalid alternative | Severity |
|---|---|---|---|---|
| In-process is the default; an authored separate-process directive takes precedence and requires explicit runtime configuration. | Backend selection must be predictable and requested isolation must not be weakened. | PhpUnitRuntimeTest, RuntimeConformanceTest | Fall back to in-process when child configuration is missing. | high |
| Compile-only examples are parsed with maintained source locations but do not select a backend, transform, bootstrap, or execute code. | A non-running example must not gain side effects merely to establish syntax evidence. | PhpUnitRuntimeTest | Execute the example in a child and call that compile-only. | high |
| Expected stdout compares captured bytes exactly only after execution or the expected-exception contract succeeds; empty output remains an explicit valid expectation. | Output evidence must not be normalized, confused with absence, or allowed to mask a primary runtime failure. | PhpUnitResultAsserterTest, PhpUnitRuntimeTest | Trim output, normalize line endings, or report output mismatch before a wrong throwable. | high |
| In-process execution uses an empty local scope, captures output, and attempts to restore guarded working-directory, error-reporting, and output-buffer state even after failure. | One example must not accidentally inherit caller variables or poison later tests. | InProcessExecutorTest, InProcessStateGuardTest | Restore state only after successful execution. | high |
| Separate-process execution uses an explicit argument vector without constructing a shell command, private temporary files, the configured project root, and a finite timeout; cleanup runs on every outcome. Expected throwables use validated private evidence rather than stderr parsing. | Child execution must resist command injection, preserve trustworthy typed exception evidence, and avoid leaving source or evidence behind. | SubprocessExecutorTest, PhpUnitRuntimeTest | Build a shell command string, scrape stderr for a type, or use a predictable shared temporary file. | critical |
| Execution returns one typed success or failure variant, preserving the first cause, phase, captured streams, duration, mapped line, and any cleanup failures. | Reporting must not erase the primary failure or hide a damaged cleanup boundary. | ExecutionResultTest, PhpUnitResultAsserterTest | Throw away structured evidence and keep only one message. | high |
Neither backend is a security sandbox. The compatibility reference defines external effects and fatal conditions that remain outside these isolation invariants.
Verification, CLI, and Boundaries
| Invariant | Why it exists | Enforcement evidence | Tempting invalid alternative | Severity |
|---|---|---|---|---|
PHPStan expectations are parsed in authored order and match diagnostics by exact count plus a deterministic one-to-one assignment. Identifier expectations also require an exact identifier and a diagnostic line within the associated statement; legacy //! expectations retain example-wide substring matching. | Greedy, partial, identifier-blind, or location-blind matching can accept the wrong diagnostic set. | ExpectationParserTest, DiagnosticMatcherTest, PhpStanConformanceTest, VerifiesPhpStanExamplesTest | Accept any diagnostic containing each substring, reuse diagnostics, or ignore the statement that an identifier directive annotates. | critical |
| PHPStan verification validates the complete selected declaration set before loading it and restores process state and temporary files afterward. | A late collision or define() can fatally pollute the hosting test process. | VerifiesPhpStanExamplesTest | Require examples one at a time and discover conflicts after mutation. | high |
Successful extraction writes only the original example plus its final-LF contract to stdout; diagnostics use stderr; statuses remain 0, 1, 2, and 70. | Consumer scripts need byte-stable output and machine-stable outcome categories. | CliConformanceTest (public streams and statuses 0, 1, and 2), ApocryphaCompatibilityTest (consumer-compatible extraction bytes), ExitCodeTest (all status values, including 70) | Mix progress text with extracted PHP or infer failure categories from prose. | critical |
| Synchronization write mode validates the complete selected set before mutation, refuses stale maintained bytes and symbolic-link paths, rejects changing selected whole-file canonical dependencies, and atomically replaces each changed document from a flushed sibling. | A partial document, overwrite of newer/unintended bytes, or replacement calculated from a canonical file that the same batch changes would corrupt or immediately stale the maintained documentation. | ApplicationTest, SynchronizationWriterTest | Resolve selected aliases after losing their spelling, truncate files in place, continue from a stale snapshot, or compute every replacement from the initial batch state. | critical |
| Formatter checking sends only inline examples through an argument-vector PHP-CS-Fixer process, verifies its protected body boundary, maps evidence to maintained source, and cleans private temporary input on every outcome. Its pure rewriter accepts only current mismatches for one document, changes exact code spans, and re-extracts the candidate before returning it. Check mode never writes; write mode repeats every formatter before using stale-byte-protected, symbolic-link-rejecting atomic replacement. | Project header rules, malformed formatter output, shell interpolation, leaked temporary source, nondeterministic formatting, or stale/cross-document replacements must not corrupt a fence or hide which documentation failed. | FormattingCheckerTest, FormattingRewriterTest, ApplicationTest, CliConformanceTest, composer test:phpunit10 | Run a shell command over the documentation file itself, trust every byte returned around the example body, or write a stale document without structural validation. | high |
| PHPUnit 10.5/11.5 and PHPStan 1.12/2.x remain optional integration dependencies, and every Akashi declaration is explicitly public or internal without exposing internal types through public signatures. | Core discovery and the CLI must autoload independently, while compatibility obligations remain reviewable. | PublicApiBoundaryTest, PackageMetadataTest, composer test:phpunit10, composer test:phpstan1 | Let autoloadable classes become public accidentally or leak integration types into core. | high |
Consumer-specific snapshots under tests/Fixtures/Compatibility supplement these invariants. They record real
acceptance evidence, while tests/Conformance exercises the supported public boundaries without depending on another
checkout.
Roadmap
A wheel of violet fire descended behind the cedar ridge, and every abandoned milestone spoke the name of a kingdom that would not be founded for seven generations.
— Revelation of the Artificial Sun 33:21
This roadmap records direction, not release-number commitments. The current Markdown and inline PHPDoc workflows are usable without the features below. Both recorded consumer migrations are complete; the immediate project work is stabilization of the documented pre-1.0 API.
Markdown MVP Acceptance
All recorded MVP acceptance gates are complete:
- Yumemi executes all 43 current PHP fences through Akashi, using child processes for two authored-namespace examples.
- Yumemi verifies all 15 relevant PHPStan examples and eight authored expectations through Akashi.
- Yumemi removed its duplicated documentation-test helpers after its replacement and complete project gates passed.
- Akashi produces byte-identical output for all eight marked Yumemi Apocrypha fixtures in a self-contained compatibility gate.
- Yumemi Apocrypha invokes
vendor/bin/akashifor all eight marked consumer fixtures and removed its duplicated extractor after its complete GitHub Actions matrix passed. - ParaTest compatibility is covered in both TestCase-level and
--functionaltest-level scheduling.
The initial API review classified every autoloadable declaration as an entry point, canonical model type, analyzer-independent PHPStan diagnostic type, public exception, or explicit internal detail. The supported surface may change between minor releases before 1.0, but architecture tests prevent accidental autoloadability from becoming API.
Canonical example metadata now unifies stable identity, runtime flags, and expected-exception values under one small grammar in Markdown, PHPDoc, and referenced canonical PHP. Legacy marker comments and one-property directives remain supported compatibility forms rather than a second internal model.
Source Discovery Ergonomics
The immutable source manifests now provide a bulk file include equivalent to:
/** @param iterable<ProjectPath|string|\SplFileInfo> $paths */
public function includeFiles(iterable $paths): self;
It applies existing file validation to arrays and iterators of project-relative paths. It also accepts SplFileInfo, so
Symfony Finder results can be passed directly while Akashi remains dependency-neutral. The built-in recursive directory
selection remains available for projects that do not need an external finder.
PHPDoc Example Maintainability
PHPDoc support is being delivered through three progressively more maintainable authoring modes:
- short inline PHPDoc fences for local demonstrations — implemented;
- references to ordinary external PHP files or stable named regions, with the external file as source of truth — implemented; and
- optional synchronized inline copies for renderers that cannot include external content — parsing, comparison, in-memory rewriting, and atomic CLI persistence implemented.
Referenced canonical examples are preferred for substantial code because IDEs, formatters, PHPStan, and PHP can operate on them directly. Named regions are preferred over fragile line-number ranges.
The synchronization library parses strictly delimited presentations in Markdown and PHPDoc, shares canonical path and
named-region validation with external references, returns typed mismatches, and can render a corrected immutable
document without changing the filesystem. Rewriting preserves surrounding authored bytes and validates the completed
document before returning it. The sync --check CLI applies check-only behavior to explicit files with stable
diagnostics, source-labelled unified diffs, and process statuses; sync --write validates the full input set and
atomically replaces each changed document with stale-byte protection. Optional PHP-CS-Fixer integration now extracts
inline examples into private valid PHP files, preserves their authored opening tags, ignores file-level formatter
additions outside a protected body boundary, and reports source-labelled diffs in check mode. Checked mismatches can be
applied to their exact inline code spans through a validated in-memory rewrite that preserves surrounding
Markdown/PHPDoc bytes and performs no filesystem access. Write mode validates a second complete formatter pass before
using stale-byte-protected atomic replacement. External canonical PHP remains the normal formatter-friendly mode. The
remaining suggested sequence is:
- Hidden support-code semantics.
- Documentation-renderer integrations.
Generated-line mappings now compose across sequential transformations while retaining Markdown, PHPDoc, whole-file, and named-region origins. Future features that combine several maintained origins will need a richer mapping model, but the current pipeline no longer requires each transform to reconstruct the original map itself.
No hidden-line syntax is selected. Any future design should remain explicit and compatible with PHP parsers, formatters, IDEs, renderers, and static analyzers. Akashi integrates with configured formatters rather than becoming a PHP formatter. The checker and pure rewriter remain independent of the CLI persistence boundary.
Runtime and Verification
Runtime skip is implemented through PHPUnit’s skipped-test reporting. Compile-only examples receive source-aware syntax validation and one PHPUnit assertion without bootstrap loading, transformation, or execution. A typed PHPUnit-familiar exception-class expectation, optional case-sensitive message substring, and optional exact integer code are implemented for both execution backends. Exact expected stdout is also implemented for successful and expected-exception examples, without trimming or line-ending normalization. Deferred extensions include broader expected-failure semantics, expected stderr, global ignore and conditional skip policies with reasons, platform conditions, configurable subprocess timeouts, alternate PHP binaries and INI profiles, and controlled child environments.
PHPStan’s identifier-oriented expectation syntax now coexists with legacy //! text expectations. Framework-neutral
command verification and external canonical fixture planning are also implemented. A future mapping model may need to
associate one diagnostic with multiple maintained origins.
External PHPStan Verification
Consumer repositories sometimes construct disposable Composer projects and run PHPStan against installed packages. Akashi is incrementally replacing their shell-level diagnostic parsing without taking ownership of package installation or compatibility-matrix orchestration.
The sequence is:
- Implemented: decode PHPStan 1.12 and 2.x JSON into a typed result without discarding top-level analyzer errors, file association, or available diagnostic evidence.
- Implemented: compare decoded per-file diagnostics with an explicit expectation map and return typed successful
matches, complete mismatches, and analyzer-wide errors independently of PHPUnit and
RuleTestCase. - Implemented: execute an explicit, boundary-preserving executable and argument vector from an explicit project root without constructing a command string, preserving exit status, standard streams, elapsed time, timeout, signal, and infrastructure failures as typed evidence.
- Implemented: compose command completion, JSON decoding, and expectation verification while keeping non-completion, malformed analyzer output, raw analysis exit status, and completed diagnostic verification distinct.
- Implemented: run the isolated PHPStan 1.12 consumer fixture’s real analyzer command through the composed verifier and check its structured result at the package boundary.
- Implemented: project selected external canonical PHP examples and named regions into direct analysis paths and grouped expectation maps while keeping disposable-project orchestration in the consumer.
The existing DiagnosticMatcher and DiagnosticMatchResult types remain the lower-level matching contract.
PhpStanJsonDecoder and PhpStanJsonResult establish the decoder boundary; PhpStanResultVerifier and
PhpStanVerificationResult establish the framework-neutral verification boundary. PhpStanCommandVerifier and its
three result variants establish the composed external-command boundary. PhpStanExternalFixturePlanner bridges the
canonical example corpus to that boundary without generating source or invoking PHPStan. Akashi will not construct
temporary Composer projects, add repositories, install dependencies, inspect packages, define another project’s
compatibility matrix, or run package-specific runtime assertions. Those responsibilities remain with the consumer
repository.
A standalone Akashi test runner, report formats, and broader plugin seams should follow concrete consumer demand. Akashi will not add registries or speculative interfaces merely to anticipate them.
Comparative Review
After the MVP architecture and public API are implemented and recorded, the owner may request a separate, documentation-only comparison with competing PHP doctest projects. That review is not part of current implementation, must record every external document consulted, and must not inspect implementation code or silently reshape Akashi’s foundational APIs to match another project.