diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c52bfd8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space + +[*.php] +indent_size = 4 + +[*.{json,jsonc}] +indent_size = 4 + +[*.{yaml,yml}] +indent_size = 2 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..9611d9f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "bmewburn.vscode-intelephense-client", + "editorconfig.editorconfig" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7dbe61e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "intelephense.environment.phpVersion": "8.4.0", + "files.watcherExclude": { + "**/vendor/**": true, + "**/workspace/vendor/**": true, + "**/workspace/.phpstan-cache/**": true, + "**/workspace/.php-cs-fixer.cache": true, + "**/workspace/.deptrac.cache": true + }, + "search.exclude": { + "**/vendor/**": true, + "**/workspace/vendor/**": true, + "**/workspace/.phpstan-cache/**": true, + "**/workspace/.php-cs-fixer.cache": true, + "**/workspace/.deptrac.cache": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..9de4950 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,94 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "EvolvePHP 2: Install Workspace Dependencies", + "type": "shell", + "command": "composer", + "args": [ + "--working-dir=workspace", + "install" + ], + "problemMatcher": [] + }, + { + "label": "EvolvePHP 2: Quality", + "type": "shell", + "command": "composer", + "args": [ + "--working-dir=workspace", + "quality" + ], + "group": "test", + "problemMatcher": [] + }, + { + "label": "EvolvePHP 2: Tests", + "type": "shell", + "command": "composer", + "args": [ + "--working-dir=workspace", + "test" + ], + "group": "test", + "problemMatcher": [] + }, + { + "label": "EvolvePHP 2: Architecture", + "type": "shell", + "command": "composer", + "args": [ + "--working-dir=workspace", + "architecture" + ], + "group": "test", + "problemMatcher": [] + }, + { + "label": "EvolvePHP 2: Static Analysis", + "type": "shell", + "command": "composer", + "args": [ + "--working-dir=workspace", + "analyse" + ], + "group": "test", + "problemMatcher": [] + }, + { + "label": "EvolvePHP 2: Style Check", + "type": "shell", + "command": "composer", + "args": [ + "--working-dir=workspace", + "style:check" + ], + "group": "test", + "problemMatcher": [] + }, + { + "label": "EvolvePHP 2: Style Fix", + "type": "shell", + "command": "composer", + "args": [ + "--working-dir=workspace", + "style:fix" + ], + "problemMatcher": [] + }, + { + "label": "EvolvePHP 2: Root Policy", + "type": "shell", + "command": "php", + "args": [ + "workspace/vendor/bin/phpunit", + "--configuration", + "phpunit.xml.dist", + "tests/Architecture", + "tests/Documentation" + ], + "group": "test", + "problemMatcher": [] + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index fbe9d1e..85006be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Documentation and governance +- Added Phase 2.8 developer-experience foundation with EditorConfig, VS Code extension recommendations, portable VS Code settings and portable task commands, PHP 8.4 language-analysis targeting, canonical Composer-script reuse, explicit non-mutating quality checks versus the Style Fix mutating task, no local executable paths, and no runtime debugging configuration. - Added Phase 2.7B repository governance evidence finalization: changed the GitHub default branch to `2.x`, activated repository rulesets for `master` and `2.x`, preserved `master` as the EvolvePHP 1 legacy line, required PR-based change on both branches, blocked deletion and force pushes, enforced strict/up-to-date required status checks on `2.x` for `Policy (PHP 8.4)`, `Workspace quality (PHP 8.4)` and `Workspace quality (PHP 8.5)`, and made no branch rename or deletion. - Added Phase 2.7A repository-owned branch-governance policy foundation for EvolvePHP 2 development on `2.x`, preserved EvolvePHP 1 maintenance on `master`, explicit separation of modern development and legacy maintenance, and preparation for a later default-branch and ruleset transition. - Added Phase 2.6 GitHub Actions CI foundation with a PHP 8.4/8.5 workspace quality matrix, separate PHP 8.4 root-policy job, lockfile-based workspace installation, immutable action pinning, and successful initial CI execution for the current workspace, tooling and package foundation. diff --git a/README.md b/README.md index f6af64c..2ffdb50 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,16 @@ The normal clone path starts on `2.x` because it is the GitHub default branch. F The EvolvePHP 2 workspace owns dependency installation and quality checks. Use [workspace/README.md](workspace/README.md) for detailed Composer, PHPUnit, PHPStan, PHP-CS-Fixer and Deptrac guidance. +## Developer Experience + +VS Code support is optional developer tooling for contributors. It is not framework runtime configuration, and it does not replace terminal PHP or Composer execution. + +The repository includes a portable `.editorconfig` foundation and committed VS Code recommendations, settings and tasks. No machine-specific executable path is committed. Contributors with multiple PHP versions must make PHP 8.4+ available to their VS Code integrated terminal and local environment before relying on the tasks. + +The VS Code tasks are convenience wrappers around the canonical Composer workspace scripts owned by `workspace/composer.json`. `EvolvePHP 2: Quality`, `EvolvePHP 2: Tests`, `EvolvePHP 2: Architecture`, `EvolvePHP 2: Static Analysis`, `EvolvePHP 2: Style Check` and `EvolvePHP 2: Root Policy` are normal non-mutating checks. `EvolvePHP 2: Install Workspace Dependencies` installs local dependency content. `EvolvePHP 2: Style Fix` is explicitly mutating. + +Runtime debugging configuration is deferred until the EvolvePHP 2 runtime implementation is complete. + ## Quality Checks The main EvolvePHP 2 validation command is: diff --git a/tests/Documentation/EvolvePhp2DeveloperExperienceTest.php b/tests/Documentation/EvolvePhp2DeveloperExperienceTest.php new file mode 100644 index 0000000..a87bf4d --- /dev/null +++ b/tests/Documentation/EvolvePhp2DeveloperExperienceTest.php @@ -0,0 +1,282 @@ +root = dirname(__DIR__, 2); + } + + public function testRequiredRepositoryEditorAssetsExist(): void + { + foreach (array( + '.editorconfig', + '.vscode/extensions.json', + '.vscode/settings.json', + '.vscode/tasks.json', + ) as $path) { + $this->assertFileExists($this->projectPath($path), $path . ' should be committed for the Phase 2.8 developer-experience foundation.'); + } + } + + public function testVsCodeConfigurationFilesContainValidJson(): void + { + foreach (array( + '.vscode/extensions.json', + '.vscode/settings.json', + '.vscode/tasks.json', + ) as $path) { + $this->readJsonFile($path); + } + } + + public function testEditorConfigDeclaresPortableWhitespacePolicy(): void + { + $content = $this->readProjectFile('.editorconfig'); + + $this->assertMatchesPattern('/^root\s*=\s*true\s*$/m', $content); + $this->assertMatchesPattern('/^\[\*\]\s*$/m', $content); + $this->assertMatchesPattern('/^charset\s*=\s*utf-8\s*$/mi', $content); + $this->assertMatchesPattern('/^end_of_line\s*=\s*lf\s*$/m', $content); + $this->assertMatchesPattern('/^insert_final_newline\s*=\s*true\s*$/m', $content); + $this->assertMatchesPattern('/^indent_style\s*=\s*space\s*$/m', $content); + $this->assertMatchesPattern('/^\[\*\.php\]\s*\R(?:.*\R)*?^indent_size\s*=\s*4\s*$/m', $content); + $this->assertMatchesPattern('/^\[\*\.\{json,jsonc\}\]\s*\R(?:.*\R)*?^indent_size\s*=\s*4\s*$/m', $content); + $this->assertMatchesPattern('/^\[\*\.\{ya?ml,yml\}\]\s*\R(?:.*\R)*?^indent_size\s*=\s*2\s*$/m', $content); + $this->assertDoesNotMatchPattern('/trim_trailing_whitespace\s*=\s*true/i', $content); + } + + public function testRecommendedVsCodeExtensionsAreMinimalPhpEditorBaseline(): void + { + $extensions = $this->readJsonFile('.vscode/extensions.json'); + + $this->assertSame( + array( + 'bmewburn.vscode-intelephense-client', + 'editorconfig.editorconfig', + ), + $extensions['recommendations'] + ); + $this->assertArrayNotHasKey('unwantedRecommendations', $extensions); + } + + public function testVsCodeSettingsTargetPhp84AndAvoidLocalRuntimeConfiguration(): void + { + $settings = $this->readJsonFile('.vscode/settings.json'); + $content = $this->readProjectFile('.vscode/settings.json'); + + $this->assertSame('8.4.0', $settings['intelephense.environment.phpVersion']); + $this->assertArrayHasKey('files.watcherExclude', $settings); + $this->assertArrayHasKey('search.exclude', $settings); + + foreach (array( + '**/vendor/**', + '**/workspace/vendor/**', + '**/workspace/.phpstan-cache/**', + '**/workspace/.php-cs-fixer.cache', + '**/workspace/.deptrac.cache', + ) as $excludedPath) { + $this->assertSame(true, $settings['files.watcherExclude'][$excludedPath], $excludedPath . ' should be excluded from file watching.'); + $this->assertSame(true, $settings['search.exclude'][$excludedPath], $excludedPath . ' should be excluded from search.'); + } + + foreach (array( + '/php(?:\.validate)?\.executablePath/i', + '/composer(?:\.executable|Path)/i', + '/formatOnSave/i', + '/defaultFormatter/i', + '/php-cs-fixer\.(?:extension|executable|onsave|path)/i', + '/xdebug/i', + '/launch\.json/i', + '/"debug\./i', + '/[A-Za-z]:\\\\/', + '#/(?:d/php|d/tools|Users|home)/#i', + ) as $pattern) { + $this->assertDoesNotMatchPattern($pattern, $content); + } + } + + public function testVsCodeTasksWrapExistingComposerScriptsAndRootPolicyCommand(): void + { + $tasksFile = $this->readJsonFile('.vscode/tasks.json'); + + $this->assertSame('2.0.0', $tasksFile['version']); + $this->assertArrayHasKey('tasks', $tasksFile); + + $tasks = $this->tasksByLabel($tasksFile['tasks']); + + $expectedComposerTasks = array( + 'EvolvePHP 2: Install Workspace Dependencies' => 'install', + 'EvolvePHP 2: Quality' => 'quality', + 'EvolvePHP 2: Tests' => 'test', + 'EvolvePHP 2: Architecture' => 'architecture', + 'EvolvePHP 2: Static Analysis' => 'analyse', + 'EvolvePHP 2: Style Check' => 'style:check', + 'EvolvePHP 2: Style Fix' => 'style:fix', + ); + + foreach ($expectedComposerTasks as $label => $script) { + $this->assertArrayHasKey($label, $tasks); + $this->assertSame('shell', $tasks[$label]['type']); + $this->assertSame('composer', $tasks[$label]['command']); + $this->assertSame(array('--working-dir=workspace', $script), $tasks[$label]['args']); + } + + $this->assertArrayHasKey('EvolvePHP 2: Root Policy', $tasks); + $this->assertSame('shell', $tasks['EvolvePHP 2: Root Policy']['type']); + $this->assertSame('php', $tasks['EvolvePHP 2: Root Policy']['command']); + $this->assertSame( + array( + 'workspace/vendor/bin/phpunit', + '--configuration', + 'phpunit.xml.dist', + 'tests/Architecture', + 'tests/Documentation', + ), + $tasks['EvolvePHP 2: Root Policy']['args'] + ); + } + + public function testVsCodeTasksAvoidMachineSpecificExecutablesAndAutomaticMutation(): void + { + $content = $this->readProjectFile('.vscode/tasks.json'); + + foreach (array( + '/[A-Za-z]:\\\\/', + '#/(?:d/php|d/tools|Users|home)/#i', + '/D:\\\\php-84/i', + '/D:\\\\tools/i', + '/C:\\\\/i', + '/xdebug/i', + '/launch\.json/i', + '/pre-commit|git hook/i', + ) as $pattern) { + $this->assertDoesNotMatchPattern($pattern, $content); + } + + $tasks = $this->tasksByLabel($this->readJsonFile('.vscode/tasks.json')['tasks']); + + foreach ($tasks as $label => $task) { + $this->assertArrayNotHasKey('runOptions', $task, $label . ' should not auto-run.'); + } + } + + public function testDocumentationDescribesPortableOptionalDeveloperExperienceContract(): void + { + $combined = $this->readProjectFile('README.md') + . "\n" + . $this->readProjectFile('workspace/README.md'); + + foreach (array( + '/VS Code.*optional developer tooling|optional developer tooling.*VS Code/i', + '/not framework runtime|framework runtime.*not/i', + '/PHP 8\.4\+.*Composer|Composer.*PHP 8\.4\+/i', + '/workspace\/composer\.json.*canonical|canonical.*workspace\/composer\.json/i', + '/Composer workspace scripts.*canonical|canonical.*Composer workspace scripts/i', + '/tasks.*convenience wrappers|convenience wrappers.*tasks/i', + '/Style Fix.*mutating|mutating.*Style Fix/i', + '/Quality.*non-mutating|non-mutating.*Quality/i', + '/Tests.*non-mutating|non-mutating.*Tests/i', + '/Architecture.*non-mutating|non-mutating.*Architecture/i', + '/Static Analysis.*non-mutating|non-mutating.*Static Analysis/i', + '/Style Check.*non-mutating|non-mutating.*Style Check/i', + '/Root Policy.*non-mutating|non-mutating.*Root Policy/i', + '/no machine-specific executable|machine-specific executable.*not/i', + '/multiple PHP versions.*PHP 8\.4\+|PHP 8\.4\+.*multiple PHP versions/i', + '/integrated terminal.*PHP 8\.4\+|PHP 8\.4\+.*integrated terminal/i', + '/runtime.*debugging.*deferred|debugging.*deferred.*runtime/i', + ) as $pattern) { + $this->assertMatchesPattern($pattern, $combined); + } + + foreach (array( + '/D:\\\\php-84/i', + '/D:\\\\tools/i', + '/[A-Za-z]:\\\\/', + '#/(?:d/php|d/tools|Users|home)/#i', + '/Xdebug.*configured|configured.*Xdebug/i', + ) as $pattern) { + $this->assertDoesNotMatchPattern($pattern, $combined); + } + } + + public function testChangelogRecordsPhase28DeveloperExperienceFoundation(): void + { + $content = $this->readProjectFile('CHANGELOG.md'); + + foreach (array( + '/Phase 2\.8/i', + '/EditorConfig/i', + '/VS Code/i', + '/PHP 8\.4.*language-analysis|language-analysis.*PHP 8\.4/i', + '/portable task commands|task commands.*portable/i', + '/Composer-script reuse|Composer scripts.*canonical/i', + '/non-mutating.*Style Fix.*mutating|mutating.*Style Fix.*non-mutating/i', + '/no local executable paths|local executable paths.*no/i', + '/no runtime debugging configuration|runtime debugging configuration.*no/i', + ) as $pattern) { + $this->assertMatchesPattern($pattern, $content); + } + } + + public function testLaunchConfigurationIsNotCreatedDuringThisPhase(): void + { + $this->assertFileDoesNotExist($this->projectPath('.vscode/launch.json')); + } + + private function tasksByLabel($tasks) + { + $this->assertIsArray($tasks); + + $indexed = array(); + + foreach ($tasks as $task) { + $this->assertArrayHasKey('label', $task); + $this->assertArrayNotHasKey($task['label'], $indexed, $task['label'] . ' should be unique.'); + + $indexed[$task['label']] = $task; + } + + ksort($indexed); + + return $indexed; + } + + private function projectPath($path) + { + return $this->root . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $path); + } + + private function readProjectFile($path) + { + $fullPath = $this->projectPath($path); + $this->assertFileExists($fullPath, $path . ' should exist before it is read.'); + + return file_get_contents($fullPath); + } + + private function readJsonFile($path) + { + $content = $this->readProjectFile($path); + $json = json_decode($content, true); + + $this->assertSame(JSON_ERROR_NONE, json_last_error(), $path . ' should contain valid JSON: ' . json_last_error_msg()); + $this->assertIsArray($json, $path . ' should decode to a JSON object.'); + + return $json; + } + + private function assertMatchesPattern($pattern, $content): void + { + $this->assertSame(1, preg_match($pattern, $content), 'Failed asserting that content matches ' . $pattern); + } + + private function assertDoesNotMatchPattern($pattern, $content): void + { + $this->assertSame(0, preg_match($pattern, $content), 'Failed asserting that content does not match ' . $pattern); + } +} diff --git a/workspace/README.md b/workspace/README.md index 89916d9..21c1ee8 100644 --- a/workspace/README.md +++ b/workspace/README.md @@ -118,6 +118,38 @@ composer --working-dir=workspace quality `quality` runs `architecture`, `analyse`, `style:check` and `test`, in that order. `style:fix` remains separate because it is mutating. +## VS Code Developer Experience + +VS Code is optional developer tooling, not framework runtime configuration. The repository root is the VS Code workspace; no separate `.code-workspace` file is required for this phase. + +The committed recommendations are intentionally minimal: EditorConfig for shared editor whitespace policy and Intelephense for PHP 8.4 language-analysis support. The Intelephense setting targets PHP 8.4 syntax and symbols for editor feedback, but it does not replace actual PHP 8.4+ execution. + +The VS Code tasks execute `php` and `composer` from the integrated terminal environment. Before relying on them, confirm that: + +```bash +php --version +composer --version +``` + +show PHP 8.4+ and Composer. If a contributor has multiple PHP versions, they should configure their local OS, terminal or VS Code user environment so the integrated terminal resolves PHP 8.4+. Do not commit a machine-specific executable path. + +`workspace/composer.json` remains the canonical source for Composer workspace scripts. `.vscode/tasks.json` is only a convenience interface, and CI remains the remote enforcement layer. + +The VS Code tasks map to the canonical commands as follows: + +| Task | Canonical command | Category | +| --- | --- | --- | +| `EvolvePHP 2: Install Workspace Dependencies` | `composer --working-dir=workspace install` | Dependency setup | +| `EvolvePHP 2: Quality` | `composer --working-dir=workspace quality` | Non-mutating | +| `EvolvePHP 2: Tests` | `composer --working-dir=workspace test` | Non-mutating | +| `EvolvePHP 2: Architecture` | `composer --working-dir=workspace architecture` | Non-mutating | +| `EvolvePHP 2: Static Analysis` | `composer --working-dir=workspace analyse` | Non-mutating | +| `EvolvePHP 2: Style Check` | `composer --working-dir=workspace style:check` | Non-mutating | +| `EvolvePHP 2: Style Fix` | `composer --working-dir=workspace style:fix` | Mutating | +| `EvolvePHP 2: Root Policy` | `php workspace/vendor/bin/phpunit --configuration phpunit.xml.dist tests/Architecture tests/Documentation` | Non-mutating | + +Runtime debugging and Xdebug launch configuration are intentionally deferred because EvolvePHP 2 runtime implementation is not complete. + ## PHPUnit Suites PHPUnit 13 is owned by the EvolvePHP 2 workspace. It must not be added to the legacy root Composer manifest, any package manifest or production requirements.