pi-lens 4.1.2 → 4.1.4
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 +1308 -0
- package/README.md +3 -0
- package/config/biome/core.jsonc +11 -2
- package/config/dependency-cruiser-eager-allowlist.json +91 -0
- package/dist/clients/actionable-warnings-logger.js +18 -3
- package/dist/clients/actionable-warnings.js +927 -80
- package/dist/clients/agent-behavior-client.js +13 -4
- package/dist/clients/ast-grep-client.js +136 -9
- package/dist/clients/ast-grep-rule-manager.js +60 -5
- package/dist/clients/ast-grep-tool-logger.js +2 -2
- package/dist/clients/availability-probe-flight.js +12 -0
- package/dist/clients/bash-file-access.js +1 -3
- package/dist/clients/biome-client.js +16 -2
- package/dist/clients/blocker-freshness.js +34 -0
- package/dist/clients/bootstrap.js +509 -73
- package/dist/clients/bounded-cache.js +152 -12
- package/dist/clients/bounded-telemetry.js +115 -23
- package/dist/clients/build-identity.js +95 -0
- package/dist/clients/bundled-resource-health.js +113 -0
- package/dist/clients/bus-events-logger.js +2 -2
- package/dist/clients/bus-publish.js +3 -0
- package/dist/clients/cache/rule-cache.js +33 -5
- package/dist/clients/cache-manager.js +105 -6
- package/dist/clients/cache-observability.js +46 -19
- package/dist/clients/cargo-manifest.js +422 -0
- package/dist/clients/cascade-logger.js +17 -3
- package/dist/clients/code-quality-warnings.js +13 -3
- package/dist/clients/complexity-client.js +16 -5
- package/dist/clients/config-core/deny.js +221 -0
- package/dist/clients/config-core/index.js +47 -0
- package/dist/clients/config-core/merge.js +357 -0
- package/dist/clients/config-core/normalize.js +340 -0
- package/dist/clients/config-core/process-spec.js +248 -0
- package/dist/clients/config-core/provenance.js +171 -0
- package/dist/clients/config-core/records.js +218 -0
- package/dist/clients/config-core/resolve.js +125 -0
- package/dist/clients/config-core/safe-object.js +78 -0
- package/dist/clients/config-core/schema.js +167 -0
- package/dist/clients/config-diagnostic-codes.js +281 -0
- package/dist/clients/config-locations.js +160 -0
- package/dist/clients/config-resolve.js +789 -0
- package/dist/clients/config-schema.js +197 -0
- package/dist/clients/config-warn.js +407 -0
- package/dist/clients/dead-code-client.js +33 -25
- package/dist/clients/dead-code-logger.js +2 -2
- package/dist/clients/deadline-utils.js +178 -0
- package/dist/clients/debug-handles.js +2 -2
- package/dist/clients/debug-heap.js +3 -3
- package/dist/clients/deferred-lsp-work.js +106 -0
- package/dist/clients/degradation-ledger.js +166 -8
- package/dist/clients/demoted-finding-render.js +14 -0
- package/dist/clients/dependency-checker.js +28 -22
- package/dist/clients/deps/minimatch.js +1 -1
- package/dist/clients/diagnostic-line-freshness.js +12 -9
- package/dist/clients/diagnostic-logger.js +14 -4
- package/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/dist/clients/dispatch/dispatcher.js +304 -19
- package/dist/clients/dispatch/fact-store.js +328 -9
- package/dist/clients/dispatch/integration.js +214 -107
- package/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/dist/clients/dispatch/plan.js +21 -5
- package/dist/clients/dispatch/runners/actionlint.js +0 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +550 -77
- package/dist/clients/dispatch/runners/biome-check.js +0 -1
- package/dist/clients/dispatch/runners/cpp-check.js +0 -1
- package/dist/clients/dispatch/runners/credo.js +0 -1
- package/dist/clients/dispatch/runners/cue-vet.js +0 -1
- package/dist/clients/dispatch/runners/dart-analyze.js +0 -1
- package/dist/clients/dispatch/runners/detekt.js +0 -1
- package/dist/clients/dispatch/runners/dotnet-build.js +0 -1
- package/dist/clients/dispatch/runners/elixir-check.js +0 -1
- package/dist/clients/dispatch/runners/eslint.js +5 -2
- package/dist/clients/dispatch/runners/fact-rules.js +0 -1
- package/dist/clients/dispatch/runners/fish-indent.js +0 -1
- package/dist/clients/dispatch/runners/gleam-check.js +0 -1
- package/dist/clients/dispatch/runners/go-vet.js +1 -3
- package/dist/clients/dispatch/runners/golangci-lint.js +0 -1
- package/dist/clients/dispatch/runners/hadolint.js +0 -1
- package/dist/clients/dispatch/runners/helm-lint.js +16 -4
- package/dist/clients/dispatch/runners/helm-render.js +54 -10
- package/dist/clients/dispatch/runners/htmlhint.js +0 -1
- package/dist/clients/dispatch/runners/index.js +1 -3
- package/dist/clients/dispatch/runners/javac.js +0 -1
- package/dist/clients/dispatch/runners/ktlint.js +0 -1
- package/dist/clients/dispatch/runners/lsp.js +37 -2
- package/dist/clients/dispatch/runners/markdownlint.js +4 -2
- package/dist/clients/dispatch/runners/mypy.js +0 -1
- package/dist/clients/dispatch/runners/oxlint.js +24 -22
- package/dist/clients/dispatch/runners/php-lint.js +0 -1
- package/dist/clients/dispatch/runners/phpstan.js +0 -1
- package/dist/clients/dispatch/runners/prisma-validate.js +0 -1
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -1
- package/dist/clients/dispatch/runners/pyright.js +7 -4
- package/dist/clients/dispatch/runners/rubocop.js +0 -1
- package/dist/clients/dispatch/runners/ruff.js +0 -1
- package/dist/clients/dispatch/runners/rust-clippy.js +9 -6
- package/dist/clients/dispatch/runners/shellcheck.js +0 -1
- package/dist/clients/dispatch/runners/shfmt.js +0 -1
- package/dist/clients/dispatch/runners/spellcheck.js +0 -1
- package/dist/clients/dispatch/runners/spotbugs.js +0 -1
- package/dist/clients/dispatch/runners/sqlfluff.js +0 -1
- package/dist/clients/dispatch/runners/stylelint.js +0 -1
- package/dist/clients/dispatch/runners/swiftlint.js +0 -1
- package/dist/clients/dispatch/runners/taplo.js +0 -1
- package/dist/clients/dispatch/runners/terragrunt.js +0 -1
- package/dist/clients/dispatch/runners/tflint.js +0 -1
- package/dist/clients/dispatch/runners/tree-sitter.js +0 -1
- package/dist/clients/dispatch/runners/trivy-config.js +0 -1
- package/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
- package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +270 -105
- package/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
- package/dist/clients/dispatch/runners/utils/toolchain-availability.js +89 -26
- package/dist/clients/dispatch/runners/vale.js +0 -1
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
- package/dist/clients/dispatch/runners/yamllint.js +0 -1
- package/dist/clients/dispatch/runners/zig-check.js +0 -1
- package/dist/clients/disposition-logger.js +3 -3
- package/dist/clients/effective-config.js +403 -0
- package/dist/clients/error-class.js +23 -0
- package/dist/clients/event-loop-hold.js +274 -0
- package/dist/clients/event-loop-monitor.js +66 -1
- package/dist/clients/extension-log.js +2 -2
- package/dist/clients/feature-hints.js +2 -1
- package/dist/clients/file-time.js +6 -1
- package/dist/clients/file-utils.js +266 -13
- package/dist/clients/finding-delivery-gate.js +55 -13
- package/dist/clients/format-service.js +6 -1
- package/dist/clients/formatters.js +303 -104
- package/dist/clients/freshness-cadence.js +17 -0
- package/dist/clients/generated-artifacts.js +114 -19
- package/dist/clients/generation-guard.js +4 -9
- package/dist/clients/git-guard.js +198 -48
- package/dist/clients/git-tracked-ignore.js +32 -1
- package/dist/clients/go-client.js +39 -0
- package/dist/clients/govulncheck-client.js +5 -1
- package/dist/clients/gradle-ktfmt-style.js +252 -0
- package/dist/clients/gzip-stage-write.js +7 -0
- package/dist/clients/hashline-anchor.js +424 -0
- package/dist/clients/hook-budgets.js +76 -0
- package/dist/clients/host-edit-normalize.js +5 -2
- package/dist/clients/host-ports.js +1 -1
- package/dist/clients/installer/index.js +200 -84
- package/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/dist/clients/instance-reaper.js +108 -243
- package/dist/clients/instance-registry-lock.js +145 -0
- package/dist/clients/instance-registry.js +435 -122
- package/dist/clients/jscpd-client.js +6 -1
- package/dist/clients/language-profile.js +4 -2
- package/dist/clients/language-registry.js +595 -0
- package/dist/clients/latency-logger.js +179 -3
- package/dist/clients/ledger-bounds.js +13 -0
- package/dist/clients/lens-config.js +141 -30
- package/dist/clients/lens-engine.js +30 -37
- package/dist/clients/lens-flag-registry.js +67 -6
- package/dist/clients/lens-map.js +3 -1
- package/dist/clients/log-cleanup.js +2 -2
- package/dist/clients/lsp/aggregation.js +3 -1
- package/dist/clients/lsp/cascade-tier.js +4 -0
- package/dist/clients/lsp/client.js +672 -49
- package/dist/clients/lsp/config.js +504 -156
- package/dist/clients/lsp/diagnostic-binding.js +10 -5
- package/dist/clients/lsp/edits.js +15 -2
- package/dist/clients/lsp/index.js +1179 -247
- package/dist/clients/lsp/jvm-runtime.js +5 -0
- package/dist/clients/lsp/language.js +18 -169
- package/dist/clients/lsp/launch.js +38 -9
- package/dist/clients/lsp/pending-aux-coverage.js +113 -12
- package/dist/clients/lsp/server.js +426 -160
- package/dist/clients/lsp/session-roots.js +192 -0
- package/dist/clients/lsp/spawn-history.js +2 -7
- package/dist/clients/lsp/tsserver-sync.js +9 -2
- package/dist/clients/lsp/workspace-diagnostics-cache.js +17 -7
- package/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/dist/clients/lsp-mutation.js +179 -23
- package/dist/clients/map-with-concurrency.js +36 -0
- package/dist/clients/mcp/analyze.js +45 -10
- package/dist/clients/mcp/session.js +19 -17
- package/dist/clients/memory-sampler.js +28 -1
- package/dist/clients/message-end-attribution.js +30 -0
- package/dist/clients/metrics-history.js +21 -4
- package/dist/clients/middle-man-analysis.js +2 -4
- package/dist/clients/module-report.js +20 -45
- package/dist/clients/mutating-tool.js +651 -0
- package/dist/clients/mutation-attribution.js +368 -0
- package/dist/clients/mutation-bridge.js +240 -0
- package/dist/clients/ndjson-logger.js +247 -20
- package/dist/clients/observed-mutation-sources.js +101 -0
- package/dist/clients/observed-mutation.js +1215 -0
- package/dist/clients/opaque-mutation-scan.js +300 -40
- package/dist/clients/opengrep-config.js +4 -0
- package/dist/clients/package-manager.js +201 -44
- package/dist/clients/partial-edit-apply.js +359 -80
- package/dist/clients/path-attribution-telemetry.js +23 -7
- package/dist/clients/path-keyed-map.js +35 -3
- package/dist/clients/path-utils.js +460 -13
- package/dist/clients/persist-debounce.js +8 -1
- package/dist/clients/php-cs-fixer-config.js +114 -0
- package/dist/clients/pipeline.js +77 -24
- package/dist/clients/probe-home-state.js +228 -0
- package/dist/clients/process-bridge.js +66 -0
- package/dist/clients/process-singletons.js +210 -0
- package/dist/clients/process-snapshot.js +68 -0
- package/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
- package/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
- package/dist/clients/project-diagnostics/scanner.js +61 -24
- package/dist/clients/project-lens-config.js +393 -122
- package/dist/clients/project-snapshot.js +85 -35
- package/dist/clients/python-environment.js +217 -0
- package/dist/clients/python-provenance.js +639 -0
- package/dist/clients/quiet-window.js +6 -1
- package/dist/clients/read-bridge.js +13 -21
- package/dist/clients/read-expansion.js +9 -46
- package/dist/clients/read-guard-logger.js +39 -6
- package/dist/clients/read-guard-tool-lines.js +364 -212
- package/dist/clients/read-guard.js +95 -7
- package/dist/clients/recent-touches.js +17 -5
- package/dist/clients/resource-sampler.js +195 -96
- package/dist/clients/review-graph/builder.js +752 -132
- package/dist/clients/review-graph/service.js +55 -3
- package/dist/clients/review-graph/shared-extraction-ir.js +7 -15
- package/dist/clients/review-graph/tsconfig-paths.js +5 -1
- package/dist/clients/review-graph/workspace-modules.js +10 -74
- package/dist/clients/review-graph-logger.js +30 -4
- package/dist/clients/runtime-agent-end.js +18 -2
- package/dist/clients/runtime-context.js +49 -16
- package/dist/clients/runtime-coordinator.js +93 -6
- package/dist/clients/runtime-session.js +169 -34
- package/dist/clients/runtime-tool-call.js +183 -35
- package/dist/clients/runtime-tool-result.js +894 -241
- package/dist/clients/runtime-turn.js +1264 -74
- package/dist/clients/rust-client.js +31 -0
- package/dist/clients/safe-spawn.js +191 -28
- package/dist/clients/sanitize.js +7 -1
- package/dist/clients/scratch-tree-policy.js +1 -3
- package/dist/clients/security-scan-client.js +51 -17
- package/dist/clients/session-lifecycle.js +238 -42
- package/dist/clients/session-start-observability.js +79 -0
- package/dist/clients/sessionstart-logger.js +12 -3
- package/dist/clients/sg-runner.js +27 -15
- package/dist/clients/sgconfig.js +6 -1
- package/dist/clients/shared-checkout-guard.js +338 -0
- package/dist/clients/skills-resolver.js +105 -0
- package/dist/clients/smells-rollup.js +2 -2
- package/dist/clients/spawn-output-cap.js +37 -0
- package/dist/clients/spawn-timeout-cooldown.js +15 -1
- package/dist/clients/startup-scan.js +6 -7
- package/dist/clients/startup-timing.js +69 -4
- package/dist/clients/string-utils.js +33 -0
- package/dist/clients/subagent-mode.js +17 -4
- package/dist/clients/test-runner-client.js +586 -118
- package/dist/clients/test-runner-delivery.js +239 -0
- package/dist/clients/tool-definition.js +41 -1
- package/dist/clients/tool-policy.js +92 -22
- package/dist/clients/tree-sitter-cache.js +40 -40
- package/dist/clients/tree-sitter-client.js +97 -53
- package/dist/clients/tree-sitter-logger.js +19 -3
- package/dist/clients/tree-sitter-query-loader.js +81 -1
- package/dist/clients/tree-sitter-shared.js +51 -46
- package/dist/clients/tree-sitter-symbol-extractor.js +28 -0
- package/dist/clients/typos-config.js +5 -0
- package/dist/clients/user-notify.js +6 -2
- package/dist/clients/warm-attach.js +8 -10
- package/dist/clients/widget-state.js +191 -26
- package/dist/clients/word-index-logger.js +15 -3
- package/dist/clients/word-index-store.js +463 -0
- package/dist/clients/word-index.js +815 -125
- package/dist/clients/workspace-topology.js +25 -1
- package/dist/clients/zizmor-config.js +29 -6
- package/dist/index.js +68577 -53950
- package/dist/mcp/analyze-cli.js +3 -1
- package/dist/mcp/build-staleness.js +8 -7
- package/dist/mcp/cli.js +3 -1
- package/dist/mcp/server.js +133 -6
- package/dist/scripts/lib/process-scan.mjs +583 -0
- package/dist/scripts/lib/skills-predicate.mjs +129 -0
- package/dist/tools/effective-config.js +89 -0
- package/dist/tools/lens-diagnostics.js +99 -24
- package/dist/tools/lsp-diagnostics.js +81 -85
- package/dist/tools/lsp-navigation.js +40 -17
- package/dist/tools/shared.js +0 -1
- package/docs/agent-guide.md +68 -3
- package/docs/ast-grep_rules_catalog.md +41 -10
- package/docs/astplayground.md +26 -17
- package/docs/configuration.md +222 -0
- package/docs/dependencies.md +3 -3
- package/docs/features.md +44 -5
- package/docs/language-coverage.md +2 -2
- package/docs/pi-lens-fixer.md +25 -0
- package/docs/pi-lens-investigator.md +25 -0
- package/docs/pi-lens-reviewer.md +27 -0
- package/docs/pi-lens-subagent.md +38 -0
- package/docs/pi-lens-warden.md +55 -0
- package/docs/public-api-stability.md +359 -0
- package/docs/release-qa-baseline.md +182 -0
- package/docs/subagent-compat.md +110 -30
- package/docs/tree-sitter_rules_catalog.md +2 -2
- package/package.json +17 -21
- package/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
- package/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
- package/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
- package/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
- package/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
- package/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
- package/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
- package/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
- package/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
- package/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
- package/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
- package/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
- package/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
- package/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
- package/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
- package/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
- package/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
- package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
- package/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
- package/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
- package/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
- package/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
- package/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
- package/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
- package/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
- package/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
- package/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
- package/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
- package/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
- package/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
- package/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
- package/rules/ast-grep-rules/rules/no-important.yml +21 -0
- package/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
- package/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
- package/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
- package/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
- package/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
- package/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
- package/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
- package/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
- package/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
- package/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
- package/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
- package/scripts/analyze-pi-lens-logs.mjs +192 -1
- package/scripts/install-selftest.mjs +58 -2
- package/scripts/lib/skills-predicate.mjs +129 -0
- package/scripts/rpc-load-check.mjs +3 -1
|
@@ -167,18 +167,95 @@ function hasGeneratedArtifactContent(content) {
|
|
|
167
167
|
const header = content.slice(0, DEFAULT_HEADER_BYTES);
|
|
168
168
|
return GENERATED_HEADER_PATTERNS.some((pattern) => pattern.test(header));
|
|
169
169
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Content-shape classifier for machine-emitted text (refs #2346).
|
|
172
|
+
*
|
|
173
|
+
* A saved/scraped page, a minified bundle, or an embedded data blob is one
|
|
174
|
+
* giant line or a handful, so mean non-empty line length on already-loaded
|
|
175
|
+
* content is a language-agnostic generated signature. Extensions covered by
|
|
176
|
+
* name patterns can never cover it (`.html` has no `.min.`-style convention),
|
|
177
|
+
* which is the exact gap #2346 hit: a 269 KB scraped page on 38 lines (mean
|
|
178
|
+
* ~7084/line) drew the full auxiliary scanner stack.
|
|
179
|
+
*
|
|
180
|
+
* The threshold is derived from measurement ON THE SHIPPED STATISTIC (the
|
|
181
|
+
* 4096-byte header/prefix mean, not full-content means — the two differ by
|
|
182
|
+
* 3-7x on long-paragraph prose). Measured 2026-08-28 across all 3,331 text
|
|
183
|
+
* files in this repo: the maximum hand-authored prefix mean is 453.2
|
|
184
|
+
* (`docs/analysisall.md`, unwrapped prose paragraphs); the widest
|
|
185
|
+
* hand-authored one-line shape observed anywhere probed is a ~2048-char
|
|
186
|
+
* barrel re-export. The threshold of 2500 sits ~5.5x above the repo's worst
|
|
187
|
+
* prose file and above every observed hand-authored shape, while the #2346
|
|
188
|
+
* evidence file (269 KB / 38 lines) remains far above it. Direction of
|
|
189
|
+
* error is deliberate per #1107: a borderline machine-emitted file that
|
|
190
|
+
* slips through (false KEEP) costs noise once; a hand-authored file
|
|
191
|
+
* silently classified generated (false DROP) vanishes from scans with no
|
|
192
|
+
* signal. Both constants are exported so the tuning claim is checkable and
|
|
193
|
+
* the regression tests pin the margin probes on both sides.
|
|
194
|
+
*
|
|
195
|
+
* Prefix-path quantization: on the 4096-byte header/prefix paths the
|
|
196
|
+
* achievable means are quantized (~4096 for a one-line prefix, ~2048 for
|
|
197
|
+
* two lines), so any threshold in (2048, 4096] behaves as "is the first
|
|
198
|
+
* line longer than 4096 bytes". A machine-emitted file with 2500-4000-char
|
|
199
|
+
* lines is missed on the prefix path even though its full content would
|
|
200
|
+
* classify — the deliberate false-KEEP direction, not a continuous 5.5x
|
|
201
|
+
* margin there. The full-content path (dispatch) has no such quantization.
|
|
202
|
+
*
|
|
203
|
+
* Degenerate cases are guarded: content below the minimum byte length never
|
|
204
|
+
* classifies generated on line shape alone (a 500-byte single-line config
|
|
205
|
+
* file is not machine-emitted evidence), and an empty or whitespace-only
|
|
206
|
+
* file yields a zero mean and stays source.
|
|
207
|
+
*/
|
|
208
|
+
export const MACHINE_EMITTED_LINE_SHAPE_MIN_CONTENT = 2048;
|
|
209
|
+
export const MACHINE_EMITTED_LINE_SHAPE_MEAN_THRESHOLD = 2500;
|
|
210
|
+
export function classifyMachineEmittedLineShape(content) {
|
|
211
|
+
const contentLength = content.length;
|
|
212
|
+
if (contentLength < MACHINE_EMITTED_LINE_SHAPE_MIN_CONTENT) {
|
|
213
|
+
return { generated: false, meanLineLength: 0 };
|
|
214
|
+
}
|
|
215
|
+
// Single O(contentLength) pass. Deliberately no `split(/\r?\n/)`: a
|
|
216
|
+
// minified blob's single line can be megabytes, and splitting allocates a
|
|
217
|
+
// copy of the whole content. `\r` contributes nothing to line length so a
|
|
218
|
+
// CRLF line is measured by its display width.
|
|
219
|
+
let nonEmptyLineTotal = 0;
|
|
220
|
+
let nonEmptyLineCount = 0;
|
|
221
|
+
let currentLineLength = 0;
|
|
222
|
+
for (let i = 0; i < contentLength; i++) {
|
|
223
|
+
const code = content.charCodeAt(i);
|
|
224
|
+
if (code === 10 /* \n */) {
|
|
225
|
+
if (currentLineLength > 0) {
|
|
226
|
+
nonEmptyLineTotal += currentLineLength;
|
|
227
|
+
nonEmptyLineCount += 1;
|
|
228
|
+
}
|
|
229
|
+
currentLineLength = 0;
|
|
230
|
+
}
|
|
231
|
+
else if (code !== 13 /* \r */) {
|
|
232
|
+
currentLineLength += 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (currentLineLength > 0) {
|
|
236
|
+
nonEmptyLineTotal += currentLineLength;
|
|
237
|
+
nonEmptyLineCount += 1;
|
|
238
|
+
}
|
|
239
|
+
if (nonEmptyLineCount === 0)
|
|
240
|
+
return { generated: false, meanLineLength: 0 };
|
|
241
|
+
const meanLineLength = nonEmptyLineTotal / nonEmptyLineCount;
|
|
242
|
+
return {
|
|
243
|
+
generated: meanLineLength > MACHINE_EMITTED_LINE_SHAPE_MEAN_THRESHOLD,
|
|
244
|
+
meanLineLength,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// Memoize the header analysis (banner + line shape) per (path, mtimeMs, size,
|
|
248
|
+
// maxBytes). The 4 KB header read (openSync + readSync + closeSync) is ~70% of
|
|
249
|
+
// the per-file cost in `collectSourceFiles`, and the same files are re-scanned
|
|
250
|
+
// by multiple background scanners (todo, project-diagnostics, language-profile)
|
|
174
251
|
// every session/turn. Keying on mtime+size makes the memo self-invalidating:
|
|
175
252
|
// an edited file misses the cache and is re-read. A single stat replaces the
|
|
176
253
|
// open/read/close on a hit. Bounded to avoid unbounded growth on giant repos.
|
|
177
254
|
const HEADER_VERDICT_MEMO_CAP = 50_000;
|
|
178
|
-
const
|
|
179
|
-
/** Test-only: drop the header-
|
|
255
|
+
const headerAnalysisMemo = new BoundedLruCache(HEADER_VERDICT_MEMO_CAP);
|
|
256
|
+
/** Test-only: drop the header-analysis memo so fixtures don't leak across cases. */
|
|
180
257
|
export function _resetGeneratedArtifactCaches() {
|
|
181
|
-
|
|
258
|
+
headerAnalysisMemo.clear();
|
|
182
259
|
}
|
|
183
260
|
function readFileHeader(filePath, maxBytes = DEFAULT_HEADER_BYTES) {
|
|
184
261
|
let fd;
|
|
@@ -202,27 +279,29 @@ function readFileHeader(filePath, maxBytes = DEFAULT_HEADER_BYTES) {
|
|
|
202
279
|
}
|
|
203
280
|
}
|
|
204
281
|
}
|
|
205
|
-
|
|
206
|
-
* Memoized form of "does this file's header contain a generated-code banner?".
|
|
207
|
-
* Returns `false` when the file cannot be stat'd/read (same fallback as the
|
|
208
|
-
* inline read path). Keyed on path + mtime + size so edits invalidate it.
|
|
209
|
-
*/
|
|
210
|
-
function fileHeaderLooksGenerated(filePath, maxBytes) {
|
|
282
|
+
function analyzeFileHeader(filePath, maxBytes) {
|
|
211
283
|
let key;
|
|
212
284
|
try {
|
|
213
285
|
const st = fs.statSync(filePath);
|
|
214
286
|
key = `${st.mtimeMs}:${st.size}:${maxBytes}:${filePath}`;
|
|
215
287
|
}
|
|
216
288
|
catch {
|
|
217
|
-
return
|
|
289
|
+
return undefined;
|
|
218
290
|
}
|
|
219
|
-
const cached =
|
|
291
|
+
const cached = headerAnalysisMemo.get(key);
|
|
220
292
|
if (cached !== undefined)
|
|
221
293
|
return cached;
|
|
222
294
|
const header = readFileHeader(filePath, maxBytes);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
295
|
+
// A header that cannot be read classifies as clean (no banner, no shape
|
|
296
|
+
// signal) and is memoized as such, mirroring the pre-#2346 fallback.
|
|
297
|
+
const analysis = header === undefined
|
|
298
|
+
? { banner: false, shape: { generated: false, meanLineLength: 0 } }
|
|
299
|
+
: {
|
|
300
|
+
banner: hasGeneratedArtifactContent(header),
|
|
301
|
+
shape: classifyMachineEmittedLineShape(header),
|
|
302
|
+
};
|
|
303
|
+
headerAnalysisMemo.set(key, analysis);
|
|
304
|
+
return analysis;
|
|
226
305
|
}
|
|
227
306
|
/**
|
|
228
307
|
* The full verdict (+ evidence tier) behind {@link isGeneratedOrArtifact}'s
|
|
@@ -272,12 +351,28 @@ export function classifyGeneratedOrArtifactDetailed(filePath, options = {}) {
|
|
|
272
351
|
if (hasGeneratedArtifactContent(options.content)) {
|
|
273
352
|
return { verdict: "generated", evidence: "content" };
|
|
274
353
|
}
|
|
354
|
+
const shape = classifyMachineEmittedLineShape(options.content);
|
|
355
|
+
if (shape.generated) {
|
|
356
|
+
return {
|
|
357
|
+
verdict: "generated",
|
|
358
|
+
evidence: "line-shape",
|
|
359
|
+
lineShapeMean: shape.meanLineLength,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
275
362
|
return { verdict: weakNameMatch ? "override" : "clean" };
|
|
276
363
|
}
|
|
277
364
|
if (options.readContentHeader) {
|
|
278
|
-
|
|
365
|
+
const analysis = analyzeFileHeader(filePath, options.maxHeaderBytes ?? DEFAULT_HEADER_BYTES);
|
|
366
|
+
if (analysis?.banner) {
|
|
279
367
|
return { verdict: "generated", evidence: "content" };
|
|
280
368
|
}
|
|
369
|
+
if (analysis?.shape.generated) {
|
|
370
|
+
return {
|
|
371
|
+
verdict: "generated",
|
|
372
|
+
evidence: "line-shape",
|
|
373
|
+
lineShapeMean: analysis.shape.meanLineLength,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
281
376
|
return { verdict: weakNameMatch ? "override" : "clean" };
|
|
282
377
|
}
|
|
283
378
|
// No content/header probe was supplied or enabled: there is no cheap way
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
* `GenerationMapOptions.normalizeKey` for the correctness reason the slow
|
|
51
51
|
* path exists at all.
|
|
52
52
|
*/
|
|
53
|
+
import { BoundedFifoMap } from "./bounded-cache.js";
|
|
53
54
|
import { incrementDegradationCount } from "./degradation-ledger.js";
|
|
54
55
|
/**
|
|
55
56
|
* Names of every generation-carrying store created through this module.
|
|
@@ -136,7 +137,7 @@ export function createGenerationMap(name, options = {}) {
|
|
|
136
137
|
// Insertion-ordered: the oldest key is evicted first. Re-stamping a key
|
|
137
138
|
// refreshes its position so a hot cwd is not evicted by a burst of
|
|
138
139
|
// one-shot ones.
|
|
139
|
-
const stamps = new
|
|
140
|
+
const stamps = new BoundedFifoMap(maxKeys);
|
|
140
141
|
// ONE ticket counter for the whole map. See GenerationMap's doc comment:
|
|
141
142
|
// per-key counters starting at 0 make eviction and forget() fail OPEN.
|
|
142
143
|
// 0 is reserved for "this key holds no stamp" and is never issued.
|
|
@@ -149,14 +150,8 @@ export function createGenerationMap(name, options = {}) {
|
|
|
149
150
|
function issue(key) {
|
|
150
151
|
nextTicket += 1;
|
|
151
152
|
stamps.delete(key);
|
|
152
|
-
stamps.set(key, nextTicket);
|
|
153
|
-
|
|
154
|
-
const oldest = stamps.keys().next().value;
|
|
155
|
-
if (oldest === undefined)
|
|
156
|
-
break;
|
|
157
|
-
stamps.delete(oldest);
|
|
158
|
-
invalidations += 1;
|
|
159
|
-
}
|
|
153
|
+
const evicted = stamps.set(key, nextTicket);
|
|
154
|
+
invalidations += evicted.length;
|
|
160
155
|
return nextTicket;
|
|
161
156
|
}
|
|
162
157
|
return {
|
|
@@ -207,7 +207,13 @@ function delimitedSubstitutionBody(command, start, opener, closer) {
|
|
|
207
207
|
}
|
|
208
208
|
return undefined;
|
|
209
209
|
}
|
|
210
|
-
|
|
210
|
+
const COMMIT_PUSH_MATCHER = {
|
|
211
|
+
id: "commit-push",
|
|
212
|
+
matchesVerb: (verb) => verb === "commit" || verb === "push",
|
|
213
|
+
indirectAlwaysMatches: true,
|
|
214
|
+
suppressPostVerbHelp: false,
|
|
215
|
+
};
|
|
216
|
+
function containsGuardedSubstitution(command, depth, matcher) {
|
|
211
217
|
if (depth > 3)
|
|
212
218
|
return false;
|
|
213
219
|
let quote;
|
|
@@ -249,8 +255,8 @@ function containsGuardedSubstitution(command, depth) {
|
|
|
249
255
|
: undefined;
|
|
250
256
|
if (substitution && substitution.end >= 0) {
|
|
251
257
|
const nested = canonicalizeGuardCommand(substitution.body);
|
|
252
|
-
if (containsGuardedSubstitution(nested, depth + 1) ||
|
|
253
|
-
tokenizeShellCommand(nested).some((segment) =>
|
|
258
|
+
if (containsGuardedSubstitution(nested, depth + 1, matcher) ||
|
|
259
|
+
tokenizeShellCommand(nested).some((segment) => containsGuardedGitVerb(segment.tokens, depth + 1, matcher))) {
|
|
254
260
|
return true;
|
|
255
261
|
}
|
|
256
262
|
i = substitution.end;
|
|
@@ -258,7 +264,169 @@ function containsGuardedSubstitution(command, depth) {
|
|
|
258
264
|
}
|
|
259
265
|
return false;
|
|
260
266
|
}
|
|
261
|
-
|
|
267
|
+
/** Git global options that consume the following token as their value. */
|
|
268
|
+
const GIT_GLOBAL_OPTIONS_WITH_VALUE = new Set([
|
|
269
|
+
"-C",
|
|
270
|
+
"-c",
|
|
271
|
+
"--config-env",
|
|
272
|
+
"--git-dir",
|
|
273
|
+
"--work-tree",
|
|
274
|
+
"--exec-path",
|
|
275
|
+
"--namespace",
|
|
276
|
+
]);
|
|
277
|
+
const GIT_GLOBAL_OPTIONS_WITH_INLINE_VALUE = [
|
|
278
|
+
"--config-env",
|
|
279
|
+
"--git-dir",
|
|
280
|
+
"--work-tree",
|
|
281
|
+
"--exec-path",
|
|
282
|
+
"--namespace",
|
|
283
|
+
];
|
|
284
|
+
/**
|
|
285
|
+
* Index of git's subcommand token in `gitTokens` (which starts at the `git`
|
|
286
|
+
* executable), after skipping global options and their values. Returns
|
|
287
|
+
* `undefined` when the invocation has no subcommand at all — `git --help`,
|
|
288
|
+
* `git --version`, or a trailing option with no verb behind it.
|
|
289
|
+
*
|
|
290
|
+
* Shared by the direct and indirect paths so both ask the same question about
|
|
291
|
+
* the same position (#2007).
|
|
292
|
+
*/
|
|
293
|
+
function gitVerbIndex(gitTokens) {
|
|
294
|
+
let i = 1;
|
|
295
|
+
while (i < gitTokens.length && gitTokens[i].startsWith("-")) {
|
|
296
|
+
const option = gitTokens[i];
|
|
297
|
+
if (["--help", "-h", "--version", "-v", "-V"].includes(option)) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
if (option === "--")
|
|
301
|
+
return i + 1 < gitTokens.length ? i + 1 : undefined;
|
|
302
|
+
if (["-C", "-c"].some((prefix) => option.startsWith(prefix) && option.length > prefix.length)) {
|
|
303
|
+
i += 1;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (GIT_GLOBAL_OPTIONS_WITH_INLINE_VALUE.some((prefix) => option.startsWith(`${prefix}=`))) {
|
|
307
|
+
i += 1;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
i += GIT_GLOBAL_OPTIONS_WITH_VALUE.has(option) ? 2 : 1;
|
|
311
|
+
}
|
|
312
|
+
return i < gitTokens.length ? i : undefined;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* True when the subcommand in git's command position is one the matcher
|
|
316
|
+
* governs. `gitTokens[0]` is the `git` executable token.
|
|
317
|
+
*/
|
|
318
|
+
export function matchGitVerbAtCommandPosition(gitTokens, matcher) {
|
|
319
|
+
const index = gitVerbIndex(gitTokens);
|
|
320
|
+
if (index === undefined)
|
|
321
|
+
return false;
|
|
322
|
+
const argsAfterVerb = gitTokens.slice(index + 1);
|
|
323
|
+
// `git checkout --help` prints documentation and touches nothing, and the
|
|
324
|
+
// global-option walk above cannot see it, because it sits AFTER the verb.
|
|
325
|
+
//
|
|
326
|
+
// Only the FIRST argument counts, and only for matchers that opt in. `-h`
|
|
327
|
+
// deeper in the argv may be an option VALUE rather than a help request —
|
|
328
|
+
// `git commit -m -h` commits with the message `-h`, `git clean -e -h`
|
|
329
|
+
// excludes a pattern named `-h`. Reading any position would need a
|
|
330
|
+
// per-verb table of value-taking options for every governed verb, which is
|
|
331
|
+
// exactly the hand-maintained parallel list this repo forbids, and every
|
|
332
|
+
// gap in it would silently UNDER-block. Leading-position-only needs no
|
|
333
|
+
// table, covers how help is actually invoked, and every other spelling
|
|
334
|
+
// falls through to a match, which is the safe direction for a guard.
|
|
335
|
+
if (matcher.suppressPostVerbHelp &&
|
|
336
|
+
(argsAfterVerb[0] === "--help" || argsAfterVerb[0] === "-h")) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
const verbs = expandGuardVerbToken(gitTokens[index] ?? "");
|
|
340
|
+
return verbs.length === 1 && matcher.matchesVerb(verbs[0], argsAfterVerb);
|
|
341
|
+
}
|
|
342
|
+
/** Strip one layer of shell quoting the lexer preserved on a value token. */
|
|
343
|
+
function unquoteGuardValue(value) {
|
|
344
|
+
const trimmed = value.trim();
|
|
345
|
+
if (trimmed.length >= 2) {
|
|
346
|
+
const first = trimmed[0];
|
|
347
|
+
const last = trimmed[trimmed.length - 1];
|
|
348
|
+
if ((first === '"' || first === "'") && first === last) {
|
|
349
|
+
return trimmed.slice(1, -1);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return trimmed;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The directory a git invocation actually targets (#2007).
|
|
356
|
+
*
|
|
357
|
+
* `git -C <dir>` and `--work-tree` retarget the command at a DIFFERENT
|
|
358
|
+
* directory, so a guard that evaluates the caller's cwd would inspect the
|
|
359
|
+
* wrong working tree and allow a destructive command against a shared one.
|
|
360
|
+
* `-C` composes cumulatively, exactly as git applies it; `--work-tree` names
|
|
361
|
+
* the working tree directly and therefore wins.
|
|
362
|
+
*
|
|
363
|
+
* Returns the resolved absolute directory, or `cwd` when the invocation
|
|
364
|
+
* carries no retargeting option.
|
|
365
|
+
*/
|
|
366
|
+
export function resolveGitTargetDirectory(gitTokens, cwd) {
|
|
367
|
+
let base = cwd;
|
|
368
|
+
let workTree;
|
|
369
|
+
let i = 1;
|
|
370
|
+
while (i < gitTokens.length && gitTokens[i].startsWith("-")) {
|
|
371
|
+
const option = gitTokens[i];
|
|
372
|
+
if (["--help", "-h", "--version", "-v", "-V"].includes(option))
|
|
373
|
+
break;
|
|
374
|
+
if (option === "--")
|
|
375
|
+
break;
|
|
376
|
+
if (option === "-C" && i + 1 < gitTokens.length) {
|
|
377
|
+
base = path.resolve(base, unquoteGuardValue(gitTokens[i + 1]));
|
|
378
|
+
i += 2;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (option.startsWith("-C") && option.length > 2) {
|
|
382
|
+
base = path.resolve(base, unquoteGuardValue(option.slice(2)));
|
|
383
|
+
i += 1;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (option === "--work-tree" && i + 1 < gitTokens.length) {
|
|
387
|
+
workTree = unquoteGuardValue(gitTokens[i + 1]);
|
|
388
|
+
i += 2;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (option.startsWith("--work-tree=")) {
|
|
392
|
+
workTree = unquoteGuardValue(option.slice("--work-tree=".length));
|
|
393
|
+
i += 1;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (option.startsWith("-c") &&
|
|
397
|
+
option.length > 2 &&
|
|
398
|
+
!option.startsWith("--")) {
|
|
399
|
+
i += 1;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (GIT_GLOBAL_OPTIONS_WITH_INLINE_VALUE.some((prefix) => option.startsWith(`${prefix}=`))) {
|
|
403
|
+
i += 1;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
i += GIT_GLOBAL_OPTIONS_WITH_VALUE.has(option) ? 2 : 1;
|
|
407
|
+
}
|
|
408
|
+
return workTree === undefined ? base : path.resolve(base, workTree);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Every git invocation the command contains, as token arrays starting at the
|
|
412
|
+
* `git` executable. Used by callers that must know WHICH directory a matched
|
|
413
|
+
* command targets, not merely that it matched (#2007).
|
|
414
|
+
*/
|
|
415
|
+
export function collectGitInvocations(toolName, input) {
|
|
416
|
+
if (toolName !== "bash")
|
|
417
|
+
return [];
|
|
418
|
+
const command = getShellCommand(input);
|
|
419
|
+
if (!command)
|
|
420
|
+
return [];
|
|
421
|
+
const invocations = [];
|
|
422
|
+
for (const segment of tokenizeShellCommand(canonicalizeGuardCommand(command))) {
|
|
423
|
+
const gitIndex = segment.tokens.findIndex((token) => isGitExecutable(token));
|
|
424
|
+
if (gitIndex >= 0)
|
|
425
|
+
invocations.push(segment.tokens.slice(gitIndex));
|
|
426
|
+
}
|
|
427
|
+
return invocations;
|
|
428
|
+
}
|
|
429
|
+
function containsGuardedGitVerb(tokens, depth, matcher) {
|
|
262
430
|
if (depth > 3 || tokens.length === 0)
|
|
263
431
|
return false;
|
|
264
432
|
let commandTokens = tokens;
|
|
@@ -276,7 +444,7 @@ function containsCommitOrPush(tokens, depth) {
|
|
|
276
444
|
const runIndex = lower.findIndex((token) => token === "--run");
|
|
277
445
|
if (runIndex >= 0 && runIndex + 1 < commandTokens.length) {
|
|
278
446
|
const nestedCommand = commandTokens.slice(runIndex + 2).join(" ");
|
|
279
|
-
return tokenizeShellCommand(canonicalizeGuardCommand(nestedCommand)).some((segment) =>
|
|
447
|
+
return tokenizeShellCommand(canonicalizeGuardCommand(nestedCommand)).some((segment) => containsGuardedGitVerb(segment.tokens, depth + 1, matcher));
|
|
280
448
|
}
|
|
281
449
|
}
|
|
282
450
|
// These commands consume their following words as text/patterns; a bare
|
|
@@ -290,43 +458,14 @@ function containsCommitOrPush(tokens, depth) {
|
|
|
290
458
|
if (gitIndex >= 0) {
|
|
291
459
|
// Any non-leading git invocation is indirect. Do not maintain a wrapper
|
|
292
460
|
// or flag allowlist: unknown launchers are the security boundary here.
|
|
293
|
-
if (gitIndex > 0)
|
|
461
|
+
if (gitIndex > 0 && matcher.indirectAlwaysMatches)
|
|
294
462
|
return true;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
"--git-dir",
|
|
302
|
-
"--work-tree",
|
|
303
|
-
"--exec-path",
|
|
304
|
-
"--namespace",
|
|
305
|
-
]);
|
|
306
|
-
while (i < gitTokens.length && gitTokens[i].startsWith("-")) {
|
|
307
|
-
const option = gitTokens[i];
|
|
308
|
-
if (["--help", "-h", "--version", "-v", "-V"].includes(option))
|
|
309
|
-
return false;
|
|
310
|
-
if (option === "--")
|
|
311
|
-
return gitTokens[i + 1] === "commit" || gitTokens[i + 1] === "push";
|
|
312
|
-
if (["-C", "-c"].some((prefix) => option.startsWith(prefix) && option.length > prefix.length)) {
|
|
313
|
-
i += 1;
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
if ([
|
|
317
|
-
"--config-env",
|
|
318
|
-
"--git-dir",
|
|
319
|
-
"--work-tree",
|
|
320
|
-
"--exec-path",
|
|
321
|
-
"--namespace",
|
|
322
|
-
].some((prefix) => option.startsWith(`${prefix}=`))) {
|
|
323
|
-
i += 1;
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
i += takesValue.has(option) ? 2 : 1;
|
|
327
|
-
}
|
|
328
|
-
const verbs = expandGuardVerbToken(gitTokens[i] ?? "");
|
|
329
|
-
return verbs.length === 1 && (verbs[0] === "commit" || verbs[0] === "push");
|
|
463
|
+
// #2007: a guard that protects the agent from its own accident must not
|
|
464
|
+
// decline `xargs git status`, so the indirect path stays armed only when
|
|
465
|
+
// a governed verb sits in git's real COMMAND POSITION. Scanning every
|
|
466
|
+
// token instead would fire on `find . -name checkout | xargs git add`,
|
|
467
|
+
// where `checkout` is a positional value and not a subcommand at all.
|
|
468
|
+
return matchGitVerbAtCommandPosition(commandTokens.slice(gitIndex), matcher);
|
|
330
469
|
}
|
|
331
470
|
const leadingExecutable = commandTokens[0] ?? "";
|
|
332
471
|
const knownCommandStringWrapper = isShellWrapper(leadingExecutable) ||
|
|
@@ -344,7 +483,7 @@ function containsCommitOrPush(tokens, depth) {
|
|
|
344
483
|
if (commandIndex >= commandTokens.length)
|
|
345
484
|
return false;
|
|
346
485
|
const nestedCommand = commandTokens.slice(commandIndex).join(" ");
|
|
347
|
-
return tokenizeShellCommand(canonicalizeGuardCommand(nestedCommand)).some((segment) =>
|
|
486
|
+
return tokenizeShellCommand(canonicalizeGuardCommand(nestedCommand)).some((segment) => containsGuardedGitVerb(segment.tokens, depth + 1, matcher));
|
|
348
487
|
}
|
|
349
488
|
const lower = commandTokens.slice(1).map((token) => token.toLowerCase());
|
|
350
489
|
const switchIndex = lower.findIndex((token) => token === "-c" ||
|
|
@@ -369,19 +508,30 @@ function containsCommitOrPush(tokens, depth) {
|
|
|
369
508
|
if (commandTokens[commandIndex] === "--")
|
|
370
509
|
commandIndex += 1;
|
|
371
510
|
const nestedCommand = commandTokens.slice(commandIndex).join(" ");
|
|
372
|
-
return tokenizeShellCommand(canonicalizeGuardCommand(nestedCommand)).some((segment) =>
|
|
511
|
+
return tokenizeShellCommand(canonicalizeGuardCommand(nestedCommand)).some((segment) => containsGuardedGitVerb(segment.tokens, depth + 1, matcher));
|
|
373
512
|
}
|
|
374
|
-
/**
|
|
375
|
-
|
|
513
|
+
/**
|
|
514
|
+
* Analyze actual executable invocations, not substrings in shell text.
|
|
515
|
+
*
|
|
516
|
+
* Shared entry point for every guard that needs "is this bash input really a
|
|
517
|
+
* git invocation of a verb I govern" (#2007). The verb question is the only
|
|
518
|
+
* parameter; wrapper, substitution, and text-consumer analysis are identical
|
|
519
|
+
* for all callers by construction.
|
|
520
|
+
*/
|
|
521
|
+
export function detectGuardedGitVerb(toolName, input, matcher) {
|
|
376
522
|
if (toolName !== "bash")
|
|
377
523
|
return false;
|
|
378
524
|
const command = getShellCommand(input);
|
|
379
525
|
if (!command)
|
|
380
526
|
return false;
|
|
381
527
|
const canonical = canonicalizeGuardCommand(command);
|
|
382
|
-
if (containsGuardedSubstitution(canonical, 0))
|
|
528
|
+
if (containsGuardedSubstitution(canonical, 0, matcher))
|
|
383
529
|
return true;
|
|
384
|
-
return tokenizeShellCommand(canonical).some((segment) =>
|
|
530
|
+
return tokenizeShellCommand(canonical).some((segment) => containsGuardedGitVerb(segment.tokens, 0, matcher));
|
|
531
|
+
}
|
|
532
|
+
/** The #1063 commit gate's own question. */
|
|
533
|
+
export function isGitCommitOrPushAttempt(toolName, input) {
|
|
534
|
+
return detectGuardedGitVerb(toolName, input, COMMIT_PUSH_MATCHER);
|
|
385
535
|
}
|
|
386
536
|
function isTurnEndFindingsCache(value) {
|
|
387
537
|
if (!value || typeof value !== "object")
|
|
@@ -416,7 +566,7 @@ function markCacheUnknown(runtime, reason) {
|
|
|
416
566
|
/** Persist a complete, bounded, content-bound guard record. */
|
|
417
567
|
export function writeGitGuardRecord(cacheManager, runtime, cwd, record) {
|
|
418
568
|
const capped = capAffectedFiles(Array.isArray(record.affectedFiles) ? record.affectedFiles : [], cwd);
|
|
419
|
-
const fileSeqByPath = { ...
|
|
569
|
+
const fileSeqByPath = { ...record.fileSeqByPath };
|
|
420
570
|
for (const file of capped.files) {
|
|
421
571
|
const key = guardPathKey(file, cwd);
|
|
422
572
|
if (fileSeqByPath[key] === undefined) {
|
|
@@ -33,8 +33,25 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import * as path from "node:path";
|
|
35
35
|
import { isExcludedDirName } from "./file-utils.js";
|
|
36
|
+
import { recordDegradationOnce } from "./degradation-ledger.js";
|
|
36
37
|
import { normalizeEphemeralMapKey, normalizeMapKey } from "./path-utils.js";
|
|
37
38
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
39
|
+
import { truncatedByOutputCap } from "./spawn-output-cap.js";
|
|
40
|
+
function recordTruncatedLsFiles(parseSite) {
|
|
41
|
+
recordDegradationOnce({
|
|
42
|
+
kind: "git-tracked-ignore-truncated",
|
|
43
|
+
subject: `git-ls-files:${parseSite}`,
|
|
44
|
+
reason: "safeSpawnAsync capped git ls-files stdout",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// The binding listing is the untracked-ignored one: a mid-size project's
|
|
48
|
+
// node_modules alone yields a ~66k-line ignored list (roughly 4-6 MiB), and
|
|
49
|
+
// a monorepo with several dependency trees can multiply that. 16 MiB leaves
|
|
50
|
+
// that headroom while stopping safeSpawnAsync from retaining unbounded
|
|
51
|
+
// stdout (an uncapped probe held 20 MiB in full). The tracked listing is
|
|
52
|
+
// far smaller (~133 KiB in this repository). A capped read fails closed to
|
|
53
|
+
// unavailable with one bounded ledger record per site.
|
|
54
|
+
const MAX_LS_FILES_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
38
55
|
/**
|
|
39
56
|
* Parses `git ls-files --others --ignored --exclude-standard` output
|
|
40
57
|
* (repo-relative paths, one per line) into normalized map-key file ids.
|
|
@@ -62,7 +79,16 @@ export function parseUntrackedIgnoredOutput(stdout, cwd) {
|
|
|
62
79
|
}
|
|
63
80
|
async function fetchUntrackedIgnoredIds(cwd) {
|
|
64
81
|
try {
|
|
65
|
-
const result = await safeSpawnAsync("git", ["ls-files", "--others", "--ignored", "--exclude-standard"], {
|
|
82
|
+
const result = await safeSpawnAsync("git", ["ls-files", "--others", "--ignored", "--exclude-standard"], {
|
|
83
|
+
cwd,
|
|
84
|
+
timeout: 10_000,
|
|
85
|
+
maxOutputBytes: MAX_LS_FILES_OUTPUT_BYTES,
|
|
86
|
+
resourceLabel: "git-tracked-ignore",
|
|
87
|
+
});
|
|
88
|
+
if (truncatedByOutputCap(result)) {
|
|
89
|
+
recordTruncatedLsFiles("untracked-ignored");
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
66
92
|
if (result.error || result.status !== 0)
|
|
67
93
|
return undefined;
|
|
68
94
|
return parseUntrackedIgnoredOutput(result.stdout, cwd);
|
|
@@ -141,8 +167,13 @@ async function fetchTrackedFiles(cwd) {
|
|
|
141
167
|
const result = await safeSpawnAsync("git", ["ls-files"], {
|
|
142
168
|
cwd,
|
|
143
169
|
timeout: 10_000,
|
|
170
|
+
maxOutputBytes: MAX_LS_FILES_OUTPUT_BYTES,
|
|
144
171
|
resourceLabel: "git-tracked-ignore",
|
|
145
172
|
});
|
|
173
|
+
if (truncatedByOutputCap(result)) {
|
|
174
|
+
recordTruncatedLsFiles("tracked");
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
146
177
|
if (result.error || result.status !== 0)
|
|
147
178
|
return undefined;
|
|
148
179
|
return parseTrackedFilesOutput(result.stdout, cwd);
|
|
@@ -62,4 +62,43 @@ export class GoClient {
|
|
|
62
62
|
isGoFile(filePath) {
|
|
63
63
|
return path.extname(filePath).toLowerCase() === ".go";
|
|
64
64
|
}
|
|
65
|
+
/** Forget the memoized go path and latched availability verdict — #2455. */
|
|
66
|
+
resetAvailability() {
|
|
67
|
+
this.availability.reset();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The one `GoClient` this process owns (#2455 fix round 4, F2).
|
|
72
|
+
*
|
|
73
|
+
* `GoClient` is not stateless: it wraps a `createToolchainAvailability` latch
|
|
74
|
+
* whose probe-class "missing" verdict never expires (`isLatchingOutcome`), and
|
|
75
|
+
* `resetGoAvailability` below re-arms exactly ONE such latch. Round 2 shipped
|
|
76
|
+
* with two instances — `dispatch/runners/go-vet.ts` built one for the runner
|
|
77
|
+
* and `bootstrap.ts` built another for `BootstrapClients.goClient`, which is
|
|
78
|
+
* the object `handleSessionStart` reads for its "Active tools" line. The
|
|
79
|
+
* session reset cleared the runner's copy and left the session-start copy
|
|
80
|
+
* latched, so a go toolchain installed between sessions stayed invisible on
|
|
81
|
+
* the surface a user actually sees. One instance, in the module that owns the
|
|
82
|
+
* class, is what makes the reset total; `tests/clients/toolchain-client-singleton.test.ts`
|
|
83
|
+
* holds the line.
|
|
84
|
+
*/
|
|
85
|
+
export const goClient = new GoClient();
|
|
86
|
+
/**
|
|
87
|
+
* Forget `goClient`'s memoized go path and latched availability verdict —
|
|
88
|
+
* #2455. Same #1496/#1535 shape as `resetZizmorTokenAvailability`; wired into
|
|
89
|
+
* `handleSessionStart` beside it (`clients/runtime-session.ts`) so a go
|
|
90
|
+
* toolchain installed mid-process is observed by the next session instead of
|
|
91
|
+
* staying "missing" for the rest of the process's life.
|
|
92
|
+
*
|
|
93
|
+
* This module — not `go-vet.ts` — because the reset must sit beside the single
|
|
94
|
+
* instance it clears (#2455 fix round 4, F2). It was invisible to the
|
|
95
|
+
* session-state sweep before round 2, but not for the reason an earlier draft
|
|
96
|
+
* of this comment gave (#2455 fix round 3, F4): the sweep skips any file
|
|
97
|
+
* exporting no reset at all, so widening the container predicate to "any class
|
|
98
|
+
* declared in `clients/`" could not have surfaced it. Adding this reset is
|
|
99
|
+
* what made the file visible — the fix preceded the detection, and the
|
|
100
|
+
* pair-with-reset blind spot (MISS 3 in `SWEEP_HEURISTIC_LIMITS`) is unchanged.
|
|
101
|
+
*/
|
|
102
|
+
export function resetGoAvailability() {
|
|
103
|
+
goClient.resetAvailability();
|
|
65
104
|
}
|
|
@@ -110,6 +110,7 @@ export class GovulncheckClient extends SecurityScanClient {
|
|
|
110
110
|
hostStallMs: goHostStallMs,
|
|
111
111
|
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
112
112
|
budgetMs: 5000,
|
|
113
|
+
classifiedBy: "probe",
|
|
113
114
|
});
|
|
114
115
|
return false;
|
|
115
116
|
}
|
|
@@ -188,8 +189,10 @@ export class GovulncheckClient extends SecurityScanClient {
|
|
|
188
189
|
// row says so and carries the install facts behind it. Every retry
|
|
189
190
|
// DID run a `go install` that failed, so `install: "failed"` is
|
|
190
191
|
// earned, and the reason names the ceiling rather than the spawn.
|
|
192
|
+
// Below the ceiling, outcome/cause are fresh off `classifyProbeFailure`
|
|
193
|
+
// above, so that row is still `"probe"` (#2209).
|
|
194
|
+
classifiedBy: exhausted ? "caller" : "probe",
|
|
191
195
|
...(exhausted && {
|
|
192
|
-
classifiedBy: "caller",
|
|
193
196
|
evidence: {
|
|
194
197
|
...describeProbeEvidence(install, "go install"),
|
|
195
198
|
install: "failed",
|
|
@@ -313,6 +316,7 @@ export class GovulncheckClient extends SecurityScanClient {
|
|
|
313
316
|
hostStallMs: reprobeHostStallMs,
|
|
314
317
|
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
315
318
|
budgetMs: 5000,
|
|
319
|
+
classifiedBy: "probe",
|
|
316
320
|
});
|
|
317
321
|
return false;
|
|
318
322
|
}
|