pi-lens 4.1.1 → 4.1.3
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 +416 -1
- package/config/dependency-cruiser-eager-allowlist.json +79 -0
- package/dist/clients/actionable-warnings-logger.js +18 -2
- package/dist/clients/actionable-warnings.js +14 -5
- package/dist/clients/advisory-provenance.js +48 -18
- package/dist/clients/ast-grep-client.js +14 -9
- package/dist/clients/atomic-write-staging.js +16 -4
- package/dist/clients/availability-probe-flight.js +12 -0
- package/dist/clients/bash-file-access.js +3 -1
- package/dist/clients/biome-client.js +8 -3
- package/dist/clients/blocker-freshness.js +41 -8
- package/dist/clients/bounded-cache.js +24 -8
- package/dist/clients/bounded-pid-file-lock.js +4 -3
- package/dist/clients/bounded-telemetry.js +56 -19
- package/dist/clients/build-identity.js +95 -0
- package/dist/clients/bus-publish.js +3 -0
- package/dist/clients/cache/rule-cache.js +2 -1
- package/dist/clients/cache-manager.js +6 -7
- package/dist/clients/cache-observability.js +463 -67
- package/dist/clients/call-graph.js +238 -63
- package/dist/clients/cascade-logger.js +15 -1
- package/dist/clients/codebase-model.js +13 -5
- package/dist/clients/complexity-client.js +1 -3
- package/dist/clients/dead-code-client.js +33 -19
- package/dist/clients/dead-code-logger.js +1 -2
- package/dist/clients/deadline-utils.js +3 -1
- package/dist/clients/debug-heap.js +8 -1
- package/dist/clients/degradation-ledger.js +57 -5
- package/dist/clients/demoted-finding-render.js +14 -0
- package/dist/clients/dependency-checker.js +31 -10
- package/dist/clients/deps/minimatch.js +1 -1
- package/dist/clients/diagnostic-dispositions.js +39 -2
- package/dist/clients/diagnostic-line-freshness.js +5 -1
- package/dist/clients/dispatch/auxiliary-lsp.js +2 -3
- package/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/dist/clients/dispatch/dispatcher.js +163 -10
- package/dist/clients/dispatch/fact-store.js +231 -9
- package/dist/clients/dispatch/facts/function-facts.js +3 -1
- package/dist/clients/dispatch/facts/import-facts.js +2 -1
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +1 -1
- package/dist/clients/dispatch/integration.js +214 -104
- package/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/dist/clients/dispatch/plan.js +21 -5
- package/dist/clients/dispatch/rules/error-swallowing.js +3 -1
- package/dist/clients/dispatch/rules/pass-through-wrappers.js +6 -2
- package/dist/clients/dispatch/rules/unsafe-boundary.js +31 -7
- package/dist/clients/dispatch/runners/actionlint.js +5 -2
- package/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
- package/dist/clients/dispatch/runners/biome-check.js +10 -14
- package/dist/clients/dispatch/runners/cpp-check.js +5 -1
- package/dist/clients/dispatch/runners/dotnet-build.js +1 -1
- package/dist/clients/dispatch/runners/elixir-check.js +3 -1
- package/dist/clients/dispatch/runners/eslint.js +30 -7
- package/dist/clients/dispatch/runners/fish-indent.js +1 -1
- package/dist/clients/dispatch/runners/gleam-check.js +1 -1
- package/dist/clients/dispatch/runners/golangci-lint.js +3 -1
- package/dist/clients/dispatch/runners/hadolint.js +13 -8
- package/dist/clients/dispatch/runners/helm-lint.js +20 -4
- package/dist/clients/dispatch/runners/helm-render.js +54 -9
- package/dist/clients/dispatch/runners/htmlhint.js +25 -10
- package/dist/clients/dispatch/runners/index.js +1 -3
- package/dist/clients/dispatch/runners/javac.js +1 -1
- package/dist/clients/dispatch/runners/ktlint.js +1 -1
- package/dist/clients/dispatch/runners/lsp.js +38 -2
- package/dist/clients/dispatch/runners/markdownlint.js +32 -5
- package/dist/clients/dispatch/runners/mypy.js +6 -8
- package/dist/clients/dispatch/runners/oxlint.js +219 -11
- package/dist/clients/dispatch/runners/php-lint.js +1 -1
- package/dist/clients/dispatch/runners/phpstan.js +8 -4
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +14 -6
- package/dist/clients/dispatch/runners/pyright.js +23 -14
- package/dist/clients/dispatch/runners/rust-clippy.js +9 -4
- package/dist/clients/dispatch/runners/shellcheck.js +7 -9
- package/dist/clients/dispatch/runners/shfmt.js +5 -2
- package/dist/clients/dispatch/runners/sqlfluff.js +7 -7
- package/dist/clients/dispatch/runners/stylelint.js +6 -8
- package/dist/clients/dispatch/runners/terragrunt.js +13 -8
- package/dist/clients/dispatch/runners/tflint.js +13 -8
- package/dist/clients/dispatch/runners/tree-sitter.js +17 -3
- package/dist/clients/dispatch/runners/utils/candidate-probe.js +7 -1
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +100 -26
- package/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/dist/clients/dispatch/runners/utils/tool-failure.js +54 -7
- package/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +52 -20
- package/dist/clients/dispatch/runners/yamllint.js +6 -8
- package/dist/clients/dispatch/runners/zig-check.js +1 -1
- package/dist/clients/dispatch/suppress-writer.js +2 -1
- package/dist/clients/dispatch/types.js +6 -1
- package/dist/clients/dispatch/utils/lsp-diagnostics.js +6 -2
- package/dist/clients/event-loop-monitor.js +66 -1
- package/dist/clients/extension-log.js +5 -3
- package/dist/clients/feature-hints.js +8 -1
- package/dist/clients/file-kinds.js +34 -129
- package/dist/clients/file-utils.js +174 -8
- package/dist/clients/finding-delivery-gate.js +62 -2
- package/dist/clients/formatters.js +169 -27
- package/dist/clients/freshness.js +38 -0
- package/dist/clients/git-guard.js +233 -53
- package/dist/clients/git-tracked-ignore.js +32 -1
- package/dist/clients/go-client.js +1 -3
- package/dist/clients/govulncheck-client.js +5 -1
- package/dist/clients/grammar-source.js +1 -1
- package/dist/clients/gzip-stage-write.js +37 -4
- package/dist/clients/host-edit-normalize.js +1 -3
- package/dist/clients/install-diagnostics.js +4 -2
- package/dist/clients/installer/index.js +211 -105
- package/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/dist/clients/instance-reaper.js +9 -15
- package/dist/clients/instance-registry-lock.js +145 -0
- package/dist/clients/instance-registry.js +436 -124
- package/dist/clients/jscpd-client.js +12 -2
- package/dist/clients/knip-client.js +29 -15
- package/dist/clients/language-profile.js +8 -3
- package/dist/clients/latency-logger.js +32 -3
- package/dist/clients/lens-config.js +1 -1
- package/dist/clients/lens-flag-registry.js +8 -0
- package/dist/clients/lens-map.js +11 -4
- package/dist/clients/live-bus-emitter.js +1 -1
- package/dist/clients/lsp/cascade-tier.js +4 -0
- package/dist/clients/lsp/client.js +516 -31
- package/dist/clients/lsp/config.js +29 -8
- package/dist/clients/lsp/diagnostic-binding.js +9 -4
- package/dist/clients/lsp/edits.js +119 -32
- package/dist/clients/lsp/index.js +595 -209
- package/dist/clients/lsp/inferred-project.js +2 -1
- package/dist/clients/lsp/jvm-runtime.js +9 -1
- package/dist/clients/lsp/launch.js +47 -11
- package/dist/clients/lsp/pending-aux-coverage.js +264 -0
- package/dist/clients/lsp/server.js +161 -20
- package/dist/clients/lsp/session-roots.js +99 -0
- package/dist/clients/lsp/tsserver-sync.js +10 -2
- package/dist/clients/lsp/workspace-diagnostics-cache.js +25 -10
- package/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/dist/clients/lsp-document-symbols.js +1 -3
- package/dist/clients/lsp-mutation.js +26 -22
- package/dist/clients/mcp/analyze.js +12 -6
- package/dist/clients/mcp/host-shim.js +1 -1
- package/dist/clients/mcp/session.js +14 -1
- package/dist/clients/memory-sampler.js +96 -8
- package/dist/clients/message-end-attribution.js +30 -0
- package/dist/clients/metrics-client.js +1 -3
- package/dist/clients/metrics-history.js +21 -4
- package/dist/clients/middle-man-analysis.js +8 -3
- package/dist/clients/module-report-lsp.js +7 -1
- package/dist/clients/module-report.js +14 -17
- package/dist/clients/ndjson-logger.js +3 -5
- package/dist/clients/opaque-mutation-scan.js +434 -0
- package/dist/clients/package-manager.js +12 -19
- package/dist/clients/path-keyed-map.js +14 -1
- package/dist/clients/path-utils.js +32 -0
- package/dist/clients/pipeline.js +49 -9
- package/dist/clients/process-singletons.js +210 -0
- package/dist/clients/project-conventions.js +2 -8
- package/dist/clients/project-diagnostics/cache.js +10 -7
- package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +2 -1
- package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +3 -1
- package/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +13 -9
- package/dist/clients/project-diagnostics/runner-adapters/trivy.js +3 -1
- package/dist/clients/project-diagnostics/scanner.js +15 -6
- package/dist/clients/project-lens-config.js +12 -5
- package/dist/clients/project-snapshot-fingerprint.js +48 -0
- package/dist/clients/project-snapshot-persist-worker.js +15 -1
- package/dist/clients/project-snapshot.js +505 -77
- package/dist/clients/read-expansion.js +12 -2
- package/dist/clients/read-guard-logger.js +29 -6
- package/dist/clients/read-guard-tool-lines.js +5 -2
- package/dist/clients/read-guard.js +75 -8
- package/dist/clients/recent-touches.js +17 -5
- package/dist/clients/resource-sampler.js +1 -1
- package/dist/clients/reverse-deps.js +6 -5
- package/dist/clients/review-graph/builder.js +680 -78
- package/dist/clients/review-graph/git-identity.js +8 -2
- package/dist/clients/review-graph/import-resolvers.js +10 -1
- package/dist/clients/review-graph/query.js +2 -1
- package/dist/clients/review-graph/tsconfig-paths.js +9 -3
- package/dist/clients/review-graph/workspace-modules.js +2 -2
- package/dist/clients/review-graph-logger.js +21 -1
- package/dist/clients/ruff-client.js +6 -5
- package/dist/clients/runtime-agent-end.js +63 -31
- package/dist/clients/runtime-context.js +12 -4
- package/dist/clients/runtime-coordinator.js +151 -4
- package/dist/clients/runtime-session.js +44 -12
- package/dist/clients/runtime-tool-call.js +74 -3
- package/dist/clients/runtime-tool-result.js +250 -37
- package/dist/clients/runtime-turn.js +373 -17
- package/dist/clients/rust-client.js +1 -3
- package/dist/clients/safe-spawn.js +308 -29
- package/dist/clients/security-scan-client.js +49 -20
- package/dist/clients/session-lifecycle.js +238 -42
- package/dist/clients/session-summary.js +3 -1
- package/dist/clients/sg-runner.js +28 -18
- package/dist/clients/shared-checkout-guard.js +338 -0
- package/dist/clients/source-filter.js +10 -6
- package/dist/clients/source-walker.js +1 -1
- package/dist/clients/spawn-output-cap.js +37 -0
- package/dist/clients/spawn-timeout-cooldown.js +110 -0
- package/dist/clients/startup-scan.js +10 -8
- package/dist/clients/startup-timing.js +69 -4
- package/dist/clients/string-utils.js +20 -0
- package/dist/clients/test-runner-client.js +419 -88
- package/dist/clients/tool-definition.js +7 -0
- package/dist/clients/tool-policy.js +27 -12
- package/dist/clients/tool-render.js +2 -1
- package/dist/clients/tree-sitter-client.js +21 -9
- package/dist/clients/tree-sitter-logger.js +19 -3
- package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
- package/dist/clients/warm-attach.js +19 -14
- package/dist/clients/widget-state.js +27 -11
- package/dist/clients/word-index-logger.js +13 -1
- package/dist/clients/word-index-store.js +448 -0
- package/dist/clients/word-index.js +585 -105
- package/dist/clients/workspace-topology.js +25 -2
- package/dist/clients/zizmor-config.js +34 -6
- package/dist/i18n.js +13 -5
- package/dist/index.js +19617 -12855
- package/dist/mcp/build-staleness.js +8 -7
- package/dist/mcp/cli.js +3 -1
- package/dist/mcp/server.js +9 -2
- package/dist/tools/ast-grep-replace.js +68 -12
- package/dist/tools/lens-diagnostic-mark.js +21 -7
- package/dist/tools/lens-diagnostics.js +168 -35
- package/dist/tools/lsp-diagnostics.js +38 -12
- package/dist/tools/lsp-navigation.js +6 -2
- package/dist/tools/module-report.js +1 -1
- package/dist/tools/project-report.js +1 -1
- package/dist/tools/symbol-search.js +5 -2
- package/docs/agent-guide.md +4 -2
- package/docs/ast-grep_rules_catalog.md +41 -10
- package/docs/astplayground.md +26 -17
- package/docs/durable-store-audit-1202.md +1 -1
- package/docs/servercapabilities.md +1 -1
- package/package.json +164 -156
- 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/typos/_typos.toml +2 -2
- package/scripts/analyze-pi-lens-logs.mjs +31 -13
- package/scripts/download-grammars.js +9 -3
- package/scripts/rpc-load-check.mjs +30 -9
|
@@ -2,20 +2,36 @@ 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 { normalizeFilePath } from "./path-utils.js";
|
|
5
6
|
const AW_LOG_DIR = getGlobalPiLensDir();
|
|
6
7
|
const AW_LOG_FILE = path.join(AW_LOG_DIR, "actionable-warnings.log");
|
|
7
8
|
const AW_LOG_BACKUP_FILE = path.join(AW_LOG_DIR, "actionable-warnings.log.1");
|
|
8
|
-
const MAX_LOG_BYTES = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_AW_LOG_MAX_BYTES ?? "1048576", 10) ||
|
|
9
|
+
const MAX_LOG_BYTES = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_AW_LOG_MAX_BYTES ?? "1048576", 10) ||
|
|
10
|
+
1048576);
|
|
9
11
|
const writer = createNdjsonLogger({
|
|
10
12
|
filePath: AW_LOG_FILE,
|
|
11
13
|
maxBytes: MAX_LOG_BYTES,
|
|
12
14
|
backupPath: AW_LOG_BACKUP_FILE,
|
|
13
15
|
});
|
|
16
|
+
/**
|
|
17
|
+
* #2219 (the #2141 class): `filePath` reaches here from
|
|
18
|
+
* `actionable-warnings.ts`'s raw `path.resolve(cwd, file)` — the file
|
|
19
|
+
* imports `normalizeMapKey` and uses it for map lookups on this same value,
|
|
20
|
+
* but never normalizes what gets logged. `filePath` is optional (several
|
|
21
|
+
* events, e.g. `report_started`/`report_complete`, carry none), so only
|
|
22
|
+
* normalize when present.
|
|
23
|
+
*/
|
|
14
24
|
export function logActionableWarningsEvent(entry) {
|
|
15
25
|
if (isTestMode()) {
|
|
16
26
|
return;
|
|
17
27
|
}
|
|
18
|
-
writer.log({
|
|
28
|
+
writer.log({
|
|
29
|
+
ts: new Date().toISOString(),
|
|
30
|
+
...entry,
|
|
31
|
+
...(entry.filePath !== undefined
|
|
32
|
+
? { filePath: normalizeFilePath(entry.filePath) }
|
|
33
|
+
: {}),
|
|
34
|
+
});
|
|
19
35
|
}
|
|
20
36
|
export function getActionableWarningsLogPath() {
|
|
21
37
|
return AW_LOG_FILE;
|
|
@@ -633,7 +633,12 @@ export async function applyConservativeActionableWarningFixes(args) {
|
|
|
633
633
|
}
|
|
634
634
|
const edit = selected.edit;
|
|
635
635
|
const applied = await applyWorkspaceEdit(edit, args.cwd, args.mutationContext
|
|
636
|
-
? {
|
|
636
|
+
? {
|
|
637
|
+
mutationContext: {
|
|
638
|
+
...args.mutationContext,
|
|
639
|
+
emitSummary: false,
|
|
640
|
+
},
|
|
641
|
+
}
|
|
637
642
|
: undefined);
|
|
638
643
|
appliedResults.push(applied);
|
|
639
644
|
for (const changedFile of applied.files)
|
|
@@ -642,8 +647,7 @@ export async function applyConservativeActionableWarningFixes(args) {
|
|
|
642
647
|
}
|
|
643
648
|
catch (err) {
|
|
644
649
|
failedCount++;
|
|
645
|
-
const partial = err
|
|
646
|
-
.appliedWorkspaceEdit;
|
|
650
|
+
const partial = err.appliedWorkspaceEdit;
|
|
647
651
|
if (partial) {
|
|
648
652
|
appliedResults.push(partial);
|
|
649
653
|
for (const changedFile of partial.files)
|
|
@@ -656,13 +660,18 @@ export async function applyConservativeActionableWarningFixes(args) {
|
|
|
656
660
|
}
|
|
657
661
|
}
|
|
658
662
|
summary.changedFiles = [...changedFiles];
|
|
659
|
-
if (args.mutationContext &&
|
|
663
|
+
if (args.mutationContext &&
|
|
664
|
+
(summary.considered > 0 || appliedResults.length > 0)) {
|
|
660
665
|
recordLspMutationBatch(args.mutationContext, {
|
|
661
666
|
results: appliedResults,
|
|
662
667
|
considered: summary.considered,
|
|
663
668
|
completed: summary.applied,
|
|
664
669
|
failedCount,
|
|
665
|
-
status: failedCount > 0
|
|
670
|
+
status: failedCount > 0
|
|
671
|
+
? "failed"
|
|
672
|
+
: appliedResults.length > 0
|
|
673
|
+
? "success"
|
|
674
|
+
: "skipped",
|
|
666
675
|
bookkeep: false,
|
|
667
676
|
});
|
|
668
677
|
}
|
|
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { logLatency } from "./latency-logger.js";
|
|
5
5
|
import { normalizeMapKey, toProjectRelativePath } from "./path-utils.js";
|
|
6
6
|
import { resolveRunnerPath } from "./dispatch/runner-context.js";
|
|
7
|
-
import {
|
|
7
|
+
import { freshnessFromMtime } from "./freshness.js";
|
|
8
8
|
export const MAX_ADVISORY_AFFECTED_FILES = 256;
|
|
9
9
|
export function advisoryPathKey(filePath, cwd) {
|
|
10
10
|
return normalizeMapKey(path.resolve(cwd, filePath));
|
|
@@ -68,25 +68,41 @@ function isFiniteNumber(value) {
|
|
|
68
68
|
return typeof value === "number" && Number.isFinite(value);
|
|
69
69
|
}
|
|
70
70
|
function isCapturedHash(value) {
|
|
71
|
-
return typeof value === "string" &&
|
|
72
|
-
(/^[a-f0-9]{64}$/.test(value) ||
|
|
71
|
+
return (typeof value === "string" &&
|
|
72
|
+
(/^[a-f0-9]{64}$/.test(value) ||
|
|
73
|
+
value === "missing" ||
|
|
74
|
+
value.startsWith("unreadable:")));
|
|
73
75
|
}
|
|
74
76
|
function isWellFormed(value) {
|
|
75
77
|
if (!value || typeof value !== "object")
|
|
76
78
|
return false;
|
|
77
79
|
const record = value;
|
|
78
80
|
const revision = record.revision;
|
|
79
|
-
return !!revision &&
|
|
80
|
-
|
|
81
|
-
isFiniteNumber(revision.
|
|
82
|
-
|
|
83
|
-
(
|
|
84
|
-
isFiniteNumber(
|
|
85
|
-
|
|
81
|
+
return (!!revision &&
|
|
82
|
+
typeof revision.sessionId === "string" &&
|
|
83
|
+
isFiniteNumber(revision.projectSeq) &&
|
|
84
|
+
isFiniteNumber(revision.turnIndex) &&
|
|
85
|
+
isFiniteNumber(revision.generation) &&
|
|
86
|
+
isFiniteNumber(revision.capturedAt) &&
|
|
87
|
+
Array.isArray(record.files) &&
|
|
88
|
+
record.files.length > 0 &&
|
|
89
|
+
record.files.every((file) => !!file &&
|
|
90
|
+
typeof file.path === "string" &&
|
|
91
|
+
(file.role === "source" ||
|
|
92
|
+
file.role === "test" ||
|
|
93
|
+
file.role === "affected") &&
|
|
94
|
+
isFiniteNumber(file.mtimeMs) &&
|
|
95
|
+
isFiniteNumber(file.size) &&
|
|
96
|
+
isCapturedHash(file.sha256)));
|
|
86
97
|
}
|
|
87
98
|
export function validateAdvisoryProvenance(record, cwd, runtime) {
|
|
88
99
|
if (!isWellFormed(record.provenance)) {
|
|
89
|
-
return {
|
|
100
|
+
return {
|
|
101
|
+
status: "unknown",
|
|
102
|
+
reasons: ["malformed-or-legacy-provenance"],
|
|
103
|
+
allFilesDeleted: false,
|
|
104
|
+
changedPathCount: 0,
|
|
105
|
+
};
|
|
90
106
|
}
|
|
91
107
|
const provenance = record.provenance;
|
|
92
108
|
const reasons = [];
|
|
@@ -149,9 +165,19 @@ export function validateAdvisoryProvenance(record, cwd, runtime) {
|
|
|
149
165
|
}
|
|
150
166
|
const allFilesDeleted = deletedFiles === provenance.files.length;
|
|
151
167
|
if (unknown)
|
|
152
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
status: "unknown",
|
|
170
|
+
reasons,
|
|
171
|
+
allFilesDeleted,
|
|
172
|
+
changedPathCount: changedPaths.size,
|
|
173
|
+
};
|
|
153
174
|
return reasons.length > 0
|
|
154
|
-
? {
|
|
175
|
+
? {
|
|
176
|
+
status: "superseded",
|
|
177
|
+
reasons,
|
|
178
|
+
allFilesDeleted,
|
|
179
|
+
changedPathCount: changedPaths.size,
|
|
180
|
+
}
|
|
155
181
|
: { status: "current", reasons: [], allFilesDeleted, changedPathCount: 0 };
|
|
156
182
|
}
|
|
157
183
|
// ── Finding-cited path existence (#1461 slice 1) ──────────────────────────────
|
|
@@ -224,11 +250,15 @@ const MAX_LOGGED_DEAD_PATHS = 3;
|
|
|
224
250
|
export function findingPathFreshness(resolvedPath, scannedAtMs) {
|
|
225
251
|
try {
|
|
226
252
|
const stat = fs.statSync(resolvedPath);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
253
|
+
// No scan timestamp recorded: the pre-kernel behavior treated the
|
|
254
|
+
// path as live (no reference to be stale against) - preserved.
|
|
255
|
+
if (scannedAtMs === undefined)
|
|
256
|
+
return "live";
|
|
257
|
+
const verdict = freshnessFromMtime({
|
|
258
|
+
mtimeMs: stat.mtimeMs,
|
|
259
|
+
referenceMs: scannedAtMs,
|
|
260
|
+
});
|
|
261
|
+
return verdict.verdict === "stale" ? "stale" : "live";
|
|
232
262
|
}
|
|
233
263
|
catch (error) {
|
|
234
264
|
const code = error.code ?? "unknown";
|
|
@@ -13,6 +13,7 @@ import * as os from "node:os";
|
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import { AstGrepRuleManager } from "./ast-grep-rule-manager.js";
|
|
15
15
|
import { resolvePackagePath } from "./package-root.js";
|
|
16
|
+
import { truncatedByOutputCap } from "./spawn-output-cap.js";
|
|
16
17
|
import { SgRunner, } from "./sg-runner.js";
|
|
17
18
|
// --- Client ---
|
|
18
19
|
function extractDebugAst(raw) {
|
|
@@ -113,9 +114,7 @@ export class AstGrepClient {
|
|
|
113
114
|
(fs.existsSync(projectRuleDir)
|
|
114
115
|
? projectRuleDir
|
|
115
116
|
: resolvePackagePath(import.meta.url, "rules"));
|
|
116
|
-
this.log = verbose
|
|
117
|
-
? createSubsystemLogger("ast-grep")
|
|
118
|
-
: () => { };
|
|
117
|
+
this.log = verbose ? createSubsystemLogger("ast-grep") : () => { };
|
|
119
118
|
this.ruleManager = new AstGrepRuleManager(this.ruleDir, this.log);
|
|
120
119
|
this.runner = new SgRunner(verbose);
|
|
121
120
|
}
|
|
@@ -265,6 +264,18 @@ export class AstGrepClient {
|
|
|
265
264
|
const result = await this.runner.execRaw(args, 10_000, options);
|
|
266
265
|
const stderr = result.stderr.trim();
|
|
267
266
|
const stdout = result.stdout.trim();
|
|
267
|
+
// FIRST (#2100): hitting `execRaw`'s cap SIGTERMs ast-grep, so a
|
|
268
|
+
// truncated run also carries the kill's error message. Read after the
|
|
269
|
+
// two checks below, this guard could only ever describe a run that
|
|
270
|
+
// exited before the signal reached it. `execRaw` re-spells `failure`
|
|
271
|
+
// in its own vocabulary but keeps the timeout/aborted spellings, so
|
|
272
|
+
// those runs still fall through to the error branch below.
|
|
273
|
+
if (truncatedByOutputCap(result)) {
|
|
274
|
+
return {
|
|
275
|
+
valid: false,
|
|
276
|
+
error: "ast-grep validation output was truncated",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
268
279
|
if (result.error)
|
|
269
280
|
return { valid: false, error: result.error };
|
|
270
281
|
if (result.status !== 0 && !(result.status === 1 && !stderr)) {
|
|
@@ -274,12 +285,6 @@ export class AstGrepClient {
|
|
|
274
285
|
`ast-grep validation failed with exit code ${result.status}`,
|
|
275
286
|
};
|
|
276
287
|
}
|
|
277
|
-
if (result.outputTruncated) {
|
|
278
|
-
return {
|
|
279
|
-
valid: false,
|
|
280
|
-
error: "ast-grep validation output was truncated",
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
288
|
if (stderrHasError(stderr))
|
|
284
289
|
return { valid: false, error: stderr };
|
|
285
290
|
const warning = stderr || stdout || undefined;
|
|
@@ -19,7 +19,13 @@ export function stageOwnerPidFromName(name) {
|
|
|
19
19
|
}
|
|
20
20
|
export async function sweepOwnStagingFiles(directories, options) {
|
|
21
21
|
const uniqueDirectories = [...new Set(directories)].filter(Boolean);
|
|
22
|
-
const result = {
|
|
22
|
+
const result = {
|
|
23
|
+
directories: uniqueDirectories.length,
|
|
24
|
+
scanned: 0,
|
|
25
|
+
removed: 0,
|
|
26
|
+
liveSkipped: 0,
|
|
27
|
+
truncated: false,
|
|
28
|
+
};
|
|
23
29
|
for (const directory of uniqueDirectories) {
|
|
24
30
|
let handle;
|
|
25
31
|
try {
|
|
@@ -47,14 +53,20 @@ export async function sweepOwnStagingFiles(directories, options) {
|
|
|
47
53
|
continue;
|
|
48
54
|
}
|
|
49
55
|
try {
|
|
50
|
-
await fs.promises.rm(path.join(directory, entry.name), {
|
|
56
|
+
await fs.promises.rm(path.join(directory, entry.name), {
|
|
57
|
+
force: true,
|
|
58
|
+
});
|
|
51
59
|
result.removed++;
|
|
52
60
|
}
|
|
53
|
-
catch {
|
|
61
|
+
catch {
|
|
62
|
+
/* leave it for the next bounded sweep */
|
|
63
|
+
}
|
|
54
64
|
}
|
|
55
65
|
result.truncated ||= reachedEntryCap;
|
|
56
66
|
}
|
|
57
|
-
catch {
|
|
67
|
+
catch {
|
|
68
|
+
/* best-effort directory scan */
|
|
69
|
+
}
|
|
58
70
|
finally {
|
|
59
71
|
await handle.close().catch(() => { });
|
|
60
72
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createSingleFlight } from "./single-flight.js";
|
|
2
|
+
/** Create a flight registry whose lifetime belongs to the caller. */
|
|
3
|
+
export function createAvailabilityProbeFlight(options = {}) {
|
|
4
|
+
const flights = createSingleFlight(options);
|
|
5
|
+
return {
|
|
6
|
+
run(key, probe) {
|
|
7
|
+
const joined = flights.has(key);
|
|
8
|
+
return { promise: flights.run(key, probe), joined };
|
|
9
|
+
},
|
|
10
|
+
clear: () => flights.clear(),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -634,7 +634,9 @@ function collectGrepCommandFiles(command, cwd) {
|
|
|
634
634
|
return {
|
|
635
635
|
hasLineNumberGrep,
|
|
636
636
|
files,
|
|
637
|
-
context: truncated
|
|
637
|
+
context: truncated
|
|
638
|
+
? { before: 0, after: 0 }
|
|
639
|
+
: (context ?? { before: 0, after: 0 }),
|
|
638
640
|
};
|
|
639
641
|
}
|
|
640
642
|
function dedupePushSearchRead(out, seen, loc) {
|
|
@@ -58,9 +58,7 @@ export class BiomeClient {
|
|
|
58
58
|
ensureFlight = createSingleFlight();
|
|
59
59
|
log;
|
|
60
60
|
constructor(verbose = false) {
|
|
61
|
-
this.log = verbose
|
|
62
|
-
? createSubsystemLogger("biome")
|
|
63
|
-
: () => { };
|
|
61
|
+
this.log = verbose ? createSubsystemLogger("biome") : () => { };
|
|
64
62
|
}
|
|
65
63
|
/**
|
|
66
64
|
* Resolve the fastest available biome binary for `cwd`.
|
|
@@ -181,6 +179,7 @@ export class BiomeClient {
|
|
|
181
179
|
latched: true,
|
|
182
180
|
hostStallMs,
|
|
183
181
|
budgetMs: PROBE_TIMEOUT_MS,
|
|
182
|
+
classifiedBy: "probe",
|
|
184
183
|
});
|
|
185
184
|
return { outcome: "success", value: true };
|
|
186
185
|
}
|
|
@@ -227,6 +226,12 @@ export class BiomeClient {
|
|
|
227
226
|
latched: true,
|
|
228
227
|
hostStallMs: this.lastProbeHostStallMs,
|
|
229
228
|
budgetMs: PROBE_TIMEOUT_MS,
|
|
229
|
+
classifiedBy: "caller",
|
|
230
|
+
evidence: {
|
|
231
|
+
install: "succeeded",
|
|
232
|
+
binary: "biome",
|
|
233
|
+
source: "managed-dir",
|
|
234
|
+
},
|
|
230
235
|
});
|
|
231
236
|
}
|
|
232
237
|
return true;
|
|
@@ -43,12 +43,32 @@
|
|
|
43
43
|
* runtime condition — that edge is invisible to this gate, so a blocker whose only
|
|
44
44
|
* stale dependency arrives that way survives a replay. This is an honest boundary,
|
|
45
45
|
* not a silently-closed one.
|
|
46
|
+
*
|
|
47
|
+
* Delivery cap (#1950). A demoted-but-confirmable entry (`alreadyStale` above)
|
|
48
|
+
* is deliberately NOT retired after its past-EOF sibling's one-delivery rule
|
|
49
|
+
* (#1944, `blocker-past-eof.ts`) — its coordinates are still in bounds, so a
|
|
50
|
+
* fresh dispatch can genuinely confirm or clear it, and retiring on delivery
|
|
51
|
+
* one would discard a recoverable finding. But nothing capped how many times
|
|
52
|
+
* the SAME demoted record re-serves: incident data (#1944's lane sweep) showed
|
|
53
|
+
* 18 `alreadyStale` re-serves in one window, with repeat deliveries carrying
|
|
54
|
+
* near-zero information after the first. `runtime-turn.ts` counts each
|
|
55
|
+
* delivery (`InlineBlockerRecord.staleDeliveryCount`) and retires the record
|
|
56
|
+
* via `RuntimeCoordinator.retireDemotedDependencyDriftBlocker` once it reaches
|
|
57
|
+
* `DEPENDENCY_DRIFT_MAX_DELIVERIES` — a count, not a TTL or a session
|
|
58
|
+
* boundary, because "how many times has the agent already been told" is the
|
|
59
|
+
* quantity that actually went to zero information, not "how much time has
|
|
60
|
+
* passed". The ledger reason says "capped, re-run can still confirm" so it
|
|
61
|
+
* reads distinctly from #1944's "retired, unrecoverable" — a capped record
|
|
62
|
+
* COULD still resolve a fresh dispatch; it just stopped being handed one for
|
|
63
|
+
* free.
|
|
46
64
|
*/
|
|
47
65
|
import * as fs from "node:fs";
|
|
48
66
|
import { normalizeEphemeralMapKey } from "./path-utils.js";
|
|
49
67
|
import { resolveImportToFiles } from "./review-graph/import-resolvers.js";
|
|
50
68
|
import { getSharedTreeSitterClient, resolveTreeSitterLanguage, } from "./tree-sitter-shared.js";
|
|
51
69
|
import { TreeSitterSymbolExtractor } from "./tree-sitter-symbol-extractor.js";
|
|
70
|
+
/** See the module doc's "Delivery cap (#1950)" section above. */
|
|
71
|
+
export const DEPENDENCY_DRIFT_MAX_DELIVERIES = 3;
|
|
52
72
|
/**
|
|
53
73
|
* Upper bound on the number of forward imports stat-checked for a single blocker.
|
|
54
74
|
* Blocking entries are rare, but one file can import many modules; the sweep must
|
|
@@ -126,7 +146,11 @@ async function resolveForwardImportsMemoized(cwd, filePath, resolveForwardImport
|
|
|
126
146
|
catch {
|
|
127
147
|
imports = [];
|
|
128
148
|
}
|
|
129
|
-
importResolutionMemo.set(filePath, {
|
|
149
|
+
importResolutionMemo.set(filePath, {
|
|
150
|
+
mtimeMs: sig.mtimeMs,
|
|
151
|
+
size: sig.size,
|
|
152
|
+
imports,
|
|
153
|
+
});
|
|
130
154
|
return imports;
|
|
131
155
|
}
|
|
132
156
|
/** Test-only: clear the extractor and import-resolution memos. */
|
|
@@ -232,17 +256,26 @@ export async function collectForwardImportMtimes(cwd, filePath, resolveForwardIm
|
|
|
232
256
|
* +50ms). +50ms comfortably clears the measured skew while staying far below the
|
|
233
257
|
* gap between genuinely distinct edits.
|
|
234
258
|
*/
|
|
235
|
-
|
|
259
|
+
// Single implementation lives in the freshness kernel (#1739); this
|
|
260
|
+
// re-export preserves every existing importer's path.
|
|
261
|
+
export { MTIME_DRIFT_TOLERANCE_MS } from "./freshness.js";
|
|
262
|
+
import { freshnessFromMtime } from "./freshness.js";
|
|
236
263
|
async function detectDrift(cwd, filePath, recordedAtMs, resolveForwardImports, turnIndex) {
|
|
237
264
|
const drifted = [];
|
|
238
|
-
const
|
|
239
|
-
|
|
265
|
+
const ownFreshness = freshnessFromMtime({
|
|
266
|
+
mtimeMs: await statMtimeMs(filePath),
|
|
267
|
+
referenceMs: recordedAtMs,
|
|
268
|
+
});
|
|
269
|
+
if (ownFreshness.verdict === "stale")
|
|
240
270
|
drifted.push(filePath);
|
|
241
|
-
}
|
|
242
271
|
const { mtimes, truncated } = await collectForwardImportMtimes(cwd, filePath, resolveForwardImports, turnIndex);
|
|
243
|
-
for (const { path, mtimeMs } of mtimes) {
|
|
244
|
-
|
|
245
|
-
|
|
272
|
+
for (const { path: depPath, mtimeMs } of mtimes) {
|
|
273
|
+
const verdict = freshnessFromMtime({
|
|
274
|
+
mtimeMs,
|
|
275
|
+
referenceMs: recordedAtMs,
|
|
276
|
+
});
|
|
277
|
+
if (verdict.verdict === "stale")
|
|
278
|
+
drifted.push(depPath);
|
|
246
279
|
}
|
|
247
280
|
return { drifted, truncated };
|
|
248
281
|
}
|
|
@@ -13,7 +13,9 @@ export class BoundedLruCache {
|
|
|
13
13
|
}
|
|
14
14
|
return value;
|
|
15
15
|
}
|
|
16
|
-
has(key) {
|
|
16
|
+
has(key) {
|
|
17
|
+
return this.entries.has(key);
|
|
18
|
+
}
|
|
17
19
|
set(key, value) {
|
|
18
20
|
this.entries.delete(key);
|
|
19
21
|
this.entries.set(key, value);
|
|
@@ -24,11 +26,25 @@ export class BoundedLruCache {
|
|
|
24
26
|
this.entries.delete(oldest);
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
|
-
delete(key) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
delete(key) {
|
|
30
|
+
return this.entries.delete(key);
|
|
31
|
+
}
|
|
32
|
+
clear() {
|
|
33
|
+
this.entries.clear();
|
|
34
|
+
}
|
|
35
|
+
get size() {
|
|
36
|
+
return this.entries.size;
|
|
37
|
+
}
|
|
38
|
+
keys() {
|
|
39
|
+
return this.entries.keys();
|
|
40
|
+
}
|
|
41
|
+
values() {
|
|
42
|
+
return this.entries.values();
|
|
43
|
+
}
|
|
44
|
+
entriesArray() {
|
|
45
|
+
return [...this.entries.entries()];
|
|
46
|
+
}
|
|
47
|
+
[Symbol.iterator]() {
|
|
48
|
+
return this.entries[Symbol.iterator]();
|
|
49
|
+
}
|
|
34
50
|
}
|
|
@@ -35,8 +35,7 @@ async function releaseQuarantineLock(lockPath, token) {
|
|
|
35
35
|
break;
|
|
36
36
|
}
|
|
37
37
|
catch (error) {
|
|
38
|
-
if (error.code !== "ENOENT" ||
|
|
39
|
-
attempt === 2)
|
|
38
|
+
if (error.code !== "ENOENT" || attempt === 2)
|
|
40
39
|
return;
|
|
41
40
|
await new Promise((resolve) => setImmediate(resolve));
|
|
42
41
|
}
|
|
@@ -111,7 +110,9 @@ async function tryAcquireQuarantineLock(lockPath, staleMs) {
|
|
|
111
110
|
await fsp.writeFile(path.join(lockPath, "owner.json"), JSON.stringify(owner), "utf8");
|
|
112
111
|
}
|
|
113
112
|
catch (error) {
|
|
114
|
-
await fsp
|
|
113
|
+
await fsp
|
|
114
|
+
.rm(lockPath, { recursive: true, force: true })
|
|
115
|
+
.catch(() => { });
|
|
115
116
|
throw error;
|
|
116
117
|
}
|
|
117
118
|
return () => releaseQuarantineLock(lockPath, owner.token);
|
|
@@ -24,10 +24,9 @@
|
|
|
24
24
|
* that must re-arm at `session_start` and will eventually be forgotten
|
|
25
25
|
* (the #1266–#1625 defect class).
|
|
26
26
|
* 2. **A per-turn cap names its turn.** `capPerTurn` carries the caller's
|
|
27
|
-
* `turnIndex` in the same object as the limit
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `noteImportResolutionMemoTurn` (`clients/blocker-freshness.ts`).
|
|
27
|
+
* `turnIndex` in the same object as the limit. A bounded 64-turn window
|
|
28
|
+
* keeps overlapping async turns independent; results older than that window
|
|
29
|
+
* fail closed. Session start clears the window when numbering restarts.
|
|
31
30
|
* 3. **Identity always survives.** Every emitted record carries the
|
|
32
31
|
* discriminating identity in `filePath` and in `metadata.identity`, so
|
|
33
32
|
* aggregation can still answer "which file, which method, which record is
|
|
@@ -85,6 +84,14 @@ export const BOUNDED_TELEMETRY_PHASES = [
|
|
|
85
84
|
* exact count.
|
|
86
85
|
*/
|
|
87
86
|
"lsp_warm_client_missing",
|
|
87
|
+
/** #2052: a file's nearest LSP root is outside the session ceiling. */
|
|
88
|
+
"lsp_capability_skip",
|
|
89
|
+
/**
|
|
90
|
+
* #2044: failed-first test state was retired after a confirmed missing path,
|
|
91
|
+
* retained because the filesystem verdict was indeterminate, or evicted at
|
|
92
|
+
* the state cap. Selection checks and detailed rows are both capped per turn.
|
|
93
|
+
*/
|
|
94
|
+
"test_runner_failed_target_state",
|
|
88
95
|
/**
|
|
89
96
|
* #1723: an event-loop block at or above the floor. Not a degradation, so
|
|
90
97
|
* no ledger kind; bounded by call cadence (one `turn_end` runs it once per
|
|
@@ -97,6 +104,17 @@ export const BOUNDED_TELEMETRY_PHASES = [
|
|
|
97
104
|
* queue of them, so it is rising-edge per event name.
|
|
98
105
|
*/
|
|
99
106
|
"session_event_stale_ctx_skip",
|
|
107
|
+
/**
|
|
108
|
+
* #2007: a worktree-mutating git command declined because another live
|
|
109
|
+
* session shares this dirty checkout. Rising edge per checkout root; the
|
|
110
|
+
* ledger keeps the exact repeat count.
|
|
111
|
+
*/
|
|
112
|
+
"shared_checkout_switch_blocked",
|
|
113
|
+
/**
|
|
114
|
+
* #2007: `git status` could not report the working-tree state, so the same
|
|
115
|
+
* command was declined on an UNKNOWN rather than assumed clean.
|
|
116
|
+
*/
|
|
117
|
+
"shared_checkout_probe_failed",
|
|
100
118
|
];
|
|
101
119
|
const REGISTERED_PHASES = new Set(BOUNDED_TELEMETRY_PHASES);
|
|
102
120
|
/** Report whether a phase name is in the registry (used by the sweep). */
|
|
@@ -104,14 +122,15 @@ export function isBoundedTelemetryPhase(phase) {
|
|
|
104
122
|
return REGISTERED_PHASES.has(phase);
|
|
105
123
|
}
|
|
106
124
|
/**
|
|
107
|
-
* Per-turn admission counters, keyed by phase.
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* count across a session boundary that happened to land on the same index).
|
|
125
|
+
* Per-turn admission counters, keyed by turn and phase. Multiple turns remain
|
|
126
|
+
* live because async work can finish T1 after T2 has already emitted. The map
|
|
127
|
+
* retains a bounded recent window; work older than that window fails closed
|
|
128
|
+
* rather than reopening an exhausted budget. Session start clears all state.
|
|
112
129
|
*/
|
|
130
|
+
const MAX_TRACKED_TURN_CAPS = 64;
|
|
113
131
|
let turnCounts = new Map();
|
|
114
|
-
let
|
|
132
|
+
let latestCountedTurnIndex;
|
|
133
|
+
let retiredThroughTurnIndex = Number.NEGATIVE_INFINITY;
|
|
115
134
|
/**
|
|
116
135
|
* Decide whether this occurrence earns a detailed record, and count it in the
|
|
117
136
|
* ledger either way. Returns `true` when the caller should write the record.
|
|
@@ -180,14 +199,25 @@ export function emitBounded(phase, identity, payload, options = {}) {
|
|
|
180
199
|
function admitAgainstTurnCap(phase, cap) {
|
|
181
200
|
if (cap === undefined)
|
|
182
201
|
return true;
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
202
|
+
if (cap.turnIndex <= retiredThroughTurnIndex)
|
|
203
|
+
return false;
|
|
204
|
+
let counts = turnCounts.get(cap.turnIndex);
|
|
205
|
+
if (!counts) {
|
|
206
|
+
if (turnCounts.size >= MAX_TRACKED_TURN_CAPS) {
|
|
207
|
+
const oldestTrackedTurn = Math.min(...turnCounts.keys());
|
|
208
|
+
if (cap.turnIndex < oldestTrackedTurn)
|
|
209
|
+
return false;
|
|
210
|
+
turnCounts.delete(oldestTrackedTurn);
|
|
211
|
+
retiredThroughTurnIndex = Math.max(retiredThroughTurnIndex, oldestTrackedTurn);
|
|
212
|
+
}
|
|
213
|
+
counts = new Map();
|
|
214
|
+
turnCounts.set(cap.turnIndex, counts);
|
|
186
215
|
}
|
|
187
|
-
|
|
216
|
+
latestCountedTurnIndex = Math.max(latestCountedTurnIndex ?? cap.turnIndex, cap.turnIndex);
|
|
217
|
+
const used = counts.get(phase) ?? 0;
|
|
188
218
|
if (used >= cap.limit)
|
|
189
219
|
return false;
|
|
190
|
-
|
|
220
|
+
counts.set(phase, used + 1);
|
|
191
221
|
return true;
|
|
192
222
|
}
|
|
193
223
|
/**
|
|
@@ -198,9 +228,16 @@ function admitAgainstTurnCap(phase, cap) {
|
|
|
198
228
|
*/
|
|
199
229
|
export function resetBoundedTelemetry() {
|
|
200
230
|
turnCounts = new Map();
|
|
201
|
-
|
|
231
|
+
latestCountedTurnIndex = undefined;
|
|
232
|
+
retiredThroughTurnIndex = Number.NEGATIVE_INFINITY;
|
|
233
|
+
}
|
|
234
|
+
/** Test-only: one turn's live admission count for a phase. */
|
|
235
|
+
export function _boundedTurnCountForTest(phase, turnIndex = latestCountedTurnIndex) {
|
|
236
|
+
if (turnIndex === undefined)
|
|
237
|
+
return 0;
|
|
238
|
+
return turnCounts.get(turnIndex)?.get(phase) ?? 0;
|
|
202
239
|
}
|
|
203
|
-
/** Test-only:
|
|
204
|
-
export function
|
|
205
|
-
return turnCounts.
|
|
240
|
+
/** Test-only: number of turn buckets retained by the bounded counter. */
|
|
241
|
+
export function _boundedTrackedTurnsForTest() {
|
|
242
|
+
return turnCounts.size;
|
|
206
243
|
}
|