@hasna/terminal 2.3.0 → 2.3.2
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/dist/App.js +404 -0
- package/dist/Browse.js +79 -0
- package/dist/FuzzyPicker.js +47 -0
- package/dist/Onboarding.js +51 -0
- package/dist/Spinner.js +12 -0
- package/dist/StatusBar.js +49 -0
- package/dist/ai.js +322 -0
- package/dist/cache.js +41 -0
- package/dist/cli.js +64 -16
- package/dist/command-rewriter.js +64 -0
- package/dist/command-validator.js +86 -0
- package/dist/compression.js +107 -0
- package/dist/context-hints.js +275 -0
- package/dist/diff-cache.js +107 -0
- package/dist/discover.js +212 -0
- package/dist/economy.js +123 -0
- package/dist/expand-store.js +38 -0
- package/dist/file-cache.js +72 -0
- package/dist/file-index.js +62 -0
- package/dist/history.js +62 -0
- package/dist/lazy-executor.js +54 -0
- package/dist/line-dedup.js +59 -0
- package/dist/loop-detector.js +75 -0
- package/dist/mcp/install.js +98 -0
- package/dist/mcp/server.js +569 -0
- package/dist/noise-filter.js +86 -0
- package/dist/output-processor.js +129 -0
- package/dist/output-router.js +41 -0
- package/dist/output-store.js +111 -0
- package/dist/parsers/base.js +2 -0
- package/dist/parsers/build.js +64 -0
- package/dist/parsers/errors.js +101 -0
- package/dist/parsers/files.js +78 -0
- package/dist/parsers/git.js +99 -0
- package/dist/parsers/index.js +48 -0
- package/dist/parsers/tests.js +89 -0
- package/dist/providers/anthropic.js +39 -0
- package/dist/providers/base.js +4 -0
- package/dist/providers/cerebras.js +95 -0
- package/dist/providers/groq.js +95 -0
- package/dist/providers/index.js +73 -0
- package/dist/providers/xai.js +95 -0
- package/dist/recipes/model.js +20 -0
- package/dist/recipes/storage.js +136 -0
- package/dist/search/content-search.js +68 -0
- package/dist/search/file-search.js +61 -0
- package/dist/search/filters.js +34 -0
- package/dist/search/index.js +5 -0
- package/dist/search/semantic.js +320 -0
- package/dist/session-boot.js +59 -0
- package/dist/session-context.js +55 -0
- package/dist/sessions-db.js +173 -0
- package/dist/smart-display.js +286 -0
- package/dist/snapshots.js +51 -0
- package/dist/supervisor.js +112 -0
- package/dist/test-watchlist.js +131 -0
- package/dist/tool-profiles.js +122 -0
- package/dist/tree.js +94 -0
- package/dist/usage-cache.js +65 -0
- package/package.json +8 -1
- package/src/ai.ts +8 -0
- package/src/cli.tsx +57 -18
- package/src/output-processor.ts +6 -1
- package/src/output-store.ts +58 -12
- package/src/tool-profiles.ts +139 -0
- package/.claude/scheduled_tasks.lock +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -20
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
- package/CONTRIBUTING.md +0 -80
- package/benchmarks/benchmark.mjs +0 -115
- package/imported_modules.txt +0 -0
- package/temp/rtk/.claude/agents/code-reviewer.md +0 -221
- package/temp/rtk/.claude/agents/debugger.md +0 -519
- package/temp/rtk/.claude/agents/rtk-testing-specialist.md +0 -461
- package/temp/rtk/.claude/agents/rust-rtk.md +0 -511
- package/temp/rtk/.claude/agents/technical-writer.md +0 -355
- package/temp/rtk/.claude/commands/diagnose.md +0 -352
- package/temp/rtk/.claude/commands/test-routing.md +0 -362
- package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +0 -16
- package/temp/rtk/.claude/hooks/rtk-rewrite.sh +0 -70
- package/temp/rtk/.claude/hooks/rtk-suggest.sh +0 -152
- package/temp/rtk/.claude/rules/cli-testing.md +0 -526
- package/temp/rtk/.claude/skills/issue-triage/SKILL.md +0 -348
- package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +0 -134
- package/temp/rtk/.claude/skills/performance.md +0 -435
- package/temp/rtk/.claude/skills/pr-triage/SKILL.md +0 -315
- package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +0 -71
- package/temp/rtk/.claude/skills/repo-recap.md +0 -206
- package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +0 -78
- package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +0 -124
- package/temp/rtk/.claude/skills/security-guardian.md +0 -503
- package/temp/rtk/.claude/skills/ship.md +0 -404
- package/temp/rtk/.github/workflows/benchmark.yml +0 -34
- package/temp/rtk/.github/workflows/dco-check.yaml +0 -12
- package/temp/rtk/.github/workflows/release-please.yml +0 -51
- package/temp/rtk/.github/workflows/release.yml +0 -343
- package/temp/rtk/.github/workflows/security-check.yml +0 -135
- package/temp/rtk/.github/workflows/validate-docs.yml +0 -78
- package/temp/rtk/.release-please-manifest.json +0 -3
- package/temp/rtk/ARCHITECTURE.md +0 -1491
- package/temp/rtk/CHANGELOG.md +0 -640
- package/temp/rtk/CLAUDE.md +0 -605
- package/temp/rtk/CONTRIBUTING.md +0 -199
- package/temp/rtk/Cargo.lock +0 -1668
- package/temp/rtk/Cargo.toml +0 -64
- package/temp/rtk/Formula/rtk.rb +0 -43
- package/temp/rtk/INSTALL.md +0 -390
- package/temp/rtk/LICENSE +0 -21
- package/temp/rtk/README.md +0 -386
- package/temp/rtk/README_es.md +0 -159
- package/temp/rtk/README_fr.md +0 -197
- package/temp/rtk/README_ja.md +0 -159
- package/temp/rtk/README_ko.md +0 -159
- package/temp/rtk/README_zh.md +0 -167
- package/temp/rtk/ROADMAP.md +0 -15
- package/temp/rtk/SECURITY.md +0 -217
- package/temp/rtk/TEST_EXEC_TIME.md +0 -102
- package/temp/rtk/build.rs +0 -57
- package/temp/rtk/docs/AUDIT_GUIDE.md +0 -432
- package/temp/rtk/docs/FEATURES.md +0 -1410
- package/temp/rtk/docs/TROUBLESHOOTING.md +0 -309
- package/temp/rtk/docs/filter-workflow.md +0 -102
- package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
- package/temp/rtk/docs/tracking.md +0 -583
- package/temp/rtk/hooks/opencode-rtk.ts +0 -39
- package/temp/rtk/hooks/rtk-awareness.md +0 -29
- package/temp/rtk/hooks/rtk-rewrite.sh +0 -61
- package/temp/rtk/hooks/test-rtk-rewrite.sh +0 -442
- package/temp/rtk/install.sh +0 -124
- package/temp/rtk/release-please-config.json +0 -10
- package/temp/rtk/scripts/benchmark.sh +0 -592
- package/temp/rtk/scripts/check-installation.sh +0 -162
- package/temp/rtk/scripts/install-local.sh +0 -37
- package/temp/rtk/scripts/rtk-economics.sh +0 -137
- package/temp/rtk/scripts/test-all.sh +0 -561
- package/temp/rtk/scripts/test-aristote.sh +0 -227
- package/temp/rtk/scripts/test-tracking.sh +0 -79
- package/temp/rtk/scripts/update-readme-metrics.sh +0 -32
- package/temp/rtk/scripts/validate-docs.sh +0 -73
- package/temp/rtk/src/aws_cmd.rs +0 -880
- package/temp/rtk/src/binlog.rs +0 -1645
- package/temp/rtk/src/cargo_cmd.rs +0 -1727
- package/temp/rtk/src/cc_economics.rs +0 -1157
- package/temp/rtk/src/ccusage.rs +0 -340
- package/temp/rtk/src/config.rs +0 -187
- package/temp/rtk/src/container.rs +0 -855
- package/temp/rtk/src/curl_cmd.rs +0 -134
- package/temp/rtk/src/deps.rs +0 -268
- package/temp/rtk/src/diff_cmd.rs +0 -367
- package/temp/rtk/src/discover/mod.rs +0 -274
- package/temp/rtk/src/discover/provider.rs +0 -388
- package/temp/rtk/src/discover/registry.rs +0 -2022
- package/temp/rtk/src/discover/report.rs +0 -202
- package/temp/rtk/src/discover/rules.rs +0 -667
- package/temp/rtk/src/display_helpers.rs +0 -402
- package/temp/rtk/src/dotnet_cmd.rs +0 -1771
- package/temp/rtk/src/dotnet_format_report.rs +0 -133
- package/temp/rtk/src/dotnet_trx.rs +0 -593
- package/temp/rtk/src/env_cmd.rs +0 -204
- package/temp/rtk/src/filter.rs +0 -462
- package/temp/rtk/src/filters/README.md +0 -52
- package/temp/rtk/src/filters/ansible-playbook.toml +0 -34
- package/temp/rtk/src/filters/basedpyright.toml +0 -47
- package/temp/rtk/src/filters/biome.toml +0 -45
- package/temp/rtk/src/filters/brew-install.toml +0 -37
- package/temp/rtk/src/filters/composer-install.toml +0 -40
- package/temp/rtk/src/filters/df.toml +0 -16
- package/temp/rtk/src/filters/dotnet-build.toml +0 -64
- package/temp/rtk/src/filters/du.toml +0 -16
- package/temp/rtk/src/filters/fail2ban-client.toml +0 -15
- package/temp/rtk/src/filters/gcc.toml +0 -49
- package/temp/rtk/src/filters/gcloud.toml +0 -22
- package/temp/rtk/src/filters/hadolint.toml +0 -24
- package/temp/rtk/src/filters/helm.toml +0 -29
- package/temp/rtk/src/filters/iptables.toml +0 -27
- package/temp/rtk/src/filters/jj.toml +0 -28
- package/temp/rtk/src/filters/jq.toml +0 -24
- package/temp/rtk/src/filters/make.toml +0 -41
- package/temp/rtk/src/filters/markdownlint.toml +0 -24
- package/temp/rtk/src/filters/mix-compile.toml +0 -27
- package/temp/rtk/src/filters/mix-format.toml +0 -15
- package/temp/rtk/src/filters/mvn-build.toml +0 -44
- package/temp/rtk/src/filters/oxlint.toml +0 -43
- package/temp/rtk/src/filters/ping.toml +0 -63
- package/temp/rtk/src/filters/pio-run.toml +0 -40
- package/temp/rtk/src/filters/poetry-install.toml +0 -50
- package/temp/rtk/src/filters/pre-commit.toml +0 -35
- package/temp/rtk/src/filters/ps.toml +0 -16
- package/temp/rtk/src/filters/quarto-render.toml +0 -41
- package/temp/rtk/src/filters/rsync.toml +0 -48
- package/temp/rtk/src/filters/shellcheck.toml +0 -27
- package/temp/rtk/src/filters/shopify-theme.toml +0 -29
- package/temp/rtk/src/filters/skopeo.toml +0 -45
- package/temp/rtk/src/filters/sops.toml +0 -16
- package/temp/rtk/src/filters/ssh.toml +0 -44
- package/temp/rtk/src/filters/stat.toml +0 -34
- package/temp/rtk/src/filters/swift-build.toml +0 -41
- package/temp/rtk/src/filters/systemctl-status.toml +0 -33
- package/temp/rtk/src/filters/terraform-plan.toml +0 -35
- package/temp/rtk/src/filters/tofu-fmt.toml +0 -16
- package/temp/rtk/src/filters/tofu-init.toml +0 -38
- package/temp/rtk/src/filters/tofu-plan.toml +0 -35
- package/temp/rtk/src/filters/tofu-validate.toml +0 -17
- package/temp/rtk/src/filters/trunk-build.toml +0 -39
- package/temp/rtk/src/filters/ty.toml +0 -50
- package/temp/rtk/src/filters/uv-sync.toml +0 -37
- package/temp/rtk/src/filters/xcodebuild.toml +0 -99
- package/temp/rtk/src/filters/yamllint.toml +0 -25
- package/temp/rtk/src/find_cmd.rs +0 -598
- package/temp/rtk/src/format_cmd.rs +0 -386
- package/temp/rtk/src/gain.rs +0 -723
- package/temp/rtk/src/gh_cmd.rs +0 -1651
- package/temp/rtk/src/git.rs +0 -2012
- package/temp/rtk/src/go_cmd.rs +0 -592
- package/temp/rtk/src/golangci_cmd.rs +0 -254
- package/temp/rtk/src/grep_cmd.rs +0 -288
- package/temp/rtk/src/gt_cmd.rs +0 -810
- package/temp/rtk/src/hook_audit_cmd.rs +0 -283
- package/temp/rtk/src/hook_check.rs +0 -171
- package/temp/rtk/src/init.rs +0 -1859
- package/temp/rtk/src/integrity.rs +0 -537
- package/temp/rtk/src/json_cmd.rs +0 -231
- package/temp/rtk/src/learn/detector.rs +0 -628
- package/temp/rtk/src/learn/mod.rs +0 -119
- package/temp/rtk/src/learn/report.rs +0 -184
- package/temp/rtk/src/lint_cmd.rs +0 -694
- package/temp/rtk/src/local_llm.rs +0 -316
- package/temp/rtk/src/log_cmd.rs +0 -248
- package/temp/rtk/src/ls.rs +0 -324
- package/temp/rtk/src/main.rs +0 -2482
- package/temp/rtk/src/mypy_cmd.rs +0 -389
- package/temp/rtk/src/next_cmd.rs +0 -241
- package/temp/rtk/src/npm_cmd.rs +0 -236
- package/temp/rtk/src/parser/README.md +0 -267
- package/temp/rtk/src/parser/error.rs +0 -46
- package/temp/rtk/src/parser/formatter.rs +0 -336
- package/temp/rtk/src/parser/mod.rs +0 -311
- package/temp/rtk/src/parser/types.rs +0 -119
- package/temp/rtk/src/pip_cmd.rs +0 -302
- package/temp/rtk/src/playwright_cmd.rs +0 -479
- package/temp/rtk/src/pnpm_cmd.rs +0 -573
- package/temp/rtk/src/prettier_cmd.rs +0 -221
- package/temp/rtk/src/prisma_cmd.rs +0 -482
- package/temp/rtk/src/psql_cmd.rs +0 -382
- package/temp/rtk/src/pytest_cmd.rs +0 -384
- package/temp/rtk/src/read.rs +0 -217
- package/temp/rtk/src/rewrite_cmd.rs +0 -50
- package/temp/rtk/src/ruff_cmd.rs +0 -402
- package/temp/rtk/src/runner.rs +0 -271
- package/temp/rtk/src/summary.rs +0 -297
- package/temp/rtk/src/tee.rs +0 -405
- package/temp/rtk/src/telemetry.rs +0 -248
- package/temp/rtk/src/toml_filter.rs +0 -1655
- package/temp/rtk/src/tracking.rs +0 -1416
- package/temp/rtk/src/tree.rs +0 -209
- package/temp/rtk/src/tsc_cmd.rs +0 -259
- package/temp/rtk/src/utils.rs +0 -432
- package/temp/rtk/src/verify_cmd.rs +0 -47
- package/temp/rtk/src/vitest_cmd.rs +0 -385
- package/temp/rtk/src/wc_cmd.rs +0 -401
- package/temp/rtk/src/wget_cmd.rs +0 -260
- package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +0 -11
- package/temp/rtk/tests/fixtures/dotnet/format_changes.json +0 -31
- package/temp/rtk/tests/fixtures/dotnet/format_empty.json +0 -1
- package/temp/rtk/tests/fixtures/dotnet/format_success.json +0 -12
- package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +0 -18
- package/tsconfig.json +0 -15
|
@@ -1,628 +0,0 @@
|
|
|
1
|
-
use lazy_static::lazy_static;
|
|
2
|
-
use regex::Regex;
|
|
3
|
-
|
|
4
|
-
#[derive(Debug, Clone, PartialEq)]
|
|
5
|
-
pub enum ErrorType {
|
|
6
|
-
UnknownFlag,
|
|
7
|
-
CommandNotFound,
|
|
8
|
-
WrongSyntax,
|
|
9
|
-
WrongPath,
|
|
10
|
-
MissingArg,
|
|
11
|
-
PermissionDenied,
|
|
12
|
-
Other(String),
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
impl ErrorType {
|
|
16
|
-
pub fn as_str(&self) -> &str {
|
|
17
|
-
match self {
|
|
18
|
-
ErrorType::UnknownFlag => "Unknown Flag",
|
|
19
|
-
ErrorType::CommandNotFound => "Command Not Found",
|
|
20
|
-
ErrorType::WrongSyntax => "Wrong Syntax",
|
|
21
|
-
ErrorType::WrongPath => "Wrong Path",
|
|
22
|
-
ErrorType::MissingArg => "Missing Argument",
|
|
23
|
-
ErrorType::PermissionDenied => "Permission Denied",
|
|
24
|
-
ErrorType::Other(s) => s,
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
#[derive(Debug, Clone)]
|
|
30
|
-
pub struct CorrectionPair {
|
|
31
|
-
pub wrong_command: String,
|
|
32
|
-
pub right_command: String,
|
|
33
|
-
pub error_output: String,
|
|
34
|
-
pub error_type: ErrorType,
|
|
35
|
-
pub confidence: f64,
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
#[derive(Debug, Clone)]
|
|
39
|
-
pub struct CorrectionRule {
|
|
40
|
-
pub wrong_pattern: String,
|
|
41
|
-
pub right_pattern: String,
|
|
42
|
-
pub error_type: ErrorType,
|
|
43
|
-
pub occurrences: usize,
|
|
44
|
-
pub base_command: String,
|
|
45
|
-
pub example_error: String,
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
lazy_static! {
|
|
49
|
-
static ref UNKNOWN_FLAG_RE: Regex = Regex::new(
|
|
50
|
-
r"(?i)(unexpected argument|unknown (option|flag)|unrecognized (option|flag)|invalid (option|flag))"
|
|
51
|
-
).unwrap();
|
|
52
|
-
|
|
53
|
-
static ref CMD_NOT_FOUND_RE: Regex = Regex::new(
|
|
54
|
-
r"(?i)(command not found|not recognized as an internal|no such file or directory.*command)"
|
|
55
|
-
).unwrap();
|
|
56
|
-
|
|
57
|
-
static ref WRONG_PATH_RE: Regex = Regex::new(
|
|
58
|
-
r"(?i)(no such file or directory|cannot find the path|file not found)"
|
|
59
|
-
).unwrap();
|
|
60
|
-
|
|
61
|
-
static ref MISSING_ARG_RE: Regex = Regex::new(
|
|
62
|
-
r"(?i)(requires a value|requires an argument|missing (required )?argument|expected.*argument)"
|
|
63
|
-
).unwrap();
|
|
64
|
-
|
|
65
|
-
static ref PERMISSION_DENIED_RE: Regex = Regex::new(
|
|
66
|
-
r"(?i)(permission denied|access denied|not permitted)"
|
|
67
|
-
).unwrap();
|
|
68
|
-
|
|
69
|
-
// User rejection patterns - NOT actual errors
|
|
70
|
-
static ref USER_REJECTION_RE: Regex = Regex::new(
|
|
71
|
-
r"(?i)(user (doesn't want|declined|rejected|cancelled)|operation (cancelled|aborted) by user)"
|
|
72
|
-
).unwrap();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/// Filters out user rejections - requires actual error-indicating content
|
|
76
|
-
pub fn is_command_error(is_error: bool, output: &str) -> bool {
|
|
77
|
-
if !is_error {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Reject if it's a user rejection
|
|
82
|
-
if USER_REJECTION_RE.is_match(output) {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Must contain error-indicating content
|
|
87
|
-
let output_lower = output.to_lowercase();
|
|
88
|
-
output_lower.contains("error")
|
|
89
|
-
|| output_lower.contains("failed")
|
|
90
|
-
|| output_lower.contains("unknown")
|
|
91
|
-
|| output_lower.contains("invalid")
|
|
92
|
-
|| output_lower.contains("not found")
|
|
93
|
-
|| output_lower.contains("permission denied")
|
|
94
|
-
|| output_lower.contains("cannot")
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
pub fn classify_error(output: &str) -> ErrorType {
|
|
98
|
-
if UNKNOWN_FLAG_RE.is_match(output) {
|
|
99
|
-
ErrorType::UnknownFlag
|
|
100
|
-
} else if CMD_NOT_FOUND_RE.is_match(output) {
|
|
101
|
-
ErrorType::CommandNotFound
|
|
102
|
-
} else if MISSING_ARG_RE.is_match(output) {
|
|
103
|
-
ErrorType::MissingArg
|
|
104
|
-
} else if PERMISSION_DENIED_RE.is_match(output) {
|
|
105
|
-
ErrorType::PermissionDenied
|
|
106
|
-
} else if WRONG_PATH_RE.is_match(output) {
|
|
107
|
-
ErrorType::WrongPath
|
|
108
|
-
} else {
|
|
109
|
-
ErrorType::Other("General Error".to_string())
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/// Represents a command with its execution result for correction detection
|
|
114
|
-
pub struct CommandExecution {
|
|
115
|
-
pub command: String,
|
|
116
|
-
pub is_error: bool,
|
|
117
|
-
pub output: String,
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const CORRECTION_WINDOW: usize = 3;
|
|
121
|
-
const MIN_CONFIDENCE: f64 = 0.6;
|
|
122
|
-
|
|
123
|
-
/// Extract base command (first 1-2 tokens, stripping env prefixes)
|
|
124
|
-
pub fn extract_base_command(cmd: &str) -> String {
|
|
125
|
-
let trimmed = cmd.trim();
|
|
126
|
-
|
|
127
|
-
// Strip common env prefixes
|
|
128
|
-
let stripped = trimmed
|
|
129
|
-
.strip_prefix("RUST_BACKTRACE=1 ")
|
|
130
|
-
.or_else(|| trimmed.strip_prefix("NODE_ENV=production "))
|
|
131
|
-
.or_else(|| trimmed.strip_prefix("DEBUG=* "))
|
|
132
|
-
.unwrap_or(trimmed);
|
|
133
|
-
|
|
134
|
-
// Get first 1-2 tokens
|
|
135
|
-
let parts: Vec<&str> = stripped.split_whitespace().collect();
|
|
136
|
-
match parts.len() {
|
|
137
|
-
0 => String::new(),
|
|
138
|
-
1 => parts[0].to_string(),
|
|
139
|
-
_ => format!("{} {}", parts[0], parts[1]),
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/// Calculate similarity between two commands using Jaccard similarity
|
|
144
|
-
/// Same base command = 0.5 base score + up to 0.5 from argument similarity
|
|
145
|
-
pub fn command_similarity(a: &str, b: &str) -> f64 {
|
|
146
|
-
let base_a = extract_base_command(a);
|
|
147
|
-
let base_b = extract_base_command(b);
|
|
148
|
-
|
|
149
|
-
if base_a != base_b {
|
|
150
|
-
return 0.0;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Extract args (everything after base command)
|
|
154
|
-
let args_a: std::collections::HashSet<&str> = a
|
|
155
|
-
.strip_prefix(&base_a)
|
|
156
|
-
.unwrap_or("")
|
|
157
|
-
.split_whitespace()
|
|
158
|
-
.collect();
|
|
159
|
-
|
|
160
|
-
let args_b: std::collections::HashSet<&str> = b
|
|
161
|
-
.strip_prefix(&base_b)
|
|
162
|
-
.unwrap_or("")
|
|
163
|
-
.split_whitespace()
|
|
164
|
-
.collect();
|
|
165
|
-
|
|
166
|
-
if args_a.is_empty() && args_b.is_empty() {
|
|
167
|
-
return 1.0; // Identical commands
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
let intersection = args_a.intersection(&args_b).count();
|
|
171
|
-
let union = args_a.union(&args_b).count();
|
|
172
|
-
|
|
173
|
-
if union == 0 {
|
|
174
|
-
return 0.5; // Same base, no args
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// 0.5 for same base + up to 0.5 for arg similarity
|
|
178
|
-
0.5 + (intersection as f64 / union as f64) * 0.5
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/// Check if error is a compilation/test error (TDD cycle, not CLI correction)
|
|
182
|
-
fn is_tdd_cycle_error(error_type: &ErrorType, output: &str) -> bool {
|
|
183
|
-
// Compilation errors
|
|
184
|
-
if output.contains("error[E") || output.contains("aborting due to") {
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Test failures
|
|
189
|
-
if output.contains("test result: FAILED") || output.contains("tests failed") {
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Only syntax errors are CLI corrections
|
|
194
|
-
matches!(error_type, ErrorType::CommandNotFound | ErrorType::Other(_))
|
|
195
|
-
&& (output.contains("error[E") || output.contains("FAILED"))
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/// Check if commands differ only by path (exploration, not correction)
|
|
199
|
-
fn differs_only_by_path(a: &str, b: &str) -> bool {
|
|
200
|
-
let base_a = extract_base_command(a);
|
|
201
|
-
let base_b = extract_base_command(b);
|
|
202
|
-
|
|
203
|
-
if base_a != base_b {
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Simple heuristic: if similarity is very high (>0.9) but not identical,
|
|
208
|
-
// likely just path differences
|
|
209
|
-
let sim = command_similarity(a, b);
|
|
210
|
-
sim > 0.9 && sim < 1.0
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
pub fn find_corrections(commands: &[CommandExecution]) -> Vec<CorrectionPair> {
|
|
214
|
-
let mut corrections = Vec::new();
|
|
215
|
-
|
|
216
|
-
for i in 0..commands.len() {
|
|
217
|
-
let cmd = &commands[i];
|
|
218
|
-
|
|
219
|
-
// Must be an actual error
|
|
220
|
-
if !is_command_error(cmd.is_error, &cmd.output) {
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
let error_type = classify_error(&cmd.output);
|
|
225
|
-
|
|
226
|
-
// Skip TDD cycle errors
|
|
227
|
-
if is_tdd_cycle_error(&error_type, &cmd.output) {
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Look ahead for correction within CORRECTION_WINDOW
|
|
232
|
-
for j in (i + 1)..std::cmp::min(i + 1 + CORRECTION_WINDOW, commands.len()) {
|
|
233
|
-
let candidate = &commands[j];
|
|
234
|
-
|
|
235
|
-
let similarity = command_similarity(&cmd.command, &candidate.command);
|
|
236
|
-
|
|
237
|
-
// Must meet minimum similarity
|
|
238
|
-
if similarity < 0.5 {
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Skip if only path differs (exploration)
|
|
243
|
-
if differs_only_by_path(&cmd.command, &candidate.command) {
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Skip if identical commands (same error repeated)
|
|
248
|
-
if cmd.command == candidate.command {
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Calculate confidence
|
|
253
|
-
let mut confidence = similarity;
|
|
254
|
-
|
|
255
|
-
// Boost confidence if correction succeeded
|
|
256
|
-
if !is_command_error(candidate.is_error, &candidate.output) {
|
|
257
|
-
confidence = (confidence + 0.2).min(1.0);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Must meet minimum confidence
|
|
261
|
-
if confidence < MIN_CONFIDENCE {
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Found a correction!
|
|
266
|
-
corrections.push(CorrectionPair {
|
|
267
|
-
wrong_command: cmd.command.clone(),
|
|
268
|
-
right_command: candidate.command.clone(),
|
|
269
|
-
error_output: cmd.output.chars().take(500).collect(),
|
|
270
|
-
error_type: error_type.clone(),
|
|
271
|
-
confidence,
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// Take first match only
|
|
275
|
-
break;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
corrections
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/// Extract the specific token that changed between wrong and right commands
|
|
283
|
-
fn extract_diff_token(wrong: &str, right: &str) -> String {
|
|
284
|
-
let wrong_parts: std::collections::HashSet<&str> = wrong.split_whitespace().collect();
|
|
285
|
-
let right_parts: std::collections::HashSet<&str> = right.split_whitespace().collect();
|
|
286
|
-
|
|
287
|
-
// Find tokens in wrong but not in right (removed)
|
|
288
|
-
let removed: Vec<&str> = wrong_parts.difference(&right_parts).copied().collect();
|
|
289
|
-
|
|
290
|
-
// Find tokens in right but not in wrong (added)
|
|
291
|
-
let added: Vec<&str> = right_parts.difference(&wrong_parts).copied().collect();
|
|
292
|
-
|
|
293
|
-
// Return the most distinctive change
|
|
294
|
-
if !removed.is_empty() && !added.is_empty() {
|
|
295
|
-
format!("{} → {}", removed[0], added[0])
|
|
296
|
-
} else if !removed.is_empty() {
|
|
297
|
-
format!("removed {}", removed[0])
|
|
298
|
-
} else if !added.is_empty() {
|
|
299
|
-
format!("added {}", added[0])
|
|
300
|
-
} else {
|
|
301
|
-
"unknown".to_string()
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
pub fn deduplicate_corrections(pairs: Vec<CorrectionPair>) -> Vec<CorrectionRule> {
|
|
306
|
-
use std::collections::HashMap;
|
|
307
|
-
|
|
308
|
-
let mut groups: HashMap<(String, String, String), Vec<CorrectionPair>> = HashMap::new();
|
|
309
|
-
|
|
310
|
-
// Group by (base_command, error_type, diff_token)
|
|
311
|
-
for pair in pairs {
|
|
312
|
-
let base = extract_base_command(&pair.wrong_command);
|
|
313
|
-
let error_type_str = pair.error_type.as_str().to_string();
|
|
314
|
-
let diff_token = extract_diff_token(&pair.wrong_command, &pair.right_command);
|
|
315
|
-
|
|
316
|
-
let key = (base, error_type_str, diff_token);
|
|
317
|
-
groups.entry(key).or_default().push(pair);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// For each group, keep the best confidence example
|
|
321
|
-
let mut rules = Vec::new();
|
|
322
|
-
for ((base_command, _error_type_str, _diff_token), mut group) in groups {
|
|
323
|
-
// Sort by confidence descending
|
|
324
|
-
group.sort_by(|a, b| {
|
|
325
|
-
b.confidence
|
|
326
|
-
.partial_cmp(&a.confidence)
|
|
327
|
-
.unwrap_or(std::cmp::Ordering::Equal)
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
let best = &group[0];
|
|
331
|
-
let occurrences = group.len();
|
|
332
|
-
|
|
333
|
-
// Reconstruct ErrorType from string (simplified - just use first one)
|
|
334
|
-
let error_type = best.error_type.clone();
|
|
335
|
-
|
|
336
|
-
rules.push(CorrectionRule {
|
|
337
|
-
wrong_pattern: best.wrong_command.clone(),
|
|
338
|
-
right_pattern: best.right_command.clone(),
|
|
339
|
-
error_type,
|
|
340
|
-
occurrences,
|
|
341
|
-
base_command,
|
|
342
|
-
example_error: best.error_output.clone(),
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Sort by occurrences descending (most common mistakes first)
|
|
347
|
-
rules.sort_by(|a, b| b.occurrences.cmp(&a.occurrences));
|
|
348
|
-
|
|
349
|
-
rules
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
#[cfg(test)]
|
|
353
|
-
mod tests {
|
|
354
|
-
use super::*;
|
|
355
|
-
|
|
356
|
-
#[test]
|
|
357
|
-
fn test_is_command_error_requires_error_flag() {
|
|
358
|
-
assert!(!is_command_error(false, "error: unknown flag"));
|
|
359
|
-
assert!(is_command_error(true, "error: unknown flag"));
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
#[test]
|
|
363
|
-
fn test_is_command_error_filters_user_rejection() {
|
|
364
|
-
assert!(!is_command_error(true, "The user doesn't want to proceed"));
|
|
365
|
-
assert!(!is_command_error(true, "Operation cancelled by user"));
|
|
366
|
-
assert!(is_command_error(true, "error: permission denied"));
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
#[test]
|
|
370
|
-
fn test_is_command_error_requires_error_content() {
|
|
371
|
-
assert!(!is_command_error(true, "All good, success!"));
|
|
372
|
-
assert!(is_command_error(true, "error: something failed"));
|
|
373
|
-
assert!(is_command_error(true, "unknown flag --foo"));
|
|
374
|
-
assert!(is_command_error(true, "invalid option"));
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
#[test]
|
|
378
|
-
fn test_classify_error_unknown_flag() {
|
|
379
|
-
assert_eq!(
|
|
380
|
-
classify_error("error: unexpected argument '--foo'"),
|
|
381
|
-
ErrorType::UnknownFlag
|
|
382
|
-
);
|
|
383
|
-
assert_eq!(
|
|
384
|
-
classify_error("unknown option: --bar"),
|
|
385
|
-
ErrorType::UnknownFlag
|
|
386
|
-
);
|
|
387
|
-
assert_eq!(
|
|
388
|
-
classify_error("unrecognized flag: -x"),
|
|
389
|
-
ErrorType::UnknownFlag
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
#[test]
|
|
394
|
-
fn test_classify_error_command_not_found() {
|
|
395
|
-
assert_eq!(
|
|
396
|
-
classify_error("bash: foobar: command not found"),
|
|
397
|
-
ErrorType::CommandNotFound
|
|
398
|
-
);
|
|
399
|
-
assert_eq!(
|
|
400
|
-
classify_error("'xyz' is not recognized as an internal or external command"),
|
|
401
|
-
ErrorType::CommandNotFound
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
#[test]
|
|
406
|
-
fn test_classify_error_all_types() {
|
|
407
|
-
assert_eq!(
|
|
408
|
-
classify_error("No such file or directory: foo.txt"),
|
|
409
|
-
ErrorType::WrongPath
|
|
410
|
-
);
|
|
411
|
-
assert_eq!(
|
|
412
|
-
classify_error("error: --output requires a value"),
|
|
413
|
-
ErrorType::MissingArg
|
|
414
|
-
);
|
|
415
|
-
assert_eq!(
|
|
416
|
-
classify_error("permission denied: /etc/shadow"),
|
|
417
|
-
ErrorType::PermissionDenied
|
|
418
|
-
);
|
|
419
|
-
assert!(matches!(
|
|
420
|
-
classify_error("something went wrong"),
|
|
421
|
-
ErrorType::Other(_)
|
|
422
|
-
));
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
#[test]
|
|
426
|
-
fn test_extract_base_command() {
|
|
427
|
-
assert_eq!(extract_base_command("git commit"), "git commit");
|
|
428
|
-
assert_eq!(extract_base_command("cargo test"), "cargo test");
|
|
429
|
-
assert_eq!(
|
|
430
|
-
extract_base_command("git commit --amend -m 'fix'"),
|
|
431
|
-
"git commit"
|
|
432
|
-
);
|
|
433
|
-
assert_eq!(
|
|
434
|
-
extract_base_command("RUST_BACKTRACE=1 cargo test"),
|
|
435
|
-
"cargo test"
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
#[test]
|
|
440
|
-
fn test_command_similarity_same_base() {
|
|
441
|
-
assert_eq!(command_similarity("git commit", "git commit"), 1.0);
|
|
442
|
-
assert_eq!(command_similarity("git status", "npm install"), 0.0);
|
|
443
|
-
let sim = command_similarity("git commit --amend", "git commit --ammend");
|
|
444
|
-
// Debug: check what similarity actually is
|
|
445
|
-
println!("Similarity: {}", sim);
|
|
446
|
-
// Same base (0.5) + both have 1 arg, 0 intersection = 0.5 + 0 = 0.5
|
|
447
|
-
assert_eq!(sim, 0.5);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
#[test]
|
|
451
|
-
fn test_find_corrections_basic() {
|
|
452
|
-
let commands = vec![
|
|
453
|
-
CommandExecution {
|
|
454
|
-
command: "git commit --ammend".to_string(),
|
|
455
|
-
is_error: true,
|
|
456
|
-
output: "error: unexpected argument '--ammend'".to_string(),
|
|
457
|
-
},
|
|
458
|
-
CommandExecution {
|
|
459
|
-
command: "git commit --amend".to_string(),
|
|
460
|
-
is_error: false,
|
|
461
|
-
output: "[main abc123] Fix bug".to_string(),
|
|
462
|
-
},
|
|
463
|
-
];
|
|
464
|
-
|
|
465
|
-
let corrections = find_corrections(&commands);
|
|
466
|
-
assert_eq!(corrections.len(), 1);
|
|
467
|
-
assert_eq!(corrections[0].wrong_command, "git commit --ammend");
|
|
468
|
-
assert_eq!(corrections[0].right_command, "git commit --amend");
|
|
469
|
-
assert!(corrections[0].confidence >= 0.6);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
#[test]
|
|
473
|
-
fn test_find_corrections_window_limit() {
|
|
474
|
-
let commands = vec![
|
|
475
|
-
CommandExecution {
|
|
476
|
-
command: "git commit --ammend".to_string(),
|
|
477
|
-
is_error: true,
|
|
478
|
-
output: "error: unexpected argument '--ammend'".to_string(),
|
|
479
|
-
},
|
|
480
|
-
CommandExecution {
|
|
481
|
-
command: "ls".to_string(),
|
|
482
|
-
is_error: false,
|
|
483
|
-
output: "file1.txt\nfile2.txt".to_string(),
|
|
484
|
-
},
|
|
485
|
-
CommandExecution {
|
|
486
|
-
command: "pwd".to_string(),
|
|
487
|
-
is_error: false,
|
|
488
|
-
output: "/home/user".to_string(),
|
|
489
|
-
},
|
|
490
|
-
CommandExecution {
|
|
491
|
-
command: "echo test".to_string(),
|
|
492
|
-
is_error: false,
|
|
493
|
-
output: "test".to_string(),
|
|
494
|
-
},
|
|
495
|
-
// Outside CORRECTION_WINDOW (3)
|
|
496
|
-
CommandExecution {
|
|
497
|
-
command: "git commit --amend".to_string(),
|
|
498
|
-
is_error: false,
|
|
499
|
-
output: "[main abc123] Fix".to_string(),
|
|
500
|
-
},
|
|
501
|
-
];
|
|
502
|
-
|
|
503
|
-
let corrections = find_corrections(&commands);
|
|
504
|
-
assert_eq!(corrections.len(), 0); // Too far apart
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
#[test]
|
|
508
|
-
fn test_find_corrections_excludes_tdd_cycle() {
|
|
509
|
-
let commands = vec![
|
|
510
|
-
CommandExecution {
|
|
511
|
-
command: "cargo test".to_string(),
|
|
512
|
-
is_error: true,
|
|
513
|
-
output: "error[E0425]: cannot find value `x`\ntest result: FAILED".to_string(),
|
|
514
|
-
},
|
|
515
|
-
CommandExecution {
|
|
516
|
-
command: "cargo test".to_string(),
|
|
517
|
-
is_error: false,
|
|
518
|
-
output: "test result: ok. 5 passed".to_string(),
|
|
519
|
-
},
|
|
520
|
-
];
|
|
521
|
-
|
|
522
|
-
let corrections = find_corrections(&commands);
|
|
523
|
-
assert_eq!(corrections.len(), 0); // TDD cycle, not CLI correction
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
#[test]
|
|
527
|
-
fn test_find_corrections_path_exploration() {
|
|
528
|
-
let commands = vec![
|
|
529
|
-
CommandExecution {
|
|
530
|
-
command: "cat file1.txt".to_string(),
|
|
531
|
-
is_error: true,
|
|
532
|
-
output: "cat: file1.txt: No such file or directory".to_string(),
|
|
533
|
-
},
|
|
534
|
-
CommandExecution {
|
|
535
|
-
command: "cat file2.txt".to_string(),
|
|
536
|
-
is_error: false,
|
|
537
|
-
output: "content here".to_string(),
|
|
538
|
-
},
|
|
539
|
-
];
|
|
540
|
-
|
|
541
|
-
let corrections = find_corrections(&commands);
|
|
542
|
-
// Should be filtered as path exploration (differs_only_by_path)
|
|
543
|
-
// Actually, this should NOT be filtered since base commands differ enough
|
|
544
|
-
// Let me adjust: they have same base "cat" but different args
|
|
545
|
-
assert_eq!(corrections.len(), 0); // Different files = exploration
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
#[test]
|
|
549
|
-
fn test_find_corrections_min_confidence() {
|
|
550
|
-
let commands = vec![
|
|
551
|
-
CommandExecution {
|
|
552
|
-
command: "git commit --foo --bar --baz".to_string(),
|
|
553
|
-
is_error: true,
|
|
554
|
-
output: "error: unexpected argument '--foo'".to_string(),
|
|
555
|
-
},
|
|
556
|
-
CommandExecution {
|
|
557
|
-
command: "git commit --qux".to_string(),
|
|
558
|
-
is_error: false,
|
|
559
|
-
output: "[main abc123] Fix".to_string(),
|
|
560
|
-
},
|
|
561
|
-
];
|
|
562
|
-
|
|
563
|
-
let corrections = find_corrections(&commands);
|
|
564
|
-
// Similarity = 0.5 (same base) + 0 (no arg overlap) = 0.5
|
|
565
|
-
// With success boost: 0.5 + 0.2 = 0.7, which passes MIN_CONFIDENCE
|
|
566
|
-
// So we expect 1 correction (this is a valid correction despite different args)
|
|
567
|
-
assert_eq!(corrections.len(), 1);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
#[test]
|
|
571
|
-
fn test_deduplicate_corrections_merges_same() {
|
|
572
|
-
let pairs = vec![
|
|
573
|
-
CorrectionPair {
|
|
574
|
-
wrong_command: "git commit --ammend".to_string(),
|
|
575
|
-
right_command: "git commit --amend".to_string(),
|
|
576
|
-
error_output: "error: unexpected argument '--ammend'".to_string(),
|
|
577
|
-
error_type: ErrorType::UnknownFlag,
|
|
578
|
-
confidence: 0.8,
|
|
579
|
-
},
|
|
580
|
-
CorrectionPair {
|
|
581
|
-
wrong_command: "git commit --ammend -m 'fix'".to_string(),
|
|
582
|
-
right_command: "git commit --amend -m 'fix'".to_string(),
|
|
583
|
-
error_output: "error: unexpected argument '--ammend'".to_string(),
|
|
584
|
-
error_type: ErrorType::UnknownFlag,
|
|
585
|
-
confidence: 0.9,
|
|
586
|
-
},
|
|
587
|
-
CorrectionPair {
|
|
588
|
-
wrong_command: "git commit --ammend".to_string(),
|
|
589
|
-
right_command: "git commit --amend".to_string(),
|
|
590
|
-
error_output: "error: unexpected argument '--ammend'".to_string(),
|
|
591
|
-
error_type: ErrorType::UnknownFlag,
|
|
592
|
-
confidence: 0.7,
|
|
593
|
-
},
|
|
594
|
-
];
|
|
595
|
-
|
|
596
|
-
let rules = deduplicate_corrections(pairs);
|
|
597
|
-
assert_eq!(rules.len(), 1); // Merged into single rule
|
|
598
|
-
assert_eq!(rules[0].occurrences, 3);
|
|
599
|
-
assert_eq!(rules[0].base_command, "git commit");
|
|
600
|
-
// Should keep highest confidence example (0.9)
|
|
601
|
-
assert!(rules[0].wrong_pattern.contains("'fix'"));
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
#[test]
|
|
605
|
-
fn test_deduplicate_corrections_keeps_distinct() {
|
|
606
|
-
let pairs = vec![
|
|
607
|
-
CorrectionPair {
|
|
608
|
-
wrong_command: "git commit --ammend".to_string(),
|
|
609
|
-
right_command: "git commit --amend".to_string(),
|
|
610
|
-
error_output: "error: unexpected argument '--ammend'".to_string(),
|
|
611
|
-
error_type: ErrorType::UnknownFlag,
|
|
612
|
-
confidence: 0.8,
|
|
613
|
-
},
|
|
614
|
-
CorrectionPair {
|
|
615
|
-
wrong_command: "git push --force".to_string(),
|
|
616
|
-
right_command: "git push --force-with-lease".to_string(),
|
|
617
|
-
error_output: "error: --force is dangerous".to_string(),
|
|
618
|
-
error_type: ErrorType::WrongSyntax,
|
|
619
|
-
confidence: 0.7,
|
|
620
|
-
},
|
|
621
|
-
];
|
|
622
|
-
|
|
623
|
-
let rules = deduplicate_corrections(pairs);
|
|
624
|
-
assert_eq!(rules.len(), 2); // Different base commands and errors
|
|
625
|
-
assert_eq!(rules[0].occurrences, 1);
|
|
626
|
-
assert_eq!(rules[1].occurrences, 1);
|
|
627
|
-
}
|
|
628
|
-
}
|