dsh-context-compression-improved 0.1.1 → 0.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/.gitattributes +1 -0
- package/.github/workflows/ci.yml +39 -0
- package/CHANGELOG.ja.md +39 -0
- package/CHANGELOG.ko.md +39 -0
- package/CHANGELOG.md +135 -0
- package/CHANGELOG.zh.md +39 -0
- package/CONTRIBUTING.md +22 -0
- package/README.ja.md +104 -0
- package/README.ko.md +103 -0
- package/README.md +89 -12
- package/README.zh.md +87 -12
- package/SECURITY.md +18 -0
- package/THIRD_PARTY_NOTICES.md +7 -31
- package/docs/installation.ja.md +76 -0
- package/docs/installation.ko.md +76 -0
- package/docs/installation.md +76 -0
- package/docs/installation.zh.md +76 -0
- package/docs/repair-log.md +582 -0
- package/eslint.config.js +30 -0
- package/package.json +84 -81
- package/packages/selector/LICENSE +21 -0
- package/packages/selector/README.md +26 -0
- package/packages/selector/README.zh.md +26 -0
- package/packages/selector/THIRD_PARTY_NOTICES.md +38 -0
- package/packages/selector/docs/history-tool-call-working-set-spec.md +112 -0
- package/packages/selector/docs/native-tool-result-selector-spec.md +34 -0
- package/packages/selector/docs/subagent-cache-reuse-spec.md +46 -0
- package/packages/selector/lib/style.css +308 -0
- package/packages/selector/package.json +115 -0
- package/{screenshots.json → packages/selector/screenshots.json} +6 -6
- package/packages/selector/src/client/CompressionProfileControls.tsx +229 -0
- package/packages/selector/src/client/CompressionProfileSelector.module.css +170 -0
- package/packages/selector/src/client/CompressionProfileSelector.tsx +79 -0
- package/packages/selector/src/client/CustomPolicyEditor.tsx +216 -0
- package/packages/selector/src/client/EstimatorControls.tsx +281 -0
- package/packages/selector/src/client/decode.ts +49 -0
- package/packages/selector/src/client/index.ts +111 -0
- package/packages/selector/src/client/locales.ts +198 -0
- package/packages/selector/src/client/preset-options.ts +70 -0
- package/packages/selector/src/client/settings-section.tsx +126 -0
- package/packages/selector/src/css-modules.d.ts +6 -0
- package/packages/selector/src/deepseek-v4-tokenizer.ts +210 -0
- package/packages/selector/src/estimator-catalog.ts +104 -0
- package/packages/selector/src/index.ts +327 -0
- package/packages/selector/src/invariant.ts +113 -0
- package/packages/selector/src/preset-overlay.ts +567 -0
- package/packages/selector/src/profiles.ts +342 -0
- package/packages/selector/src/pruner/content.ts +188 -0
- package/packages/selector/src/pruner/session.ts +94 -0
- package/packages/selector/src/pruner/state.ts +43 -0
- package/packages/selector/src/pruner/tuning.ts +23 -0
- package/packages/selector/src/pruner/types.ts +60 -0
- package/packages/selector/src/pruner.ts +2144 -0
- package/packages/selector/src/runtime/adaptive-cost.ts +194 -0
- package/packages/selector/src/runtime/audit.ts +215 -0
- package/packages/selector/src/runtime/config.ts +613 -0
- package/packages/selector/src/runtime/custom-policy.ts +278 -0
- package/packages/selector/src/runtime/deepseek-official-pricing.ts +298 -0
- package/packages/selector/src/runtime/deepseek-v4-vision-tokens.ts +254 -0
- package/packages/selector/src/runtime/measurement.ts +403 -0
- package/packages/selector/src/runtime/reducers.ts +656 -0
- package/packages/selector/src/runtime/retrieve.ts +457 -0
- package/packages/selector/src/runtime/session-events.ts +17 -0
- package/packages/selector/src/runtime/tail-trim.ts +166 -0
- package/packages/selector/src/runtime/token-count.ts +72 -0
- package/packages/selector/src/runtime/tokenpilot/dedup.ts +81 -0
- package/packages/selector/src/runtime/tokenpilot/estimator.ts +183 -0
- package/packages/selector/src/runtime/tokenpilot/locator.ts +128 -0
- package/packages/selector/src/runtime/tokenpilot/read-state.ts +77 -0
- package/packages/selector/src/runtime/types.ts +309 -0
- package/packages/selector/src/runtime/value.ts +48 -0
- package/packages/selector/tests/auto-compact.client.spec.tsx +226 -0
- package/packages/selector/tests/built/client-artifact.spec.ts +51 -0
- package/packages/selector/tests/cache-prefix-audit.spec.ts +123 -0
- package/packages/selector/tests/code-skeleton.client.spec.ts +88 -0
- package/packages/selector/tests/custom-contract.client.spec.ts +202 -0
- package/packages/selector/tests/estimator-catalog.spec.ts +70 -0
- package/packages/selector/tests/estimator-channel.client.spec.tsx +247 -0
- package/packages/selector/tests/estimator-route-registration.host.spec.ts +176 -0
- package/packages/selector/tests/host-preset-overlay.host.spec.ts +204 -0
- package/packages/selector/tests/preset-options-write.client.spec.ts +181 -0
- package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +196 -0
- package/packages/selector/tests/preset-overlay.host.spec.ts +243 -0
- package/packages/selector/tests/profiles.client.spec.tsx +434 -0
- package/packages/selector/tests/public/package-contract.client.spec.ts +33 -0
- package/packages/selector/tests/runtime/adaptive-cost.spec.ts +167 -0
- package/packages/selector/tests/runtime/audit.spec.ts +129 -0
- package/packages/selector/tests/runtime/auto-compact-config.spec.ts +523 -0
- package/packages/selector/tests/runtime/code-skeleton.spec.ts +141 -0
- package/packages/selector/tests/runtime/deepseek-official-pricing.spec.ts +186 -0
- package/packages/selector/tests/runtime/deepseek-v4-tokenizer.spec.ts +122 -0
- package/packages/selector/tests/runtime/deepseek-v4-vision-tokens.spec.ts +122 -0
- package/packages/selector/tests/runtime/fixtures/profile-baseline.json +273 -0
- package/packages/selector/tests/runtime/fixtures/tokenizer-golden.json +106 -0
- package/packages/selector/tests/runtime/fixtures/vision-golden.json +459 -0
- package/packages/selector/tests/runtime/public/public-runtime.spec.ts +2531 -0
- package/packages/selector/tests/runtime/session-events.spec.ts +27 -0
- package/packages/selector/tests/runtime/tokenizer-golden.spec.ts +53 -0
- package/packages/selector/tests/runtime/tokenpilot/dedup.spec.ts +52 -0
- package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +56 -0
- package/packages/selector/tests/runtime/tokenpilot/locator.spec.ts +76 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +100 -0
- package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +58 -0
- package/packages/selector/tests/runtime/value.spec.ts +23 -0
- package/packages/selector/tests/standing-generation.host.spec.ts +631 -0
- package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +250 -0
- package/packages/selector/tests/support/cache-prefix-audit.ts +105 -0
- package/packages/selector/tests/support/mock-adapter.ts +37 -0
- package/packages/selector/tests/support/ui-primitives.tsx +34 -0
- package/packages/selector/tsconfig.json +11 -0
- package/packages/selector/tsdown.client.config.ts +102 -0
- package/packages/selector/tsdown.config.ts +20 -0
- package/pnpm-workspace.yaml +19 -0
- package/scripts/capture-profile-baseline.ts +80 -0
- package/scripts/generate-tokenizer-fixtures.py +81 -0
- package/scripts/generate-vision-fixtures.py +208 -0
- package/scripts/packed-components-smoke.ts +713 -0
- package/scripts/packed-install-e2e.ts +1072 -0
- package/scripts/verify-release.ts +300 -0
- package/tests/TEST_INVENTORY.md +42 -0
- package/tsconfig.base.json +18 -0
- package/tsconfig.json +7 -0
- package/tsconfig.scripts.json +13 -0
- package/tsconfig.tests.json +15 -0
- package/vitest.built.config.ts +9 -0
- package/vitest.config.ts +43 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/LICENSE.DeepSeek-V4-Pro.txt +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/manifest.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer_config.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/LICENSE.DeepSeek-V4-Flash-Vision-Exp.txt +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/manifest.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer_config.json +0 -0
- /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-profiles.jpg +0 -0
- /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-settings.png +0 -0
- /package/{cordis.patch.yml → packages/selector/cordis.patch.yml} +0 -0
- /package/{dsh.plugin.json → packages/selector/dsh.plugin.json} +0 -0
- /package/{lib → packages/selector/lib}/client.d.ts +0 -0
- /package/{lib → packages/selector/lib}/client.js +0 -0
- /package/{lib → packages/selector/lib}/config.js +0 -0
- /package/{lib → packages/selector/lib}/index.d.ts +0 -0
- /package/{lib → packages/selector/lib}/index.js +0 -0
- /package/{lib → packages/selector/lib}/invariant.d.ts +0 -0
- /package/{lib → packages/selector/lib}/invariant.js +0 -0
- /package/{lib → packages/selector/lib}/pruner.d.ts +0 -0
- /package/{lib → packages/selector/lib}/pruner.js +0 -0
- /package/{lib → packages/selector/lib}/tail-trim.js +0 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
/** Deterministic, evidence-backed reducers for fresh tool results. */
|
|
2
|
+
|
|
3
|
+
import { codePointLength } from './config.ts'
|
|
4
|
+
|
|
5
|
+
/** Input shared by every fresh-result reducer. */
|
|
6
|
+
export interface ReducerInput {
|
|
7
|
+
readonly toolName: string
|
|
8
|
+
readonly argumentsText: string
|
|
9
|
+
readonly text: string
|
|
10
|
+
readonly budgetChars: number
|
|
11
|
+
readonly sourceRef: string
|
|
12
|
+
readonly isError: boolean
|
|
13
|
+
/**
|
|
14
|
+
* Orthogonal user gate for the `hypa-code-skeleton` candidate. Absent or
|
|
15
|
+
* false keeps source-code content on its existing head/tail reducers.
|
|
16
|
+
*/
|
|
17
|
+
readonly codeSkeleton?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One verified reducer candidate. */
|
|
21
|
+
export interface ReducerOutput {
|
|
22
|
+
readonly text: string
|
|
23
|
+
readonly reducer: string
|
|
24
|
+
readonly lossy: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ANSI_PATTERN = /\u001B(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001B\\))/gu
|
|
28
|
+
const IMPORTANT_PATTERN = new RegExp([
|
|
29
|
+
String.raw`\b(?:error|failed|failure|fatal|panic|exception|warning|warn|conflict|denied|forbidden|`,
|
|
30
|
+
String.raw`timeout|timed out|not found|cannot|unable|invalid|exit(?:ed)?\s+(?:code|status)|traceback|`,
|
|
31
|
+
String.raw`assert(?:ion)?|segmentation fault|oom|out of memory)\b`,
|
|
32
|
+
].join(''), 'i')
|
|
33
|
+
const STATUS_PATTERN = new RegExp([
|
|
34
|
+
String.raw`\b(?:success|succeeded|passed|installed|added|removed|updated|built|compiled|`,
|
|
35
|
+
String.raw`tests?\s+(?:passed|failed)|exit(?:ed)?\s+(?:code|status))\b`,
|
|
36
|
+
].join(''), 'i')
|
|
37
|
+
const PATH_LINE_PATTERN = /^(.*?):(\d+)(?::\d+)?(?::|\s+-\s+)(.*)$/
|
|
38
|
+
const GIT_STATUS_PATTERN = new RegExp([
|
|
39
|
+
String.raw`^(?:On branch|Your branch|HEAD detached|Changes |Untracked |Unmerged |\s*(?:modified|deleted|`,
|
|
40
|
+
String.raw`new file|renamed|both modified):)`,
|
|
41
|
+
].join(''), 'i')
|
|
42
|
+
const CODE_IMPORT_PATTERN = new RegExp([
|
|
43
|
+
String.raw`^\s*(?:import\b|from\s+[\w.]+\s+import\b|use\s+\w|package\s+|#include\b|`,
|
|
44
|
+
String.raw`using\s+[\w.]+;|require\s*\(|extern\s+crate\b)`,
|
|
45
|
+
].join(''))
|
|
46
|
+
const CODE_STRUCTURE_PATTERN = new RegExp([
|
|
47
|
+
String.raw`^\s*(?:@[\w.]+|export\s+|default\s+|declare\s+|abstract\s+|public\s+|private\s+|protected\s+|`,
|
|
48
|
+
String.raw`internal\s+|static\s+|final\s+|sealed\s+|override\s+|pub(?:\([^)]*\))?\s+|async\s+|unsafe\s+)*`,
|
|
49
|
+
String.raw`(?:function\b|class\b|interface\b|enum\b|struct\b|impl\b|trait\b|type\s+\w|fn\s|func\b|`,
|
|
50
|
+
String.raw`def\s|module\b|namespace\b|sub\s)`,
|
|
51
|
+
].join(''))
|
|
52
|
+
const PYTHON_STRUCTURE_PATTERN = /^\s*(?:async\s+)?def\s|^\s*class\s/
|
|
53
|
+
const CODE_DECORATOR_PATTERN = /^\s*@[\w.]+/
|
|
54
|
+
const CODE_COMMENT_PATTERN = /^\s*(?:\/\/|#|\/\*|\*)/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Select a reducer from verified tool, command, and content evidence.
|
|
58
|
+
* @param input - original result text, recovery source, and output budget.
|
|
59
|
+
* @returns a verified candidate, or `null` when every reducer fails open.
|
|
60
|
+
*/
|
|
61
|
+
export function reduceFreshToolResult(input: ReducerInput): ReducerOutput | null {
|
|
62
|
+
const normalized = normalizeTerminalText(input.text)
|
|
63
|
+
const prepared = { ...input, text: normalized }
|
|
64
|
+
const command = extractCommand(input.argumentsText)
|
|
65
|
+
const name = input.toolName.toLowerCase()
|
|
66
|
+
const candidates: Array<() => ReducerOutput | null> = []
|
|
67
|
+
|
|
68
|
+
if (looksLikeJson(normalized)) candidates.push(() => reduceJson(prepared))
|
|
69
|
+
if (isSearchTool(name, command)) candidates.push(() => reduceSearch(prepared))
|
|
70
|
+
if (isGitCommand(name, command)) candidates.push(() => reduceGit(prepared, command))
|
|
71
|
+
if (isPackageCommand(command)) candidates.push(() => reducePatternLog(prepared, 'hypa-package', packagePattern()))
|
|
72
|
+
if (isBuildOrTestCommand(command)) candidates.push(() => reducePatternLog(prepared, 'hypa-build-test', buildPattern()))
|
|
73
|
+
if (input.codeSkeleton === true && looksLikeSourceCode(normalized)) candidates.push(() => reduceCodeSkeleton(prepared))
|
|
74
|
+
if (isReadTool(name)) candidates.push(() => reduceHead(prepared, 'pi-head'))
|
|
75
|
+
if (isShellTool(name) || command !== '') candidates.push(() => reduceShell(prepared))
|
|
76
|
+
candidates.push(() => reduceSalient(prepared, 'generic-salience'))
|
|
77
|
+
|
|
78
|
+
for (const make of candidates) {
|
|
79
|
+
const candidate = make()
|
|
80
|
+
if (candidate !== null && verifyReduction(input, candidate)) return candidate
|
|
81
|
+
}
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build a recoverable placeholder for an old tool result.
|
|
87
|
+
* @param input - tool identity, source reference, size, status, and retained evidence.
|
|
88
|
+
* @returns a lossy placeholder that cites the immutable source event.
|
|
89
|
+
*/
|
|
90
|
+
export function historicalPlaceholder(input: {
|
|
91
|
+
readonly toolName: string
|
|
92
|
+
readonly sourceRef: string
|
|
93
|
+
readonly charsBefore: number
|
|
94
|
+
readonly isError: boolean
|
|
95
|
+
readonly text: string
|
|
96
|
+
readonly compact?: boolean
|
|
97
|
+
}): ReducerOutput {
|
|
98
|
+
const anchor = input.compact ? '' : importantAnchor(input.text, 360)
|
|
99
|
+
const lines = [
|
|
100
|
+
'[Old tool result content cleared from active context]',
|
|
101
|
+
`tool: ${input.toolName || 'unknown'}`,
|
|
102
|
+
`status: ${input.isError ? 'error' : 'completed'}`,
|
|
103
|
+
`original_chars: ${String(input.charsBefore)}`,
|
|
104
|
+
`source: ${input.sourceRef}`,
|
|
105
|
+
'retrieve: context_compression_retrieve({"ref":"' + input.sourceRef + '"})',
|
|
106
|
+
]
|
|
107
|
+
if (anchor !== '') lines.push(`retained_anchor: ${anchor}`)
|
|
108
|
+
return {
|
|
109
|
+
text: lines.join('\n'),
|
|
110
|
+
reducer: input.compact ? 'pair-preserving-tail-aging' : 'historical-tool-result-aging',
|
|
111
|
+
lossy: true,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Validate shrinkage, budget, recovery, and error retention.
|
|
117
|
+
* @param input - original reducer input and its safety requirements.
|
|
118
|
+
* @param output - candidate reduced text and reducer metadata.
|
|
119
|
+
* @returns whether the candidate is safe to land.
|
|
120
|
+
*/
|
|
121
|
+
export function verifyReduction(input: ReducerInput, output: ReducerOutput): boolean {
|
|
122
|
+
const before = codePointLength(input.text)
|
|
123
|
+
const after = codePointLength(output.text)
|
|
124
|
+
if (after <= 0 || after >= before || after > input.budgetChars) return false
|
|
125
|
+
if (output.lossy && !output.text.includes(input.sourceRef)) return false
|
|
126
|
+
if ((input.isError || IMPORTANT_PATTERN.test(input.text))
|
|
127
|
+
&& !IMPORTANT_PATTERN.test(output.text) && !output.text.includes('status: error')) return false
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Strip ANSI, collapse carriage-return progress redraws, and fold exact repeats.
|
|
133
|
+
* @param text - raw terminal output.
|
|
134
|
+
* @returns normalized terminal text.
|
|
135
|
+
*/
|
|
136
|
+
export function normalizeTerminalText(text: string): string {
|
|
137
|
+
const withoutAnsi = text.replace(ANSI_PATTERN, '')
|
|
138
|
+
const logical = withoutAnsi.split('\n').map((line) => {
|
|
139
|
+
const redraws = line.split('\r').filter(part => part !== '')
|
|
140
|
+
return redraws.at(-1) ?? ''
|
|
141
|
+
})
|
|
142
|
+
const folded: string[] = []
|
|
143
|
+
let previous: string | undefined
|
|
144
|
+
let count = 0
|
|
145
|
+
const flush = (): void => {
|
|
146
|
+
if (previous === undefined) return
|
|
147
|
+
folded.push(previous)
|
|
148
|
+
if (count > 1) folded.push(`[previous line repeated ${String(count - 1)} more times]`)
|
|
149
|
+
}
|
|
150
|
+
for (const line of logical) {
|
|
151
|
+
if (line === previous) {
|
|
152
|
+
count++
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
flush()
|
|
156
|
+
previous = line
|
|
157
|
+
count = 1
|
|
158
|
+
}
|
|
159
|
+
flush()
|
|
160
|
+
return folded.join('\n')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function reduceHead(input: ReducerInput, reducer: string): ReducerOutput | null {
|
|
164
|
+
const marker = omissionMarker(input, reducer)
|
|
165
|
+
const available = input.budgetChars - codePointLength(marker) - 1
|
|
166
|
+
if (available <= 0) return null
|
|
167
|
+
const head = takeWholeLinesFromHead(input.text, available)
|
|
168
|
+
if (head === input.text || head === '') return null
|
|
169
|
+
return { text: `${head}\n${marker}`, reducer, lossy: true }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function reduceTail(input: ReducerInput, reducer: string): ReducerOutput | null {
|
|
173
|
+
const marker = omissionMarker(input, reducer)
|
|
174
|
+
const available = input.budgetChars - codePointLength(marker) - 1
|
|
175
|
+
if (available <= 0) return null
|
|
176
|
+
const tail = takeWholeLinesFromTail(input.text, available)
|
|
177
|
+
if (tail === input.text || tail === '') return null
|
|
178
|
+
return { text: `${marker}\n${tail}`, reducer, lossy: true }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function reduceJson(input: ReducerInput): ReducerOutput | null {
|
|
182
|
+
let value: unknown
|
|
183
|
+
try {
|
|
184
|
+
value = JSON.parse(input.text)
|
|
185
|
+
} catch {
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
const minified = JSON.stringify(value)
|
|
189
|
+
if (codePointLength(minified) < codePointLength(input.text)
|
|
190
|
+
&& codePointLength(minified) <= input.budgetChars) {
|
|
191
|
+
return { text: minified, reducer: 'json-minify', lossy: false }
|
|
192
|
+
}
|
|
193
|
+
const envelope = {
|
|
194
|
+
$dsh_compression: {
|
|
195
|
+
kind: 'json-preview',
|
|
196
|
+
source: input.sourceRef,
|
|
197
|
+
original_chars: codePointLength(input.text),
|
|
198
|
+
},
|
|
199
|
+
value: shrinkJson(value, 0),
|
|
200
|
+
}
|
|
201
|
+
const text = JSON.stringify(envelope, null, 2)
|
|
202
|
+
if (codePointLength(text) <= input.budgetChars) {
|
|
203
|
+
return { text, reducer: 'json-structure-preview', lossy: true }
|
|
204
|
+
}
|
|
205
|
+
return null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function shrinkJson(value: unknown, depth: number): unknown {
|
|
209
|
+
if (depth >= 5) {
|
|
210
|
+
if (Array.isArray(value)) return `[array length=${String(value.length)} omitted]`
|
|
211
|
+
if (typeof value === 'object' && value !== null) return '[object omitted]'
|
|
212
|
+
return value
|
|
213
|
+
}
|
|
214
|
+
if (Array.isArray(value)) {
|
|
215
|
+
if (value.length <= 8) return value.map(entry => shrinkJson(entry, depth + 1))
|
|
216
|
+
return [
|
|
217
|
+
...value.slice(0, 3).map(entry => shrinkJson(entry, depth + 1)),
|
|
218
|
+
{ $dsh_omitted_items: value.length - 5 },
|
|
219
|
+
...value.slice(-2).map(entry => shrinkJson(entry, depth + 1)),
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
if (typeof value !== 'object' || value === null) {
|
|
223
|
+
if (typeof value === 'string' && codePointLength(value) > 800) {
|
|
224
|
+
return `${Array.from(value).slice(0, 500).join('')}…[${String(codePointLength(value) - 700)} chars omitted]…${Array.from(value).slice(-200).join('')}`
|
|
225
|
+
}
|
|
226
|
+
return value
|
|
227
|
+
}
|
|
228
|
+
const entries = Object.entries(value)
|
|
229
|
+
const important = entries.filter(([key]) => /error|warn|status|code|message|path|file|line|summary/i.test(key))
|
|
230
|
+
const selected = entries.length <= 18
|
|
231
|
+
? entries
|
|
232
|
+
: [...entries.slice(0, 10), ...important.filter(entry => !entries.slice(0, 10).includes(entry)).slice(0, 6), ...entries.slice(-2)]
|
|
233
|
+
const result: Record<string, unknown> = {}
|
|
234
|
+
for (const [key, entry] of selected) result[key] = shrinkJson(entry, depth + 1)
|
|
235
|
+
if (selected.length < entries.length) result.$dsh_omitted_keys = entries.length - selected.length
|
|
236
|
+
return result
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function reduceSearch(input: ReducerInput): ReducerOutput | null {
|
|
240
|
+
const lines = splitLines(input.text)
|
|
241
|
+
const groups = new Map<string, Array<{ line: string; important: boolean }>>()
|
|
242
|
+
const ungrouped: Array<{ line: string; important: boolean }> = []
|
|
243
|
+
for (const line of lines) {
|
|
244
|
+
const match = PATH_LINE_PATTERN.exec(line)
|
|
245
|
+
const row = { line, important: IMPORTANT_PATTERN.test(line) }
|
|
246
|
+
if (match === null) {
|
|
247
|
+
ungrouped.push(row)
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
const path = match[1] ?? '<unknown>'
|
|
251
|
+
const bucket = groups.get(path) ?? []
|
|
252
|
+
bucket.push(row)
|
|
253
|
+
groups.set(path, bucket)
|
|
254
|
+
}
|
|
255
|
+
if (groups.size === 0) return reduceSalient(input, 'search-salience')
|
|
256
|
+
const selected: string[] = []
|
|
257
|
+
let omitted = 0
|
|
258
|
+
for (const [path, rows] of groups) {
|
|
259
|
+
const keep = new Set<number>([0, rows.length - 1])
|
|
260
|
+
rows.forEach((row, index) => { if (row.important) keep.add(index) })
|
|
261
|
+
for (let index = 0; index < rows.length && keep.size < 5; index++) keep.add(index)
|
|
262
|
+
const indexes = [...keep].filter(index => index >= 0).sort((a, b) => a - b)
|
|
263
|
+
selected.push(`## ${path} (${String(rows.length)} matches)`)
|
|
264
|
+
for (const index of indexes) {
|
|
265
|
+
const row = rows[index]
|
|
266
|
+
if (row !== undefined) selected.push(row.line)
|
|
267
|
+
}
|
|
268
|
+
omitted += rows.length - indexes.length
|
|
269
|
+
}
|
|
270
|
+
for (const row of ungrouped.filter(row => row.important).slice(0, 12)) selected.push(row.line)
|
|
271
|
+
const header = `[search results compressed; ${String(omitted)} matches omitted; source: ${input.sourceRef}]`
|
|
272
|
+
const text = fitLines([header, ...selected], input.budgetChars, input.sourceRef)
|
|
273
|
+
return text === null ? null : { text, reducer: 'search-by-file', lossy: true }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function reduceGit(input: ReducerInput, command: string): ReducerOutput | null {
|
|
277
|
+
const lines = splitLines(input.text)
|
|
278
|
+
const lower = command.toLowerCase()
|
|
279
|
+
let keep: string[]
|
|
280
|
+
let reducer: string
|
|
281
|
+
if (/\bgit\s+(?:diff|show)\b/.test(lower)) {
|
|
282
|
+
reducer = 'hypa-git-diff'
|
|
283
|
+
keep = lines.filter(line => /^(?:diff --git|index |--- |\+\+\+ |@@ |[+-](?![+-]))/.test(line)
|
|
284
|
+
|| IMPORTANT_PATTERN.test(line))
|
|
285
|
+
} else if (/\bgit\s+(?:status|switch|checkout|merge|rebase|cherry-pick)\b/.test(lower)) {
|
|
286
|
+
reducer = 'hypa-git-status'
|
|
287
|
+
keep = lines.filter(line => GIT_STATUS_PATTERN.test(line)
|
|
288
|
+
|| IMPORTANT_PATTERN.test(line))
|
|
289
|
+
} else {
|
|
290
|
+
reducer = 'hypa-git-log'
|
|
291
|
+
keep = lines.filter(line => /^(?:commit\s+[0-9a-f]+|Author:|Date:|[0-9a-f]{7,}\s)/i.test(line)
|
|
292
|
+
|| IMPORTANT_PATTERN.test(line))
|
|
293
|
+
}
|
|
294
|
+
if (keep.length === 0) return reduceSalient(input, reducer)
|
|
295
|
+
const header = `[git output compressed; source: ${input.sourceRef}]`
|
|
296
|
+
const text = fitLines([header, ...keep, ...lines.slice(-8)], input.budgetChars, input.sourceRef)
|
|
297
|
+
return text === null ? null : { text, reducer, lossy: true }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function reducePatternLog(input: ReducerInput, reducer: string, pattern: RegExp): ReducerOutput | null {
|
|
301
|
+
const lines = splitLines(input.text)
|
|
302
|
+
const important = lines.filter(line => pattern.test(line) || IMPORTANT_PATTERN.test(line) || STATUS_PATTERN.test(line))
|
|
303
|
+
const header = `[command output compressed by ${reducer}; source: ${input.sourceRef}]`
|
|
304
|
+
const text = fitLines([header, ...important, ...lines.slice(-20)], input.budgetChars, input.sourceRef)
|
|
305
|
+
return text === null ? null : { text, reducer, lossy: true }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function reduceShell(input: ReducerInput): ReducerOutput | null {
|
|
309
|
+
const lines = splitLines(input.text)
|
|
310
|
+
const important = lines.filter(line => IMPORTANT_PATTERN.test(line))
|
|
311
|
+
if (important.length === 0) return reduceTail(input, 'pi-tail')
|
|
312
|
+
const header = `[shell/log output compressed; source: ${input.sourceRef}]`
|
|
313
|
+
const text = fitLines([header, ...important, '--- final output ---', ...lines.slice(-40)], input.budgetChars, input.sourceRef)
|
|
314
|
+
return text === null ? null : { text, reducer: 'shell-salience-tail', lossy: true }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function reduceSalient(input: ReducerInput, reducer: string): ReducerOutput | null {
|
|
318
|
+
const lines = splitLines(input.text)
|
|
319
|
+
if (lines.length < 3) return reduceHead(input, reducer)
|
|
320
|
+
const marker = omissionMarker(input, reducer)
|
|
321
|
+
const headBudget = Math.max(1, Math.floor((input.budgetChars - codePointLength(marker)) * 0.34))
|
|
322
|
+
const tailBudget = headBudget
|
|
323
|
+
const head = takeWholeLinesFromHead(input.text, headBudget)
|
|
324
|
+
const tail = takeWholeLinesFromTail(input.text, tailBudget)
|
|
325
|
+
const salient = lines.filter(line => IMPORTANT_PATTERN.test(line) || STATUS_PATTERN.test(line)).slice(0, 24)
|
|
326
|
+
const text = fitLines([head, ...salient, marker, tail], input.budgetChars, input.sourceRef)
|
|
327
|
+
return text === null ? null : { text, reducer, lossy: true }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Keep a source-file skeleton: imports, decorators, declaration signatures,
|
|
332
|
+
* comments at brace depth zero, and every error-signalling line, eliding the
|
|
333
|
+
* remaining bodies with counted markers. Covers brace languages (TS/JS, Rust,
|
|
334
|
+
* Go, Java, C family) and indent blocks (Python); unknown syntax fails open to
|
|
335
|
+
* the next candidate. Output is compressed evidence, not required to parse.
|
|
336
|
+
* @param input - original result text, recovery source, and output budget.
|
|
337
|
+
* @returns a verified candidate, or `null` when the text is not code-like.
|
|
338
|
+
*/
|
|
339
|
+
function reduceCodeSkeleton(input: ReducerInput): ReducerOutput | null {
|
|
340
|
+
const lines = splitLines(input.text)
|
|
341
|
+
const kept: string[] = []
|
|
342
|
+
let elided = 0
|
|
343
|
+
const flushElided = (): void => {
|
|
344
|
+
if (elided > 0) kept.push(`[... ${String(elided)} lines elided ...]`)
|
|
345
|
+
elided = 0
|
|
346
|
+
}
|
|
347
|
+
let depth = 0
|
|
348
|
+
let index = 0
|
|
349
|
+
const elideBraceBody = (): void => {
|
|
350
|
+
const startDepth = depth
|
|
351
|
+
index += 1
|
|
352
|
+
while (index < lines.length && depth > startDepth) {
|
|
353
|
+
const body = lines[index]
|
|
354
|
+
if (body === undefined) break
|
|
355
|
+
if (IMPORTANT_PATTERN.test(body)) {
|
|
356
|
+
flushElided()
|
|
357
|
+
kept.push(body)
|
|
358
|
+
} else {
|
|
359
|
+
elided += 1
|
|
360
|
+
}
|
|
361
|
+
depth += braceDelta(body)
|
|
362
|
+
index += 1
|
|
363
|
+
}
|
|
364
|
+
flushElided()
|
|
365
|
+
}
|
|
366
|
+
const keepPythonSignature = (signatureLine: string): void => {
|
|
367
|
+
// The signature line is already kept; every path below must advance the
|
|
368
|
+
// cursor past it so the caller's `continue` cannot revisit the same line.
|
|
369
|
+
index += 1
|
|
370
|
+
if (/:\s*$/.test(signatureLine)) {
|
|
371
|
+
elideIndentedBody(leadingIndent(signatureLine))
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
// Multi-line signature: keep continuation lines until the colon, then
|
|
375
|
+
// elide the indented body at the colon line's indent.
|
|
376
|
+
for (let guard = 0; guard < 6 && index < lines.length; guard += 1) {
|
|
377
|
+
const next = lines[index]
|
|
378
|
+
if (next === undefined) break
|
|
379
|
+
if (next.trim() !== '' && leadingIndent(next) <= leadingIndent(signatureLine)) break
|
|
380
|
+
flushElided()
|
|
381
|
+
kept.push(next)
|
|
382
|
+
index += 1
|
|
383
|
+
if (/:\s*$/.test(next)) {
|
|
384
|
+
elideIndentedBody(leadingIndent(next))
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
if (next.trim() !== '' && !/[:,(]\s*$/.test(next)) break
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const elideIndentedBody = (indent: number): void => {
|
|
391
|
+
while (index < lines.length) {
|
|
392
|
+
const body = lines[index]
|
|
393
|
+
if (body === undefined) break
|
|
394
|
+
if (body.trim() !== '' && leadingIndent(body) <= indent) break
|
|
395
|
+
if (IMPORTANT_PATTERN.test(body)) {
|
|
396
|
+
flushElided()
|
|
397
|
+
kept.push(body)
|
|
398
|
+
index += 1
|
|
399
|
+
continue
|
|
400
|
+
}
|
|
401
|
+
if (isCodeStructureLine(body) || CODE_DECORATOR_PATTERN.test(body)) {
|
|
402
|
+
flushElided()
|
|
403
|
+
kept.push(body)
|
|
404
|
+
keepPythonSignature(body)
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
elided += 1
|
|
408
|
+
index += 1
|
|
409
|
+
}
|
|
410
|
+
flushElided()
|
|
411
|
+
}
|
|
412
|
+
while (index < lines.length) {
|
|
413
|
+
const line = lines[index]
|
|
414
|
+
if (line === undefined) break
|
|
415
|
+
const delta = braceDelta(line)
|
|
416
|
+
if (IMPORTANT_PATTERN.test(line)) {
|
|
417
|
+
flushElided()
|
|
418
|
+
kept.push(line)
|
|
419
|
+
depth += delta
|
|
420
|
+
index += 1
|
|
421
|
+
continue
|
|
422
|
+
}
|
|
423
|
+
if (isCodeStructureLine(line) || CODE_IMPORT_PATTERN.test(line) || CODE_DECORATOR_PATTERN.test(line)) {
|
|
424
|
+
flushElided()
|
|
425
|
+
kept.push(line)
|
|
426
|
+
depth += delta
|
|
427
|
+
if (delta > 0) {
|
|
428
|
+
elideBraceBody()
|
|
429
|
+
continue
|
|
430
|
+
}
|
|
431
|
+
if (PYTHON_STRUCTURE_PATTERN.test(line)) {
|
|
432
|
+
keepPythonSignature(line)
|
|
433
|
+
continue
|
|
434
|
+
}
|
|
435
|
+
// Brace-language signature continuation: keep following lines until one
|
|
436
|
+
// opens a block, then elide that block.
|
|
437
|
+
let opened = false
|
|
438
|
+
for (let guard = 0; guard < 6 && index + 1 < lines.length; guard += 1) {
|
|
439
|
+
const next = lines[index + 1]
|
|
440
|
+
if (next === undefined) break
|
|
441
|
+
const nextDelta = braceDelta(next)
|
|
442
|
+
if (nextDelta === 0 && next.trim() !== '' && !/[:,(]\s*$/.test(next)) break
|
|
443
|
+
flushElided()
|
|
444
|
+
kept.push(next)
|
|
445
|
+
depth += nextDelta
|
|
446
|
+
index += 1
|
|
447
|
+
if (nextDelta > 0) {
|
|
448
|
+
opened = true
|
|
449
|
+
break
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (opened) elideBraceBody()
|
|
453
|
+
else index += 1
|
|
454
|
+
continue
|
|
455
|
+
}
|
|
456
|
+
if (depth === 0 && CODE_COMMENT_PATTERN.test(line)) {
|
|
457
|
+
flushElided()
|
|
458
|
+
kept.push(line)
|
|
459
|
+
} else {
|
|
460
|
+
elided += 1
|
|
461
|
+
}
|
|
462
|
+
depth += delta
|
|
463
|
+
index += 1
|
|
464
|
+
}
|
|
465
|
+
flushElided()
|
|
466
|
+
return finishSkeleton(kept, lines, input)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function finishSkeleton(
|
|
470
|
+
kept: readonly string[],
|
|
471
|
+
lines: readonly string[],
|
|
472
|
+
input: ReducerInput,
|
|
473
|
+
): ReducerOutput | null {
|
|
474
|
+
const header = `[code output compressed by hypa-code-skeleton; source: ${input.sourceRef}]`
|
|
475
|
+
const text = fitLines([header, ...kept, ...lines.slice(-4)], input.budgetChars, input.sourceRef)
|
|
476
|
+
return text === null ? null : { text, reducer: 'hypa-code-skeleton', lossy: true }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Net brace delta of one line, ignoring braces inside string literals. */
|
|
480
|
+
function braceDelta(line: string): number {
|
|
481
|
+
let delta = 0
|
|
482
|
+
let quote: string | null = null
|
|
483
|
+
for (let position = 0; position < line.length; position += 1) {
|
|
484
|
+
const char = line[position]
|
|
485
|
+
if (quote !== null) {
|
|
486
|
+
if (char === '\\') position += 1
|
|
487
|
+
else if (char === quote) quote = null
|
|
488
|
+
continue
|
|
489
|
+
}
|
|
490
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
491
|
+
quote = char
|
|
492
|
+
continue
|
|
493
|
+
}
|
|
494
|
+
if (char === '{') delta += 1
|
|
495
|
+
else if (char === '}') delta -= 1
|
|
496
|
+
}
|
|
497
|
+
return delta
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function leadingIndent(line: string): number {
|
|
501
|
+
return codePointLength(line) - codePointLength(line.trimStart())
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function isCodeStructureLine(line: string): boolean {
|
|
505
|
+
return CODE_STRUCTURE_PATTERN.test(line) || PYTHON_STRUCTURE_PATTERN.test(line)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Require content evidence of source code: enough declaration, import, or
|
|
510
|
+
* decorator lines among a bounded prefix. Failing this keeps prose, logs, and
|
|
511
|
+
* data on their existing reducers.
|
|
512
|
+
* @param text - normalized result text.
|
|
513
|
+
* @returns whether the text qualifies as source code.
|
|
514
|
+
*/
|
|
515
|
+
function looksLikeSourceCode(text: string): boolean {
|
|
516
|
+
const lines = splitLines(text)
|
|
517
|
+
if (lines.length < 12) return false
|
|
518
|
+
let evidence = 0
|
|
519
|
+
for (const line of lines.slice(0, 400)) {
|
|
520
|
+
if (isCodeStructureLine(line) || CODE_IMPORT_PATTERN.test(line) || CODE_DECORATOR_PATTERN.test(line)) {
|
|
521
|
+
evidence += 1
|
|
522
|
+
if (evidence >= 3) return true
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return false
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function omissionMarker(input: ReducerInput, reducer: string): string {
|
|
529
|
+
return `[... ${reducer} omitted content; original_chars=${String(codePointLength(input.text))}; source=${input.sourceRef}; retrieve with context_compression_retrieve ...]`
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function importantAnchor(text: string, maxChars: number): string {
|
|
533
|
+
const lines = splitLines(normalizeTerminalText(text))
|
|
534
|
+
const chosen = lines.find(line => IMPORTANT_PATTERN.test(line)) ?? lines.at(-1) ?? ''
|
|
535
|
+
return Array.from(chosen.trim()).slice(0, maxChars).join('')
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function fitLines(lines: readonly string[], budgetChars: number, requiredRef: string): string | null {
|
|
539
|
+
const unique: string[] = []
|
|
540
|
+
const seen = new Set<string>()
|
|
541
|
+
for (const line of lines) {
|
|
542
|
+
if (line === '' || seen.has(line)) continue
|
|
543
|
+
seen.add(line)
|
|
544
|
+
unique.push(line)
|
|
545
|
+
}
|
|
546
|
+
const output: string[] = []
|
|
547
|
+
let used = 0
|
|
548
|
+
for (const line of unique) {
|
|
549
|
+
const cost = codePointLength(line) + (output.length === 0 ? 0 : 1)
|
|
550
|
+
if (used + cost > budgetChars) continue
|
|
551
|
+
output.push(line)
|
|
552
|
+
used += cost
|
|
553
|
+
}
|
|
554
|
+
const text = output.join('\n')
|
|
555
|
+
return text.includes(requiredRef) ? text : null
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function takeWholeLinesFromHead(text: string, budgetChars: number): string {
|
|
559
|
+
const output: string[] = []
|
|
560
|
+
let used = 0
|
|
561
|
+
for (const line of splitLines(text)) {
|
|
562
|
+
const cost = codePointLength(line) + (output.length === 0 ? 0 : 1)
|
|
563
|
+
if (used + cost > budgetChars) break
|
|
564
|
+
output.push(line)
|
|
565
|
+
used += cost
|
|
566
|
+
}
|
|
567
|
+
if (output.length === 0) return Array.from(text).slice(0, budgetChars).join('')
|
|
568
|
+
return output.join('\n')
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function takeWholeLinesFromTail(text: string, budgetChars: number): string {
|
|
572
|
+
const lines = splitLines(text)
|
|
573
|
+
const output: string[] = []
|
|
574
|
+
let used = 0
|
|
575
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
576
|
+
const line = lines[index]
|
|
577
|
+
if (line === undefined) continue
|
|
578
|
+
const cost = codePointLength(line) + (output.length === 0 ? 0 : 1)
|
|
579
|
+
if (used + cost > budgetChars) break
|
|
580
|
+
output.unshift(line)
|
|
581
|
+
used += cost
|
|
582
|
+
}
|
|
583
|
+
if (output.length === 0) return Array.from(text).slice(-budgetChars).join('')
|
|
584
|
+
return output.join('\n')
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function splitLines(text: string): string[] {
|
|
588
|
+
const lines = text.split('\n')
|
|
589
|
+
if (text.endsWith('\n')) lines.pop()
|
|
590
|
+
return lines
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function extractCommand(argumentsText: string): string {
|
|
594
|
+
try {
|
|
595
|
+
const parsed = JSON.parse(argumentsText) as unknown
|
|
596
|
+
if (typeof parsed !== 'object' || parsed === null) return ''
|
|
597
|
+
const record = parsed as Record<string, unknown>
|
|
598
|
+
for (const key of ['command', 'cmd', 'script', 'input']) {
|
|
599
|
+
const value = record[key]
|
|
600
|
+
if (typeof value === 'string') return value
|
|
601
|
+
}
|
|
602
|
+
} catch {
|
|
603
|
+
return ''
|
|
604
|
+
}
|
|
605
|
+
return ''
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function looksLikeJson(text: string): boolean {
|
|
609
|
+
const trimmed = text.trim()
|
|
610
|
+
return (trimmed.startsWith('{') && trimmed.endsWith('}'))
|
|
611
|
+
|| (trimmed.startsWith('[') && trimmed.endsWith(']'))
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function isReadTool(name: string): boolean {
|
|
615
|
+
return /(?:^|[-_/])(?:read|cat|view|open_file)(?:$|[-_/])/.test(name)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function isSearchTool(name: string, command: string): boolean {
|
|
619
|
+
return /(?:grep|search|glob|find|ripgrep|rg)/.test(name)
|
|
620
|
+
|| /(?:^|\s)(?:rg|grep|find|fd)\s/.test(command)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function isShellTool(name: string): boolean {
|
|
624
|
+
return /(?:bash|shell|terminal|powershell|pwsh|exec|command)/.test(name)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function isGitCommand(name: string, command: string): boolean {
|
|
628
|
+
return name.includes('git') || /(?:^|\s)git\s/.test(command)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function isPackageCommand(command: string): boolean {
|
|
632
|
+
return /(?:^|\s)(?:npm|pnpm|yarn|bun|pip|pip3|uv|poetry)\s/.test(command)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function isBuildOrTestCommand(command: string): boolean {
|
|
636
|
+
const pattern = new RegExp([
|
|
637
|
+
String.raw`(?:^|\s)(?:tsc|dotnet\s+(?:build|test)|pytest|cargo\s+(?:build|test|check)|go\s+test|mvn\s+test|`,
|
|
638
|
+
String.raw`gradle|npm\s+(?:test|run\s+build)|pnpm\s+(?:test|build|lint)|yarn\s+(?:test|build|lint))\b`,
|
|
639
|
+
].join(''))
|
|
640
|
+
return pattern.test(command)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function packagePattern(): RegExp {
|
|
644
|
+
return new RegExp([
|
|
645
|
+
String.raw`(?:ERR!|WARN|warning|error|failed|conflict|peer dep|added\s+\d+|removed\s+\d+|installed|success|`,
|
|
646
|
+
String.raw`up to date|packages?\s+(?:added|removed|changed)|resolution|No matching distribution|Could not find a version)`,
|
|
647
|
+
].join(''), 'i')
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function buildPattern(): RegExp {
|
|
651
|
+
return new RegExp([
|
|
652
|
+
String.raw`(?:error\s+TS\d+|warning\s+TS\d+|FAILED|FAIL\b|AssertionError|expected|actual|`,
|
|
653
|
+
String.raw`tests?\s+(?:run|passed|failed|skipped)|Build\s+(?:succeeded|FAILED)|\d+\s+Error\(s\)|`,
|
|
654
|
+
String.raw`\d+\s+Warning\(s\)|Finished\s+test|compilation failed)`,
|
|
655
|
+
].join(''), 'i')
|
|
656
|
+
}
|