PHP Language Analyzer
Contract ID:
CONTRACT-SCANNER-PHP-ANALYZER-013Status: Published · Version: 1.0.0 · Published: 2025-12-05 Owners: PHP Analyzer Guild, Scanner Guild
Reconciliation note (2026-05-30): This contract was originally drafted as a bootstrap design. The PHP analyzer has since shipped, and several sections described aspirational shapes that differ from the implementation. The document has been reconciled against the source at
src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Php/. Sections that describe behaviour not yet present in source are marked (Roadmap — not yet implemented). The C# type signatures below mirror the actualinternaltypes unless explicitly flagged as illustrative.
Overview
This contract defines the Stella Ops PHP language analyzer — the Scanner component that inventories a PHP project’s dependencies and attack surface. It covers Composer manifest/lock parsing, virtual file system (VFS) normalization, framework/CMS fingerprinting, runtime capability and PHAR/FFI scanning, and the language-component records the analyzer emits for deterministic, offline PHP project analysis.
Audience: engineers maintaining or extending the PHP analyzer, and Scanner contributors consuming its language-component output downstream (emit, SBOM serialization, reachability).
The implemented entry point is PhpLanguageAnalyzer (analyzer id php, display name PHP Analyzer), which implements IScannerLanguageAnalyzer and is activated through the PhpScannerAnalyzerPlugin plug-in (IScannerLanguageAnalyzerPlugin).
Scope
The PHP analyzer:
- Parses
composer.json(PhpComposerManifestReader) andcomposer.lock(ComposerLockReader) - Builds an in-memory virtual file system from the scan root, vendor directory, and configs (
PhpInputNormalizer) - Detects framework/CMS fingerprints (Laravel, Symfony, WordPress, Drupal, and 13 others) via
PhpFrameworkFingerprinter - Emits language-component records with composer PURLs plus dotted metadata keys
- Scans for autoload edges, include/require graphs, runtime capabilities, PHAR archives, FFI usage, framework surface, PHP config/extensions, and version conflicts
Note on output format: The analyzer emits Stella Ops language-component records (consumed by the Scanner emit pipeline), not CycloneDX SBOM documents directly. See Output Schema. CycloneDX/SPDX serialization is a downstream concern of
StellaOps.Scanner.Emit.
Input Normalization
PhpInputNormalizer.NormalizeAsync(rootPath, ct) builds a PhpProjectInput from the scan root. It does not merge fixed mount points such as /app, /vendor, or /etc/php*; it recursively enumerates the supplied rootPath.
File Discovery (implemented)
The normalizer enumerates the root tree and classifies files by source into a PhpVirtualFileSystem. There is no IPhpSourceDiscovery interface or PhpDiscoveryOptions record in source. Discovery behaviour is fixed:
- PHP source extensions collected:
.php,.phtml,.inc,.module(thevendor/tree is enumerated separately and collects only.phpfiles plusvendor/composer/installed.json). - Excluded directories:
.git,.svn,.hg,node_modules,.idea,.vscode. - Config files collected:
.htaccess,php.ini(andphp/php.ini,etc/php.ini),conf.d/*.inivariants, and PHP-FPM*.conffiles. - Composer manifests collected:
composer.json,composer.lock.
Each collected file is tagged with a PhpFileSource (see File Sources).
(Roadmap — not yet implemented) A configurable discovery surface (include/exclude globs, optional vendor/test inclusion) is not present. The previously documented
IPhpSourceDiscovery/PhpDiscoveryOptionsAPI and the default exclude patterns (*.min.php,cache/*) do not exist in source.
File Sources
internal enum PhpFileSource
{
SourceTree, // application code
Vendor, // composer vendor directory
ContainerLayer, // OCI/Docker layer (enum value present; not yet populated)
PhpConfig, // php.ini, conf.d
WebServerConfig, // .htaccess
FpmConfig, // php-fpm.conf / pool configs
ComposerManifest // composer.json / composer.lock
}
Composer Schema
composer.json Parsing
PhpComposerManifestReader.LoadAsync parses composer.json into a PhpComposerManifest with: Name, Description, Type, Version, License, Authors, Require, RequireDev, Autoload, AutoloadDev, Scripts, Bin, MinimumStability, and Sha256 (SHA-256 of the manifest file). Computed views include RequiredPhpVersion (the php constraint in require) and RequiredExtensions (the ext-* keys).
{
"name": "vendor/package",
"version": "1.2.3",
"type": "library|project|metapackage|composer-plugin",
"require": {
"php": ">=8.1",
"vendor/dependency": "^2.0"
},
"require-dev": { },
"autoload": {
"psr-4": { "App\\": "src/" },
"classmap": ["database/"],
"files": ["helpers.php"]
}
}
composer.lock Parsing
ComposerLockReader extracts the lockfile-level content-hash and plugin-api-version, plus per-package metadata into ComposerPackage records. The lock data carries a LockSha256 (SHA-256 of the composer.lock file itself, lowercase hex), used as the package evidence hash.
internal sealed record ComposerPackage(
string Name,
string Version,
string? Type, // library, composer-plugin, etc.
bool IsDev, // from "packages-dev"
string? SourceType, // source.type: git|hg|svn
string? SourceReference, // source.reference (commit/tag)
string? DistSha256, // dist.shasum (falls back to dist.checksum)
string? DistUrl, // dist.url
ComposerAutoloadData Autoload);
Field reconciliation: The lockfile
content-hashis a composer integrity hash over the root requirements, not a SHA-256 of package contents. The per-packagedist.shasum(exposed asDistSha256) is the package archive checksum. The previously documentedComposerLockPackagerecord (with a singleContentHash = "SHA256 of package contents") did not match either field and has been replaced with the actualComposerPackageshape above.
ComposerLockData holds: LockPath, ContentHash, PluginApiVersion, Packages, DevPackages, LockSha256.
Autoload Data
internal sealed class ComposerAutoloadData
{
IReadOnlyList<string> Psr4; // entries serialized as "Namespace\->path"
IReadOnlyList<string> Classmap;
IReadOnlyList<string> Files;
}
Note: PSR-0 is not parsed by the lock reader (only
psr-4,classmap,files). The autoload graph builder (PhpAutoloadGraphBuilder) does emit aautoload.psr0_countmetadata key, sourced separately frominstalled.json.
PURL Format
PhpPackage.Purl produces:
pkg:composer/vendor/package@version
pkg:composer/laravel/framework@10.48.7
pkg:composer/symfony/http-kernel@6.3.0
The component key for a package is purl::pkg:composer/<vendor>/<package>@<version>.
Virtual File System
Model (implemented)
The VFS is the internal type PhpVirtualFileSystem, built via a Builder. It is not a public record and does not carry a ContentHash of its own.
internal sealed class PhpVirtualFileSystem
{
IReadOnlyList<PhpVirtualFile> Files { get; } // deterministically ordered
int Count { get; }
bool TryGetFile(string relativePath, out PhpVirtualFile? file);
IEnumerable<PhpVirtualFile> GetFilesBySource(PhpFileSource source);
IEnumerable<PhpVirtualFile> GetFilesByPattern(string pattern); // simple glob
IEnumerable<PhpVirtualFile> GetPhpFiles();
}
internal sealed record PhpVirtualFile
{
string RelativePath { get; } // normalized to forward slashes
string AbsolutePath { get; }
PhpFileSource Source { get; }
string? Sha256 { get; init; } // optional; not populated during normalization
long? Size { get; init; } // optional
IReadOnlyDictionary<string, string>? Metadata { get; init; }
}
Reconciliation: The previously documented public
PhpVirtualFileSystemrecord (withRootPath,Entries,PhpConfig,Composer, BLAKE3ContentHash), theVfsEntryrecord, and theVfsEntryTypeenum do not exist in source. There is no BLAKE3 hashing; per-file hashing uses SHA-256 and is optional. The project-level container isPhpProjectInput(RootPath, FileSystem, Config, Framework, ComposerLock, ComposerManifest).
Deterministic Ordering
PhpVirtualFileSystem.Builder.Build() sorts files by RelativePath using StringComparer.Ordinal (case-sensitive, lexicographic). Duplicate relative paths are de-duplicated case-insensitively, with the last AddFile winning.
Framework Detection
Fingerprint Rules (implemented)
PhpFrameworkFingerprinter ships 17 framework/CMS definitions. Each definition scores by the fraction of its file markers that exist on disk (with a small boost when 3+ markers match); the highest-scoring candidate wins. When no on-disk match is found, DetectFromPackages falls back to composer package markers (fixed confidence 0.95). Confidence is a float in [0.0, 1.0], not a High/Medium/Low enum — there is no ConfidenceLevel type.
| Framework | Selected file markers (any-of, scored) | Composer marker |
|---|---|---|
| Laravel | artisan, bootstrap/app.php, config/app.php, routes/web.php, app/Http/Kernel.php | laravel/framework |
| Symfony | bin/console, config/bundles.php, src/Kernel.php, symfony.lock | symfony/framework-bundle |
| WordPress | wp-config.php, wp-includes/version.php, wp-admin/admin.php, wp-content/themes, wp-content/plugins | — |
| Drupal | core/lib/Drupal.php, sites/default/settings.php, core/modules, modules/contrib | drupal/core |
| Joomla | configuration.php, libraries/src/Version.php, administrator/index.php, components | — |
| Magento | app/Mage.php, app/etc/config.php, app/etc/env.php, pub/index.php | magento/framework |
| CodeIgniter | system/CodeIgniter.php, app/Config/App.php, spark | codeigniter4/framework |
| CakePHP | bin/cake, config/app.php, src/Application.php | cakephp/cakephp |
| Slim | public/index.php | slim/slim |
| Laminas | config/application.config.php, module/Application | laminas/laminas-mvc |
| Yii | yii, config/web.php, controllers, views | yiisoft/yii2 |
| MediaWiki | LocalSettings.php, includes/DefaultSettings.php, skins/Vector | — |
| phpBB | config.php, phpbb/di/container_builder.php, styles/prosilver | — |
| Craft | craft, config/general.php, templates | craftcms/cms |
| TYPO3 | typo3/sysext, typo3conf/LocalConfiguration.php | typo3/cms-core |
| October | artisan, modules/system, plugins | october/rain |
| PrestaShop | config/settings.inc.php, classes/PrestaShopAutoload.php, modules, themes | — |
Version extraction (when a VersionFile + regex is defined) reads a marker file such as wp-includes/version.php ($wp_version) or vendor/.../Application.php (const VERSION).
Reconciliation of prior table: The earlier table claimed “Magento detected via
Magento\namespace” and “CodeIgniter viasystem/core/CodeIgniter.php” — the implementation uses file-path markers only (app/etc/env.php,system/CodeIgniter.php). Confidence was previously typed asHigh/Medium; it is a numeric score in source.
Fingerprint Output (implemented)
internal sealed class PhpFrameworkFingerprint
{
PhpFrameworkKind Kind { get; } // enum: None, Laravel, ... Custom
string Name { get; } // "laravel", "symfony", ... ("unknown" when none)
string? Version { get; }
float Confidence { get; } // 0.0 .. 1.0
IReadOnlyList<string> Evidence { get; } // contributing markers, ordinal-sorted
bool IsDetected { get; } // Kind != None
}
Reconciliation: The prior
PhpFrameworkFingerprintrecord usedConfidenceLevel ConfidenceandIndicatorFiles; the real type uses afloatConfidence, anEvidencelist, and addsKind/IsDetected.
Framework signals surface in metadata as php.framework.kind, php.framework.name, php.framework.version, php.framework.confidence (formatted F2), and php.framework.evidence (;-joined), plus the project-level php.project.framework / php.project.framework_version.
PHP Configuration
Config File Discovery (implemented)
PhpConfigCollector.CollectAsync probes (relative to the scan root):
php.ini, php/php.ini, etc/php.ini, etc/php/php.ini, usr/local/etc/php/php.ini
conf.d/, php/conf.d/, etc/php/conf.d/, etc/php.d/, usr/local/etc/php/conf.d/ (*.ini)
.htaccess
php-fpm.conf, php-fpm.d/, etc/php-fpm.conf, etc/php-fpm.d/,
etc/php/fpm/php-fpm.conf, etc/php/fpm/pool.d/, usr/local/etc/php-fpm.conf, usr/local/etc/php-fpm.d/
Reconciliation: The prior absolute-path list (
/etc/php/*/php.ini,/etc/php/*/conf.d/*.ini,/etc/php-fpm.d/*.conf,/usr/local/etc/php/php.ini) used a different layout; the collector resolves paths relative to the scan root, not absolute container paths.
Security-Relevant Settings (implemented)
Settings are surfaced by PhpExtensionScanner into PhpEnvironmentSettings, which groups them into PhpSecuritySettings, PhpUploadSettings, PhpSessionSettings, PhpErrorSettings, and PhpResourceLimits. There is no single PhpSecurityConfig record.
internal sealed record PhpSecuritySettings(
IReadOnlyList<string> DisabledFunctions,
IReadOnlyList<string> DisabledClasses,
bool OpenBasedir,
string? OpenBasedirValue,
bool AllowUrlFopen,
bool AllowUrlInclude,
bool ExposePhp,
bool RegisterGlobals);
display_errors / display_startup_errors / log_errors live in PhpErrorSettings (emitted as error.display_errors, etc.). Security metadata keys include security.allow_url_fopen, security.allow_url_include, security.open_basedir, security.expose_php, security.disabled_functions[_count], and security.disabled_classes_count.
Reconciliation: The prior
PhpSecurityConfigrecord (withOpenBasedir/DisableFunctions/DisableClasses/DisplayErrorsas a single bag) does not exist. Disable lists areDisabledFunctions/DisabledClassesonPhpSecuritySettings;DisplayErrorsis onPhpErrorSettings.
Output Schema
The analyzer writes language-component records through a ScannerComponentSinkWriter. Each record carries analyzerId (php), componentKey, optional purl, name, version, type, usedByEntrypoint, a flat metadata string→string map (dotted keys), and an evidence array.
Package Component (actual emitted shape)
{
"analyzerId": "php",
"componentKey": "purl::pkg:composer/laravel/framework@10.48.7",
"purl": "pkg:composer/laravel/framework@10.48.7",
"name": "laravel/framework",
"version": "10.48.7",
"type": "composer",
"usedByEntrypoint": false,
"metadata": {
"composer.dev": "false",
"composer.type": "library",
"composer.source.type": "git",
"composer.source.ref": "0123...4567",
"composer.autoload.psr4": "Illuminate\\->src/Illuminate;...",
"composer.autoload.files": "src/Illuminate/Foundation/helpers.php",
"composer.dist.sha256": "6f1b...",
"composer.dist.url": "https://api.github.com/repos/laravel/framework/zipball/...",
"composer.plugin_api_version": "2.6.0",
"composer.content_hash": "e01f9b7d...",
"php.capability.framework": "laravel"
},
"evidence": [
{
"kind": "file",
"source": "composer.lock",
"locator": "composer.lock",
"value": "laravel/framework@10.48.7",
"sha256": "885d825c..."
}
]
}
Reconciliation: The earlier “SBOM Component” example used CycloneDX shapes (
bom-ref,properties[]withstellaops:php:*names, and anevidence.identityblock withmethods/technique). The analyzer does not emit CycloneDX directly, does not usestellaops:php:*property names (it uses dotted keys likecomposer.*andphp.*), and usesusedByEntrypoint+ a flat evidence array (kind/source/locator/value/sha256). CycloneDX/SPDX serialization happens later inStellaOps.Scanner.Emit.
Component Types Emitted
In addition to per-package composer components, PhpLanguageAnalyzer emits:
| Type | Component key | Purpose |
|---|---|---|
composer | purl::pkg:composer/<v>/<p>@<ver> | One per locked package (packages + packages-dev) |
php-project | php::project-summary | Aggregated project metadata (autoload/include/capability/phar/surface/env/ffi/conflict summaries) |
php-bin | php::bin::<name> | Composer bin entrypoints |
php-plugin | php::plugin::<package> | Composer plugins (classes implementing the plugin API) |
php-phar | php::phar::<relativePath> | Discovered PHAR archives |
Project-Summary Metadata Keys (implemented)
The php-project summary aggregates dotted-key metadata across the analysis passes. Observed key prefixes (from golden fixtures):
php.project.*—file_count,uses_composer,has_vendor,framework,framework_versioncomposer.manifest.*—name,type,license,php_version,extensions,require_count,require_dev_count,sha256autoload.*—psr4_count,psr0_count,classmap_count,files_count,edge_count,bin_count,plugin_countinclude.*—edge_count,static_count,dynamic_count,include_count,require_count,bootstrap_chain_countcapability.*—has_exec,has_network,has_filesystem,has_crypto,has_database,has_dynamic_code,has_serialization,has_reflection,has_stream_wrapper,has_upload,has_session,has_environment, risk counts,total_count,unique_function_countphar.*—archive_count,usage_count,archives_with_vendor,total_archived_filessurface.*—route_count,controller_count,middleware_count,cli_command_count,cron_job_count,event_listener_count,public_routes,protected_routes,http_methodsenv.*—extension_count,extensions_<category>(e.g.extensions_core,extensions_crypto)security.*,upload.*,session.*,error.*,limits.*— PHP config settings (see PHP Configuration)ffi.*—detected,enabled_setting,usage_countconflict.*—detected,countphp.config.entry_count
(Completeness gap closed) The capability/PHAR/FFI/surface/include/conflict scanning passes (
PhpCapabilityScanner,PhpPharScanner,PhpFfiDetector,PhpFrameworkSurfaceScanner,PhpIncludeGraphBuilder,PhpVersionConflictDetector,PhpExtensionScanner) were absent from the original contract. They are a substantial part of the shipped analyzer output.
Analysis Store Keys
(Roadmap — not implemented) There is no
PhpAnalysisKeysstatic class in source. The analyzer does not persist named analysis-store entries; it emits language-component records (above) via the component sink. The previously documented constants (php.vfs,php.composer,php.frameworks,php.security-config,php.autoload) do not exist.
Offline Kit Target
(Roadmap — not yet implemented) No offline-kit bundle, Packagist mirror, or offline composer repository exists in source. There is no
IOfflineComposerRepositoryinterface orComposerPackage/ComposerManifestoffline-resolution API. The analyzer is already offline-friendly because it reads only localcomposer.lock/composer.json/vendor/content and never contacts a remote registry, but the bundle layout below is aspirational.
offline/php-analyzer/ # PLANNED — not produced today
├── manifests/
│ └── php-analyzer-manifest.json
├── fixtures/
│ ├── laravel-app/
│ ├── symfony-app/
│ └── wordpress-site/
├── vendor-cache/
│ └── packages.json
└── SHA256SUMS
Test Fixtures
Actual Fixtures (implemented)
Golden fixtures live under src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Php.Tests/Fixtures/lang/php/. Each fixture directory contains a composer.lock and a single expected.json (the full ordered array of emitted language-component records). The fixtures are:
| Fixture | Coverage |
|---|---|
basic/ | Minimal composer project (one runtime + one dev package) |
container/ | Container-layer style layout |
laravel-extended/ | Laravel framework signals |
legacy/ | Older/legacy PHP project |
phar/ | PHAR archive discovery |
symfony/ | Symfony framework signals |
wordpress/ | WordPress CMS signals |
Reconciliation: The prior fixture list (
laravel-10-app/,symfony-6-app/,wordpress-6-site/,drupal-10-site/,composer-only/,legacy-php56/) and the per-fixtureEXPECTED.sbom.json/EXPECTED.vfs.json/EXPECTED.meta.jsongolden split do not match the source. Actual fixtures use a singleexpected.jsongolden pluscomposer.lock. No Drupal or PHP-5.6-specific fixture exists today.
Golden Output Format (actual)
Fixtures/lang/php/<name>/
├── composer.lock
└── expected.json # ordered array of emitted language-component records
Determinism is enforced by PhpLanguageAnalyzerTests comparing emitted records against expected.json.
Implementation Status
The analyzer is implemented (manifest org.stellaops.analyzer.status: production, plug-in version 0.1.0). The original phased plan is recorded below for history; all three phases have effectively shipped, with offline-kit bundling remaining as roadmap (see Offline Kit Target).
Phase 1: Core Parser — DONE
PhpComposerManifestReader(composer.json) — implementedComposerLockReader(composer.lock) — implemented- PURL generation (
PhpPackage.Purl) — implemented - VFS construction (
PhpInputNormalizer+PhpVirtualFileSystem) — implemented
Phase 2: Framework Detection — DONE
- Fingerprint rules (
PhpFrameworkFingerprinter, 17 definitions) — implemented - Float confidence scoring — implemented
- Version extraction via marker-file regex — implemented
Phase 3: Offline Support — PARTIAL
- Vendor/lockfile-only reads (offline by construction) — implemented
- Offline composer repository — not implemented (roadmap)
- Offline-kit bundle generation — not implemented (roadmap)
Project Location
The implemented analyzer lives under __Libraries, not the top-level path. The top-level src/Scanner/StellaOps.Scanner.Analyzers.Lang.Php/ directory currently contains only AGENTS.md (guild charter).
src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Php/
├── StellaOps.Scanner.Analyzers.Lang.Php.csproj
├── PhpLanguageAnalyzer.cs # IScannerLanguageAnalyzer (id "php")
├── PhpScannerAnalyzerPlugin.cs # IScannerLanguageAnalyzerPlugin
├── PhpScannerSemanticEnricherPlugin.cs # IScannerSemanticEnricherPlugin
├── GlobalUsings.cs
├── manifest.json # plug-in manifest
└── Internal/
├── PhpInputNormalizer.cs
├── PhpVirtualFileSystem.cs / PhpVirtualFile.cs
├── PhpProjectInput.cs
├── PhpComposerManifest.cs / PhpComposerManifestReader.cs
├── ComposerLockData.cs / ComposerLockReader.cs / ComposerPackage.cs
├── ComposerAutoloadData.cs / PhpAutoloadGraphBuilder.cs / PhpAutoloadEdge.cs
├── PhpFrameworkFingerprint.cs / PhpFrameworkFingerprinter.cs
├── PhpFrameworkSurface.cs / PhpFrameworkSurfaceScanner.cs
├── PhpCapability*.cs (Evidence/ScanBuilder/ScanResult/Scanner/Signals)
├── PhpPharArchive.cs / PhpPharScanner.cs
├── PhpFfiDetector.cs
├── PhpInclude*.cs (Edge/GraphBuilder/Scanner)
├── PhpConfig*.cs (Collection/Collector/Entry)
├── PhpExtension.cs / PhpExtensionScanner.cs
├── PhpInstalledJsonReader.cs
├── PhpPackage.cs / PhpPackageCollector.cs
├── PhpVersionConflictDetector.cs
└── Runtime/
Reconciliation: The prior file list (
ComposerJsonParser.cs,ComposerLockParser.cs,PhpVirtualFileSystem.csat root,PhpFrameworkDetector.cs,PhpAnalyzerPlugin.cs,README.md) did not match source. Actual readers arePhpComposerManifestReader/ComposerLockReader; the fingerprinter isPhpFrameworkFingerprinter; the plug-in isPhpScannerAnalyzerPlugin; there is noREADME.mdin the project.
Plug-in identity (implemented)
| Field | Value |
|---|---|
| Manifest id | stellaops.analyzer.lang.php |
Plug-in (IScannerLanguageAnalyzerPlugin.Id) | stellaops.scanner.plugin.php |
| Enricher plug-in id | stellaops.scanner.plugin.php.enricher |
Analyzer id (IScannerLanguageAnalyzer.Id) | php |
| Version | 0.1.0 |
requiresRestart | true |
| Capabilities | language-analyzer, php, composer |
Source defect flagged (not corrected here — doc-only change):
manifest.jsondeclaresentryPoint.typeName = "StellaOps.Scanner.Analyzers.Lang.Php.PhpAnalyzerPlugin", but noPhpAnalyzerPlugintype exists; the actual plug-in type isPhpScannerAnalyzerPlugin. This is a source/manifest mismatch that the PHP Analyzer Guild should reconcile in code (it is outside the scope of this documentation edit).
Related Contracts
- RichGraph v1 — reachability graph schema; defines the PHP SymbolID/CodeID tuple this analyzer’s symbols map onto.
- Scanner Surface — attack-surface analysis framework (PHP entry-point discovery is on its roadmap).
Changelog
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0.0 | 2025-12-05 | PHP Analyzer Guild | Initial contract |
| 1.0.0 (reconciled) | 2026-05-30 | Docs reconciliation | Aligned C# signatures, framework table (17 frameworks, float confidence), composer lock/package shapes, VFS model, PHP config types, emitted component types and metadata keys, fixtures, and project location with shipped source; marked offline-kit, IPhpSourceDiscovery/PhpDiscoveryOptions, PhpAnalysisKeys, IOfflineComposerRepository, BLAKE3/VfsEntry/VfsEntryType, and CycloneDX-shaped output as not-implemented or roadmap; flagged the manifest.json typeName defect |
