From 59d8f2e57cb2f04e32027f0a310716d41a267fc1 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Fri, 2 Jan 2026 10:55:00 +0200 Subject: [PATCH 01/20] Expand Gemini context settings. --- .gemini/settings.json | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/.gemini/settings.json b/.gemini/settings.json index 13a8d903846..25297fc85e8 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -1,3 +1,31 @@ { - "contextFileName": "AGENTS.md" -} \ No newline at end of file + "context": { + "fileName": [ + "AGENTS.md", + "docs/context/js/component-conventions.md", + "docs/context/js/module-architecture.md", + "docs/context/js/widgets.md", + "docs/context/js/feature-tours.md", + "docs/context/js/jsdoc.md", + "docs/context/js/notifications.md", + "docs/context/js/tests.md", + "docs/context/js/utils.md", + "docs/context/js/state-management.md", + "docs/context/js/hooks.md", + "docs/context/js/storybook.md", + "docs/context/js/event-tracking.md", + "docs/context/js/feature-flags.md", + "docs/context/php/admin-features.md", + "docs/context/php/asset-management.md", + "docs/context/php/context-pattern.md", + "docs/context/php/dependency-injection.md", + "docs/context/php/module-architecture.md", + "docs/context/php/naming-conventions.md", + "docs/context/php/prompts-and-dismissals.md", + "docs/context/php/rest-api.md", + "docs/context/php/settings-management.md", + "docs/context/php/storage-patterns.md", + "docs/context/php/trait-composition.md" + ] + } +} From d1b7c918bdc04666789ab791d2d9bfd51c598b08 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Fri, 2 Jan 2026 11:01:08 +0200 Subject: [PATCH 02/20] Configure Gemini settings for MCP servers. --- .gemini/settings.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gemini/settings.json b/.gemini/settings.json index 25297fc85e8..7d246249e51 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -27,5 +27,14 @@ "docs/context/php/storage-patterns.md", "docs/context/php/trait-composition.md" ] + }, + "mcpServers": { + "github": { + "trust": false, + "httpUrl": "https://api.githubcopilot.com/mcp/", + "headers": { + "Authorization": "Bearer ${GITHUB_TOKEN}" + } + } } } From 73030b5cf864520fa99f9640cf1fbb5d9a24d5bf Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Fri, 2 Jan 2026 11:42:58 +0200 Subject: [PATCH 03/20] Add `implement.toml` Gemini command. --- .gemini/commands/implement.toml | 607 ++++++++++++++++++++++++++++++++ .gitignore | 2 + 2 files changed, 609 insertions(+) create mode 100644 .gemini/commands/implement.toml diff --git a/.gemini/commands/implement.toml b/.gemini/commands/implement.toml new file mode 100644 index 00000000000..46ae0ec5367 --- /dev/null +++ b/.gemini/commands/implement.toml @@ -0,0 +1,607 @@ +description = "Implements a GitHub issue locally with automated review and iteration" + +prompt = """ +You are tasked with implementing GitHub issue #{{args}} from the google/site-kit-wp repository following a strict quality workflow. + +═══════════════════════════════════════════════════════════════════════════════ +CRITICAL INSTRUCTIONS +═══════════════════════════════════════════════════════════════════════════════ + +- You MUST ACTUALLY CREATE, MODIFY, and DELETE files in the repository +- DO NOT just output proposed changes or describe what should be done +- Use your developer tools to make ACTUAL file modifications +- This is a LOCAL implementation - DO NOT create commits, pull requests, or push to remote +- All changes remain local for the developer to review, test, and commit manually + +═══════════════════════════════════════════════════════════════════════════════ +WORKFLOW OVERVIEW +═══════════════════════════════════════════════════════════════════════════════ + +This is a multi-phase workflow with quality enforcement: +1. Fetch GitHub Issue +2. Reference Context Documentation +3. Implement Changes +4. Code Review (self-review with scoring) +5. Iteration (up to 3 times if score < 0.85) +6. Documentation (optional, for new architectural patterns) + +You will track your iteration count and exit gracefully if unable to reach a score of 0.85 after 3 iterations. + +═══════════════════════════════════════════════════════════════════════════════ +PHASE 1: FETCH GITHUB ISSUE +═══════════════════════════════════════════════════════════════════════════════ + +**Objective**: Retrieve the issue details and extract implementation instructions + +**Steps**: +1. Use GitHub MCP tools to fetch issue #{{args}} from repository `google/site-kit-wp` + - If MCP tools are not available, use: !{gh issue view {{args}} --repo google/site-kit-wp --json title,body} + +2. Extract the "Implementation Brief" section from the issue body + - This section typically appears between "## Implementation Brief" and "## QA Brief" + - If the section markers are not present, use the entire issue body as the implementation task + +**Error Handling**: +- If the issue does not exist: STOP and report "ERROR: Issue #{{args}} not found in google/site-kit-wp" +- If GitHub API is unavailable: STOP and report the error with suggestions +- If the issue body is empty: STOP and report "ERROR: Issue #{{args}} has no description" + +**Output**: Store the implementation instructions for use in subsequent phases + +═══════════════════════════════════════════════════════════════════════════════ +PHASE 2: REFERENCE CONTEXT DOCUMENTATION +═══════════════════════════════════════════════════════════════════════════════ + +**Objective**: Understand the mandatory conventions and patterns for this codebase + +**Context Files Available**: +All project context files are already loaded via .gemini/settings.json. You have access to: + +**JavaScript/React Context** (13 files in docs/context/js/): +- component-conventions.md - Component structure and naming +- module-architecture.md - Module organization patterns +- state-management.md - State management with WordPress data stores +- hooks.md - React hooks usage and custom hooks +- tests.md - Testing conventions and patterns +- jsdoc.md - Documentation standards +- event-tracking.md - Analytics and event tracking +- feature-flags.md - Feature flag implementation +- feature-tours.md - User onboarding tours +- notifications.md - Notification system patterns +- widgets.md - Dashboard widget creation +- storybook.md - Component story requirements +- utils.md - Utility function conventions + +**PHP Context** (11 files in docs/context/php/): +- module-architecture.md - PHP module patterns +- settings-management.md - Settings API usage +- dependency-injection.md - DI container patterns +- context-pattern.md - Context object usage +- admin-features.md - Admin interface patterns +- rest-api.md - REST API conventions +- asset-management.md - Asset registration +- storage-patterns.md - Data storage patterns +- prompts-and-dismissals.md - User prompt patterns +- trait-composition.md - PHP trait usage +- naming-conventions.md - PHP naming standards + +**Project Overview**: +- AGENTS.md - High-level project architecture and development workflow + +**CRITICAL**: These files define MANDATORY conventions. Any deviation is considered a critical violation. + +═══════════════════════════════════════════════════════════════════════════════ +PHASE 3: IMPLEMENTATION +═══════════════════════════════════════════════════════════════════════════════ + +**Objective**: Implement the issue following documented principles + +Follow these implementation guidelines: + +**1. Understand the Task** + - Carefully read the Implementation Brief from Phase 1 + - Break down complex requirements into discrete steps + - Plan your implementation approach before writing code + - Identify all files that need to be created, modified, or deleted + +**2. Implement Following Strict Principles** + - Strictly follow ALL conventions from context documentation + - Study existing code patterns in similar features before implementing + - Use appropriate naming conventions from context docs + - Implement proper error handling and consider edge cases + - Ensure accessibility when implementing UI components + - Follow security best practices (avoid XSS, SQL injection, etc.) + - Use existing utilities and patterns rather than reinventing + +**3. Write Comprehensive Tests** + - **For JS**: Follow testing conventions from docs/context/js/tests.md + - **For PHP**: Follow PHPUnit patterns from existing tests in tests/phpunit/ + - Write unit tests for ALL new functionality + - Include tests for edge cases and error scenarios + - Ensure tests are clear, maintainable, and follow existing patterns + - Mock external dependencies appropriately + - Aim for thorough test coverage of business logic + +**4. Document Your Code** + - **For JS**: Follow JSDoc conventions from docs/context/js/jsdoc.md + - **For PHP**: Follow PHPDoc conventions and WordPress documentation standards + - Document all exported functions, components, classes, and complex logic + - Include clear descriptions, parameter types, and return types + - Add inline comments for non-obvious logic + - Update README files if introducing new features + +**5. Consider Integration Points** + + **For JavaScript/React**: + - **Event Tracking**: Use patterns from docs/context/js/event-tracking.md if user actions need tracking + - **Feature Flags**: Follow docs/context/js/feature-flags.md if feature should be toggleable + - **Feature Tours**: Follow docs/context/js/feature-tours.md for new UI features + - **Notifications**: Use docs/context/js/notifications.md for user notifications + - **State Management**: Follow docs/context/js/state-management.md for data stores + - **Widgets**: Follow docs/context/js/widgets.md for dashboard widgets + + **For PHP**: + - **Module Architecture**: Follow docs/context/php/module-architecture.md for module structure + - **Settings**: Use docs/context/php/settings-management.md for settings API + - **REST API**: Follow docs/context/php/rest-api.md for REST endpoints + - **Dependency Injection**: Use docs/context/php/dependency-injection.md for DI patterns + - **Storage**: Follow docs/context/php/storage-patterns.md for data storage + - **Context Pattern**: Use docs/context/php/context-pattern.md for context objects + - **Asset Management**: Follow docs/context/php/asset-management.md for registering assets + - **Admin Features**: Use docs/context/php/admin-features.md for admin UI + - **Prompts**: Follow docs/context/php/prompts-and-dismissals.md for user prompts + +**6. Create Supporting Files** + - **For JS**: Add Storybook stories for UI components (docs/context/js/storybook.md) + - **For PHP**: Create appropriate test fixtures and mock data + - Create fixture data for tests if needed + - Update or create example usage if helpful + +**7. Verify Your Implementation** + Run these verification steps: + + a. **Linting**: + - **For JS changes**: !{npm run lint:js} + - **For PHP changes**: !{composer run lint} + - Fix any linting errors immediately before proceeding + - Do not proceed with failing lints + + b. **Tests**: Run tests for changed files + - **For JS changes**: Identify and run related Jest tests + - **For PHP changes**: Run related PHPUnit tests + - Ensure all tests pass + - Fix any failing tests before proceeding + + c. **Build**: If you made significant changes, verify the build works + - Run !{npm run build} if appropriate + - Fix any build errors + +**8. Provide Implementation Summary** + After completing implementation, provide a structured summary: + + ``` + IMPLEMENTATION SUMMARY + ====================== + Files Created: [count] + - [file paths with brief description] + + Files Modified: [count] + - [file paths with brief description of changes] + + Files Deleted: [count] + - [file paths with reason for deletion] + + Key Features Implemented: + - [feature 1 with brief description] + - [feature 2 with brief description] + + Tests Added/Modified: + - [test file paths with coverage description] + - Edge cases covered: [list] + + Context Compliance: + - [list of context conventions followed] + - [reference specific docs used] + + Verification Results: + - Linting: [pass/fail with details] + - Tests: [pass/fail with details] + - Build: [if applicable] + ``` + +═══════════════════════════════════════════════════════════════════════════════ +PHASE 4: CODE REVIEW +═══════════════════════════════════════════════════════════════════════════════ + +**Objective**: Evaluate implementation quality and adherence to documented principles + +Follow these code review guidelines: + +**1. Verify Context Adherence (MANDATORY)** + + Check strict compliance with documented principles. Review each relevant context file: + + **For JavaScript/React changes**: + - ✓ Component structure follows component-conventions.md + - ✓ Module organization follows module-architecture.md + - ✓ State management follows state-management.md patterns + - ✓ Hooks usage follows hooks.md guidelines + - ✓ Tests follow tests.md conventions + - ✓ JSDoc follows jsdoc.md standards + - ✓ Event tracking follows event-tracking.md (if applicable) + - ✓ Feature flags follow feature-flags.md (if applicable) + - ✓ Tours follow feature-tours.md (if applicable) + - ✓ Notifications follow notifications.md (if applicable) + - ✓ Widgets follow widgets.md (if applicable) + - ✓ Storybook stories follow storybook.md (if UI components) + - ✓ Utils follow utils.md (if utility functions) + + **For PHP changes**: + - ✓ Module structure follows php/module-architecture.md + - ✓ Settings follow php/settings-management.md + - ✓ DI follows php/dependency-injection.md + - ✓ Context usage follows php/context-pattern.md + - ✓ Admin features follow php/admin-features.md + - ✓ REST API follows php/rest-api.md + - ✓ Assets follow php/asset-management.md + - ✓ Storage follows php/storage-patterns.md + - ✓ Prompts follow php/prompts-and-dismissals.md + - ✓ Traits follow php/trait-composition.md + - ✓ Naming follows php/naming-conventions.md + + **CRITICAL**: Document ANY violation found: + - Which principle was violated + - Which context file defines the violated principle (with section reference) + - How the code violates it (specific examples) + - What must be fixed to comply + - Which files are affected + +**2. Analyze Code Quality** + + Review for general code quality issues: + - Code structure and organization + - Readability and maintainability + - Error handling and edge cases + - Security vulnerabilities (XSS, injection, auth bypass, etc.) + - Performance issues (unnecessary re-renders, N+1 queries, etc.) + - Test coverage and test quality + - Documentation completeness and clarity + - Accessibility compliance (WCAG standards) + - Browser/PHP version compatibility + +**3. Score the Implementation (0.0 to 1.0)** + + Assign a score based on this rubric: + + **0.0 - 0.5**: Poor quality OR contains principle violations + - Use this range if ANY documented principles are violated + - Use this if critical security or functionality issues exist + - Use this if tests are missing or failing + + **0.5 - 0.84**: Below acceptable quality + - Code works but doesn't meet quality standards + - Multiple code quality issues present + - Tests exist but are incomplete + - Documentation is sparse or unclear + + **0.85 - 0.94**: Good quality, production-ready + - Follows all documented principles + - Code is clean and maintainable + - Comprehensive tests with good coverage + - Well documented + - Minor improvements possible but not required + + **0.95 - 1.0**: Excellent, exemplary code + - Exceeds quality standards + - Perfect adherence to all principles + - Exceptional test coverage + - Outstanding documentation + - Sets a positive example for future development + +**4. Provide Review Results** + + Format your review as follows: + + ``` + CODE REVIEW RESULTS + =================== + Score: [0.0 - 1.0] + Status: [approved | needs_improvement] + Iteration: [current iteration number, 1-3] + + CONTEXT VIOLATIONS: [count] + + [If violations exist, list each:] + + Violation #[n]: + - Principle: [what was violated] + - Context File: [docs/context/.../file.md - section name] + - Violation Details: [how code violates the principle] + - Fix Required: [specific steps to bring into compliance] + - Affected Files: [list of files] + + QUALITY RECOMMENDATIONS: [count] + + [List recommendations by priority:] + + Recommendation #[n]: + - Category: [Security | Performance | Maintainability | Testing | Documentation | Accessibility] + - Priority: [critical | high | medium | low] + - Issue: [description of the issue] + - Suggestion: [specific actionable suggestion] + - Affected Files: [list of files] + + STRENGTHS: + - [positive aspects of the implementation] + - [things done well] + + FILES REVIEWED: [count] + - [list of all files reviewed] + ``` + +═══════════════════════════════════════════════════════════════════════════════ +PHASE 5: ITERATION (if score < 0.85) +═══════════════════════════════════════════════════════════════════════════════ + +**Objective**: Fix issues until code reaches acceptable quality (score ≥ 0.85) + +**Iteration Rules**: +- Maximum 3 iteration attempts +- Track your current iteration number (1, 2, or 3) +- Each iteration must address ALL context violations +- After each iteration, return to Phase 4 for re-review + +**If score < 0.85, follow these steps**: + +**Iteration [1, 2, or 3]**: + +1. **Address ALL Context Violations** (MANDATORY) + - Context violations are non-negotiable and must be fixed + - Follow the "Fix Required" instructions from the review + - Bring code into full compliance with documented principles + - Make ACTUAL file modifications to fix each violation + +2. **Address Critical and High Priority Recommendations** + - Fix security vulnerabilities immediately + - Address performance problems + - Improve code structure and maintainability + - Add missing tests or documentation + +3. **Re-run Verification** + - Run linting: !{npm run lint:js} + - Run tests for affected files + - Ensure fixes don't break existing functionality + +4. **Return to Phase 4** + - Re-review your updated implementation + - Provide new score and updated review results + - Include iteration number in review + +5. **Check Score** + - If score ≥ 0.85: Proceed to Phase 6 + - If score < 0.85 AND iteration < 3: Start next iteration + - If score < 0.85 AND iteration = 3: Execute graceful exit (below) + +**Graceful Exit** (if score < 0.85 after 3 iterations): + +If you cannot achieve a score of 0.85 after 3 iterations, exit with this summary: + +``` +IMPLEMENTATION INCOMPLETE - MANUAL INTERVENTION REQUIRED +========================================================= + +Final Score: [score after 3rd iteration] +Iterations Completed: 3 +Status: Needs manual review and fixes + +SUMMARY OF CHANGES MADE: +All changes have been preserved in the working directory for your review. + +Files Created: [count] +- [list with descriptions] + +Files Modified: [count] +- [list with descriptions] + +Files Deleted: [count] +- [list] + +REMAINING ISSUES: + +Context Violations Still Present: [count] +[List each violation with fix instructions] + +Quality Issues Still Present: [count] +[List critical and high priority issues] + +RECOMMENDATIONS FOR MANUAL FIXES: + +1. [Most important fix needed] + - Files: [list] + - Action: [specific steps] + - Reference: [context doc section] + +2. [Second most important fix] + - Files: [list] + - Action: [specific steps] + +[Continue for top 5 issues] + +WHAT WAS COMPLETED SUCCESSFULLY: +- [aspects that meet quality standards] +- [working features] + +FILES REQUIRING ATTENTION: +Priority 1 (Critical): [files with violations] +Priority 2 (Important): [files with quality issues] +Priority 3 (Polish): [files with minor improvements] + +NEXT STEPS: +1. Review the changes made in the working directory +2. Address the remaining context violations (see above) +3. Fix critical quality issues +4. Re-run tests: npm test +5. Re-run linting: npm run lint +6. Consider manual code review with team +``` + +After providing this summary, STOP. Do not attempt further fixes. + +═══════════════════════════════════════════════════════════════════════════════ +PHASE 6: DOCUMENTATION (optional - only if score ≥ 0.85) +═══════════════════════════════════════════════════════════════════════════════ + +**Objective**: Document new architectural patterns or significant concepts + +**When to create documentation**: +- Only if you introduced NEW architectural patterns not covered in docs/context/ +- Only if you made significant design decisions worth documenting +- Only if the implementation adds complex workflows or processes + +**Skip this phase if**: +- You only implemented standard features using existing patterns +- Everything follows documented conventions in docs/context/ +- No new architectural concepts were introduced + +**If documentation is needed**, follow these documentation guidelines: + +**1. Identify Documentation Needs** + - What new concepts were introduced? + - What architectural decisions were made? + - What complex patterns or workflows were created? + +**2. Create Developer Documentation** + - Location: docs/concepts/ directory + - Format: Markdown (.md files) + - Naming: Use descriptive kebab-case names (e.g., widget-lifecycle-management.md) + +**3. Documentation Structure** + Include these sections: + - **Overview**: What is this concept and why does it exist? + - **Core Principles**: Key architectural principles + - **How It Works**: Technical explanation of the pattern + - **Usage Examples**: Code examples showing how to use it + - **Best Practices**: Guidelines for working with this pattern + - **Common Pitfalls**: What to avoid + - **Related Concepts**: Links to related documentation + +**4. Documentation Style** + - Write for human developers, not AI agents + - Use clear, conversational language + - Include practical code examples + - Focus on WHY and HOW, not just WHAT + - Add diagrams if they help understanding (Mermaid or ASCII) + +**5. Example Documentation File**: + ```markdown + # [Concept Name] + + ## Overview + Brief description of what this is and why it exists. + + ## Core Principles + - Principle 1 + - Principle 2 + + ## Architecture + How this concept fits into the system... + + ## Usage Examples + \`\`\`javascript + // Example code + \`\`\` + + ## Best Practices + - Practice 1 + - Practice 2 + + ## Common Pitfalls + - Pitfall 1: Description and how to avoid + + ## Related Concepts + - [Link to related doc] + ``` + +**6. Update Existing Documentation** (if needed) + - If changes affect existing docs/concepts/ files, update them + - Mark deprecated information clearly + - Add new sections rather than replacing content + +═══════════════════════════════════════════════════════════════════════════════ +FINAL SUMMARY +═══════════════════════════════════════════════════════════════════════════════ + +After completing all phases (or exiting gracefully), provide a final summary: + +**If score ≥ 0.85 (Success)**: + +``` +IMPLEMENTATION COMPLETE ✓ +========================= + +Issue #{{args}}: [issue title] +Final Score: [score] +Status: Production-ready +Iterations: [number of iterations needed] + +FILES CHANGED: + +Created ([count]): +- [file path]: [brief description] + +Modified ([count]): +- [file path]: [brief description of changes] + +Deleted ([count]): +- [file path]: [reason] + +IMPLEMENTATION HIGHLIGHTS: +- [key feature 1] +- [key feature 2] +- [notable technical decisions] + +QUALITY ASSURANCE: +✓ All documented principles followed +✓ Comprehensive tests added +✓ Linting passed +✓ Tests passed +✓ Code reviewed and approved + +TEST COVERAGE: +- [areas covered by tests] +- [edge cases handled] + +DOCUMENTATION: +- JSDoc added for all new functions/components +- [Concept docs created if applicable] + +NEXT STEPS FOR DEVELOPER: +1. Review the changes in your working directory +2. Run the full test suite: npm test +3. Run full linting: npm run lint +4. Test manually in browser/environment +5. Create a commit: git add . && git commit -m "Implement issue #{{args}}" +6. Create a pull request if desired +7. Request code review from team + +The implementation is ready for your review and testing! +``` + +**If score < 0.85 after 3 iterations (Graceful Exit)**: +Use the "Graceful Exit" summary format from Phase 5. + +═══════════════════════════════════════════════════════════════════════════════ +IMPORTANT REMINDERS +═══════════════════════════════════════════════════════════════════════════════ + +✓ ACTUALLY CREATE AND MODIFY FILES - do not just describe changes +✓ DO NOT create commits or pull requests - keep all changes local +✓ DO NOT proceed to next phase if previous phase failed critically +✓ DO NOT accept score < 0.85 without trying to fix (up to 3 iterations) +✓ DO read and follow ALL context documentation +✓ DO fix ALL violations of documented principles +✓ DO exit gracefully after 3 iterations if unable to reach 0.85 +✓ DO preserve all changes for human review + +Begin with Phase 1: Fetch GitHub Issue #{{args}} +""" diff --git a/.gitignore b/.gitignore index dfe36a39090..19061d93043 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ !.gemini/ .gemini/* !.gemini/settings.json +!.gemini/commands/ +!.gemini/commands/** !.github !.vscode .vscode/* From 412a03804fd6a5a0dfd7e733f333cc67afa3550f Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Fri, 2 Jan 2026 12:07:42 +0200 Subject: [PATCH 04/20] Add GitHub Actions workflow to automate issue implementation via Gemini CLI. --- .github/workflows/gemini.yml | 123 +++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 .github/workflows/gemini.yml diff --git a/.github/workflows/gemini.yml b/.github/workflows/gemini.yml new file mode 100644 index 00000000000..dd40140d92c --- /dev/null +++ b/.github/workflows/gemini.yml @@ -0,0 +1,123 @@ +name: Gemini Implement Issue + +on: + workflow_dispatch: + inputs: + issue_number: + description: 'GitHub issue number to implement' + required: true + type: number + +jobs: + implement: + name: Implement Issue + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: write + issues: write + pull-requests: write + services: + mysql: + image: mysql:5.7 + env: + MYSQL_ROOT_PASSWORD: wordpress + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + env: + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + MYSQL_USER: root + MYSQL_PASSWORD: wordpress + MYSQL_DATABASE: wordpress_test + WP_VERSION: latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + + - name: Install SVN + run: sudo apt-get update && sudo apt-get install -y subversion + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + extensions: mysqli, runkit7, uopz + tools: composer:2.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Validate Composer configuration + run: composer validate --strict + + - name: Install Composer dependencies + run: composer install --no-interaction --no-progress + + - name: Install npm dependencies + run: npm ci -w assets -w storybook -w tests/js -w tests/e2e --include-workspace-root + + - name: Set up WordPress test data + run: tests/phpunit/bin/install-wp-tests.sh "${MYSQL_DATABASE}" "${MYSQL_USER}" "${MYSQL_PASSWORD}" "${DB_HOST}":"${DB_PORT}" "${WP_VERSION}" + + - name: Install Gemini CLI + run: | + npm install -g @google/gemini-cli + gemini --version + + - name: Run /implement command + run: | + gemini --yolo --model gemini-3-pro-preview --prompt "/implement ${{ inputs.issue_number }}" + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create Pull Request + id: create-pr + uses: peter-evans/create-pull-request@v7 + with: + commit-message: Implement issue #${{ inputs.issue_number }} + branch: gemini/issue-${{ inputs.issue_number }} + title: "Implement #${{ inputs.issue_number }}" + body: | + 🤖 Automated implementation of issue #${{ inputs.issue_number }} + + Generated by Gemini CLI using the `/implement` command. + + **Review checklist:** + - [ ] Code follows documented principles + - [ ] Tests pass locally + - [ ] Linting passes + - [ ] Manual testing completed + - [ ] Code review score ≥ 0.85 + + Closes #${{ inputs.issue_number }} + draft: true + + - name: Comment on issue + if: steps.create-pr.outputs.pull-request-number + run: | + gh issue comment ${{ inputs.issue_number }} --body "🤖 Automated implementation created in PR #${{ steps.create-pr.outputs.pull-request-number }} + + This is a draft PR generated by Gemini CLI. Please review the changes, run tests, and verify quality before marking ready for review." + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 244a263082a9d0821b404053a6af43ea25542d0b Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Fri, 2 Jan 2026 12:18:05 +0200 Subject: [PATCH 05/20] Add PHPUnit testing documentation and update Gemini settings to include it. --- .gemini/commands/implement.toml | 2 +- .gemini/settings.json | 1 + docs/context/php/phpunit.md | 955 ++++++++++++++++++++++++++++++++ 3 files changed, 957 insertions(+), 1 deletion(-) create mode 100644 docs/context/php/phpunit.md diff --git a/.gemini/commands/implement.toml b/.gemini/commands/implement.toml index 46ae0ec5367..e565a5c7a4a 100644 --- a/.gemini/commands/implement.toml +++ b/.gemini/commands/implement.toml @@ -115,7 +115,7 @@ Follow these implementation guidelines: **3. Write Comprehensive Tests** - **For JS**: Follow testing conventions from docs/context/js/tests.md - - **For PHP**: Follow PHPUnit patterns from existing tests in tests/phpunit/ + - **For PHP**: Follow PHPUnit patterns from docs/context/php/phpunit.md - Write unit tests for ALL new functionality - Include tests for edge cases and error scenarios - Ensure tests are clear, maintainable, and follow existing patterns diff --git a/.gemini/settings.json b/.gemini/settings.json index 7d246249e51..9416098f8b8 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -21,6 +21,7 @@ "docs/context/php/dependency-injection.md", "docs/context/php/module-architecture.md", "docs/context/php/naming-conventions.md", + "docs/context/php/phpunit.md", "docs/context/php/prompts-and-dismissals.md", "docs/context/php/rest-api.md", "docs/context/php/settings-management.md", diff --git a/docs/context/php/phpunit.md b/docs/context/php/phpunit.md new file mode 100644 index 00000000000..bf4fa4a7e2b --- /dev/null +++ b/docs/context/php/phpunit.md @@ -0,0 +1,955 @@ +# PHPUnit Testing + +Site Kit uses PHPUnit with the WordPress test framework to provide comprehensive integration testing for all PHP code. All tests are integration tests that interact with WordPress core functionality and the database. + +## Overview + +The PHPUnit testing system provides: + +- **Integration testing**: All tests interact with WordPress and database +- **WordPress test framework**: Uses `wp-phpunit` for WordPress-specific testing +- **Comprehensive test utilities**: Traits, fakes, and custom assertions +- **Contract testing**: Interface compliance validation through traits +- **Mock HTTP handlers**: Simulated Google API responses + +## Directory Structure + +**Location**: `tests/phpunit/` + +``` +tests/phpunit/ +├── bootstrap.php # Test suite bootstrap +├── wp-tests-config.php # WordPress test configuration +├── bin/ +│ └── install-wp-tests.sh # Test environment setup script +├── includes/ +│ ├── TestCase.php # Base test class +│ ├── Core/ +│ │ └── Modules/ +│ │ ├── SettingsTestCase.php # Settings test base class +│ │ ├── FakeModule.php # Module fake implementation +│ │ └── Module_With_*_ContractTests.php # Contract test traits +│ ├── Fake/ +│ │ ├── FakeHttp.php # Google API HTTP mock +│ │ ├── MethodSpy.php # Method invocation recorder +│ │ └── ... # Other test doubles +│ └── Utils/ +│ ├── ModulesHelperTrait.php # Module test helpers +│ ├── UserAuthenticationTrait.php # Auth test helpers +│ └── ... # Other utility traits +└── integration/ + ├── Core/ # Core functionality tests + │ ├── Authentication/ + │ ├── Modules/ + │ ├── Storage/ + │ └── ... + └── Modules/ # Module-specific tests + ├── Analytics_4/ + ├── AdSense/ + └── ... +``` + +## Base Test Classes + +### Primary Test Class + +**Location**: `tests/phpunit/includes/TestCase.php` + +All tests extend the base `TestCase` which provides WordPress integration: + +```php +namespace Google\Site_Kit\Tests; + +use WP_UnitTestCase; + +class TestCase extends WP_UnitTestCase { + protected $preserveGlobalState = false; + + public function set_up() { + parent::set_up(); + + // Initialize wp_scripts and wp_styles for each test + wp_scripts(); + wp_styles(); + } + + public function tear_down() { + parent::tear_down(); + + // Clean up globals + unset( $GLOBALS['current_screen'] ); + $GLOBALS['wp_scripts'] = null; + $GLOBALS['wp_styles'] = null; + } +} +``` + +**Key Features**: +- Extends WordPress `WP_UnitTestCase` for full WordPress integration +- Sets `$preserveGlobalState = false` to handle closures in global state +- Manages feature flags from `feature-flags.json` +- Provides custom assertions (see Assertions section) + +### Settings Test Base Class + +**Location**: `tests/phpunit/includes/Modules/SettingsTestCase.php` + +For testing module settings classes: + +```php +namespace Google\Site_Kit\Tests\Modules; + +use Google\Site_Kit\Tests\TestCase; + +abstract class SettingsTestCase extends TestCase { + protected $object; + + /** + * Get the option name for the setting. + * + * @return string + */ + abstract protected function get_option_name(); + + public function set_up() { + parent::set_up(); + + $option_name = $this->get_option_name(); + + // Unregister the setting + unregister_setting( 'group', $option_name ); + + // Remove all filters for the option + remove_all_filters( "sanitize_option_{$option_name}" ); + remove_all_filters( "default_option_{$option_name}" ); + + // Delete option and site_option + delete_option( $option_name ); + delete_site_option( $option_name ); + } +} +``` + +**Usage**: + +```php +use Google\Site_Kit\Tests\Modules\SettingsTestCase; +use Google\Site_Kit\Modules\Analytics_4\Settings; + +class SettingsTest extends SettingsTestCase { + protected function get_option_name() { + return Settings::OPTION; + } + + public function set_up() { + parent::set_up(); + + $this->object = new Settings( $this->options ); + } + + public function test_register() { + $this->object->register(); + + $this->assertSettingRegistered( Settings::OPTION ); + } +} +``` + +## Test Naming Conventions + +### File Naming + +Tests mirror the source code structure: + +``` +Source: includes/Modules/Analytics_4/Settings.php +Test: tests/phpunit/integration/Modules/Analytics_4/SettingsTest.php + +Source: includes/Core/Authentication/Authentication.php +Test: tests/phpunit/integration/Core/Authentication/AuthenticationTest.php +``` + +**Pattern**: `{ClassName}Test.php` + +### Test Method Naming + +```php +// Basic test +public function test_register() + +// Test with specific scenario +public function test_get_default() + +// Test with multiple scenarios (use descriptive suffixes) +public function test_network_mode_get() +public function test_get_data__current_module_owner_without_shared_role() + +// Data provider pattern +/** + * @dataProvider data_tag_ids + */ +public function test_google_tag_ids( $tag, $id, $expected ) + +public function data_tag_ids() { + return array( + 'googleTagID is valid G-XXXX string' => array( 'googleTagID', 'G-XXXX', 'G-XXXX' ), + 'googleTagID is invalid string' => array( 'googleTagID', 'xxxx', '' ), + ); +} +``` + +**Pattern**: `test_{method_or_behavior}__{specific_case}` + +## Setup and Teardown Patterns + +### Standard Setup + +```php +private $context; +private $options; +private $user_options; +private $authentication; +private $user; + +public function set_up() { + parent::set_up(); + + $this->context = new Context( GOOGLESITEKIT_PLUGIN_MAIN_FILE ); + $this->options = new Options( $this->context ); + $this->user_options = new User_Options( $this->context ); + $this->authentication = new Authentication( $this->context, $this->options, $this->user_options ); + + // Create test user + $this->user = $this->factory()->user->create_and_get( array( 'role' => 'administrator' ) ); + + wp_set_current_user( $this->user->ID ); +} +``` + +### Module Test Setup + +```php +private $module; + +public function set_up() { + parent::set_up(); + + $this->context = new Context( GOOGLESITEKIT_PLUGIN_MAIN_FILE ); + $this->options = new Options( $this->context ); + + $this->module = new Analytics_4( $this->context, $this->options ); + + // Set up module dependencies + $this->activate_modules( 'analytics-4' ); +} +``` + +### Minimal Teardown + +Most cleanup is automatic via WordPress test framework: + +```php +public function tear_down() { + parent::tear_down(); + // Additional cleanup only if needed +} +``` + +**Note**: The WordPress test framework automatically: +- Resets database between tests +- Clears transients and caches +- Removes registered hooks +- Deletes created posts, users, terms + +## Test Doubles and Fakes + +### FakeHttp - Google API Mocking + +**Location**: `tests/phpunit/includes/Fake/FakeHttp.php` + +Mock Google API responses: + +```php +use Google\Site_Kit\Tests\Fake\FakeHttp; + +public function test_get_accounts() { + $google_client = new Google_Client(); + + $mock_accounts = array( + array( + 'name' => 'accounts/12345', + 'displayName' => 'Test Account', + ), + ); + + FakeHttp::fake_google_http_handler( + $google_client, + function ( Request $request ) use ( $mock_accounts ) { + return new Response( 200, array(), json_encode( $mock_accounts ) ); + } + ); + + $service = new GoogleAnalyticsAdmin( $google_client ); + $accounts = $service->accounts->listAccounts(); + + $this->assertCount( 1, $accounts ); +} +``` + +### MethodSpy - Invocation Recording + +**Location**: `tests/phpunit/includes/Fake/MethodSpy.php` + +Record method calls for verification: + +```php +use Google\Site_Kit\Tests\Fake\MethodSpy; + +public function test_method_called_with_args() { + $spy = new MethodSpy(); + + // Attach spy to an object method + add_filter( 'some_filter', array( $spy, 'callback' ) ); + + // Trigger the filter + apply_filters( 'some_filter', 'arg1', 'arg2' ); + + // Verify invocations + $this->assertCount( 1, $spy->invocations['callback'] ); + $this->assertEquals( array( 'arg1', 'arg2' ), $spy->invocations['callback'][0] ); +} +``` + +### FakeModule - Module Testing + +**Location**: `tests/phpunit/includes/Core/Modules/FakeModule.php` + +Complete fake module for testing module system: + +```php +use Google\Site_Kit\Tests\Core\Modules\FakeModule; + +public function test_module_registration() { + $module = new FakeModule( $this->context ); + + // Configure module behavior + $module->set_force_active( true ); + + $modules = new Modules( $this->context ); + $modules->register_module( $module ); + + $this->assertTrue( $modules->is_module_active( $module->slug ) ); +} +``` + +## Helper Traits + +### ModulesHelperTrait + +**Location**: `tests/phpunit/includes/Utils/ModulesHelperTrait.php` + +Module activation and connection helpers: + +```php +use Google\Site_Kit\Tests\Utils\ModulesHelperTrait; + +class MyTest extends TestCase { + use ModulesHelperTrait; + + public function test_module_functionality() { + // Activate modules + $this->activate_modules( 'analytics-4', 'adsense' ); + + // Force connect modules (skip OAuth) + $this->force_connect_modules( 'analytics-4' ); + + $this->assertTrue( $this->modules->is_module_connected( 'analytics-4' ) ); + } +} +``` + +### Fake_Site_Connection_Trait + +**Location**: `tests/phpunit/includes/Fake_Site_Connection_Trait.php` + +Mock Google OAuth connections: + +```php +use Google\Site_Kit\Tests\Fake_Site_Connection_Trait; + +class MyTest extends TestCase { + use Fake_Site_Connection_Trait; + + public function test_authenticated_request() { + // Mock site connection and authentication + $this->fake_site_connection(); + + // Now authentication checks will pass + $this->assertTrue( $this->authentication->is_authenticated() ); + } + + public function test_proxy_connection() { + // Mock proxy-based connection + $this->fake_proxy_site_connection(); + + $this->assertEquals( + Authentication::METHOD_PROXY, + $this->authentication->get_auth_method() + ); + } +} +``` + +### RestTestTrait + +**Location**: `tests/phpunit/includes/RestTestTrait.php` + +Register REST routes for testing: + +```php +use Google\Site_Kit\Tests\RestTestTrait; + +class REST_Controller_Test extends TestCase { + use RestTestTrait; + + public function set_up() { + parent::set_up(); + + // Ensure REST routes are registered + $this->register_rest_routes(); + } + + public function test_list_endpoint() { + $request = new WP_REST_Request( 'GET', '/google-site-kit/v1/core/modules/data/list' ); + $response = rest_do_request( $request ); + + $this->assertEquals( 200, $response->get_status() ); + } +} +``` + +### UserAuthenticationTrait + +**Location**: `tests/phpunit/includes/Utils/UserAuthenticationTrait.php` + +Set user access tokens: + +```php +use Google\Site_Kit\Tests\Utils\UserAuthenticationTrait; + +class MyTest extends TestCase { + use UserAuthenticationTrait; + + public function test_authenticated_api_request() { + $access_token = array( + 'access_token' => 'test-access-token', + 'expires_in' => 3600, + ); + + $this->set_user_access_token( $this->user->ID, $access_token ); + + $this->assertTrue( $this->authentication->is_authenticated() ); + } +} +``` + +## Contract Testing Traits + +Contract test traits validate interface implementations. + +### Module_With_Settings_ContractTests + +**Location**: `tests/phpunit/includes/Core/Modules/Module_With_Settings_ContractTests.php` + +```php +use Google\Site_Kit\Tests\Core\Modules\Module_With_Settings_ContractTests; + +class Analytics_4Test extends TestCase { + use Module_With_Settings_ContractTests; + + public function test_get_settings() { + $this->assertInstanceOf( + Settings::class, + $this->module->get_settings() + ); + } +} +``` + +**Available Contract Traits**: +- `Module_With_Settings_ContractTests` +- `Module_With_Owner_ContractTests` +- `Module_With_Scopes_ContractTests` +- `Module_With_Service_Entity_ContractTests` +- `Module_With_Data_Available_State_ContractTests` + +## Custom Assertions + +### WordPress-Specific Assertions + +```php +// Option assertions +$this->assertOptionExists( 'googlesitekit_option_name' ); +$this->assertOptionNotExists( 'googlesitekit_option_name' ); + +// Transient assertions +$this->assertTransientExists( 'googlesitekit_transient' ); + +// Post meta assertions +$this->assertPostMetaHasValue( $post_id, 'meta_key', 'expected_value' ); + +// Setting registration +$this->assertSettingRegistered( 'googlesitekit_setting' ); + +// WP_Error assertions +$result = $this->modules->activate_module( 'invalid-module' ); +$this->assertWPError( $result ); +$this->assertWPErrorWithMessage( 'Invalid module', $result ); +``` + +### Using force_get_property and force_set_property + +Test private/protected properties: + +```php +// Set protected property +$this->force_set_property( $object, 'private_property', 'test_value' ); + +// Get protected property +$value = $this->force_get_property( $object, 'private_property' ); + +$this->assertEquals( 'test_value', $value ); +``` + +## Testing Patterns + +### Testing Module Registration + +```php +public function test_register() { + $this->module->register(); + + // Verify settings registered + $this->assertSettingRegistered( Settings::OPTION ); + + // Verify REST routes registered + $this->register_rest_routes(); + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( + '/google-site-kit/v1/modules/analytics-4/data/accounts', + $routes + ); + + // Verify hooks registered + $this->assertTrue( has_action( 'wp_head' ) ); +} +``` + +### Testing Settings + +```php +public function test_get_default() { + $settings = $this->module->get_settings(); + + $expected = array( + 'accountID' => '', + 'propertyID' => '', + 'webDataStreamID' => '', + 'measurementID' => '', + ); + + $this->assertEquals( $expected, $settings->get() ); +} + +public function test_settings_validation() { + $settings = $this->module->get_settings(); + + // Invalid data should return WP_Error + $result = $settings->set( array( 'accountID' => 'invalid' ) ); + $this->assertWPError( $result ); + + // Valid data should be saved + $valid_data = array( 'accountID' => 'accounts/12345' ); + $settings->set( $valid_data ); + + $this->assertEquals( 'accounts/12345', $settings->get()['accountID'] ); +} +``` + +### Testing REST Endpoints + +```php +public function test_get_data_endpoint() { + $this->register_rest_routes(); + + // Mock Google API response + $this->fake_site_connection(); + + $request = new WP_REST_Request( + 'GET', + '/google-site-kit/v1/modules/analytics-4/data/accounts' + ); + + $response = rest_do_request( $request ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertIsArray( $response->get_data() ); +} + +public function test_endpoint_requires_authentication() { + $this->register_rest_routes(); + + // Remove authentication + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( + 'GET', + '/google-site-kit/v1/modules/analytics-4/data/accounts' + ); + + $response = rest_do_request( $request ); + + $this->assertEquals( 403, $response->get_status() ); +} +``` + +### Testing with Data Providers + +```php +/** + * @dataProvider provider_invalid_property_ids + */ +public function test_invalid_property_ids( $property_id ) { + $settings = $this->module->get_settings(); + + $result = $settings->set( array( 'propertyID' => $property_id ) ); + + $this->assertWPError( $result ); +} + +public function provider_invalid_property_ids() { + return array( + 'empty string' => array( '' ), + 'numeric' => array( 12345 ), + 'invalid format' => array( 'properties-12345' ), + 'missing prefix' => array( '12345' ), + ); +} +``` + +### Testing Redirects + +```php +use Google\Site_Kit\Tests\Exception\RedirectException; + +public function test_redirect_on_action() { + try { + $this->controller->handle_redirect_action(); + + $this->fail( 'Expected RedirectException was not thrown' ); + } catch ( RedirectException $e ) { + $this->assertEquals( 'https://example.com/redirect', $e->get_location() ); + $this->assertEquals( 302, $e->get_status() ); + } +} +``` + +### Testing HTTP Requests + +```php +public function test_http_request_made() { + $request_url = null; + + $unsubscribe = $this->subscribe_to_wp_http_requests( + function ( $url, $args ) use ( &$request_url ) { + $request_url = $url; + }, + array( + 'response' => array( 'code' => 200 ), + 'body' => json_encode( array( 'success' => true ) ), + ) + ); + + $this->module->make_api_request(); + + $this->assertStringContainsString( + 'https://www.googleapis.com/analytics', + $request_url + ); + + $unsubscribe(); // Clean up subscription +} +``` + +### Testing Multisite + +```php +/** + * @group ms-required + */ +public function test_network_mode_functionality() { + $this->network_activate_site_kit(); + + add_filter( 'googlesitekit_is_network_mode', '__return_true' ); + + $this->assertTrue( $this->context->is_network_mode() ); +} + +/** + * @group ms-excluded + */ +public function test_single_site_only_feature() { + // This test only runs on single site + $this->assertFalse( is_multisite() ); +} +``` + +### Testing Feature Flags + +```php +public function test_feature_flag_enabled() { + $reset = $this->enable_feature( 'userInput' ); + + $this->assertTrue( Feature_Flags::enabled( 'userInput' ) ); + + // Reset feature flag after test + $reset(); + + $this->assertFalse( Feature_Flags::enabled( 'userInput' ) ); +} +``` + +## PHPUnit Groups + +Organize tests with `@group` annotations: + +```php +/** + * @group Modules + * @group Analytics + */ +class Analytics_4Test extends TestCase { + // Tests here +} +``` + +**Common Groups**: +- `@group Modules` - Module tests +- `@group Storage` - Storage-related tests +- `@group Authentication` - Authentication tests +- `@group Assets` - Asset management tests +- `@group Util` - Utility function tests +- `@group ms-required` - Requires multisite +- `@group ms-excluded` - Excluded from multisite + +**Running Specific Groups**: +```bash +# Run only module tests +composer test:php -- --group=Modules + +# Run only Analytics tests +composer test:php -- --group=Analytics + +# Exclude multisite tests +composer test:php -- --exclude-group=ms-required +``` + +## WordPress Test Configuration + +### Test Database + +**Location**: `tests/phpunit/wp-tests-config.php` + +```php +define( 'DB_NAME', 'wordpress_test' ); +define( 'DB_USER', 'root' ); +define( 'DB_PASSWORD', '' ); +define( 'DB_HOST', 'mysql' ); + +// Block external HTTP requests +define( 'WP_HTTP_BLOCK_EXTERNAL', true ); + +// Enable debug mode +define( 'WP_DEBUG', true ); +``` + +### Bootstrap + +**Location**: `tests/phpunit/bootstrap.php` + +The bootstrap file: +1. Loads WordPress test framework +2. Loads Site Kit plugin +3. Initializes test environment +4. Sets up autoloading for test classes + +## Best Practices + +### DO + +1. **Always extend TestCase** + ```php + use Google\Site_Kit\Tests\TestCase; + + class MyTest extends TestCase { + ``` + +2. **Use snake_case for setup/teardown** + ```php + public function set_up() { + parent::set_up(); + } + + public function tear_down() { + parent::tear_down(); + } + ``` + +3. **Call parent methods first** + ```php + public function set_up() { + parent::set_up(); // Always first + + // Your setup code + } + ``` + +4. **Use helper traits** + ```php + use ModulesHelperTrait; + use Fake_Site_Connection_Trait; + + $this->activate_modules( 'analytics-4' ); + $this->fake_site_connection(); + ``` + +5. **Mock Google API calls** + ```php + FakeHttp::fake_google_http_handler( $google_client, $handler ); + ``` + +6. **Use custom assertions** + ```php + $this->assertOptionExists( 'option_name' ); + $this->assertWPError( $result ); + ``` + +7. **Test both success and failure cases** + ```php + public function test_valid_input() { /* ... */ } + public function test_invalid_input() { /* ... */ } + ``` + +8. **Use descriptive test names** + ```php + public function test_get_accounts__without_authentication__returns_wp_error() + ``` + +### DON'T + +1. **Don't test WordPress core functionality** + ```php + // Bad - testing WordPress core + public function test_add_option_creates_option() { + add_option( 'test', 'value' ); + $this->assertEquals( 'value', get_option( 'test' ) ); + } + ``` + +2. **Don't make real API requests** + ```php + // Bad - makes real HTTP request + public function test_api_request() { + $response = wp_remote_get( 'https://www.googleapis.com/...' ); + } + + // Good - mock HTTP requests + public function test_api_request() { + FakeHttp::fake_google_http_handler( $client, $handler ); + } + ``` + +3. **Don't skip setup/teardown** + ```php + // Bad - missing parent call + public function set_up() { + $this->object = new MyClass(); + } + + // Good + public function set_up() { + parent::set_up(); + $this->object = new MyClass(); + } + ``` + +4. **Don't test implementation details** + ```php + // Bad - testing private method implementation + public function test_private_method_logic() { + $value = $this->force_get_property( $object, 'private_prop' ); + // Testing internal state + } + + // Good - test public API behavior + public function test_public_method_returns_expected_result() { + $result = $object->public_method(); + $this->assertEquals( 'expected', $result ); + } + ``` + +5. **Don't leave dangling filters/actions** + ```php + // WordPress test framework automatically cleans up hooks + // But if you need manual cleanup: + remove_filter( 'filter_name', array( $this, 'callback' ) ); + ``` + +## Running Tests + +### Run All Tests + +```bash +composer test:php +``` + +### Run Specific Test File + +```bash +composer test:php tests/phpunit/integration/Modules/Analytics_4/SettingsTest.php +``` + +### Run Specific Test Method + +```bash +composer test:php --filter=test_register +``` + +### Run Tests with Coverage + +```bash +composer test:php -- --coverage-html coverage/ +``` + +### Run Tests for Specific Group + +```bash +composer test:php -- --group=Modules +composer test:php -- --group=Analytics +``` + +## Code Style + +PHPUnit tests follow WordPress coding standards with some exceptions: + +```php +// Disable assertion message requirement +// phpcs:disable PHPCS.PHPUnit.RequireAssertionMessage.MissingAssertionMessage + +public function test_something() { + // Most tests don't include assertion messages + $this->assertTrue( $condition ); +} +``` + +**Note**: The codebase intentionally omits assertion messages in most tests for brevity. This is documented in test files. From b049e255cd1accdac051be7dd17b82b70e643a0b Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Tue, 6 Jan 2026 17:25:15 +0200 Subject: [PATCH 06/20] Remove unnecessary `--prompt` flag from Gemini CLI command. --- .github/workflows/gemini.yml | 234 +++++++++++++++++------------------ 1 file changed, 117 insertions(+), 117 deletions(-) diff --git a/.github/workflows/gemini.yml b/.github/workflows/gemini.yml index dd40140d92c..b04e075ab93 100644 --- a/.github/workflows/gemini.yml +++ b/.github/workflows/gemini.yml @@ -1,123 +1,123 @@ name: Gemini Implement Issue on: - workflow_dispatch: - inputs: - issue_number: - description: 'GitHub issue number to implement' - required: true - type: number + workflow_dispatch: + inputs: + issue_number: + description: 'GitHub issue number to implement' + required: true + type: number jobs: - implement: - name: Implement Issue - runs-on: ubuntu-latest - timeout-minutes: 60 - permissions: - contents: write - issues: write - pull-requests: write - services: - mysql: - image: mysql:5.7 + implement: + name: Implement Issue + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: write + issues: write + pull-requests: write + services: + mysql: + image: mysql:5.7 + env: + MYSQL_ROOT_PASSWORD: wordpress + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 env: - MYSQL_ROOT_PASSWORD: wordpress - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 - env: - DB_HOST: 127.0.0.1 - DB_PORT: 3306 - MYSQL_USER: root - MYSQL_PASSWORD: wordpress - MYSQL_DATABASE: wordpress_test - WP_VERSION: latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: .nvmrc - cache: npm - - - name: Install SVN - run: sudo apt-get update && sudo apt-get install -y subversion - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - extensions: mysqli, runkit7, uopz - tools: composer:2.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Get Composer Cache Directory - id: composer-cache - run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" - - - name: Cache Composer dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-composer- - - - name: Validate Composer configuration - run: composer validate --strict - - - name: Install Composer dependencies - run: composer install --no-interaction --no-progress - - - name: Install npm dependencies - run: npm ci -w assets -w storybook -w tests/js -w tests/e2e --include-workspace-root - - - name: Set up WordPress test data - run: tests/phpunit/bin/install-wp-tests.sh "${MYSQL_DATABASE}" "${MYSQL_USER}" "${MYSQL_PASSWORD}" "${DB_HOST}":"${DB_PORT}" "${WP_VERSION}" - - - name: Install Gemini CLI - run: | - npm install -g @google/gemini-cli - gemini --version - - - name: Run /implement command - run: | - gemini --yolo --model gemini-3-pro-preview --prompt "/implement ${{ inputs.issue_number }}" - env: - GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Create Pull Request - id: create-pr - uses: peter-evans/create-pull-request@v7 - with: - commit-message: Implement issue #${{ inputs.issue_number }} - branch: gemini/issue-${{ inputs.issue_number }} - title: "Implement #${{ inputs.issue_number }}" - body: | - 🤖 Automated implementation of issue #${{ inputs.issue_number }} - - Generated by Gemini CLI using the `/implement` command. - - **Review checklist:** - - [ ] Code follows documented principles - - [ ] Tests pass locally - - [ ] Linting passes - - [ ] Manual testing completed - - [ ] Code review score ≥ 0.85 - - Closes #${{ inputs.issue_number }} - draft: true - - - name: Comment on issue - if: steps.create-pr.outputs.pull-request-number - run: | - gh issue comment ${{ inputs.issue_number }} --body "🤖 Automated implementation created in PR #${{ steps.create-pr.outputs.pull-request-number }} - - This is a draft PR generated by Gemini CLI. Please review the changes, run tests, and verify quality before marking ready for review." - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + MYSQL_USER: root + MYSQL_PASSWORD: wordpress + MYSQL_DATABASE: wordpress_test + WP_VERSION: latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + + - name: Install SVN + run: sudo apt-get update && sudo apt-get install -y subversion + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + extensions: mysqli, runkit7, uopz + tools: composer:2.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Validate Composer configuration + run: composer validate --strict + + - name: Install Composer dependencies + run: composer install --no-interaction --no-progress + + - name: Install npm dependencies + run: npm ci -w assets -w storybook -w tests/js -w tests/e2e --include-workspace-root + + - name: Set up WordPress test data + run: tests/phpunit/bin/install-wp-tests.sh "${MYSQL_DATABASE}" "${MYSQL_USER}" "${MYSQL_PASSWORD}" "${DB_HOST}":"${DB_PORT}" "${WP_VERSION}" + + - name: Install Gemini CLI + run: | + npm install -g @google/gemini-cli + gemini --version + + - name: Run /implement command + run: | + gemini --yolo --model gemini-3-pro-preview "/implement ${{ inputs.issue_number }}" + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create Pull Request + id: create-pr + uses: peter-evans/create-pull-request@v7 + with: + commit-message: Implement issue #${{ inputs.issue_number }} + branch: gemini/issue-${{ inputs.issue_number }} + title: 'Implement #${{ inputs.issue_number }}' + body: | + 🤖 Automated implementation of issue #${{ inputs.issue_number }} + + Generated by Gemini CLI using the `/implement` command. + + **Review checklist:** + - [ ] Code follows documented principles + - [ ] Tests pass locally + - [ ] Linting passes + - [ ] Manual testing completed + - [ ] Code review score ≥ 0.85 + + Closes #${{ inputs.issue_number }} + draft: true + + - name: Comment on issue + if: steps.create-pr.outputs.pull-request-number + run: | + gh issue comment ${{ inputs.issue_number }} --body "🤖 Automated implementation created in PR #${{ steps.create-pr.outputs.pull-request-number }} + + This is a draft PR generated by Gemini CLI. Please review the changes, run tests, and verify quality before marking ready for review." + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 7daa5b0da04b69e68a9512a0b243dd7db2c67279 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Tue, 6 Jan 2026 17:28:44 +0200 Subject: [PATCH 07/20] Pin `@google/gemini-cli` to version `0.22.5` in the Gemini workflow. --- .github/workflows/gemini.yml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/.github/workflows/gemini.yml b/.github/workflows/gemini.yml index b04e075ab93..ec1ebce7813 100644 --- a/.github/workflows/gemini.yml +++ b/.github/workflows/gemini.yml @@ -32,20 +32,16 @@ jobs: MYSQL_PASSWORD: wordpress MYSQL_DATABASE: wordpress_test WP_VERSION: latest - steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version-file: .nvmrc cache: npm - - name: Install SVN run: sudo apt-get update && sudo apt-get install -y subversion - - name: Setup PHP uses: shivammathur/setup-php@v2 with: @@ -54,11 +50,9 @@ jobs: tools: composer:2.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Get Composer Cache Directory id: composer-cache run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" - - name: Cache Composer dependencies uses: actions/cache@v4 with: @@ -66,31 +60,24 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: | ${{ runner.os }}-composer- - - name: Validate Composer configuration run: composer validate --strict - - name: Install Composer dependencies run: composer install --no-interaction --no-progress - - name: Install npm dependencies run: npm ci -w assets -w storybook -w tests/js -w tests/e2e --include-workspace-root - - name: Set up WordPress test data run: tests/phpunit/bin/install-wp-tests.sh "${MYSQL_DATABASE}" "${MYSQL_USER}" "${MYSQL_PASSWORD}" "${DB_HOST}":"${DB_PORT}" "${WP_VERSION}" - - name: Install Gemini CLI run: | - npm install -g @google/gemini-cli + npm install -g @google/gemini-cli@0.22.5 gemini --version - - name: Run /implement command run: | gemini --yolo --model gemini-3-pro-preview "/implement ${{ inputs.issue_number }}" env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Create Pull Request id: create-pr uses: peter-evans/create-pull-request@v7 @@ -112,7 +99,6 @@ jobs: Closes #${{ inputs.issue_number }} draft: true - - name: Comment on issue if: steps.create-pr.outputs.pull-request-number run: | From 960ae91835d020229319df8d5bcca1745b447db8 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Tue, 6 Jan 2026 20:18:04 +0200 Subject: [PATCH 08/20] Reformat Gemini workflow YAML indentation. --- .github/workflows/gemini.yml | 198 +++++++++++++++++------------------ 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/.github/workflows/gemini.yml b/.github/workflows/gemini.yml index ec1ebce7813..b8e20ad8d36 100644 --- a/.github/workflows/gemini.yml +++ b/.github/workflows/gemini.yml @@ -1,109 +1,109 @@ name: Gemini Implement Issue on: - workflow_dispatch: - inputs: - issue_number: - description: 'GitHub issue number to implement' - required: true - type: number + workflow_dispatch: + inputs: + issue_number: + description: GitHub issue number to implement + required: true + type: number jobs: - implement: - name: Implement Issue - runs-on: ubuntu-latest - timeout-minutes: 60 - permissions: - contents: write - issues: write - pull-requests: write - services: - mysql: - image: mysql:5.7 - env: - MYSQL_ROOT_PASSWORD: wordpress - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + implement: + name: Implement Issue + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: write + issues: write + pull-requests: write + services: + mysql: + image: mysql:5.7 env: - DB_HOST: 127.0.0.1 - DB_PORT: 3306 - MYSQL_USER: root - MYSQL_PASSWORD: wordpress - MYSQL_DATABASE: wordpress_test - WP_VERSION: latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: .nvmrc - cache: npm - - name: Install SVN - run: sudo apt-get update && sudo apt-get install -y subversion - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - extensions: mysqli, runkit7, uopz - tools: composer:2.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Get Composer Cache Directory - id: composer-cache - run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" - - name: Cache Composer dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-composer- - - name: Validate Composer configuration - run: composer validate --strict - - name: Install Composer dependencies - run: composer install --no-interaction --no-progress - - name: Install npm dependencies - run: npm ci -w assets -w storybook -w tests/js -w tests/e2e --include-workspace-root - - name: Set up WordPress test data - run: tests/phpunit/bin/install-wp-tests.sh "${MYSQL_DATABASE}" "${MYSQL_USER}" "${MYSQL_PASSWORD}" "${DB_HOST}":"${DB_PORT}" "${WP_VERSION}" - - name: Install Gemini CLI - run: | - npm install -g @google/gemini-cli@0.22.5 - gemini --version - - name: Run /implement command - run: | - gemini --yolo --model gemini-3-pro-preview "/implement ${{ inputs.issue_number }}" - env: - GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Create Pull Request - id: create-pr - uses: peter-evans/create-pull-request@v7 - with: - commit-message: Implement issue #${{ inputs.issue_number }} - branch: gemini/issue-${{ inputs.issue_number }} - title: 'Implement #${{ inputs.issue_number }}' - body: | - 🤖 Automated implementation of issue #${{ inputs.issue_number }} + MYSQL_ROOT_PASSWORD: wordpress + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + env: + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + MYSQL_USER: root + MYSQL_PASSWORD: wordpress + MYSQL_DATABASE: wordpress_test + WP_VERSION: latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + - name: Install SVN + run: sudo apt-get update && sudo apt-get install -y subversion + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + extensions: mysqli, runkit7, uopz + tools: composer:2.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Get Composer Cache Directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + - name: Validate Composer configuration + run: composer validate --strict + - name: Install Composer dependencies + run: composer install --no-interaction --no-progress + - name: Install npm dependencies + run: npm ci -w assets -w storybook -w tests/js -w tests/e2e --include-workspace-root + - name: Set up WordPress test data + run: tests/phpunit/bin/install-wp-tests.sh "${MYSQL_DATABASE}" "${MYSQL_USER}" "${MYSQL_PASSWORD}" "${DB_HOST}":"${DB_PORT}" "${WP_VERSION}" + - name: Install Gemini CLI + run: | + npm install -g @google/gemini-cli@0.22.5 + gemini --version + - name: Run /implement command + run: | + gemini --yolo --model gemini-3-pro-preview "/implement ${{ inputs.issue_number }}" + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Create Pull Request + id: create-pr + uses: peter-evans/create-pull-request@v7 + with: + commit-message: Implement issue #${{ inputs.issue_number }} + branch: gemini/issue-${{ inputs.issue_number }} + title: 'Implement #${{ inputs.issue_number }}' + body: | + 🤖 Automated implementation of issue #${{ inputs.issue_number }} - Generated by Gemini CLI using the `/implement` command. + Generated by Gemini CLI using the `/implement` command. - **Review checklist:** - - [ ] Code follows documented principles - - [ ] Tests pass locally - - [ ] Linting passes - - [ ] Manual testing completed - - [ ] Code review score ≥ 0.85 + **Review checklist:** + - [ ] Code follows documented principles + - [ ] Tests pass locally + - [ ] Linting passes + - [ ] Manual testing completed + - [ ] Code review score ≥ 0.85 - Closes #${{ inputs.issue_number }} - draft: true - - name: Comment on issue - if: steps.create-pr.outputs.pull-request-number - run: | - gh issue comment ${{ inputs.issue_number }} --body "🤖 Automated implementation created in PR #${{ steps.create-pr.outputs.pull-request-number }} + Closes #${{ inputs.issue_number }} + draft: true + - name: Comment on issue + if: steps.create-pr.outputs.pull-request-number + run: | + gh issue comment ${{ inputs.issue_number }} --body "🤖 Automated implementation created in PR #${{ steps.create-pr.outputs.pull-request-number }} - This is a draft PR generated by Gemini CLI. Please review the changes, run tests, and verify quality before marking ready for review." - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + This is a draft PR generated by Gemini CLI. Please review the changes, run tests, and verify quality before marking ready for review." + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 4998b8af9d00ba58bfab3319c7d69b6d0ba3dc3f Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Thu, 8 Jan 2026 14:07:20 +0000 Subject: [PATCH 09/20] Apply suggestions from code review Co-authored-by: Evan Mattson --- .github/workflows/gemini.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gemini.yml b/.github/workflows/gemini.yml index b8e20ad8d36..f135572e9b4 100644 --- a/.github/workflows/gemini.yml +++ b/.github/workflows/gemini.yml @@ -82,7 +82,7 @@ jobs: id: create-pr uses: peter-evans/create-pull-request@v7 with: - commit-message: Implement issue #${{ inputs.issue_number }} + commit-message: 'Implement issue #${{ inputs.issue_number }}' branch: gemini/issue-${{ inputs.issue_number }} title: 'Implement #${{ inputs.issue_number }}' body: | From a4248f116afdfe84f91a82935dd42566b03686af Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Thu, 8 Jan 2026 19:44:52 +0200 Subject: [PATCH 10/20] Address code review feedback. --- .gemini/commands/implement.toml | 51 ++++++++++++++++++++++++--------- .github/workflows/gemini.yml | 2 -- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/.gemini/commands/implement.toml b/.gemini/commands/implement.toml index e565a5c7a4a..0d28aeeb8e3 100644 --- a/.gemini/commands/implement.toml +++ b/.gemini/commands/implement.toml @@ -7,7 +7,7 @@ You are tasked with implementing GitHub issue #{{args}} from the google/site-kit CRITICAL INSTRUCTIONS ═══════════════════════════════════════════════════════════════════════════════ -- You MUST ACTUALLY CREATE, MODIFY, and DELETE files in the repository +- You MUST ACTUALLY CREATE, MODIFY, and DELETE files in the current working directory - DO NOT just output proposed changes or describe what should be done - Use your developer tools to make ACTUAL file modifications - This is a LOCAL implementation - DO NOT create commits, pull requests, or push to remote @@ -35,16 +35,22 @@ PHASE 1: FETCH GITHUB ISSUE **Steps**: 1. Use GitHub MCP tools to fetch issue #{{args}} from repository `google/site-kit-wp` - - If MCP tools are not available, use: !{gh issue view {{args}} --repo google/site-kit-wp --json title,body} + - If MCP tools are not available, use: !{gh issue view {{args}} --json title,body} -2. Extract the "Implementation Brief" section from the issue body +2. Extract the "Acceptance criteria" section from the issue body + - This section typically appears between "## Acceptance criteria" and "## Implementation Brief" + - If the section markers are not present, stop execution and show an error + +3. Extract the "Implementation Brief" section from the issue body - This section typically appears between "## Implementation Brief" and "## QA Brief" - - If the section markers are not present, use the entire issue body as the implementation task + - If the section markers are not present, stop execution and show an error **Error Handling**: - If the issue does not exist: STOP and report "ERROR: Issue #{{args}} not found in google/site-kit-wp" - If GitHub API is unavailable: STOP and report the error with suggestions - If the issue body is empty: STOP and report "ERROR: Issue #{{args}} has no description" +- If "Acceptance criteria" markers are missing: STOP and report "ERROR: Acceptance criteria section markers not found" +- If the "Implementation Brief" markers are missing: STOP and report "ERROR: Implementation Brief section markers not found" **Output**: Store the implementation instructions for use in subsequent phases @@ -217,7 +223,15 @@ PHASE 4: CODE REVIEW Follow these code review guidelines: -**1. Verify Context Adherence (MANDATORY)** +**1. Verify Requirements Adherence (MANDATORY)** + + Check compliance with the extracted "Acceptance criteria": + - ✓ Implementation fulfills all points in "Acceptance criteria" + - ✓ No required functionality is missing + - ✓ Behavioral requirements are met + - ✓ Edge cases defined in AC are handled + +**2. Verify Context Adherence (MANDATORY)** Check strict compliance with documented principles. Review each relevant context file: @@ -256,7 +270,7 @@ Follow these code review guidelines: - What must be fixed to comply - Which files are affected -**2. Analyze Code Quality** +**3. Analyze Code Quality** Review for general code quality issues: - Code structure and organization @@ -269,11 +283,12 @@ Follow these code review guidelines: - Accessibility compliance (WCAG standards) - Browser/PHP version compatibility -**3. Score the Implementation (0.0 to 1.0)** +**4. Score the Implementation (0.0 to 1.0)** Assign a score based on this rubric: - **0.0 - 0.5**: Poor quality OR contains principle violations + **0.0 - 0.5**: Poor quality OR contains requirements/principle violations + - Use this range if ANY "Acceptance criteria" are not met - Use this range if ANY documented principles are violated - Use this if critical security or functionality issues exist - Use this if tests are missing or failing @@ -285,6 +300,7 @@ Follow these code review guidelines: - Documentation is sparse or unclear **0.85 - 0.94**: Good quality, production-ready + - Fulfills ALL requirements from "Acceptance criteria" - Follows all documented principles - Code is clean and maintainable - Comprehensive tests with good coverage @@ -298,7 +314,7 @@ Follow these code review guidelines: - Outstanding documentation - Sets a positive example for future development -**4. Provide Review Results** +**5. Provide Review Results** Format your review as follows: @@ -309,6 +325,15 @@ Follow these code review guidelines: Status: [approved | needs_improvement] Iteration: [current iteration number, 1-3] + REQUIREMENTS VIOLATIONS: [count] + + [If violations exist, list each:] + + Requirement Violation #[n]: + - AC Point: [which point from Acceptance criteria was violated] + - Details: [why the implementation does not meet this requirement] + - Fix Required: [what must be changed to implement the requirement] + CONTEXT VIOLATIONS: [count] [If violations exist, list each:] @@ -348,17 +373,17 @@ PHASE 5: ITERATION (if score < 0.85) **Iteration Rules**: - Maximum 3 iteration attempts - Track your current iteration number (1, 2, or 3) -- Each iteration must address ALL context violations +- Each iteration must address ALL requirements and context violations - After each iteration, return to Phase 4 for re-review **If score < 0.85, follow these steps**: **Iteration [1, 2, or 3]**: -1. **Address ALL Context Violations** (MANDATORY) - - Context violations are non-negotiable and must be fixed +1. **Address ALL Requirements and Context Violations** (MANDATORY) + - Requirements and context violations are non-negotiable and must be fixed - Follow the "Fix Required" instructions from the review - - Bring code into full compliance with documented principles + - Bring code into full compliance with requirements and documented principles - Make ACTUAL file modifications to fix each violation 2. **Address Critical and High Priority Recommendations** diff --git a/.github/workflows/gemini.yml b/.github/workflows/gemini.yml index f135572e9b4..97ade7de830 100644 --- a/.github/workflows/gemini.yml +++ b/.github/workflows/gemini.yml @@ -96,8 +96,6 @@ jobs: - [ ] Linting passes - [ ] Manual testing completed - [ ] Code review score ≥ 0.85 - - Closes #${{ inputs.issue_number }} draft: true - name: Comment on issue if: steps.create-pr.outputs.pull-request-number From 31f27c101b98d751ddf2583931a0cf3dff96cfca Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Mon, 12 Jan 2026 16:48:42 +0200 Subject: [PATCH 11/20] Update gemini version to 0.23.0. --- .github/workflows/gemini.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gemini.yml b/.github/workflows/gemini.yml index 97ade7de830..88fa584e510 100644 --- a/.github/workflows/gemini.yml +++ b/.github/workflows/gemini.yml @@ -70,7 +70,7 @@ jobs: run: tests/phpunit/bin/install-wp-tests.sh "${MYSQL_DATABASE}" "${MYSQL_USER}" "${MYSQL_PASSWORD}" "${DB_HOST}":"${DB_PORT}" "${WP_VERSION}" - name: Install Gemini CLI run: | - npm install -g @google/gemini-cli@0.22.5 + npm install -g @google/gemini-cli@0.23.0 gemini --version - name: Run /implement command run: | From 585b7cfc427922d86426db826bb06a6675924c0a Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Mon, 12 Jan 2026 16:49:31 +0200 Subject: [PATCH 12/20] Fix issues in context files and in the implement command. --- .gemini/commands/implement.toml | 4 +- docs/context/js/hooks.md | 6 +- docs/context/js/jsdoc.md | 84 +++++++++++----------- docs/context/php/asset-management.md | 14 ++-- docs/context/php/context-pattern.md | 44 ++++++------ docs/context/php/dependency-injection.md | 14 ++-- docs/context/php/module-architecture.md | 14 ++-- docs/context/php/naming-conventions.md | 30 ++++---- docs/context/php/phpunit.md | 2 +- docs/context/php/prompts-and-dismissals.md | 84 +++++++++++----------- docs/context/php/rest-api.md | 28 ++++---- docs/context/php/settings-management.md | 46 ++++++------ docs/context/php/storage-patterns.md | 58 +++++++-------- docs/context/php/trait-composition.md | 48 ++++++------- 14 files changed, 238 insertions(+), 238 deletions(-) diff --git a/.gemini/commands/implement.toml b/.gemini/commands/implement.toml index 0d28aeeb8e3..6debca23b40 100644 --- a/.gemini/commands/implement.toml +++ b/.gemini/commands/implement.toml @@ -532,9 +532,9 @@ PHASE 6: DOCUMENTATION (optional - only if score ≥ 0.85) How this concept fits into the system... ## Usage Examples - \`\`\`javascript + ```javascript // Example code - \`\`\` + ``` ## Best Practices - Practice 1 diff --git a/docs/context/js/hooks.md b/docs/context/js/hooks.md index a1c72260a1c..9f278630821 100644 --- a/docs/context/js/hooks.md +++ b/docs/context/js/hooks.md @@ -422,10 +422,10 @@ import { useSelect, useDispatch } from 'googlesitekit-data'; /** * Custom hook description. * - * @since 1.25.0 + * @​since 1.25.0 * - * @param {string} parameter Description of parameter. - * @return {*} Description of return value. + * @​param {string} parameter Description of parameter. + * @​return {*} Description of return value. */ export default function useCustomHook(parameter) { // Hook implementation diff --git a/docs/context/js/jsdoc.md b/docs/context/js/jsdoc.md index 1652858feb4..29cd68b6c4c 100644 --- a/docs/context/js/jsdoc.md +++ b/docs/context/js/jsdoc.md @@ -38,11 +38,11 @@ All utility functions, hooks, and complex API functions should follow this patte * * [Brief description of function purpose] * - * @since n.e.x.t + * @​since n.e.x.t * - * @param {type} paramName Description of parameter. - * @param {type} [optionalParam] Description of optional parameter. - * @return {type} Description of return value. + * @​param {type} paramName Description of parameter. + * @​param {type} [optionalParam] Description of optional parameter. + * @​return {type} Description of return value. */ function myFunction( paramName, optionalParam = defaultValue ) { // implementation @@ -51,66 +51,66 @@ function myFunction( paramName, optionalParam = defaultValue ) { ### Required JSDoc Tags -#### @since Tag +#### @​since Tag **Always required** - Documents when the feature was introduced, always use `n.e.x.t` value which will be replaced with the actual version later on. ```javascript /** * Returns a callback to activate a module. * - * @since n.e.x.t + * @​since n.e.x.t * - * @param {string} moduleSlug Module slug. - * @return {Function|null} Callback to activate module. + * @​param {string} moduleSlug Module slug. + * @​return {Function|null} Callback to activate module. */ ``` -#### @param Tag +#### @​param Tag **Required for all parameters** - Documents parameter types and descriptions: ```javascript // Basic parameter -@param {string} dateRange The date range slug. +@​param {string} dateRange The date range slug. // Optional parameter with default -@param {boolean} [invertColor=false] Whether to reverse the +/- colors. +@​param {boolean} [invertColor=false] Whether to reverse the +/- colors. // Complex object parameter -@param {Object} options Configuration options. -@param {string} options.baseName The base name to use. -@param {Function} options.controlCallback Callback function to issue the API request. -@param {Function} [options.validateParams] Optional validation function. +@​param {Object} options Configuration options. +@​param {string} options.baseName The base name to use. +@​param {Function} options.controlCallback Callback function to issue the API request. +@​param {Function} [options.validateParams] Optional validation function. ``` -#### @return Tag +#### @​return Tag **Required for functions that return values** - Documents return type and description: ```javascript -@return {Object} Partial store object with properties 'actions', 'controls', and 'reducer'. -@return {Function|null} Callback function or null if module doesn't exist. -@return {Array.} Array of widget objects. +@​return {Object} Partial store object with properties 'actions', 'controls', and 'reducer'. +@​return {Function|null} Callback function or null if module doesn't exist. +@​return {Array.} Array of widget objects. ``` ### Complex Type Documentation -#### @typedef for Complex Data Structures +#### @​typedef for Complex Data Structures -Use `@typedef` to define complex object structures: +Use `@​typedef` to define complex object structures: ```javascript /** * Parse Analytics 4 report into data suitable for rendering. * - * @typedef {Object} OverallPageMetricsData - * @property {string} metric Google Analytics metric identifier. - * @property {string} title Translated metric title. - * @property {Array.} sparkLineData Data for rendering the sparkline. - * @property {string} [datapointUnit] Optional datapoint unit, e.g. '%', 's'. - * @property {number} total Total count for the metric. - * @property {number} change Monthly change for the metric. + * @​typedef {Object} OverallPageMetricsData + * @​property {string} metric Google Analytics metric identifier. + * @​property {string} title Translated metric title. + * @​property {Array.} sparkLineData Data for rendering the sparkline. + * @​property {string} [datapointUnit] Optional datapoint unit, e.g. '%', 's'. + * @​property {number} total Total count for the metric. + * @​property {number} change Monthly change for the metric. * - * @param {Object} report Raw Analytics report data. - * @return {OverallPageMetricsData} Processed metrics data. + * @​param {Object} report Raw Analytics report data. + * @​return {OverallPageMetricsData} Processed metrics data. */ function parseReportData( report ) { // implementation @@ -126,10 +126,10 @@ Custom hooks require comprehensive JSDoc documentation: * Returns a callback to activate a module. If the call to activate the module * fails, an error will be returned to the returned callback. * - * @since n.e.x.t + * @​since n.e.x.t * - * @param {string} moduleSlug Module slug. - * @return {Function|null} Callback to activate module, null if the module doesn't exist. + * @​param {string} moduleSlug Module slug. + * @​return {Function|null} Callback to activate module, null if the module doesn't exist. */ export default function useActivateModuleCallback( moduleSlug ) { // hook implementation @@ -145,15 +145,15 @@ Utility functions require detailed documentation with examples for complex cases * Creates a store object implementing the necessary infrastructure for making * asynchronous API requests and storing their data. * - * @since n.e.x.t + * @​since n.e.x.t * - * @param {Object} args Arguments for creating the fetch store. - * @param {string} args.baseName The base name to use for all actions. - * @param {Function} args.controlCallback Callback function to issue the API request. - * @param {Function} [args.reducerCallback] Optional reducer to modify state. - * @param {Function} [args.argsToParams] Function that reduces argument list to params. - * @param {Function} [args.validateParams] Function that validates params before request. - * @return {Object} Partial store object with properties 'actions', 'controls', and 'reducer'. + * @​param {Object} args Arguments for creating the fetch store. + * @​param {string} args.baseName The base name to use for all actions. + * @​param {Function} args.controlCallback Callback function to issue the API request. + * @​param {Function} [args.reducerCallback] Optional reducer to modify state. + * @​param {Function} [args.argsToParams] Function that reduces argument list to params. + * @​param {Function} [args.validateParams] Function that validates params before request. + * @​return {Object} Partial store object with properties 'actions', 'controls', and 'reducer'. */ export default function createFetchStore( { baseName, @@ -199,7 +199,7 @@ export default function createFetchStore( { 4. **Create typedefs** for complex recurring data structures ### Version Tracking -1. **Always include @since** for new functions and significant changes using `n.e.x.t` value +1. **Always include @​since** for new functions and significant changes using `n.e.x.t` value 3. **Document breaking changes** in function descriptions ### Consistency Rules diff --git a/docs/context/php/asset-management.md b/docs/context/php/asset-management.md index e1c3feccef1..2fe119acaf3 100644 --- a/docs/context/php/asset-management.md +++ b/docs/context/php/asset-management.md @@ -598,14 +598,14 @@ interface Module_With_Assets { /** * Get assets to register for the module. * - * @return Asset[] Array of Asset instances. + * @​return Asset[] Array of Asset instances. */ public function get_assets(); /** * Enqueue all assets for the module. * - * @param string $asset_context Context constant (Asset::CONTEXT_*). + * @​param string $asset_context Context constant (Asset::CONTEXT_*). */ public function enqueue_assets( $asset_context = Asset::CONTEXT_ADMIN_SITEKIT ); } @@ -651,7 +651,7 @@ trait Module_With_Assets_Trait { /** * Set up module assets. * - * @return Asset[] Array of Asset instances. + * @​return Asset[] Array of Asset instances. */ abstract protected function setup_assets(); } @@ -688,8 +688,8 @@ interface Module_With_Inline_Data { /** * Get inline data for the module. * - * @param array $modules_data Existing modules data. - * @return array Updated modules data with module's data added. + * @​param array $modules_data Existing modules data. + * @​return array Updated modules data with module's data added. */ public function get_inline_data( $modules_data ); } @@ -901,8 +901,8 @@ final class Manifest { /** * Get manifest entry for asset handle. * - * @param string $handle Asset handle. - * @return array [ $filename, $hash ] or [ null, null ] if not found. + * @​param string $handle Asset handle. + * @​return array [ $filename, $hash ] or [ null, null ] if not found. */ public static function get( $handle ) { if ( null === self::$data ) { diff --git a/docs/context/php/context-pattern.md b/docs/context/php/context-pattern.md index 6402072ec67..d9629d59b74 100644 --- a/docs/context/php/context-pattern.md +++ b/docs/context/php/context-pattern.md @@ -28,8 +28,8 @@ The Context object provides five main categories of functionality: /** * Get absolute path to plugin directory or file. * - * @param string $relative_path Optional. Relative path within plugin. Default '/'. - * @return string Absolute path. + * @​param string $relative_path Optional. Relative path within plugin. Default '/'. + * @​return string Absolute path. */ public function path( $relative_path = '/' ) ``` @@ -58,8 +58,8 @@ $assets_dir = $context->path( 'dist/assets/' ); /** * Get URL to plugin directory or file. * - * @param string $relative_path Optional. Relative path within plugin. Default '/'. - * @return string URL. + * @​param string $relative_path Optional. Relative path within plugin. Default '/'. + * @​return string URL. */ public function url( $relative_path = '/' ) ``` @@ -84,9 +84,9 @@ $script_url = $context->url( 'dist/assets/js/googlesitekit-dashboard.js' ); /** * Get admin URL for a specific Site Kit page. * - * @param string $slug Page slug (e.g., 'dashboard', 'settings'). - * @param array $query_args Optional query parameters. - * @return string Admin URL. + * @​param string $slug Page slug (e.g., 'dashboard', 'settings'). + * @​param array $query_args Optional query parameters. + * @​return string Admin URL. */ public function admin_url( $slug = 'dashboard', array $query_args = array() ) ``` @@ -116,14 +116,14 @@ $settings_url = $context->admin_url( 'settings', array( /** * Check if current request is an AMP request. * - * @return bool True if AMP request. + * @​return bool True if AMP request. */ public function is_amp() /** * Get the AMP mode for the site. * - * @return string 'primary', 'secondary', or empty string if not AMP. + * @​return string 'primary', 'secondary', or empty string if not AMP. */ public function get_amp_mode() ``` @@ -152,14 +152,14 @@ if ( 'primary' === $amp_mode ) { /** * Check if plugin is in network mode (multisite). * - * @return bool True if network mode. + * @​return bool True if network mode. */ public function is_network_mode() /** * Check if plugin is network active. * - * @return bool True if network active. + * @​return bool True if network active. */ public function is_network_active() ``` @@ -190,7 +190,7 @@ if ( $context->is_network_active() ) { /** * Get the reference site URL for the current request context. * - * @return string Reference site URL. + * @​return string Reference site URL. */ public function get_reference_site_url() ``` @@ -235,7 +235,7 @@ private function filter_reference_url( $url = '' ) { /** * Get the canonical home URL. * - * @return string Canonical home URL. + * @​return string Canonical home URL. */ public function get_canonical_home_url() ``` @@ -255,7 +255,7 @@ $home_url = $context->get_canonical_home_url(); /** * Get the reference entity for the current context. * - * @return array Entity information array. + * @​return array Entity information array. */ public function get_reference_entity() ``` @@ -279,9 +279,9 @@ $entity = $context->get_reference_entity(); /** * Get locale for a specific context. * - * @param string $context 'site' or 'user'. Default 'site'. - * @param string $format 'language-code' or 'default'. Default 'default'. - * @return string Locale string. + * @​param string $context 'site' or 'user'. Default 'site'. + * @​param string $format 'language-code' or 'default'. Default 'default'. + * @​return string Locale string. */ public function get_locale( $context = 'site', $format = 'default' ) ``` @@ -314,11 +314,11 @@ The Context provides a safe abstraction for accessing superglobals (GET, POST, e /** * Gets a specific external variable by name and optionally filters it. * - * @param int $type One of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV. - * @param string $variable_name Name of a variable to get. - * @param int $filter [optional] The ID of the filter to apply. Default FILTER_DEFAULT. - * @param mixed $options [optional] Associative array of options or bitwise disjunction of flags. - * @return mixed Value of the requested variable on success. + * @​param int $type One of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV. + * @​param string $variable_name Name of a variable to get. + * @​param int $filter [optional] The ID of the filter to apply. Default FILTER_DEFAULT. + * @​param mixed $options [optional] Associative array of options or bitwise disjunction of flags. + * @​return mixed Value of the requested variable on success. */ public function filter( $type, $variable_name, $filter = FILTER_DEFAULT, $options = 0 ) ``` diff --git a/docs/context/php/dependency-injection.md b/docs/context/php/dependency-injection.md index 0dce6d2614d..d6f6ce60cba 100644 --- a/docs/context/php/dependency-injection.md +++ b/docs/context/php/dependency-injection.md @@ -419,12 +419,12 @@ class MyClass { /** * Constructor. * - * @since 1.0.0 + * @​since 1.0.0 * - * @param Context $context Plugin context instance. - * @param Options $options Optional. Options instance. Default is a new instance. - * @param User_Options $user_options Optional. User options instance. Default is a new instance. - * @param Authentication $authentication Optional. Authentication instance. Default is a new instance. + * @​param Context $context Plugin context instance. + * @​param Options $options Optional. Options instance. Default is a new instance. + * @​param User_Options $user_options Optional. User options instance. Default is a new instance. + * @​param Authentication $authentication Optional. Authentication instance. Default is a new instance. */ public function __construct( Context $context, @@ -459,8 +459,8 @@ public function __construct( ```php /** - * @param Context $context Plugin context. - * @param Options $options Settings storage. + * @​param Context $context Plugin context. + * @​param Options $options Settings storage. */ ``` diff --git a/docs/context/php/module-architecture.md b/docs/context/php/module-architecture.md index 54a3ced28de..aafdbbbf471 100644 --- a/docs/context/php/module-architecture.md +++ b/docs/context/php/module-architecture.md @@ -118,7 +118,7 @@ interface Module_With_Settings { /** * Get module settings instance. * - * @return Module_Settings + * @​return Module_Settings */ public function get_settings(); } @@ -153,7 +153,7 @@ interface Module_With_Scopes { /** * Get required OAuth scopes. * - * @return array List of Google OAuth scopes. + * @​return array List of Google OAuth scopes. */ public function get_scopes(); } @@ -187,7 +187,7 @@ interface Module_With_Assets { /** * Get module assets to enqueue. * - * @return array Array of Asset objects. + * @​return array Array of Asset objects. */ public function get_assets(); } @@ -228,7 +228,7 @@ interface Module_With_Tag { /** * Get the module tag instance. * - * @return Module_Tag + * @​return Module_Tag */ public function get_tag(); } @@ -259,7 +259,7 @@ interface Module_With_Service_Entity { /** * Get the service entity access. * - * @return Service_Entity_Access + * @​return Service_Entity_Access */ public function get_service_entity(); } @@ -278,7 +278,7 @@ interface Module_With_Inline_Data { /** * Get inline data for the module. * - * @return array Associative array of inline data. + * @​return array Associative array of inline data. */ public function get_inline_data(); } @@ -316,7 +316,7 @@ interface Provides_Feature_Metrics { /** * Get the feature metrics instance. * - * @return Feature_Metrics + * @​return Feature_Metrics */ public function get_feature_metrics(); } diff --git a/docs/context/php/naming-conventions.md b/docs/context/php/naming-conventions.md index 09179d80f6d..3aa7d0d7b24 100644 --- a/docs/context/php/naming-conventions.md +++ b/docs/context/php/naming-conventions.md @@ -514,7 +514,7 @@ function googlesitekit_is_network_mode() { } /** * Class for managing module settings. * - * @since 1.0.0 + * @​since 1.0.0 * @access private * @ignore */ @@ -527,11 +527,11 @@ final class Module_Settings extends Setting { /** * Get module settings. * - * @since 1.0.0 + * @​since 1.0.0 * - * @param string $key Optional. Setting key. Default empty string. - * @param mixed $default Optional. Default value. Default null. - * @return mixed Setting value or default. + * @​param string $key Optional. Setting key. Default empty string. + * @​param mixed $default Optional. Default value. Default null. + * @​return mixed Setting value or default. */ public function get( $key = '', $default = null ) { ``` @@ -542,24 +542,24 @@ public function get( $key = '', $default = null ) { /** * Plugin context instance. * - * @since 1.0.0 - * @var Context + * @​since 1.0.0 + * @​var Context */ private $context; ``` -### @since Tag Convention +### @​since Tag Convention -For all new code (classes, methods, properties, constants), use `@since n.e.x.t` as a placeholder. +For all new code (classes, methods, properties, constants), use `@​since n.e.x.t` as a placeholder. ```php /** * New method added in development. * - * @since n.e.x.t + * @​since n.e.x.t * - * @param string $param Parameter description. - * @return bool True on success, false otherwise. + * @​param string $param Parameter description. + * @​return bool True on success, false otherwise. */ public function new_method( $param ) { ``` @@ -568,7 +568,7 @@ public function new_method( $param ) { /** * New class added in development. * - * @since n.e.x.t + * @​since n.e.x.t */ final class New_Feature { ``` @@ -577,8 +577,8 @@ final class New_Feature { /** * New property added in development. * - * @since n.e.x.t - * @var string + * @​since n.e.x.t + * @​var string */ private $new_property; ``` diff --git a/docs/context/php/phpunit.md b/docs/context/php/phpunit.md index bf4fa4a7e2b..84abc1e55c9 100644 --- a/docs/context/php/phpunit.md +++ b/docs/context/php/phpunit.md @@ -107,7 +107,7 @@ abstract class SettingsTestCase extends TestCase { /** * Get the option name for the setting. * - * @return string + * @​return string */ abstract protected function get_option_name(); diff --git a/docs/context/php/prompts-and-dismissals.md b/docs/context/php/prompts-and-dismissals.md index 1ab9a452bfd..b1c52d36a0b 100644 --- a/docs/context/php/prompts-and-dismissals.md +++ b/docs/context/php/prompts-and-dismissals.md @@ -68,9 +68,9 @@ final class Dismissed_Prompts extends User_Setting { /** * Add or update a dismissed prompt. * - * @param string $prompt Prompt slug. - * @param int $expires_in_seconds Expiration in seconds (0 = permanent). - * @return bool True on success. + * @​param string $prompt Prompt slug. + * @​param int $expires_in_seconds Expiration in seconds (0 = permanent). + * @​return bool True on success. */ public function add( $prompt, $expires_in_seconds = self::DISMISS_PROMPT_PERMANENTLY ) { $prompts = $this->get(); @@ -98,8 +98,8 @@ final class Dismissed_Prompts extends User_Setting { /** * Remove a dismissed prompt. * - * @param string $prompt Prompt slug. - * @return bool True on success. + * @​param string $prompt Prompt slug. + * @​return bool True on success. */ public function remove( $prompt ) { $prompts = $this->get(); @@ -115,7 +115,7 @@ final class Dismissed_Prompts extends User_Setting { /** * Get all dismissed prompts. * - * @return array Dismissed prompts with metadata. + * @​return array Dismissed prompts with metadata. */ public function get() { return parent::get() ?: array(); @@ -124,7 +124,7 @@ final class Dismissed_Prompts extends User_Setting { /** * Get default value. * - * @return array Empty array. + * @​return array Empty array. */ protected function get_default() { return array(); @@ -133,8 +133,8 @@ final class Dismissed_Prompts extends User_Setting { /** * Sanitize prompts data. * - * @param array $prompts Prompts data. - * @return array Sanitized prompts. + * @​param array $prompts Prompts data. + * @​return array Sanitized prompts. */ protected function sanitize_callback( $prompts ) { if ( ! is_array( $prompts ) ) { @@ -313,9 +313,9 @@ function AdBlockingRecoveryWidget() { /** * Dismiss a prompt. * - * @param {string} slug Prompt slug. - * @param {Object} options Options object. - * @param {number} options.expiresInSeconds Expiration in seconds (0 = permanent). + * @​param {string} slug Prompt slug. + * @​param {Object} options Options object. + * @​param {number} options.expiresInSeconds Expiration in seconds (0 = permanent). */ *dismissPrompt( slug, { expiresInSeconds = 0 } = {} ) ``` @@ -341,31 +341,31 @@ dismissPrompt( 'my-prompt', { expiresInSeconds: 86400 } ); /** * Get all active dismissed prompts (filters expired). * - * @return {Array} Array of prompt slugs. + * @​return {Array} Array of prompt slugs. */ getDismissedPrompts(); /** * Get dismiss count for a prompt. * - * @param {string} slug Prompt slug. - * @return {number} Number of times dismissed. + * @​param {string} slug Prompt slug. + * @​return {number} Number of times dismissed. */ getPromptDismissCount( slug ); /** * Check if prompt is dismissed. * - * @param {string} slug Prompt slug. - * @return {boolean} True if dismissed and not expired. + * @​param {string} slug Prompt slug. + * @​return {boolean} True if dismissed and not expired. */ isPromptDismissed( slug ); /** * Check if currently dismissing a prompt. * - * @param {string} slug Prompt slug. - * @return {boolean} True if dismissing. + * @​param {string} slug Prompt slug. + * @​return {boolean} True if dismissing. */ isDismissingPrompt( slug ); ``` @@ -464,9 +464,9 @@ final class Dismissed_Items extends User_Setting { /** * Add or update a dismissed item. * - * @param string $item Item slug. - * @param int $expires_in_seconds Expiration in seconds (0 = permanent). - * @return bool True on success. + * @​param string $item Item slug. + * @​param int $expires_in_seconds Expiration in seconds (0 = permanent). + * @​return bool True on success. */ public function add( $item, $expires_in_seconds = self::DISMISS_ITEM_PERMANENTLY ) { $items = $this->get(); @@ -483,8 +483,8 @@ final class Dismissed_Items extends User_Setting { /** * Remove a dismissed item. * - * @param string $item Item slug. - * @return bool True on success. + * @​param string $item Item slug. + * @​return bool True on success. */ public function remove( $item ) { $items = $this->get(); @@ -500,7 +500,7 @@ final class Dismissed_Items extends User_Setting { /** * Get all dismissed items (including expired). * - * @return array Dismissed items with expiration values. + * @​return array Dismissed items with expiration values. */ public function get() { return parent::get() ?: array(); @@ -509,8 +509,8 @@ final class Dismissed_Items extends User_Setting { /** * Check if item is dismissed and not expired. * - * @param string $item Item slug. - * @return bool True if dismissed and not expired. + * @​param string $item Item slug. + * @​return bool True if dismissed and not expired. */ public function is_dismissed( $item ) { $items = $this->get(); @@ -528,7 +528,7 @@ final class Dismissed_Items extends User_Setting { /** * Get only active dismissed items (filters expired). * - * @return array Array of item slugs. + * @​return array Array of item slugs. */ public function get_dismissed_items() { $items = $this->get(); @@ -538,8 +538,8 @@ final class Dismissed_Items extends User_Setting { /** * Filter out expired items. * - * @param array $items Items with expiration values. - * @return array Filtered items. + * @​param array $items Items with expiration values. + * @​return array Filtered items. */ private function filter_dismissed_items( $items ) { return array_filter( @@ -553,7 +553,7 @@ final class Dismissed_Items extends User_Setting { /** * Get default value. * - * @return array Empty array. + * @​return array Empty array. */ protected function get_default() { return array(); @@ -562,8 +562,8 @@ final class Dismissed_Items extends User_Setting { /** * Sanitize items data. * - * @param array $items Items data. - * @return array Sanitized items. + * @​param array $items Items data. + * @​return array Sanitized items. */ protected function sanitize_callback( $items ) { if ( ! is_array( $items ) ) { @@ -805,16 +805,16 @@ function MyNotice() { /** * Dismiss an item. * - * @param {string} slug Item slug. - * @param {Object} options Options object. - * @param {number} options.expiresInSeconds Expiration in seconds (0 = permanent). + * @​param {string} slug Item slug. + * @​param {Object} options Options object. + * @​param {number} options.expiresInSeconds Expiration in seconds (0 = permanent). */ *dismissItem( slug, { expiresInSeconds = 0 } = {} ) /** * Remove dismissed items. * - * @param {...string} slugs Item slugs to remove. + * @​param {...string} slugs Item slugs to remove. */ *removeDismissedItems( ...slugs ) ``` @@ -843,23 +843,23 @@ removeDismissedItems( 'item-1', 'item-2' ); /** * Get all active dismissed items (filters expired). * - * @return {Array} Array of item slugs. + * @​return {Array} Array of item slugs. */ getDismissedItems(); /** * Check if item is dismissed. * - * @param {string} slug Item slug. - * @return {boolean} True if dismissed and not expired. + * @​param {string} slug Item slug. + * @​return {boolean} True if dismissed and not expired. */ isItemDismissed( slug ); /** * Check if currently dismissing an item. * - * @param {string} slug Item slug. - * @return {boolean} True if dismissing. + * @​param {string} slug Item slug. + * @​return {boolean} True if dismissing. */ isDismissingItem( slug ); ``` diff --git a/docs/context/php/rest-api.md b/docs/context/php/rest-api.md index dd472ab4171..5af03e604d2 100644 --- a/docs/context/php/rest-api.md +++ b/docs/context/php/rest-api.md @@ -64,7 +64,7 @@ final class REST_Routes { /** * Filters the list of available REST routes. * - * @param array $routes List of REST_Route objects. + * @​param array $routes List of REST_Route objects. */ return apply_filters( 'googlesitekit_rest_routes', $routes ); } @@ -85,9 +85,9 @@ final class REST_Route { /** * Constructor. * - * @param string $uri Route URI pattern. - * @param array $endpoints Route endpoints configuration. - * @param array $args Optional route arguments. + * @​param string $uri Route URI pattern. + * @​param array $endpoints Route endpoints configuration. + * @​param array $args Optional route arguments. */ public function __construct( $uri, array $endpoints, array $args = array() ) { $this->uri = trim( $uri, '/' ); @@ -135,8 +135,8 @@ final class REST_Route { /** * Parse parameter arguments to ensure proper schema. * - * @param array $args Parameter arguments. - * @return array Parsed arguments. + * @​param array $args Parameter arguments. + * @​return array Parsed arguments. */ private function parse_param_args( $args ) { $parsed_args = array(); @@ -185,7 +185,7 @@ class REST_Feature_Controller { /** * Get REST route definitions. * - * @return array Array of REST_Route objects. + * @​return array Array of REST_Route objects. */ private function get_rest_routes() { // Permission callbacks @@ -241,8 +241,8 @@ class REST_Feature_Controller { /** * GET callback for list endpoint. * - * @param WP_REST_Request $request REST request object. - * @return WP_REST_Response|WP_Error Response object or error. + * @​param WP_REST_Request $request REST request object. + * @​return WP_REST_Response|WP_Error Response object or error. */ public function get_list( WP_REST_Request $request ) { $status = $request->get_param( 'status' ); @@ -256,8 +256,8 @@ class REST_Feature_Controller { /** * POST callback for save endpoint. * - * @param WP_REST_Request $request REST request object. - * @return WP_REST_Response|WP_Error Response object or error. + * @​param WP_REST_Request $request REST request object. + * @​return WP_REST_Response|WP_Error Response object or error. */ public function save_item( WP_REST_Request $request ) { $name = $request->get_param( 'name' ); @@ -360,7 +360,7 @@ final class REST_Modules_Controller { /** * Get list of modules. * - * @return WP_REST_Response Response with modules list. + * @​return WP_REST_Response Response with modules list. */ public function get_modules() { $modules = array_values( @@ -373,8 +373,8 @@ final class REST_Modules_Controller { /** * Activate a module. * - * @param WP_REST_Request $request REST request. - * @return WP_REST_Response|WP_Error Response or error. + * @​param WP_REST_Request $request REST request. + * @​return WP_REST_Response|WP_Error Response or error. */ public function activate_module( WP_REST_Request $request ) { $slug = $request->get_param( 'slug' ); diff --git a/docs/context/php/settings-management.md b/docs/context/php/settings-management.md index fc30c74e5fb..c9172d4fd14 100644 --- a/docs/context/php/settings-management.md +++ b/docs/context/php/settings-management.md @@ -51,7 +51,7 @@ abstract class Setting { /** * Check if setting exists in database. * - * @return bool True if setting exists. + * @​return bool True if setting exists. */ public function has() { $value = $this->get(); @@ -63,7 +63,7 @@ abstract class Setting { /** * Get setting value. * - * @return mixed Setting value. + * @​return mixed Setting value. */ public function get() { $option = $this->get_option(); @@ -78,8 +78,8 @@ abstract class Setting { /** * Set setting value. * - * @param mixed $value New value. - * @return bool True on success. + * @​param mixed $value New value. + * @​return bool True on success. */ public function set( $value ) { return $this->update_option( $value ); @@ -88,7 +88,7 @@ abstract class Setting { /** * Delete setting. * - * @return bool True on success. + * @​return bool True on success. */ public function delete() { return $this->delete_option(); @@ -97,8 +97,8 @@ abstract class Setting { /** * Register callback for setting changes. * - * @param callable $callback Function to call when setting changes. - * @return callable Unsubscribe function. + * @​param callable $callback Function to call when setting changes. + * @​return callable Unsubscribe function. */ public function on_change( callable $callback ) { // Observer pattern implementation @@ -107,7 +107,7 @@ abstract class Setting { /** * Get setting type. * - * @return string Setting type (string, number, integer, boolean, array, object). + * @​return string Setting type (string, number, integer, boolean, array, object). */ protected function get_type() { return 'array'; @@ -116,7 +116,7 @@ abstract class Setting { /** * Get default value. * - * @return mixed Default value. + * @​return mixed Default value. */ protected function get_default() { return array(); @@ -125,7 +125,7 @@ abstract class Setting { /** * Get sanitization callback. * - * @return callable Sanitization function. + * @​return callable Sanitization function. */ protected function get_sanitize_callback() { return null; @@ -174,8 +174,8 @@ abstract class Module_Settings extends Setting { /** * Merge partial settings with existing settings. * - * @param array $partial Partial settings to merge. - * @return bool True on success. + * @​param array $partial Partial settings to merge. + * @​return bool True on success. */ public function merge( array $partial ) { $settings = $this->get(); @@ -197,7 +197,7 @@ abstract class Module_Settings extends Setting { /** * Check if any settings have changed from their saved values. * - * @return bool True if any setting has changed. + * @​return bool True if any setting has changed. */ public function have_changed() { $settings = $this->get(); @@ -209,7 +209,7 @@ abstract class Module_Settings extends Setting { /** * Get saved settings (before any modifications). * - * @return array Saved settings. + * @​return array Saved settings. */ protected function get_saved() { return $this->get(); @@ -253,7 +253,7 @@ final class Settings extends Module_Settings { /** * Get default settings. * - * @return array Default settings. + * @​return array Default settings. */ protected function get_default() { return array( @@ -275,7 +275,7 @@ final class Settings extends Module_Settings { /** * Get setting type. * - * @return string Setting type. + * @​return string Setting type. */ protected function get_type() { return 'array'; @@ -284,7 +284,7 @@ final class Settings extends Module_Settings { /** * Get sanitization callback. * - * @return callable Sanitization function. + * @​return callable Sanitization function. */ protected function get_sanitize_callback() { return function ( $option ) { @@ -319,8 +319,8 @@ final class Settings extends Module_Settings { /** * Sanitize property ID. * - * @param string $property_id Property ID to sanitize. - * @return string Sanitized property ID. + * @​param string $property_id Property ID to sanitize. + * @​return string Sanitized property ID. */ private function sanitize_property_id( $property_id ) { // Allow special values @@ -349,7 +349,7 @@ final class Analytics_4 extends Module implements Module_With_Settings { /** * Set up module settings. * - * @return Module_Settings Settings instance. + * @​return Module_Settings Settings instance. */ protected function setup_settings() { return new Settings( $this->options ); @@ -386,7 +386,7 @@ trait Setting_With_Owned_Keys_Trait { /** * Get owned setting keys. * - * @return array List of owned setting keys. + * @​return array List of owned setting keys. */ protected function get_owned_keys() { return array(); @@ -395,7 +395,7 @@ trait Setting_With_Owned_Keys_Trait { /** * Check if owned settings have changed. * - * @return bool True if owned settings changed. + * @​return bool True if owned settings changed. */ public function have_owned_settings_changed() { $settings = $this->get(); @@ -448,7 +448,7 @@ interface Setting_With_ViewOnly_Keys_Interface { /** * Get view-only setting keys. * - * @return array List of view-only keys. + * @​return array List of view-only keys. */ public function get_view_only_keys(); } diff --git a/docs/context/php/storage-patterns.md b/docs/context/php/storage-patterns.md index c96a3355cc0..52d20819523 100644 --- a/docs/context/php/storage-patterns.md +++ b/docs/context/php/storage-patterns.md @@ -42,8 +42,8 @@ final class Options implements Options_Interface { /** * Get option value. * - * @param string $option Option name. - * @return mixed Option value or false if not found. + * @​param string $option Option name. + * @​return mixed Option value or false if not found. */ public function get( $option ) { if ( $this->context->is_network_mode() ) { @@ -55,9 +55,9 @@ final class Options implements Options_Interface { /** * Set option value. * - * @param string $option Option name. - * @param mixed $value Option value. - * @return bool True on success. + * @​param string $option Option name. + * @​param mixed $value Option value. + * @​return bool True on success. */ public function set( $option, $value ) { if ( $this->context->is_network_mode() ) { @@ -69,8 +69,8 @@ final class Options implements Options_Interface { /** * Delete option. * - * @param string $option Option name. - * @return bool True on success. + * @​param string $option Option name. + * @​return bool True on success. */ public function delete( $option ) { if ( $this->context->is_network_mode() ) { @@ -82,8 +82,8 @@ final class Options implements Options_Interface { /** * Check if option exists. * - * @param string $option Option name. - * @return bool True if option exists. + * @​param string $option Option name. + * @​return bool True if option exists. */ public function has( $option ) { $value = $this->get( $option ); @@ -176,8 +176,8 @@ final class User_Options implements User_Options_Interface { /** * Get option for the current user. * - * @param string $option Option name. - * @return mixed Option value or false. + * @​param string $option Option name. + * @​return mixed Option value or false. */ public function get( $option ) { $user_id = $this->get_user_id(); @@ -196,9 +196,9 @@ final class User_Options implements User_Options_Interface { /** * Set option for the current user. * - * @param string $option Option name. - * @param mixed $value Option value. - * @return bool True on success. + * @​param string $option Option name. + * @​param mixed $value Option value. + * @​return bool True on success. */ public function set( $option, $value ) { $user_id = $this->get_user_id(); @@ -212,8 +212,8 @@ final class User_Options implements User_Options_Interface { /** * Delete user option. * - * @param string $option Option name. - * @return bool True on success. + * @​param string $option Option name. + * @​return bool True on success. */ public function delete( $option ) { $user_id = $this->get_user_id(); @@ -227,8 +227,8 @@ final class User_Options implements User_Options_Interface { /** * Check if user has option. * - * @param string $option Option name. - * @return bool True if option exists. + * @​param string $option Option name. + * @​return bool True if option exists. */ public function has( $option ) { return false !== $this->get( $option ); @@ -237,7 +237,7 @@ final class User_Options implements User_Options_Interface { /** * Get current user ID. * - * @return int User ID. + * @​return int User ID. */ public function get_user_id() { return $this->user_id; @@ -246,8 +246,8 @@ final class User_Options implements User_Options_Interface { /** * Switch to a different user context. * - * @param int $user_id User ID to switch to. - * @return bool True on success. + * @​param int $user_id User ID to switch to. + * @​return bool True on success. */ public function switch_user( $user_id ) { $user_id = (int) $user_id; @@ -375,8 +375,8 @@ final class Transients { /** * Get transient value. * - * @param string $transient Transient name. - * @return mixed Transient value or false if not found/expired. + * @​param string $transient Transient name. + * @​return mixed Transient value or false if not found/expired. */ public function get( $transient ) { if ( $this->context->is_network_mode() ) { @@ -388,10 +388,10 @@ final class Transients { /** * Set transient value. * - * @param string $transient Transient name. - * @param mixed $value Transient value. - * @param int $expiration Expiration in seconds. - * @return bool True on success. + * @​param string $transient Transient name. + * @​param mixed $value Transient value. + * @​param int $expiration Expiration in seconds. + * @​return bool True on success. */ public function set( $transient, $value, $expiration = 0 ) { if ( $this->context->is_network_mode() ) { @@ -403,8 +403,8 @@ final class Transients { /** * Delete transient. * - * @param string $transient Transient name. - * @return bool True on success. + * @​param string $transient Transient name. + * @​return bool True on success. */ public function delete( $transient ) { if ( $this->context->is_network_mode() ) { diff --git a/docs/context/php/trait-composition.md b/docs/context/php/trait-composition.md index d65d6ad68fa..e6e7ab1ea77 100644 --- a/docs/context/php/trait-composition.md +++ b/docs/context/php/trait-composition.md @@ -26,8 +26,8 @@ trait Method_Proxy_Trait { /** * Get a proxy closure for a class method. * - * @param string $method Method name. - * @return callable Proxy closure. + * @​param string $method Method name. + * @​return callable Proxy closure. */ private function get_method_proxy( $method ) { return function ( ...$args ) use ( $method ) { @@ -38,8 +38,8 @@ trait Method_Proxy_Trait { /** * Get a proxy closure that only executes once. * - * @param string $method Method name. - * @return callable Proxy closure. + * @​param string $method Method name. + * @​return callable Proxy closure. */ private function get_method_proxy_once( $method ) { return function ( ...$args ) use ( $method ) { @@ -91,7 +91,7 @@ trait User_Aware_Trait { /** * Get current user ID. * - * @return int User ID. + * @​return int User ID. */ public function get_user_id() { return $this->user_id; @@ -100,8 +100,8 @@ trait User_Aware_Trait { /** * Switch to a different user context. * - * @param int $user_id User ID to switch to. - * @return bool True on success. + * @​param int $user_id User ID to switch to. + * @​return bool True on success. */ public function switch_user( $user_id ) { $this->user_id = (int) $user_id; @@ -146,14 +146,14 @@ trait Module_With_Settings_Trait { /** * Set up module settings. * - * @return Module_Settings Settings instance. + * @​return Module_Settings Settings instance. */ abstract protected function setup_settings(); /** * Get module settings instance. * - * @return Module_Settings Settings instance. + * @​return Module_Settings Settings instance. */ public function get_settings() { if ( ! $this->settings instanceof Module_Settings ) { @@ -194,14 +194,14 @@ trait Module_With_Scopes_Trait { /** * Set up required OAuth scopes. * - * @return array List of OAuth scopes. + * @​return array List of OAuth scopes. */ abstract protected function setup_scopes(); /** * Get required OAuth scopes. * - * @return array OAuth scopes. + * @​return array OAuth scopes. */ public function get_scopes() { if ( empty( $this->scopes ) ) { @@ -240,7 +240,7 @@ trait Module_With_Owner_Trait { /** * Get the module owner's user ID. * - * @return int Owner user ID. + * @​return int Owner user ID. */ public function get_owner_id() { if ( ! $this instanceof Module_With_Settings ) { @@ -254,7 +254,7 @@ trait Module_With_Owner_Trait { /** * Get OAuth client for the module owner. * - * @return OAuth_Client Owner's OAuth client. + * @​return OAuth_Client Owner's OAuth client. */ public function get_owner_oauth_client() { if ( $this->owner_oauth_client instanceof OAuth_Client ) { @@ -283,8 +283,8 @@ trait Module_With_Owner_Trait { /** * Set the module owner. * - * @param int $owner_id Owner user ID. - * @return bool True on success. + * @​param int $owner_id Owner user ID. + * @​return bool True on success. */ public function set_owner_id( $owner_id ) { if ( ! $this instanceof Module_With_Settings ) { @@ -327,14 +327,14 @@ trait Module_With_Assets_Trait { /** * Set up module assets. * - * @return array List of Asset objects. + * @​return array List of Asset objects. */ abstract protected function setup_assets(); /** * Get module assets. * - * @return array Asset objects. + * @​return array Asset objects. */ public function get_assets() { if ( null === $this->module_assets ) { @@ -380,14 +380,14 @@ trait Module_With_Tag_Trait { /** * Set up module tag. * - * @return Module_Tag Tag instance. + * @​return Module_Tag Tag instance. */ abstract protected function setup_tag(); /** * Get module tag instance. * - * @return Module_Tag Tag instance. + * @​return Module_Tag Tag instance. */ public function get_tag() { if ( ! $this->tag instanceof Module_Tag ) { @@ -429,14 +429,14 @@ trait Module_With_Data_Available_State_Trait { /** * Set up data available state. * - * @return Data_Available_State State instance. + * @​return Data_Available_State State instance. */ abstract protected function setup_data_available_state(); /** * Get data available state instance. * - * @return Data_Available_State State instance. + * @​return Data_Available_State State instance. */ public function get_data_available_state() { if ( ! $this->data_available_state instanceof Data_Available_State ) { @@ -460,7 +460,7 @@ trait Setting_With_Owned_Keys_Trait { /** * Get owned setting keys. * - * @return array List of owned keys. + * @​return array List of owned keys. */ protected function get_owned_keys() { return array(); @@ -469,7 +469,7 @@ trait Setting_With_Owned_Keys_Trait { /** * Check if owned settings have changed. * - * @return bool True if owned settings changed. + * @​return bool True if owned settings changed. */ public function have_owned_settings_changed() { $settings = $this->get(); @@ -490,7 +490,7 @@ trait Setting_With_Owned_Keys_Trait { /** * Get owned settings slugs. * - * @return array Owned setting keys. + * @​return array Owned setting keys. */ public function get_owned_settings_slugs() { return $this->get_owned_keys(); From 56f7b2d13496d02140b303b04d01ec5cb0e673f5 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Mon, 12 Jan 2026 17:16:56 +0200 Subject: [PATCH 13/20] Update the github mcp settings to whitelist only issue reading tools. --- .gemini/settings.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.gemini/settings.json b/.gemini/settings.json index 9416098f8b8..e3b2346fe01 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -32,10 +32,22 @@ "mcpServers": { "github": { "trust": false, - "httpUrl": "https://api.githubcopilot.com/mcp/", + "httpUrl": "https://api.githubcopilot.com/mcp/?toolsets=issues", "headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" } } + }, + "tools": { + "exclude": [ + "github__add_issue_comment", + "github__assign_copilot_to_issue", + "github__get_label", + "github__issue_write", + "github__list_issue_types", + "github__list_issues", + "github__search_issues", + "github__sub_issue_write" + ] } } From 873d47c58841132001703df3090371c868cbdaf0 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Wed, 14 Jan 2026 15:25:11 +0200 Subject: [PATCH 14/20] Replace @ escaping to use the back slash approach. --- docs/context/js/hooks.md | 6 +- docs/context/js/jsdoc.md | 84 +++++++++++----------- docs/context/php/asset-management.md | 14 ++-- docs/context/php/context-pattern.md | 44 ++++++------ docs/context/php/dependency-injection.md | 14 ++-- docs/context/php/module-architecture.md | 14 ++-- docs/context/php/naming-conventions.md | 30 ++++---- docs/context/php/phpunit.md | 2 +- docs/context/php/prompts-and-dismissals.md | 84 +++++++++++----------- docs/context/php/rest-api.md | 28 ++++---- docs/context/php/settings-management.md | 46 ++++++------ docs/context/php/storage-patterns.md | 58 +++++++-------- docs/context/php/trait-composition.md | 48 ++++++------- 13 files changed, 236 insertions(+), 236 deletions(-) diff --git a/docs/context/js/hooks.md b/docs/context/js/hooks.md index 9f278630821..d0d6f8f365b 100644 --- a/docs/context/js/hooks.md +++ b/docs/context/js/hooks.md @@ -422,10 +422,10 @@ import { useSelect, useDispatch } from 'googlesitekit-data'; /** * Custom hook description. * - * @​since 1.25.0 + * \@since 1.25.0 * - * @​param {string} parameter Description of parameter. - * @​return {*} Description of return value. + * \@param {string} parameter Description of parameter. + * \@return {*} Description of return value. */ export default function useCustomHook(parameter) { // Hook implementation diff --git a/docs/context/js/jsdoc.md b/docs/context/js/jsdoc.md index 29cd68b6c4c..b71e7351c95 100644 --- a/docs/context/js/jsdoc.md +++ b/docs/context/js/jsdoc.md @@ -38,11 +38,11 @@ All utility functions, hooks, and complex API functions should follow this patte * * [Brief description of function purpose] * - * @​since n.e.x.t + * \@since n.e.x.t * - * @​param {type} paramName Description of parameter. - * @​param {type} [optionalParam] Description of optional parameter. - * @​return {type} Description of return value. + * \@param {type} paramName Description of parameter. + * \@param {type} [optionalParam] Description of optional parameter. + * \@return {type} Description of return value. */ function myFunction( paramName, optionalParam = defaultValue ) { // implementation @@ -51,66 +51,66 @@ function myFunction( paramName, optionalParam = defaultValue ) { ### Required JSDoc Tags -#### @​since Tag +#### \@since Tag **Always required** - Documents when the feature was introduced, always use `n.e.x.t` value which will be replaced with the actual version later on. ```javascript /** * Returns a callback to activate a module. * - * @​since n.e.x.t + * \@since n.e.x.t * - * @​param {string} moduleSlug Module slug. - * @​return {Function|null} Callback to activate module. + * \@param {string} moduleSlug Module slug. + * \@return {Function|null} Callback to activate module. */ ``` -#### @​param Tag +#### \@param Tag **Required for all parameters** - Documents parameter types and descriptions: ```javascript // Basic parameter -@​param {string} dateRange The date range slug. +\@param {string} dateRange The date range slug. // Optional parameter with default -@​param {boolean} [invertColor=false] Whether to reverse the +/- colors. +\@param {boolean} [invertColor=false] Whether to reverse the +/- colors. // Complex object parameter -@​param {Object} options Configuration options. -@​param {string} options.baseName The base name to use. -@​param {Function} options.controlCallback Callback function to issue the API request. -@​param {Function} [options.validateParams] Optional validation function. +\@param {Object} options Configuration options. +\@param {string} options.baseName The base name to use. +\@param {Function} options.controlCallback Callback function to issue the API request. +\@param {Function} [options.validateParams] Optional validation function. ``` -#### @​return Tag +#### \@return Tag **Required for functions that return values** - Documents return type and description: ```javascript -@​return {Object} Partial store object with properties 'actions', 'controls', and 'reducer'. -@​return {Function|null} Callback function or null if module doesn't exist. -@​return {Array.} Array of widget objects. +\@return {Object} Partial store object with properties 'actions', 'controls', and 'reducer'. +\@return {Function|null} Callback function or null if module doesn't exist. +\@return {Array.} Array of widget objects. ``` ### Complex Type Documentation -#### @​typedef for Complex Data Structures +#### \@typedef for Complex Data Structures -Use `@​typedef` to define complex object structures: +Use `\@typedef` to define complex object structures: ```javascript /** * Parse Analytics 4 report into data suitable for rendering. * - * @​typedef {Object} OverallPageMetricsData - * @​property {string} metric Google Analytics metric identifier. - * @​property {string} title Translated metric title. - * @​property {Array.} sparkLineData Data for rendering the sparkline. - * @​property {string} [datapointUnit] Optional datapoint unit, e.g. '%', 's'. - * @​property {number} total Total count for the metric. - * @​property {number} change Monthly change for the metric. + * \@typedef {Object} OverallPageMetricsData + * \@property {string} metric Google Analytics metric identifier. + * \@property {string} title Translated metric title. + * \@property {Array.} sparkLineData Data for rendering the sparkline. + * \@property {string} [datapointUnit] Optional datapoint unit, e.g. '%', 's'. + * \@property {number} total Total count for the metric. + * \@property {number} change Monthly change for the metric. * - * @​param {Object} report Raw Analytics report data. - * @​return {OverallPageMetricsData} Processed metrics data. + * \@param {Object} report Raw Analytics report data. + * \@return {OverallPageMetricsData} Processed metrics data. */ function parseReportData( report ) { // implementation @@ -126,10 +126,10 @@ Custom hooks require comprehensive JSDoc documentation: * Returns a callback to activate a module. If the call to activate the module * fails, an error will be returned to the returned callback. * - * @​since n.e.x.t + * \@since n.e.x.t * - * @​param {string} moduleSlug Module slug. - * @​return {Function|null} Callback to activate module, null if the module doesn't exist. + * \@param {string} moduleSlug Module slug. + * \@return {Function|null} Callback to activate module, null if the module doesn't exist. */ export default function useActivateModuleCallback( moduleSlug ) { // hook implementation @@ -145,15 +145,15 @@ Utility functions require detailed documentation with examples for complex cases * Creates a store object implementing the necessary infrastructure for making * asynchronous API requests and storing their data. * - * @​since n.e.x.t + * \@since n.e.x.t * - * @​param {Object} args Arguments for creating the fetch store. - * @​param {string} args.baseName The base name to use for all actions. - * @​param {Function} args.controlCallback Callback function to issue the API request. - * @​param {Function} [args.reducerCallback] Optional reducer to modify state. - * @​param {Function} [args.argsToParams] Function that reduces argument list to params. - * @​param {Function} [args.validateParams] Function that validates params before request. - * @​return {Object} Partial store object with properties 'actions', 'controls', and 'reducer'. + * \@param {Object} args Arguments for creating the fetch store. + * \@param {string} args.baseName The base name to use for all actions. + * \@param {Function} args.controlCallback Callback function to issue the API request. + * \@param {Function} [args.reducerCallback] Optional reducer to modify state. + * \@param {Function} [args.argsToParams] Function that reduces argument list to params. + * \@param {Function} [args.validateParams] Function that validates params before request. + * \@return {Object} Partial store object with properties 'actions', 'controls', and 'reducer'. */ export default function createFetchStore( { baseName, @@ -199,7 +199,7 @@ export default function createFetchStore( { 4. **Create typedefs** for complex recurring data structures ### Version Tracking -1. **Always include @​since** for new functions and significant changes using `n.e.x.t` value +1. **Always include \@since** for new functions and significant changes using `n.e.x.t` value 3. **Document breaking changes** in function descriptions ### Consistency Rules diff --git a/docs/context/php/asset-management.md b/docs/context/php/asset-management.md index 2fe119acaf3..e3bc7eb9f45 100644 --- a/docs/context/php/asset-management.md +++ b/docs/context/php/asset-management.md @@ -598,14 +598,14 @@ interface Module_With_Assets { /** * Get assets to register for the module. * - * @​return Asset[] Array of Asset instances. + * \@return Asset[] Array of Asset instances. */ public function get_assets(); /** * Enqueue all assets for the module. * - * @​param string $asset_context Context constant (Asset::CONTEXT_*). + * \@param string $asset_context Context constant (Asset::CONTEXT_*). */ public function enqueue_assets( $asset_context = Asset::CONTEXT_ADMIN_SITEKIT ); } @@ -651,7 +651,7 @@ trait Module_With_Assets_Trait { /** * Set up module assets. * - * @​return Asset[] Array of Asset instances. + * \@return Asset[] Array of Asset instances. */ abstract protected function setup_assets(); } @@ -688,8 +688,8 @@ interface Module_With_Inline_Data { /** * Get inline data for the module. * - * @​param array $modules_data Existing modules data. - * @​return array Updated modules data with module's data added. + * \@param array $modules_data Existing modules data. + * \@return array Updated modules data with module's data added. */ public function get_inline_data( $modules_data ); } @@ -901,8 +901,8 @@ final class Manifest { /** * Get manifest entry for asset handle. * - * @​param string $handle Asset handle. - * @​return array [ $filename, $hash ] or [ null, null ] if not found. + * \@param string $handle Asset handle. + * \@return array [ $filename, $hash ] or [ null, null ] if not found. */ public static function get( $handle ) { if ( null === self::$data ) { diff --git a/docs/context/php/context-pattern.md b/docs/context/php/context-pattern.md index d9629d59b74..9e6dc0364aa 100644 --- a/docs/context/php/context-pattern.md +++ b/docs/context/php/context-pattern.md @@ -28,8 +28,8 @@ The Context object provides five main categories of functionality: /** * Get absolute path to plugin directory or file. * - * @​param string $relative_path Optional. Relative path within plugin. Default '/'. - * @​return string Absolute path. + * \@param string $relative_path Optional. Relative path within plugin. Default '/'. + * \@return string Absolute path. */ public function path( $relative_path = '/' ) ``` @@ -58,8 +58,8 @@ $assets_dir = $context->path( 'dist/assets/' ); /** * Get URL to plugin directory or file. * - * @​param string $relative_path Optional. Relative path within plugin. Default '/'. - * @​return string URL. + * \@param string $relative_path Optional. Relative path within plugin. Default '/'. + * \@return string URL. */ public function url( $relative_path = '/' ) ``` @@ -84,9 +84,9 @@ $script_url = $context->url( 'dist/assets/js/googlesitekit-dashboard.js' ); /** * Get admin URL for a specific Site Kit page. * - * @​param string $slug Page slug (e.g., 'dashboard', 'settings'). - * @​param array $query_args Optional query parameters. - * @​return string Admin URL. + * \@param string $slug Page slug (e.g., 'dashboard', 'settings'). + * \@param array $query_args Optional query parameters. + * \@return string Admin URL. */ public function admin_url( $slug = 'dashboard', array $query_args = array() ) ``` @@ -116,14 +116,14 @@ $settings_url = $context->admin_url( 'settings', array( /** * Check if current request is an AMP request. * - * @​return bool True if AMP request. + * \@return bool True if AMP request. */ public function is_amp() /** * Get the AMP mode for the site. * - * @​return string 'primary', 'secondary', or empty string if not AMP. + * \@return string 'primary', 'secondary', or empty string if not AMP. */ public function get_amp_mode() ``` @@ -152,14 +152,14 @@ if ( 'primary' === $amp_mode ) { /** * Check if plugin is in network mode (multisite). * - * @​return bool True if network mode. + * \@return bool True if network mode. */ public function is_network_mode() /** * Check if plugin is network active. * - * @​return bool True if network active. + * \@return bool True if network active. */ public function is_network_active() ``` @@ -190,7 +190,7 @@ if ( $context->is_network_active() ) { /** * Get the reference site URL for the current request context. * - * @​return string Reference site URL. + * \@return string Reference site URL. */ public function get_reference_site_url() ``` @@ -235,7 +235,7 @@ private function filter_reference_url( $url = '' ) { /** * Get the canonical home URL. * - * @​return string Canonical home URL. + * \@return string Canonical home URL. */ public function get_canonical_home_url() ``` @@ -255,7 +255,7 @@ $home_url = $context->get_canonical_home_url(); /** * Get the reference entity for the current context. * - * @​return array Entity information array. + * \@return array Entity information array. */ public function get_reference_entity() ``` @@ -279,9 +279,9 @@ $entity = $context->get_reference_entity(); /** * Get locale for a specific context. * - * @​param string $context 'site' or 'user'. Default 'site'. - * @​param string $format 'language-code' or 'default'. Default 'default'. - * @​return string Locale string. + * \@param string $context 'site' or 'user'. Default 'site'. + * \@param string $format 'language-code' or 'default'. Default 'default'. + * \@return string Locale string. */ public function get_locale( $context = 'site', $format = 'default' ) ``` @@ -314,11 +314,11 @@ The Context provides a safe abstraction for accessing superglobals (GET, POST, e /** * Gets a specific external variable by name and optionally filters it. * - * @​param int $type One of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV. - * @​param string $variable_name Name of a variable to get. - * @​param int $filter [optional] The ID of the filter to apply. Default FILTER_DEFAULT. - * @​param mixed $options [optional] Associative array of options or bitwise disjunction of flags. - * @​return mixed Value of the requested variable on success. + * \@param int $type One of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV. + * \@param string $variable_name Name of a variable to get. + * \@param int $filter [optional] The ID of the filter to apply. Default FILTER_DEFAULT. + * \@param mixed $options [optional] Associative array of options or bitwise disjunction of flags. + * \@return mixed Value of the requested variable on success. */ public function filter( $type, $variable_name, $filter = FILTER_DEFAULT, $options = 0 ) ``` diff --git a/docs/context/php/dependency-injection.md b/docs/context/php/dependency-injection.md index d6f6ce60cba..8738892ac5a 100644 --- a/docs/context/php/dependency-injection.md +++ b/docs/context/php/dependency-injection.md @@ -419,12 +419,12 @@ class MyClass { /** * Constructor. * - * @​since 1.0.0 + * \@since 1.0.0 * - * @​param Context $context Plugin context instance. - * @​param Options $options Optional. Options instance. Default is a new instance. - * @​param User_Options $user_options Optional. User options instance. Default is a new instance. - * @​param Authentication $authentication Optional. Authentication instance. Default is a new instance. + * \@param Context $context Plugin context instance. + * \@param Options $options Optional. Options instance. Default is a new instance. + * \@param User_Options $user_options Optional. User options instance. Default is a new instance. + * \@param Authentication $authentication Optional. Authentication instance. Default is a new instance. */ public function __construct( Context $context, @@ -459,8 +459,8 @@ public function __construct( ```php /** - * @​param Context $context Plugin context. - * @​param Options $options Settings storage. + * \@param Context $context Plugin context. + * \@param Options $options Settings storage. */ ``` diff --git a/docs/context/php/module-architecture.md b/docs/context/php/module-architecture.md index aafdbbbf471..8f3dec3f736 100644 --- a/docs/context/php/module-architecture.md +++ b/docs/context/php/module-architecture.md @@ -118,7 +118,7 @@ interface Module_With_Settings { /** * Get module settings instance. * - * @​return Module_Settings + * \@return Module_Settings */ public function get_settings(); } @@ -153,7 +153,7 @@ interface Module_With_Scopes { /** * Get required OAuth scopes. * - * @​return array List of Google OAuth scopes. + * \@return array List of Google OAuth scopes. */ public function get_scopes(); } @@ -187,7 +187,7 @@ interface Module_With_Assets { /** * Get module assets to enqueue. * - * @​return array Array of Asset objects. + * \@return array Array of Asset objects. */ public function get_assets(); } @@ -228,7 +228,7 @@ interface Module_With_Tag { /** * Get the module tag instance. * - * @​return Module_Tag + * \@return Module_Tag */ public function get_tag(); } @@ -259,7 +259,7 @@ interface Module_With_Service_Entity { /** * Get the service entity access. * - * @​return Service_Entity_Access + * \@return Service_Entity_Access */ public function get_service_entity(); } @@ -278,7 +278,7 @@ interface Module_With_Inline_Data { /** * Get inline data for the module. * - * @​return array Associative array of inline data. + * \@return array Associative array of inline data. */ public function get_inline_data(); } @@ -316,7 +316,7 @@ interface Provides_Feature_Metrics { /** * Get the feature metrics instance. * - * @​return Feature_Metrics + * \@return Feature_Metrics */ public function get_feature_metrics(); } diff --git a/docs/context/php/naming-conventions.md b/docs/context/php/naming-conventions.md index 3aa7d0d7b24..16e2d8529b4 100644 --- a/docs/context/php/naming-conventions.md +++ b/docs/context/php/naming-conventions.md @@ -514,7 +514,7 @@ function googlesitekit_is_network_mode() { } /** * Class for managing module settings. * - * @​since 1.0.0 + * \@since 1.0.0 * @access private * @ignore */ @@ -527,11 +527,11 @@ final class Module_Settings extends Setting { /** * Get module settings. * - * @​since 1.0.0 + * \@since 1.0.0 * - * @​param string $key Optional. Setting key. Default empty string. - * @​param mixed $default Optional. Default value. Default null. - * @​return mixed Setting value or default. + * \@param string $key Optional. Setting key. Default empty string. + * \@param mixed $default Optional. Default value. Default null. + * \@return mixed Setting value or default. */ public function get( $key = '', $default = null ) { ``` @@ -542,24 +542,24 @@ public function get( $key = '', $default = null ) { /** * Plugin context instance. * - * @​since 1.0.0 - * @​var Context + * \@since 1.0.0 + * \@var Context */ private $context; ``` -### @​since Tag Convention +### \@since Tag Convention -For all new code (classes, methods, properties, constants), use `@​since n.e.x.t` as a placeholder. +For all new code (classes, methods, properties, constants), use `\@since n.e.x.t` as a placeholder. ```php /** * New method added in development. * - * @​since n.e.x.t + * \@since n.e.x.t * - * @​param string $param Parameter description. - * @​return bool True on success, false otherwise. + * \@param string $param Parameter description. + * \@return bool True on success, false otherwise. */ public function new_method( $param ) { ``` @@ -568,7 +568,7 @@ public function new_method( $param ) { /** * New class added in development. * - * @​since n.e.x.t + * \@since n.e.x.t */ final class New_Feature { ``` @@ -577,8 +577,8 @@ final class New_Feature { /** * New property added in development. * - * @​since n.e.x.t - * @​var string + * \@since n.e.x.t + * \@var string */ private $new_property; ``` diff --git a/docs/context/php/phpunit.md b/docs/context/php/phpunit.md index 84abc1e55c9..ec75e971c69 100644 --- a/docs/context/php/phpunit.md +++ b/docs/context/php/phpunit.md @@ -107,7 +107,7 @@ abstract class SettingsTestCase extends TestCase { /** * Get the option name for the setting. * - * @​return string + * \@return string */ abstract protected function get_option_name(); diff --git a/docs/context/php/prompts-and-dismissals.md b/docs/context/php/prompts-and-dismissals.md index b1c52d36a0b..98585ceaa1d 100644 --- a/docs/context/php/prompts-and-dismissals.md +++ b/docs/context/php/prompts-and-dismissals.md @@ -68,9 +68,9 @@ final class Dismissed_Prompts extends User_Setting { /** * Add or update a dismissed prompt. * - * @​param string $prompt Prompt slug. - * @​param int $expires_in_seconds Expiration in seconds (0 = permanent). - * @​return bool True on success. + * \@param string $prompt Prompt slug. + * \@param int $expires_in_seconds Expiration in seconds (0 = permanent). + * \@return bool True on success. */ public function add( $prompt, $expires_in_seconds = self::DISMISS_PROMPT_PERMANENTLY ) { $prompts = $this->get(); @@ -98,8 +98,8 @@ final class Dismissed_Prompts extends User_Setting { /** * Remove a dismissed prompt. * - * @​param string $prompt Prompt slug. - * @​return bool True on success. + * \@param string $prompt Prompt slug. + * \@return bool True on success. */ public function remove( $prompt ) { $prompts = $this->get(); @@ -115,7 +115,7 @@ final class Dismissed_Prompts extends User_Setting { /** * Get all dismissed prompts. * - * @​return array Dismissed prompts with metadata. + * \@return array Dismissed prompts with metadata. */ public function get() { return parent::get() ?: array(); @@ -124,7 +124,7 @@ final class Dismissed_Prompts extends User_Setting { /** * Get default value. * - * @​return array Empty array. + * \@return array Empty array. */ protected function get_default() { return array(); @@ -133,8 +133,8 @@ final class Dismissed_Prompts extends User_Setting { /** * Sanitize prompts data. * - * @​param array $prompts Prompts data. - * @​return array Sanitized prompts. + * \@param array $prompts Prompts data. + * \@return array Sanitized prompts. */ protected function sanitize_callback( $prompts ) { if ( ! is_array( $prompts ) ) { @@ -313,9 +313,9 @@ function AdBlockingRecoveryWidget() { /** * Dismiss a prompt. * - * @​param {string} slug Prompt slug. - * @​param {Object} options Options object. - * @​param {number} options.expiresInSeconds Expiration in seconds (0 = permanent). + * \@param {string} slug Prompt slug. + * \@param {Object} options Options object. + * \@param {number} options.expiresInSeconds Expiration in seconds (0 = permanent). */ *dismissPrompt( slug, { expiresInSeconds = 0 } = {} ) ``` @@ -341,31 +341,31 @@ dismissPrompt( 'my-prompt', { expiresInSeconds: 86400 } ); /** * Get all active dismissed prompts (filters expired). * - * @​return {Array} Array of prompt slugs. + * \@return {Array} Array of prompt slugs. */ getDismissedPrompts(); /** * Get dismiss count for a prompt. * - * @​param {string} slug Prompt slug. - * @​return {number} Number of times dismissed. + * \@param {string} slug Prompt slug. + * \@return {number} Number of times dismissed. */ getPromptDismissCount( slug ); /** * Check if prompt is dismissed. * - * @​param {string} slug Prompt slug. - * @​return {boolean} True if dismissed and not expired. + * \@param {string} slug Prompt slug. + * \@return {boolean} True if dismissed and not expired. */ isPromptDismissed( slug ); /** * Check if currently dismissing a prompt. * - * @​param {string} slug Prompt slug. - * @​return {boolean} True if dismissing. + * \@param {string} slug Prompt slug. + * \@return {boolean} True if dismissing. */ isDismissingPrompt( slug ); ``` @@ -464,9 +464,9 @@ final class Dismissed_Items extends User_Setting { /** * Add or update a dismissed item. * - * @​param string $item Item slug. - * @​param int $expires_in_seconds Expiration in seconds (0 = permanent). - * @​return bool True on success. + * \@param string $item Item slug. + * \@param int $expires_in_seconds Expiration in seconds (0 = permanent). + * \@return bool True on success. */ public function add( $item, $expires_in_seconds = self::DISMISS_ITEM_PERMANENTLY ) { $items = $this->get(); @@ -483,8 +483,8 @@ final class Dismissed_Items extends User_Setting { /** * Remove a dismissed item. * - * @​param string $item Item slug. - * @​return bool True on success. + * \@param string $item Item slug. + * \@return bool True on success. */ public function remove( $item ) { $items = $this->get(); @@ -500,7 +500,7 @@ final class Dismissed_Items extends User_Setting { /** * Get all dismissed items (including expired). * - * @​return array Dismissed items with expiration values. + * \@return array Dismissed items with expiration values. */ public function get() { return parent::get() ?: array(); @@ -509,8 +509,8 @@ final class Dismissed_Items extends User_Setting { /** * Check if item is dismissed and not expired. * - * @​param string $item Item slug. - * @​return bool True if dismissed and not expired. + * \@param string $item Item slug. + * \@return bool True if dismissed and not expired. */ public function is_dismissed( $item ) { $items = $this->get(); @@ -528,7 +528,7 @@ final class Dismissed_Items extends User_Setting { /** * Get only active dismissed items (filters expired). * - * @​return array Array of item slugs. + * \@return array Array of item slugs. */ public function get_dismissed_items() { $items = $this->get(); @@ -538,8 +538,8 @@ final class Dismissed_Items extends User_Setting { /** * Filter out expired items. * - * @​param array $items Items with expiration values. - * @​return array Filtered items. + * \@param array $items Items with expiration values. + * \@return array Filtered items. */ private function filter_dismissed_items( $items ) { return array_filter( @@ -553,7 +553,7 @@ final class Dismissed_Items extends User_Setting { /** * Get default value. * - * @​return array Empty array. + * \@return array Empty array. */ protected function get_default() { return array(); @@ -562,8 +562,8 @@ final class Dismissed_Items extends User_Setting { /** * Sanitize items data. * - * @​param array $items Items data. - * @​return array Sanitized items. + * \@param array $items Items data. + * \@return array Sanitized items. */ protected function sanitize_callback( $items ) { if ( ! is_array( $items ) ) { @@ -805,16 +805,16 @@ function MyNotice() { /** * Dismiss an item. * - * @​param {string} slug Item slug. - * @​param {Object} options Options object. - * @​param {number} options.expiresInSeconds Expiration in seconds (0 = permanent). + * \@param {string} slug Item slug. + * \@param {Object} options Options object. + * \@param {number} options.expiresInSeconds Expiration in seconds (0 = permanent). */ *dismissItem( slug, { expiresInSeconds = 0 } = {} ) /** * Remove dismissed items. * - * @​param {...string} slugs Item slugs to remove. + * \@param {...string} slugs Item slugs to remove. */ *removeDismissedItems( ...slugs ) ``` @@ -843,23 +843,23 @@ removeDismissedItems( 'item-1', 'item-2' ); /** * Get all active dismissed items (filters expired). * - * @​return {Array} Array of item slugs. + * \@return {Array} Array of item slugs. */ getDismissedItems(); /** * Check if item is dismissed. * - * @​param {string} slug Item slug. - * @​return {boolean} True if dismissed and not expired. + * \@param {string} slug Item slug. + * \@return {boolean} True if dismissed and not expired. */ isItemDismissed( slug ); /** * Check if currently dismissing an item. * - * @​param {string} slug Item slug. - * @​return {boolean} True if dismissing. + * \@param {string} slug Item slug. + * \@return {boolean} True if dismissing. */ isDismissingItem( slug ); ``` diff --git a/docs/context/php/rest-api.md b/docs/context/php/rest-api.md index 5af03e604d2..2f2dd113550 100644 --- a/docs/context/php/rest-api.md +++ b/docs/context/php/rest-api.md @@ -64,7 +64,7 @@ final class REST_Routes { /** * Filters the list of available REST routes. * - * @​param array $routes List of REST_Route objects. + * \@param array $routes List of REST_Route objects. */ return apply_filters( 'googlesitekit_rest_routes', $routes ); } @@ -85,9 +85,9 @@ final class REST_Route { /** * Constructor. * - * @​param string $uri Route URI pattern. - * @​param array $endpoints Route endpoints configuration. - * @​param array $args Optional route arguments. + * \@param string $uri Route URI pattern. + * \@param array $endpoints Route endpoints configuration. + * \@param array $args Optional route arguments. */ public function __construct( $uri, array $endpoints, array $args = array() ) { $this->uri = trim( $uri, '/' ); @@ -135,8 +135,8 @@ final class REST_Route { /** * Parse parameter arguments to ensure proper schema. * - * @​param array $args Parameter arguments. - * @​return array Parsed arguments. + * \@param array $args Parameter arguments. + * \@return array Parsed arguments. */ private function parse_param_args( $args ) { $parsed_args = array(); @@ -185,7 +185,7 @@ class REST_Feature_Controller { /** * Get REST route definitions. * - * @​return array Array of REST_Route objects. + * \@return array Array of REST_Route objects. */ private function get_rest_routes() { // Permission callbacks @@ -241,8 +241,8 @@ class REST_Feature_Controller { /** * GET callback for list endpoint. * - * @​param WP_REST_Request $request REST request object. - * @​return WP_REST_Response|WP_Error Response object or error. + * \@param WP_REST_Request $request REST request object. + * \@return WP_REST_Response|WP_Error Response object or error. */ public function get_list( WP_REST_Request $request ) { $status = $request->get_param( 'status' ); @@ -256,8 +256,8 @@ class REST_Feature_Controller { /** * POST callback for save endpoint. * - * @​param WP_REST_Request $request REST request object. - * @​return WP_REST_Response|WP_Error Response object or error. + * \@param WP_REST_Request $request REST request object. + * \@return WP_REST_Response|WP_Error Response object or error. */ public function save_item( WP_REST_Request $request ) { $name = $request->get_param( 'name' ); @@ -360,7 +360,7 @@ final class REST_Modules_Controller { /** * Get list of modules. * - * @​return WP_REST_Response Response with modules list. + * \@return WP_REST_Response Response with modules list. */ public function get_modules() { $modules = array_values( @@ -373,8 +373,8 @@ final class REST_Modules_Controller { /** * Activate a module. * - * @​param WP_REST_Request $request REST request. - * @​return WP_REST_Response|WP_Error Response or error. + * \@param WP_REST_Request $request REST request. + * \@return WP_REST_Response|WP_Error Response or error. */ public function activate_module( WP_REST_Request $request ) { $slug = $request->get_param( 'slug' ); diff --git a/docs/context/php/settings-management.md b/docs/context/php/settings-management.md index c9172d4fd14..e05d8b1f0d4 100644 --- a/docs/context/php/settings-management.md +++ b/docs/context/php/settings-management.md @@ -51,7 +51,7 @@ abstract class Setting { /** * Check if setting exists in database. * - * @​return bool True if setting exists. + * \@return bool True if setting exists. */ public function has() { $value = $this->get(); @@ -63,7 +63,7 @@ abstract class Setting { /** * Get setting value. * - * @​return mixed Setting value. + * \@return mixed Setting value. */ public function get() { $option = $this->get_option(); @@ -78,8 +78,8 @@ abstract class Setting { /** * Set setting value. * - * @​param mixed $value New value. - * @​return bool True on success. + * \@param mixed $value New value. + * \@return bool True on success. */ public function set( $value ) { return $this->update_option( $value ); @@ -88,7 +88,7 @@ abstract class Setting { /** * Delete setting. * - * @​return bool True on success. + * \@return bool True on success. */ public function delete() { return $this->delete_option(); @@ -97,8 +97,8 @@ abstract class Setting { /** * Register callback for setting changes. * - * @​param callable $callback Function to call when setting changes. - * @​return callable Unsubscribe function. + * \@param callable $callback Function to call when setting changes. + * \@return callable Unsubscribe function. */ public function on_change( callable $callback ) { // Observer pattern implementation @@ -107,7 +107,7 @@ abstract class Setting { /** * Get setting type. * - * @​return string Setting type (string, number, integer, boolean, array, object). + * \@return string Setting type (string, number, integer, boolean, array, object). */ protected function get_type() { return 'array'; @@ -116,7 +116,7 @@ abstract class Setting { /** * Get default value. * - * @​return mixed Default value. + * \@return mixed Default value. */ protected function get_default() { return array(); @@ -125,7 +125,7 @@ abstract class Setting { /** * Get sanitization callback. * - * @​return callable Sanitization function. + * \@return callable Sanitization function. */ protected function get_sanitize_callback() { return null; @@ -174,8 +174,8 @@ abstract class Module_Settings extends Setting { /** * Merge partial settings with existing settings. * - * @​param array $partial Partial settings to merge. - * @​return bool True on success. + * \@param array $partial Partial settings to merge. + * \@return bool True on success. */ public function merge( array $partial ) { $settings = $this->get(); @@ -197,7 +197,7 @@ abstract class Module_Settings extends Setting { /** * Check if any settings have changed from their saved values. * - * @​return bool True if any setting has changed. + * \@return bool True if any setting has changed. */ public function have_changed() { $settings = $this->get(); @@ -209,7 +209,7 @@ abstract class Module_Settings extends Setting { /** * Get saved settings (before any modifications). * - * @​return array Saved settings. + * \@return array Saved settings. */ protected function get_saved() { return $this->get(); @@ -253,7 +253,7 @@ final class Settings extends Module_Settings { /** * Get default settings. * - * @​return array Default settings. + * \@return array Default settings. */ protected function get_default() { return array( @@ -275,7 +275,7 @@ final class Settings extends Module_Settings { /** * Get setting type. * - * @​return string Setting type. + * \@return string Setting type. */ protected function get_type() { return 'array'; @@ -284,7 +284,7 @@ final class Settings extends Module_Settings { /** * Get sanitization callback. * - * @​return callable Sanitization function. + * \@return callable Sanitization function. */ protected function get_sanitize_callback() { return function ( $option ) { @@ -319,8 +319,8 @@ final class Settings extends Module_Settings { /** * Sanitize property ID. * - * @​param string $property_id Property ID to sanitize. - * @​return string Sanitized property ID. + * \@param string $property_id Property ID to sanitize. + * \@return string Sanitized property ID. */ private function sanitize_property_id( $property_id ) { // Allow special values @@ -349,7 +349,7 @@ final class Analytics_4 extends Module implements Module_With_Settings { /** * Set up module settings. * - * @​return Module_Settings Settings instance. + * \@return Module_Settings Settings instance. */ protected function setup_settings() { return new Settings( $this->options ); @@ -386,7 +386,7 @@ trait Setting_With_Owned_Keys_Trait { /** * Get owned setting keys. * - * @​return array List of owned setting keys. + * \@return array List of owned setting keys. */ protected function get_owned_keys() { return array(); @@ -395,7 +395,7 @@ trait Setting_With_Owned_Keys_Trait { /** * Check if owned settings have changed. * - * @​return bool True if owned settings changed. + * \@return bool True if owned settings changed. */ public function have_owned_settings_changed() { $settings = $this->get(); @@ -448,7 +448,7 @@ interface Setting_With_ViewOnly_Keys_Interface { /** * Get view-only setting keys. * - * @​return array List of view-only keys. + * \@return array List of view-only keys. */ public function get_view_only_keys(); } diff --git a/docs/context/php/storage-patterns.md b/docs/context/php/storage-patterns.md index 52d20819523..c63e51e6cbb 100644 --- a/docs/context/php/storage-patterns.md +++ b/docs/context/php/storage-patterns.md @@ -42,8 +42,8 @@ final class Options implements Options_Interface { /** * Get option value. * - * @​param string $option Option name. - * @​return mixed Option value or false if not found. + * \@param string $option Option name. + * \@return mixed Option value or false if not found. */ public function get( $option ) { if ( $this->context->is_network_mode() ) { @@ -55,9 +55,9 @@ final class Options implements Options_Interface { /** * Set option value. * - * @​param string $option Option name. - * @​param mixed $value Option value. - * @​return bool True on success. + * \@param string $option Option name. + * \@param mixed $value Option value. + * \@return bool True on success. */ public function set( $option, $value ) { if ( $this->context->is_network_mode() ) { @@ -69,8 +69,8 @@ final class Options implements Options_Interface { /** * Delete option. * - * @​param string $option Option name. - * @​return bool True on success. + * \@param string $option Option name. + * \@return bool True on success. */ public function delete( $option ) { if ( $this->context->is_network_mode() ) { @@ -82,8 +82,8 @@ final class Options implements Options_Interface { /** * Check if option exists. * - * @​param string $option Option name. - * @​return bool True if option exists. + * \@param string $option Option name. + * \@return bool True if option exists. */ public function has( $option ) { $value = $this->get( $option ); @@ -176,8 +176,8 @@ final class User_Options implements User_Options_Interface { /** * Get option for the current user. * - * @​param string $option Option name. - * @​return mixed Option value or false. + * \@param string $option Option name. + * \@return mixed Option value or false. */ public function get( $option ) { $user_id = $this->get_user_id(); @@ -196,9 +196,9 @@ final class User_Options implements User_Options_Interface { /** * Set option for the current user. * - * @​param string $option Option name. - * @​param mixed $value Option value. - * @​return bool True on success. + * \@param string $option Option name. + * \@param mixed $value Option value. + * \@return bool True on success. */ public function set( $option, $value ) { $user_id = $this->get_user_id(); @@ -212,8 +212,8 @@ final class User_Options implements User_Options_Interface { /** * Delete user option. * - * @​param string $option Option name. - * @​return bool True on success. + * \@param string $option Option name. + * \@return bool True on success. */ public function delete( $option ) { $user_id = $this->get_user_id(); @@ -227,8 +227,8 @@ final class User_Options implements User_Options_Interface { /** * Check if user has option. * - * @​param string $option Option name. - * @​return bool True if option exists. + * \@param string $option Option name. + * \@return bool True if option exists. */ public function has( $option ) { return false !== $this->get( $option ); @@ -237,7 +237,7 @@ final class User_Options implements User_Options_Interface { /** * Get current user ID. * - * @​return int User ID. + * \@return int User ID. */ public function get_user_id() { return $this->user_id; @@ -246,8 +246,8 @@ final class User_Options implements User_Options_Interface { /** * Switch to a different user context. * - * @​param int $user_id User ID to switch to. - * @​return bool True on success. + * \@param int $user_id User ID to switch to. + * \@return bool True on success. */ public function switch_user( $user_id ) { $user_id = (int) $user_id; @@ -375,8 +375,8 @@ final class Transients { /** * Get transient value. * - * @​param string $transient Transient name. - * @​return mixed Transient value or false if not found/expired. + * \@param string $transient Transient name. + * \@return mixed Transient value or false if not found/expired. */ public function get( $transient ) { if ( $this->context->is_network_mode() ) { @@ -388,10 +388,10 @@ final class Transients { /** * Set transient value. * - * @​param string $transient Transient name. - * @​param mixed $value Transient value. - * @​param int $expiration Expiration in seconds. - * @​return bool True on success. + * \@param string $transient Transient name. + * \@param mixed $value Transient value. + * \@param int $expiration Expiration in seconds. + * \@return bool True on success. */ public function set( $transient, $value, $expiration = 0 ) { if ( $this->context->is_network_mode() ) { @@ -403,8 +403,8 @@ final class Transients { /** * Delete transient. * - * @​param string $transient Transient name. - * @​return bool True on success. + * \@param string $transient Transient name. + * \@return bool True on success. */ public function delete( $transient ) { if ( $this->context->is_network_mode() ) { diff --git a/docs/context/php/trait-composition.md b/docs/context/php/trait-composition.md index e6e7ab1ea77..177c5682091 100644 --- a/docs/context/php/trait-composition.md +++ b/docs/context/php/trait-composition.md @@ -26,8 +26,8 @@ trait Method_Proxy_Trait { /** * Get a proxy closure for a class method. * - * @​param string $method Method name. - * @​return callable Proxy closure. + * \@param string $method Method name. + * \@return callable Proxy closure. */ private function get_method_proxy( $method ) { return function ( ...$args ) use ( $method ) { @@ -38,8 +38,8 @@ trait Method_Proxy_Trait { /** * Get a proxy closure that only executes once. * - * @​param string $method Method name. - * @​return callable Proxy closure. + * \@param string $method Method name. + * \@return callable Proxy closure. */ private function get_method_proxy_once( $method ) { return function ( ...$args ) use ( $method ) { @@ -91,7 +91,7 @@ trait User_Aware_Trait { /** * Get current user ID. * - * @​return int User ID. + * \@return int User ID. */ public function get_user_id() { return $this->user_id; @@ -100,8 +100,8 @@ trait User_Aware_Trait { /** * Switch to a different user context. * - * @​param int $user_id User ID to switch to. - * @​return bool True on success. + * \@param int $user_id User ID to switch to. + * \@return bool True on success. */ public function switch_user( $user_id ) { $this->user_id = (int) $user_id; @@ -146,14 +146,14 @@ trait Module_With_Settings_Trait { /** * Set up module settings. * - * @​return Module_Settings Settings instance. + * \@return Module_Settings Settings instance. */ abstract protected function setup_settings(); /** * Get module settings instance. * - * @​return Module_Settings Settings instance. + * \@return Module_Settings Settings instance. */ public function get_settings() { if ( ! $this->settings instanceof Module_Settings ) { @@ -194,14 +194,14 @@ trait Module_With_Scopes_Trait { /** * Set up required OAuth scopes. * - * @​return array List of OAuth scopes. + * \@return array List of OAuth scopes. */ abstract protected function setup_scopes(); /** * Get required OAuth scopes. * - * @​return array OAuth scopes. + * \@return array OAuth scopes. */ public function get_scopes() { if ( empty( $this->scopes ) ) { @@ -240,7 +240,7 @@ trait Module_With_Owner_Trait { /** * Get the module owner's user ID. * - * @​return int Owner user ID. + * \@return int Owner user ID. */ public function get_owner_id() { if ( ! $this instanceof Module_With_Settings ) { @@ -254,7 +254,7 @@ trait Module_With_Owner_Trait { /** * Get OAuth client for the module owner. * - * @​return OAuth_Client Owner's OAuth client. + * \@return OAuth_Client Owner's OAuth client. */ public function get_owner_oauth_client() { if ( $this->owner_oauth_client instanceof OAuth_Client ) { @@ -283,8 +283,8 @@ trait Module_With_Owner_Trait { /** * Set the module owner. * - * @​param int $owner_id Owner user ID. - * @​return bool True on success. + * \@param int $owner_id Owner user ID. + * \@return bool True on success. */ public function set_owner_id( $owner_id ) { if ( ! $this instanceof Module_With_Settings ) { @@ -327,14 +327,14 @@ trait Module_With_Assets_Trait { /** * Set up module assets. * - * @​return array List of Asset objects. + * \@return array List of Asset objects. */ abstract protected function setup_assets(); /** * Get module assets. * - * @​return array Asset objects. + * \@return array Asset objects. */ public function get_assets() { if ( null === $this->module_assets ) { @@ -380,14 +380,14 @@ trait Module_With_Tag_Trait { /** * Set up module tag. * - * @​return Module_Tag Tag instance. + * \@return Module_Tag Tag instance. */ abstract protected function setup_tag(); /** * Get module tag instance. * - * @​return Module_Tag Tag instance. + * \@return Module_Tag Tag instance. */ public function get_tag() { if ( ! $this->tag instanceof Module_Tag ) { @@ -429,14 +429,14 @@ trait Module_With_Data_Available_State_Trait { /** * Set up data available state. * - * @​return Data_Available_State State instance. + * \@return Data_Available_State State instance. */ abstract protected function setup_data_available_state(); /** * Get data available state instance. * - * @​return Data_Available_State State instance. + * \@return Data_Available_State State instance. */ public function get_data_available_state() { if ( ! $this->data_available_state instanceof Data_Available_State ) { @@ -460,7 +460,7 @@ trait Setting_With_Owned_Keys_Trait { /** * Get owned setting keys. * - * @​return array List of owned keys. + * \@return array List of owned keys. */ protected function get_owned_keys() { return array(); @@ -469,7 +469,7 @@ trait Setting_With_Owned_Keys_Trait { /** * Check if owned settings have changed. * - * @​return bool True if owned settings changed. + * \@return bool True if owned settings changed. */ public function have_owned_settings_changed() { $settings = $this->get(); @@ -490,7 +490,7 @@ trait Setting_With_Owned_Keys_Trait { /** * Get owned settings slugs. * - * @​return array Owned setting keys. + * \@return array Owned setting keys. */ public function get_owned_settings_slugs() { return $this->get_owned_keys(); From 4255bc0e4a79a5afc026bbafa37714482e3e7973 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Wed, 14 Jan 2026 15:34:35 +0200 Subject: [PATCH 15/20] Update the gemini workflow to use a readonly token to read an issue. --- .github/workflows/gemini.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gemini.yml b/.github/workflows/gemini.yml index 88fa584e510..528bf1f58b9 100644 --- a/.github/workflows/gemini.yml +++ b/.github/workflows/gemini.yml @@ -77,7 +77,7 @@ jobs: gemini --yolo --model gemini-3-pro-preview "/implement ${{ inputs.issue_number }}" env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_READONLY_TOKEN }} - name: Create Pull Request id: create-pr uses: peter-evans/create-pull-request@v7 From f515e39e157c20640713edf70065e94e0887500f Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Wed, 14 Jan 2026 15:43:15 +0200 Subject: [PATCH 16/20] Update the implement command to enforce usage of tests. --- .gemini/commands/implement.toml | 39 +++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/.gemini/commands/implement.toml b/.gemini/commands/implement.toml index 6debca23b40..2f67d564f4f 100644 --- a/.gemini/commands/implement.toml +++ b/.gemini/commands/implement.toml @@ -172,14 +172,16 @@ Follow these implementation guidelines: - Fix any linting errors immediately before proceeding - Do not proceed with failing lints - b. **Tests**: Run tests for changed files - - **For JS changes**: Identify and run related Jest tests - - **For PHP changes**: Run related PHPUnit tests - - Ensure all tests pass - - Fix any failing tests before proceeding + b. **Tests**: Run tests to verify changes (MANDATORY - DO NOT SKIP) + - **For JS changes**: Run !{npm run test:js} to execute Jest tests + - **For PHP changes**: Run !{composer run test} to execute PHPUnit tests + - **CRITICAL**: You MUST actually execute these test commands and verify output + - **CRITICAL**: ALL tests must pass before proceeding - a passing test run is REQUIRED + - If tests fail, fix the issues and re-run tests until they pass + - Do NOT proceed to the next phase if tests are failing c. **Build**: If you made significant changes, verify the build works - - Run !{npm run build} if appropriate + - Run !{npm run build:dev} if appropriate - Fix any build errors **8. Provide Implementation Summary** @@ -209,10 +211,13 @@ Follow these implementation guidelines: - [list of context conventions followed] - [reference specific docs used] - Verification Results: + Verification Results (MUST INCLUDE ACTUAL TEST OUTPUT): - Linting: [pass/fail with details] - - Tests: [pass/fail with details] + - JS Tests: [pass/fail - include output from: npm run test:js] + - PHP Tests: [pass/fail - include output from: composer run test] - Build: [if applicable] + + NOTE: Tests MUST have been executed. If tests were not run, go back and run them. ``` ═══════════════════════════════════════════════════════════════════════════════ @@ -292,6 +297,7 @@ Follow these code review guidelines: - Use this range if ANY documented principles are violated - Use this if critical security or functionality issues exist - Use this if tests are missing or failing + - Use this if tests were NOT actually executed (npm run test:js / composer run test) **0.5 - 0.84**: Below acceptable quality - Code works but doesn't meet quality standards @@ -304,6 +310,7 @@ Follow these code review guidelines: - Follows all documented principles - Code is clean and maintainable - Comprehensive tests with good coverage + - Tests were ACTUALLY EXECUTED and ALL passed (npm run test:js / composer run test) - Well documented - Minor improvements possible but not required @@ -392,9 +399,15 @@ PHASE 5: ITERATION (if score < 0.85) - Improve code structure and maintainability - Add missing tests or documentation -3. **Re-run Verification** - - Run linting: !{npm run lint:js} - - Run tests for affected files +3. **Re-run Verification** (MANDATORY - DO NOT SKIP) + - **Linting**: + - For JS: !{npm run lint:js} + - For PHP: !{composer run lint} + - **Tests** (MUST EXECUTE AND PASS): + - For JS: !{npm run test:js} + - For PHP: !{composer run test} + - **CRITICAL**: You MUST actually run these test commands and verify they pass + - **CRITICAL**: Do NOT proceed to Phase 4 re-review until ALL tests pass - Ensure fixes don't break existing functionality 4. **Return to Phase 4** @@ -627,6 +640,10 @@ IMPORTANT REMINDERS ✓ DO fix ALL violations of documented principles ✓ DO exit gracefully after 3 iterations if unable to reach 0.85 ✓ DO preserve all changes for human review +✓ MUST RUN TESTS before completing implementation: + - JS: !{npm run test:js} + - PHP: !{composer run test} +✓ DO NOT consider implementation complete until ALL tests pass Begin with Phase 1: Fetch GitHub Issue #{{args}} """ From d94350c44455789a6b8c00171d5be3b09e34c5b0 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Thu, 15 Jan 2026 10:59:44 +0200 Subject: [PATCH 17/20] Refactor Gemini workflow to separate implementation and pull request creation using patch files and granular permissions. --- .github/workflows/gemini.yml | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gemini.yml b/.github/workflows/gemini.yml index 528bf1f58b9..57c536d9bd9 100644 --- a/.github/workflows/gemini.yml +++ b/.github/workflows/gemini.yml @@ -14,9 +14,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 permissions: - contents: write - issues: write - pull-requests: write + contents: read + issues: read services: mysql: image: mysql:5.7 @@ -78,6 +77,33 @@ jobs: env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_READONLY_TOKEN }} + - name: Create patch + run: | + git add . + git diff --staged --binary > implementation.patch + - name: Upload patch + uses: actions/upload-artifact@v4 + with: + name: implementation-patch + path: implementation.patch + + create-pr: + name: Create Pull Request + needs: implement + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Download patch + uses: actions/download-artifact@v4 + with: + name: implementation-patch + - name: Apply patch + run: git apply implementation.patch - name: Create Pull Request id: create-pr uses: peter-evans/create-pull-request@v7 From 04ee04da13187e295c171cd57175f0eb92c48bf8 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Thu, 15 Jan 2026 11:00:23 +0200 Subject: [PATCH 18/20] Update Gemini workflow to use the default `GITHUB_TOKEN` secret instead of a read-only one. --- .github/workflows/gemini.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gemini.yml b/.github/workflows/gemini.yml index 57c536d9bd9..89668335475 100644 --- a/.github/workflows/gemini.yml +++ b/.github/workflows/gemini.yml @@ -76,7 +76,7 @@ jobs: gemini --yolo --model gemini-3-pro-preview "/implement ${{ inputs.issue_number }}" env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_READONLY_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create patch run: | git add . From c6b0c713a92e5d83a0d2623a3aab17ece068c1dd Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Thu, 15 Jan 2026 15:33:54 +0200 Subject: [PATCH 19/20] Switch Gemini GitHub tool configuration from an exclusion list to an inclusion list. --- .gemini/settings.json | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/.gemini/settings.json b/.gemini/settings.json index e3b2346fe01..5c54359156f 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -35,19 +35,12 @@ "httpUrl": "https://api.githubcopilot.com/mcp/?toolsets=issues", "headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" - } + }, + "includeTools": [ + "issue_read", + "list_issues", + "search_issues" + ] } - }, - "tools": { - "exclude": [ - "github__add_issue_comment", - "github__assign_copilot_to_issue", - "github__get_label", - "github__issue_write", - "github__list_issue_types", - "github__list_issues", - "github__search_issues", - "github__sub_issue_write" - ] } } From 1a22b2d107f5b9bbecf0bd1f32aa5db9892f83e7 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Thu, 15 Jan 2026 15:41:55 +0200 Subject: [PATCH 20/20] Update Gemini implement command configuration. --- .gemini/commands/implement.toml | 45 +++++++++++++++++---------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/.gemini/commands/implement.toml b/.gemini/commands/implement.toml index 2f67d564f4f..0dc6cda1de9 100644 --- a/.gemini/commands/implement.toml +++ b/.gemini/commands/implement.toml @@ -104,13 +104,14 @@ PHASE 3: IMPLEMENTATION Follow these implementation guidelines: -**1. Understand the Task** - - Carefully read the Implementation Brief from Phase 1 +1. **Understand the Task** + - Carefully read the Implementation Brief and Acceptance Criteria (AC) from Phase 1 + - Note that the AC are the actual requirements that MUST be met - Break down complex requirements into discrete steps - Plan your implementation approach before writing code - Identify all files that need to be created, modified, or deleted -**2. Implement Following Strict Principles** +2. **Implement Following Strict Principles** - Strictly follow ALL conventions from context documentation - Study existing code patterns in similar features before implementing - Use appropriate naming conventions from context docs @@ -119,7 +120,7 @@ Follow these implementation guidelines: - Follow security best practices (avoid XSS, SQL injection, etc.) - Use existing utilities and patterns rather than reinventing -**3. Write Comprehensive Tests** +3. **Write Comprehensive Tests** - **For JS**: Follow testing conventions from docs/context/js/tests.md - **For PHP**: Follow PHPUnit patterns from docs/context/php/phpunit.md - Write unit tests for ALL new functionality @@ -128,7 +129,7 @@ Follow these implementation guidelines: - Mock external dependencies appropriately - Aim for thorough test coverage of business logic -**4. Document Your Code** +4. **Document Your Code** - **For JS**: Follow JSDoc conventions from docs/context/js/jsdoc.md - **For PHP**: Follow PHPDoc conventions and WordPress documentation standards - Document all exported functions, components, classes, and complex logic @@ -136,7 +137,7 @@ Follow these implementation guidelines: - Add inline comments for non-obvious logic - Update README files if introducing new features -**5. Consider Integration Points** +5. **Consider Integration Points** **For JavaScript/React**: - **Event Tracking**: Use patterns from docs/context/js/event-tracking.md if user actions need tracking @@ -157,13 +158,13 @@ Follow these implementation guidelines: - **Admin Features**: Use docs/context/php/admin-features.md for admin UI - **Prompts**: Follow docs/context/php/prompts-and-dismissals.md for user prompts -**6. Create Supporting Files** +6. **Create Supporting Files** - **For JS**: Add Storybook stories for UI components (docs/context/js/storybook.md) - **For PHP**: Create appropriate test fixtures and mock data - Create fixture data for tests if needed - Update or create example usage if helpful -**7. Verify Your Implementation** +7. **Verify Your Implementation** Run these verification steps: a. **Linting**: @@ -184,7 +185,7 @@ Follow these implementation guidelines: - Run !{npm run build:dev} if appropriate - Fix any build errors -**8. Provide Implementation Summary** +8. **Provide Implementation Summary** After completing implementation, provide a structured summary: ``` @@ -228,7 +229,7 @@ PHASE 4: CODE REVIEW Follow these code review guidelines: -**1. Verify Requirements Adherence (MANDATORY)** +1. **Verify Requirements Adherence (MANDATORY)** Check compliance with the extracted "Acceptance criteria": - ✓ Implementation fulfills all points in "Acceptance criteria" @@ -236,7 +237,7 @@ Follow these code review guidelines: - ✓ Behavioral requirements are met - ✓ Edge cases defined in AC are handled -**2. Verify Context Adherence (MANDATORY)** +2. **Verify Context Adherence (MANDATORY)** Check strict compliance with documented principles. Review each relevant context file: @@ -275,7 +276,7 @@ Follow these code review guidelines: - What must be fixed to comply - Which files are affected -**3. Analyze Code Quality** +3. **Analyze Code Quality** Review for general code quality issues: - Code structure and organization @@ -288,7 +289,7 @@ Follow these code review guidelines: - Accessibility compliance (WCAG standards) - Browser/PHP version compatibility -**4. Score the Implementation (0.0 to 1.0)** +4. **Score the Implementation (0.0 to 1.0)** Assign a score based on this rubric: @@ -321,7 +322,7 @@ Follow these code review guidelines: - Outstanding documentation - Sets a positive example for future development -**5. Provide Review Results** +5. **Provide Review Results** Format your review as follows: @@ -387,7 +388,7 @@ PHASE 5: ITERATION (if score < 0.85) **Iteration [1, 2, or 3]**: -1. **Address ALL Requirements and Context Violations** (MANDATORY) +1. **Address ALL Requirements and Context Violations (MANDATORY)** - Requirements and context violations are non-negotiable and must be fixed - Follow the "Fix Required" instructions from the review - Bring code into full compliance with requirements and documented principles @@ -399,7 +400,7 @@ PHASE 5: ITERATION (if score < 0.85) - Improve code structure and maintainability - Add missing tests or documentation -3. **Re-run Verification** (MANDATORY - DO NOT SKIP) +3. **Re-run Verification (MANDATORY - DO NOT SKIP)** - **Linting**: - For JS: !{npm run lint:js} - For PHP: !{composer run lint} @@ -503,17 +504,17 @@ PHASE 6: DOCUMENTATION (optional - only if score ≥ 0.85) **If documentation is needed**, follow these documentation guidelines: -**1. Identify Documentation Needs** +1. **Identify Documentation Needs** - What new concepts were introduced? - What architectural decisions were made? - What complex patterns or workflows were created? -**2. Create Developer Documentation** +2. **Create Developer Documentation** - Location: docs/concepts/ directory - Format: Markdown (.md files) - Naming: Use descriptive kebab-case names (e.g., widget-lifecycle-management.md) -**3. Documentation Structure** +3. **Documentation Structure** Include these sections: - **Overview**: What is this concept and why does it exist? - **Core Principles**: Key architectural principles @@ -523,14 +524,14 @@ PHASE 6: DOCUMENTATION (optional - only if score ≥ 0.85) - **Common Pitfalls**: What to avoid - **Related Concepts**: Links to related documentation -**4. Documentation Style** +4. **Documentation Style** - Write for human developers, not AI agents - Use clear, conversational language - Include practical code examples - Focus on WHY and HOW, not just WHAT - Add diagrams if they help understanding (Mermaid or ASCII) -**5. Example Documentation File**: +5. **Example Documentation File:** ```markdown # [Concept Name] @@ -560,7 +561,7 @@ PHASE 6: DOCUMENTATION (optional - only if score ≥ 0.85) - [Link to related doc] ``` -**6. Update Existing Documentation** (if needed) +6. **Update Existing Documentation (if needed)** - If changes affect existing docs/concepts/ files, update them - Mark deprecated information clearly - Add new sections rather than replacing content