diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..851e813 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,122 @@ +name: Quality + +on: + push: + branches: + - main + paths: + - ".github/workflows/quality.yml" + - ".gitignore" + - "AGENTS.md" + - "addons/mimic/**/*.gd" + - "addons/mimic/**/*.res" + - "addons/mimic/**/*.tres" + - "addons/mimic/**/*.tscn" + - "addons/mimic/plugin.cfg" + - "CLAUDE.md" + - "export_presets.cfg" + - "examples/**/*.gd" + - "examples/**/*.res" + - "examples/**/*.tres" + - "examples/**/*.tscn" + - "project.godot" + - "test/**/*.gd" + - "test/**/*.res" + - "test/**/*.tres" + - "test/**/*.tscn" + - "tools/.gdignore" + - "tools/*.ps1" + - "tools/**/*.ps1" + - "tools/quality/**" + - "README.md" + - "docs/guides/ai_quality_workflow.md" + - "mkdocs.yml" + pull_request: + branches: + - main + paths: + - ".github/workflows/quality.yml" + - ".gitignore" + - "AGENTS.md" + - "addons/mimic/**/*.gd" + - "addons/mimic/**/*.res" + - "addons/mimic/**/*.tres" + - "addons/mimic/**/*.tscn" + - "addons/mimic/plugin.cfg" + - "CLAUDE.md" + - "export_presets.cfg" + - "examples/**/*.gd" + - "examples/**/*.res" + - "examples/**/*.tres" + - "examples/**/*.tscn" + - "project.godot" + - "test/**/*.gd" + - "test/**/*.res" + - "test/**/*.tres" + - "test/**/*.tscn" + - "tools/.gdignore" + - "tools/*.ps1" + - "tools/**/*.ps1" + - "tools/quality/**" + - "README.md" + - "docs/guides/ai_quality_workflow.md" + - "mkdocs.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + static_quality: + name: Static Quality + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: "22" + cache: npm + cache-dependency-path: tools/quality/package-lock.json + + - name: Setup Python + id: setup-python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + + - name: Cache bootstrapped quality tools + uses: actions/cache@v5 + with: + path: tools/.bin + key: quality-tools-${{ runner.os }}-python-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('tools/quality.ps1', 'tools/quality/gdstyle.toml', 'tools/quality/gdcruiser.json', 'tools/quality/requirements_quality.txt') }} + + - name: Run Mimic quality checks + shell: pwsh + run: ./tools/quality.ps1 -BootstrapTools + + godot_regression: + name: Godot Regression + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Godot + uses: chickensoft-games/setup-godot@v2 + with: + version: "4.6.3" + use-dotnet: false + include-templates: false + + - name: Verify Godot setup + shell: pwsh + run: godot --version + + - name: Run Godot regression checks + shell: pwsh + run: ./tools/verify.ps1 -SkipQuality diff --git a/.gitignore b/.gitignore index aae6668..4a38182 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ /android/ .vscode/settings.json /test/.output/ +/tools/.bin/ +/tools/quality/node_modules/ __pycache__/ # Generated documentation and export artifacts diff --git a/AGENTS.md b/AGENTS.md index d6a5608..5b34fbd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ Consistency updates: When changing public behavior, public API, Project Settings Godot MCP: Use the repo-local `.mcp.json` server named `godot` when an MCP-capable agent needs to query Godot, launch the editor, run the project, inspect project info, or capture debug output. The server is configured to run `npx -y @coding-solo/godot-mcp@latest` with `GODOT_PATH` set to `C:\Programming_Files\Godot\Godot_v4.6.3-stable_win64.exe\Godot_v4.6.3-stable_win64.exe`. Keep MCP configuration local to this repository unless explicitly requested otherwise. -Testing and automation: Treat tests as regression guardrails for AI-assisted changes, not as a mandatory TDD ceremony. Add or update GUT tests in `res://test/unit/` when changing public Mimic behavior, fixing bugs, or touching connection lifecycle, project settings, editor plugin behavior, networking helpers, or example flows that should stay stable. For meaningful feature changes, behavior changes, and risky refactors, run `powershell -NoProfile -ExecutionPolicy Bypass -File tools/verify.ps1` before final response; for especially risky work, run it before and after the change to catch regressions early. Do not add tests for docs-only edits, comments-only edits, or mechanical formatting with no behavior impact. Use `tools/run_two_instances.ps1` for explicit local ENet server/client smoke coverage; prefer these deterministic CLI scripts over MCP as the source of truth for CI-style verification. +Testing and automation: Treat tests as regression guardrails for AI-assisted changes, not as a mandatory TDD ceremony. Add or update GUT tests in `res://test/unit/` when changing public Mimic behavior, fixing bugs, or touching connection lifecycle, project settings, editor plugin behavior, networking helpers, or example flows that should stay stable. For meaningful feature changes, behavior changes, and risky refactors, run `powershell -NoProfile -ExecutionPolicy Bypass -File tools/verify.ps1` before final response; for especially risky work, run it before and after the change to catch regressions early. Use `powershell -NoProfile -ExecutionPolicy Bypass -File tools/quality.ps1` for the fast AI-focused quality gate: Mimic policy and public API documentation checks, PowerShell syntax checks, lockfile-backed `jscpd@4.2.4` duplicate-code detection with a committed baseline, optional hash-locked `gdcruiser==1.7.0` dependency architecture checks, and optional checksum-verified `gdstyle v0.1.4` diagnostics. Do not add tests for docs-only edits, comments-only edits, or mechanical formatting with no behavior impact. Use `tools/run_two_instances.ps1` for explicit local ENet server/client smoke coverage; prefer these deterministic CLI scripts over MCP as the source of truth for CI-style verification. Git commits: Use Conventional Commits in type(scope): summary form, such as feat(mimic): add connection logging. @@ -84,6 +84,12 @@ test/integration/mimic_connection_probe.gd: Explicit server/client probe script tools/: Local PowerShell automation entry points. tools/godot.ps1: Repo-local Godot CLI wrapper with Godot 4.6.3 fallback. tools/verify.ps1: Full local verification pass for import, unit tests, startup smoke, ENet explicit/auto-connect smoke, and WebSocket smoke. +tools/quality.ps1: Fast AI-focused quality gate for Mimic policy and public API documentation checks, PowerShell syntax checks, duplicate-code detection, dependency architecture checks, and GDScript style diagnostics. +tools/quality/: Static quality tool configuration kept out of Godot's resource scan by tools/.gdignore. +tools/quality/jscpd_baseline.json: Committed duplicate-code baseline used by the jscpd ratchet. +tools/quality/package.json: Local Node tool manifest for locked jscpd execution. +tools/quality/package-lock.json: npm lockfile with integrity hashes for jscpd and its transitive dependencies. +tools/quality/requirements_quality.txt: Hash-locked Python requirement used when bootstrapping gdcruiser. tools/run_two_instances.ps1: Explicit ENet/WebSocket server/client and auto-connect smoke test runner. tools/mkdocs_hooks.py: MkDocs hook that generates API docs and copies SVG/PNG brand assets into the built documentation site. ``` diff --git a/CLAUDE.md b/CLAUDE.md index d6a5608..5b34fbd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ Consistency updates: When changing public behavior, public API, Project Settings Godot MCP: Use the repo-local `.mcp.json` server named `godot` when an MCP-capable agent needs to query Godot, launch the editor, run the project, inspect project info, or capture debug output. The server is configured to run `npx -y @coding-solo/godot-mcp@latest` with `GODOT_PATH` set to `C:\Programming_Files\Godot\Godot_v4.6.3-stable_win64.exe\Godot_v4.6.3-stable_win64.exe`. Keep MCP configuration local to this repository unless explicitly requested otherwise. -Testing and automation: Treat tests as regression guardrails for AI-assisted changes, not as a mandatory TDD ceremony. Add or update GUT tests in `res://test/unit/` when changing public Mimic behavior, fixing bugs, or touching connection lifecycle, project settings, editor plugin behavior, networking helpers, or example flows that should stay stable. For meaningful feature changes, behavior changes, and risky refactors, run `powershell -NoProfile -ExecutionPolicy Bypass -File tools/verify.ps1` before final response; for especially risky work, run it before and after the change to catch regressions early. Do not add tests for docs-only edits, comments-only edits, or mechanical formatting with no behavior impact. Use `tools/run_two_instances.ps1` for explicit local ENet server/client smoke coverage; prefer these deterministic CLI scripts over MCP as the source of truth for CI-style verification. +Testing and automation: Treat tests as regression guardrails for AI-assisted changes, not as a mandatory TDD ceremony. Add or update GUT tests in `res://test/unit/` when changing public Mimic behavior, fixing bugs, or touching connection lifecycle, project settings, editor plugin behavior, networking helpers, or example flows that should stay stable. For meaningful feature changes, behavior changes, and risky refactors, run `powershell -NoProfile -ExecutionPolicy Bypass -File tools/verify.ps1` before final response; for especially risky work, run it before and after the change to catch regressions early. Use `powershell -NoProfile -ExecutionPolicy Bypass -File tools/quality.ps1` for the fast AI-focused quality gate: Mimic policy and public API documentation checks, PowerShell syntax checks, lockfile-backed `jscpd@4.2.4` duplicate-code detection with a committed baseline, optional hash-locked `gdcruiser==1.7.0` dependency architecture checks, and optional checksum-verified `gdstyle v0.1.4` diagnostics. Do not add tests for docs-only edits, comments-only edits, or mechanical formatting with no behavior impact. Use `tools/run_two_instances.ps1` for explicit local ENet server/client smoke coverage; prefer these deterministic CLI scripts over MCP as the source of truth for CI-style verification. Git commits: Use Conventional Commits in type(scope): summary form, such as feat(mimic): add connection logging. @@ -84,6 +84,12 @@ test/integration/mimic_connection_probe.gd: Explicit server/client probe script tools/: Local PowerShell automation entry points. tools/godot.ps1: Repo-local Godot CLI wrapper with Godot 4.6.3 fallback. tools/verify.ps1: Full local verification pass for import, unit tests, startup smoke, ENet explicit/auto-connect smoke, and WebSocket smoke. +tools/quality.ps1: Fast AI-focused quality gate for Mimic policy and public API documentation checks, PowerShell syntax checks, duplicate-code detection, dependency architecture checks, and GDScript style diagnostics. +tools/quality/: Static quality tool configuration kept out of Godot's resource scan by tools/.gdignore. +tools/quality/jscpd_baseline.json: Committed duplicate-code baseline used by the jscpd ratchet. +tools/quality/package.json: Local Node tool manifest for locked jscpd execution. +tools/quality/package-lock.json: npm lockfile with integrity hashes for jscpd and its transitive dependencies. +tools/quality/requirements_quality.txt: Hash-locked Python requirement used when bootstrapping gdcruiser. tools/run_two_instances.ps1: Explicit ENet/WebSocket server/client and auto-connect smoke test runner. tools/mkdocs_hooks.py: MkDocs hook that generates API docs and copies SVG/PNG brand assets into the built documentation site. ``` diff --git a/README.md b/README.md index b4663a6..1c53175 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ This project is intentionally smaller than full networking frameworks. Mimic is - [Mimic Or NetFox?](#mimic-or-netfox) - [Current Limitations](#current-limitations) - [Minimal Local Test](#minimal-local-test) -- [Regression Testing And Automation](#regression-testing-and-automation) - [Editor Multi-Instance Testing](#editor-multi-instance-testing) ## Compatibility Policy @@ -403,53 +402,6 @@ Expected result: - The second instance joins as client. - Connection events appear in the Godot output. -## Regression Testing And Automation - -Mimic includes automated checks intended to keep current behavior stable as the addon evolves. These tests are regression guardrails, not a requirement to practice test-driven development before every change. - -Run the full local verification pass from PowerShell: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File tools/verify.ps1 -``` - -This uses the repo-local Godot wrapper in `tools/godot.ps1`. By default it prefers a valid `MIMIC_GODOT_PATH`, then a valid `GODOT_PATH`, then the local Godot 4.6.3 path: - -```text -C:\Programming_Files\Godot\Godot_v4.6.3-stable_win64.exe\Godot_v4.6.3-stable_win64_console.exe -``` - -Override the executable for one run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File tools/verify.ps1 -GodotPath "C:\path\to\Godot.exe" -``` - -The verification pass does six things: - -- Imports project resources with Godot in headless mode. -- Runs GUT unit regression tests from `res://test/unit/`. -- Runs a minimal project startup probe headlessly without opening a network peer. -- Runs a two-instance ENet explicit host/client smoke test through `res://test/integration/mimic_connection_probe.tscn`. -- Runs a two-instance ENet `Server Then Client` smoke test. -- Runs a two-instance WebSocket explicit host/client smoke test. - -Run just the unit tests: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File tools/godot.ps1 --headless --path . -s res://addons/gut/gut_cmdln.gd -gconfig=res://.gutconfig.json -gexit -``` - -Run just the two-instance connection smoke test: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File tools/run_two_instances.ps1 -Transport enet -ConnectMode explicit -Port 18910 -powershell -NoProfile -ExecutionPolicy Bypass -File tools/run_two_instances.ps1 -Transport enet -ConnectMode server_then_client -Port 18911 -powershell -NoProfile -ExecutionPolicy Bypass -File tools/run_two_instances.ps1 -Transport websocket -ConnectMode explicit -Port 18912 -``` - -Unit tests use the vendored GUT addon in `res://addons/gut/`. Add tests when changing public Mimic behavior, fixing a bug, or touching connection/project-settings code that automated changes could easily regress later. - ## Editor Multi-Instance Testing Godot can launch multiple local game instances from the editor: diff --git a/docs/guides/ai_quality_workflow.md b/docs/guides/ai_quality_workflow.md new file mode 100644 index 0000000..dfdbc75 --- /dev/null +++ b/docs/guides/ai_quality_workflow.md @@ -0,0 +1,124 @@ +# AI Quality Workflow + +Mimic uses a small deterministic quality gate to catch the two failure modes AI-assisted changes tend to create first: regressions and duplicated code. + +This workflow is inspired by tools like [Impeccable](https://impeccable.style/) and [Fallow](https://fallow.tools/), but it is shaped for a Godot addon instead of a frontend app or JavaScript/TypeScript codebase. + +## What This Is + +Run the fast quality pass: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File tools/quality.ps1 +``` + +This command requires Node.js because the duplicate-code gate runs pinned `jscpd` through `npx`. + +Run it and bootstrap the optional pinned `gdcruiser` and `gdstyle` tools into ignored local tooling: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File tools/quality.ps1 -BootstrapTools +``` + +Run the full regression pass: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File tools/verify.ps1 +``` + +`tools/verify.ps1` runs `tools/quality.ps1` first, then Godot import, GUT unit tests, startup smoke, ENet smoke, ENet auto-connect smoke, and WebSocket smoke. + +Plain local runs use any optional pinned tools that are already present. CI runs `tools/quality.ps1 -BootstrapTools`, and local developers can run that command once to install hash-locked `gdcruiser` and checksum-verified `gdstyle` copies under ignored `tools/.bin/`. + +The GitHub Actions workflow also runs a Godot regression job with `tools/verify.ps1 -SkipQuality`, so pull requests get static quality checks and runtime regression checks without running the static gate twice. + +Use `tools/verify.ps1 -SkipQuality` when you need only the Godot regression checks on a machine without Node.js. + +## Checks + +The quality pass has five layers: + +- Mimic AI policy checks for project-specific mistakes and public API documentation. +- PowerShell syntax checks for repo automation scripts. +- Lockfile-backed `jscpd@4.2.4` duplicate-code detection for GDScript. +- Hash-locked `gdcruiser==1.7.0` dependency architecture checks when bootstrapped. +- Checksum-verified `gdstyle v0.1.4` diagnostics when available. + +The duplicate-code gate uses a 2% threshold with 8-line and 70-token minimums, then compares every reported clone fingerprint against `tools/quality/jscpd_baseline.json`. That makes it a ratchet: total duplication must stay under budget, and new clone fingerprints fail unless they are intentionally added to the baseline. The current baseline is two intentional-looking test helper/setup clones at about 1.8% duplication: + +- Project Settings name lists in `test_mimic_connection_processes.gd` and `test_mimic_project_settings.gd`. +- Project Settings save/restore helpers in `test_mimic_connection_processes.gd` and `test_mimic_runtime.gd`. + +Treat any new clone report as a refactor prompt. If a duplicate is deliberately clearer than an abstraction, keep it, but update `tools/quality/jscpd_baseline.json` in the same review with a plain reason. For rare generated or fixture blocks that would be worse when abstracted, use a narrow `# jscpd:ignore-start` / `# jscpd:ignore-end` block with a nearby explanation and keep the total project duplication under the threshold. + +## AI Policy Rules + +The custom policy check targets mistakes that generic linters do not understand: + +- String-based `call_deferred("...")` instead of typed callable dispatch. +- Direct addon runtime `print()`, `prints()`, or `printerr()` outside `MimicLog`. +- Scattered `mimic_multiplayer/*` Project Settings reads/writes outside `MimicProjectSettings`. +- Production addon `preload("res://addons/mimic/...")` dependencies instead of `class_name` usage. +- New `MultiplayerSpawner` logic before the project explicitly returns to spawn/despawn design. +- Raw RPC layers inside the addon before there is a design request for that behavior. +- Missing `##` documentation comments for addon-owned public classes, signals, enums, enum values, exported/public variables, constants, methods, and inner classes. +- Runtime references to quality tooling from `project.godot`. + +These are intentionally Mimic-shaped. They are not a general Godot style guide. +The documentation rule accepts Godot's official member documentation shapes: +`##` comments immediately before a public member or inline `##` comments on the +member line. Mimic still prefers preceding comments for readability. + +## Godot Style Guide Coverage + +`gdstyle` already covers a large part of the official Godot GDScript style guide, including file naming, identifier naming, tabs instead of spaces, line length, trailing whitespace, comment spacing, enum formatting, member ordering, and several quality rules such as duplicate dictionary keys, duplicated loads, unreachable code, allocation in loops, and `get_node()` in process callbacks. + +Mimic keeps `class_name X extends Y` on one line when both are present, so the workflow should not enforce the upstream split declaration style. Project-specific policy checks cover a few style choices that generic tools do not know, such as typed callable dispatch instead of string-based `call_deferred("...")`, preferring `class_name` dependencies over production addon preloads, and requiring public API documentation comments. + +The style layer is intentionally split: warning-level diagnostics are advisory, while error-level safety rules in `tools/quality/gdstyle.toml` fail the quality pass. The main styleguide-adjacent gap left for later is stricter member ordering after the current advisory diagnostics are cleaned up. It should not block this first workflow because a noisy style gate trains agents to ignore the gate. + +## Automation Syntax Rules + +The quality pass parses every PowerShell script in `tools/` before running the heavier checks. This is a small regression guard for the verification scripts themselves: broken PowerShell should fail quickly, before Godot or external tools are involved. + +## Dependency Architecture Rules + +`gdcruiser` gives Mimic a lightweight Fallow-like dependency graph for Godot files. It parses GDScript plus scene/resource references, checks `tools/quality/gdcruiser.json`, fails on detected cycles, and fails when parser errors would make the graph incomplete. + +The current rules are intentionally narrow: + +- Addon runtime code must not depend on examples, tests, docs, or tooling. +- Godot project modules must stay acyclic. + +The current baseline is 21 modules, 29 dependency edges, zero cycles, and zero parser errors, with expected unresolved `GutTest` base classes in unit tests because vendored GUT is excluded from project-owned analysis. + +## Impeccable Comparison + +Impeccable is valuable because it turns vague "AI slop" into deterministic checks and shared vocabulary. Its public CLI advertises deterministic design anti-pattern detection for PR checks, while its skill teaches AI agents how to avoid generic frontend defaults. + +Mimic needs the same operating model, but for code: + +- deterministic checks before agent opinions; +- project vocabulary captured in docs and scripts; +- reports that agents can act on without guessing; +- ratchets that improve the codebase over time instead of demanding a risky all-at-once rewrite. + +Impeccable itself is still frontend/design oriented. It is not a GDScript analyzer, duplicate-code detector, Godot runtime checker, or multiplayer regression tool. + +## Godot Tool Direction + +Useful Godot-adjacent options: + +- `jscpd` now supports GDScript and is the best duplicate-code detector for this repo; Mimic runs it from `tools/quality/package-lock.json` rather than floating through `npx`. +- `gdcruiser` is useful for dependency and architecture drift, especially circular dependencies and addon-to-example/test mistakes. +- `gdstyle` is promising for GDScript linting/formatting; Mimic currently blocks only on its error-level safety rules and treats the remaining warnings as advisory. +- GDQuest's GDScript formatter is useful for editor formatting, but it is not the duplicate/regression layer Mimic needs. +- Godot warnings and the existing GUT/integration scripts remain the source of truth for runtime behavior. + +The practical direction is not to install a large Godot editor plugin. Keep the gate local, scriptable, reviewable, and CI-friendly. + +## Dev-Only Boundary + +The workflow lives under `tools/` and writes reports under `test/.output/quality/`. Both are development-only locations, and `tools/.gdignore` keeps the tooling out of Godot's resource scan. + +Do not reference `tools/quality.ps1`, `jscpd`, `gdcruiser`, `gdstyle`, or generated quality reports from addon runtime code, scenes, autoloads, exported resources, or Project Settings. diff --git a/docs/quick_start.md b/docs/quick_start.md index e99bcf6..1065762 100644 --- a/docs/quick_start.md +++ b/docs/quick_start.md @@ -107,11 +107,3 @@ func _on_peer_disconnected(peer_id: int) -> void: func _on_stopped() -> void: MimicLog.log("Networking stopped") ``` - -## Verify Locally - -From this repository, run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File tools/verify.ps1 -``` diff --git a/mkdocs.yml b/mkdocs.yml index 8c8c15b..6b81ece 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,7 @@ plugins: - guides/project_settings.md: Mimic Project Settings reference. - guides/websockets.md: WebSocket and browser deployment guidance. - guides/deployment.md: Practical deployment notes. + - guides/ai_quality_workflow.md: AI-focused quality, duplicate-code, and regression workflow. Reference: - nodes/mimic.md: Runtime singleton teaching page. - nodes/mimic_connector.md: Connector node teaching page. @@ -93,6 +94,7 @@ nav: - Project Settings: guides/project_settings.md - WebSockets: guides/websockets.md - Deployment: guides/deployment.md + - AI Quality Workflow: guides/ai_quality_workflow.md - Examples: - Single To Multiplayer: examples/single_to_multiplayer.md - Other Multiplayer Addons: diff --git a/research/.gdignore b/research/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/test_mimic_run_instance_grid.gd b/test/unit/test_mimic_run_instance_grid.gd index 1dc7276..23d59c3 100644 --- a/test/unit/test_mimic_run_instance_grid.gd +++ b/test/unit/test_mimic_run_instance_grid.gd @@ -126,17 +126,7 @@ func test_fit_frame_rect_stays_inside_tiny_cell() -> void: var reference_client_size := Vector2i(1152, 648) var frame_margins := Vector4i(4, 28, 4, 4) - var fitted_rect: Rect2i = _grid.call( - "_fit_frame_rect_to_cell", - cell_rect, - reference_client_size, - frame_margins - ) - - assert_gte(fitted_rect.position.x, cell_rect.position.x) - assert_gte(fitted_rect.position.y, cell_rect.position.y) - assert_lte(fitted_rect.end.x, cell_rect.end.x) - assert_lte(fitted_rect.end.y, cell_rect.end.y) + _assert_fit_frame_rect_inside_cell(cell_rect, reference_client_size, frame_margins) func test_fit_frame_rect_clamps_when_cell_is_smaller_than_window_chrome() -> void: @@ -144,17 +134,7 @@ func test_fit_frame_rect_clamps_when_cell_is_smaller_than_window_chrome() -> voi var reference_client_size := Vector2i(1152, 648) var frame_margins := Vector4i(4, 28, 4, 4) - var fitted_rect: Rect2i = _grid.call( - "_fit_frame_rect_to_cell", - cell_rect, - reference_client_size, - frame_margins - ) - - assert_gte(fitted_rect.position.x, cell_rect.position.x) - assert_gte(fitted_rect.position.y, cell_rect.position.y) - assert_lte(fitted_rect.end.x, cell_rect.end.x) - assert_lte(fitted_rect.end.y, cell_rect.end.y) + _assert_fit_frame_rect_inside_cell(cell_rect, reference_client_size, frame_margins) func test_frame_border_is_hardcoded_not_measured_to_avoid_window_gaps() -> void: @@ -311,6 +291,24 @@ func _should_fit( ) +func _assert_fit_frame_rect_inside_cell( + cell_rect: Rect2i, + reference_client_size: Vector2i, + frame_margins: Vector4i +) -> void: + var fitted_rect: Rect2i = _grid.call( + "_fit_frame_rect_to_cell", + cell_rect, + reference_client_size, + frame_margins + ) + + assert_gte(fitted_rect.position.x, cell_rect.position.x) + assert_gte(fitted_rect.position.y, cell_rect.position.y) + assert_lte(fitted_rect.end.x, cell_rect.end.x) + assert_lte(fitted_rect.end.y, cell_rect.end.y) + + func _attach_grid_to_multiplayer_root(root: Node, index: int, count: int) -> void: root.add_child(_grid) _grid.set("_base_title", "Mimic Multiplayer") diff --git a/tools/export_web_example.ps1 b/tools/export_web_example.ps1 index d0d7b73..4b804b3 100644 --- a/tools/export_web_example.ps1 +++ b/tools/export_web_example.ps1 @@ -56,5 +56,60 @@ $arguments += @( (Join-Path $outputPath "index.html") ) -& $godotScript @arguments -exit $LASTEXITCODE +function Get-PowerShellExecutable { + $binaryName = "powershell.exe" + if ($PSVersionTable.PSEdition -eq "Core") { + $binaryName = "pwsh.exe" + if (-not ($env:OS -eq "Windows_NT")) { + $binaryName = "pwsh" + } + } + + $hostPath = Join-Path $PSHOME $binaryName + if (Test-Path -LiteralPath $hostPath) { + return $hostPath + } + + if ($PSVersionTable.PSEdition -eq "Core") { + return "pwsh" + } + return "powershell" +} + +$previousErrorActionPreference = $ErrorActionPreference +$ErrorActionPreference = "Continue" +try { + $exportOutput = & (Get-PowerShellExecutable) -NoProfile -ExecutionPolicy Bypass -File $godotScript @arguments 2>&1 + $exportExitCode = $LASTEXITCODE +} finally { + $ErrorActionPreference = $previousErrorActionPreference +} +$exportOutput | ForEach-Object { Write-Output $_ } + +if ($exportExitCode -ne 0) { + exit $exportExitCode +} + +if ($exportOutput -match "Cannot export project|Project export .* failed") { + exit 1 +} + +$expectedArtifacts = @( + "index.html", + "index.js", + "index.pck", + "index.wasm" +) + +$missingArtifacts = @() +foreach ($artifact in $expectedArtifacts) { + if (-not (Test-Path -LiteralPath (Join-Path $outputPath $artifact))) { + $missingArtifacts += $artifact + } +} + +if ($missingArtifacts.Count -gt 0) { + throw "Godot export did not write expected web artifact(s): $($missingArtifacts -join ', ')." +} + +exit 0 diff --git a/tools/quality.ps1 b/tools/quality.ps1 new file mode 100644 index 0000000..0b1c721 --- /dev/null +++ b/tools/quality.ps1 @@ -0,0 +1,1052 @@ +param( + [double] $DuplicateThreshold = 2.0, + [int] $DuplicateMinLines = 8, + [int] $DuplicateMinTokens = 70, + [string] $OutputDir = "test/.output/quality", + [switch] $BootstrapTools, + [switch] $SkipDuplicateCheck, + [switch] $SkipGdstyle, + [switch] $SkipDependencyCheck, + [switch] $SkipPowerShellSyntaxCheck, + [switch] $StrictGdstyle, + [switch] $SkipPolicyCheck +) + +$ErrorActionPreference = "Stop" + +$repoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path +$qualityRoot = Join-Path $PSScriptRoot "quality" +$resolvedOutputDir = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $OutputDir)) +$gdstyleVersion = "v0.1.4" +$gdcruiserVersion = "1.7.0" +$gdstyleConfig = Join-Path $qualityRoot "gdstyle.toml" +$gdcruiserConfig = Join-Path $qualityRoot "gdcruiser.json" +$jscpdBaselinePath = Join-Path $qualityRoot "jscpd_baseline.json" +$jscpdPackageRoot = $qualityRoot +$projectPaths = @("addons/mimic", "examples", "test") + +New-Item -ItemType Directory -Force -Path $resolvedOutputDir | Out-Null + +function Get-RelativePath { + param([string] $Path) + + $fullPath = [System.IO.Path]::GetFullPath($Path) + $rootWithSeparator = $repoRoot.TrimEnd( + [System.IO.Path]::DirectorySeparatorChar, + [System.IO.Path]::AltDirectorySeparatorChar + ) + [System.IO.Path]::DirectorySeparatorChar + if ($fullPath.StartsWith($rootWithSeparator, [System.StringComparison]::OrdinalIgnoreCase)) { + return $fullPath.Substring($rootWithSeparator.Length).Replace("\", "/") + } + return $fullPath.Replace("\", "/") +} + +function Resolve-ProjectPath { + param([string] $Path) + + return Join-Path $repoRoot $Path +} + +function Invoke-CheckedCommand { + param( + [string] $Label, + [string] $Command, + [string[]] $Arguments, + [bool] $AllowFailure = $false + ) + + Write-Host $Label + Push-Location $repoRoot + try { + & $Command @Arguments + $exitCode = $LASTEXITCODE + } finally { + Pop-Location + } + if ($exitCode -ne 0 -and -not $AllowFailure) { + throw "$Label failed with exit code $exitCode." + } +} + +function Get-GdstyleAssetMetadata { + if ($IsWindows -or $env:OS -eq "Windows_NT") { + return @{ + Name = "gdstyle-x86_64-pc-windows-msvc.zip" + Sha256 = "9cf3e7bf5ab56ac0e2a568c11f1184dfed87450e0069a053d73f78538a7fb05f" + } + } + if ($IsMacOS) { + $machine = "" + $uname = Get-Command uname -ErrorAction SilentlyContinue + if ($null -ne $uname) { + $machine = (& $uname.Source "-m") + } + if ($machine -eq "arm64" -or $machine -eq "aarch64") { + return @{ + Name = "gdstyle-aarch64-apple-darwin.tar.gz" + Sha256 = "6c46b740ffee6224fa299c3fc9d9e2e643ca58135f30e7aefbd68a44e14e8634" + } + } + return @{ + Name = "gdstyle-x86_64-apple-darwin.tar.gz" + Sha256 = "cb470f366334301821573a5e1517927a4780d268a45f4785483474f247ef8e9e" + } + } + return @{ + Name = "gdstyle-x86_64-unknown-linux-gnu.tar.gz" + Sha256 = "84c518c023d797e82cf6fc21ba9deb1a6abdf74cd7016cba9421efc420f0a299" + } +} + +function Get-LocalGdstylePath { + $binaryName = "gdstyle" + if ($IsWindows -or $env:OS -eq "Windows_NT") { + $binaryName = "gdstyle.exe" + } + + $localPath = Join-Path $repoRoot "tools/.bin/gdstyle/$gdstyleVersion/$binaryName" + if (Test-Path -LiteralPath $localPath) { + return $localPath + } + return "" +} + +function Get-PythonCommand { + $candidateNames = @("python", "python3") + foreach ($candidateName in $candidateNames) { + $candidate = Get-Command $candidateName -ErrorAction SilentlyContinue + if ($null -eq $candidate) { + continue + } + $versionOutput = & $candidate.Source "-c" "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" + if ($LASTEXITCODE -ne 0) { + continue + } + if ([version] $versionOutput -ge [version] "3.13") { + return $candidate.Source + } + } + + return "" +} + +function Get-NpmCommand { + $candidate = Get-Command npm -ErrorAction SilentlyContinue + if ($null -eq $candidate) { + return "" + } + return $candidate.Source +} + +function Get-LocalJscpdPath { + $binaryName = "jscpd" + if ($IsWindows -or $env:OS -eq "Windows_NT") { + $binaryName = "jscpd.cmd" + } + + $localPath = Join-Path $jscpdPackageRoot "node_modules/.bin/$binaryName" + if (Test-Path -LiteralPath $localPath) { + return $localPath + } + return "" +} + +function Install-Jscpd { + $npm = Get-NpmCommand + if ([string]::IsNullOrWhiteSpace($npm)) { + throw "npm was not found. Install Node.js to run the duplicate-code gate." + } + + Write-Host "Installing locked jscpd dependencies from $(Get-RelativePath (Join-Path $jscpdPackageRoot "package-lock.json"))..." + & $npm ` + ci ` + --ignore-scripts ` + --no-audit ` + --no-fund ` + --prefix ` + $jscpdPackageRoot | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Failed to install locked jscpd dependencies." + } + + $localPath = Get-LocalJscpdPath + if ([string]::IsNullOrWhiteSpace($localPath)) { + throw "Installed jscpd dependencies, but could not find the local jscpd executable." + } + return $localPath +} + +function Resolve-Jscpd { + $localPath = Get-LocalJscpdPath + if (-not [string]::IsNullOrWhiteSpace($localPath)) { + return $localPath + } + return Install-Jscpd +} + +function Get-LocalGdcruiserRoot { + return Join-Path $repoRoot "tools/.bin/gdcruiser/v$gdcruiserVersion" +} + +function Get-LocalGdcruiserPath { + $binaryName = "gdcruiser" + if ($IsWindows -or $env:OS -eq "Windows_NT") { + $binaryName = "gdcruiser.exe" + } + + $localPath = Join-Path (Get-LocalGdcruiserRoot) "bin/$binaryName" + if (Test-Path -LiteralPath $localPath) { + return $localPath + } + return "" +} + +function Install-Gdstyle { + $existingPath = Get-LocalGdstylePath + if (-not [string]::IsNullOrWhiteSpace($existingPath)) { + return $existingPath + } + + $asset = Get-GdstyleAssetMetadata + $assetName = [string] $asset.Name + $installRoot = Join-Path $repoRoot "tools/.bin/gdstyle/$gdstyleVersion" + $tempRoot = Join-Path $resolvedOutputDir "gdstyle-download" + $archivePath = Join-Path $tempRoot $assetName + $url = "https://github.com/atelico/gdstyle/releases/download/$gdstyleVersion/$assetName" + + New-Item -ItemType Directory -Force -Path $installRoot, $tempRoot | Out-Null + Write-Host "Downloading gdstyle $gdstyleVersion from $url" + $webRequestArgs = @{ + Uri = $url + OutFile = $archivePath + } + if ($PSVersionTable.PSEdition -eq "Desktop") { + $webRequestArgs["UseBasicParsing"] = $true + } + Invoke-WebRequest @webRequestArgs + $actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $archivePath).Hash.ToLowerInvariant() + if ($actualHash -ne $asset.Sha256) { + throw "gdstyle archive checksum mismatch for $assetName. Expected $($asset.Sha256), got $actualHash." + } + + if ($assetName.EndsWith(".zip")) { + Expand-Archive -LiteralPath $archivePath -DestinationPath $installRoot -Force + } else { + tar -xzf $archivePath -C $installRoot + if ($LASTEXITCODE -ne 0) { + throw "Failed to extract $archivePath." + } + } + + $installedPath = Get-LocalGdstylePath + if ([string]::IsNullOrWhiteSpace($installedPath)) { + $binary = Get-ChildItem -LiteralPath $installRoot -Recurse -File | + Where-Object { $_.Name -eq "gdstyle" -or $_.Name -eq "gdstyle.exe" } | + Select-Object -First 1 + if ($null -eq $binary) { + throw "Downloaded gdstyle, but could not find the executable in $installRoot." + } + $installedPath = $binary.FullName + } + return $installedPath +} + +function Install-Gdcruiser { + $existingPath = Get-LocalGdcruiserPath + if (-not [string]::IsNullOrWhiteSpace($existingPath)) { + return $existingPath + } + + $python = Get-PythonCommand + if ([string]::IsNullOrWhiteSpace($python)) { + throw "Python was not found. Install Python 3.13+ to bootstrap gdcruiser." + } + + $installRoot = Get-LocalGdcruiserRoot + New-Item -ItemType Directory -Force -Path $installRoot | Out-Null + Write-Host "Installing gdcruiser $gdcruiserVersion into $(Get-RelativePath $installRoot)" + $requirementsPath = Resolve-ProjectPath "tools/quality/requirements_quality.txt" + & $python ` + -m ` + pip ` + install ` + --quiet ` + --disable-pip-version-check ` + --no-deps ` + --only-binary=:all: ` + --require-hashes ` + --target ` + $installRoot ` + -r ` + $requirementsPath + if ($LASTEXITCODE -ne 0) { + throw "Failed to install gdcruiser $gdcruiserVersion." + } + + $installedPath = Get-LocalGdcruiserPath + if ([string]::IsNullOrWhiteSpace($installedPath)) { + throw "Installed gdcruiser, but could not find the executable in $installRoot." + } + return $installedPath +} + +function Resolve-Gdstyle { + $localPath = Get-LocalGdstylePath + if (-not [string]::IsNullOrWhiteSpace($localPath)) { + return $localPath + } + + if ($BootstrapTools) { + return Install-Gdstyle + } + + $pathCommand = Get-Command gdstyle -ErrorAction SilentlyContinue + if ($null -ne $pathCommand) { + return $pathCommand.Source + } + + return "" +} + +function Resolve-Gdcruiser { + $localPath = Get-LocalGdcruiserPath + if (-not [string]::IsNullOrWhiteSpace($localPath)) { + return $localPath + } + + if ($BootstrapTools) { + return Install-Gdcruiser + } + + return "" +} + +function Assert-GdstyleVersion { + param([string] $GdstylePath) + + $versionOutput = & $GdstylePath "--version" + if ($LASTEXITCODE -ne 0) { + throw "Failed to read gdstyle version from $GdstylePath." + } + if ($versionOutput -notmatch "0\.1\.4") { + throw "Expected gdstyle $gdstyleVersion, but found '$versionOutput' at $GdstylePath." + } +} + +function Invoke-GdcruiserCommand { + param( + [string] $GdcruiserPath, + [string[]] $Arguments + ) + + $localRoot = Get-LocalGdcruiserRoot + $previousPythonPath = $env:PYTHONPATH + if ($GdcruiserPath.StartsWith($localRoot, [System.StringComparison]::OrdinalIgnoreCase)) { + if ([string]::IsNullOrWhiteSpace($previousPythonPath)) { + $env:PYTHONPATH = $localRoot + } else { + $env:PYTHONPATH = "$localRoot$([System.IO.Path]::PathSeparator)$previousPythonPath" + } + } + + Push-Location $repoRoot + try { + & $GdcruiserPath @Arguments + $exitCode = $LASTEXITCODE + } finally { + Pop-Location + $env:PYTHONPATH = $previousPythonPath + } + return $exitCode +} + +function Add-PolicyIssue { + param( + [System.Collections.Generic.List[object]] $Issues, + [string] $Rule, + [string] $Message, + [string] $Path, + [int] $Line = 1, + [string] $Severity = "error" + ) + + $Issues.Add([PSCustomObject] @{ + severity = $Severity + rule = $Rule + path = $Path + line = $Line + message = $Message + }) | Out-Null +} + +function Add-RegexPolicyIssues { + param( + [System.Collections.Generic.List[object]] $Issues, + [string] $Rule, + [string] $Message, + [string[]] $Files, + [string] $Pattern, + [string[]] $Exclude = @(), + [string] $Severity = "error" + ) + + foreach ($file in $Files) { + $relativePath = Get-RelativePath $file + if ($Exclude -contains $relativePath) { + continue + } + $matches = Select-String -LiteralPath $file -Pattern $Pattern -AllMatches + foreach ($match in $matches) { + if ($match.Line.TrimStart().StartsWith("#")) { + continue + } + Add-PolicyIssue ` + -Issues $Issues ` + -Rule $Rule ` + -Message $Message ` + -Path $relativePath ` + -Line $match.LineNumber ` + -Severity $Severity + } + } +} + +function Add-RawRegexPolicyIssues { + param( + [System.Collections.Generic.List[object]] $Issues, + [string] $Rule, + [string] $Message, + [string[]] $Files, + [string] $Pattern, + [string[]] $Exclude = @(), + [string] $Severity = "error" + ) + + $regex = [regex]::new($Pattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) + foreach ($file in $Files) { + $relativePath = Get-RelativePath $file + if ($Exclude -contains $relativePath) { + continue + } + + $content = Get-Content -Raw -LiteralPath $file + foreach ($match in $regex.Matches($content)) { + $line = ($content.Substring(0, $match.Index) -split "`r?`n").Count + $lineText = (Get-Content -LiteralPath $file -TotalCount $line | Select-Object -Last 1) + if ($lineText.TrimStart().StartsWith("#")) { + continue + } + Add-PolicyIssue ` + -Issues $Issues ` + -Rule $Rule ` + -Message $Message ` + -Path $relativePath ` + -Line $line ` + -Severity $Severity + } + } +} + +function Test-GdscriptDocCommentBefore { + param( + [string[]] $Lines, + [int] $Index + ) + + for ($lineIndex = $Index - 1; $lineIndex -ge 0; $lineIndex--) { + $trimmedLine = $Lines[$lineIndex].Trim() + if ($trimmedLine.StartsWith("@") -or (Test-GdscriptAnnotationContinuation -Line $trimmedLine)) { + continue + } + return $trimmedLine.StartsWith("##") + } + return $false +} + +function Test-GdscriptAnnotationContinuation { + param([string] $Line) + + if ($Line -eq "") { + return $false + } + + return $Line -match '^[\)\]\}]\s*,?$' -or + $Line -match '^(?:"[^"]*"|''[^'']*''|[A-Za-z_][A-Za-z0-9_\.]*|-?\d+(?:\.\d+)?|true|false|null)\s*,?$' +} + +function Test-GdscriptDocCommentAfter { + param( + [string[]] $Lines, + [int] $Index + ) + + if ($Index + 1 -ge $Lines.Count) { + return $false + } + return $Lines[$Index + 1].Trim().StartsWith("##") +} + +function Test-GdscriptInlineDocComment { + param([string] $Line) + + $docIndex = $Line.IndexOf("##", [System.StringComparison]::Ordinal) + return $docIndex -gt 0 -and -not [string]::IsNullOrWhiteSpace($Line.Substring($docIndex + 2)) +} + +function Test-GdscriptMemberDocComment { + param( + [string[]] $Lines, + [int] $Index + ) + + return (Test-GdscriptDocCommentBefore -Lines $Lines -Index $Index) -or + (Test-GdscriptInlineDocComment -Line $Lines[$Index]) +} + +function Get-BraceDelta { + param([string] $Line) + + return ([regex]::Matches($Line, "\{").Count - [regex]::Matches($Line, "\}").Count) +} + +function Add-GdscriptPublicDocumentationIssues { + param( + [System.Collections.Generic.List[object]] $Issues, + [string[]] $Files + ) + + foreach ($file in $Files) { + $relativePath = Get-RelativePath $file + $lines = Get-Content -LiteralPath $file + $publicEnumDepth = 0 + + for ($index = 0; $index -lt $lines.Count; $index++) { + $line = $lines[$index] + $trimmedLine = $line.Trim() + $lineNumber = $index + 1 + + if ($trimmedLine -eq "" -or $trimmedLine.StartsWith("#")) { + continue + } + + if ($publicEnumDepth -gt 0) { + if ($trimmedLine -match '^([A-Z][A-Z0-9_]*)\s*(?:=\s*[^,]+)?\s*,?(?:\s*#.*)?$') { + $valueName = $Matches[1] + if (-not (Test-GdscriptMemberDocComment -Lines $lines -Index $index)) { + Add-PolicyIssue ` + -Issues $Issues ` + -Rule "style/public-enum-value-doc" ` + -Message "Document public enum value '$valueName' with a GDScript ## comment." ` + -Path $relativePath ` + -Line $lineNumber + } + } + + $publicEnumDepth += Get-BraceDelta $line + if ($publicEnumDepth -le 0) { + $publicEnumDepth = 0 + } + continue + } + + $isTopLevel = $line -notmatch '^\s' + if (-not $isTopLevel) { + continue + } + + if ($trimmedLine -match '^(class_name\s+[A-Za-z_][A-Za-z0-9_]*(?:\s+extends\s+[A-Za-z_][A-Za-z0-9_]*)?|extends\s+[A-Za-z_][A-Za-z0-9_]*)\b') { + if (-not (Test-GdscriptDocCommentAfter -Lines $lines -Index $index)) { + Add-PolicyIssue ` + -Issues $Issues ` + -Rule "style/public-class-doc" ` + -Message "Document addon script declarations with a GDScript ## class comment immediately after the declaration." ` + -Path $relativePath ` + -Line $lineNumber + } + continue + } + + if ($trimmedLine -match '^signal\s+([A-Za-z_][A-Za-z0-9_]*)\b') { + $signalName = $Matches[1] + if (-not $signalName.StartsWith("_") -and -not (Test-GdscriptMemberDocComment -Lines $lines -Index $index)) { + Add-PolicyIssue ` + -Issues $Issues ` + -Rule "style/public-signal-doc" ` + -Message "Document public signal '$signalName' with a GDScript ## comment." ` + -Path $relativePath ` + -Line $lineNumber + } + continue + } + + if ($trimmedLine -match '^class\s+([A-Za-z_][A-Za-z0-9_]*)\s*:') { + $innerClassName = $Matches[1] + if (-not $innerClassName.StartsWith("_") -and -not (Test-GdscriptMemberDocComment -Lines $lines -Index $index)) { + Add-PolicyIssue ` + -Issues $Issues ` + -Rule "style/public-inner-class-doc" ` + -Message "Document public inner class '$innerClassName' with a GDScript ## comment." ` + -Path $relativePath ` + -Line $lineNumber + } + continue + } + + if ($trimmedLine -match '^enum\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{?') { + $enumName = $Matches[1] + $isPublicEnum = -not $enumName.StartsWith("_") + if ($isPublicEnum -and -not (Test-GdscriptMemberDocComment -Lines $lines -Index $index)) { + Add-PolicyIssue ` + -Issues $Issues ` + -Rule "style/public-enum-doc" ` + -Message "Document public enum '$enumName' with a GDScript ## comment." ` + -Path $relativePath ` + -Line $lineNumber + } + if ($isPublicEnum) { + $publicEnumDepth = Get-BraceDelta $line + } + continue + } + + $symbolLine = $trimmedLine -replace '^(@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)+', '' + if ($symbolLine -match '^(?:static\s+)?var\s+([A-Za-z_][A-Za-z0-9_]*)\b') { + $variableName = $Matches[1] + if (-not $variableName.StartsWith("_") -and -not (Test-GdscriptMemberDocComment -Lines $lines -Index $index)) { + Add-PolicyIssue ` + -Issues $Issues ` + -Rule "style/public-variable-doc" ` + -Message "Document public variable '$variableName' with a GDScript ## comment." ` + -Path $relativePath ` + -Line $lineNumber + } + continue + } + + if ($symbolLine -match '^const\s+([A-Za-z_][A-Za-z0-9_]*)\b') { + $constantName = $Matches[1] + if (-not $constantName.StartsWith("_") -and -not (Test-GdscriptMemberDocComment -Lines $lines -Index $index)) { + Add-PolicyIssue ` + -Issues $Issues ` + -Rule "style/public-constant-doc" ` + -Message "Document public constant '$constantName' with a GDScript ## comment." ` + -Path $relativePath ` + -Line $lineNumber + } + continue + } + + if ($symbolLine -match '^(?:static\s+)?func\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(') { + $methodName = $Matches[1] + if (-not $methodName.StartsWith("_") -and -not (Test-GdscriptMemberDocComment -Lines $lines -Index $index)) { + Add-PolicyIssue ` + -Issues $Issues ` + -Rule "style/public-method-doc" ` + -Message "Document public method '$methodName' with a GDScript ## comment." ` + -Path $relativePath ` + -Line $lineNumber + } + } + } + } +} + +function Get-DevOnlyReferenceFiles { + $extensions = @(".cfg", ".gd", ".godot", ".tres", ".tscn") + $paths = @( + Resolve-ProjectPath "project.godot", + Resolve-ProjectPath "export_presets.cfg", + Resolve-ProjectPath "addons/mimic", + Resolve-ProjectPath "examples" + ) + + $files = @() + foreach ($path in $paths) { + if (-not (Test-Path -LiteralPath $path)) { + continue + } + + $item = Get-Item -LiteralPath $path + if ($item.PSIsContainer) { + $files += Get-ChildItem -LiteralPath $path -Recurse -File | + Where-Object { $extensions -contains $_.Extension.ToLowerInvariant() } | + ForEach-Object { $_.FullName } + } else { + $files += $item.FullName + } + } + + return [string[]] $files +} + +function Get-Sha256Text { + param([string] $Text) + + $bytes = [System.Text.Encoding]::UTF8.GetBytes($Text) + $sha256 = [System.Security.Cryptography.SHA256]::Create() + try { + $hashBytes = $sha256.ComputeHash($bytes) + return [System.BitConverter]::ToString($hashBytes).Replace("-", "").ToLowerInvariant() + } finally { + $sha256.Dispose() + } +} + +function Get-NormalizedJscpdPath { + param([string] $Path) + + return $Path.Replace("\", "/") +} + +function Get-JscpdCloneFingerprint { + param([object] $Clone) + + $paths = @( + (Get-NormalizedJscpdPath $Clone.firstFile.name), + (Get-NormalizedJscpdPath $Clone.secondFile.name) + ) | Sort-Object + $fragmentHash = Get-Sha256Text ([string] $Clone.fragment) + return "$($paths[0])|$($paths[1])|$fragmentHash" +} + +function Assert-JscpdBaseline { + param([string] $ReportPath) + + if (-not (Test-Path -LiteralPath $jscpdBaselinePath)) { + throw "jscpd baseline file is missing at $(Get-RelativePath $jscpdBaselinePath)." + } + + $baseline = Get-Content -Raw -LiteralPath $jscpdBaselinePath | ConvertFrom-Json + $allowedFingerprints = [System.Collections.Generic.HashSet[string]]::new() + foreach ($allowedClone in @($baseline.allowedClones)) { + $paths = @( + (Get-NormalizedJscpdPath $allowedClone.firstFile), + (Get-NormalizedJscpdPath $allowedClone.secondFile) + ) | Sort-Object + [void] $allowedFingerprints.Add("$($paths[0])|$($paths[1])|$($allowedClone.fragmentSha256)") + } + + $report = Get-Content -Raw -LiteralPath $ReportPath | ConvertFrom-Json + $unexpectedClones = [System.Collections.Generic.List[string]]::new() + foreach ($clone in @($report.duplicates)) { + $fingerprint = Get-JscpdCloneFingerprint $clone + if (-not $allowedFingerprints.Contains($fingerprint)) { + $firstPath = Get-NormalizedJscpdPath $clone.firstFile.name + $secondPath = Get-NormalizedJscpdPath $clone.secondFile.name + $unexpectedClones.Add( + "${firstPath}:$($clone.firstFile.start)-$($clone.firstFile.end) ~ " + + "${secondPath}:$($clone.secondFile.start)-$($clone.secondFile.end)" + ) + } + } + + if ($unexpectedClones.Count -eq 0) { + Write-Output "jscpd baseline ratchet passed with $(@($report.duplicates).Count) known clone(s)." + return + } + + Write-Output "jscpd found $($unexpectedClones.Count) unapproved clone(s):" + foreach ($unexpectedClone in $unexpectedClones) { + Write-Output $unexpectedClone + } + throw "Duplicate-code ratchet failed. Refactor new clones or update $(Get-RelativePath $jscpdBaselinePath) with an intentional baseline change." +} + +function Invoke-MimicPolicyCheck { + $issues = [System.Collections.Generic.List[object]]::new() + $gdFiles = Get-ChildItem ` + -LiteralPath (Resolve-ProjectPath "addons/mimic"), (Resolve-ProjectPath "examples"), (Resolve-ProjectPath "test") ` + -Recurse ` + -File ` + -Filter "*.gd" | + Where-Object { $_.FullName -notlike "*\addons\gut\*" } | + ForEach-Object { $_.FullName } + $addonFiles = $gdFiles | Where-Object { (Get-RelativePath $_).StartsWith("addons/mimic/") } + + Add-RegexPolicyIssues ` + -Issues $issues ` + -Rule "ai/no-string-call-deferred" ` + -Message "Use typed Callable dispatch, such as some_method.call_deferred(args), instead of string-based call_deferred()." ` + -Files $gdFiles ` + -Pattern 'call_deferred\s*\(\s*"' + + Add-RegexPolicyIssues ` + -Issues $issues ` + -Rule "ai/no-direct-addon-print" ` + -Message "Route addon runtime output through MimicLog so log level and formatting stay centralized." ` + -Files $addonFiles ` + -Pattern '\b(print|prints|printerr)\s*\(' ` + -Exclude @("addons/mimic/debug/mimic_log.gd") + + Add-RawRegexPolicyIssues ` + -Issues $issues ` + -Rule "ai/settings-centralized" ` + -Message "Read and write mimic_multiplayer ProjectSettings through MimicProjectSettings instead of scattering string keys." ` + -Files $addonFiles ` + -Pattern 'ProjectSettings\.(get_setting|set_setting|clear|has_setting)\s*\(\s*"mimic_multiplayer/' ` + -Exclude @("addons/mimic/settings/mimic_project_settings.gd") + + Add-RegexPolicyIssues ` + -Issues $issues ` + -Rule "ai/no-production-preload-addons" ` + -Message "Prefer class_name dependencies in addon code; reserve addon preloads for tests or narrowly justified cases." ` + -Files $addonFiles ` + -Pattern 'preload\s*\(\s*"res://addons/mimic/' + + Add-RegexPolicyIssues ` + -Issues $issues ` + -Rule "ai/no-addon-dev-resource-dependencies" ` + -Message "Addon runtime code must not load docs or tool scripts/resources." ` + -Files $addonFiles ` + -Pattern '\b(load|preload)\s*\(\s*"res://(docs|tools)/' + + Add-RegexPolicyIssues ` + -Issues $issues ` + -Rule "ai/no-custom-spawn-system" ` + -Message "Mimic's current focus excludes custom spawn/despawn behavior; discuss design before adding MultiplayerSpawner logic." ` + -Files $addonFiles ` + -Pattern '\bMultiplayerSpawner\b' + + Add-RegexPolicyIssues ` + -Issues $issues ` + -Rule "ai/no-raw-rpc-layer" ` + -Message "Do not add raw RPC protocols to the addon without an explicit design request; keep the MVP on Godot high-level helpers." ` + -Files $addonFiles ` + -Pattern '(^|\s)@rpc\b|\brpc(_id)?\s*\(' + + Add-GdscriptPublicDocumentationIssues -Issues $issues -Files $addonFiles + + Add-RawRegexPolicyIssues ` + -Issues $issues ` + -Rule "dev/no-quality-runtime-reference" ` + -Message "Quality tooling must not be referenced by project settings, export presets, scenes, resources, or runtime code." ` + -Files (Get-DevOnlyReferenceFiles) ` + -Pattern 'tools/(quality|\.bin)|quality\.ps1|jscpd|gdcruiser|gdstyle|test[/\\]\.output[/\\]quality' + + if (-not (Test-Path -LiteralPath (Join-Path $repoRoot "tools/.gdignore"))) { + Add-PolicyIssue ` + -Issues $issues ` + -Rule "dev/export-boundary" ` + -Message "tools/.gdignore must exist so quality scripts stay out of Godot's resource scan." ` + -Path "tools/.gdignore" + } + + $policyJsonPath = Join-Path $resolvedOutputDir "mimic-policy.json" + [object[]] $policyIssueArray = $issues.ToArray() + ConvertTo-Json -InputObject $policyIssueArray -Depth 4 | + Set-Content -LiteralPath $policyJsonPath -Encoding UTF8 + + if ($issues.Count -eq 0) { + Write-Output "Mimic AI policy check passed." + return + } + + Write-Output "Mimic AI policy check found $($issues.Count) issue(s):" + foreach ($issue in $issues) { + Write-Output "$($issue.path):$($issue.line) [$($issue.severity)] $($issue.rule): $($issue.message)" + } + throw "Mimic AI policy check failed. See $(Get-RelativePath $policyJsonPath)." +} + +function Invoke-PowerShellSyntaxCheck { + $issues = [System.Collections.Generic.List[object]]::new() + $scriptFiles = Get-ChildItem ` + -LiteralPath (Resolve-ProjectPath "tools") ` + -Recurse ` + -File ` + -Filter "*.ps1" | + Where-Object { $_.FullName -notlike "*\tools\.bin\*" } | + ForEach-Object { $_.FullName } + + foreach ($scriptFile in $scriptFiles) { + $tokens = $null + $parseErrors = $null + [System.Management.Automation.Language.Parser]::ParseFile( + $scriptFile, + [ref] $tokens, + [ref] $parseErrors + ) | Out-Null + + foreach ($parseError in $parseErrors) { + Add-PolicyIssue ` + -Issues $issues ` + -Rule "dev/powershell-syntax" ` + -Message $parseError.Message ` + -Path (Get-RelativePath $scriptFile) ` + -Line $parseError.Extent.StartLineNumber + } + } + + $syntaxJsonPath = Join-Path $resolvedOutputDir "powershell-syntax.json" + [object[]] $syntaxIssueArray = $issues.ToArray() + ConvertTo-Json -InputObject $syntaxIssueArray -Depth 4 | + Set-Content -LiteralPath $syntaxJsonPath -Encoding UTF8 + + if ($issues.Count -eq 0) { + Write-Output "PowerShell syntax check passed for $($scriptFiles.Count) script(s)." + return + } + + Write-Output "PowerShell syntax check found $($issues.Count) issue(s):" + foreach ($issue in $issues) { + Write-Output "$($issue.path):$($issue.line) [$($issue.severity)] $($issue.rule): $($issue.message)" + } + throw "PowerShell syntax check failed. See $(Get-RelativePath $syntaxJsonPath)." +} + +if (-not $SkipPolicyCheck) { + Invoke-MimicPolicyCheck +} else { + Write-Output "Skipping Mimic AI policy check." +} + +if (-not $SkipPowerShellSyntaxCheck) { + Invoke-PowerShellSyntaxCheck +} else { + Write-Output "Skipping PowerShell syntax check." +} + +if (-not $SkipDuplicateCheck) { + $jscpd = Resolve-Jscpd + + $jscpdOutput = Join-Path $resolvedOutputDir "jscpd" + New-Item -ItemType Directory -Force -Path $jscpdOutput | Out-Null + $jscpdReportPath = Join-Path $jscpdOutput "jscpd-report.json" + $jscpdArgs = @( + "--reporters", + "console,ai,json", + "--output", + $jscpdOutput, + "--min-lines", + [string] $DuplicateMinLines, + "--min-tokens", + [string] $DuplicateMinTokens, + "--threshold", + [string] $DuplicateThreshold, + "--formats-exts", + "gdscript:gd", + "--ignore", + "**/addons/gut/**,**/.godot/**,**/build/**,**/docs/api/**,**/test/.output/**,**/tools/.bin/**", + "--noTips" + ) + $projectPaths + + Invoke-CheckedCommand ` + -Label "Running jscpd duplicate-code check at <= $DuplicateThreshold% duplication..." ` + -Command $jscpd ` + -Arguments $jscpdArgs + Assert-JscpdBaseline -ReportPath $jscpdReportPath +} else { + Write-Output "Skipping duplicate-code check." +} + +if (-not $SkipDependencyCheck) { + $gdcruiser = Resolve-Gdcruiser + if ([string]::IsNullOrWhiteSpace($gdcruiser)) { + Write-Output "gdcruiser was not found. Run tools/quality.ps1 -BootstrapTools to enable the dependency architecture check locally." + } else { + $gdcruiserJsonPath = Join-Path $resolvedOutputDir "gdcruiser.json" + $gdcruiserArgs = @( + ".", + "--config", + $gdcruiserConfig, + "-f", + "json", + "-o", + $gdcruiserJsonPath + ) + + Write-Output "Running gdcruiser dependency architecture check with $(Get-RelativePath $gdcruiserConfig)..." + $gdcruiserExit = Invoke-GdcruiserCommand -GdcruiserPath $gdcruiser -Arguments $gdcruiserArgs + if ($gdcruiserExit -ne 0) { + throw "gdcruiser dependency check failed with exit code $gdcruiserExit. See $(Get-RelativePath $gdcruiserJsonPath)." + } + + $dependencyReport = Get-Content -Raw -LiteralPath $gdcruiserJsonPath | ConvertFrom-Json + $cycleCount = @($dependencyReport.cycles).Count + $errorCount = @($dependencyReport.errors).Count + Write-Output "gdcruiser checked $($dependencyReport.graph.stats.module_count) module(s), $($dependencyReport.graph.stats.dependency_count) dependency edge(s), $cycleCount cycle(s), and $errorCount parser error(s)." + if ($errorCount -gt 0) { + Write-Output "gdcruiser parser errors:" + @($dependencyReport.errors) | + Select-Object -First 5 | + ForEach-Object { + Write-Output ($_ | ConvertTo-Json -Compress -Depth 6) + } + throw "gdcruiser reported $errorCount parser error(s). See $(Get-RelativePath $gdcruiserJsonPath)." + } + } +} else { + Write-Output "Skipping dependency architecture check." +} + +if (-not $SkipGdstyle) { + $gdstyle = Resolve-Gdstyle + if ([string]::IsNullOrWhiteSpace($gdstyle)) { + Write-Output "gdstyle was not found. Run tools/quality.ps1 -BootstrapTools to enable the GDScript style gate locally." + } else { + Assert-GdstyleVersion $gdstyle + $gdstyleJsonPath = Join-Path $resolvedOutputDir "gdstyle.json" + $gdstyleArgs = @( + "check", + "--config", + $gdstyleConfig, + "--format", + "json", + "--no-color" + ) + $projectPaths + + Write-Output "Running gdstyle check with $(Get-RelativePath $gdstyleConfig)..." + Push-Location $repoRoot + try { + $gdstyleOutput = & $gdstyle @gdstyleArgs + $gdstyleExit = $LASTEXITCODE + } finally { + Pop-Location + } + $gdstyleText = ($gdstyleOutput -join "`n") + Set-Content -LiteralPath $gdstyleJsonPath -Value $gdstyleText -Encoding UTF8 + if ($gdstyleExit -ne 0) { + throw "gdstyle check failed with exit code $gdstyleExit. See $(Get-RelativePath $gdstyleJsonPath)." + } + $gdstyleDiagnostics = @() + if (-not [string]::IsNullOrWhiteSpace($gdstyleText)) { + $convertedDiagnostics = $gdstyleText | ConvertFrom-Json + if ($convertedDiagnostics -is [System.Array]) { + $gdstyleDiagnostics = $convertedDiagnostics + } else { + $gdstyleDiagnostics = @($convertedDiagnostics) + } + } + if ($gdstyleDiagnostics.Count -gt 0) { + Write-Output "gdstyle reported $($gdstyleDiagnostics.Count) advisory diagnostic(s). Full report: $(Get-RelativePath $gdstyleJsonPath)." + $gdstyleDiagnostics | + Select-Object -First 8 | + ForEach-Object { + Write-Output "$($_.file):$($_.span.line) [$($_.rule)] $($_.message)" + } + if ($gdstyleDiagnostics.Count -gt 8) { + Write-Output "...and $($gdstyleDiagnostics.Count - 8) more advisory diagnostic(s)." + } + } else { + Write-Output "gdstyle reported no diagnostics." + } + + if ($StrictGdstyle) { + $gdstyleFmtArgs = @( + "fmt", + "--check", + "--config", + $gdstyleConfig, + "--no-color" + ) + $projectPaths + Invoke-CheckedCommand ` + -Label "Running strict gdstyle formatting check..." ` + -Command $gdstyle ` + -Arguments $gdstyleFmtArgs + } + } +} else { + Write-Output "Skipping gdstyle check." +} + +Write-Output "Mimic quality checks completed." diff --git a/tools/quality/gdcruiser.json b/tools/quality/gdcruiser.json new file mode 100644 index 0000000..8abff56 --- /dev/null +++ b/tools/quality/gdcruiser.json @@ -0,0 +1,33 @@ +{ + "options": { + "exclude": [ + "^res://addons/gut/", + "^res://build/", + "^res://docs/api/", + "^res://site/", + "^res://test/.output/" + ] + }, + "forbidden": [ + { + "name": "no-addon-to-examples-or-tests", + "comment": "Production addon code must not depend on examples or regression tests.", + "from": { + "path": "^res://addons/mimic/" + }, + "to": { + "path": "^res://(examples|test)/" + } + }, + { + "name": "no-addon-to-dev-tooling", + "comment": "Quality and documentation tooling must stay outside addon runtime dependencies.", + "from": { + "path": "^res://addons/mimic/" + }, + "to": { + "path": "^res://(docs|tools)/" + } + } + ] +} diff --git a/tools/quality/gdstyle.toml b/tools/quality/gdstyle.toml new file mode 100644 index 0000000..13a0237 --- /dev/null +++ b/tools/quality/gdstyle.toml @@ -0,0 +1,46 @@ +# Mimic runs gdstyle as a cautious advisory/error gate for AI-generated GDScript. +# This config intentionally checks only project-owned scripts selected by +# tools/quality.ps1; vendored GUT files stay out of scope. + +max_line_length = 100 +use_tabs = true +max_function_length = 70 +max_file_length = 700 +max_parameters = 7 +max_returns = 8 +max_nesting_depth = 4 +max_local_variables = 14 +max_branches = 10 +max_class_variables = 30 +max_public_methods = 25 +max_inner_classes = 5 +exclude = [ + ".godot", + "addons/gut", + "build", + "docs/api", + "test/.output", + "tools", +] + +[rules] +"syntax/lex-error" = "error" +"naming/file-name-snake-case" = "error" +"format/trailing-whitespace" = "error" +"format/trailing-newline" = "error" +"format/no-tabs-as-spaces" = "error" +"format/one-statement-per-line" = "error" +"quality/self-comparison" = "error" +"quality/no-self-assign" = "error" +"quality/duplicate-dict-key" = "error" +"quality/duplicated-load" = "error" +"quality/unreachable-code" = "error" +"quality/await-in-loop" = "error" +"quality/allocation-in-loop" = "error" +"quality/process-get-node" = "error" +"quality/no-debug-print" = "warn" +"quality/type-hint" = "warn" +"quality/max-function-length" = "warn" +"quality/max-file-length" = "warn" +"quality/max-public-methods" = "warn" +"quality/max-class-variables" = "warn" diff --git a/tools/quality/jscpd_baseline.json b/tools/quality/jscpd_baseline.json new file mode 100644 index 0000000..14a1420 --- /dev/null +++ b/tools/quality/jscpd_baseline.json @@ -0,0 +1,16 @@ +{ + "allowedClones": [ + { + "firstFile": "test/unit/test_mimic_connection_processes.gd", + "secondFile": "test/unit/test_mimic_project_settings.gd", + "fragmentSha256": "a7d8d8b02bcecf0e361e9cb65fd9a461e268320e782d0ecec748efbd568b0a2e", + "reason": "Shared ProjectSettings key list in existing unit tests." + }, + { + "firstFile": "test/unit/test_mimic_connection_processes.gd", + "secondFile": "test/unit/test_mimic_runtime.gd", + "fragmentSha256": "240492ff019629bd2be5cd41ecfcd856c55562bb2c1ad53391f4ff4a5605ba29", + "reason": "Shared ProjectSettings save/restore helper in existing unit tests." + } + ] +} diff --git a/tools/quality/package-lock.json b/tools/quality/package-lock.json new file mode 100644 index 0000000..2fa65fd --- /dev/null +++ b/tools/quality/package-lock.json @@ -0,0 +1,1438 @@ +{ + "name": "mimic-quality-tools", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mimic-quality-tools", + "devDependencies": { + "jscpd": "4.2.4" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@jscpd/badge-reporter": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@jscpd/badge-reporter/-/badge-reporter-4.2.4.tgz", + "integrity": "sha512-g5vu05u0lX9rcHA0k3CptLfpOiuMzxh5+mUe2iYRAznTwH3ks6JAVAf9aPi5mBFttMCRiJh2zSt3xnSadHtMGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "badgen": "^3.2.3", + "colors": "^1.4.0", + "fs-extra": "^11.2.0" + } + }, + "node_modules/@jscpd/core": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@jscpd/core/-/core-4.2.4.tgz", + "integrity": "sha512-9V9YzmmhYg9682kFqi+n0KGOhXNSoqxHbuIP3i/l/oSd6upBOnnSeBWDZMGOenQRQnyKEtCIbnS9YFz+3B+siQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1" + } + }, + "node_modules/@jscpd/finder": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@jscpd/finder/-/finder-4.2.4.tgz", + "integrity": "sha512-4LLEuAAmAraud/TAAlB5BByVdWfy7SYiPKacj5yEggpkNs0qsw2kiZ5EyU3LonB+/vntJJEDDpJMmvOeS58e0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jscpd/core": "4.2.4", + "@jscpd/tokenizer": "4.2.4", + "blamer": "^1.0.6", + "bytes": "^3.1.2", + "cli-table3": "^0.6.5", + "colors": "^1.4.0", + "fast-glob": "^3.3.2", + "fs-extra": "^11.2.0", + "markdown-table": "^2.0.0", + "pug": "^3.0.3" + } + }, + "node_modules/@jscpd/html-reporter": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@jscpd/html-reporter/-/html-reporter-4.2.4.tgz", + "integrity": "sha512-6UljCTVGf7O+o6D6fs1zNBG+vR1PTn47W2mSgb5hzSrvNw60rLrVoAMZMnr/TeIEdd/OEgAu+icbdvvVBfnvJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "colors": "1.4.0", + "fs-extra": "^11.2.0", + "pug": "^3.0.3" + } + }, + "node_modules/@jscpd/tokenizer": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@jscpd/tokenizer/-/tokenizer-4.2.4.tgz", + "integrity": "sha512-nM4kGyDvpcevt8t0zOsMQ82ShSc65c3LIQUHClTYwraiOGOmWgUQyen+JIiFCNF8eDCGR2Qa5iI5XBfGWYQzIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jscpd/core": "4.2.4", + "spark-md5": "^3.0.2" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assert-never": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/badgen": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/badgen/-/badgen-3.3.2.tgz", + "integrity": "sha512-fbQwK9norfdzbdsoPwbLIAmgBXDGEme3jeIyqPAH7o6vp9lmuLHS7uXULvOiQ6XnMLkYNG4gDjILf74hgtTAug==", + "dev": true, + "license": "MIT" + }, + "node_modules/blamer": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/blamer/-/blamer-1.0.7.tgz", + "integrity": "sha512-GbBStl/EVlSWkiJQBZps3H1iARBrC7vt++Jb/TTmCNu/jZ04VW7tSN1nScbFXBUy1AN+jzeL7Zep9sbQxLhXKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^4.0.0", + "which": "^2.0.2" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-regex": "^1.0.3" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "object-assign": "^4.1.1" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jscpd": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/jscpd/-/jscpd-4.2.4.tgz", + "integrity": "sha512-PSo2U0G8OxULayGyQMv7T/0ZQ+c3PPltdMOz/57v9Xnmq5xSIhh4cnZ0oYZPKqejy10aFwAbMVxqAlo24+PQ3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jscpd/badge-reporter": "4.2.4", + "@jscpd/core": "4.2.4", + "@jscpd/finder": "4.2.4", + "@jscpd/html-reporter": "4.2.4", + "@jscpd/tokenizer": "4.2.4", + "colors": "^1.4.0", + "commander": "^5.0.0", + "fs-extra": "^11.2.0", + "jscpd-sarif-reporter": "4.2.4" + }, + "bin": { + "jscpd": "bin/jscpd" + } + }, + "node_modules/jscpd-sarif-reporter": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/jscpd-sarif-reporter/-/jscpd-sarif-reporter-4.2.4.tgz", + "integrity": "sha512-JtX79kFSyAhqJh5TdLUcvtYJtJd1F8UW8b4Miaga+EIgUn2/nR0N2zWL9mH5cRXgbzLuQbbsw9kReUVIECApwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "colors": "^1.4.0", + "fs-extra": "^11.2.0", + "node-sarif-builder": "^3.4.0" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "node_modules/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "repeat-string": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/node-sarif-builder": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/pug": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.4.tgz", + "integrity": "sha512-kFfq5mMzrS7+wrl5pLJzZEzemx34OQ0w4SARfhy/3yxTlhbstsudDwJzhf1hP02yHzbjoVMSXUj/Sz6RNfMyXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-code-gen": "^3.0.4", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" + } + }, + "node_modules/pug-attrs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" + } + }, + "node_modules/pug-code-gen": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.4.tgz", + "integrity": "sha512-6okWYIKdasTyXICyEtvobmTZAVX57JkzgzIi4iRJlin8kmhG+Xry2dsus+Mun/nGCn6F2U49haHI5mkELXB14g==", + "dev": true, + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.1.0", + "pug-runtime": "^3.0.1", + "void-elements": "^3.1.0", + "with": "^7.0.0" + } + }, + "node_modules/pug-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pug-filters": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "jstransformer": "1.0.0", + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" + } + }, + "node_modules/pug-lexer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-linker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-load": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "token-stream": "1.0.0" + } + }, + "node_modules/pug-runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pug-strip-comments": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/spark-md5": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/spark-md5/-/spark-md5-3.0.2.tgz", + "integrity": "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/token-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/with": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/tools/quality/package.json b/tools/quality/package.json new file mode 100644 index 0000000..caf8b76 --- /dev/null +++ b/tools/quality/package.json @@ -0,0 +1,7 @@ +{ + "name": "mimic-quality-tools", + "private": true, + "devDependencies": { + "jscpd": "4.2.4" + } +} diff --git a/tools/quality/requirements_quality.txt b/tools/quality/requirements_quality.txt new file mode 100644 index 0000000..8b6af72 --- /dev/null +++ b/tools/quality/requirements_quality.txt @@ -0,0 +1,2 @@ +gdcruiser==1.7.0 \ + --hash=sha256:8b270694c5660f93f0c5daa03ece2a2c182378c388bab7de306544714a421e06 diff --git a/tools/run_two_instances.ps1 b/tools/run_two_instances.ps1 index 55aa68c..7c5076d 100644 --- a/tools/run_two_instances.ps1 +++ b/tools/run_two_instances.ps1 @@ -11,7 +11,7 @@ param( $ErrorActionPreference = "Stop" -$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") +$repoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path if ([string]::IsNullOrWhiteSpace($ResultsDir)) { $ResultsDir = Join-Path $repoRoot "test/.output" @@ -121,18 +121,36 @@ function Stop-IfRunning { } } +function Start-GodotProbe { + param( + [string[]] $Arguments, + [string] $StandardOutputPath, + [string] $StandardErrorPath + ) + + $startParameters = @{ + FilePath = $godotExecutable + ArgumentList = $Arguments + RedirectStandardOutput = $StandardOutputPath + RedirectStandardError = $StandardErrorPath + PassThru = $true + } + if ($env:OS -eq "Windows_NT") { + $startParameters["WindowStyle"] = "Hidden" + } + + return Start-Process @startParameters +} + $server = $null $client = $null $godotExecutable = Resolve-GodotPath try { - $server = Start-Process ` - -FilePath $godotExecutable ` - -ArgumentList (New-GodotArgumentList "server") ` - -RedirectStandardOutput $serverOut ` - -RedirectStandardError $serverErr ` - -WindowStyle Hidden ` - -PassThru + $server = Start-GodotProbe ` + -Arguments (New-GodotArgumentList "server") ` + -StandardOutputPath $serverOut ` + -StandardErrorPath $serverErr $serverReady = Wait-ForLogLine ` -Path $serverOut ` @@ -143,13 +161,10 @@ try { throw "Server probe did not become ready before the client started. See $serverOut and $serverErr." } - $client = Start-Process ` - -FilePath $godotExecutable ` - -ArgumentList (New-GodotArgumentList "client") ` - -RedirectStandardOutput $clientOut ` - -RedirectStandardError $clientErr ` - -WindowStyle Hidden ` - -PassThru + $client = Start-GodotProbe ` + -Arguments (New-GodotArgumentList "client") ` + -StandardOutputPath $clientOut ` + -StandardErrorPath $clientErr Wait-Process -Id $server.Id, $client.Id -Timeout $TimeoutSeconds -ErrorAction Stop $server.Refresh() diff --git a/tools/verify.ps1 b/tools/verify.ps1 index 65539ba..99c49c5 100644 --- a/tools/verify.ps1 +++ b/tools/verify.ps1 @@ -1,14 +1,16 @@ param( [string] $GodotPath = "", - [int] $IntegrationPort = 18910 + [int] $IntegrationPort = 18910, + [switch] $SkipQuality ) $ErrorActionPreference = "Stop" -$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") +$repoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path $godotWrapper = Join-Path $PSScriptRoot "godot.ps1" $resultsDir = Join-Path $repoRoot "test/.output" $junitPath = Join-Path $resultsDir "gut-junit.xml" +$gutConfigPath = Join-Path $repoRoot ".gutconfig.json" New-Item -ItemType Directory -Force -Path $resultsDir | Out-Null @@ -21,12 +23,61 @@ function Invoke-Godot { } $wrapperArgs += $Arguments - & $godotWrapper @wrapperArgs + & (Get-PowerShellExecutable) -NoProfile -ExecutionPolicy Bypass -File $godotWrapper @wrapperArgs if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } +function Invoke-PowerShellScript { + param( + [string] $ScriptPath, + [hashtable] $Parameters = @{} + ) + + $scriptArguments = @( + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + $ScriptPath + ) + foreach ($key in $Parameters.Keys) { + $scriptArguments += "-$key" + $scriptArguments += [string] $Parameters[$key] + } + + & (Get-PowerShellExecutable) @scriptArguments + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } +} + +function Get-PowerShellExecutable { + $binaryName = "powershell.exe" + if ($PSVersionTable.PSEdition -eq "Core") { + $binaryName = "pwsh.exe" + if (-not ($env:OS -eq "Windows_NT")) { + $binaryName = "pwsh" + } + } + + $hostPath = Join-Path $PSHOME $binaryName + if (Test-Path -LiteralPath $hostPath) { + return $hostPath + } + + if ($PSVersionTable.PSEdition -eq "Core") { + return "pwsh" + } + return "powershell" +} + +if (-not $SkipQuality) { + Write-Output "Running static quality and duplicate-code checks..." + Invoke-PowerShellScript (Join-Path $PSScriptRoot "quality.ps1") +} + Write-Output "Importing Godot project resources..." Invoke-Godot @("--headless", "--import", "--path", $repoRoot) @@ -37,8 +88,10 @@ Invoke-Godot @( $repoRoot, "-s", "res://addons/gut/gut_cmdln.gd", - "-gconfig=res://.gutconfig.json", - "-gjunit_xml_file=$junitPath", + "-gconfig", + $gutConfigPath, + "-gjunit_xml_file", + $junitPath, "-gexit" ) @@ -77,10 +130,7 @@ $twoInstanceArgs = @{ if (-not [string]::IsNullOrWhiteSpace($GodotPath)) { $twoInstanceArgs["GodotPath"] = $GodotPath } -& (Join-Path $PSScriptRoot "run_two_instances.ps1") @twoInstanceArgs -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} +Invoke-PowerShellScript (Join-Path $PSScriptRoot "run_two_instances.ps1") $twoInstanceArgs Write-Output "Running two-instance ENet auto-connect smoke test..." $twoInstanceArgs = @{ @@ -92,10 +142,7 @@ $twoInstanceArgs = @{ if (-not [string]::IsNullOrWhiteSpace($GodotPath)) { $twoInstanceArgs["GodotPath"] = $GodotPath } -& (Join-Path $PSScriptRoot "run_two_instances.ps1") @twoInstanceArgs -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} +Invoke-PowerShellScript (Join-Path $PSScriptRoot "run_two_instances.ps1") $twoInstanceArgs Write-Output "Running two-instance WebSocket connection smoke test..." $twoInstanceArgs = @{ @@ -106,9 +153,6 @@ $twoInstanceArgs = @{ if (-not [string]::IsNullOrWhiteSpace($GodotPath)) { $twoInstanceArgs["GodotPath"] = $GodotPath } -& (Join-Path $PSScriptRoot "run_two_instances.ps1") @twoInstanceArgs -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} +Invoke-PowerShellScript (Join-Path $PSScriptRoot "run_two_instances.ps1") $twoInstanceArgs Write-Output "Mimic verification passed."