← CLI

CLI FEATURE

Project Graph Introspection

A mature framework should explain itself under pressure. These commands turn route and container introspection into a first-class debugging surface.

Feature Guide

A quick orientation block that answers the essential questions: what this feature does, how it works, why it matters, and the key concepts behind it.

What this does

The CLI can describe the framework graph directly: routes, modules, bindings, and handler invariants are queryable artifacts.

How it works

ai:ask route (backed by dev:graph:route) renders the payload-to-template chain, ai:ask project summarizes modules and listeners, routes:list inventories discovered endpoints, and contracts:list exposes DI bindings.

Why it matters

This shortens debugging and onboarding dramatically. Instead of reconstructing framework state by reading scattered attributes and registrations, you ask the system to explain itself.

Key concepts

ai:ask route
Explains one route from payload through handlers, resource, template, and auth posture.
ai:ask project
Summarizes modules, routes, listeners, and structural counts for the current project.
contracts:list
Shows which implementation is active for each registered service contract.

Explain The System

Use the CLI to inspect the framework graph instead of guessing

These commands turn route discovery, module structure, DI binding, and handler validation into explicit artifacts. That matters for both humans and AI operators when the codebase is large.

Route chain visibility. ai:ask route (backed by dev:graph:route) shows payload, handlers, resource, template, and auth posture for one endpoint.

Project-level map. ai:ask project and routes:list expose modules, counts, and discovered request surfaces.

Binding and rule checks. contracts:list and lint:* help debug DI bindings and architectural invariants before runtime incidents.

Command Purpose Why it matters
bin/semitexa ai:ask route --path=/demo/api/schema-discovery --json Explain the full execution chain for one route. Ideal when a page behaves unexpectedly and you need the exact payload → handler → resource path.
bin/semitexa ai:ask project --json Emit a high-level project overview with modules, routes, and listeners. Useful for onboarding, architecture review, and AI navigation of unfamiliar projects.
bin/semitexa routes:list --json List all discovered routes with source metadata. Gives a stable route inventory instead of relying on tribal knowledge.
bin/semitexa contracts:list --json Show service contracts and their active implementation. Shortens DI debugging when multiple modules can satisfy the same interface.
bin/semitexa lint:handlers Validate handler signatures, bindings, and return types. Catches architecture drift before it becomes a runtime failure.

Inspect one route end to end

bin/semitexa ai:ask route --path=/demo/rendering/reactive-ai --method=GET
bin/semitexa ai:ask route --path=/demo/rendering/reactive-ai --json

Map the project and routes

bin/semitexa ai:ask project --json
bin/semitexa routes:list --json

Check DI and handler invariants

bin/semitexa contracts:list --json
bin/semitexa lint:handlers
ai:ask dev:graph:route dev:graph:project dev:graph:module dev:graph:event routes:list contracts:list lint:*

Verified against Semitexa Ultimate 2026.09.19.1020

Project Graph Introspection

A mature framework should explain itself under pressure. These commands turn route and container introspection into a first-class debugging surface.

How it works

ai:ask route --path=/… (backed by dev:graph:route) shows the full execution chain for one endpoint — payload, handlers, resource, template, and auth posture. ai:ask project and routes:list expose the module-level structure and all discovered request surfaces. ai:ask module --name=… drills into a single module. contracts:list and lint:* help validate DI bindings and architectural invariants before runtime incidents.

Why this matters

The biggest gain is not convenience. It is shortening the distance between "something feels wrong" and "here is the exact part of the system that explains it." Both human engineers and AI operators benefit from a framework that can describe its own graph instead of requiring manual reconstruction.

© Linus Torvalds: "Talk is cheap. Show me the code."

