pi-lens 4.1.5 → 4.2.0
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 +415 -0
- package/README.md +13 -8
- package/config/dependency-cruiser-eager-allowlist.json +6 -1
- package/dist/clients/actionable-warnings.js +30 -4
- package/dist/clients/agent-behavior-client.js +2 -0
- package/dist/clients/analysed-root.js +1 -0
- package/dist/clients/ast-grep-tool-logger.js +1 -1
- package/dist/clients/bash-file-access.js +26 -0
- package/dist/clients/biome-client.js +13 -2
- package/dist/clients/blocker-freshness.js +403 -35
- package/dist/clients/bootstrap.js +6 -1
- package/dist/clients/bounded-telemetry.js +2 -0
- package/dist/clients/cache-observability.js +122 -1
- package/dist/clients/cascade-format.js +200 -4
- package/dist/clients/code-quality-warnings.js +10 -2
- package/dist/clients/complexity-client.js +5 -0
- package/dist/clients/config-diagnostic-codes.js +2 -0
- package/dist/clients/config-resolve.js +6 -3
- package/dist/clients/config-schema.js +17 -0
- package/dist/clients/config-warn.js +2 -2
- package/dist/clients/dead-code-client.js +8 -1
- package/dist/clients/degradation-ledger.js +16 -1
- package/dist/clients/dependency-checker.js +5 -1
- package/dist/clients/diagnostic-dispositions.js +12 -1
- package/dist/clients/dispatch/ast-grep-catalog.js +65 -0
- package/dist/clients/dispatch/auxiliary-lsp.js +28 -0
- package/dist/clients/dispatch/dispatcher.js +47 -17
- package/dist/clients/dispatch/facts/function-facts.js +1 -1
- package/dist/clients/dispatch/facts/import-facts.js +5 -1
- package/dist/clients/dispatch/finding-policy.js +222 -0
- package/dist/clients/dispatch/indent-detect.js +389 -13
- package/dist/clients/dispatch/integration.js +175 -25
- package/dist/clients/dispatch/rule-ignores.js +62 -0
- package/dist/clients/dispatch/runners/actionlint.js +2 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
- package/dist/clients/dispatch/runners/biome-check.js +8 -4
- package/dist/clients/dispatch/runners/cpp-check.js +4 -3
- package/dist/clients/dispatch/runners/credo.js +2 -1
- package/dist/clients/dispatch/runners/cue-vet.js +2 -1
- package/dist/clients/dispatch/runners/dart-analyze.js +2 -1
- package/dist/clients/dispatch/runners/detekt.js +2 -1
- package/dist/clients/dispatch/runners/dotnet-build.js +2 -1
- package/dist/clients/dispatch/runners/elixir-check.js +2 -1
- package/dist/clients/dispatch/runners/eslint.js +2 -1
- package/dist/clients/dispatch/runners/fish-indent.js +2 -1
- package/dist/clients/dispatch/runners/gleam-check.js +2 -1
- package/dist/clients/dispatch/runners/go-vet.js +2 -1
- package/dist/clients/dispatch/runners/golangci-lint.js +2 -1
- package/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/dist/clients/dispatch/runners/helm-lint.js +3 -1
- package/dist/clients/dispatch/runners/helm-render.js +2 -1
- package/dist/clients/dispatch/runners/htmlhint.js +2 -1
- package/dist/clients/dispatch/runners/javac.js +2 -1
- package/dist/clients/dispatch/runners/ktlint.js +2 -1
- package/dist/clients/dispatch/runners/lsp.js +19 -1
- package/dist/clients/dispatch/runners/markdownlint.js +3 -2
- package/dist/clients/dispatch/runners/mypy.js +2 -1
- package/dist/clients/dispatch/runners/oxlint.js +2 -1
- package/dist/clients/dispatch/runners/php-lint.js +2 -1
- package/dist/clients/dispatch/runners/phpstan.js +2 -1
- package/dist/clients/dispatch/runners/prisma-validate.js +2 -1
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +12 -12
- package/dist/clients/dispatch/runners/pyright.js +2 -1
- package/dist/clients/dispatch/runners/rubocop.js +2 -1
- package/dist/clients/dispatch/runners/ruff.js +2 -2
- package/dist/clients/dispatch/runners/rust-clippy.js +12 -11
- package/dist/clients/dispatch/runners/shellcheck.js +2 -1
- package/dist/clients/dispatch/runners/shfmt.js +4 -13
- package/dist/clients/dispatch/runners/spellcheck.js +2 -2
- package/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/dist/clients/dispatch/runners/sqlfluff.js +3 -2
- package/dist/clients/dispatch/runners/stylelint.js +3 -2
- package/dist/clients/dispatch/runners/swiftlint.js +2 -1
- package/dist/clients/dispatch/runners/taplo.js +2 -1
- package/dist/clients/dispatch/runners/terragrunt.js +2 -1
- package/dist/clients/dispatch/runners/tflint.js +2 -1
- package/dist/clients/dispatch/runners/tree-sitter.js +2 -21
- package/dist/clients/dispatch/runners/trivy-config.js +2 -1
- package/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
- package/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
- package/dist/clients/dispatch/runners/vale.js +2 -1
- package/dist/clients/dispatch/runners/yamllint.js +4 -3
- package/dist/clients/dispatch/runners/zig-check.js +2 -1
- package/dist/clients/dispatch/types.js +4 -1
- package/dist/clients/disposition-publish.js +1 -0
- package/dist/clients/effective-config.js +12 -2
- package/dist/clients/extension-log.js +2 -0
- package/dist/clients/file-kinds.js +26 -1
- package/dist/clients/file-role.js +23 -6
- package/dist/clients/file-utils.js +156 -10
- package/dist/clients/finding-delivery-gate.js +60 -9
- package/dist/clients/format-service.js +39 -21
- package/dist/clients/formatters.js +113 -200
- package/dist/clients/generation-guard.js +6 -2
- package/dist/clients/git-guard.js +11 -3
- package/dist/clients/gitleaks-client.js +42 -6
- package/dist/clients/govulncheck-client.js +48 -7
- package/dist/clients/indent-retarget.js +78 -8
- package/dist/clients/installer/index.js +524 -161
- package/dist/clients/instance-reaper-state.js +7 -0
- package/dist/clients/instance-reaper.js +20 -10
- package/dist/clients/jscpd-client.js +9 -1
- package/dist/clients/knip-client.js +9 -2
- package/dist/clients/language-profile.js +35 -3
- package/dist/clients/language-registry.js +5 -1
- package/dist/clients/latency-logger.js +6 -1
- package/dist/clients/lens-config.js +4 -0
- package/dist/clients/lens-engine.js +12 -3
- package/dist/clients/lens-events.js +2 -1
- package/dist/clients/lens-flag-registry.js +79 -0
- package/dist/clients/live-bus-emitter.js +2 -1
- package/dist/clients/lsp/client.js +25 -21
- package/dist/clients/lsp/config.js +36 -7
- package/dist/clients/lsp/diagnostic-binding.js +24 -1
- package/dist/clients/lsp/document-drift.js +78 -0
- package/dist/clients/lsp/index.js +288 -45
- package/dist/clients/lsp/launch.js +16 -4
- package/dist/clients/lsp/server.js +79 -15
- package/dist/clients/lsp/workspace-diagnostics-cache.js +39 -5
- package/dist/clients/lsp-mutation.js +5 -3
- package/dist/clients/map-with-concurrency.js +12 -8
- package/dist/clients/mcp/analyze.js +3 -0
- package/dist/clients/mcp/session.js +38 -4
- package/dist/clients/ndjson-logger.js +24 -7
- package/dist/clients/observed-mutation.js +40 -2
- package/dist/clients/opaque-mutation-scan.js +23 -3
- package/dist/clients/opengrep-client.js +111 -14
- package/dist/clients/package-manager.js +2 -1
- package/dist/clients/path-utils.js +123 -3
- package/dist/clients/performance-report.js +5 -1
- package/dist/clients/php-cs-fixer-config.js +8 -3
- package/dist/clients/pipeline.js +77 -7
- package/dist/clients/project-diagnostics/cache.js +37 -5
- package/dist/clients/project-diagnostics/fresh-fetch.js +173 -18
- package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
- package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
- package/dist/clients/project-diagnostics/scanner.js +59 -3
- package/dist/clients/project-lens-config.js +48 -2
- package/dist/clients/read-guard-logger.js +2 -0
- package/dist/clients/read-guard.js +70 -11
- package/dist/clients/resource-sampler.js +147 -9
- package/dist/clients/review-graph-logger.js +2 -0
- package/dist/clients/ruff-client.js +18 -9
- package/dist/clients/runtime-agent-end.js +42 -5
- package/dist/clients/runtime-context.js +6 -2
- package/dist/clients/runtime-coordinator.js +82 -7
- package/dist/clients/runtime-session.js +33 -5
- package/dist/clients/runtime-tool-call.js +43 -12
- package/dist/clients/runtime-tool-result.js +334 -86
- package/dist/clients/runtime-turn.js +195 -18
- package/dist/clients/safe-spawn.js +258 -5
- package/dist/clients/scratch-tree-policy.js +6 -0
- package/dist/clients/security-scan-client.js +2 -2
- package/dist/clients/session-event-guard.js +119 -6
- package/dist/clients/sg-runner.js +25 -12
- package/dist/clients/sgconfig.js +79 -2
- package/dist/clients/situational-tool-telemetry.js +108 -0
- package/dist/clients/test-runner-client.js +258 -78
- package/dist/clients/test-runner-delivery.js +43 -0
- package/dist/clients/tool-agreement.js +423 -0
- package/dist/clients/tool-config.js +249 -0
- package/dist/clients/tool-cwd.js +270 -0
- package/dist/clients/tool-policy.js +163 -7
- package/dist/clients/tool-probe.js +53 -0
- package/dist/clients/tool-set-policy.js +42 -0
- package/dist/clients/tree-sitter-client.js +120 -0
- package/dist/clients/tree-sitter-query-loader.js +127 -189
- package/dist/clients/trivy-client.js +9 -1
- package/dist/clients/turn-context.js +52 -0
- package/dist/clients/widget-state.js +102 -3
- package/dist/clients/word-index.js +5 -2
- package/dist/clients/write-ordering-guard.js +7 -0
- package/dist/index.js +26968 -21741
- package/dist/mcp/analyze-cli.js +3 -2
- package/dist/mcp/server.js +140 -161
- package/dist/tools/activate-tools.js +10 -7
- package/dist/tools/ast-grep-outline.js +2 -13
- package/dist/tools/ast-grep-replace.js +8 -13
- package/dist/tools/ast-grep-search.js +83 -39
- package/dist/tools/effective-config.js +4 -3
- package/dist/tools/lens-diagnostic-mark.js +8 -11
- package/dist/tools/lens-diagnostics.js +516 -133
- package/dist/tools/lsp-diagnostics.js +265 -182
- package/dist/tools/lsp-navigation.js +20 -44
- package/dist/tools/module-report.js +5 -9
- package/dist/tools/project-report.js +9 -17
- package/dist/tools/render-compact.js +280 -1
- package/dist/tools/symbol-search.js +2 -2
- package/docs/agent-guide.md +3 -4
- package/docs/agent-tools.md +46 -22
- package/docs/audit1.md +5 -5
- package/docs/configuration.md +9 -0
- package/docs/dependencies.md +11 -0
- package/docs/dispositions.md +52 -0
- package/docs/environment-variables.md +2 -7
- package/docs/features.md +7 -7
- package/docs/globalconfig.md +14 -2
- package/docs/language-coverage.md +1 -0
- package/docs/mcp.md +13 -1
- package/docs/pi-lens-fixer.md +48 -18
- package/docs/pi-lens-investigator.md +38 -18
- package/docs/pi-lens-monitor.md +88 -0
- package/docs/pi-lens-retro.md +97 -0
- package/docs/pi-lens-reviewer.md +53 -31
- package/docs/pi-lens-subagent.md +12 -4
- package/docs/public-api-stability.md +1 -0
- package/docs/real-harness.md +46 -0
- package/docs/release-qa-baseline.md +27 -1
- package/docs/servercapabilities.md +1 -6
- package/docs/settings.md +44 -6
- package/docs/tools_improvement2.md +4 -4
- package/docs/usage.md +15 -3
- package/package.json +10 -1
- package/rules/tree-sitter-queries/typescript/sql-injection.yml +23 -7
- package/scripts/analyze-pi-lens-logs.mjs +40 -0
- package/skills/pi-lens-ast-grep/SKILL.md +23 -6
- package/skills/pi-lens-lsp-navigation/SKILL.md +26 -9
- package/dist/tools/ast-dump.js +0 -103
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { logExtension } from "./extension-log.js";
|
|
5
|
+
import { isRealGitMarker, isAtOrAboveHomeDir, isUnderDir, isWindowsPath, nameMatchesMarkerGlob, normalizeEphemeralMapKey, } from "./path-utils.js";
|
|
6
|
+
import { getDegradationLedgerGeneration, recordDegradationOnce, } from "./degradation-ledger.js";
|
|
7
|
+
import { createGenerationMap, createGenerationSource, } from "./generation-guard.js";
|
|
8
|
+
import { rootMarkersForFile } from "./language-profile.js";
|
|
9
|
+
export const FORMATTER_MARKERS = {
|
|
10
|
+
biome: ["biome.json", "biome.jsonc", "package.json", ".gitignore"],
|
|
11
|
+
prettier: [
|
|
12
|
+
".prettierrc",
|
|
13
|
+
".prettierrc.json",
|
|
14
|
+
".prettierrc.yaml",
|
|
15
|
+
".prettierrc.yml",
|
|
16
|
+
".prettierrc.js",
|
|
17
|
+
".prettierrc.cjs",
|
|
18
|
+
".prettierrc.mjs",
|
|
19
|
+
"prettier.config.js",
|
|
20
|
+
"prettier.config.cjs",
|
|
21
|
+
"prettier.config.mjs",
|
|
22
|
+
".prettierignore",
|
|
23
|
+
"package.json",
|
|
24
|
+
".gitignore",
|
|
25
|
+
],
|
|
26
|
+
ruff: ["pyproject.toml", "ruff.toml", ".ruff.toml", ".gitignore"],
|
|
27
|
+
black: ["pyproject.toml", "black.toml", ".black", ".gitignore"],
|
|
28
|
+
"php-cs-fixer": [".php-cs-fixer.php", ".php-cs-fixer.dist.php", ".gitignore"],
|
|
29
|
+
sqlfluff: [".sqlfluff", "pyproject.toml", "setup.cfg", ".gitignore"],
|
|
30
|
+
oxfmt: [
|
|
31
|
+
"oxfmt.toml",
|
|
32
|
+
".oxfmtrc.json",
|
|
33
|
+
"vite-plus.json",
|
|
34
|
+
"package.json",
|
|
35
|
+
".gitignore",
|
|
36
|
+
],
|
|
37
|
+
rustfmt: ["rustfmt.toml", ".rustfmt.toml", "Cargo.toml", ".gitignore"],
|
|
38
|
+
rubocop: [".rubocop.yml", ".rubocop.yaml", ".gitignore"],
|
|
39
|
+
standardrb: [".standard.yml", ".standard.yaml", ".gitignore"],
|
|
40
|
+
"clang-format": [".clang-format", "_clang-format", ".gitignore"],
|
|
41
|
+
stylua: ["stylua.toml", ".stylua.toml", ".gitignore"],
|
|
42
|
+
ocamlformat: [".ocamlformat", ".gitignore"],
|
|
43
|
+
"google-java-format": [".google-java-format", ".editorconfig", ".gitignore"],
|
|
44
|
+
cljfmt: [".cljfmt.edn", "cljfmt.edn", ".cljfmt", ".gitignore"],
|
|
45
|
+
"cmake-format": [
|
|
46
|
+
".cmake-format",
|
|
47
|
+
".cmake-format.yaml",
|
|
48
|
+
".cmake-format.yml",
|
|
49
|
+
".cmake-format.json",
|
|
50
|
+
".cmake-format.py",
|
|
51
|
+
"cmake-format.yaml",
|
|
52
|
+
"cmake-format.yml",
|
|
53
|
+
".editorconfig",
|
|
54
|
+
".gitignore",
|
|
55
|
+
],
|
|
56
|
+
"psscriptanalyzer-format": [
|
|
57
|
+
"PSScriptAnalyzerSettings.psd1",
|
|
58
|
+
"ScriptAnalyzerSettings.psd1",
|
|
59
|
+
".gitignore",
|
|
60
|
+
],
|
|
61
|
+
csharpier: [
|
|
62
|
+
".csharpierrc",
|
|
63
|
+
".csharpierrc.json",
|
|
64
|
+
".csharpierrc.yaml",
|
|
65
|
+
".csharpierrc.yml",
|
|
66
|
+
".gitignore",
|
|
67
|
+
],
|
|
68
|
+
ormolu: [".ormolu", ".gitignore"],
|
|
69
|
+
taplo: ["taplo.toml", ".taplo.toml", ".gitignore"],
|
|
70
|
+
terraform: [".terraform.lock.hcl", ".gitignore"],
|
|
71
|
+
swiftformat: [".swiftformat", ".gitignore"],
|
|
72
|
+
fantomas: [".fantomasignore", ".editorconfig", ".gitignore"],
|
|
73
|
+
mix: [".formatter.exs", ".gitignore"],
|
|
74
|
+
shfmt: [".editorconfig", ".gitignore"],
|
|
75
|
+
ktlint: [".editorconfig", ".gitignore"],
|
|
76
|
+
ktfmt: [
|
|
77
|
+
".editorconfig",
|
|
78
|
+
".ktfmt",
|
|
79
|
+
".ktfmt.kts",
|
|
80
|
+
"build.gradle",
|
|
81
|
+
"build.gradle.kts",
|
|
82
|
+
"settings.gradle",
|
|
83
|
+
"settings.gradle.kts",
|
|
84
|
+
".gitignore",
|
|
85
|
+
],
|
|
86
|
+
typstyle: [".gitignore"],
|
|
87
|
+
};
|
|
88
|
+
const toolCwdGeneration = createGenerationSource("tool-cwd");
|
|
89
|
+
const logged = createGenerationMap("tool-cwd-resolution-log");
|
|
90
|
+
/** Pure key seam for resolution-log and runner-advisory identity. */
|
|
91
|
+
export function _toolCwdEphemeralKey(parts) {
|
|
92
|
+
return parts.map(normalizeEphemeralMapKey).join("\0");
|
|
93
|
+
}
|
|
94
|
+
function syncGeneration() {
|
|
95
|
+
if (toolCwdGeneration.current() === getDegradationLedgerGeneration())
|
|
96
|
+
return;
|
|
97
|
+
logged.clear();
|
|
98
|
+
while (toolCwdGeneration.current() < getDegradationLedgerGeneration()) {
|
|
99
|
+
toolCwdGeneration.bump();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function walkMarkerRoot(startDir, markers, homeDir) {
|
|
103
|
+
let current = path.resolve(startDir);
|
|
104
|
+
for (let depth = 0; depth < 64; depth++) {
|
|
105
|
+
if (isAtOrAboveHomeDir(current, homeDir))
|
|
106
|
+
break;
|
|
107
|
+
for (const marker of markers) {
|
|
108
|
+
const slash = marker.replace(/\\/g, "/").lastIndexOf("/");
|
|
109
|
+
const parent = slash >= 0 ? marker.slice(0, slash) : "";
|
|
110
|
+
const basename = slash >= 0 ? marker.slice(slash + 1) : marker;
|
|
111
|
+
const target = parent
|
|
112
|
+
? path.join(current, ...parent.split("/").filter(Boolean))
|
|
113
|
+
: current;
|
|
114
|
+
let found = false;
|
|
115
|
+
if (basename.includes("*")) {
|
|
116
|
+
try {
|
|
117
|
+
found = readdirSync(target, { withFileTypes: true }).some((entry) => (entry.isFile() || entry.isSymbolicLink()) &&
|
|
118
|
+
nameMatchesMarkerGlob(entry.name, basename));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
found = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
found = existsSync(path.join(target, basename));
|
|
126
|
+
}
|
|
127
|
+
if (found)
|
|
128
|
+
return { root: current, marker };
|
|
129
|
+
}
|
|
130
|
+
const parentDir = path.dirname(current);
|
|
131
|
+
if (parentDir === current)
|
|
132
|
+
break;
|
|
133
|
+
current = parentDir;
|
|
134
|
+
}
|
|
135
|
+
return { root: null };
|
|
136
|
+
}
|
|
137
|
+
function findMarkerRoot(startDir, markers, homeDir) {
|
|
138
|
+
syncGeneration();
|
|
139
|
+
return walkMarkerRoot(startDir, markers, homeDir);
|
|
140
|
+
}
|
|
141
|
+
function markersFor(kind, tool, file, ctx) {
|
|
142
|
+
if (kind === "lsp")
|
|
143
|
+
return ctx.rootMarkers ?? [];
|
|
144
|
+
// #2965: runner fallback uses the language table that also anchors the
|
|
145
|
+
// dispatch context. A caller-owned table remains an explicit override.
|
|
146
|
+
if (kind === "runner")
|
|
147
|
+
return ctx.rootMarkers ?? rootMarkersForFile(file, tool);
|
|
148
|
+
return FORMATTER_MARKERS[tool] ?? [".gitignore"];
|
|
149
|
+
}
|
|
150
|
+
function emitResolution(kind, tool, cwd, reason) {
|
|
151
|
+
syncGeneration();
|
|
152
|
+
const key = _toolCwdEphemeralKey([kind, tool, cwd, reason]);
|
|
153
|
+
if (logged.current(key) !== 0)
|
|
154
|
+
return;
|
|
155
|
+
logged.bump(key);
|
|
156
|
+
logExtension({
|
|
157
|
+
subsystem: "tool-cwd",
|
|
158
|
+
level: "debug",
|
|
159
|
+
message: `cwd ${kind} ${tool} cwd=${cwd} reason=${reason}`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Emit a constant runner advisory once per (tool, resolved root) per session
|
|
164
|
+
* (#2811): the yamllint/stylelint/sqlfluff "no config detected" lines and
|
|
165
|
+
* markdownlint's spawn-timeout cooldown notice logged once PER FILE at error
|
|
166
|
+
* level — 17 lines in one session for one tool. The throttle shares the
|
|
167
|
+
* resolution-log map and its ledger-generation reset, so `session_start`
|
|
168
|
+
* re-arms it like every once-latch in this module. The message rides
|
|
169
|
+
* `ctx.log` at debug level so the dispatch seam stays the one sink for
|
|
170
|
+
* runner lines, with its own filePath/kind metadata.
|
|
171
|
+
*/
|
|
172
|
+
export function logRunnerAdvisoryOnce(ctx, tool, root, message) {
|
|
173
|
+
syncGeneration();
|
|
174
|
+
const key = _toolCwdEphemeralKey(["runner-advisory", tool, root]);
|
|
175
|
+
if (logged.current(key) !== 0)
|
|
176
|
+
return;
|
|
177
|
+
logged.bump(key);
|
|
178
|
+
ctx.log(message, "debug");
|
|
179
|
+
}
|
|
180
|
+
/** Resolve every child process cwd/root through one bounded, synchronous seam. */
|
|
181
|
+
export function resolveToolCwd(kind, tool, file, ctx) {
|
|
182
|
+
const dispatchRoot = path.resolve(ctx.cwd ?? process.cwd());
|
|
183
|
+
const absoluteFile = path.resolve(file);
|
|
184
|
+
const fileDir = path.dirname(absoluteFile);
|
|
185
|
+
const homeDir = ctx.homeDir ?? os.homedir();
|
|
186
|
+
const insideDispatch = isUnderDir(absoluteFile, dispatchRoot);
|
|
187
|
+
if (ctx.serverRoot) {
|
|
188
|
+
const rootPath = isWindowsPath(ctx.serverRoot) ? path.win32 : path;
|
|
189
|
+
const serverRoot = rootPath.resolve(ctx.serverRoot);
|
|
190
|
+
if (!ctx.suppressTelemetry)
|
|
191
|
+
emitResolution(kind, tool, serverRoot, "server-root");
|
|
192
|
+
return { cwd: serverRoot };
|
|
193
|
+
}
|
|
194
|
+
const markers = markersFor(kind, tool, absoluteFile, ctx);
|
|
195
|
+
const markerResult = markers.length
|
|
196
|
+
? findMarkerRoot(fileDir, markers, ctx.allowHomeMarker ? path.parse(homeDir).root : homeDir)
|
|
197
|
+
: { root: null };
|
|
198
|
+
const markerRoot = markerResult.root;
|
|
199
|
+
if (markerRoot &&
|
|
200
|
+
(insideDispatch ? isUnderDir(markerRoot, dispatchRoot) : true)) {
|
|
201
|
+
const finalReason = `marker:${markerResult.marker ?? markers[0]}`;
|
|
202
|
+
if (!ctx.suppressTelemetry)
|
|
203
|
+
emitResolution(kind, tool, markerRoot, finalReason);
|
|
204
|
+
return {
|
|
205
|
+
cwd: markerRoot,
|
|
206
|
+
...(markerResult.marker !== undefined
|
|
207
|
+
? { marker: markerResult.marker }
|
|
208
|
+
: {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const memo = ctx.toolCwdMemo;
|
|
212
|
+
if (memo && memo.gitRoot === undefined) {
|
|
213
|
+
const gitResult = findMarkerRoot(fileDir, [".git"], homeDir);
|
|
214
|
+
memo.gitRoot =
|
|
215
|
+
gitResult.root && isRealGitMarker(path.join(gitResult.root, ".git"))
|
|
216
|
+
? gitResult.root
|
|
217
|
+
: null;
|
|
218
|
+
}
|
|
219
|
+
const gitRoot = memo && memo.gitRoot !== undefined
|
|
220
|
+
? memo.gitRoot
|
|
221
|
+
: (() => {
|
|
222
|
+
const gitResult = findMarkerRoot(fileDir, [".git"], homeDir);
|
|
223
|
+
return gitResult.root &&
|
|
224
|
+
isRealGitMarker(path.join(gitResult.root, ".git"))
|
|
225
|
+
? gitResult.root
|
|
226
|
+
: null;
|
|
227
|
+
})();
|
|
228
|
+
if (gitRoot && (insideDispatch ? isUnderDir(gitRoot, dispatchRoot) : true)) {
|
|
229
|
+
if (!ctx.suppressTelemetry)
|
|
230
|
+
emitResolution(kind, tool, gitRoot, "git-root");
|
|
231
|
+
return { cwd: gitRoot, marker: ".git" };
|
|
232
|
+
}
|
|
233
|
+
if (insideDispatch) {
|
|
234
|
+
if (kind === "formatter") {
|
|
235
|
+
if (!ctx.suppressTelemetry)
|
|
236
|
+
emitResolution(kind, tool, fileDir, "file-dir-fallback");
|
|
237
|
+
if (!ctx.suppressTelemetry && !isUnderDir(fileDir, homeDir)) {
|
|
238
|
+
recordDegradationOnce({
|
|
239
|
+
kind: "tool-cwd-resolution",
|
|
240
|
+
subject: tool,
|
|
241
|
+
reason: `${kind}:home-cap:${absoluteFile}`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return { cwd: fileDir };
|
|
245
|
+
}
|
|
246
|
+
if (!ctx.suppressTelemetry)
|
|
247
|
+
emitResolution(kind, tool, dispatchRoot, "dispatch-root");
|
|
248
|
+
return { cwd: dispatchRoot };
|
|
249
|
+
}
|
|
250
|
+
const reason = isUnderDir(fileDir, homeDir)
|
|
251
|
+
? "file-dir-fallback"
|
|
252
|
+
: "home-cap";
|
|
253
|
+
const fallback = isUnderDir(fileDir, homeDir) ? fileDir : homeDir;
|
|
254
|
+
if (!ctx.suppressTelemetry)
|
|
255
|
+
recordDegradationOnce({
|
|
256
|
+
kind: "tool-cwd-resolution",
|
|
257
|
+
subject: tool,
|
|
258
|
+
reason: `${kind}:${reason}:${absoluteFile}`,
|
|
259
|
+
});
|
|
260
|
+
if (!ctx.suppressTelemetry)
|
|
261
|
+
emitResolution(kind, tool, fallback, reason);
|
|
262
|
+
return { cwd: fallback };
|
|
263
|
+
}
|
|
264
|
+
/** Runner-shaped adapter kept at the same seam for every runner consumer. */
|
|
265
|
+
export function resolveRunnerCwd(ctx, tool) {
|
|
266
|
+
return resolveToolCwd("runner", tool, ctx.filePath, ctx).cwd;
|
|
267
|
+
}
|
|
268
|
+
export function resolveRunnerCwdWithReason(ctx, tool) {
|
|
269
|
+
return resolveToolCwd("runner", tool, ctx.filePath, ctx);
|
|
270
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { TERRAGRUNT_FILENAMES } from "./file-kinds.js";
|
|
4
|
+
import { recordDegradationOnce } from "./degradation-ledger.js";
|
|
4
5
|
import { logLatency } from "./latency-logger.js";
|
|
5
6
|
import { resolvePackagePath } from "./package-root.js";
|
|
6
7
|
import { findNearestContaining, normalizeMapKey, walkUpDirs, } from "./path-utils.js";
|
|
@@ -371,6 +372,24 @@ const FORMATTER_POLICY_BY_EXTENSION = new Map([
|
|
|
371
372
|
gate: "smart-default",
|
|
372
373
|
},
|
|
373
374
|
],
|
|
375
|
+
[
|
|
376
|
+
".typ",
|
|
377
|
+
{
|
|
378
|
+
formatterNames: ["typstyle"],
|
|
379
|
+
defaultFormatter: "typstyle",
|
|
380
|
+
defaultWhenUnconfigured: true,
|
|
381
|
+
gate: "smart-default",
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
[
|
|
385
|
+
".typc",
|
|
386
|
+
{
|
|
387
|
+
formatterNames: ["typstyle"],
|
|
388
|
+
defaultFormatter: "typstyle",
|
|
389
|
+
defaultWhenUnconfigured: true,
|
|
390
|
+
gate: "smart-default",
|
|
391
|
+
},
|
|
392
|
+
],
|
|
374
393
|
[
|
|
375
394
|
".c",
|
|
376
395
|
{
|
|
@@ -738,6 +757,7 @@ const AUTO_INSTALLABLE_DEFAULT_FORMATTERS = new Map([
|
|
|
738
757
|
["shfmt", "shfmt"],
|
|
739
758
|
["taplo", "taplo"],
|
|
740
759
|
["ktlint", "ktlint"],
|
|
760
|
+
["typstyle", "typstyle"],
|
|
741
761
|
]);
|
|
742
762
|
// `gate: "smart-default"` so a matched file is formatted only when nothing else
|
|
743
763
|
// claims it. Misdetection edge: `root.hcl` is filename-detected as terragrunt, so
|
|
@@ -1633,17 +1653,25 @@ export function getAutofixPolicyForFile(filePath, context = {}) {
|
|
|
1633
1653
|
export function getPreferredAutofixTools(filePath, context) {
|
|
1634
1654
|
return getAutofixPolicyForFile(filePath, context)?.preferredTools ?? [];
|
|
1635
1655
|
}
|
|
1636
|
-
|
|
1656
|
+
// Flat-config filenames resolve like ESLint itself: upward from cwd without
|
|
1657
|
+
// stopping at an intermediate package.json (#3017, shape 39). Legacy
|
|
1658
|
+
// `.eslintrc.*` names and the `package.json#eslintConfig` field keep the
|
|
1659
|
+
// historic per-package boundary below.
|
|
1660
|
+
const ESLINT_FLAT_CONFIGS = [
|
|
1661
|
+
"eslint.config.js",
|
|
1662
|
+
"eslint.config.mjs",
|
|
1663
|
+
"eslint.config.cjs",
|
|
1664
|
+
"eslint.config.ts",
|
|
1665
|
+
"eslint.config.mts",
|
|
1666
|
+
"eslint.config.cts",
|
|
1667
|
+
];
|
|
1668
|
+
const ESLINT_LEGACY_CONFIGS = [
|
|
1637
1669
|
".eslintrc",
|
|
1638
1670
|
".eslintrc.js",
|
|
1639
1671
|
".eslintrc.cjs",
|
|
1640
1672
|
".eslintrc.json",
|
|
1641
1673
|
".eslintrc.yaml",
|
|
1642
1674
|
".eslintrc.yml",
|
|
1643
|
-
"eslint.config.js",
|
|
1644
|
-
"eslint.config.mjs",
|
|
1645
|
-
"eslint.config.cjs",
|
|
1646
|
-
"eslint.config.ts",
|
|
1647
1675
|
];
|
|
1648
1676
|
function* walkUpDirsUntilPackageJson(cwd) {
|
|
1649
1677
|
for (const dir of walkUpDirs(cwd)) {
|
|
@@ -1692,8 +1720,19 @@ export function hasNearestPackageJsonField(cwd, fieldName) {
|
|
|
1692
1720
|
return false;
|
|
1693
1721
|
}
|
|
1694
1722
|
export function hasEslintConfig(cwd) {
|
|
1723
|
+
// Flat configs mirror ESLint's own discovery: every ancestor dir up to
|
|
1724
|
+
// the filesystem root, ignoring intermediate package.json boundaries, so
|
|
1725
|
+
// a monorepo root config stays visible from inside a nested package.
|
|
1726
|
+
for (const dir of walkUpDirs(cwd)) {
|
|
1727
|
+
for (const cfg of ESLINT_FLAT_CONFIGS) {
|
|
1728
|
+
if (fs.existsSync(path.join(dir, cfg)))
|
|
1729
|
+
return true;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
// Legacy `.eslintrc.*` and `package.json#eslintConfig` keep per-package
|
|
1733
|
+
// semantics: the walk stops at the nearest package.json.
|
|
1695
1734
|
for (const dir of walkUpDirsUntilPackageJson(cwd)) {
|
|
1696
|
-
for (const cfg of
|
|
1735
|
+
for (const cfg of ESLINT_LEGACY_CONFIGS) {
|
|
1697
1736
|
if (fs.existsSync(path.join(dir, cfg)))
|
|
1698
1737
|
return true;
|
|
1699
1738
|
}
|
|
@@ -2231,6 +2270,16 @@ const KOTLIN_GRADLE_FILES = [
|
|
|
2231
2270
|
"settings.gradle.kts",
|
|
2232
2271
|
"settings.gradle",
|
|
2233
2272
|
];
|
|
2273
|
+
const GRADLE_KTLINT_PLUGIN_PATTERN = /(?:id\s*\(\s*|id\s+|apply\s+plugin\s*:\s*)["']org\.jlleitschuh\.gradle\.ktlint["']/g;
|
|
2274
|
+
const GRADLE_BUILD_LOGIC_DIRS = ["buildSrc", "build-logic"];
|
|
2275
|
+
const GRADLE_BUILD_LOGIC_EXTENSIONS = [".gradle", ".gradle.kts", ".kt"];
|
|
2276
|
+
const GRADLE_INCLUDE_BUILD_PATTERN = /\bincludeBuild\s*\(\s*["']([^"']+)["']/g;
|
|
2277
|
+
/**
|
|
2278
|
+
* Bound the synchronous ownership probe so a large convention tree cannot
|
|
2279
|
+
* stall every autofix. Exceeding it is an unknown ownership result, not proof
|
|
2280
|
+
* of no owner, and callers must decline the write (#3004).
|
|
2281
|
+
*/
|
|
2282
|
+
export const GRADLE_BUILD_LOGIC_SCAN_MAX_ENTRIES = 10_000;
|
|
2234
2283
|
const spotlessKotlinConfigCache = new Map();
|
|
2235
2284
|
let spotlessGradleReadCount = 0;
|
|
2236
2285
|
/** Test-only observability for asserting the hot-path I/O bound. */
|
|
@@ -2429,6 +2478,110 @@ export function getSpotlessKotlinFormatter(cwd) {
|
|
|
2429
2478
|
export function hasKtlintConfig(cwd) {
|
|
2430
2479
|
return getSpotlessKotlinFormatter(cwd) === "ktlint";
|
|
2431
2480
|
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Whether Gradle build logic applies the ktlint Gradle plugin. The plugin
|
|
2483
|
+
* establishes project ownership of ktlint, but its plugin version is not a
|
|
2484
|
+
* ktlint CLI version and must never be used as one (#3000).
|
|
2485
|
+
*/
|
|
2486
|
+
export function hasGradleKtlintPlugin(cwd) {
|
|
2487
|
+
const files = new Set();
|
|
2488
|
+
const scan = { entries: 0, exceeded: false, indeterminate: false };
|
|
2489
|
+
for (const dir of walkUpDirs(cwd)) {
|
|
2490
|
+
for (const gradle of KOTLIN_GRADLE_FILES) {
|
|
2491
|
+
const filePath = path.join(dir, gradle);
|
|
2492
|
+
if (fs.existsSync(filePath))
|
|
2493
|
+
files.add(filePath);
|
|
2494
|
+
}
|
|
2495
|
+
for (const buildLogicDir of GRADLE_BUILD_LOGIC_DIRS) {
|
|
2496
|
+
if (!addGradleBuildLogicFiles(path.join(dir, buildLogicDir), files, scan)) {
|
|
2497
|
+
scan.exceeded = true;
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
for (const settings of ["settings.gradle.kts", "settings.gradle"]) {
|
|
2501
|
+
const settingsPath = path.join(dir, settings);
|
|
2502
|
+
if (!fs.existsSync(settingsPath))
|
|
2503
|
+
continue;
|
|
2504
|
+
try {
|
|
2505
|
+
const raw = fs.readFileSync(settingsPath, "utf-8");
|
|
2506
|
+
const stripped = stripGradleCommentsAndStrings(raw);
|
|
2507
|
+
GRADLE_INCLUDE_BUILD_PATTERN.lastIndex = 0;
|
|
2508
|
+
let match;
|
|
2509
|
+
while ((match = GRADLE_INCLUDE_BUILD_PATTERN.exec(raw)) !== null) {
|
|
2510
|
+
const code = stripped.slice(match.index, match.index + match[0].length);
|
|
2511
|
+
const includedBuild = match[1];
|
|
2512
|
+
if (includedBuild && /^\s*includeBuild\s*\(\s*/.test(code)) {
|
|
2513
|
+
if (!addGradleBuildLogicFiles(path.resolve(dir, includedBuild), files, scan)) {
|
|
2514
|
+
scan.exceeded = true;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
catch {
|
|
2520
|
+
scan.indeterminate = true;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
if (scan.exceeded) {
|
|
2525
|
+
recordDegradationOnce({
|
|
2526
|
+
kind: "gradle-ktlint-scan-budget-exceeded",
|
|
2527
|
+
subject: "ktlint:gradle-build-logic",
|
|
2528
|
+
reason: `Gradle build-logic ownership scan exceeded its ${GRADLE_BUILD_LOGIC_SCAN_MAX_ENTRIES}-entry budget; ` +
|
|
2529
|
+
"ownership cannot be established, so ktlint autofix is declined",
|
|
2530
|
+
});
|
|
2531
|
+
return { kind: "indeterminate" };
|
|
2532
|
+
}
|
|
2533
|
+
if (scan.indeterminate)
|
|
2534
|
+
return { kind: "indeterminate" };
|
|
2535
|
+
for (const filePath of files) {
|
|
2536
|
+
try {
|
|
2537
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
2538
|
+
const stripped = stripGradleCommentsAndStrings(raw);
|
|
2539
|
+
GRADLE_KTLINT_PLUGIN_PATTERN.lastIndex = 0;
|
|
2540
|
+
let match;
|
|
2541
|
+
while ((match = GRADLE_KTLINT_PLUGIN_PATTERN.exec(raw)) !== null) {
|
|
2542
|
+
const code = stripped.slice(match.index, match.index + match[0].length);
|
|
2543
|
+
if (!/^\s*$/.test(code))
|
|
2544
|
+
return { kind: "owned" };
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
catch {
|
|
2548
|
+
return { kind: "indeterminate" };
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
return { kind: "not-owned" };
|
|
2552
|
+
}
|
|
2553
|
+
function addGradleBuildLogicFiles(root, files, scan) {
|
|
2554
|
+
if (!fs.existsSync(root))
|
|
2555
|
+
return true;
|
|
2556
|
+
const pending = [root];
|
|
2557
|
+
while (pending.length > 0) {
|
|
2558
|
+
const dir = pending.pop();
|
|
2559
|
+
if (!dir)
|
|
2560
|
+
continue;
|
|
2561
|
+
let entries;
|
|
2562
|
+
try {
|
|
2563
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2564
|
+
}
|
|
2565
|
+
catch {
|
|
2566
|
+
scan.indeterminate = true;
|
|
2567
|
+
continue;
|
|
2568
|
+
}
|
|
2569
|
+
for (const entry of entries) {
|
|
2570
|
+
scan.entries += 1;
|
|
2571
|
+
if (scan.entries > GRADLE_BUILD_LOGIC_SCAN_MAX_ENTRIES)
|
|
2572
|
+
return false;
|
|
2573
|
+
const entryPath = path.join(dir, entry.name);
|
|
2574
|
+
if (entry.isDirectory()) {
|
|
2575
|
+
pending.push(entryPath);
|
|
2576
|
+
}
|
|
2577
|
+
else if (entry.isFile() &&
|
|
2578
|
+
GRADLE_BUILD_LOGIC_EXTENSIONS.some((extension) => entry.name.endsWith(extension))) {
|
|
2579
|
+
files.add(entryPath);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return true;
|
|
2584
|
+
}
|
|
2432
2585
|
export function hasKtfmtConfig(cwd) {
|
|
2433
2586
|
const spotlessFormatter = getSpotlessKotlinFormatter(cwd);
|
|
2434
2587
|
if (spotlessFormatter)
|
|
@@ -2557,7 +2710,10 @@ const OXLINT_CONFIGS = [
|
|
|
2557
2710
|
"oxlint.config.mts",
|
|
2558
2711
|
];
|
|
2559
2712
|
export function hasOxlintConfig(cwd) {
|
|
2560
|
-
|
|
2713
|
+
// Oxlint auto-discovers the nearest config by file location, so the walk
|
|
2714
|
+
// ignores intermediate package.json boundaries just like the flat
|
|
2715
|
+
// ESLint branch above (#3017).
|
|
2716
|
+
for (const dir of walkUpDirs(cwd)) {
|
|
2561
2717
|
for (const cfg of OXLINT_CONFIGS) {
|
|
2562
2718
|
if (fs.existsSync(path.join(dir, cfg)))
|
|
2563
2719
|
return true;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool-probe spawn seam (#2894).
|
|
3
|
+
*
|
|
4
|
+
* A sibling of `tool-cwd.ts`: that module owns where a child that READS A
|
|
5
|
+
* PROJECT starts, this one owns the fact that a child which reads no project
|
|
6
|
+
* starts nowhere in particular.
|
|
7
|
+
*
|
|
8
|
+
* It is a module of its own rather than another export on `safe-spawn.ts`
|
|
9
|
+
* deliberately. Every availability test in this repo fakes the process
|
|
10
|
+
* boundary with `vi.mock("clients/safe-spawn.js", … { safeSpawnAsync })`, and
|
|
11
|
+
* a same-module helper would call the module's own unmocked binding — the
|
|
12
|
+
* probe would spawn for real while the test believed it had the boundary. One
|
|
13
|
+
* module hop keeps every existing double production-faithful.
|
|
14
|
+
*/
|
|
15
|
+
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
16
|
+
/**
|
|
17
|
+
* Run a tool's own presence/version invocation — `<tool> --version`, `cl`,
|
|
18
|
+
* `go version`, `which <pm>`, `Get-Module -ListAvailable …` — and hand back
|
|
19
|
+
* the raw spawn result for the caller's availability policy to classify.
|
|
20
|
+
*
|
|
21
|
+
* ## The contract: a probe never gets a `cwd`
|
|
22
|
+
*
|
|
23
|
+
* A probe asks whether a binary exists and what it calls itself. The answer
|
|
24
|
+
* does not depend on the directory the child starts in, so this seam passes
|
|
25
|
+
* NONE — the child inherits the host's directory and nothing it reports is
|
|
26
|
+
* read out of a project. That is why the sweep in
|
|
27
|
+
* `tests/clients/dispatch/runners/runner-spawn-cwd-sweep.test.ts` admits ONE
|
|
28
|
+
* cwd-less spawn here instead of the 23 it admitted before: every one of those
|
|
29
|
+
* sites re-decided the same thing in its own words, and 23 sentences a
|
|
30
|
+
* reviewer has to re-read one at a time is how an admission table stops being
|
|
31
|
+
* auditable (#2872 round 3 shipped 127 rows carrying two canned sentences,
|
|
32
|
+
* four of them false where they were read).
|
|
33
|
+
*
|
|
34
|
+
* `cwd` is STRIPPED rather than merely left out of {@link ProbeSpawnOptions}:
|
|
35
|
+
* the type stops an object literal from carrying one, and the destructure
|
|
36
|
+
* below removes one that arrived inside an already-typed options object a
|
|
37
|
+
* caller widened or spread. Both halves are guarded — deleting the
|
|
38
|
+
* destructure changes this call's text, which retires the sweep's admission
|
|
39
|
+
* row, and `tests/clients/probe-and-root-spawn-cwd.test.ts` reds on the
|
|
40
|
+
* smuggled cwd reaching the child. A probe that genuinely needs to
|
|
41
|
+
* run somewhere — `mix credo --version` needs a mix project, `cargo clippy
|
|
42
|
+
* --version` needs a package, `eslint --version` resolves a project-local
|
|
43
|
+
* binary — must call `safeSpawnAsync` directly and be admitted by that sweep
|
|
44
|
+
* on its own reason.
|
|
45
|
+
*/
|
|
46
|
+
export async function probeToolAsync(command, args, options) {
|
|
47
|
+
// Destructured away rather than overwritten with `undefined`: the key is
|
|
48
|
+
// then genuinely ABSENT from what reaches the spawn, which is both what the
|
|
49
|
+
// contract says and what `exactOptionalPropertyTypes` wants (a present
|
|
50
|
+
// `cwd: undefined` is a strictness spike the `tests/config` ratchet counts).
|
|
51
|
+
const { cwd: _strippedCwd, ...rest } = (options ?? {});
|
|
52
|
+
return safeSpawnAsync(command, [...args], { ...rest });
|
|
53
|
+
}
|
|
@@ -1,4 +1,46 @@
|
|
|
1
1
|
import { logLatency } from "./latency-logger.js";
|
|
2
|
+
import { BoundedFifoMap } from "./bounded-cache.js";
|
|
3
|
+
import { recordDegradationOnce } from "./degradation-ledger.js";
|
|
4
|
+
// pi re-runs the extension factory for session rebuilds, but imports this
|
|
5
|
+
// module once per process. Keep conversation activation memory here, keyed by
|
|
6
|
+
// pi's session file rather than by a factory closure or process-wide session.
|
|
7
|
+
export const REMEMBERED_LAZY_TOOLS_MAX_SESSIONS = 128;
|
|
8
|
+
const rememberedLazyToolsBySessionFile = new BoundedFifoMap(REMEMBERED_LAZY_TOOLS_MAX_SESSIONS);
|
|
9
|
+
export function rememberLazyTools(sessionFile, names) {
|
|
10
|
+
if (!sessionFile) {
|
|
11
|
+
recordDegradationOnce({
|
|
12
|
+
kind: "tool-set-session-file-unavailable",
|
|
13
|
+
subject: "activation",
|
|
14
|
+
reason: "session-file identity unavailable; activation memory is inert",
|
|
15
|
+
});
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const remembered = rememberedLazyToolsBySessionFile.get(sessionFile) ?? new Set();
|
|
19
|
+
for (const name of names)
|
|
20
|
+
remembered.add(name);
|
|
21
|
+
rememberedLazyToolsBySessionFile.set(sessionFile, remembered);
|
|
22
|
+
}
|
|
23
|
+
export function getRememberedLazyTools(sessionFile) {
|
|
24
|
+
return sessionFile
|
|
25
|
+
? (rememberedLazyToolsBySessionFile.get(sessionFile) ?? new Set())
|
|
26
|
+
: new Set();
|
|
27
|
+
}
|
|
28
|
+
export function clearRememberedLazyTools(sessionFile) {
|
|
29
|
+
if (sessionFile)
|
|
30
|
+
rememberedLazyToolsBySessionFile.delete(sessionFile);
|
|
31
|
+
}
|
|
32
|
+
export function inheritRememberedLazyTools(parentSessionFile, childSessionFile) {
|
|
33
|
+
if (!parentSessionFile ||
|
|
34
|
+
!childSessionFile ||
|
|
35
|
+
parentSessionFile === childSessionFile)
|
|
36
|
+
return;
|
|
37
|
+
const remembered = rememberedLazyToolsBySessionFile.get(parentSessionFile);
|
|
38
|
+
if (remembered)
|
|
39
|
+
rememberedLazyToolsBySessionFile.set(childSessionFile, new Set(remembered));
|
|
40
|
+
}
|
|
41
|
+
export function resetRememberedLazyToolsForTests() {
|
|
42
|
+
rememberedLazyToolsBySessionFile.clear();
|
|
43
|
+
}
|
|
2
44
|
/**
|
|
3
45
|
* Whether the host will send this model deferred (searchable) tool
|
|
4
46
|
* definitions rather than the full inline list.
|