mirror of https://github.com/curusarn/resh
parent
2c7947225e
commit
9de1f9d5cc
@ -1,246 +0,0 @@ |
|||||||
package histanal |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"encoding/json" |
|
||||||
"fmt" |
|
||||||
"log" |
|
||||||
"math/rand" |
|
||||||
"os" |
|
||||||
"os/exec" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
"github.com/curusarn/resh/pkg/strat" |
|
||||||
"github.com/jpillora/longestcommon" |
|
||||||
|
|
||||||
"github.com/schollz/progressbar" |
|
||||||
) |
|
||||||
|
|
||||||
type matchJSON struct { |
|
||||||
Match bool |
|
||||||
Distance int |
|
||||||
CharsRecalled int |
|
||||||
} |
|
||||||
|
|
||||||
type multiMatchItemJSON struct { |
|
||||||
Distance int |
|
||||||
CharsRecalled int |
|
||||||
} |
|
||||||
|
|
||||||
type multiMatchJSON struct { |
|
||||||
Match bool |
|
||||||
Entries []multiMatchItemJSON |
|
||||||
} |
|
||||||
|
|
||||||
type strategyJSON struct { |
|
||||||
Title string |
|
||||||
Description string |
|
||||||
Matches []matchJSON |
|
||||||
PrefixMatches []multiMatchJSON |
|
||||||
} |
|
||||||
|
|
||||||
// HistEval evaluates history
|
|
||||||
type HistEval struct { |
|
||||||
HistLoad |
|
||||||
BatchMode bool |
|
||||||
maxCandidates int |
|
||||||
Strategies []strategyJSON |
|
||||||
} |
|
||||||
|
|
||||||
// NewHistEval constructs new HistEval
|
|
||||||
func NewHistEval(inputPath string, |
|
||||||
maxCandidates int, skipFailedCmds bool, |
|
||||||
debugRecords float64, sanitizedInput bool) HistEval { |
|
||||||
|
|
||||||
e := HistEval{ |
|
||||||
HistLoad: HistLoad{ |
|
||||||
skipFailedCmds: skipFailedCmds, |
|
||||||
debugRecords: debugRecords, |
|
||||||
sanitizedInput: sanitizedInput, |
|
||||||
}, |
|
||||||
maxCandidates: maxCandidates, |
|
||||||
BatchMode: false, |
|
||||||
} |
|
||||||
records := e.loadHistoryRecords(inputPath) |
|
||||||
device := deviceRecords{Records: records} |
|
||||||
user := userRecords{} |
|
||||||
user.Devices = append(user.Devices, device) |
|
||||||
e.UsersRecords = append(e.UsersRecords, user) |
|
||||||
e.preprocessRecords() |
|
||||||
return e |
|
||||||
} |
|
||||||
|
|
||||||
// NewHistEvalBatchMode constructs new HistEval in batch mode
|
|
||||||
func NewHistEvalBatchMode(input string, inputDataRoot string, |
|
||||||
maxCandidates int, skipFailedCmds bool, |
|
||||||
debugRecords float64, sanitizedInput bool) HistEval { |
|
||||||
|
|
||||||
e := HistEval{ |
|
||||||
HistLoad: HistLoad{ |
|
||||||
skipFailedCmds: skipFailedCmds, |
|
||||||
debugRecords: debugRecords, |
|
||||||
sanitizedInput: sanitizedInput, |
|
||||||
}, |
|
||||||
maxCandidates: maxCandidates, |
|
||||||
BatchMode: false, |
|
||||||
} |
|
||||||
e.UsersRecords = e.loadHistoryRecordsBatchMode(input, inputDataRoot) |
|
||||||
e.preprocessRecords() |
|
||||||
return e |
|
||||||
} |
|
||||||
|
|
||||||
func (e *HistEval) preprocessDeviceRecords(device deviceRecords) deviceRecords { |
|
||||||
sessionIDs := map[string]uint64{} |
|
||||||
var nextID uint64 |
|
||||||
nextID = 1 // start with 1 because 0 won't get saved to json
|
|
||||||
for k, record := range device.Records { |
|
||||||
id, found := sessionIDs[record.SessionID] |
|
||||||
if found == false { |
|
||||||
id = nextID |
|
||||||
sessionIDs[record.SessionID] = id |
|
||||||
nextID++ |
|
||||||
} |
|
||||||
device.Records[k].SeqSessionID = id |
|
||||||
// assert
|
|
||||||
if record.Sanitized != e.sanitizedInput { |
|
||||||
if e.sanitizedInput { |
|
||||||
log.Fatal("ASSERT failed: '--sanitized-input' is present but data is not sanitized") |
|
||||||
} |
|
||||||
log.Fatal("ASSERT failed: data is sanitized but '--sanitized-input' is not present") |
|
||||||
} |
|
||||||
device.Records[k].SeqSessionID = id |
|
||||||
if e.debugRecords > 0 && rand.Float64() < e.debugRecords { |
|
||||||
device.Records[k].DebugThisRecord = true |
|
||||||
} |
|
||||||
} |
|
||||||
// sort.SliceStable(device.Records, func(x, y int) bool {
|
|
||||||
// if device.Records[x].SeqSessionID == device.Records[y].SeqSessionID {
|
|
||||||
// return device.Records[x].RealtimeAfterLocal < device.Records[y].RealtimeAfterLocal
|
|
||||||
// }
|
|
||||||
// return device.Records[x].SeqSessionID < device.Records[y].SeqSessionID
|
|
||||||
// })
|
|
||||||
|
|
||||||
// iterate from back and mark last record of each session
|
|
||||||
sessionIDSet := map[string]bool{} |
|
||||||
for i := len(device.Records) - 1; i >= 0; i-- { |
|
||||||
var record *records.EnrichedRecord |
|
||||||
record = &device.Records[i] |
|
||||||
if sessionIDSet[record.SessionID] { |
|
||||||
continue |
|
||||||
} |
|
||||||
sessionIDSet[record.SessionID] = true |
|
||||||
record.LastRecordOfSession = true |
|
||||||
} |
|
||||||
return device |
|
||||||
} |
|
||||||
|
|
||||||
// enrich records and add sequential session ID
|
|
||||||
func (e *HistEval) preprocessRecords() { |
|
||||||
for i := range e.UsersRecords { |
|
||||||
for j := range e.UsersRecords[i].Devices { |
|
||||||
e.UsersRecords[i].Devices[j] = e.preprocessDeviceRecords(e.UsersRecords[i].Devices[j]) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Evaluate a given strategy
|
|
||||||
func (e *HistEval) Evaluate(strategy strat.IStrategy) error { |
|
||||||
title, description := strategy.GetTitleAndDescription() |
|
||||||
log.Println("Evaluating strategy:", title, "-", description) |
|
||||||
strategyData := strategyJSON{Title: title, Description: description} |
|
||||||
for i := range e.UsersRecords { |
|
||||||
for j := range e.UsersRecords[i].Devices { |
|
||||||
bar := progressbar.New(len(e.UsersRecords[i].Devices[j].Records)) |
|
||||||
var prevRecord records.EnrichedRecord |
|
||||||
for _, record := range e.UsersRecords[i].Devices[j].Records { |
|
||||||
if e.skipFailedCmds && record.ExitCode != 0 { |
|
||||||
continue |
|
||||||
} |
|
||||||
candidates := strategy.GetCandidates(records.Stripped(record)) |
|
||||||
if record.DebugThisRecord { |
|
||||||
log.Println() |
|
||||||
log.Println("===================================================") |
|
||||||
log.Println("STRATEGY:", title, "-", description) |
|
||||||
log.Println("===================================================") |
|
||||||
log.Println("Previous record:") |
|
||||||
if prevRecord.RealtimeBefore == 0 { |
|
||||||
log.Println("== NIL") |
|
||||||
} else { |
|
||||||
rec, _ := prevRecord.ToString() |
|
||||||
log.Println(rec) |
|
||||||
} |
|
||||||
log.Println("---------------------------------------------------") |
|
||||||
log.Println("Recommendations for:") |
|
||||||
rec, _ := record.ToString() |
|
||||||
log.Println(rec) |
|
||||||
log.Println("---------------------------------------------------") |
|
||||||
for i, candidate := range candidates { |
|
||||||
if i > 10 { |
|
||||||
break |
|
||||||
} |
|
||||||
log.Println(string(candidate)) |
|
||||||
} |
|
||||||
log.Println("===================================================") |
|
||||||
} |
|
||||||
|
|
||||||
matchFound := false |
|
||||||
longestPrefixMatchLength := 0 |
|
||||||
multiMatch := multiMatchJSON{} |
|
||||||
for i, candidate := range candidates { |
|
||||||
// make an option (--calculate-total) to turn this on/off ?
|
|
||||||
// if i >= e.maxCandidates {
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
commonPrefixLength := len(longestcommon.Prefix([]string{candidate, record.CmdLine})) |
|
||||||
if commonPrefixLength > longestPrefixMatchLength { |
|
||||||
longestPrefixMatchLength = commonPrefixLength |
|
||||||
prefixMatch := multiMatchItemJSON{Distance: i + 1, CharsRecalled: commonPrefixLength} |
|
||||||
multiMatch.Match = true |
|
||||||
multiMatch.Entries = append(multiMatch.Entries, prefixMatch) |
|
||||||
} |
|
||||||
if candidate == record.CmdLine { |
|
||||||
match := matchJSON{Match: true, Distance: i + 1, CharsRecalled: record.CmdLength} |
|
||||||
matchFound = true |
|
||||||
strategyData.Matches = append(strategyData.Matches, match) |
|
||||||
strategyData.PrefixMatches = append(strategyData.PrefixMatches, multiMatch) |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
if matchFound == false { |
|
||||||
strategyData.Matches = append(strategyData.Matches, matchJSON{}) |
|
||||||
strategyData.PrefixMatches = append(strategyData.PrefixMatches, multiMatch) |
|
||||||
} |
|
||||||
err := strategy.AddHistoryRecord(&record) |
|
||||||
if err != nil { |
|
||||||
log.Println("Error while evauating", err) |
|
||||||
return err |
|
||||||
} |
|
||||||
bar.Add(1) |
|
||||||
prevRecord = record |
|
||||||
} |
|
||||||
strategy.ResetHistory() |
|
||||||
fmt.Println() |
|
||||||
} |
|
||||||
} |
|
||||||
e.Strategies = append(e.Strategies, strategyData) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// CalculateStatsAndPlot results
|
|
||||||
func (e *HistEval) CalculateStatsAndPlot(scriptName string) { |
|
||||||
evalJSON, err := json.Marshal(e) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("json marshal error", err) |
|
||||||
} |
|
||||||
buffer := bytes.Buffer{} |
|
||||||
buffer.Write(evalJSON) |
|
||||||
// run python script to stat and plot/
|
|
||||||
cmd := exec.Command(scriptName) |
|
||||||
cmd.Stdout = os.Stdout |
|
||||||
cmd.Stderr = os.Stderr |
|
||||||
cmd.Stdin = &buffer |
|
||||||
err = cmd.Run() |
|
||||||
if err != nil { |
|
||||||
log.Printf("Command finished with error: %v", err) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,180 +0,0 @@ |
|||||||
package histanal |
|
||||||
|
|
||||||
import ( |
|
||||||
"bufio" |
|
||||||
"encoding/json" |
|
||||||
"fmt" |
|
||||||
"io/ioutil" |
|
||||||
"log" |
|
||||||
"math/rand" |
|
||||||
"os" |
|
||||||
"path/filepath" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
type deviceRecords struct { |
|
||||||
Name string |
|
||||||
Records []records.EnrichedRecord |
|
||||||
} |
|
||||||
|
|
||||||
type userRecords struct { |
|
||||||
Name string |
|
||||||
Devices []deviceRecords |
|
||||||
} |
|
||||||
|
|
||||||
// HistLoad loads history
|
|
||||||
type HistLoad struct { |
|
||||||
UsersRecords []userRecords |
|
||||||
skipFailedCmds bool |
|
||||||
sanitizedInput bool |
|
||||||
debugRecords float64 |
|
||||||
} |
|
||||||
|
|
||||||
func (e *HistLoad) preprocessDeviceRecords(device deviceRecords) deviceRecords { |
|
||||||
sessionIDs := map[string]uint64{} |
|
||||||
var nextID uint64 |
|
||||||
nextID = 1 // start with 1 because 0 won't get saved to json
|
|
||||||
for k, record := range device.Records { |
|
||||||
id, found := sessionIDs[record.SessionID] |
|
||||||
if found == false { |
|
||||||
id = nextID |
|
||||||
sessionIDs[record.SessionID] = id |
|
||||||
nextID++ |
|
||||||
} |
|
||||||
device.Records[k].SeqSessionID = id |
|
||||||
// assert
|
|
||||||
if record.Sanitized != e.sanitizedInput { |
|
||||||
if e.sanitizedInput { |
|
||||||
log.Fatal("ASSERT failed: '--sanitized-input' is present but data is not sanitized") |
|
||||||
} |
|
||||||
log.Fatal("ASSERT failed: data is sanitized but '--sanitized-input' is not present") |
|
||||||
} |
|
||||||
device.Records[k].SeqSessionID = id |
|
||||||
if e.debugRecords > 0 && rand.Float64() < e.debugRecords { |
|
||||||
device.Records[k].DebugThisRecord = true |
|
||||||
} |
|
||||||
} |
|
||||||
// sort.SliceStable(device.Records, func(x, y int) bool {
|
|
||||||
// if device.Records[x].SeqSessionID == device.Records[y].SeqSessionID {
|
|
||||||
// return device.Records[x].RealtimeAfterLocal < device.Records[y].RealtimeAfterLocal
|
|
||||||
// }
|
|
||||||
// return device.Records[x].SeqSessionID < device.Records[y].SeqSessionID
|
|
||||||
// })
|
|
||||||
|
|
||||||
// iterate from back and mark last record of each session
|
|
||||||
sessionIDSet := map[string]bool{} |
|
||||||
for i := len(device.Records) - 1; i >= 0; i-- { |
|
||||||
var record *records.EnrichedRecord |
|
||||||
record = &device.Records[i] |
|
||||||
if sessionIDSet[record.SessionID] { |
|
||||||
continue |
|
||||||
} |
|
||||||
sessionIDSet[record.SessionID] = true |
|
||||||
record.LastRecordOfSession = true |
|
||||||
} |
|
||||||
return device |
|
||||||
} |
|
||||||
|
|
||||||
// enrich records and add sequential session ID
|
|
||||||
func (e *HistLoad) preprocessRecords() { |
|
||||||
for i := range e.UsersRecords { |
|
||||||
for j := range e.UsersRecords[i].Devices { |
|
||||||
e.UsersRecords[i].Devices[j] = e.preprocessDeviceRecords(e.UsersRecords[i].Devices[j]) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (e *HistLoad) loadHistoryRecordsBatchMode(fname string, dataRootPath string) []userRecords { |
|
||||||
var records []userRecords |
|
||||||
info, err := os.Stat(dataRootPath) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Error: Directory", dataRootPath, "does not exist - exiting! (", err, ")") |
|
||||||
} |
|
||||||
if info.IsDir() == false { |
|
||||||
log.Fatal("Error:", dataRootPath, "is not a directory - exiting!") |
|
||||||
} |
|
||||||
users, err := ioutil.ReadDir(dataRootPath) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Could not read directory:", dataRootPath) |
|
||||||
} |
|
||||||
fmt.Println("Listing users in <", dataRootPath, ">...") |
|
||||||
for _, user := range users { |
|
||||||
userRecords := userRecords{Name: user.Name()} |
|
||||||
userFullPath := filepath.Join(dataRootPath, user.Name()) |
|
||||||
if user.IsDir() == false { |
|
||||||
log.Println("Warn: Unexpected file (not a directory) <", userFullPath, "> - skipping.") |
|
||||||
continue |
|
||||||
} |
|
||||||
fmt.Println() |
|
||||||
fmt.Printf("*- %s\n", user.Name()) |
|
||||||
devices, err := ioutil.ReadDir(userFullPath) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Could not read directory:", userFullPath) |
|
||||||
} |
|
||||||
for _, device := range devices { |
|
||||||
deviceRecords := deviceRecords{Name: device.Name()} |
|
||||||
deviceFullPath := filepath.Join(userFullPath, device.Name()) |
|
||||||
if device.IsDir() == false { |
|
||||||
log.Println("Warn: Unexpected file (not a directory) <", deviceFullPath, "> - skipping.") |
|
||||||
continue |
|
||||||
} |
|
||||||
fmt.Printf(" \\- %s\n", device.Name()) |
|
||||||
files, err := ioutil.ReadDir(deviceFullPath) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Could not read directory:", deviceFullPath) |
|
||||||
} |
|
||||||
for _, file := range files { |
|
||||||
fileFullPath := filepath.Join(deviceFullPath, file.Name()) |
|
||||||
if file.Name() == fname { |
|
||||||
fmt.Printf(" \\- %s - loading ...", file.Name()) |
|
||||||
// load the data
|
|
||||||
deviceRecords.Records = e.loadHistoryRecords(fileFullPath) |
|
||||||
fmt.Println(" OK ✓") |
|
||||||
} else { |
|
||||||
fmt.Printf(" \\- %s - skipped\n", file.Name()) |
|
||||||
} |
|
||||||
} |
|
||||||
userRecords.Devices = append(userRecords.Devices, deviceRecords) |
|
||||||
} |
|
||||||
records = append(records, userRecords) |
|
||||||
} |
|
||||||
return records |
|
||||||
} |
|
||||||
|
|
||||||
func (e *HistLoad) loadHistoryRecords(fname string) []records.EnrichedRecord { |
|
||||||
file, err := os.Open(fname) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Open() resh history file error:", err) |
|
||||||
} |
|
||||||
defer file.Close() |
|
||||||
|
|
||||||
var recs []records.EnrichedRecord |
|
||||||
scanner := bufio.NewScanner(file) |
|
||||||
for scanner.Scan() { |
|
||||||
record := records.Record{} |
|
||||||
fallbackRecord := records.FallbackRecord{} |
|
||||||
line := scanner.Text() |
|
||||||
err = json.Unmarshal([]byte(line), &record) |
|
||||||
if err != nil { |
|
||||||
err = json.Unmarshal([]byte(line), &fallbackRecord) |
|
||||||
if err != nil { |
|
||||||
log.Println("Line:", line) |
|
||||||
log.Fatal("Decoding error:", err) |
|
||||||
} |
|
||||||
record = records.Convert(&fallbackRecord) |
|
||||||
} |
|
||||||
if e.sanitizedInput == false { |
|
||||||
if record.CmdLength != 0 { |
|
||||||
log.Fatal("Assert failed - 'cmdLength' is set in raw data. Maybe you want to use '--sanitized-input' option?") |
|
||||||
} |
|
||||||
record.CmdLength = len(record.CmdLine) |
|
||||||
} else if record.CmdLength == 0 { |
|
||||||
log.Fatal("Assert failed - 'cmdLength' is unset in the data. This should not happen.") |
|
||||||
} |
|
||||||
if !e.skipFailedCmds || record.ExitCode == 0 { |
|
||||||
recs = append(recs, records.Enriched(record)) |
|
||||||
} |
|
||||||
} |
|
||||||
return recs |
|
||||||
} |
|
||||||
@ -1,47 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import "github.com/curusarn/resh/pkg/records" |
|
||||||
|
|
||||||
// DirectorySensitive prediction/recommendation strategy
|
|
||||||
type DirectorySensitive struct { |
|
||||||
history map[string][]string |
|
||||||
lastPwd string |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *DirectorySensitive) Init() { |
|
||||||
s.history = map[string][]string{} |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *DirectorySensitive) GetTitleAndDescription() (string, string) { |
|
||||||
return "directory sensitive (recent)", "Use recent commands executed is the same directory" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *DirectorySensitive) GetCandidates() []string { |
|
||||||
return s.history[s.lastPwd] |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *DirectorySensitive) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
// work on history for PWD
|
|
||||||
pwd := record.Pwd |
|
||||||
// remove previous occurance of record
|
|
||||||
for i, cmd := range s.history[pwd] { |
|
||||||
if cmd == record.CmdLine { |
|
||||||
s.history[pwd] = append(s.history[pwd][:i], s.history[pwd][i+1:]...) |
|
||||||
} |
|
||||||
} |
|
||||||
// append new record
|
|
||||||
s.history[pwd] = append([]string{record.CmdLine}, s.history[pwd]...) |
|
||||||
s.lastPwd = record.PwdAfter |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *DirectorySensitive) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
s.history = map[string][]string{} |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,29 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import "github.com/curusarn/resh/pkg/records" |
|
||||||
|
|
||||||
// Dummy prediction/recommendation strategy
|
|
||||||
type Dummy struct { |
|
||||||
history []string |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *Dummy) GetTitleAndDescription() (string, string) { |
|
||||||
return "dummy", "Return empty candidate list" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *Dummy) GetCandidates() []string { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *Dummy) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
s.history = append(s.history, record.CmdLine) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *Dummy) ResetHistory() error { |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,91 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import ( |
|
||||||
"math" |
|
||||||
"sort" |
|
||||||
"strconv" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// DynamicRecordDistance prediction/recommendation strategy
|
|
||||||
type DynamicRecordDistance struct { |
|
||||||
history []records.EnrichedRecord |
|
||||||
DistParams records.DistParams |
|
||||||
pwdHistogram map[string]int |
|
||||||
realPwdHistogram map[string]int |
|
||||||
gitOriginHistogram map[string]int |
|
||||||
MaxDepth int |
|
||||||
Label string |
|
||||||
} |
|
||||||
|
|
||||||
type strDynDistEntry struct { |
|
||||||
cmdLine string |
|
||||||
distance float64 |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *DynamicRecordDistance) Init() { |
|
||||||
s.history = nil |
|
||||||
s.pwdHistogram = map[string]int{} |
|
||||||
s.realPwdHistogram = map[string]int{} |
|
||||||
s.gitOriginHistogram = map[string]int{} |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *DynamicRecordDistance) GetTitleAndDescription() (string, string) { |
|
||||||
return "dynamic record distance (depth:" + strconv.Itoa(s.MaxDepth) + ";" + s.Label + ")", "Use TF-IDF record distance to recommend commands" |
|
||||||
} |
|
||||||
|
|
||||||
func (s *DynamicRecordDistance) idf(count int) float64 { |
|
||||||
return math.Log(float64(len(s.history)) / float64(count)) |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *DynamicRecordDistance) GetCandidates(strippedRecord records.EnrichedRecord) []string { |
|
||||||
if len(s.history) == 0 { |
|
||||||
return nil |
|
||||||
} |
|
||||||
var mapItems []strDynDistEntry |
|
||||||
for i, record := range s.history { |
|
||||||
if s.MaxDepth != 0 && i > s.MaxDepth { |
|
||||||
break |
|
||||||
} |
|
||||||
distParams := records.DistParams{ |
|
||||||
Pwd: s.DistParams.Pwd * s.idf(s.pwdHistogram[strippedRecord.PwdAfter]), |
|
||||||
RealPwd: s.DistParams.RealPwd * s.idf(s.realPwdHistogram[strippedRecord.RealPwdAfter]), |
|
||||||
Git: s.DistParams.Git * s.idf(s.gitOriginHistogram[strippedRecord.GitOriginRemote]), |
|
||||||
Time: s.DistParams.Time, |
|
||||||
SessionID: s.DistParams.SessionID, |
|
||||||
} |
|
||||||
distance := record.DistanceTo(strippedRecord, distParams) |
|
||||||
mapItems = append(mapItems, strDynDistEntry{record.CmdLine, distance}) |
|
||||||
} |
|
||||||
sort.SliceStable(mapItems, func(i int, j int) bool { return mapItems[i].distance < mapItems[j].distance }) |
|
||||||
var hist []string |
|
||||||
histSet := map[string]bool{} |
|
||||||
for _, item := range mapItems { |
|
||||||
if histSet[item.cmdLine] { |
|
||||||
continue |
|
||||||
} |
|
||||||
histSet[item.cmdLine] = true |
|
||||||
hist = append(hist, item.cmdLine) |
|
||||||
} |
|
||||||
return hist |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *DynamicRecordDistance) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
// append record to front
|
|
||||||
s.history = append([]records.EnrichedRecord{*record}, s.history...) |
|
||||||
s.pwdHistogram[record.Pwd]++ |
|
||||||
s.realPwdHistogram[record.RealPwd]++ |
|
||||||
s.gitOriginHistogram[record.GitOriginRemote]++ |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *DynamicRecordDistance) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,53 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import ( |
|
||||||
"sort" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// Frequent prediction/recommendation strategy
|
|
||||||
type Frequent struct { |
|
||||||
history map[string]int |
|
||||||
} |
|
||||||
|
|
||||||
type strFrqEntry struct { |
|
||||||
cmdLine string |
|
||||||
count int |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *Frequent) Init() { |
|
||||||
s.history = map[string]int{} |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *Frequent) GetTitleAndDescription() (string, string) { |
|
||||||
return "frequent", "Use frequent commands" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *Frequent) GetCandidates() []string { |
|
||||||
var mapItems []strFrqEntry |
|
||||||
for cmdLine, count := range s.history { |
|
||||||
mapItems = append(mapItems, strFrqEntry{cmdLine, count}) |
|
||||||
} |
|
||||||
sort.Slice(mapItems, func(i int, j int) bool { return mapItems[i].count > mapItems[j].count }) |
|
||||||
var hist []string |
|
||||||
for _, item := range mapItems { |
|
||||||
hist = append(hist, item.cmdLine) |
|
||||||
} |
|
||||||
return hist |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *Frequent) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
s.history[record.CmdLine]++ |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *Frequent) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,97 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import ( |
|
||||||
"sort" |
|
||||||
"strconv" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
"github.com/mb-14/gomarkov" |
|
||||||
) |
|
||||||
|
|
||||||
// MarkovChainCmd prediction/recommendation strategy
|
|
||||||
type MarkovChainCmd struct { |
|
||||||
Order int |
|
||||||
history []strMarkCmdHistoryEntry |
|
||||||
historyCmds []string |
|
||||||
} |
|
||||||
|
|
||||||
type strMarkCmdHistoryEntry struct { |
|
||||||
cmd string |
|
||||||
cmdLine string |
|
||||||
} |
|
||||||
|
|
||||||
type strMarkCmdEntry struct { |
|
||||||
cmd string |
|
||||||
transProb float64 |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *MarkovChainCmd) Init() { |
|
||||||
s.history = nil |
|
||||||
s.historyCmds = nil |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *MarkovChainCmd) GetTitleAndDescription() (string, string) { |
|
||||||
return "command-based markov chain (order " + strconv.Itoa(s.Order) + ")", "Use command-based markov chain to recommend commands" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *MarkovChainCmd) GetCandidates() []string { |
|
||||||
if len(s.history) < s.Order { |
|
||||||
var hist []string |
|
||||||
for _, item := range s.history { |
|
||||||
hist = append(hist, item.cmdLine) |
|
||||||
} |
|
||||||
return hist |
|
||||||
} |
|
||||||
chain := gomarkov.NewChain(s.Order) |
|
||||||
|
|
||||||
chain.Add(s.historyCmds) |
|
||||||
|
|
||||||
cmdsSet := map[string]bool{} |
|
||||||
var entries []strMarkCmdEntry |
|
||||||
for _, cmd := range s.historyCmds { |
|
||||||
if cmdsSet[cmd] { |
|
||||||
continue |
|
||||||
} |
|
||||||
cmdsSet[cmd] = true |
|
||||||
prob, _ := chain.TransitionProbability(cmd, s.historyCmds[len(s.historyCmds)-s.Order:]) |
|
||||||
entries = append(entries, strMarkCmdEntry{cmd: cmd, transProb: prob}) |
|
||||||
} |
|
||||||
sort.Slice(entries, func(i int, j int) bool { return entries[i].transProb > entries[j].transProb }) |
|
||||||
var hist []string |
|
||||||
histSet := map[string]bool{} |
|
||||||
for i := len(s.history) - 1; i >= 0; i-- { |
|
||||||
if histSet[s.history[i].cmdLine] { |
|
||||||
continue |
|
||||||
} |
|
||||||
histSet[s.history[i].cmdLine] = true |
|
||||||
if s.history[i].cmd == entries[0].cmd { |
|
||||||
hist = append(hist, s.history[i].cmdLine) |
|
||||||
} |
|
||||||
} |
|
||||||
// log.Println("################")
|
|
||||||
// log.Println(s.history[len(s.history)-s.order:])
|
|
||||||
// log.Println(" -> ")
|
|
||||||
// x := math.Min(float64(len(hist)), 3)
|
|
||||||
// log.Println(entries[:int(x)])
|
|
||||||
// x = math.Min(float64(len(hist)), 5)
|
|
||||||
// log.Println(hist[:int(x)])
|
|
||||||
// log.Println("################")
|
|
||||||
return hist |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *MarkovChainCmd) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
s.history = append(s.history, strMarkCmdHistoryEntry{cmdLine: record.CmdLine, cmd: record.Command}) |
|
||||||
s.historyCmds = append(s.historyCmds, record.Command) |
|
||||||
// s.historySet[record.CmdLine] = true
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *MarkovChainCmd) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,76 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import ( |
|
||||||
"sort" |
|
||||||
"strconv" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
"github.com/mb-14/gomarkov" |
|
||||||
) |
|
||||||
|
|
||||||
// MarkovChain prediction/recommendation strategy
|
|
||||||
type MarkovChain struct { |
|
||||||
Order int |
|
||||||
history []string |
|
||||||
} |
|
||||||
|
|
||||||
type strMarkEntry struct { |
|
||||||
cmdLine string |
|
||||||
transProb float64 |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *MarkovChain) Init() { |
|
||||||
s.history = nil |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *MarkovChain) GetTitleAndDescription() (string, string) { |
|
||||||
return "markov chain (order " + strconv.Itoa(s.Order) + ")", "Use markov chain to recommend commands" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *MarkovChain) GetCandidates() []string { |
|
||||||
if len(s.history) < s.Order { |
|
||||||
return s.history |
|
||||||
} |
|
||||||
chain := gomarkov.NewChain(s.Order) |
|
||||||
|
|
||||||
chain.Add(s.history) |
|
||||||
|
|
||||||
cmdLinesSet := map[string]bool{} |
|
||||||
var entries []strMarkEntry |
|
||||||
for _, cmdLine := range s.history { |
|
||||||
if cmdLinesSet[cmdLine] { |
|
||||||
continue |
|
||||||
} |
|
||||||
cmdLinesSet[cmdLine] = true |
|
||||||
prob, _ := chain.TransitionProbability(cmdLine, s.history[len(s.history)-s.Order:]) |
|
||||||
entries = append(entries, strMarkEntry{cmdLine: cmdLine, transProb: prob}) |
|
||||||
} |
|
||||||
sort.Slice(entries, func(i int, j int) bool { return entries[i].transProb > entries[j].transProb }) |
|
||||||
var hist []string |
|
||||||
for _, item := range entries { |
|
||||||
hist = append(hist, item.cmdLine) |
|
||||||
} |
|
||||||
// log.Println("################")
|
|
||||||
// log.Println(s.history[len(s.history)-s.order:])
|
|
||||||
// log.Println(" -> ")
|
|
||||||
// x := math.Min(float64(len(hist)), 5)
|
|
||||||
// log.Println(hist[:int(x)])
|
|
||||||
// log.Println("################")
|
|
||||||
return hist |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *MarkovChain) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
s.history = append(s.history, record.CmdLine) |
|
||||||
// s.historySet[record.CmdLine] = true
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *MarkovChain) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,57 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import ( |
|
||||||
"math/rand" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// Random prediction/recommendation strategy
|
|
||||||
type Random struct { |
|
||||||
CandidatesSize int |
|
||||||
history []string |
|
||||||
historySet map[string]bool |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *Random) Init() { |
|
||||||
s.history = nil |
|
||||||
s.historySet = map[string]bool{} |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *Random) GetTitleAndDescription() (string, string) { |
|
||||||
return "random", "Use random commands" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *Random) GetCandidates() []string { |
|
||||||
seed := time.Now().UnixNano() |
|
||||||
rand.Seed(seed) |
|
||||||
var candidates []string |
|
||||||
candidateSet := map[string]bool{} |
|
||||||
for len(candidates) < s.CandidatesSize && len(candidates)*2 < len(s.historySet) { |
|
||||||
x := rand.Intn(len(s.history)) |
|
||||||
candidate := s.history[x] |
|
||||||
if candidateSet[candidate] == false { |
|
||||||
candidateSet[candidate] = true |
|
||||||
candidates = append(candidates, candidate) |
|
||||||
continue |
|
||||||
} |
|
||||||
} |
|
||||||
return candidates |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *Random) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
s.history = append([]string{record.CmdLine}, s.history...) |
|
||||||
s.historySet[record.CmdLine] = true |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *Random) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,56 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import "github.com/curusarn/resh/pkg/records" |
|
||||||
|
|
||||||
// RecentBash prediction/recommendation strategy
|
|
||||||
type RecentBash struct { |
|
||||||
histfile []string |
|
||||||
histfileSnapshot map[string][]string |
|
||||||
history map[string][]string |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *RecentBash) Init() { |
|
||||||
s.histfileSnapshot = map[string][]string{} |
|
||||||
s.history = map[string][]string{} |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *RecentBash) GetTitleAndDescription() (string, string) { |
|
||||||
return "recent (bash-like)", "Behave like bash" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *RecentBash) GetCandidates(strippedRecord records.EnrichedRecord) []string { |
|
||||||
// populate the local history from histfile
|
|
||||||
if s.histfileSnapshot[strippedRecord.SessionID] == nil { |
|
||||||
s.histfileSnapshot[strippedRecord.SessionID] = s.histfile |
|
||||||
} |
|
||||||
return append(s.history[strippedRecord.SessionID], s.histfileSnapshot[strippedRecord.SessionID]...) |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *RecentBash) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
// remove previous occurance of record
|
|
||||||
for i, cmd := range s.history[record.SessionID] { |
|
||||||
if cmd == record.CmdLine { |
|
||||||
s.history[record.SessionID] = append(s.history[record.SessionID][:i], s.history[record.SessionID][i+1:]...) |
|
||||||
} |
|
||||||
} |
|
||||||
// append new record
|
|
||||||
s.history[record.SessionID] = append([]string{record.CmdLine}, s.history[record.SessionID]...) |
|
||||||
|
|
||||||
if record.LastRecordOfSession { |
|
||||||
// append history of the session to histfile and clear session history
|
|
||||||
s.histfile = append(s.history[record.SessionID], s.histfile...) |
|
||||||
s.histfileSnapshot[record.SessionID] = nil |
|
||||||
s.history[record.SessionID] = nil |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *RecentBash) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,37 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import "github.com/curusarn/resh/pkg/records" |
|
||||||
|
|
||||||
// Recent prediction/recommendation strategy
|
|
||||||
type Recent struct { |
|
||||||
history []string |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *Recent) GetTitleAndDescription() (string, string) { |
|
||||||
return "recent", "Use recent commands" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *Recent) GetCandidates() []string { |
|
||||||
return s.history |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *Recent) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
// remove previous occurance of record
|
|
||||||
for i, cmd := range s.history { |
|
||||||
if cmd == record.CmdLine { |
|
||||||
s.history = append(s.history[:i], s.history[i+1:]...) |
|
||||||
} |
|
||||||
} |
|
||||||
// append new record
|
|
||||||
s.history = append([]string{record.CmdLine}, s.history...) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *Recent) ResetHistory() error { |
|
||||||
s.history = nil |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,70 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import ( |
|
||||||
"sort" |
|
||||||
"strconv" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// RecordDistance prediction/recommendation strategy
|
|
||||||
type RecordDistance struct { |
|
||||||
history []records.EnrichedRecord |
|
||||||
DistParams records.DistParams |
|
||||||
MaxDepth int |
|
||||||
Label string |
|
||||||
} |
|
||||||
|
|
||||||
type strDistEntry struct { |
|
||||||
cmdLine string |
|
||||||
distance float64 |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *RecordDistance) Init() { |
|
||||||
s.history = nil |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *RecordDistance) GetTitleAndDescription() (string, string) { |
|
||||||
return "record distance (depth:" + strconv.Itoa(s.MaxDepth) + ";" + s.Label + ")", "Use record distance to recommend commands" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *RecordDistance) GetCandidates(strippedRecord records.EnrichedRecord) []string { |
|
||||||
if len(s.history) == 0 { |
|
||||||
return nil |
|
||||||
} |
|
||||||
var mapItems []strDistEntry |
|
||||||
for i, record := range s.history { |
|
||||||
if s.MaxDepth != 0 && i > s.MaxDepth { |
|
||||||
break |
|
||||||
} |
|
||||||
distance := record.DistanceTo(strippedRecord, s.DistParams) |
|
||||||
mapItems = append(mapItems, strDistEntry{record.CmdLine, distance}) |
|
||||||
} |
|
||||||
sort.SliceStable(mapItems, func(i int, j int) bool { return mapItems[i].distance < mapItems[j].distance }) |
|
||||||
var hist []string |
|
||||||
histSet := map[string]bool{} |
|
||||||
for _, item := range mapItems { |
|
||||||
if histSet[item.cmdLine] { |
|
||||||
continue |
|
||||||
} |
|
||||||
histSet[item.cmdLine] = true |
|
||||||
hist = append(hist, item.cmdLine) |
|
||||||
} |
|
||||||
return hist |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *RecordDistance) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
// append record to front
|
|
||||||
s.history = append([]records.EnrichedRecord{*record}, s.history...) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *RecordDistance) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,46 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import ( |
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// ISimpleStrategy interface
|
|
||||||
type ISimpleStrategy interface { |
|
||||||
GetTitleAndDescription() (string, string) |
|
||||||
GetCandidates() []string |
|
||||||
AddHistoryRecord(record *records.EnrichedRecord) error |
|
||||||
ResetHistory() error |
|
||||||
} |
|
||||||
|
|
||||||
// IStrategy interface
|
|
||||||
type IStrategy interface { |
|
||||||
GetTitleAndDescription() (string, string) |
|
||||||
GetCandidates(r records.EnrichedRecord) []string |
|
||||||
AddHistoryRecord(record *records.EnrichedRecord) error |
|
||||||
ResetHistory() error |
|
||||||
} |
|
||||||
|
|
||||||
type simpleStrategyWrapper struct { |
|
||||||
strategy ISimpleStrategy |
|
||||||
} |
|
||||||
|
|
||||||
// NewSimpleStrategyWrapper returns IStrategy created by wrapping given ISimpleStrategy
|
|
||||||
func NewSimpleStrategyWrapper(strategy ISimpleStrategy) *simpleStrategyWrapper { |
|
||||||
return &simpleStrategyWrapper{strategy: strategy} |
|
||||||
} |
|
||||||
|
|
||||||
func (s *simpleStrategyWrapper) GetTitleAndDescription() (string, string) { |
|
||||||
return s.strategy.GetTitleAndDescription() |
|
||||||
} |
|
||||||
|
|
||||||
func (s *simpleStrategyWrapper) GetCandidates(r records.EnrichedRecord) []string { |
|
||||||
return s.strategy.GetCandidates() |
|
||||||
} |
|
||||||
|
|
||||||
func (s *simpleStrategyWrapper) AddHistoryRecord(r *records.EnrichedRecord) error { |
|
||||||
return s.strategy.AddHistoryRecord(r) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *simpleStrategyWrapper) ResetHistory() error { |
|
||||||
return s.strategy.ResetHistory() |
|
||||||
} |
|
||||||
Loading…
Reference in new issue