ai:ask Command Implementation slice
<?phpdeclare(strict_types=1);namespace Semitexa\Dev\Application\Console\Command;use Semitexa\Core\Attribute\AsCommand;use Semitexa\Core\Console\BaseCommand;use Semitexa\Dev\Application\Service\Console\CommandDelegator;use Symfony\Component\Console\Input\InputArgument;use Symfony\Component\Console\Input\InputInterface;use Symfony\Component\Console\Input\InputOption;use Symfony\Component\Console\Output\OutputInterface;/** * Agent-facing aggregator for read-only introspection. One entry point, one * argument (the "subject"), forwarded to the underlying command that owns * the implementation. * *   ai:ask capabilities [--json] *   ai:ask project      [--json] *   ai:ask module       --name=Billing [--json] *   ai:ask route        --path=/billing/{id} [--method=GET] [--json] *   ai:ask event        [--name=InvoicePaid] [--json] *   ai:ask logs         [--file=app] [--lines=200] [--grep=…] [--json] * * Output contracts (envelope shapes, JSON keys, exit codes) belong to the * dispatched targets — `ai:ask` only routes; the dev:graph:* / logs:app * commands own behavior. */#[AsCommand(name: 'ai:ask', description: 'Agent-facing introspection aggregator (capabilities, mechanisms, project, module, route, event, path, logs)')]final class AiAskCommand extends BaseCommand{    /**     * Subject → underlying command. Keeping the list short is the point: if     * a new read-only surface is needed, add it here rather than spawning     * another top-level command.     *     * @var array<string, string>     */    private const SUBJECT_MAP = [        'capabilities' => 'dev:graph:capabilities',        // Distinct from `capabilities` on purpose: that one lists the CLI        // commands available to run, this one lists what the installed        // FRAMEWORK can do (deferred regions, components, live transport).        // Both answer "what can I do here"; merging them buries the handful of        // mechanisms under a wall of command help.        'mechanisms'   => 'dev:graph:mechanisms',        'project'      => 'dev:graph:project',        'module'       => 'dev:graph:module',        'route'        => 'dev:graph:route',        'event'        => 'dev:graph:event',        'logs'         => 'logs:app',        // `path` explains a file/directory path using global module-structure        // rules + any package-local extension. Auto-selected when --path is        // passed without an explicit subject (see execute()).        'path'         => 'dev:graph:path',    ];    public function __construct()    {        parent::__construct('ai:ask');    }    protected function configure(): void    {        $this            // Subject is now OPTIONAL: when omitted, the command auto-selects            // `path` if --path is provided (lets `ai:ask --path=…` work            // without naming a subject). Existing subjects keep their full            // routing behavior.            ->addArgument('subject', InputArgument::OPTIONAL, 'One of: ' . implode(', ', array_keys(self::SUBJECT_MAP)) . ' (omit if --path is provided to auto-select `path`)')            // Union of options accepted by the delegated targets. Only the            // ones the target actually declares are forwarded (see            // CommandDelegator). Anything else is silently dropped.            ->addOption('name', null, InputOption::VALUE_REQUIRED, 'Module/event name (for subject=module|event)')            ->addOption('path', null, InputOption::VALUE_REQUIRED, 'Path: route path (subject=route) | file/directory path (subject=path, default when --path used)')            ->addOption('method', null, InputOption::VALUE_REQUIRED, 'HTTP method (for subject=route)')            ->addOption('file', null, InputOption::VALUE_REQUIRED, 'Log file alias (for subject=logs)')            ->addOption('lines', null, InputOption::VALUE_REQUIRED, 'Line count (for subject=logs)')            ->addOption('grep', null, InputOption::VALUE_REQUIRED, 'Filter (for subject=logs)')            ->addOption('level', null, InputOption::VALUE_REQUIRED, 'Log level filter (for subject=logs)')            ->addOption('since', null, InputOption::VALUE_REQUIRED, 'Time window (for subject=logs)')            ->addOption('around', null, InputOption::VALUE_REQUIRED, 'Context timestamp (for subject=logs)')            ->addOption('context', null, InputOption::VALUE_REQUIRED, 'Context radius (for subject=logs)')            ->addOption('list', null, InputOption::VALUE_NONE, 'List available logs (for subject=logs)')            ->addOption('id', null, InputOption::VALUE_REQUIRED, 'Capability id (for subject=mechanisms)')            ->addOption('area', null, InputOption::VALUE_REQUIRED, 'Capability area prefix, e.g. ssr|ui (for subject=mechanisms)')            ->addOption('json', null, InputOption::VALUE_NONE, 'Emit JSON envelope (target-dependent shape)');    }    protected function execute(InputInterface $input, OutputInterface $output): int    {        $subject = (string) ($input->getArgument('subject') ?? '');        // When subject is omitted but --path is provided, default to        // `path`. Keeps `ai:ask --path=foo` ergonomic for AI agents while        // preserving the explicit-subject flow for everyone else.        if ($subject === '' && (string) ($input->getOption('path') ?? '') !== '') {            $subject = 'path';        }        if ($subject === '') {            $output->writeln(json_encode([                'kind'     => 'error',                'error'    => 'missing subject (and no --path provided to auto-select)',                'subjects' => array_keys(self::SUBJECT_MAP),            ], JSON_UNESCAPED_SLASHES));            return self::FAILURE;        }        $target = self::SUBJECT_MAP[$subject] ?? null;        if ($target === null) {            $output->writeln(json_encode([                'kind'     => 'error',                'error'    => "unknown subject '{$subject}'",                'subjects' => array_keys(self::SUBJECT_MAP),            ], JSON_UNESCAPED_SLASHES));            return self::FAILURE;        }        $app = $this->getApplication();        if ($app === null) {            $output->writeln(json_encode([                'kind'  => 'error',                'error' => 'Application not available — cannot dispatch subject',            ], JSON_UNESCAPED_SLASHES));            return self::FAILURE;        }        return CommandDelegator::run($app, $target, $input, $output);    }}

Operational Payoff

Where these commands save real time

The biggest gain is not convenience. It is shortening the distance between “something feels wrong” and “here is the exact part of the system that explains it.”

Reach for ai:ask route before you start manually tracing attributes across payloads, handlers, and resources.

Use routes:list and ai:ask project to orient both humans and agents in larger installations or modular monorepos.

Use contracts:list when interface resolution is ambiguous, especially in module override scenarios.

Run lints as architectural guardrails, not only as a last-minute CI formality.

How it works

ai:ask route (backed by dev:graph:route) renders the payload-to-template chain, ai:ask project summarizes modules and listeners, routes:list inventories discovered endpoints, and contracts:list exposes DI bindings.

Why it matters

This shortens debugging and onboarding dramatically. Instead of reconstructing framework state by reading scattered attributes and registrations, you ask the system to explain itself.

Key concepts

ai:ask route
Explains one route from payload through handlers, resource, template, and auth posture.
ai:ask project
Summarizes modules, routes, listeners, and structural counts for the current project.
contracts:list
Shows which implementation is active for each registered service contract.

Support Semitexa
Built for developers who prefer control over magic. Your support helps keep it fast, open, and evolving.

Donate via PayPal