reanimator 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/CHANGELOG.md +207 -0
- data/LICENSE.txt +21 -0
- data/README.md +483 -0
- data/SECURITY.md +32 -0
- data/exe/reanimator +6 -0
- data/lib/reanimator/acceptance_criterion.rb +75 -0
- data/lib/reanimator/agent_connection.rb +62 -0
- data/lib/reanimator/agent_profile.rb +51 -0
- data/lib/reanimator/agents/adapter.rb +49 -0
- data/lib/reanimator/agents/capability.rb +29 -0
- data/lib/reanimator/agents/constraints.rb +57 -0
- data/lib/reanimator/agents/hosted.rb +38 -0
- data/lib/reanimator/agents/local.rb +46 -0
- data/lib/reanimator/agents/patch.rb +60 -0
- data/lib/reanimator/agents/repair_task.rb +51 -0
- data/lib/reanimator/agents/result.rb +42 -0
- data/lib/reanimator/agents/strategies/arithmetic_fix.rb +51 -0
- data/lib/reanimator/agents/strategies/base.rb +26 -0
- data/lib/reanimator/agents.rb +38 -0
- data/lib/reanimator/autonomy_level.rb +41 -0
- data/lib/reanimator/baseline.rb +106 -0
- data/lib/reanimator/baseline_runner.rb +81 -0
- data/lib/reanimator/bounded_command.rb +87 -0
- data/lib/reanimator/classifier.rb +89 -0
- data/lib/reanimator/cli.rb +273 -0
- data/lib/reanimator/cohort.rb +61 -0
- data/lib/reanimator/commissioning/fixture.rb +69 -0
- data/lib/reanimator/commissioning/report.rb +45 -0
- data/lib/reanimator/commissioning/test.rb +145 -0
- data/lib/reanimator/confidence.rb +66 -0
- data/lib/reanimator/configuration.rb +35 -0
- data/lib/reanimator/contract_file.rb +116 -0
- data/lib/reanimator/contract_snapshot.rb +188 -0
- data/lib/reanimator/criteria_stack.rb +91 -0
- data/lib/reanimator/criterion_result.rb +70 -0
- data/lib/reanimator/detectors/base.rb +49 -0
- data/lib/reanimator/detectors/bundler.rb +68 -0
- data/lib/reanimator/detectors/commands.rb +112 -0
- data/lib/reanimator/detectors/fork.rb +25 -0
- data/lib/reanimator/detectors/github_actions.rb +68 -0
- data/lib/reanimator/detectors/operational_proof.rb +48 -0
- data/lib/reanimator/detectors/rails_app.rb +51 -0
- data/lib/reanimator/detectors/relationships.rb +46 -0
- data/lib/reanimator/detectors/ruby_gem.rb +40 -0
- data/lib/reanimator/detectors/security_controls.rb +36 -0
- data/lib/reanimator/discovered_fact.rb +99 -0
- data/lib/reanimator/discovery.rb +48 -0
- data/lib/reanimator/discovery_result.rb +89 -0
- data/lib/reanimator/drift_detector.rb +227 -0
- data/lib/reanimator/drift_report.rb +86 -0
- data/lib/reanimator/effective_contract.rb +108 -0
- data/lib/reanimator/effective_policy_resolver.rb +116 -0
- data/lib/reanimator/errors.rb +21 -0
- data/lib/reanimator/evaluators/base.rb +55 -0
- data/lib/reanimator/evaluators/command.rb +34 -0
- data/lib/reanimator/evaluators/documentation_check.rb +25 -0
- data/lib/reanimator/evaluators/http_smoke.rb +19 -0
- data/lib/reanimator/evaluators/repository_integrity.rb +22 -0
- data/lib/reanimator/evaluators/security_evidence.rb +24 -0
- data/lib/reanimator/evaluators/support_matrix.rb +25 -0
- data/lib/reanimator/evaluators.rb +41 -0
- data/lib/reanimator/family/gate.rb +40 -0
- data/lib/reanimator/family/gate_result.rb +46 -0
- data/lib/reanimator/family/gate_runner.rb +91 -0
- data/lib/reanimator/family.rb +89 -0
- data/lib/reanimator/family_inference.rb +113 -0
- data/lib/reanimator/git.rb +72 -0
- data/lib/reanimator/isolation.rb +36 -0
- data/lib/reanimator/language_inference.rb +43 -0
- data/lib/reanimator/lifecycle_intent.rb +93 -0
- data/lib/reanimator/obligation.rb +35 -0
- data/lib/reanimator/permission_model.rb +61 -0
- data/lib/reanimator/portfolio.rb +68 -0
- data/lib/reanimator/portfolio_drift.rb +120 -0
- data/lib/reanimator/profile_inference.rb +37 -0
- data/lib/reanimator/profiles.rb +110 -0
- data/lib/reanimator/proof_of_life.rb +45 -0
- data/lib/reanimator/proposed_change.rb +55 -0
- data/lib/reanimator/provenance.rb +39 -0
- data/lib/reanimator/relationship.rb +62 -0
- data/lib/reanimator/repair_run.rb +146 -0
- data/lib/reanimator/reports/agents.rb +28 -0
- data/lib/reanimator/reports/baseline.rb +73 -0
- data/lib/reanimator/reports/commissioning.rb +48 -0
- data/lib/reanimator/reports/contract.rb +70 -0
- data/lib/reanimator/reports/discovery.rb +38 -0
- data/lib/reanimator/reports/drift.rb +63 -0
- data/lib/reanimator/reports/families.rb +41 -0
- data/lib/reanimator/reports/family_gates.rb +41 -0
- data/lib/reanimator/reports/github_check.rb +45 -0
- data/lib/reanimator/reports/inventory.rb +37 -0
- data/lib/reanimator/reports/permissions.rb +32 -0
- data/lib/reanimator/reports/portfolio_baseline.rb +68 -0
- data/lib/reanimator/reports/portfolio_drift.rb +107 -0
- data/lib/reanimator/reports/run.rb +52 -0
- data/lib/reanimator/reports/snapshot.rb +58 -0
- data/lib/reanimator/reports.rb +36 -0
- data/lib/reanimator/repository.rb +96 -0
- data/lib/reanimator/repository_metadata.rb +78 -0
- data/lib/reanimator/repository_state.rb +48 -0
- data/lib/reanimator/routing/decision.rb +35 -0
- data/lib/reanimator/routing/router.rb +122 -0
- data/lib/reanimator/run.rb +48 -0
- data/lib/reanimator/safety_floor.rb +81 -0
- data/lib/reanimator/security_control.rb +87 -0
- data/lib/reanimator/security_posture.rb +80 -0
- data/lib/reanimator/snapshot_store.rb +78 -0
- data/lib/reanimator/task_class.rb +63 -0
- data/lib/reanimator/version.rb +5 -0
- data/lib/reanimator/vitality_profile.rb +44 -0
- data/lib/reanimator/waiver.rb +74 -0
- data/lib/reanimator.rb +214 -0
- metadata +262 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
# A suggested triage grouping for a repository during intake (specification
|
|
5
|
+
# section 04). Cohorts exist so a maintainer can accept sensible defaults in
|
|
6
|
+
# bulk while still seeing - and overriding - the intent behind each grouping.
|
|
7
|
+
# Crucially, the tool never preselects every repository; it recommends a
|
|
8
|
+
# small, high-confidence set that can reach a useful baseline quickly.
|
|
9
|
+
class Cohort
|
|
10
|
+
attr_reader :key, :title, :default_intent, :treatment
|
|
11
|
+
|
|
12
|
+
def initialize(key:, title:, default_intent:, treatment:)
|
|
13
|
+
@key = key
|
|
14
|
+
@title = title
|
|
15
|
+
@default_intent = default_intent
|
|
16
|
+
@treatment = treatment
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Whether repositories in this cohort are recommended for active
|
|
21
|
+
# onboarding (versus deferred, observed, or preserved).
|
|
22
|
+
def recommended_for_onboarding?
|
|
23
|
+
key == :active_original
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
ALL = {
|
|
27
|
+
active_original: new(
|
|
28
|
+
key: :active_original, title: "Active original repositories",
|
|
29
|
+
default_intent: :maintain, treatment: "Recommend for onboarding."
|
|
30
|
+
),
|
|
31
|
+
recent_experiment: new(
|
|
32
|
+
key: :recent_experiment, title: "Recent experiments",
|
|
33
|
+
default_intent: :incubate, treatment: "Suggest incubate or monitor."
|
|
34
|
+
),
|
|
35
|
+
readme_only: new(
|
|
36
|
+
key: :readme_only, title: "README-only concepts and presentations",
|
|
37
|
+
default_intent: :incubate, treatment: "Ask whether to incubate, preserve, or ignore."
|
|
38
|
+
),
|
|
39
|
+
archived: new(
|
|
40
|
+
key: :archived, title: "Archived repositories",
|
|
41
|
+
default_intent: :preserve, treatment: "Suggest preserve or retire; never auto-modernize."
|
|
42
|
+
),
|
|
43
|
+
old_unarchived: new(
|
|
44
|
+
key: :old_unarchived, title: "Old unarchived repositories",
|
|
45
|
+
default_intent: :monitor, treatment: "Flag intent ambiguity rather than assuming maintenance."
|
|
46
|
+
),
|
|
47
|
+
fork_observation: new(
|
|
48
|
+
key: :fork_observation, title: "Forks",
|
|
49
|
+
default_intent: :upstream, treatment: "Upstream observation until unique-commit intent is confirmed."
|
|
50
|
+
),
|
|
51
|
+
constrained: new(
|
|
52
|
+
key: :constrained, title: "Hardware or credential-dependent projects",
|
|
53
|
+
default_intent: :monitor, treatment: "Suggest a constrained-environment profile."
|
|
54
|
+
)
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
def self.fetch(key)
|
|
58
|
+
ALL.fetch(key.to_sym) { raise UnknownError, "Unknown cohort #{key.inspect}." }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Reanimator
|
|
7
|
+
module Commissioning
|
|
8
|
+
# Builds a disposable fixture repository with a single, seeded defect
|
|
9
|
+
# (specification section 08 step 2: "run a synthetic repair against a
|
|
10
|
+
# disposable fixture repository"). The fixture ships an addition helper that
|
|
11
|
+
# wrongly subtracts and a required check that fails because of it, plus a
|
|
12
|
+
# protected verifier file the agent must not touch. Nothing here depends on
|
|
13
|
+
# Bundler, so commissioning is fast and hermetic.
|
|
14
|
+
module Fixture
|
|
15
|
+
BUGGY_LIB = <<~RUBY
|
|
16
|
+
# frozen_string_literal: true
|
|
17
|
+
|
|
18
|
+
module Calc
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def add(a, b)
|
|
22
|
+
a - b
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
RUBY
|
|
26
|
+
|
|
27
|
+
CHECK = <<~RUBY
|
|
28
|
+
# frozen_string_literal: true
|
|
29
|
+
|
|
30
|
+
require "calc"
|
|
31
|
+
|
|
32
|
+
raise "add(2, 2) should be 4, got \#{Calc.add(2, 2)}" unless Calc.add(2, 2) == 4
|
|
33
|
+
puts "ok"
|
|
34
|
+
RUBY
|
|
35
|
+
|
|
36
|
+
CONTRACT = <<~YAML
|
|
37
|
+
version: 1
|
|
38
|
+
intent: maintain
|
|
39
|
+
profile: generic
|
|
40
|
+
acceptance:
|
|
41
|
+
protected_paths:
|
|
42
|
+
- check.rb
|
|
43
|
+
checks:
|
|
44
|
+
- id: arithmetic
|
|
45
|
+
claim: "add(2, 2) equals 4."
|
|
46
|
+
run: "ruby -Ilib check.rb"
|
|
47
|
+
required: true
|
|
48
|
+
YAML
|
|
49
|
+
|
|
50
|
+
module_function
|
|
51
|
+
|
|
52
|
+
# @return [String] path to a fresh disposable fixture
|
|
53
|
+
def build
|
|
54
|
+
dir = Dir.mktmpdir("reanimator-commission")
|
|
55
|
+
write(dir, "lib/calc.rb", BUGGY_LIB)
|
|
56
|
+
write(dir, "check.rb", CHECK)
|
|
57
|
+
write(dir, ".reanimator.yml", CONTRACT)
|
|
58
|
+
write(dir, "README.md", "# Commissioning fixture\n")
|
|
59
|
+
dir
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def write(dir, relative, contents)
|
|
63
|
+
full = File.join(dir, relative)
|
|
64
|
+
FileUtils.mkdir_p(File.dirname(full))
|
|
65
|
+
File.write(full, contents)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Commissioning
|
|
5
|
+
# The outcome of a commissioning test (specification section 08 step 6:
|
|
6
|
+
# "mark the profile ready, degraded, incompatible, or unavailable"), plus
|
|
7
|
+
# the observable facts step 5 requires: cost, latency, permissions, tool
|
|
8
|
+
# use, and any network destinations. Reanimator computes the verdict
|
|
9
|
+
# itself from independent evaluation; the agent never grades its own work.
|
|
10
|
+
class Report
|
|
11
|
+
attr_reader :provider, :readiness, :reason, :capabilities, :objective,
|
|
12
|
+
:pre_state, :post_state, :cost_usd, :latency_s, :tool_use, :network
|
|
13
|
+
|
|
14
|
+
def initialize(provider:, readiness:, reason: nil, capabilities: [], objective: nil,
|
|
15
|
+
pre_state: nil, post_state: nil, cost_usd: 0.0, latency_s: 0.0, tool_use: [], network: [])
|
|
16
|
+
@provider = provider
|
|
17
|
+
@readiness = readiness.to_sym
|
|
18
|
+
@reason = reason
|
|
19
|
+
@capabilities = capabilities
|
|
20
|
+
@objective = objective
|
|
21
|
+
@pre_state = pre_state
|
|
22
|
+
@post_state = post_state
|
|
23
|
+
@cost_usd = cost_usd
|
|
24
|
+
@latency_s = latency_s
|
|
25
|
+
@tool_use = tool_use
|
|
26
|
+
@network = network
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ready? = readiness == :ready
|
|
30
|
+
|
|
31
|
+
# The whole point of the slice: a profile can be proven ready without any
|
|
32
|
+
# repository write. Commissioning only ever touches disposable fixtures.
|
|
33
|
+
def proven_without_writes? = true
|
|
34
|
+
|
|
35
|
+
def to_h
|
|
36
|
+
{
|
|
37
|
+
provider: provider, readiness: readiness, reason: reason,
|
|
38
|
+
capabilities: capabilities, objective: objective,
|
|
39
|
+
pre_state: pre_state, post_state: post_state,
|
|
40
|
+
cost_usd: cost_usd, latency_s: latency_s, tool_use: tool_use, network: network
|
|
41
|
+
}.compact
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Reanimator
|
|
7
|
+
module Commissioning
|
|
8
|
+
# Runs the six-step commissioning test from specification section 08:
|
|
9
|
+
# validate credentials and capabilities, run a synthetic repair against a
|
|
10
|
+
# disposable fixture, require a structured plan and patch, evaluate the
|
|
11
|
+
# result *independently*, report the observable cost/latency/tools/network,
|
|
12
|
+
# and mark the profile ready/degraded/incompatible/unavailable.
|
|
13
|
+
#
|
|
14
|
+
# Verifier isolation (section 09) is structural here: the agent works in its
|
|
15
|
+
# own copy and returns a patch as data; the harness applies that patch to a
|
|
16
|
+
# *separate, clean* copy and grades it with Reanimator's own evaluators.
|
|
17
|
+
# A patch that reaches for a protected or verifier path is refused - the
|
|
18
|
+
# anti-gaming rule, enforced.
|
|
19
|
+
class Test
|
|
20
|
+
ALLOWED_EDIT_PATHS = ["lib/**", "*.rb"].freeze
|
|
21
|
+
|
|
22
|
+
def initialize(adapter, fixture: Fixture)
|
|
23
|
+
@adapter = adapter
|
|
24
|
+
@fixture = fixture
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Commissioning::Report]
|
|
28
|
+
def run
|
|
29
|
+
return unavailable unless @adapter.available?
|
|
30
|
+
|
|
31
|
+
fixture_path = @fixture.build
|
|
32
|
+
scratch = [fixture_path]
|
|
33
|
+
begin
|
|
34
|
+
contract = contract_for(fixture_path)
|
|
35
|
+
pre = BaselineRunner.new(contract, execute: true).run
|
|
36
|
+
objective = pre.results.find { |r| r.required? && r.failed? }&.criterion
|
|
37
|
+
return incompatible("The fixture did not present a failing required objective.", pre: pre) unless objective
|
|
38
|
+
|
|
39
|
+
workspace = copy(fixture_path, scratch)
|
|
40
|
+
result = @adapter.repair(build_task(objective, workspace, contract, pre))
|
|
41
|
+
return incompatible("The agent returned no structured plan and patch.", pre: pre, result: result) unless result.structured?
|
|
42
|
+
|
|
43
|
+
if (violation = protected_violation(build_task(objective, workspace, contract, pre), result))
|
|
44
|
+
return incompatible(violation, pre: pre, result: result)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
eval_dir = copy(fixture_path, scratch)
|
|
48
|
+
result.patch.apply(eval_dir)
|
|
49
|
+
post = BaselineRunner.new(contract_for(eval_dir), execute: true).run
|
|
50
|
+
|
|
51
|
+
verdict(objective, pre, post, result)
|
|
52
|
+
ensure
|
|
53
|
+
scratch.each { |dir| FileUtils.remove_entry(dir, true) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def contract_for(path)
|
|
60
|
+
EffectivePolicyResolver.new(Repository.at(path)).resolve
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_task(objective, workspace, contract, pre)
|
|
64
|
+
Agents::RepairTask.new(
|
|
65
|
+
objective: objective, workspace: workspace, evidence: pre,
|
|
66
|
+
constraints: Agents::Constraints.new(
|
|
67
|
+
allowed_paths: ALLOWED_EDIT_PATHS,
|
|
68
|
+
protected_paths: contract.protected_paths,
|
|
69
|
+
max_cost_usd: @adapter.connection.max_cost_usd,
|
|
70
|
+
code_egress: @adapter.code_egress
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Anti-gaming (section 09): a repair may not edit a protected or
|
|
76
|
+
# out-of-scope path. This is what stops a task from rewriting the rules of
|
|
77
|
+
# proof and then passing under the new rules.
|
|
78
|
+
def protected_violation(task, result)
|
|
79
|
+
bad = result.patch.paths.reject { |path| task.may_edit?(path) }
|
|
80
|
+
"The patch attempted to edit protected or out-of-scope paths: #{bad.join(", ")}." unless bad.empty?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def verdict(objective, pre, post, result)
|
|
84
|
+
target = post.results.find { |r| r.criterion.id == objective.id }
|
|
85
|
+
regressions = regressed(pre, post)
|
|
86
|
+
over_budget = result.cost_usd > @adapter.connection.max_cost_usd
|
|
87
|
+
|
|
88
|
+
if target&.verified? && regressions.empty? && !over_budget
|
|
89
|
+
report(:ready, pre: pre, post: post, result: result, objective: objective)
|
|
90
|
+
else
|
|
91
|
+
report(:degraded, pre: pre, post: post, result: result, objective: objective,
|
|
92
|
+
reason: degraded_reason(target, regressions, over_budget))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Required criteria that were verified before the repair but are not
|
|
97
|
+
# after it - genuine regressions the patch must not introduce.
|
|
98
|
+
def regressed(pre, post)
|
|
99
|
+
proven = pre.results.select { |r| r.required? && r.verified? }.map { |r| r.criterion.id }
|
|
100
|
+
post.results.select { |r| proven.include?(r.criterion.id) && !r.verified? }.map { |r| r.criterion.id }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def degraded_reason(target, regressions, over_budget)
|
|
104
|
+
if target.nil? || !target.verified?
|
|
105
|
+
"The patch did not resolve the objective."
|
|
106
|
+
elsif over_budget
|
|
107
|
+
"The repair exceeded the connection budget."
|
|
108
|
+
else
|
|
109
|
+
"The repair regressed: #{regressions.join(", ")}."
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def copy(source, scratch)
|
|
114
|
+
destination = Dir.mktmpdir("reanimator-ws")
|
|
115
|
+
FileUtils.cp_r(File.join(source, "."), destination)
|
|
116
|
+
scratch << destination
|
|
117
|
+
destination
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def unavailable
|
|
121
|
+
report(:unavailable, reason: @adapter.unavailable_reason)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def incompatible(reason, **rest)
|
|
125
|
+
report(:incompatible, reason: reason, **rest)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def report(readiness, pre: nil, post: nil, result: nil, objective: nil, reason: nil)
|
|
129
|
+
Report.new(
|
|
130
|
+
provider: @adapter.provider,
|
|
131
|
+
readiness: readiness,
|
|
132
|
+
reason: reason,
|
|
133
|
+
capabilities: @adapter.capabilities,
|
|
134
|
+
objective: objective&.id,
|
|
135
|
+
pre_state: pre&.state&.to_sym,
|
|
136
|
+
post_state: post&.state&.to_sym,
|
|
137
|
+
cost_usd: result&.cost_usd || 0.0,
|
|
138
|
+
latency_s: result&.latency_s || 0.0,
|
|
139
|
+
tool_use: result&.tool_use || [],
|
|
140
|
+
network: result&.network || []
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
# How strongly a discovered fact is believed, and - just as importantly -
|
|
5
|
+
# what the product is allowed to do with that belief (specification section
|
|
6
|
+
# 06). Confidence never silently becomes authority: even a high-confidence
|
|
7
|
+
# fact is only *preselected*, and it still asks for confirmation when it
|
|
8
|
+
# would affect permissions, cost, privacy, or action authority.
|
|
9
|
+
class Confidence
|
|
10
|
+
include Comparable
|
|
11
|
+
|
|
12
|
+
LEVELS = %i[low medium high].freeze
|
|
13
|
+
|
|
14
|
+
BEHAVIOR = {
|
|
15
|
+
high: "Preselect and ask for confirmation only when the fact affects " \
|
|
16
|
+
"permissions, cost, privacy, or action authority.",
|
|
17
|
+
medium: "Show as a suggestion with evidence and alternatives.",
|
|
18
|
+
low: "Use to prioritize a question; never convert directly into an obligation."
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :level
|
|
22
|
+
|
|
23
|
+
def initialize(level)
|
|
24
|
+
@level = level.to_sym
|
|
25
|
+
unless LEVELS.include?(@level)
|
|
26
|
+
raise UnknownError, "Unknown confidence #{level.inspect}. Known: #{LEVELS.join(", ")}."
|
|
27
|
+
end
|
|
28
|
+
freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
HIGH = new(:high)
|
|
32
|
+
MEDIUM = new(:medium)
|
|
33
|
+
LOW = new(:low)
|
|
34
|
+
|
|
35
|
+
def self.[](level)
|
|
36
|
+
case level
|
|
37
|
+
when Confidence then level
|
|
38
|
+
when :high, "high" then HIGH
|
|
39
|
+
when :medium, "medium" then MEDIUM
|
|
40
|
+
when :low, "low" then LOW
|
|
41
|
+
else new(level)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Product behavior mandated for this confidence level.
|
|
46
|
+
# @return [String]
|
|
47
|
+
def behavior = BEHAVIOR.fetch(level)
|
|
48
|
+
|
|
49
|
+
# High-confidence facts may be preselected in the review UI; lower ones may
|
|
50
|
+
# only be suggested or used to prioritize a question.
|
|
51
|
+
def preselectable? = level == :high
|
|
52
|
+
|
|
53
|
+
# A low-confidence fact must never be converted directly into an
|
|
54
|
+
# obligation - it can only shape a question.
|
|
55
|
+
def obligatable? = level != :low
|
|
56
|
+
|
|
57
|
+
def <=>(other)
|
|
58
|
+
return nil unless other.is_a?(Confidence)
|
|
59
|
+
LEVELS.index(level) <=> LEVELS.index(other.level)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def to_sym = level
|
|
63
|
+
|
|
64
|
+
def to_s = level.to_s
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
# Process-wide configuration. Everything here has a safe, quiet default so
|
|
5
|
+
# the library is useful with zero setup - configuration is for tightening,
|
|
6
|
+
# not for enabling basic behavior.
|
|
7
|
+
class Configuration
|
|
8
|
+
# The file a repository owner may commit to declare intent and overrides.
|
|
9
|
+
DEFAULT_CONTRACT_FILENAME = ".reanimator.yml"
|
|
10
|
+
|
|
11
|
+
# @return [Symbol] the security posture applied when a repository does not
|
|
12
|
+
# declare one (:strict, :balanced, :local, :custom). Section 20 sets
|
|
13
|
+
# :balanced for public repositories as the v1 default.
|
|
14
|
+
attr_accessor :security_posture
|
|
15
|
+
|
|
16
|
+
# @return [String] the filename scanned for a repository-owned contract.
|
|
17
|
+
attr_accessor :contract_filename
|
|
18
|
+
|
|
19
|
+
# @return [Integer] wall-clock ceiling, in seconds, for any single check
|
|
20
|
+
# executed during a baseline. Baselines are bounded by construction.
|
|
21
|
+
attr_accessor :command_timeout
|
|
22
|
+
|
|
23
|
+
# @return [Boolean] whether baseline checks may execute at all. Even when
|
|
24
|
+
# true, execution is opt-in per run; when false the runner only ever
|
|
25
|
+
# plans, never spawns a process.
|
|
26
|
+
attr_accessor :allow_execution
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
@security_posture = (ENV["REANIMATOR_SECURITY_POSTURE"] || "balanced").to_sym
|
|
30
|
+
@contract_filename = ENV["REANIMATOR_CONTRACT_FILE"] || DEFAULT_CONTRACT_FILENAME
|
|
31
|
+
@command_timeout = Integer(ENV["REANIMATOR_COMMAND_TIMEOUT"] || 900)
|
|
32
|
+
@allow_execution = ENV["REANIMATOR_ALLOW_EXECUTION"] != "false"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
module Reanimator
|
|
7
|
+
# Reads and writes the optional, repository-owned .reanimator.yml
|
|
8
|
+
# (specification section 07). The target for ordinary configuration is tiny -
|
|
9
|
+
# "one lifecycle decision and one reusable profile" - but the same file can
|
|
10
|
+
# carry the full resolved schema for advanced owners. Export must round-trip
|
|
11
|
+
# without semantic loss, so this class is the single reader and writer for
|
|
12
|
+
# that format.
|
|
13
|
+
class ContractFile
|
|
14
|
+
CURRENT_VERSION = 1
|
|
15
|
+
|
|
16
|
+
# @return [Hash] the parsed document with symbol keys
|
|
17
|
+
attr_reader :data
|
|
18
|
+
# @return [String, nil] path the document was loaded from
|
|
19
|
+
attr_reader :source_path
|
|
20
|
+
|
|
21
|
+
def initialize(data, source_path: nil)
|
|
22
|
+
@data = normalize(data)
|
|
23
|
+
@source_path = source_path
|
|
24
|
+
validate!
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Loads the contract file from a repository root, or returns nil when the
|
|
28
|
+
# owner has not committed one (the common, YAML-free case).
|
|
29
|
+
#
|
|
30
|
+
# @param root [String]
|
|
31
|
+
# @param filename [String]
|
|
32
|
+
# @return [ContractFile, nil]
|
|
33
|
+
def self.load(root, filename = Reanimator.configuration.contract_filename)
|
|
34
|
+
full = File.join(root, filename)
|
|
35
|
+
return nil unless File.file?(full)
|
|
36
|
+
|
|
37
|
+
# Date/Time are permitted so a waiver's `expires:` can be a bare date
|
|
38
|
+
# literal; no other non-primitive classes are allowed to load.
|
|
39
|
+
raw = YAML.safe_load_file(full, permitted_classes: [Date, Time], aliases: false) || {}
|
|
40
|
+
new(raw, source_path: full)
|
|
41
|
+
rescue Psych::SyntaxError => e
|
|
42
|
+
raise ContractError, "#{filename} is not valid YAML: #{e.message}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Serializes a contract hash (typically {EffectiveContract#to_contract_hash})
|
|
46
|
+
# back to YAML. Keys are stringified and ordered for a stable, diff-friendly
|
|
47
|
+
# export.
|
|
48
|
+
#
|
|
49
|
+
# @param hash [Hash]
|
|
50
|
+
# @return [String]
|
|
51
|
+
def self.dump(hash)
|
|
52
|
+
YAML.dump(stringify(hash)).sub(/\A---\n/, "")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def version = data.fetch(:version, CURRENT_VERSION)
|
|
56
|
+
|
|
57
|
+
# @return [Symbol, nil]
|
|
58
|
+
def intent
|
|
59
|
+
value = data[:intent]
|
|
60
|
+
value&.to_sym
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Symbol, nil]
|
|
64
|
+
def profile
|
|
65
|
+
value = data[:profile]
|
|
66
|
+
value&.to_sym
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def agents = data[:agents] || {}
|
|
70
|
+
|
|
71
|
+
def acceptance = data[:acceptance] || {}
|
|
72
|
+
|
|
73
|
+
def security = data[:security] || {}
|
|
74
|
+
|
|
75
|
+
def autonomy = data[:autonomy] || {}
|
|
76
|
+
|
|
77
|
+
def upgrades = data[:upgrades] || {}
|
|
78
|
+
|
|
79
|
+
def notifications = data[:notifications] || {}
|
|
80
|
+
|
|
81
|
+
def waivers = data[:waivers] || []
|
|
82
|
+
|
|
83
|
+
def family = data[:family]
|
|
84
|
+
|
|
85
|
+
def to_yaml = ContractFile.dump(data)
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def validate!
|
|
90
|
+
unless version.is_a?(Integer)
|
|
91
|
+
raise ContractError, "Contract version must be an integer, got #{version.inspect}."
|
|
92
|
+
end
|
|
93
|
+
if intent && !LifecycleIntent.valid?(intent)
|
|
94
|
+
raise ContractError, "Unknown lifecycle intent #{intent.inspect} in contract."
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def normalize(value)
|
|
99
|
+
case value
|
|
100
|
+
when Hash then value.each_with_object({}) { |(k, v), h| h[k.to_sym] = normalize(v) }
|
|
101
|
+
when Array then value.map { |v| normalize(v) }
|
|
102
|
+
else value
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def self.stringify(value)
|
|
107
|
+
case value
|
|
108
|
+
when Hash then value.each_with_object({}) { |(k, v), h| h[k.to_s] = stringify(v) }
|
|
109
|
+
when Array then value.map { |v| stringify(v) }
|
|
110
|
+
when Symbol then value.to_s
|
|
111
|
+
else value
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
private_class_method :stringify
|
|
115
|
+
end
|
|
116
|
+
end
|