brainiac-featureparity 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +135 -0
- data/lib/brainiac/plugins/featureparity/cli.rb +136 -0
- data/lib/brainiac/plugins/featureparity/config.rb +146 -0
- data/lib/brainiac/plugins/featureparity/handlers.rb +510 -0
- data/lib/brainiac/plugins/featureparity/metadata.rb +22 -0
- data/lib/brainiac/plugins/featureparity/prompts.rb +242 -0
- data/lib/brainiac/plugins/featureparity/signature.rb +89 -0
- data/lib/brainiac/plugins/featureparity/version.rb +9 -0
- data/lib/brainiac/plugins/featureparity.rb +156 -0
- data/lib/brainiac_featureparity.rb +4 -0
- metadata +121 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5e779e16dcca90b0d9197b375590889530e09c9cef791ebf5c08af05bf01152d
|
|
4
|
+
data.tar.gz: 3a59dc6ffe38d72fe0034c9aa3180faf08e0dc27bc0d8aa2b1ae9cc4192a1743
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b239f505695e27823695c4db25265848b267a68ae752533aae0f535248aa68a86e86bf7887da5bd5804d2b04ca546ce05222580627e67d182f91d54a0c5c9069
|
|
7
|
+
data.tar.gz: bc016d62bb67fc6d3f522c3d3f0b5f02f5189725fa216b2f98946615ed496779ec83b95ead917eeccf892d630fb893c985caaa32bc3221f019ecdf58975f5d25
|
data/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# brainiac-featureparity
|
|
2
|
+
|
|
3
|
+
FeatureParity plugin for [Brainiac](https://github.com/stowzilla/brainiac): your agents take
|
|
4
|
+
work straight from [FeatureParity](https://featureparity.dev).
|
|
5
|
+
|
|
6
|
+
- **Start work.** Someone presses **Start work** on a requirement and picks one of your
|
|
7
|
+
agents. That agent gets a worktree for the requirement and implements it, with tests
|
|
8
|
+
bound to it (`fp:<slug>`).
|
|
9
|
+
- **Tags.** Someone tags your agent in a requirement's comments (for example "please
|
|
10
|
+
re-check the suspect tests"). The agent reads the comment, does it, and replies on the
|
|
11
|
+
requirement.
|
|
12
|
+
- **Coverage gaps** (opt-in). A requirement goes live on a surface with no tests. An agent
|
|
13
|
+
writes them.
|
|
14
|
+
- **Rejections.** A human rejects a change your agent proposed. The same agent reworks it in
|
|
15
|
+
its existing worktree.
|
|
16
|
+
|
|
17
|
+
Every delivery is signed by FeatureParity, and the plugin refuses anything it can't verify.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
You need Brainiac running with a public URL that reaches it (Brainiac's setup uses an
|
|
22
|
+
[ngrok](https://ngrok.com) tunnel), and an account on FeatureParity.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
gem install brainiac # if you don't have Brainiac yet
|
|
26
|
+
brainiac setup # its own first-run setup
|
|
27
|
+
brainiac install featureparity
|
|
28
|
+
brainiac featureparity setup # writes ~/.brainiac/featureparity.json
|
|
29
|
+
brainiac restart
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Every repository your agents should work in must be a project in `~/.brainiac/projects.json`
|
|
33
|
+
(Brainiac's usual setup). FeatureParity names projects by their slug (`fp projects`). Name the
|
|
34
|
+
Brainiac project the same, or map it (see Configuration).
|
|
35
|
+
|
|
36
|
+
## Connect FeatureParity
|
|
37
|
+
|
|
38
|
+
### 1. Point your agents at Brainiac
|
|
39
|
+
|
|
40
|
+
In FeatureParity, open **Agents & API keys** (under your account). For each agent that runs
|
|
41
|
+
on this Brainiac, click **Wake-up** and enter:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
https://<your-public-url>/featureparity
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
FeatureParity shows a signing secret (`whsec_...`) **once**. Add it to
|
|
48
|
+
`~/.brainiac/featureparity.json`:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{ "signing_secrets": ["whsec_..."] }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
All your agents can share the one address, because each delivery names the agent it's for.
|
|
55
|
+
Each key has its own secret, so list every one.
|
|
56
|
+
|
|
57
|
+
The agent key's name has to match the Brainiac agent: a key named `galen` or shown as `Galen`
|
|
58
|
+
reaches the Brainiac agent `Galen`. Map names that differ with `"agents"`.
|
|
59
|
+
|
|
60
|
+
### 2. Optional: project-wide events
|
|
61
|
+
|
|
62
|
+
For events no single agent owns, add a webhook in **Project Settings → Integrations**: same
|
|
63
|
+
URL, provider **generic**. Subscribe to **Change rejected** and, if you turn on
|
|
64
|
+
`dispatch_gaps`, **Coverage gap**. Add its signing secret to `signing_secrets` too.
|
|
65
|
+
|
|
66
|
+
If your agents already have wake-up addresses, there's no need to subscribe this webhook to
|
|
67
|
+
**Work started** or **Agent tagged**. If you do, the plugin notices the repeat and starts the
|
|
68
|
+
agent only once.
|
|
69
|
+
|
|
70
|
+
### 3. Try it
|
|
71
|
+
|
|
72
|
+
Open an approved requirement, click **Start work**, and pick your agent. The page tells you
|
|
73
|
+
whether the agent received it. `brainiac logs` shows the dispatch. Each webhook's **Recent
|
|
74
|
+
deliveries** in FeatureParity shows what was sent and what Brainiac answered.
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
`~/.brainiac/featureparity.json` (reloaded when it changes):
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"api_base_url": "https://api.featureparity.dev",
|
|
83
|
+
"environment": "prod",
|
|
84
|
+
"accept_environments": ["prod"],
|
|
85
|
+
"signing_secrets": ["whsec_..."],
|
|
86
|
+
"agents": { "claude-laptop": "Galen" },
|
|
87
|
+
"default_agent": "Galen",
|
|
88
|
+
"projects": { "feature-parity": "feature_parity" },
|
|
89
|
+
"dispatch_gaps": false,
|
|
90
|
+
"gap_agent": "Galen"
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| Key | What it does |
|
|
95
|
+
|-----|--------------|
|
|
96
|
+
| `signing_secrets` | The `whsec_` secrets FeatureParity showed you. With any set, unsigned or wrongly signed deliveries are refused (401), and so is one stamped more than 5 minutes away from now. Leave it empty only while setting up. |
|
|
97
|
+
| `agents` | FeatureParity agent key name (or display name) → Brainiac agent name, for names that differ. Otherwise they match case-insensitively. |
|
|
98
|
+
| `default_agent` | Who takes **Start work** when no agent was picked. |
|
|
99
|
+
| `projects` | FeatureParity project slug → Brainiac project key, for keys that differ. A project in `projects.json` can also declare `"featureparity_project": "<slug>"`. |
|
|
100
|
+
| `dispatch_gaps` | Start an agent on `coverage.gap_detected` to write the missing tests. Off by default, because approving a batch of requirements reports a gap for each one. |
|
|
101
|
+
| `gap_agent` | Who writes those tests (defaults to `default_agent`). |
|
|
102
|
+
| `environment`, `accept_environments` | Which FeatureParity environments this Brainiac serves. A delivery from a preview environment (`fizzy-1234`) is ignored unless listed here. `["*"]` accepts all. |
|
|
103
|
+
|
|
104
|
+
## Events
|
|
105
|
+
|
|
106
|
+
| Event | What happens |
|
|
107
|
+
|-------|--------------|
|
|
108
|
+
| `requirement.work_started` | The named agent (or `default_agent`) gets the requirement's worktree (`fp-<slug>`, reused if it exists) and implements it. |
|
|
109
|
+
| `requirement.agent_mentioned` | The tagged agent, if it runs here, answers the comment, working in the requirement's worktree when there is one. |
|
|
110
|
+
| `coverage.gap_detected` | With `dispatch_gaps`, `gap_agent` writes tests for the surfaces listed. Otherwise it's acknowledged. |
|
|
111
|
+
| `requirement.change_rejected` | The agent that proposed the change reworks it in its worktree, with the reviewer's reason. |
|
|
112
|
+
| others | Acknowledged. |
|
|
113
|
+
|
|
114
|
+
The plugin answers within FeatureParity's 5-second window and does the work in the
|
|
115
|
+
background. FeatureParity retries a delivery that gets no answer; the plugin acts on each
|
|
116
|
+
delivery, and each Start work, comment or gap, once.
|
|
117
|
+
|
|
118
|
+
## CLI
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
brainiac featureparity setup # create the config and show these steps
|
|
122
|
+
brainiac featureparity config # show the config (secrets shortened)
|
|
123
|
+
brainiac featureparity status # what's set up
|
|
124
|
+
brainiac featureparity help
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Requirements
|
|
128
|
+
|
|
129
|
+
- Brainiac >= 0.1.8
|
|
130
|
+
- Ruby >= 3.4
|
|
131
|
+
- A FeatureParity account (agent wake-up addresses and signed deliveries need the current FeatureParity)
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brainiac
|
|
4
|
+
module Plugins
|
|
5
|
+
module Featureparity
|
|
6
|
+
# CLI commands for the FeatureParity plugin.
|
|
7
|
+
# Accessed via `brainiac featureparity <command>`.
|
|
8
|
+
module CLI
|
|
9
|
+
CONFIG_FILE = File.join(BRAINIAC_DIR, "featureparity.json")
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def cli(args)
|
|
13
|
+
command = args.shift
|
|
14
|
+
case command
|
|
15
|
+
when "setup"
|
|
16
|
+
setup_command
|
|
17
|
+
when "config"
|
|
18
|
+
config_command
|
|
19
|
+
when "status"
|
|
20
|
+
status_command
|
|
21
|
+
when "help", nil
|
|
22
|
+
help_command
|
|
23
|
+
else
|
|
24
|
+
puts "Unknown command: #{command}"
|
|
25
|
+
puts "Run 'brainiac featureparity help' for usage."
|
|
26
|
+
exit 1
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def completions
|
|
31
|
+
%w[setup config status help]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def configured?
|
|
35
|
+
File.exist?(CONFIG_FILE)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def help_text
|
|
39
|
+
"Manage FeatureParity webhook integration"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def setup_command
|
|
45
|
+
puts "Setting up FeatureParity plugin..."
|
|
46
|
+
|
|
47
|
+
if File.exist?(CONFIG_FILE)
|
|
48
|
+
puts "Config file already exists at #{CONFIG_FILE}"
|
|
49
|
+
print "Overwrite? [y/N] "
|
|
50
|
+
answer = $stdin.gets&.strip&.downcase
|
|
51
|
+
return unless answer == "y"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
config = {
|
|
55
|
+
"api_base_url" => "https://api.featureparity.dev",
|
|
56
|
+
"signing_secrets" => [],
|
|
57
|
+
"default_agent" => nil,
|
|
58
|
+
"dispatch_gaps" => false,
|
|
59
|
+
"authorized_users" => [],
|
|
60
|
+
"authorized_role_ids" => []
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
File.write(CONFIG_FILE, JSON.pretty_generate(config))
|
|
64
|
+
puts "Created config at #{CONFIG_FILE}\n\n"
|
|
65
|
+
puts <<~STEPS
|
|
66
|
+
Next steps (your public URL is whatever reaches this server, e.g. an ngrok tunnel):
|
|
67
|
+
1. In FeatureParity, Agents & API keys: give each agent that runs here the wake-up address
|
|
68
|
+
https://<your-public-url>/featureparity, and put the secret it shows in "signing_secrets".
|
|
69
|
+
2. Optional: Project Settings > Integrations > Add webhook, same URL, provider 'generic'
|
|
70
|
+
('Change rejected', 'Coverage gap'). Add its secret to "signing_secrets" too.
|
|
71
|
+
3. Agents match by name and projects by slug; map any that differ with "agents"/"projects".
|
|
72
|
+
|
|
73
|
+
STEPS
|
|
74
|
+
puts "Run 'brainiac restart' to load the plugin."
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def config_command
|
|
78
|
+
unless File.exist?(CONFIG_FILE)
|
|
79
|
+
puts "No config file found. Run 'brainiac featureparity setup' first."
|
|
80
|
+
exit 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
config = JSON.parse(File.read(CONFIG_FILE))
|
|
84
|
+
# Never print the signing secrets themselves.
|
|
85
|
+
config["signing_secrets"] = Array(config["signing_secrets"]).map { |s| "#{s.to_s[0, 10]}…" }
|
|
86
|
+
puts "FeatureParity Config (#{CONFIG_FILE}):"
|
|
87
|
+
puts JSON.pretty_generate(config)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def status_command
|
|
91
|
+
puts "FeatureParity Plugin Status"
|
|
92
|
+
puts "=" * 30
|
|
93
|
+
|
|
94
|
+
if File.exist?(CONFIG_FILE)
|
|
95
|
+
config = JSON.parse(File.read(CONFIG_FILE))
|
|
96
|
+
puts "Config: ✓ #{CONFIG_FILE}"
|
|
97
|
+
puts "API URL: #{config['api_base_url']}"
|
|
98
|
+
secrets = Array(config["signing_secrets"]).size
|
|
99
|
+
puts(secrets.zero? ? "Signatures: ✗ not checked (add signing_secrets)" : "Signatures: ✓ #{secrets} secret(s)")
|
|
100
|
+
gap_agent = config["gap_agent"] || config["default_agent"]
|
|
101
|
+
puts "Coverage gaps: #{config['dispatch_gaps'] ? "dispatched to #{gap_agent}" : 'acknowledged only'}"
|
|
102
|
+
else
|
|
103
|
+
puts "Config: ✗ Not configured (run 'brainiac featureparity setup')"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
puts
|
|
107
|
+
puts "Webhook endpoint: POST /featureparity"
|
|
108
|
+
puts "API endpoint: GET /api/featureparity"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def help_command
|
|
112
|
+
puts <<~HELP
|
|
113
|
+
brainiac featureparity — #{help_text}
|
|
114
|
+
|
|
115
|
+
Usage:
|
|
116
|
+
brainiac featureparity setup Create config file and show setup instructions
|
|
117
|
+
brainiac featureparity config Show current configuration
|
|
118
|
+
brainiac featureparity status Show plugin status
|
|
119
|
+
brainiac featureparity help Show this help message
|
|
120
|
+
|
|
121
|
+
Webhook Events:
|
|
122
|
+
The plugin receives webhooks from FeatureParity and dispatches agents
|
|
123
|
+
when requirements are rejected, evidence fails, or coverage gaps are detected.
|
|
124
|
+
|
|
125
|
+
Currently handled:
|
|
126
|
+
- requirement.change_rejected → Dispatch agent to fix rejected requirement
|
|
127
|
+
|
|
128
|
+
Configuration:
|
|
129
|
+
Config is stored at ~/.brainiac/featureparity.json
|
|
130
|
+
HELP
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brainiac
|
|
4
|
+
module Plugins
|
|
5
|
+
module Featureparity
|
|
6
|
+
# Config file at ~/.brainiac/featureparity.json
|
|
7
|
+
#
|
|
8
|
+
# {
|
|
9
|
+
# "api_base_url": "https://api.featureparity.dev",
|
|
10
|
+
# "environment": "prod",
|
|
11
|
+
# "accept_environments": ["prod", "staging"],
|
|
12
|
+
# "signing_secrets": ["whsec_..."],
|
|
13
|
+
# "agents": { "galen": "Galen" },
|
|
14
|
+
# "default_agent": "Galen",
|
|
15
|
+
# "projects": { "feature-parity": "feature_parity" },
|
|
16
|
+
# "dispatch_gaps": false,
|
|
17
|
+
# "gap_agent": "Galen",
|
|
18
|
+
# "authorized_users": [],
|
|
19
|
+
# "authorized_role_ids": []
|
|
20
|
+
# }
|
|
21
|
+
#
|
|
22
|
+
# signing_secrets: the whsec_ secrets FeatureParity showed you, one per project webhook
|
|
23
|
+
# or agent wake-up address pointed here. When any are set, an unsigned or wrongly
|
|
24
|
+
# signed delivery is refused (401). Leave empty only while setting up.
|
|
25
|
+
# agents: FeatureParity agent key name (or display name) → Brainiac agent name, for when
|
|
26
|
+
# they differ. Otherwise the names are matched case-insensitively.
|
|
27
|
+
# default_agent: who takes "Start work" with no agent named.
|
|
28
|
+
# projects: FeatureParity project slug → key in ~/.brainiac/projects.json, for when they
|
|
29
|
+
# differ. A project entry can also say "featureparity_project": "<slug>".
|
|
30
|
+
# dispatch_gaps: start an agent on coverage.gap_detected (write the missing tests).
|
|
31
|
+
# Off by default: approving a batch of requirements reports a gap for each.
|
|
32
|
+
#
|
|
33
|
+
# Environment matching:
|
|
34
|
+
# - `environment`: Which environment this brainiac instance represents (default: "prod")
|
|
35
|
+
# - `accept_environments`: Which environments' webhooks to process (default: same as environment)
|
|
36
|
+
#
|
|
37
|
+
# This prevents dev/ephemeral webhooks from triggering prod agents and vice versa.
|
|
38
|
+
# An ephemeral FP environment like "fizzy-1234" sends webhooks with that environment
|
|
39
|
+
# in the metadata — unless accept_environments includes "fizzy-1234", those are ignored.
|
|
40
|
+
module Config
|
|
41
|
+
CONFIG_FILE = File.join(BRAINIAC_DIR, "featureparity.json")
|
|
42
|
+
@config = nil
|
|
43
|
+
@config_mtime = nil
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
def load!
|
|
47
|
+
@config = read_config_file
|
|
48
|
+
@config_mtime = File.exist?(CONFIG_FILE) ? File.mtime(CONFIG_FILE) : nil
|
|
49
|
+
LOG.info "[FeatureParity] Config loaded from #{CONFIG_FILE}" if @config
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def current
|
|
53
|
+
reload_if_changed
|
|
54
|
+
@config || {}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def reload_if_changed
|
|
58
|
+
return unless File.exist?(CONFIG_FILE)
|
|
59
|
+
|
|
60
|
+
mtime = File.mtime(CONFIG_FILE)
|
|
61
|
+
return if @config_mtime && mtime == @config_mtime
|
|
62
|
+
|
|
63
|
+
@config = read_config_file
|
|
64
|
+
@config_mtime = mtime
|
|
65
|
+
LOG.debug "[FeatureParity] Config reloaded" if LOG.debug?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def api_base_url
|
|
69
|
+
current["api_base_url"] || "https://api.featureparity.dev"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Which environment this brainiac instance represents
|
|
73
|
+
def environment
|
|
74
|
+
current["environment"] || "prod"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Which environments' webhooks to process. Defaults to just this environment.
|
|
78
|
+
# Use ["prod", "staging", "dev"] to accept webhooks from multiple environments.
|
|
79
|
+
# Use ["*"] to accept webhooks from any environment (useful for dev/testing).
|
|
80
|
+
def accept_environments
|
|
81
|
+
current["accept_environments"] || [environment]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Check if this brainiac instance should process a webhook from the given environment.
|
|
85
|
+
# Returns true if:
|
|
86
|
+
# - accept_environments includes "*" (accept all)
|
|
87
|
+
# - accept_environments includes the webhook's environment
|
|
88
|
+
# - The webhook doesn't specify an environment (legacy/backward compatibility)
|
|
89
|
+
def should_accept_environment?(webhook_environment)
|
|
90
|
+
# No environment specified in webhook = backward compatibility, accept it
|
|
91
|
+
return true if webhook_environment.nil? || webhook_environment.to_s.strip.empty?
|
|
92
|
+
|
|
93
|
+
# Accept all environments
|
|
94
|
+
return true if accept_environments.include?("*")
|
|
95
|
+
|
|
96
|
+
# Check if this specific environment is accepted
|
|
97
|
+
accept_environments.include?(webhook_environment.to_s)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def signing_secrets
|
|
101
|
+
Array(current["signing_secrets"]).map(&:to_s).reject(&:empty?)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def agent_map
|
|
105
|
+
(current["agents"] || {}).transform_keys { |k| k.to_s.downcase }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def default_agent
|
|
109
|
+
current["default_agent"]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def project_map
|
|
113
|
+
current["projects"] || {}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def dispatch_gaps?
|
|
117
|
+
current["dispatch_gaps"] == true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def gap_agent
|
|
121
|
+
current["gap_agent"] || default_agent
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def authorized_users
|
|
125
|
+
current["authorized_users"] || []
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def authorized_role_ids
|
|
129
|
+
current["authorized_role_ids"] || []
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def read_config_file
|
|
135
|
+
return {} unless File.exist?(CONFIG_FILE)
|
|
136
|
+
|
|
137
|
+
JSON.parse(File.read(CONFIG_FILE))
|
|
138
|
+
rescue JSON::ParserError => e
|
|
139
|
+
LOG.warn "[FeatureParity] Invalid JSON in config: #{e.message}"
|
|
140
|
+
{}
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|