pi-lens 3.8.72 → 3.8.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1162 -0
- package/README.md +45 -28
- package/dist/clients/atomic-write.js +3 -3
- package/dist/clients/bus-events-logger.js +5 -1
- package/dist/clients/cache/rule-cache.js +43 -2
- package/dist/clients/cache-observability.js +211 -0
- package/dist/clients/call-graph.js +20 -0
- package/dist/clients/cascade-logger.js +5 -1
- package/dist/clients/complexity-client.js +4 -7
- package/dist/clients/dependency-checker.js +151 -20
- package/dist/clients/deps/js-yaml.js +1 -1
- package/dist/clients/diagnostic-dispositions.js +22 -2
- package/dist/clients/dispatch/auxiliary-lsp.js +34 -0
- package/dist/clients/dispatch/dispatcher.js +16 -1
- package/dist/clients/dispatch/fact-store.js +11 -0
- package/dist/clients/dispatch/facts/function-facts.js +14 -0
- package/dist/clients/dispatch/facts/import-facts.js +100 -101
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +5 -8
- package/dist/clients/dispatch/facts/try-catch-facts.js +13 -0
- package/dist/clients/dispatch/integration.js +94 -15
- package/dist/clients/dispatch/rules/async-noise.js +1 -0
- package/dist/clients/dispatch/rules/unsafe-boundary.js +7 -0
- package/dist/clients/dispatch/runners/ast-grep-napi.js +84 -8
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -1
- package/dist/clients/dispatch/runners/tree-sitter.js +153 -166
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +17 -18
- package/dist/clients/feature-hints.js +53 -7
- package/dist/clients/file-kinds.js +86 -31
- package/dist/clients/fix-worklog.js +14 -2
- package/dist/clients/generated-artifacts.js +21 -0
- package/dist/clients/gzip-stage-write.js +84 -0
- package/dist/clients/installer/index.js +358 -129
- package/dist/clients/instance-registry.js +12 -1
- package/dist/clients/knip-client.js +64 -1
- package/dist/clients/language-profile.js +61 -72
- package/dist/clients/latency-logger.js +6 -16
- package/dist/clients/lens-config.js +128 -165
- package/dist/clients/lens-engine.js +39 -5
- package/dist/clients/lens-flag-registry.js +285 -0
- package/dist/clients/lens-map.js +2 -7
- package/dist/clients/log-cleanup.js +6 -17
- package/dist/clients/lsp/cascade-tier.js +22 -2
- package/dist/clients/lsp/client.js +74 -10
- package/dist/clients/lsp/config.js +62 -9
- package/dist/clients/lsp/index.js +113 -31
- package/dist/clients/lsp/language.js +4 -1
- package/dist/clients/lsp/launch.js +8 -6
- package/dist/clients/lsp/server.js +84 -29
- package/dist/clients/lsp-document-symbols.js +112 -0
- package/dist/clients/mcp/analyze.js +5 -4
- package/dist/clients/mcp/review.js +36 -3
- package/dist/clients/module-report.js +100 -80
- package/dist/clients/ndjson-logger.js +51 -21
- package/dist/clients/path-keyed-map.js +76 -0
- package/dist/clients/path-utils.js +35 -0
- package/dist/clients/performance-report.js +327 -0
- package/dist/clients/pipeline.js +7 -0
- package/dist/clients/project-changes.js +77 -4
- package/dist/clients/project-diagnostics/extractors.js +26 -115
- package/dist/clients/project-diagnostics/fresh-fetch.js +149 -19
- package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +109 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +37 -5
- package/dist/clients/project-diagnostics/scanner.js +247 -127
- package/dist/clients/project-lens-config.js +53 -26
- package/dist/clients/project-report.js +39 -14
- package/dist/clients/project-scale.js +8 -10
- package/dist/clients/project-snapshot-persist-worker.js +6 -0
- package/dist/clients/project-snapshot.js +611 -7
- package/dist/clients/quiet-window.js +9 -1
- package/dist/clients/read-expansion.js +22 -12
- package/dist/clients/read-guard.js +78 -0
- package/dist/clients/redact/secrets.js +449 -0
- package/dist/clients/resource-sampler.js +133 -14
- package/dist/clients/reverse-deps.js +48 -0
- package/dist/clients/review-graph/builder.js +1522 -179
- package/dist/clients/review-graph/persist-worker.js +10 -0
- package/dist/clients/review-graph/query.js +5 -0
- package/dist/clients/review-graph/shared-extraction-ir.js +90 -0
- package/dist/clients/review-graph-logger.js +24 -0
- package/dist/clients/runtime-agent-end.js +4 -2
- package/dist/clients/runtime-coordinator.js +2 -2
- package/dist/clients/runtime-session.js +433 -59
- package/dist/clients/runtime-tool-call.js +46 -14
- package/dist/clients/runtime-turn.js +97 -26
- package/dist/clients/safe-spawn.js +102 -15
- package/dist/clients/session-start-observability.js +15 -0
- package/dist/clients/session-state-store.js +2 -1
- package/dist/clients/sessionstart-logger.js +18 -0
- package/dist/clients/sg-runner.js +48 -33
- package/dist/clients/source-filter.js +49 -25
- package/dist/clients/startup-marker.js +6 -0
- package/dist/clients/startup-timing.js +9 -0
- package/dist/clients/todo-scanner.js +30 -3
- package/dist/clients/tree-sitter-cache.js +147 -139
- package/dist/clients/tree-sitter-client.js +1340 -67
- package/dist/clients/tree-sitter-logger.js +43 -1
- package/dist/clients/tree-sitter-query-loader.js +110 -11
- package/dist/clients/tree-sitter-shared.js +43 -19
- package/dist/clients/tree-sitter-symbol-extractor.js +71 -4
- package/dist/clients/widget-state.js +50 -14
- package/dist/clients/word-index-logger.js +44 -0
- package/dist/clients/word-index.js +252 -35
- package/dist/clients/workspace-topology.js +11 -1
- package/dist/index.js +45976 -39387
- package/dist/mcp/analyze-cli.js +3 -0
- package/dist/mcp/cli.js +110 -0
- package/dist/mcp/server.js +63 -8
- package/dist/tools/lens-diagnostics.js +47 -5
- package/dist/tools/lsp-diagnostics.js +126 -9
- package/dist/tools/lsp-navigation.js +5 -28
- package/dist/tools/symbol-search.js +21 -2
- package/docs/agent-guide.md +247 -0
- package/docs/astplayground.md +3 -4
- package/docs/audit1.md +5 -5
- package/docs/environment-variables.md +153 -11
- package/docs/features.md +4 -3
- package/docs/globalconfig.md +56 -4
- package/docs/language-coverage.md +2 -2
- package/docs/mcp.md +17 -16
- package/docs/servercapabilities.md +5 -2
- package/docs/settings.md +181 -0
- package/docs/tools.md +14 -0
- package/docs/tree-sitter_rules_catalog.md +11 -31
- package/docs/usage.md +13 -0
- package/package.json +139 -139
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-js-test.yml +1 -0
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-test.yml +2 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-test.yml +4 -0
- package/rules/ast-grep-rules/rules/hardcoded-url-js.yml +22 -4
- package/rules/ast-grep-rules/rules/hardcoded-url.yml +22 -4
- package/rules/ast-grep-rules/rules/no-case-declarations-js.yml +4 -3
- package/rules/ast-grep-rules/rules/no-case-declarations.yml +4 -3
- package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +10 -0
- package/rules/ast-grep-rules/rules/no-console-except-error.yml +10 -0
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +42 -6
- package/rules/ast-grep-rules/rules/no-javascript-url.yml +42 -6
- package/rules/ast-grep-rules/rules/strict-equality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-equality.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality.yml +14 -1
- package/rules/tree-sitter-queries/cpp/no-memset-sensitive-data.yml +9 -3
- package/rules/tree-sitter-queries/cpp/no-scoped-lock-without-args.yml +6 -6
- package/rules/tree-sitter-queries/css/calc-spacing.yml +8 -6
- package/rules/tree-sitter-queries/go/go-mutex-copy.yml +21 -18
- package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +3 -3
- package/rules/tree-sitter-queries/java/infinite-loop-java.yml +3 -3
- package/rules/tree-sitter-queries/java/junit-call-super.yml +6 -4
- package/rules/tree-sitter-queries/java/no-double-checked-locking.yml +10 -8
- package/rules/tree-sitter-queries/java/no-field-shadowing.yml +1 -1
- package/rules/tree-sitter-queries/java/prepared-statement-valid-indices.yml +2 -3
- package/rules/tree-sitter-queries/java/resources-closed.yml +2 -2
- package/rules/tree-sitter-queries/java/springboot-default-package.yml +6 -3
- package/rules/tree-sitter-queries/java/switch-fall-through.yml +6 -4
- package/rules/tree-sitter-queries/java/switch-non-case-labels.yml +4 -3
- package/rules/tree-sitter-queries/java/tests-include-assertions.yml +6 -5
- package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +5 -8
- package/rules/tree-sitter-queries/javascript/switch-non-case-labels.yml +3 -2
- package/rules/tree-sitter-queries/kotlin/prepared-statement-indices.yml +9 -5
- package/rules/tree-sitter-queries/php/this-in-static-context.yml +5 -5
- package/rules/tree-sitter-queries/python/exit-signature-check.yml +2 -9
- package/rules/tree-sitter-queries/python/in-operator-unsupported.yml +3 -4
- package/rules/tree-sitter-queries/python/no-super-torchscript.yml +4 -8
- package/rules/tree-sitter-queries/python/notimplemented-boolean-context.yml +1 -5
- package/rules/tree-sitter-queries/python/python-empty-except.yml +1 -1
- package/rules/tree-sitter-queries/python/return-in-init.yml +0 -2
- package/rules/tree-sitter-queries/python/yield-return-outside-function.yml +0 -3
- package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +1 -12
- package/rules/tree-sitter-queries/ruby/ruby-eval.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +4 -4
- package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +4 -11
- package/rules/tree-sitter-queries/ruby/ruby-open-struct.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-string-eval.yml +2 -3
- package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +4 -4
- package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +4 -3
- package/rules/tree-sitter-queries/typescript/console-statement.yml +11 -0
- package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +4 -9
- package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +13 -9
- package/rules/tree-sitter-queries/typescript/infinite-loop.yml +2 -2
- package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +10 -1
- package/rules/tree-sitter-queries/typescript/no-equality-in-for-condition.yml +1 -0
- package/rules/tree-sitter-queries/typescript/no-jump-in-finally.yml +1 -0
- package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +5 -8
- package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +7 -10
- package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +4 -2
- package/rules/typos/_typos.toml +17 -0
- package/skills/pi-lens-write-tree-sitter-rule/SKILL.md +5 -0
- package/dist/clients/config-enabled-shape.js +0 -39
- package/docs/env_variables.md +0 -10
- package/rules/tree-sitter-queries/abap-disabled/delete-where.yml +0 -46
- package/rules/tree-sitter-queries/cpp/no-confused-move-forward.yml +0 -59
- package/rules/tree-sitter-queries/cpp/noexcept-functions.yml +0 -58
- package/rules/tree-sitter-queries/java/mockito-initialized.yml +0 -66
- package/rules/tree-sitter-queries/java/no-wait-notify-on-thread.yml +0 -58
- package/rules/tree-sitter-queries/java/short-circuit-logic.yml +0 -57
- package/rules/tree-sitter-queries/java/spring-session-attributes-setcomplete.yml +0 -75
- package/rules/tree-sitter-queries/plsql-disabled/delete-update-where.yml +0 -56
- package/rules/tree-sitter-queries/plsql-disabled/end-loop-semicolon.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/fetch-bulk-collect-limit.yml +0 -47
- package/rules/tree-sitter-queries/plsql-disabled/forallsave-exceptions.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/nchar-nvarchar2-bytes.yml +0 -54
- package/rules/tree-sitter-queries/plsql-disabled/not-null-initialization.yml +0 -59
- package/rules/tree-sitter-queries/plsql-disabled/raise-application-error-codes.yml +0 -50
- package/rules/tree-sitter-queries/typescript-disabled/ts-path-traversal.yml +0 -71
package/README.md
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
pi-lens gives AI coding agents fast, language-aware feedback while they write/edit.
|
|
8
8
|
|
|
9
|
+
> **Working in this project as an AI agent?** Read the [agent guide](docs/agent-guide.md)
|
|
10
|
+
> for how pi-lens surfaces diagnostics (honesty labels, blockers, read-before-edit)
|
|
11
|
+
> and how to respond.
|
|
12
|
+
|
|
9
13
|
## What It Does
|
|
10
14
|
|
|
11
15
|
- LSP diagnostics and navigation across supported languages
|
|
@@ -47,6 +51,7 @@ pi install git:github.com/apmantza/pi-lens
|
|
|
47
51
|
|
|
48
52
|
## Documentation
|
|
49
53
|
|
|
54
|
+
- [Agent guide](docs/agent-guide.md) — how an AI agent should consume and respond to pi-lens
|
|
50
55
|
- [Agent tools](docs/agent-tools.md) — pi tool names, scopes, and arguments
|
|
51
56
|
- [Usage guide](docs/usage.md) — lifecycle, tool behavior, MCP notes, and
|
|
52
57
|
troubleshooting
|
|
@@ -56,9 +61,11 @@ pi install git:github.com/apmantza/pi-lens
|
|
|
56
61
|
- [Tools and commands](docs/tools.md) — runtime flags and slash commands
|
|
57
62
|
- [Diagnostic dispositions](docs/dispositions.md) — triage: false-positive,
|
|
58
63
|
suppress, defer, flagged-to-fix
|
|
64
|
+
- [Settings](docs/settings.md) — the configuration hub: defaults, env vars, CLI
|
|
65
|
+
flags, and global vs project config at a glance
|
|
59
66
|
- [Configuration](docs/globalconfig.md) — global and project config files
|
|
60
|
-
- [Environment variables](docs/
|
|
61
|
-
reference
|
|
67
|
+
- [Environment variables](docs/environment-variables.md) — common env vars and
|
|
68
|
+
full reference
|
|
62
69
|
- [Language coverage](docs/language-coverage.md) — supported languages, runners,
|
|
63
70
|
and formatters
|
|
64
71
|
- [Dependencies](docs/dependencies.md) — auto-install policy and external tools
|
|
@@ -79,52 +86,61 @@ Thanks goes to these wonderful people:
|
|
|
79
86
|
<!-- prettier-ignore-start -->
|
|
80
87
|
<!-- markdownlint-disable -->
|
|
81
88
|
<table>
|
|
82
|
-
<tbody>
|
|
83
|
-
<tr>
|
|
89
|
+
<tbody>
|
|
90
|
+
<tr>
|
|
84
91
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/apmantza"><img src="https://avatars.githubusercontent.com/u/247365598?v=4" width="100px;" alt=""/><br /><sub><b>Apostolos Mantzaris</b></sub></a><br /><a href="#code-apmantza" title="Code">💻</a> <a href="#doc-apmantza" title="Documentation">📖</a> <a href="#ideas-apmantza" title="Ideas & Planning">🤔</a> <a href="#maintenance-apmantza" title="Maintenance">🚧</a> <a href="#review-apmantza" title="Reviewed Pull Requests">👀</a></td>
|
|
92
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AngriestBird"><img src="https://avatars.githubusercontent.com/u/12663269?v=4" width="100px;" alt=""/><br /><sub><b>Kenneth McCormick</b></sub></a><br /><a href="#bug-AngriestBird" title="Bug reports">🐛</a> <a href="#code-AngriestBird" title="Code">💻</a></td>
|
|
85
93
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/anh-chu"><img src="https://avatars.githubusercontent.com/u/34973633?v=4" width="100px;" alt=""/><br /><sub><b>Anh Chu</b></sub></a><br /><a href="#code-anh-chu" title="Code">💻</a></td>
|
|
86
94
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loss-and-quick"><img src="https://avatars.githubusercontent.com/u/39405619?v=4" width="100px;" alt=""/><br /><sub><b>minicx</b></sub></a><br /><a href="#code-loss-and-quick" title="Code">💻</a></td>
|
|
87
95
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/silvanshade"><img src="https://avatars.githubusercontent.com/u/11022302?v=4" width="100px;" alt=""/><br /><sub><b>silvanshade</b></sub></a><br /><a href="#code-silvanshade" title="Code">💻</a></td>
|
|
88
96
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tifandotme"><img src="https://avatars.githubusercontent.com/u/33323177?v=4" width="100px;" alt=""/><br /><sub><b>Tifan Dwi Avianto</b></sub></a><br /><a href="#code-tifandotme" title="Code">💻</a></td>
|
|
89
97
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/amit-gshe"><img src="https://avatars.githubusercontent.com/u/7383028?v=4" width="100px;" alt=""/><br /><sub><b>Amit</b></sub></a><br /><a href="#code-amit-gshe" title="Code">💻</a> <a href="#bug-amit-gshe" title="Bug reports">🐛</a></td>
|
|
90
|
-
|
|
91
|
-
</tr><br />
|
|
98
|
+
</tr>
|
|
92
99
|
<tr>
|
|
100
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fractiunate"><img src="https://avatars.githubusercontent.com/u/78024279?v=4" width="100px;" alt=""/><br /><sub><b>Fractiunate // David Rahäuser</b></sub></a><br /><a href="#code-fractiunate" title="Code">💻</a></td>
|
|
93
101
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vkarasen"><img src="https://avatars.githubusercontent.com/u/7932536?v=4" width="100px;" alt=""/><br /><sub><b>Vitali Karasenko</b></sub></a><br /><a href="#code-vkarasen" title="Code">💻</a></td>
|
|
94
102
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rfxlamia"><img src="https://avatars.githubusercontent.com/u/222023708?v=4" width="100px;" alt=""/><br /><sub><b>V</b></sub></a><br /><a href="#code-rfxlamia" title="Code">💻</a></td>
|
|
95
103
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fabio-dee"><img src="https://avatars.githubusercontent.com/u/10808443?v=4" width="100px;" alt=""/><br /><sub><b>Fabio Dee</b></sub></a><br /><a href="#code-fabio-dee" title="Code">💻</a></td>
|
|
96
104
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/moj02090"><img src="https://avatars.githubusercontent.com/u/57255166?v=4" width="100px;" alt=""/><br /><sub><b>moj02090</b></sub></a><br /><a href="#code-moj02090" title="Code">💻</a></td>
|
|
97
105
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mehalter"><img src="https://avatars.githubusercontent.com/u/1591837?v=4" width="100px;" alt=""/><br /><sub><b>Micah Halter</b></sub></a><br /><a href="#code-mehalter" title="Code">💻</a></td>
|
|
98
106
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JulienEllie"><img src="https://avatars.githubusercontent.com/u/670518?v=4" width="100px;" alt=""/><br /><sub><b>Julien Ellie</b></sub></a><br /><a href="#code-JulienEllie" title="Code">💻</a></td>
|
|
99
|
-
|
|
100
|
-
</tr><br />
|
|
107
|
+
</tr>
|
|
101
108
|
<tr>
|
|
109
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Istar-Eldritch"><img src="https://avatars.githubusercontent.com/u/3746468?v=4" width="100px;" alt=""/><br /><sub><b>Ruben Paz</b></sub></a><br /><a href="#code-Istar-Eldritch" title="Code">💻</a> <a href="#bug-Istar-Eldritch" title="Bug reports">🐛</a></td>
|
|
102
110
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cjunxiang"><img src="https://avatars.githubusercontent.com/u/26619858?v=4" width="100px;" alt=""/><br /><sub><b>C.Junxiang</b></sub></a><br /><a href="#code-cjunxiang" title="Code">💻</a> <a href="#bug-cjunxiang" title="Bug reports">🐛</a></td>
|
|
103
111
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/trvon"><img src="https://avatars.githubusercontent.com/u/6031322?v=4" width="100px;" alt=""/><br /><sub><b>Trevon</b></sub></a><br /><a href="#code-trvon" title="Code">💻</a></td>
|
|
104
112
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cmptr"><img src="https://avatars.githubusercontent.com/u/32746889?v=4" width="100px;" alt=""/><br /><sub><b>Aaron Bell</b></sub></a><br /><a href="#code-cmptr" title="Code">💻</a> <a href="#bug-cmptr" title="Bug reports">🐛</a></td>
|
|
105
113
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/StartupBros"><img src="https://avatars.githubusercontent.com/u/16693591?v=4" width="100px;" alt=""/><br /><sub><b>Will Mitchell</b></sub></a><br /><a href="#code-StartupBros" title="Code">💻</a></td>
|
|
106
114
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/spyrosbazios"><img src="https://avatars.githubusercontent.com/u/37960233?v=4" width="100px;" alt=""/><br /><sub><b>spyrosbazios</b></sub></a><br /><a href="#code-spyrosbazios" title="Code">💻</a></td>
|
|
107
115
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Roman-Galeev"><img src="https://avatars.githubusercontent.com/u/40388226?v=4" width="100px;" alt=""/><br /><sub><b>Roman Galeev</b></sub></a><br /><a href="#code-Roman-Galeev" title="Code">💻</a></td>
|
|
108
|
-
|
|
109
|
-
</tr><br />
|
|
116
|
+
</tr>
|
|
110
117
|
<tr>
|
|
118
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jerryfan"><img src="https://avatars.githubusercontent.com/u/2540814?v=4" width="100px;" alt=""/><br /><sub><b>jerryfan</b></sub></a><br /><a href="#code-jerryfan" title="Code">💻</a></td>
|
|
111
119
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shaDmx"><img src="https://avatars.githubusercontent.com/u/91132641?v=4" width="100px;" alt=""/><br /><sub><b>Max L.</b></sub></a><br /><a href="#code-shaDmx" title="Code">💻</a></td>
|
|
112
120
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/radge"><img src="https://avatars.githubusercontent.com/u/129205?v=4" width="100px;" alt=""/><br /><sub><b>David Ryan</b></sub></a><br /><a href="#code-radge" title="Code">💻</a></td>
|
|
113
121
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JayceFreeman"><img src="https://avatars.githubusercontent.com/u/92962110?v=4" width="100px;" alt=""/><br /><sub><b>JayceFreeman</b></sub></a><br /><a href="#code-JayceFreeman" title="Code">💻</a></td>
|
|
114
122
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bloodf"><img src="https://avatars.githubusercontent.com/u/1626923?v=4" width="100px;" alt=""/><br /><sub><b>Heitor Ramon Ribeiro</b></sub></a><br /><a href="#code-bloodf" title="Code">💻</a></td>
|
|
115
123
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/feoh"><img src="https://avatars.githubusercontent.com/u/330070?v=4" width="100px;" alt=""/><br /><sub><b>Chris Patti</b></sub></a><br /><a href="#code-feoh" title="Code">💻</a> <a href="#bug-feoh" title="Bug reports">🐛</a></td>
|
|
116
124
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chenxin-yan"><img src="https://avatars.githubusercontent.com/u/71162231?v=4" width="100px;" alt=""/><br /><sub><b>Chenxin Yan</b></sub></a><br /><a href="#code-chenxin-yan" title="Code">💻</a> <a href="#doc-chenxin-yan" title="Documentation">📖</a> <a href="#bug-chenxin-yan" title="Bug reports">🐛</a></td>
|
|
117
|
-
|
|
118
|
-
</tr><br />
|
|
125
|
+
</tr>
|
|
119
126
|
<tr>
|
|
120
|
-
<td align="center" valign="top" width="14.28%"><a href="https://github.com/
|
|
127
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/flowing-abyss"><img src="https://avatars.githubusercontent.com/u/98622217?v=4" width="100px;" alt=""/><br /><sub><b>flowing-abyss</b></sub></a><br /><a href="#code-flowing-abyss" title="Code">💻</a></td>
|
|
128
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shaochong-theoai"><img src="https://avatars.githubusercontent.com/u/305592060?v=4" width="100px;" alt=""/><br /><sub><b>shaochong-theoai</b></sub></a><br /><a href="#code-shaochong-theoai" title="Code">💻</a></td>
|
|
129
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xuli500177"><img src="https://avatars.githubusercontent.com/u/62830942?v=4" width="100px;" alt=""/><br /><sub><b>Xu Yili</b></sub></a><br /><a href="#bug-xuli500177" title="Bug reports">🐛</a> <a href="#code-xuli500177" title="Code">💻</a></td>
|
|
121
130
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ricardoraposo"><img src="https://avatars.githubusercontent.com/u/50217712?v=4" width="100px;" alt=""/><br /><sub><b>Ricardo Raposo</b></sub></a><br /><a href="#code-ricardoraposo" title="Code">💻</a></td>
|
|
122
131
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jsmestad"><img src="https://avatars.githubusercontent.com/u/16150?v=4" width="100px;" alt=""/><br /><sub><b>Justin Smestad</b></sub></a><br /><a href="#code-jsmestad" title="Code">💻</a></td>
|
|
123
132
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/anasalsbey-glitch"><img src="https://avatars.githubusercontent.com/u/283609702?v=4" width="100px;" alt=""/><br /><sub><b>anasalsbey-glitch</b></sub></a><br /><a href="#code-anasalsbey-glitch" title="Code">💻</a> <a href="#bug-anasalsbey-glitch" title="Bug reports">🐛</a></td>
|
|
124
133
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PeraSite"><img src="https://avatars.githubusercontent.com/u/19837403?v=4" width="100px;" alt=""/><br /><sub><b>정제훈</b></sub></a><br /><a href="#code-PeraSite" title="Code">💻</a></td>
|
|
134
|
+
</tr>
|
|
135
|
+
<tr>
|
|
136
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ymek"><img src="https://avatars.githubusercontent.com/u/300569?v=4" width="100px;" alt=""/><br /><sub><b>Myke Stubbs</b></sub></a><br /><a href="#code-ymek" title="Code">💻</a> <a href="#bug-ymek" title="Bug reports">🐛</a></td>
|
|
137
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JSap0914"><img src="https://avatars.githubusercontent.com/u/116227558?v=4" width="100px;" alt=""/><br /><sub><b>JSap0914</b></sub></a><br /><a href="#code-JSap0914" title="Code">💻</a></td>
|
|
138
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/knthmn"><img src="https://avatars.githubusercontent.com/u/60438008?v=4" width="100px;" alt=""/><br /><sub><b>Kent Hou Man</b></sub></a><br /><a href="#code-knthmn" title="Code">💻</a> <a href="#bug-knthmn" title="Bug reports">🐛</a></td>
|
|
139
|
+
<td align="center" valign="top" width="14.28%"><a href="https://neumie.dev/"><img src="https://avatars.githubusercontent.com/u/23665482?v=4" width="100px;" alt=""/><br /><sub><b>Jakub Neumann</b></sub></a><br /><a href="#code-neumie" title="Code">💻</a></td>
|
|
140
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cy7372"><img src="https://avatars.githubusercontent.com/u/142197117?v=4" width="100px;" alt=""/><br /><sub><b>CyYu</b></sub></a><br /><a href="#code-cy7372" title="Code">💻</a></td>
|
|
125
141
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carolitascl"><img src="https://avatars.githubusercontent.com/u/26188349?v=4" width="100px;" alt=""/><br /><sub><b>Carolina</b></sub></a><br /><a href="#bug-carolitascl" title="Bug reports">🐛</a></td>
|
|
126
142
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/leakedby"><img src="https://avatars.githubusercontent.com/u/4213260?v=4" width="100px;" alt=""/><br /><sub><b>LeakedBy</b></sub></a><br /><a href="#bug-leakedby" title="Bug reports">🐛</a></td>
|
|
127
|
-
</tr
|
|
143
|
+
</tr>
|
|
128
144
|
<tr>
|
|
129
145
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mdbooth"><img src="https://avatars.githubusercontent.com/u/1318691?v=4" width="100px;" alt=""/><br /><sub><b>Matthew Booth</b></sub></a><br /><a href="#bug-mdbooth" title="Bug reports">🐛</a></td>
|
|
130
146
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Wiedzmin89"><img src="https://avatars.githubusercontent.com/u/61706855?v=4" width="100px;" alt=""/><br /><sub><b>Wiedzmin89</b></sub></a><br /><a href="#ideas-Wiedzmin89" title="Ideas & Planning">🤔</a></td>
|
|
@@ -133,7 +149,7 @@ Thanks goes to these wonderful people:
|
|
|
133
149
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ywh555hhh"><img src="https://avatars.githubusercontent.com/u/121592812?v=4" width="100px;" alt=""/><br /><sub><b>Wayne E</b></sub></a><br /><a href="#bug-ywh555hhh" title="Bug reports">🐛</a></td>
|
|
134
150
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RimuruW"><img src="https://avatars.githubusercontent.com/u/59136309?v=4" width="100px;" alt=""/><br /><sub><b>RimuruW</b></sub></a><br /><a href="#ideas-RimuruW" title="Ideas & Planning">🤔</a></td>
|
|
135
151
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Bjynt"><img src="https://avatars.githubusercontent.com/u/22177300?v=4" width="100px;" alt=""/><br /><sub><b>Bjynt</b></sub></a><br /><a href="#bug-Bjynt" title="Bug reports">🐛</a> <a href="#ideas-Bjynt" title="Ideas & Planning">🤔</a></td>
|
|
136
|
-
</tr
|
|
152
|
+
</tr>
|
|
137
153
|
<tr>
|
|
138
154
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pvtri96"><img src="https://avatars.githubusercontent.com/u/28696888?v=4" width="100px;" alt=""/><br /><sub><b>Tri Van Pham</b></sub></a><br /><a href="#bug-pvtri96" title="Bug reports">🐛</a></td>
|
|
139
155
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BingWuJ"><img src="https://avatars.githubusercontent.com/u/117666511?v=4" width="100px;" alt=""/><br /><sub><b>BingWuJ</b></sub></a><br /><a href="#bug-BingWuJ" title="Bug reports">🐛</a></td>
|
|
@@ -142,7 +158,7 @@ Thanks goes to these wonderful people:
|
|
|
142
158
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ProbabilityEngineer"><img src="https://avatars.githubusercontent.com/u/38498804?v=4" width="100px;" alt=""/><br /><sub><b>ProbabilityEngineer</b></sub></a><br /><a href="#ideas-ProbabilityEngineer" title="Ideas & Planning">🤔</a></td>
|
|
143
159
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LeonardoRick"><img src="https://avatars.githubusercontent.com/u/17517057?v=4" width="100px;" alt=""/><br /><sub><b>Leonardo Rick</b></sub></a><br /><a href="#bug-LeonardoRick" title="Bug reports">🐛</a> <a href="#ideas-LeonardoRick" title="Ideas & Planning">🤔</a></td>
|
|
144
160
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kenbanks-peng"><img src="https://avatars.githubusercontent.com/u/26904200?v=4" width="100px;" alt=""/><br /><sub><b>Ken Banks</b></sub></a><br /><a href="#bug-kenbanks-peng" title="Bug reports">🐛</a></td>
|
|
145
|
-
</tr
|
|
161
|
+
</tr>
|
|
146
162
|
<tr>
|
|
147
163
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rstacruz"><img src="https://avatars.githubusercontent.com/u/74385?v=4" width="100px;" alt=""/><br /><sub><b>Rico Sta. Cruz</b></sub></a><br /><a href="#ideas-rstacruz" title="Ideas & Planning">🤔</a></td>
|
|
148
164
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/acidnik"><img src="https://avatars.githubusercontent.com/u/1227955?v=4" width="100px;" alt=""/><br /><sub><b>Nikita Bilous</b></sub></a><br /><a href="#bug-acidnik" title="Bug reports">🐛</a></td>
|
|
@@ -151,7 +167,7 @@ Thanks goes to these wonderful people:
|
|
|
151
167
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nodnarbnitram"><img src="https://avatars.githubusercontent.com/u/44812862?v=4" width="100px;" alt=""/><br /><sub><b>Brandon Martin</b></sub></a><br /><a href="#bug-nodnarbnitram" title="Bug reports">🐛</a></td>
|
|
152
168
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/alpertarhan"><img src="https://avatars.githubusercontent.com/u/50966980?v=4" width="100px;" alt=""/><br /><sub><b>Alper Tarhan</b></sub></a><br /><a href="#bug-alpertarhan" title="Bug reports">🐛</a></td>
|
|
153
169
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asolopovas"><img src="https://avatars.githubusercontent.com/u/6893216?v=4" width="100px;" alt=""/><br /><sub><b>Andrius Solopovas</b></sub></a><br /><a href="#bug-asolopovas" title="Bug reports">🐛</a></td>
|
|
154
|
-
</tr
|
|
170
|
+
</tr>
|
|
155
171
|
<tr>
|
|
156
172
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GrahamJenkins"><img src="https://avatars.githubusercontent.com/u/6607975?v=4" width="100px;" alt=""/><br /><sub><b>Graham Jenkins</b></sub></a><br /><a href="#bug-GrahamJenkins" title="Bug reports">🐛</a></td>
|
|
157
173
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wings1848"><img src="https://avatars.githubusercontent.com/u/120104016?v=4" width="100px;" alt=""/><br /><sub><b>Wings Butterfly</b></sub></a><br /><a href="#bug-wings1848" title="Bug reports">🐛</a></td>
|
|
@@ -160,7 +176,7 @@ Thanks goes to these wonderful people:
|
|
|
160
176
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fmatray"><img src="https://avatars.githubusercontent.com/u/8267716?v=4" width="100px;" alt=""/><br /><sub><b>Frédéric</b></sub></a><br /><a href="#bug-fmatray" title="Bug reports">🐛</a></td>
|
|
161
177
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fuentesjr"><img src="https://avatars.githubusercontent.com/u/9240?v=4" width="100px;" alt=""/><br /><sub><b>Salvador Fuentes Jr</b></sub></a><br /><a href="#bug-fuentesjr" title="Bug reports">🐛</a></td>
|
|
162
178
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Viterkim"><img src="https://avatars.githubusercontent.com/u/17838985?v=4" width="100px;" alt=""/><br /><sub><b>Viktor</b></sub></a><br /><a href="#bug-Viterkim" title="Bug reports">🐛</a></td>
|
|
163
|
-
</tr
|
|
179
|
+
</tr>
|
|
164
180
|
<tr>
|
|
165
181
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ortonomy"><img src="https://avatars.githubusercontent.com/u/6688676?v=4" width="100px;" alt=""/><br /><sub><b>Gregory Orton</b></sub></a><br /><a href="#bug-ortonomy" title="Bug reports">🐛</a></td>
|
|
166
182
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jimallen"><img src="https://avatars.githubusercontent.com/u/868773?v=4" width="100px;" alt=""/><br /><sub><b>Jim Allen</b></sub></a><br /><a href="#bug-jimallen" title="Bug reports">🐛</a></td>
|
|
@@ -169,7 +185,7 @@ Thanks goes to these wonderful people:
|
|
|
169
185
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chozandrias76"><img src="https://avatars.githubusercontent.com/u/2087677?v=4" width="100px;" alt=""/><br /><sub><b>Colin Swenson-Healey</b></sub></a><br /><a href="#bug-chozandrias76" title="Bug reports">🐛</a></td>
|
|
170
186
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JustlyAI"><img src="https://avatars.githubusercontent.com/u/12634140?v=4" width="100px;" alt=""/><br /><sub><b>Laurent Wiesel</b></sub></a><br /><a href="#bug-JustlyAI" title="Bug reports">🐛</a></td>
|
|
171
187
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/stark-bit"><img src="https://avatars.githubusercontent.com/u/44064758?v=4" width="100px;" alt=""/><br /><sub><b>Rei Starks</b></sub></a><br /><a href="#bug-stark-bit" title="Bug reports">🐛</a></td>
|
|
172
|
-
</tr
|
|
188
|
+
</tr>
|
|
173
189
|
<tr>
|
|
174
190
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/robertoecf"><img src="https://avatars.githubusercontent.com/u/54923863?v=4" width="100px;" alt=""/><br /><sub><b>Roberto Freitas</b></sub></a><br /><a href="#bug-robertoecf" title="Bug reports">🐛</a></td>
|
|
175
191
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tmttodd"><img src="https://avatars.githubusercontent.com/u/160077416?v=4" width="100px;" alt=""/><br /><sub><b>tmttodd</b></sub></a><br /><a href="#bug-tmttodd" title="Bug reports">🐛</a></td>
|
|
@@ -178,30 +194,31 @@ Thanks goes to these wonderful people:
|
|
|
178
194
|
<td align="center" valign="top" width="14.28%"><a href="http://jasonrimmer.com/"><img src="https://avatars.githubusercontent.com/u/629?v=4" width="100px;" alt=""/><br /><sub><b>Jason Rimmer</b></sub></a><br /><a href="#bug-jrimmer" title="Bug reports">🐛</a></td>
|
|
179
195
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ArthurHeymans"><img src="https://avatars.githubusercontent.com/u/15137817?v=4" width="100px;" alt=""/><br /><sub><b>Arthur Heymans</b></sub></a><br /><a href="#ideas-ArthurHeymans" title="Ideas & Planning">🤔</a></td>
|
|
180
196
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Blue-B"><img src="https://avatars.githubusercontent.com/u/55532956?v=4" width="100px;" alt=""/><br /><sub><b>Blue-B</b></sub></a><br /><a href="#bug-Blue-B" title="Bug reports">🐛</a></td>
|
|
181
|
-
</tr
|
|
197
|
+
</tr>
|
|
182
198
|
<tr>
|
|
183
199
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bisko"><img src="https://avatars.githubusercontent.com/u/184938?v=4" width="100px;" alt=""/><br /><sub><b>Biser Perchinkov</b></sub></a><br /><a href="#bug-bisko" title="Bug reports">🐛</a></td>
|
|
184
200
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xificurC"><img src="https://avatars.githubusercontent.com/u/4800719?v=4" width="100px;" alt=""/><br /><sub><b>Peter Nagy</b></sub></a><br /><a href="#bug-xificurC" title="Bug reports">🐛</a></td>
|
|
185
201
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orest-tokovenko-block"><img src="https://avatars.githubusercontent.com/u/190554467?v=4" width="100px;" alt=""/><br /><sub><b>Orest Tokovenko</b></sub></a><br /><a href="#bug-orest-tokovenko-block" title="Bug reports">🐛</a></td>
|
|
186
202
|
<td align="center" valign="top" width="14.28%"><a href="https://eisterman.dev/"><img src="https://avatars.githubusercontent.com/u/3028953?v=4" width="100px;" alt=""/><br /><sub><b>Federico Pasqua</b></sub></a><br /><a href="#bug-eisterman" title="Bug reports">🐛</a></td>
|
|
187
|
-
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ymek"><img src="https://avatars.githubusercontent.com/u/300569?v=4" width="100px;" alt=""/><br /><sub><b>Myke Stubbs</b></sub></a><br /><a href="#code-ymek" title="Code">💻</a> <a href="#bug-ymek" title="Bug reports">🐛</a></td>
|
|
188
203
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/black-snow"><img src="https://avatars.githubusercontent.com/u/579733?v=4" width="100px;" alt=""/><br /><sub><b>Ronald</b></sub></a><br /><a href="#bug-black-snow" title="Bug reports">🐛</a></td>
|
|
189
|
-
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JSap0914"><img src="https://avatars.githubusercontent.com/u/116227558?v=4" width="100px;" alt=""/><br /><sub><b>JSap0914</b></sub></a><br /><a href="#code-JSap0914" title="Code">💻</a></td>
|
|
190
|
-
</tr><br />
|
|
191
|
-
<tr>
|
|
192
|
-
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AngriestBird"><img src="https://avatars.githubusercontent.com/u/12663269?v=4" width="100px;" alt=""/><br /><sub><b>Kenneth McCormick</b></sub></a><br /><a href="#bug-AngriestBird" title="Bug reports">🐛</a></td>
|
|
193
|
-
<td align="center" valign="top" width="14.28%"><a href="https://github.com/knthmn"><img src="https://avatars.githubusercontent.com/u/60438008?v=4" width="100px;" alt=""/><br /><sub><b>Kent Hou Man</b></sub></a><br /><a href="#code-knthmn" title="Code">💻</a> <a href="#bug-knthmn" title="Bug reports">🐛</a></td>
|
|
194
204
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aaronkyriesenbach"><img src="https://avatars.githubusercontent.com/u/12665860?v=4" width="100px;" alt=""/><br /><sub><b>Aaron Ky-Riesenbach</b></sub></a><br /><a href="#bug-aaronkyriesenbach" title="Bug reports">🐛</a></td>
|
|
195
205
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LovelyLoong"><img src="https://avatars.githubusercontent.com/u/54889641?v=4" width="100px;" alt=""/><br /><sub><b>可爱的龙</b></sub></a><br /><a href="#bug-LovelyLoong" title="Bug reports">🐛</a></td>
|
|
206
|
+
</tr>
|
|
207
|
+
<tr>
|
|
196
208
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pppobear"><img src="https://avatars.githubusercontent.com/u/21952175?v=4" width="100px;" alt=""/><br /><sub><b>pppobear</b></sub></a><br /><a href="#bug-pppobear" title="Bug reports">🐛</a></td>
|
|
197
209
|
</tr>
|
|
198
|
-
</tbody>
|
|
210
|
+
</tbody>
|
|
199
211
|
</table>
|
|
200
212
|
|
|
201
|
-
|
|
202
213
|
<!-- markdownlint-restore -->
|
|
203
214
|
<!-- prettier-ignore-end -->
|
|
204
215
|
|
|
205
216
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
|
206
217
|
|
|
218
|
+
The following commit identities also appear in `git shortlog -sne HEAD` but are
|
|
219
|
+
not represented in the generated table above: Anas Alsbei, Claude,
|
|
220
|
+
Christopher Patti,
|
|
221
|
+
dependabot[bot], Fabio-D, github-actions[bot], JSup, Kenny McCormick,
|
|
222
|
+
Max Lupus, Moritz Hofmann, ricardo, and root.
|
|
223
|
+
|
|
207
224
|
If you land a pull request or report an issue that gets fixed, we'll add you here.
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import * as fs from "node:fs";
|
|
32
32
|
/**
|
|
33
|
-
* Synchronous atomic write
|
|
33
|
+
* Synchronous atomic write of text or binary data to `${targetPath}.tmp-${pid}`,
|
|
34
34
|
* then `fs.renameSync` over `targetPath`. On any failure (from either step),
|
|
35
35
|
* attempts a best-effort `fs.rmSync(tmp, { force: true })` cleanup, then
|
|
36
36
|
* either swallows (default) or rethrows per `options.bestEffort`.
|
|
@@ -43,7 +43,7 @@ export function writeFileAtomic(targetPath, data, options) {
|
|
|
43
43
|
const bestEffort = options?.bestEffort ?? true;
|
|
44
44
|
const tmpPath = `${targetPath}.tmp-${process.pid}`;
|
|
45
45
|
try {
|
|
46
|
-
fs.writeFileSync(tmpPath, data, "utf-8");
|
|
46
|
+
fs.writeFileSync(tmpPath, data, typeof data === "string" ? "utf-8" : undefined);
|
|
47
47
|
fs.renameSync(tmpPath, targetPath);
|
|
48
48
|
}
|
|
49
49
|
catch (err) {
|
|
@@ -67,7 +67,7 @@ export async function writeFileAtomicAsync(targetPath, data, options) {
|
|
|
67
67
|
const bestEffort = options?.bestEffort ?? true;
|
|
68
68
|
const tmpPath = `${targetPath}.tmp-${process.pid}`;
|
|
69
69
|
try {
|
|
70
|
-
await fs.promises.writeFile(tmpPath, data, "utf-8");
|
|
70
|
+
await fs.promises.writeFile(tmpPath, data, typeof data === "string" ? "utf-8" : undefined);
|
|
71
71
|
await fs.promises.rename(tmpPath, targetPath);
|
|
72
72
|
}
|
|
73
73
|
catch (err) {
|
|
@@ -36,8 +36,12 @@ import * as path from "node:path";
|
|
|
36
36
|
import { isTestMode } from "./env-utils.js";
|
|
37
37
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
38
38
|
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
39
|
+
import { getMaxLogSizeMB } from "./log-cleanup.js";
|
|
39
40
|
const BUS_EVENTS_LOG_FILE = path.join(getGlobalPiLensDir(), "bus-events.log");
|
|
40
|
-
const writer = createNdjsonLogger({
|
|
41
|
+
const writer = createNdjsonLogger({
|
|
42
|
+
filePath: BUS_EVENTS_LOG_FILE,
|
|
43
|
+
maxBytes: getMaxLogSizeMB() * 1024 * 1024,
|
|
44
|
+
});
|
|
41
45
|
export function logBusEvent(entry) {
|
|
42
46
|
if (isTestMode()) {
|
|
43
47
|
return;
|
|
@@ -9,11 +9,24 @@ import * as fs from "node:fs";
|
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { getProjectDataDir } from "../file-utils.js";
|
|
11
11
|
import { readJsonCache } from "../json-cache-read.js";
|
|
12
|
-
|
|
12
|
+
// v4: cache skip_test_files + fix_action — v3 entries silently dropped them,
|
|
13
|
+
// and ruleHash (rule-file mtimes) never invalidates on a code-only fix.
|
|
14
|
+
// v5 (#675): rule SELECTION changed in code — javascript no longer inherits the
|
|
15
|
+
// typescript rule set and tsx now does. A v4 entry holds the old merge, and
|
|
16
|
+
// queries now compile against the file's parse language, so replaying it would
|
|
17
|
+
// fire typescript rules on javascript trees for real this time.
|
|
18
|
+
// v6 (#878): the ruleHash FINGERPRINT now covers the full effective rule set —
|
|
19
|
+
// inherited rule-source directories (tsx also runs typescript rules), not just
|
|
20
|
+
// the language's own. A v5 tsx entry was hashed over tsx files only, so a
|
|
21
|
+
// typescript-rule edit never invalidated it; v5 entries self-miss anyway (the
|
|
22
|
+
// hash input set changed), the bump just makes the semantics break explicit.
|
|
23
|
+
export const CACHE_VERSION = "v6";
|
|
13
24
|
export class RuleCache {
|
|
14
25
|
cacheFile;
|
|
15
26
|
cacheDir;
|
|
27
|
+
language;
|
|
16
28
|
constructor(language, rootDir = process.cwd()) {
|
|
29
|
+
this.language = language;
|
|
17
30
|
this.cacheDir = path.join(getProjectDataDir(rootDir), "cache");
|
|
18
31
|
this.cacheFile = path.join(this.cacheDir, `${language}-rules-${CACHE_VERSION}.json`);
|
|
19
32
|
}
|
|
@@ -40,7 +53,8 @@ export class RuleCache {
|
|
|
40
53
|
const currentHash = this.computeRuleHash(ruleFiles);
|
|
41
54
|
const cached = readJsonCache(this.cacheFile, (parsed) => {
|
|
42
55
|
const entry = parsed;
|
|
43
|
-
if (entry.version !== CACHE_VERSION ||
|
|
56
|
+
if (entry.version !== CACHE_VERSION ||
|
|
57
|
+
entry.ruleHash !== currentHash) {
|
|
44
58
|
return undefined; // Cache invalid
|
|
45
59
|
}
|
|
46
60
|
return entry;
|
|
@@ -61,11 +75,38 @@ export class RuleCache {
|
|
|
61
75
|
queries,
|
|
62
76
|
};
|
|
63
77
|
fs.writeFileSync(this.cacheFile, JSON.stringify(entry, null, 2));
|
|
78
|
+
this.pruneStaleVersions();
|
|
64
79
|
}
|
|
65
80
|
catch {
|
|
66
81
|
// Cache write failure is non-fatal
|
|
67
82
|
}
|
|
68
83
|
}
|
|
84
|
+
// Orphaned `<language>-rules-v<N>.json` files from a prior CACHE_VERSION
|
|
85
|
+
// (e.g. v3 entries left behind by the #448 v3→v4 bump) never got cleaned up
|
|
86
|
+
// on their own — nothing ever read or removed them again once the version
|
|
87
|
+
// bumped. Delete every sibling for this language that isn't the current file.
|
|
88
|
+
pruneStaleVersions() {
|
|
89
|
+
const currentName = path.basename(this.cacheFile);
|
|
90
|
+
const pattern = new RegExp(`^${this.language}-rules-v\\d+\\.json$`);
|
|
91
|
+
let dirents;
|
|
92
|
+
try {
|
|
93
|
+
dirents = fs.readdirSync(this.cacheDir);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
for (const name of dirents) {
|
|
99
|
+
if (name === currentName || !pattern.test(name))
|
|
100
|
+
continue;
|
|
101
|
+
try {
|
|
102
|
+
fs.unlinkSync(path.join(this.cacheDir, name));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Best-effort; ENOENT (already gone) or any other removal failure
|
|
106
|
+
// shouldn't undo the write that already succeeded above.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
69
110
|
clear() {
|
|
70
111
|
try {
|
|
71
112
|
if (fs.existsSync(this.cacheFile)) {
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-cache observability (#1018).
|
|
3
|
+
*
|
|
4
|
+
* Two provider-independent signals, both sinking into ~/.pi-lens/latency.log
|
|
5
|
+
* via {@link logLatency} (matching the neighboring `type: "phase"` records):
|
|
6
|
+
*
|
|
7
|
+
* 1. Response-side cache usage (`cache_usage`) — on each assistant
|
|
8
|
+
* `message_end`, record the provider-reported token/cost breakdown so a
|
|
9
|
+
* session's actual cacheRead/cacheWrite behavior is queryable after the
|
|
10
|
+
* fact. This is what the provider DID, not what we hoped it would do.
|
|
11
|
+
*
|
|
12
|
+
* 2. Request-side prefix stability (`cache_prefix_break`) — a content hash of
|
|
13
|
+
* `messages[0]` observed on every `context` call. After #1016 the first
|
|
14
|
+
* message must stay byte-stable across a whole session; a logged CHANGE
|
|
15
|
+
* flags that something (pi-lens or otherwise) broke the cache prefix, which
|
|
16
|
+
* would silently invalidate the entire prompt-cache on every prefix-caching
|
|
17
|
+
* provider. Pure observation — it never alters context injection.
|
|
18
|
+
*
|
|
19
|
+
* Both paths are defensive: `usage` (and its fields) may be absent on older
|
|
20
|
+
* hosts or non-assistant messages, so every access is guarded and the handlers
|
|
21
|
+
* never throw — on error they `dbg(...)` and no-op, like the other index.ts
|
|
22
|
+
* event handlers.
|
|
23
|
+
*/
|
|
24
|
+
import { createHash } from "node:crypto";
|
|
25
|
+
import { logLatency } from "./latency-logger.js";
|
|
26
|
+
/**
|
|
27
|
+
* Part 1 — log one `cache_usage` record for an assistant `message_end` that
|
|
28
|
+
* carries a `usage`. Provider/model come straight off the message itself
|
|
29
|
+
* (`AssistantMessage.provider` / `.model` in pi-ai) — no dependency on the
|
|
30
|
+
* runtime telemetry identity. Skips silently (no record) when the message is
|
|
31
|
+
* not an assistant message or has no usage; never logs a zeros-only record for
|
|
32
|
+
* a message that simply lacks usage.
|
|
33
|
+
*/
|
|
34
|
+
export function logCacheUsage(message, dbg) {
|
|
35
|
+
try {
|
|
36
|
+
if (!message || typeof message !== "object")
|
|
37
|
+
return;
|
|
38
|
+
const msg = message;
|
|
39
|
+
// Only assistant messages carry LLM usage; tool-result / user messages
|
|
40
|
+
// (and unknown custom AgentMessage variants) are skipped.
|
|
41
|
+
if (msg.role !== "assistant")
|
|
42
|
+
return;
|
|
43
|
+
const usage = msg.usage;
|
|
44
|
+
if (!usage || typeof usage !== "object")
|
|
45
|
+
return;
|
|
46
|
+
const u = usage;
|
|
47
|
+
logLatency({
|
|
48
|
+
type: "phase",
|
|
49
|
+
filePath: "<pi-lens>",
|
|
50
|
+
phase: "cache_usage",
|
|
51
|
+
durationMs: 0,
|
|
52
|
+
metadata: {
|
|
53
|
+
provider: typeof msg.provider === "string" ? msg.provider : undefined,
|
|
54
|
+
model: typeof msg.model === "string" ? msg.model : undefined,
|
|
55
|
+
cacheRead: u.cacheRead,
|
|
56
|
+
cacheWrite: u.cacheWrite,
|
|
57
|
+
input: u.input,
|
|
58
|
+
output: u.output,
|
|
59
|
+
// `Usage.cost` is a breakdown object; the total is the headline number.
|
|
60
|
+
cost: u.cost?.total,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
dbg?.(`cache-usage: failed to log message_end usage: ${err}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Content hash of the first transcript message. Stable for identical content
|
|
70
|
+
* (role + content are serialized in a fixed order), so a changing hash means
|
|
71
|
+
* `messages[0]` actually changed byte-for-byte.
|
|
72
|
+
*/
|
|
73
|
+
function hashFirstMessage(first) {
|
|
74
|
+
const serialized = JSON.stringify({ role: first.role, content: first.content });
|
|
75
|
+
return createHash("sha256").update(serialized).digest("hex");
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Per-session baseline hash of `messages[0]`, keyed by the STABLE pi session id
|
|
79
|
+
* (`ctx.sessionManager.getSessionId()`). A single module-scoped var was wrong:
|
|
80
|
+
* in ONE process, multiple logical conversations touch this module, and they
|
|
81
|
+
* must not share a baseline —
|
|
82
|
+
* - new / fork / a concurrent in-process subagent (#473) is a DIFFERENT id →
|
|
83
|
+
* its OWN independent baseline, never compared against another id's (else a
|
|
84
|
+
* benign session boundary logs a spurious `cache_prefix_break`, and a
|
|
85
|
+
* concurrent subagent + its parent stomp each other's baseline, each
|
|
86
|
+
* emitting a false positive — fatal for a signal whose value is trust);
|
|
87
|
+
* - an IN-PROCESS reload / resume reuses the SAME id → the baseline is still in
|
|
88
|
+
* this map, so a genuine break is caught (no blinding reset). NOTE: a full
|
|
89
|
+
* process restart (quit → `pi --session <id>`) starts a NEW process with an
|
|
90
|
+
* empty map, so the first post-restart `context` re-anchors a fresh baseline
|
|
91
|
+
* rather than comparing across the restart. That's acceptable here — this is a
|
|
92
|
+
* pure observability signal (at worst a missed `cache_prefix_break` log on the
|
|
93
|
+
* first post-restart turn, never a user-facing action) — unlike genuine
|
|
94
|
+
* session state (read guard #1041, widget #190) which IS rehydrated from the
|
|
95
|
+
* sidecar.
|
|
96
|
+
*
|
|
97
|
+
* Bounded as an insertion-ordered LRU (evict oldest-inserted past the cap) so a
|
|
98
|
+
* long-lived process cycling through many sessions can't grow this unbounded.
|
|
99
|
+
*/
|
|
100
|
+
const MAX_TRACKED_SESSIONS = 32;
|
|
101
|
+
const prefixHashBySession = new Map();
|
|
102
|
+
/**
|
|
103
|
+
* Bucket key used when no session id is available (undefined/empty). Degrades
|
|
104
|
+
* gracefully to the old single-var semantics — one shared baseline — rather than
|
|
105
|
+
* throwing or dropping the signal.
|
|
106
|
+
*/
|
|
107
|
+
const NO_SESSION_KEY = "<no-session>";
|
|
108
|
+
/**
|
|
109
|
+
* Store `hash` for `key`, refreshing its LRU recency (re-insert moves it to the
|
|
110
|
+
* newest position since `Map` preserves insertion order) and evicting the
|
|
111
|
+
* oldest-inserted entries once the cap is exceeded.
|
|
112
|
+
*/
|
|
113
|
+
function recordSessionHash(key, hash) {
|
|
114
|
+
prefixHashBySession.delete(key);
|
|
115
|
+
prefixHashBySession.set(key, hash);
|
|
116
|
+
while (prefixHashBySession.size > MAX_TRACKED_SESSIONS) {
|
|
117
|
+
const oldest = prefixHashBySession.keys().next().value;
|
|
118
|
+
if (oldest === undefined)
|
|
119
|
+
break;
|
|
120
|
+
prefixHashBySession.delete(oldest);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Part 2 — observe `messages[0]` stability turn-over-turn, keyed by session id.
|
|
125
|
+
* Logs a baseline the first time it sees a non-empty transcript FOR A GIVEN
|
|
126
|
+
* session id, then logs a `cache_prefix_break` whenever that same session's
|
|
127
|
+
* first-message hash changes. Pure observation: it never inspects or mutates
|
|
128
|
+
* anything but its own per-session hash map, and never throws.
|
|
129
|
+
*
|
|
130
|
+
* `sessionId` should be pi's STABLE id (`ctx.sessionManager.getSessionId()`),
|
|
131
|
+
* which uniquely identifies the CURRENTLY-firing session: a concurrent
|
|
132
|
+
* in-process subagent runs its own `AgentSession`/`sessionManager`, so its
|
|
133
|
+
* `context` calls carry a DIFFERENT id than the parent's and get their own
|
|
134
|
+
* baseline. (`runtime.telemetrySessionId` cannot be used here: per the #473
|
|
135
|
+
* guard a concurrent secondary skips `updateRuntimeIdentityFromEvent`, so that
|
|
136
|
+
* process-global singleton stays pinned to the PARENT — it would collapse
|
|
137
|
+
* parent and subagent onto one baseline.) Residual limitation: if the host ever
|
|
138
|
+
* does NOT supply a stable id, all such sessions collapse onto `NO_SESSION_KEY`
|
|
139
|
+
* and behave like the old single-var (a concurrent subagent could then still
|
|
140
|
+
* cross-contaminate) — accepted as graceful degradation, not silently perfect.
|
|
141
|
+
*
|
|
142
|
+
* Does nothing on an empty transcript (nothing to anchor a prefix to yet).
|
|
143
|
+
*/
|
|
144
|
+
export function observeCachePrefix(messages, turnIndex, sessionId, sessionRole, dbg) {
|
|
145
|
+
try {
|
|
146
|
+
if (!messages || messages.length === 0)
|
|
147
|
+
return;
|
|
148
|
+
const first = messages[0];
|
|
149
|
+
if (!first || typeof first !== "object")
|
|
150
|
+
return;
|
|
151
|
+
const key = sessionId?.trim() ? sessionId.trim() : NO_SESSION_KEY;
|
|
152
|
+
const currentHash = hashFirstMessage(first);
|
|
153
|
+
const previousHash = prefixHashBySession.get(key);
|
|
154
|
+
if (previousHash === undefined) {
|
|
155
|
+
// Baseline: record this session's starting prefix so a later break has a
|
|
156
|
+
// reference point in the log. `previousHash: null` marks the baseline.
|
|
157
|
+
recordSessionHash(key, currentHash);
|
|
158
|
+
logLatency({
|
|
159
|
+
type: "phase",
|
|
160
|
+
filePath: "<pi-lens>",
|
|
161
|
+
phase: "cache_prefix_break",
|
|
162
|
+
durationMs: 0,
|
|
163
|
+
metadata: {
|
|
164
|
+
turnIndex,
|
|
165
|
+
previousHash: null,
|
|
166
|
+
currentHash,
|
|
167
|
+
baseline: true,
|
|
168
|
+
sessionId: key,
|
|
169
|
+
sessionRole,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (currentHash !== previousHash) {
|
|
175
|
+
logLatency({
|
|
176
|
+
type: "phase",
|
|
177
|
+
filePath: "<pi-lens>",
|
|
178
|
+
phase: "cache_prefix_break",
|
|
179
|
+
durationMs: 0,
|
|
180
|
+
metadata: {
|
|
181
|
+
turnIndex,
|
|
182
|
+
previousHash,
|
|
183
|
+
currentHash,
|
|
184
|
+
sessionId: key,
|
|
185
|
+
sessionRole,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// Refresh recency (and update the stored hash after a break) so an active
|
|
190
|
+
// session stays warm in the LRU and isn't evicted while still in use.
|
|
191
|
+
recordSessionHash(key, currentHash);
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
dbg?.(`cache-prefix: failed to observe messages[0]: ${err}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Drop a single session's prefix baseline. Called from the `session_shutdown`
|
|
199
|
+
* handler (primary path only — the concurrent-secondary guard there returns
|
|
200
|
+
* first, and the LRU cap backstops any secondary entry left behind) so an ended
|
|
201
|
+
* conversation's entry is reclaimed promptly rather than only when the LRU
|
|
202
|
+
* evicts it. Idempotent and never throws.
|
|
203
|
+
*/
|
|
204
|
+
export function clearCachePrefixSession(sessionId) {
|
|
205
|
+
const key = sessionId?.trim() ? sessionId.trim() : NO_SESSION_KEY;
|
|
206
|
+
prefixHashBySession.delete(key);
|
|
207
|
+
}
|
|
208
|
+
/** Clear all per-session prefix hashes. For tests / session boundaries. */
|
|
209
|
+
export function resetCachePrefixObservation() {
|
|
210
|
+
prefixHashBySession.clear();
|
|
211
|
+
}
|
|
@@ -65,6 +65,26 @@ export function impact(graph, startKey, maxDepth = 3, minWeight = 0.1) {
|
|
|
65
65
|
// Sort by depth then symbolKey for stable output
|
|
66
66
|
return results.sort((a, b) => a.depth - b.depth || a.symbolKey.localeCompare(b.symbolKey));
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Splits a `SymbolKey` into its file and symbol-name parts. Two shapes exist
|
|
70
|
+
* (see `buildCallGraph` above): the normal `"filePath:symbolName"` form, and
|
|
71
|
+
* the file-level fallback `"file:filePath"` used when no enclosing symbol was
|
|
72
|
+
* found for a call site (module-level code). Splitting at the LAST colon
|
|
73
|
+
* (rather than a naive `split(":")`) means a Windows drive letter in
|
|
74
|
+
* `filePath` (`C:\...`) is never mistaken for the name/file separator; the
|
|
75
|
+
* `"file:"` fallback is matched as a literal prefix first so it isn't
|
|
76
|
+
* misparsed by the same last-colon rule.
|
|
77
|
+
*/
|
|
78
|
+
export function parseSymbolKey(key) {
|
|
79
|
+
if (key.startsWith("file:")) {
|
|
80
|
+
return { filePath: key.slice("file:".length) };
|
|
81
|
+
}
|
|
82
|
+
const idx = key.lastIndexOf(":");
|
|
83
|
+
if (idx === -1)
|
|
84
|
+
return { filePath: key };
|
|
85
|
+
const symbolName = key.slice(idx + 1);
|
|
86
|
+
return { filePath: key.slice(0, idx), symbolName: symbolName || undefined };
|
|
87
|
+
}
|
|
68
88
|
/**
|
|
69
89
|
* Format an impact result set as a compact human-readable summary.
|
|
70
90
|
* Example: "handleToolResult (WillBreak) → handleAgentEnd (MayBreak) → 3 Review callers"
|
|
@@ -2,9 +2,13 @@ import * as path from "node:path";
|
|
|
2
2
|
import { isTestMode } from "./env-utils.js";
|
|
3
3
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
4
4
|
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
5
|
+
import { getMaxLogSizeMB } from "./log-cleanup.js";
|
|
5
6
|
const CASCADE_LOG_DIR = getGlobalPiLensDir();
|
|
6
7
|
const CASCADE_LOG_FILE = path.join(CASCADE_LOG_DIR, "cascade.log");
|
|
7
|
-
const writer = createNdjsonLogger({
|
|
8
|
+
const writer = createNdjsonLogger({
|
|
9
|
+
filePath: CASCADE_LOG_FILE,
|
|
10
|
+
maxBytes: getMaxLogSizeMB() * 1024 * 1024,
|
|
11
|
+
});
|
|
8
12
|
export function logCascade(entry) {
|
|
9
13
|
if (isTestMode()) {
|
|
10
14
|
return;
|