predictability-engine 0.8.6 → 0.11.5
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.
- checksums.yaml +4 -4
- data/bin/predictability-engine +7 -6
- data/bin/setup +55 -16
- data/bin/setup.bat +2 -0
- data/bin/setup.ps1 +94 -0
- data/lib/predictability_engine/cli.rb +83 -114
- data/lib/predictability_engine/data_sources/base.rb +8 -0
- data/lib/predictability_engine/data_sources/csv.rb +4 -3
- data/lib/predictability_engine/data_sources/excel.rb +1 -1
- data/lib/predictability_engine/jira_config_prompter.rb +12 -2
- data/lib/predictability_engine/version.rb +1 -1
- metadata +7 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f44e183717ab04fd8fe35647721a39741d26fc337d360d1792407814bf6298ef
|
|
4
|
+
data.tar.gz: 0df772dc4b55abc978aa1f9d7ba8994d375683f0fc899bbcb8c18c1af1fd9ecc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c8adb03e9f72ad485039b7957616ceb8dc2fb533d7b5ff0b72d611028c2cf224485299ec56f14d9a390ef8c35ff91d2be02b1b0aaad0b931643c7e3a79f354bd
|
|
7
|
+
data.tar.gz: 55ad22940cef1ffcad66759ee9f345fcc397db8dc2f24a71e1217c2a7ce12c7a5642ebd1ad514dd1bec9c8ffc5b54f3ae4d4b465f95aec2e0d1fc523fc5a7f3f
|
data/bin/predictability-engine
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
# When running from the source tree (not an installed gem), point Bundler
|
|
5
5
|
# at this gem's Gemfile so the binary works from any CWD.
|
|
6
6
|
gemfile = File.expand_path('../Gemfile', __dir__)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
begin
|
|
10
|
-
|
|
11
|
-
rescue LoadError
|
|
12
|
-
|
|
7
|
+
if File.exist?(gemfile)
|
|
8
|
+
ENV['BUNDLE_GEMFILE'] ||= gemfile
|
|
9
|
+
begin
|
|
10
|
+
require 'bundler/setup'
|
|
11
|
+
rescue LoadError
|
|
12
|
+
# Bundler not available, hope for the best with system gems
|
|
13
|
+
end
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
data/bin/setup
CHANGED
|
@@ -1,31 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# bin/setup — ensures Ruby, then bootstraps Bundler and delegates to the PE CLI.
|
|
2
|
+
# bin/setup — ensures Ruby + Node.js, then bootstraps Bundler and delegates to the PE CLI.
|
|
3
|
+
#
|
|
4
|
+
# Works on macOS, Linux, and WSL with zero prerequisites:
|
|
5
|
+
# - If mise/asdf/rbenv/rvm is already installed, uses it.
|
|
6
|
+
# - Otherwise, installs mise automatically (brew on macOS, curl on Linux/WSL).
|
|
7
|
+
# mise reads .ruby-version AND .tool-versions, so Ruby + Node.js land in one shot.
|
|
8
|
+
#
|
|
9
|
+
# For Windows native, use: bin\setup.bat (invokes bin\setup.ps1 via PowerShell)
|
|
3
10
|
set -euo pipefail
|
|
4
11
|
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
12
|
|
|
6
13
|
# ── Ruby version guard ────────────────────────────────────────────────────────
|
|
7
14
|
REQUIRED_RUBY_MAJOR=${REQUIRED_RUBY_MAJOR:-4}
|
|
8
15
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
echo "==> Ruby ${major}.x found but Ruby >= ${REQUIRED_RUBY_MAJOR}.0 is required."
|
|
16
|
+
install_mise() {
|
|
17
|
+
echo "==> Installing mise (manages Ruby + Node.js from .tool-versions)..."
|
|
18
|
+
if [[ "$(uname)" == "Darwin" ]] && command -v brew &>/dev/null; then
|
|
19
|
+
brew install mise
|
|
14
20
|
else
|
|
15
|
-
|
|
21
|
+
curl -fsSL https://mise.run | sh || true
|
|
22
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
16
23
|
fi
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if command -v mise &>/dev/null; then mise install && return 0; fi
|
|
20
|
-
if command -v asdf &>/dev/null; then asdf install && return 0; fi
|
|
21
|
-
if command -v rbenv &>/dev/null; then rbenv install --skip-existing && return 0; fi
|
|
22
|
-
if command -v rvm &>/dev/null; then
|
|
23
|
-
rvm install "$(cat .ruby-version)" && rvm use "$(cat .ruby-version)" && return 0
|
|
24
|
+
if command -v mise &>/dev/null; then
|
|
25
|
+
eval "$(mise activate bash)" 2>/dev/null || true
|
|
24
26
|
fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_ruby_version_file_major() {
|
|
30
|
+
sed 's/ruby-//' .ruby-version 2>/dev/null | cut -d. -f1 || echo 0
|
|
31
|
+
}
|
|
25
32
|
|
|
26
|
-
|
|
33
|
+
_setup_error() {
|
|
34
|
+
local ruby_version
|
|
35
|
+
ruby_version=$(sed 's/ruby-//' .ruby-version 2>/dev/null || echo "${REQUIRED_RUBY_MAJOR}.x")
|
|
27
36
|
echo ""
|
|
28
|
-
echo "ERROR:
|
|
37
|
+
echo "ERROR: Could not auto-install Ruby ${ruby_version}."
|
|
38
|
+
echo " Install Ruby ${ruby_version} manually:"
|
|
29
39
|
echo ""
|
|
30
40
|
echo " macOS: brew install rbenv && rbenv install ${ruby_version}"
|
|
31
41
|
echo " Linux: curl -fsSL https://mise.run | sh && mise install"
|
|
@@ -39,6 +49,35 @@ ensure_ruby() {
|
|
|
39
49
|
exit 1
|
|
40
50
|
}
|
|
41
51
|
|
|
52
|
+
ensure_ruby() {
|
|
53
|
+
if command -v ruby &>/dev/null; then
|
|
54
|
+
major=$(RUBYOPT= ruby -e 'puts RUBY_VERSION.split(".").first')
|
|
55
|
+
[ "$major" -ge "$REQUIRED_RUBY_MAJOR" ] && return 0
|
|
56
|
+
echo "==> Ruby ${major}.x found but Ruby >= ${REQUIRED_RUBY_MAJOR}.0 is required."
|
|
57
|
+
else
|
|
58
|
+
echo "==> Ruby not found. Ruby >= ${REQUIRED_RUBY_MAJOR}.0 is required."
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
echo "==> Trying version managers..."
|
|
62
|
+
if command -v mise &>/dev/null; then mise install && return 0; fi
|
|
63
|
+
if command -v asdf &>/dev/null; then asdf install && return 0; fi
|
|
64
|
+
if command -v rbenv &>/dev/null; then rbenv install --skip-existing && return 0; fi
|
|
65
|
+
if command -v rvm &>/dev/null; then
|
|
66
|
+
rvm install "$(cat .ruby-version)" && rvm use "$(cat .ruby-version)" && return 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Only attempt mise auto-install when .ruby-version can satisfy the requirement;
|
|
70
|
+
# skip immediately to the error when it cannot (e.g. REQUIRED_RUBY_MAJOR=99).
|
|
71
|
+
if [ "$(_ruby_version_file_major)" -ge "$REQUIRED_RUBY_MAJOR" ] 2>/dev/null; then
|
|
72
|
+
install_mise
|
|
73
|
+
if command -v mise &>/dev/null && mise install; then
|
|
74
|
+
return 0
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
_setup_error
|
|
79
|
+
}
|
|
80
|
+
|
|
42
81
|
ensure_ruby
|
|
43
82
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
44
83
|
|
data/bin/setup.bat
ADDED
data/bin/setup.ps1
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
Bootstrap Ruby and the Predictability Engine on Windows.
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Installs Ruby 4.x + MSYS2 DevKit via winget (if Ruby >= 4 is not already present),
|
|
7
|
+
then runs gem install bundler, bundle install, and predictability-engine setup
|
|
8
|
+
(which handles Node.js, Playwright, and Chromium).
|
|
9
|
+
|
|
10
|
+
Invoke via the thin CMD wrapper: bin\setup.bat
|
|
11
|
+
Or directly from PowerShell: powershell -ExecutionPolicy Bypass -File bin\setup.ps1
|
|
12
|
+
#>
|
|
13
|
+
|
|
14
|
+
Set-StrictMode -Version Latest
|
|
15
|
+
$ErrorActionPreference = 'Stop'
|
|
16
|
+
|
|
17
|
+
$RequiredRubyMajor = 4
|
|
18
|
+
|
|
19
|
+
# Change to repo root (one level above bin/)
|
|
20
|
+
$RepoRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
|
21
|
+
Set-Location $RepoRoot
|
|
22
|
+
|
|
23
|
+
function Get-RubyMajorVersion {
|
|
24
|
+
try {
|
|
25
|
+
$out = & ruby -e 'puts RUBY_VERSION.split(".").first' 2>$null
|
|
26
|
+
$val = $out.Trim()
|
|
27
|
+
if ($val -match '^\d+$') { return [int]$val }
|
|
28
|
+
} catch { }
|
|
29
|
+
return $null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Refresh-Path {
|
|
33
|
+
$machine = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine')
|
|
34
|
+
$user = [System.Environment]::GetEnvironmentVariable('PATH', 'User')
|
|
35
|
+
$env:PATH = "$machine;$user"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function Install-Ruby {
|
|
39
|
+
$rubyVersion = (Get-Content .ruby-version).Trim() -replace '^ruby-', ''
|
|
40
|
+
|
|
41
|
+
if (Get-Command winget -ErrorAction SilentlyContinue) {
|
|
42
|
+
Write-Host "==> Installing Ruby+Devkit 4.x via winget..."
|
|
43
|
+
winget install --id RubyInstallerTeam.RubyWithDevKit.4 --source winget --silent --accept-package-agreements --accept-source-agreements
|
|
44
|
+
Refresh-Path
|
|
45
|
+
} else {
|
|
46
|
+
Write-Host @"
|
|
47
|
+
|
|
48
|
+
ERROR: winget not found. Install Ruby $rubyVersion manually:
|
|
49
|
+
|
|
50
|
+
Option 1 (Recommended):
|
|
51
|
+
Download Ruby+Devkit $rubyVersion-x64 from https://rubyinstaller.org/downloads/
|
|
52
|
+
Run the installer and check "Add Ruby to PATH".
|
|
53
|
+
|
|
54
|
+
Option 2:
|
|
55
|
+
Install winget (App Installer) from the Microsoft Store, then re-run:
|
|
56
|
+
bin\setup.bat
|
|
57
|
+
|
|
58
|
+
After installing Ruby, re-run: bin\setup.bat
|
|
59
|
+
"@
|
|
60
|
+
exit 1
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# ── Main ───────────────────────────────────────────────────────────────────────
|
|
65
|
+
$major = Get-RubyMajorVersion
|
|
66
|
+
if ($null -eq $major) {
|
|
67
|
+
Write-Host "==> Ruby not found."
|
|
68
|
+
Install-Ruby
|
|
69
|
+
} elseif ($major -lt $RequiredRubyMajor) {
|
|
70
|
+
Write-Host "==> Ruby $major.x found but Ruby >= $RequiredRubyMajor.0 is required."
|
|
71
|
+
Install-Ruby
|
|
72
|
+
} else {
|
|
73
|
+
Write-Host "==> Ruby $major.x found."
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Verify Ruby is reachable after install (winget may require a new terminal on first install)
|
|
77
|
+
$major = Get-RubyMajorVersion
|
|
78
|
+
if ($null -eq $major -or $major -lt $RequiredRubyMajor) {
|
|
79
|
+
Write-Host @"
|
|
80
|
+
|
|
81
|
+
ERROR: Ruby is not in PATH after installation.
|
|
82
|
+
Close this terminal, open a new one, and re-run: bin\setup.bat
|
|
83
|
+
"@
|
|
84
|
+
exit 1
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
Write-Host "==> Installing Bundler..."
|
|
88
|
+
& gem install bundler --conservative
|
|
89
|
+
|
|
90
|
+
Write-Host "==> Installing Ruby gems..."
|
|
91
|
+
& bundle install --jobs 4 --retry 3
|
|
92
|
+
|
|
93
|
+
Write-Host "==> Running Predictability Engine setup (Node.js, Playwright, Chromium)..."
|
|
94
|
+
& bundle exec predictability-engine setup
|
|
@@ -104,21 +104,6 @@ module PredictabilityEngine
|
|
|
104
104
|
run_and_print_report(source, :html, output: output)
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
desc 'landscape SOURCE [OUTPUT]', 'Alias for html_all'
|
|
108
|
-
def landscape(source, output = nil)
|
|
109
|
-
run_and_print_report(source, :landscape, output: output)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
desc 'dashboard SOURCE [OUTPUT]', 'Alias for landscape'
|
|
113
|
-
def dashboard(source, output = nil)
|
|
114
|
-
landscape(source, output)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
desc 'all_html SOURCE [OUTPUT]', 'Alias for html_all'
|
|
118
|
-
def all_html(source, output = nil)
|
|
119
|
-
html_all(source, output)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
107
|
desc 'pdf SOURCE [OUTPUT]', 'Generate a PDF report'
|
|
123
108
|
def pdf(source, output = nil)
|
|
124
109
|
run_and_print_report(source, :pdf, output: output)
|
|
@@ -134,21 +119,11 @@ module PredictabilityEngine
|
|
|
134
119
|
run_and_print_report(source, :markdown, output: output)
|
|
135
120
|
end
|
|
136
121
|
|
|
137
|
-
desc 'md SOURCE [OUTPUT]', 'Alias for markdown'
|
|
138
|
-
def md(source, output = nil)
|
|
139
|
-
markdown(source, output)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
122
|
desc 'confluence SOURCE [OUTPUT]', 'Generate a Confluence markup report'
|
|
143
123
|
def confluence(source, output = nil)
|
|
144
124
|
run_and_print_report(source, :confluence, output: output)
|
|
145
125
|
end
|
|
146
126
|
|
|
147
|
-
desc 'conf SOURCE [OUTPUT]', 'Alias for confluence'
|
|
148
|
-
def conf(source, output = nil)
|
|
149
|
-
confluence(source, output)
|
|
150
|
-
end
|
|
151
|
-
|
|
152
127
|
desc 'png SOURCE [OUTPUT]', 'Generate a PNG report'
|
|
153
128
|
def png(source, output = nil)
|
|
154
129
|
run_and_print_report(source, :png, output: output)
|
|
@@ -192,41 +167,29 @@ module PredictabilityEngine
|
|
|
192
167
|
end
|
|
193
168
|
end
|
|
194
169
|
|
|
195
|
-
class
|
|
170
|
+
class Jira < Thor
|
|
196
171
|
include CliBase
|
|
197
172
|
include JiraConfigPrompter
|
|
198
173
|
|
|
199
|
-
desc '
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
PredictabilityEngine.run_and_print_report(input_source, format, options, output: output)
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
desc 'batch SOURCE', 'Run all report formats for the given SOURCE'
|
|
219
|
-
method_option :color, type: :boolean, default: true, desc: 'Enable/disable color output'
|
|
220
|
-
def batch(source)
|
|
221
|
-
Viz.new([], options).all_formats(source)
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
desc 'setup', 'Install/update all dependencies (Ruby gems + Node.js + Playwright + Chromium)'
|
|
225
|
-
def setup
|
|
226
|
-
SetupManager.new.run
|
|
174
|
+
desc 'config PROFILE', 'Generate/Update Jira credentials in ~/.config/jira/jira_credentials.yml'
|
|
175
|
+
method_option :auth_mode, aliases: '-a', default: 'basic',
|
|
176
|
+
desc: 'Auth mode: basic | bearer | cookie | mfa_api | mfa_browser'
|
|
177
|
+
def config(profile)
|
|
178
|
+
site = ask('Jira site (e.g., https://your-domain.atlassian.net):')
|
|
179
|
+
context_path = ask('Context path, if any (e.g., /jira — leave blank for Atlassian Cloud):')
|
|
180
|
+
mode = options[:auth_mode]
|
|
181
|
+
profile_data = build_profile_data(site, context_path, mode)
|
|
182
|
+
path = Config.jira_credentials_file
|
|
183
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
184
|
+
cfg = File.exist?(path) ? Config.load_yaml_file(path) : {}
|
|
185
|
+
cfg ||= {}
|
|
186
|
+
cfg['profiles'] ||= {}
|
|
187
|
+
cfg['profiles'][profile] = profile_data
|
|
188
|
+
File.write(path, cfg.to_yaml)
|
|
189
|
+
PredictabilityEngine.logger.info { "Jira credentials for profile '#{profile}' saved to #{path}" }
|
|
227
190
|
end
|
|
228
191
|
|
|
229
|
-
desc 'init FILENAME', 'Create a template YAML file for
|
|
192
|
+
desc 'init FILENAME', 'Create a template YAML file for Jira source'
|
|
230
193
|
def init(filename)
|
|
231
194
|
filename += '.yml' unless filename.end_with?('.yml', '.yaml')
|
|
232
195
|
content = <<~YAML
|
|
@@ -241,47 +204,25 @@ module PredictabilityEngine
|
|
|
241
204
|
PredictabilityEngine.logger.info { "Template created at #{filename}" }
|
|
242
205
|
end
|
|
243
206
|
|
|
244
|
-
desc '
|
|
245
|
-
method_option :auth_mode, aliases: '-a', default: 'basic',
|
|
246
|
-
desc: 'Auth mode: basic | bearer | cookie | mfa_api | mfa_browser'
|
|
247
|
-
def jira_config(profile)
|
|
248
|
-
site = ask('Jira site (e.g., https://your-domain.atlassian.net):')
|
|
249
|
-
context_path = ask('Context path, if any (e.g., /jira — leave blank for Atlassian Cloud):')
|
|
250
|
-
mode = options[:auth_mode]
|
|
251
|
-
|
|
252
|
-
profile_data = build_profile_data(site, context_path, mode)
|
|
253
|
-
|
|
254
|
-
path = Config.jira_credentials_file
|
|
255
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
256
|
-
|
|
257
|
-
config = File.exist?(path) ? Config.load_yaml_file(path) : {}
|
|
258
|
-
config ||= {}
|
|
259
|
-
config['profiles'] ||= {}
|
|
260
|
-
config['profiles'][profile] = profile_data
|
|
261
|
-
|
|
262
|
-
File.write(path, config.to_yaml)
|
|
263
|
-
PredictabilityEngine.logger.info { "Jira credentials for profile '#{profile}' saved to #{path}" }
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
desc 'jira_workflow PROFILE [OUTPUT]',
|
|
207
|
+
desc 'workflow PROFILE [OUTPUT]',
|
|
267
208
|
'Extract Jira workflow statuses for PROFILE into an editable YAML mapping ' \
|
|
268
209
|
'(default: ~/.config/jira/<profile>.workflow.yml). Re-running refreshes the ' \
|
|
269
210
|
'snapshot while preserving any roles you already set.'
|
|
270
|
-
def
|
|
211
|
+
def workflow(profile, output = nil)
|
|
271
212
|
path = output || JiraWorkflow.default_path(profile)
|
|
272
213
|
fresh = JiraWorkflow.extract(profile)
|
|
273
|
-
|
|
274
|
-
|
|
214
|
+
wf = File.exist?(path) ? JiraWorkflow.load(path).refresh(fresh) : fresh
|
|
215
|
+
wf.write(path)
|
|
275
216
|
action = File.exist?(path) ? 'refreshed' : 'written'
|
|
276
217
|
PredictabilityEngine.logger.info { "Workflow for profile '#{profile}' #{action}: #{path}" }
|
|
277
218
|
PredictabilityEngine.logger.info { "Review #{path} and set role: arrival / departure / null per status." }
|
|
278
219
|
end
|
|
279
220
|
|
|
280
|
-
desc '
|
|
221
|
+
desc 'workflow_merge OUTPUT SOURCES...',
|
|
281
222
|
'Merge multiple workflow configs into a shared config. Each SOURCE is ' \
|
|
282
223
|
'either a profile name (resolved to ~/.config/jira/<profile>.workflow.yml) ' \
|
|
283
224
|
'or an explicit path to a workflow YAML file.'
|
|
284
|
-
def
|
|
225
|
+
def workflow_merge(output, *sources)
|
|
285
226
|
raise Error, 'Need at least one workflow source to merge' if sources.empty?
|
|
286
227
|
|
|
287
228
|
configs = sources.map do |src|
|
|
@@ -291,6 +232,35 @@ module PredictabilityEngine
|
|
|
291
232
|
JiraWorkflow.merge(configs).write(output)
|
|
292
233
|
PredictabilityEngine.logger.info { "Merged workflow from #{sources.join(', ')} written to #{output}" }
|
|
293
234
|
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
class Cli < Thor
|
|
238
|
+
include CliBase
|
|
239
|
+
|
|
240
|
+
package_name "predictability-engine #{VERSION}"
|
|
241
|
+
|
|
242
|
+
desc 'batch SOURCE', 'Run all report formats for the given SOURCE'
|
|
243
|
+
method_option :color, type: :boolean, default: true, desc: 'Enable/disable color output'
|
|
244
|
+
def batch(source)
|
|
245
|
+
Viz.new([], options).all_formats(source)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
desc 'report SOURCE FORMAT [OUTPUT]', 'Generate a full report in various formats (terminal, html, pdf, md, conf)'
|
|
249
|
+
method_option :color, type: :boolean, default: true, desc: 'Enable/disable color output'
|
|
250
|
+
method_option :clean, type: :boolean, default: true, desc: 'Clean the report directory before generation'
|
|
251
|
+
def report(input_source, format = 'terminal', output = nil)
|
|
252
|
+
if format.to_sym != :terminal && output.nil? && options[:clean]
|
|
253
|
+
ReportGenerator.clean_report_dir(input_source, **options)
|
|
254
|
+
end
|
|
255
|
+
PredictabilityEngine.run_and_print_report(input_source, format, options, output: output)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
desc 'summary SOURCE', 'Load data from SOURCE and show flow metrics summary'
|
|
259
|
+
method_option :color, type: :boolean, default: true, desc: 'Enable/disable color output'
|
|
260
|
+
def summary(source)
|
|
261
|
+
items = PredictabilityEngine.load_items(source)
|
|
262
|
+
PredictabilityEngine.logger.info { SummaryVisualizer.metrics_terminal(items, color: options[:color]) }
|
|
263
|
+
end
|
|
294
264
|
|
|
295
265
|
desc 'forecast SOURCE BACKLOG_COUNT', 'Run Monte Carlo simulation for BACKLOG_COUNT items'
|
|
296
266
|
def forecast(source, backlog_count)
|
|
@@ -343,18 +313,41 @@ module PredictabilityEngine
|
|
|
343
313
|
PredictabilityEngine.logger.info { "Synthetic #{options[:size]} dataset written to #{path}" }
|
|
344
314
|
end
|
|
345
315
|
|
|
346
|
-
|
|
316
|
+
desc 'viz SUBCOMMAND ...ARGS', 'Visualization commands (individual charts and format-specific reports)'
|
|
317
|
+
subcommand 'viz', Viz
|
|
347
318
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
319
|
+
desc 'setup', 'Install/update all dependencies (Ruby gems + Node.js + Playwright + Chromium)'
|
|
320
|
+
def setup
|
|
321
|
+
SetupManager.new.run
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
desc 'ask_ai SOURCE QUESTION', 'Ask the AI assistant about the data in SOURCE'
|
|
325
|
+
def ask_ai(source, question)
|
|
326
|
+
manager = DataManager.new
|
|
327
|
+
manager.load(source)
|
|
328
|
+
assistant = Agents::Assistant.new(manager)
|
|
329
|
+
PredictabilityEngine.logger.info { 'AI Thinking...' }
|
|
330
|
+
response = assistant.ask(question)
|
|
331
|
+
PredictabilityEngine.logger.info { 'AI Response:' }
|
|
332
|
+
PredictabilityEngine.logger.info { '------------' }
|
|
333
|
+
if response.respond_to?(:content)
|
|
334
|
+
PredictabilityEngine.logger.info { response.content }
|
|
353
335
|
else
|
|
354
|
-
|
|
336
|
+
PredictabilityEngine.logger.info { response }
|
|
355
337
|
end
|
|
356
338
|
end
|
|
357
339
|
|
|
340
|
+
desc 'jira SUBCOMMAND ...ARGS', 'Jira connection and workflow configuration'
|
|
341
|
+
subcommand 'jira', Jira
|
|
342
|
+
|
|
343
|
+
desc 'version', 'Print the predictability-engine version'
|
|
344
|
+
def version
|
|
345
|
+
say VERSION
|
|
346
|
+
end
|
|
347
|
+
map '--version' => :version
|
|
348
|
+
|
|
349
|
+
private
|
|
350
|
+
|
|
358
351
|
def print_calibration_results(result)
|
|
359
352
|
PredictabilityEngine.logger.info { 'Monte Carlo Hindcast Calibration' }
|
|
360
353
|
PredictabilityEngine.logger.info { '---------------------------------' }
|
|
@@ -386,29 +379,5 @@ module PredictabilityEngine
|
|
|
386
379
|
PredictabilityEngine.logger.info { " #{p}% confidence: Done in #{val} days" }
|
|
387
380
|
end
|
|
388
381
|
end
|
|
389
|
-
|
|
390
|
-
public
|
|
391
|
-
|
|
392
|
-
desc 'ask_ai SOURCE QUESTION', 'Ask the AI assistant about the data in SOURCE'
|
|
393
|
-
def ask_ai(source, question)
|
|
394
|
-
# Assistant needs the manager or at least items.
|
|
395
|
-
manager = DataManager.new
|
|
396
|
-
manager.load(source)
|
|
397
|
-
|
|
398
|
-
assistant = Agents::Assistant.new(manager)
|
|
399
|
-
PredictabilityEngine.logger.info { 'AI Thinking...' }
|
|
400
|
-
response = assistant.ask(question)
|
|
401
|
-
|
|
402
|
-
# response is an array of messages or similar depending on langchain version
|
|
403
|
-
# In recent langchainrb versions assistant.run returns the last message
|
|
404
|
-
PredictabilityEngine.logger.info { 'AI Response:' }
|
|
405
|
-
PredictabilityEngine.logger.info { '------------' }
|
|
406
|
-
# Assuming response is a message object with .content
|
|
407
|
-
if response.respond_to?(:content)
|
|
408
|
-
PredictabilityEngine.logger.info { response.content }
|
|
409
|
-
else
|
|
410
|
-
PredictabilityEngine.logger.info { response }
|
|
411
|
-
end
|
|
412
|
-
end
|
|
413
382
|
end
|
|
414
383
|
end
|
|
@@ -74,6 +74,14 @@ module PredictabilityEngine
|
|
|
74
74
|
(@priority_aliases || {})[name.to_s] || name
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
def resolve_path(path)
|
|
78
|
+
return path if Pathname.new(path).absolute? || File.exist?(path)
|
|
79
|
+
|
|
80
|
+
gem_root = File.expand_path('../../..', __dir__)
|
|
81
|
+
gem_relative = File.join(gem_root, path)
|
|
82
|
+
File.exist?(gem_relative) ? gem_relative : path
|
|
83
|
+
end
|
|
84
|
+
|
|
77
85
|
def mock_data(env_key)
|
|
78
86
|
json = ENV.fetch(env_key, '[]')
|
|
79
87
|
JSON.parse(json, symbolize_names: true)
|
|
@@ -12,11 +12,12 @@ module PredictabilityEngine
|
|
|
12
12
|
}.freeze
|
|
13
13
|
|
|
14
14
|
def perform_load(path)
|
|
15
|
-
|
|
15
|
+
resolved = resolve_path(path)
|
|
16
|
+
config = load_csv_config(resolved)
|
|
16
17
|
@url_prefix ||= config['url_prefix']
|
|
17
18
|
@done_statuses = load_done_statuses(config)
|
|
18
|
-
@source_url = "file://#{File.expand_path(
|
|
19
|
-
CSV.open(
|
|
19
|
+
@source_url = "file://#{File.expand_path(resolved)}"
|
|
20
|
+
CSV.open(resolved, headers: true, header_converters: :symbol, encoding: 'bom|UTF-8', row_sep: :auto)
|
|
20
21
|
.then { |csv| load_data(csv.map { |row| apply_jira_header_map(row.to_h) }) }
|
|
21
22
|
end
|
|
22
23
|
|
|
@@ -8,7 +8,7 @@ module PredictabilityEngine
|
|
|
8
8
|
def perform_load(path)
|
|
9
9
|
return build_work_items(mock_data('MOCK_EXCEL_DATA')) if ENV['MOCK_EXCEL_DATA']
|
|
10
10
|
|
|
11
|
-
xlsx = Roo::Spreadsheet.open(path)
|
|
11
|
+
xlsx = Roo::Spreadsheet.open(resolve_path(path))
|
|
12
12
|
iterator = xlsx.sheet(0).each(id: 'id', start_date: 'start_date', end_date: 'end_date')
|
|
13
13
|
.reject { |row| row[:id] == 'id' }
|
|
14
14
|
load_data(iterator)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PredictabilityEngine
|
|
4
|
-
# Mixin providing interactive credential prompts for `
|
|
5
|
-
# Relies on `ask`
|
|
4
|
+
# Mixin providing interactive credential prompts for `jira config --auth-mode`.
|
|
5
|
+
# Relies on `ask` being available from the including Thor class.
|
|
6
6
|
module JiraConfigPrompter
|
|
7
7
|
def build_profile_data(site, context_path, mode)
|
|
8
8
|
data = { 'site' => site }
|
|
@@ -29,6 +29,16 @@ module PredictabilityEngine
|
|
|
29
29
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
|
+
def ask_secret(prompt)
|
|
33
|
+
if $stdin.isatty
|
|
34
|
+
result = ask(prompt, echo: false)
|
|
35
|
+
puts ''
|
|
36
|
+
result
|
|
37
|
+
else
|
|
38
|
+
ask(prompt)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
32
42
|
def prompt_mfa_api_fields
|
|
33
43
|
field = ask('Token field in login response (default: access_token):').strip
|
|
34
44
|
{ 'email' => ask('Jira email:'),
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: predictability-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- cbp-org
|
|
@@ -349,16 +349,16 @@ dependencies:
|
|
|
349
349
|
name: rake-gem-maintenance
|
|
350
350
|
requirement: !ruby/object:Gem::Requirement
|
|
351
351
|
requirements:
|
|
352
|
-
- - "
|
|
352
|
+
- - "~>"
|
|
353
353
|
- !ruby/object:Gem::Version
|
|
354
|
-
version: '0'
|
|
354
|
+
version: '0.3'
|
|
355
355
|
type: :development
|
|
356
356
|
prerelease: false
|
|
357
357
|
version_requirements: !ruby/object:Gem::Requirement
|
|
358
358
|
requirements:
|
|
359
|
-
- - "
|
|
359
|
+
- - "~>"
|
|
360
360
|
- !ruby/object:Gem::Version
|
|
361
|
-
version: '0'
|
|
361
|
+
version: '0.3'
|
|
362
362
|
- !ruby/object:Gem::Dependency
|
|
363
363
|
name: redcarpet
|
|
364
364
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -465,6 +465,8 @@ files:
|
|
|
465
465
|
- bin/predictability-engine
|
|
466
466
|
- bin/predictability-engine.bat
|
|
467
467
|
- bin/setup
|
|
468
|
+
- bin/setup.bat
|
|
469
|
+
- bin/setup.ps1
|
|
468
470
|
- data/samples/sample_data.csv
|
|
469
471
|
- data/samples/sample_data_large.csv
|
|
470
472
|
- data/samples/wip_data.csv
|