The committed PHPUnit runner lived at tools/.bin/phpunit.phar, away from
the suite it runs. Move it next to the tests it drives —
tests/phpunit.phar — and update every invocation to
`php tests/phpunit.phar -c tests/phpunit.xml.dist`:
- CI workflows (ci, build-release, build_pre-release) + the ci.yml header,
- CLAUDE.md, CONTRIBUTING.md, tools/README.md, the qa-lead-reviewer agent,
- docs/en (dev-workflow, updates_checklist, phpunit-phar, refactoring).
docs/ru is generated from docs/en (make docs-translate) and is left for
the next regeneration, per the docs workflow.
Add a version selector to the docs and publish one snapshot per release instead
of a single rolling site, so readers can pick the docs matching their installed
version (the docs change release to release).
- mkdocs.yml: enable the Material version selector (extra.version.provider: mike,
alias: true).
- docs/requirements.txt: add mike==2.1.3.
- pages.yml: deploy with mike, triggered by a release TAG (semver) instead of
every push to main — publishing is tied to the release because docs/ru is only
regenerated then. Deploys `X.Y.Z` + the `latest` alias to the gh-pages branch
and sets latest as default. workflow_dispatch takes an explicit version.
- updates_checklist.md: note the tag-triggered versioned publish.
One-time setup (GitHub UI): Settings → Pages → Deploy from a branch → gh-pages.
Bundled binaries are stored in plain Git now, so the release/pre-release builds no longer need (and must not depend on) a Git LFS fetch — which was failing once the repo hit its LFS budget. The Makefile verify_no_lfs_pointers guard still fails the build if an LFS pointer stub ever slips into the archive.
PHP-CS-Fixer's `no_unused_imports` is conservative — it treats a class name that
merely appears in a PHPDoc *description* as "used", so genuinely-dead imports
(e.g. `use ...Request;` next to a "Request IP" doc description) were never
flagged. Slevomat's UnusedUses is precise: it parses annotation *types*
(@param/@return/@var), so it keeps docblock-typed imports but removes truly
unused ones — matching what Intelephense (P1003) reports.
- Swap require-dev: friendsofphp/php-cs-fixer -> squizlabs/php_codesniffer +
slevomat/coding-standard (+ phpcodesniffer-composer-installer, allow-listed).
- New narrow ruleset build/phpcs.xml.dist (import/namespace hygiene only, NOT
full PSR-12): UnusedUses (searchAnnotations=true), UseFromSameNamespace,
UseDoesNotStartWithBackslash, AlphabeticallySortedUses, UseSpacing,
NamespaceSpacing. View templates stay excluded.
- Makefile: `make cs` -> phpcs, `make cs-fix` -> phpcbf (same target names).
- CI code-style job, CLAUDE.md, CONTRIBUTING.md, docs, .gitignore updated;
build/.php-cs-fixer.dist.php removed. Committed vendor stays production-only.
Move the generated site out of the repo root: site_dir -> build/site, with the
Pages upload path and .gitignore updated to match. build/ already holds tooling
config, so the build output no longer clutters the top-level listing.
CI translation was slow, so move it out of GitHub Actions: docs/ru is now a
committed, generated tree refreshed LOCALLY before a release; CI only builds it.
- pages.yml: drop the setup-python/cache/translate steps — the workflow now just
installs the build toolchain and runs `mkdocs build --strict` on the committed
en+ru trees.
- .gitignore: stop ignoring docs/ru (now committed); keep site/ + .docs-cache/.
- Split deps: docs/requirements.txt = build only (mkdocs-material, static-i18n,
used by CI); tools/docs/requirements.txt = translators (local-only).
- Makefile: docs-venv installs both; docs-build/docs-serve no longer translate
(translation is the deliberate `make docs-translate` release step).
- updates_checklist.md: new "Regenerate translated documentation" step
(make docs-translate + docs-build, commit docs/ru with the release commit).
- Commit the generated docs/ru (37 files, translators/yandex).
Rule: edit ONLY docs/en; docs/ru is generated — never hand-edit it.
Replace the hand-maintained Docsify site (parallel docs/en + docs/ru trees
that had already drifted) with a MkDocs Material build where English is the
single source of truth and Russian is generated at build time.
Engine & structure
- mkdocs.yml: Material theme, site_url for the /XC_VM/ Pages subpath, and a
two-tab information architecture — User Guide (administration, API/Swagger,
UI translations, diagnostics, info/FAQ) vs Developer Guide (architecture,
workflow, security, integrations, build). Files are NOT moved — the split is
nav-only, so URLs and cross-links stay stable.
- mkdocs-static-i18n (folder mode): English at root, Russian under /ru/, with a
language switcher. `mkdocs build --strict` validates every link/anchor.
Translation pipeline (tools/docs/translate.py)
- Engine-agnostic via DOCS_TRANSLATE_PROVIDER: translators (free, no API key —
default), anthropic, deepl, or noop. Per-file sha256 cache so only changed
English files are re-translated. Markdown-safe: code, URLs, HTML tags and
glossary terms (XC_VM, FFmpeg, HLS, ...) are masked and never translated.
A file whose translation fails falls back to English so the build never breaks.
- docs/ru is generated and gitignored — never committed. It is produced locally
(`make docs-serve` / `docs-build`) and in CI.
CI & tooling
- pages.yml: build-then-upload (setup-python -> install -> restore .docs-cache
with restore-keys -> translate -> mkdocs build --strict -> deploy), replacing
the verbatim docs/ upload.
- Makefile: docs-venv / docs-translate / docs-build / docs-serve.
- docs/requirements.txt; .gitignore for docs/ru, site/, .docs-cache.
Migration details
- Removed Docsify control files (index.html, _navbar.md, _sidebar.md, .nojekyll)
and de-Docsify-ed body links in 6 English files (en-us/ aliases -> relative,
swagger _media paths, stripped ':ignore' link syntax).
- Preserved the two Russian-only planning docs (no English source) by moving
them into docs/adr/ as *.ru.md (repo-internal, excluded from the site).
Add a push-to-main workflow that parses issue references in the pushed
commit messages and moves each referenced issue's card on the org
Projects v2 board to the "Review & Deploy" status column, without
closing the issue. Reuses the existing ADD_TO_PROJECT_PAT secret.
Ministra stops being a module — the whole Stalker portal (portal.php,
MinistraBootstrap, PortalHandler/PortalHelpers and the STB front-end) now
lives in src/Ministra/ under the XcVm\Ministra namespace, served at
/home/xc_vm/Ministra via the nginx alias.
- src/ministra/* and Modules/ministra_85a7d/{PortalHandler,PortalHelpers}
→ src/Ministra/; MinistraModule.php + module.json removed. Ministra was
the only committed module, so src/Modules/ keeps a .gitkeep.
- portal.php resolves PortalHandler as a sibling and derives MAIN_HOME from
its new location (glob crutch gone).
- nginx alias + AuthRepository $rAlias switched to /home/xc_vm/Ministra
(PascalCase); ministra entry dropped from bundled_modules.php.
- Makefile: Modules/ removed from LB_DIRS — all modules are MAIN-only, so
the ~50 MB of portal assets no longer ship to LB nodes.
- ArchitectureTest: zero committed modules is now a valid state.
- PHPStan: analyse src/Ministra, exclude the procedural portal.php entry,
repath the ministra baseline entries.
- Docs (architecture, ministra-browser-emulation, extraction plan) updated
to the new layout; the "extract to a separate repo" plan is cancelled.
Verified: php -l, make gates, make phpstan (No errors), full unit suite
(432 tests). On-server smoke: handshake + get_profile work end-to-end with
a registered MAC after deploy.
BoundaryInterface was a marker interface with no runtime consumer —
nothing read getEntryPoint()/isIsolated() and, being static-less
metadata, it enforced nothing. Its only implementor was MinistraModule.
Removes the interface, drops `implements BoundaryInterface` plus the two
orphaned methods from MinistraModule (getName/getVersion stay — they come
from BaseModule/ModuleInterface), and deletes the now-empty Core/Boundary/.
Test contract (InterfaceContractTest) loses the three BoundaryInterface
assertions; docs (en/ru architecture + modules, .github instructions) now
describe isolated subsystems like Ministra as a convention — own entry
point + bootstrap — rather than a marker interface.
Verified: php -l, make gates, make phpstan (No errors);
InterfaceContractTest + ArchitectureTest green (33 tests, 96 assertions).
Relocate the dev-tooling config into build/ so the project root only holds
source and first-class project files:
- phpstan.dist.neon -> build/phpstan.dist.neon
- phpstan-baseline.neon -> build/phpstan-baseline.neon
- .php-cs-fixer.dist.php -> build/.php-cs-fixer.dist.php
- .php-cs-fixer.cache -> build/ (regenerated there; gitignored)
Because neon/CS-Fixer resolve relative paths against the config file's own
directory, the internal references are re-anchored one level up (src/ ->
../src/, tools/ -> ../tools/, Finder in(__DIR__.'/../src'); the baseline's
path: entries likewise). The Makefile now points PHPStan at the config with
-c build/phpstan.dist.neon (it previously relied on root auto-discovery),
generates the baseline into build/, and passes --config=build/... to CS-Fixer;
the CS-Fixer cache is pinned to build/ via setCacheFile and re-gitignored.
No behaviour change. Verified: make phpstan (No errors), make cs (0 fixable),
make gates. CI runs through these make targets, so it is covered.
dependabot.yml: the comment claimed "no Composer/npm manifests committed",
but src/composer.json + composer.lock ARE committed. Added a grouped
composer ecosystem (directory /src) so the 4 prod deps and the dev tools
get advisory monitoring; noted the production-only vendor recommit step.
instructions/php-conventions + architecture-rules: rewritten to the
current Composer PSR-4 architecture. They previously described the
pre-migration state and misdirected Copilot:
- "No autoloading via Composer — custom src/autoload.php" (file removed)
and "Do NOT introduce Composer dependencies" → Composer PSR-4, vendor
committed production-only, dump-autoload workflow
- lowercase paths (src/cli, src/core, src/modules, …) → real PascalCase
(src/Cli, src/Core, src/Modules, src/Streaming, …)
- setDb() / $r-prefix (both gone from the codebase) → DatabaseAware +
self::db(); dropped the dead $r naming rule
- inverted namespace guidance → new code is namespaced; legacy coexists,
don't mass-migrate
- dangling ARCHITECTURE.md ref → docs/en/development/architecture.md
(which exists), src/config/modules.php path fixed
`php -l` syntax checking is redundant with the real linters/validators (PHPStan
parses the code, PHP-CS-Fixer and the PHPUnit bootstrap also fail on parse
errors). Remove the script and every reference to it:
- Makefile: drop the `syntax_check` target and its .PHONY entry.
- CI (ci.yml): drop the dedicated `lint` (PHP Syntax Check) job.
- Release workflows (build-release, build_pre-release): drop the "Check syntax"
step from the Quality Gate (PHPUnit remains).
- CONTRIBUTING.md: replace the syntax-check pre-commit guidance with the real
checks (make dev-tools / phpstan / cs / gates / phpunit).
- updates_checklist (en/ru): replace `make syntax_check` with the quality-check
suite and drop the stale "Security scan" snippet that referenced the removed
script and a non-existent tools/run_scan.sh (Semgrep runs automatically in CI).
Most checks run automatically in CI, so the manual "run X locally" checklist
items were redundant. Add an "Automated checks" section listing what CI runs
(PHP syntax check, PHPStan, PHP-CS-Fixer, PHPUnit, the PSR-4 gates, composer
audit) and drop the local syntax-check / phpunit checklist items. Also remove
the stale Git LFS line (the repo commits binaries directly, not via LFS). Keep
the human-judgment items: conventions, tests for the change, docs, no secrets.
Switch from "commit vendor with dev deps + strip at release" to the standard
application model: the committed src/vendor/ is PRODUCTION-ONLY, and dev tooling
(PHPStan, PHP-CS-Fixer + ~37 transitive deps) is installed on demand with
"composer install".
- Regenerate the committed src/vendor/ via "composer install --no-dev"
(34 MB -> ~0.5 MB; only the Composer autoloader + gemorroj/m3u-parser +
chrisyue/php-m3u8 remain). This also stops PHPStan\PharAutoloader registering
in production.
- Commit src/composer.lock (un-ignored) — this is an application, so the lock is
committed to make "composer install" reproducible across dev/CI.
- Revert the release-time strip step (Makefile hooks + tools/build/
strip-dev-vendor.sh) — no longer needed; the archive ships the prod vendor as-is.
- CI: the phpstan and code-style jobs now run "composer install --working-dir=src"
(with tools: composer) to obtain the dev tools before running.
- New gate tools/ci/check-vendor-prod-only.sh (+ make check-vendor-prod-only,
wired into "make gates"): asserts no require-dev package from composer.lock is
committed under src/vendor/ — guards against accidentally committing a
dev-bloated vendor. Inspects git-tracked files, so it is correct even in a CI
job that already ran "composer install".
- Fix verify-lb-archive.sh: LB legitimately ships most of Cli/Commands and
Cli/CronJobs (edge commands + certbot/cache/cleanup crons), so flag only the
genuinely privileged dirs + the specific install/root files, not the whole dirs.
- .gitignore / composer.json notes updated.
Verified before pruning: PHPStan no errors, PHPUnit 303, cs + gates green. After
pruning: PHPUnit 303 (prod-only vendor), all three gates green. Local dev tools
restored afterwards with "composer install" (not committed).
enigma, episode, episodes, process_monitor and stream_view referenced migrated
classes (UserRepository, BouquetService, StreamRepository, RequestManager,
SettingsManager, ...) by short name with either no `use` or a `use` placed
*below* the first usage. PHP imports outside the top scope are positional, so the
short name resolved to a now-nonexistent global class — a runtime fatal on those
admin pages (php -l passes; not covered by PHPStan/PHPUnit). The migration's
automated `use` insertion missed them due to the interleaved HTML / short-tag
(<? , <?=) structure, and the later php-cs-fixer pass stripped some as 'unused'
because it could not see usage inside short-tag blocks.
- Consolidate every needed `use XcVm\...;` into a single top-of-file PHP block.
- Exclude Public/Views and Modules/*/views from php-cs-fixer (no_unused_imports
is unreliable on short-tag templates); their import correctness is enforced by
the new check_procedural_use gate instead.
Verified: php -l (short_open_tag=1) clean; PHPStan no errors; PHPUnit green.
Add friendsofphp/php-cs-fixer as a committed Composer dev dependency (src/vendor/,
same model as PHPStan — no composer install on deploy).
- .php-cs-fixer.dist.php: deliberately NARROW ruleset — no_unused_imports,
ordered_imports, no_leading_import_slash, single_line_after_imports,
blank_line_after_namespace, no_extra_blank_lines[use]. NO @PSR12 / indentation
rules: the codebase is tab-indented legacy and a full reformat would be
unreviewable. Indent forced to tabs, LF endings. Excludes vendor, the bundled
Modules/tmdb/lib, tmp/, backups/.
- Makefile: 'make cs' (dry-run, fails on diff — CI) and 'make cs-fix' (apply).
- CI: new 'Code Style (PHP-CS-Fixer)' job running 'make cs' on PHP 8.3.
- .gitignore: ignore .php-cs-fixer.cache.
- add gemorroj/m3u-parser 6.0.1 to committed vendor/ (PHP >=8.0.2;
upstream 6.1.0 requires PHP 8.2, incompatible with the 8.1 target)
- remove vendored Core/Parsing/M3uParser snapshot and manual bootstraps
- autoload \M3uParser\ from committed vendor/ instead of a path mapping
- stop tracking composer.lock (already gitignored); CI now audits the
committed vendor/composer/installed.json without --locked
Introduce a committed Composer PSR-4 autoloader without changing class
resolution behavior, as the foundation for the incremental PSR-4 migration.
- src/composer.json: PSR-4 (XcVm\ -> ./, M3uParser\, Chrisyue\PhpM3u8\),
platform php 8.1.33 (deploy runtime), optimize-autoloader/classmap-authoritative
false (live path resolution, no class-map cache). autoload.files left empty:
global functions are still loaded by existing require glue; moving them is
deferred until that glue is removed.
- src/vendor/ + src/composer.lock: committed (deploy path has no Composer);
generated with 'composer update' from src/. Regenerate with dump-autoload.
- src/bootstrap.php, tests/bootstrap.php: require vendor/autoload.php first,
then the legacy autoload.php.
- src/autoload.php: drop the igbinary disk cache (enableFileCache/saveCache/
shutdown handler/root-chown + bottom call); register at the END of the SPL
queue (prepend=false) so Composer wins for XcVm\* and only still-global
classes fall through to the in-memory scanner.
- Makefile: add vendor to LB_DIRS so load-balancer archives ship the loader.
- phpstan.dist.neon: exclude src/vendor/* from analysis.
- .gitignore: document that src/vendor/ is intentionally tracked.
- ci.yml: add composer-audit job (no-op until real require deps exist).
Verified: php -l clean; Composer first / XC_Autoloader last in the SPL stack;
tmp/cache/autoload_map no longer written; PHPUnit 292/292; PHPStan no errors.
- .github/workflows/ci.yml: new `phpstan` job (PHP 8.3, no Composer) running
`make phpstan` on every push/PR.
- phpstan.dist.neon: include phpstan-baseline.neon so the gate is green on the
~446 pre-existing (mostly false-positive/cosmetic) findings and fails only on
NEW issues. Shrink the baseline over time via `make phpstan-baseline`.