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,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Reports
|
|
5
|
+
# Lists coding-agent adapters and whether each is available right now
|
|
6
|
+
# (specification section 08). Hosted providers report themselves unavailable
|
|
7
|
+
# with a reason until connected, so the surface is honest about what can
|
|
8
|
+
# actually run.
|
|
9
|
+
class Agents
|
|
10
|
+
def initialize(adapters)
|
|
11
|
+
@adapters = adapters
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render
|
|
15
|
+
(["Coding agents:"] + @adapters.map { |adapter| line(adapter) }).join("\n")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def line(adapter)
|
|
21
|
+
status = adapter.available? ? "available" : "unavailable"
|
|
22
|
+
detail = adapter.available? ? "" : " — #{adapter.unavailable_reason}"
|
|
23
|
+
" #{Reports.glyph(adapter.available? ? :verified : :skipped)} #{adapter.provider} " \
|
|
24
|
+
"(#{status}) [#{adapter.capabilities.join(", ")}]#{detail}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Reports
|
|
5
|
+
# A full single-repository baseline report. It separates verified, failed,
|
|
6
|
+
# and unproven criteria the way section 14 requires, states the mode
|
|
7
|
+
# plainly (read-only/planned vs executed), and ends with the promise that
|
|
8
|
+
# nothing was changed.
|
|
9
|
+
class Baseline
|
|
10
|
+
def initialize(baseline)
|
|
11
|
+
@baseline = baseline
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render
|
|
15
|
+
[
|
|
16
|
+
header,
|
|
17
|
+
criteria_section,
|
|
18
|
+
accepted_risk_section,
|
|
19
|
+
notes_section,
|
|
20
|
+
"NO SOURCE CHANGES WERE MADE"
|
|
21
|
+
].compact.join("\n\n")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def accepted_risk_section
|
|
27
|
+
risks = @baseline.accepted_risks
|
|
28
|
+
return if risks.empty?
|
|
29
|
+
|
|
30
|
+
lines = risks.map { |r| " #{Reports.glyph(:accepted_risk)} #{r.criterion.id}: #{r.detail}" }
|
|
31
|
+
(["Accepted risk (waived) — counted as risk, not verified vitality:"] + lines).join("\n")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def header
|
|
35
|
+
contract = @baseline.contract
|
|
36
|
+
intent = "#{contract.intent} (#{contract.intent_confirmed? ? "confirmed" : "provisional"})"
|
|
37
|
+
# The baseline owns which criteria the rate is measured over, so the
|
|
38
|
+
# fraction and the percentage cannot drift apart.
|
|
39
|
+
rated = @baseline.rated_required
|
|
40
|
+
accepted = @baseline.accepted_risks.size
|
|
41
|
+
rate = (@baseline.verified_vitality_rate * 100).round
|
|
42
|
+
vvr = "Verified Vitality Rate: #{rate}% (#{rated.count(&:verified?)}/#{rated.size} required"
|
|
43
|
+
vvr += ", #{accepted} accepted risk" if accepted.positive?
|
|
44
|
+
vvr += ")"
|
|
45
|
+
[
|
|
46
|
+
"Baseline — #{@baseline.repository.full_name}",
|
|
47
|
+
"State: #{@baseline.state.to_s.upcase.tr("_", " ")} — #{@baseline.state.meaning}",
|
|
48
|
+
"Mode: #{@baseline.executed? ? "executed (bounded, read-only)" : "read-only (planned)"}",
|
|
49
|
+
"Intent: #{intent}",
|
|
50
|
+
"Profile: #{contract.profile.id} (#{contract.profile_source})",
|
|
51
|
+
"Contract: #{@baseline.policy_version}",
|
|
52
|
+
vvr
|
|
53
|
+
].join("\n")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def criteria_section
|
|
57
|
+
lines = @baseline.results.map do |result|
|
|
58
|
+
mark = result.required? ? "*" : " "
|
|
59
|
+
timing = result.duration ? " (#{result.duration}s)" : ""
|
|
60
|
+
detail = result.detail ? " — #{result.detail}" : ""
|
|
61
|
+
" #{Reports.glyph(result.status)}#{mark} #{result.criterion.id}: #{result.criterion.claim}#{detail}#{timing}"
|
|
62
|
+
end
|
|
63
|
+
(["Criteria (* = required):"] + lines).join("\n")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def notes_section
|
|
67
|
+
return if @baseline.contract.notes.empty?
|
|
68
|
+
|
|
69
|
+
(["Notes:"] + @baseline.contract.notes.map { |n| " - #{n}" }).join("\n")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Reports
|
|
5
|
+
# Renders a commissioning report (specification section 08). It states the
|
|
6
|
+
# readiness verdict, the objective's before/after state, and the observable
|
|
7
|
+
# cost/latency/tools/network - and closes by affirming the slice's promise:
|
|
8
|
+
# a profile was proven ready without any repository write.
|
|
9
|
+
class Commissioning
|
|
10
|
+
def initialize(report)
|
|
11
|
+
@report = report
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render
|
|
15
|
+
lines = [
|
|
16
|
+
"Commissioning — #{@report.provider}",
|
|
17
|
+
"Readiness: #{@report.readiness.to_s.upcase}",
|
|
18
|
+
"Capabilities: #{present(@report.capabilities)}"
|
|
19
|
+
]
|
|
20
|
+
lines << objective_line if @report.objective
|
|
21
|
+
lines << telemetry_line unless @report.readiness == :unavailable
|
|
22
|
+
lines << "Reason: #{@report.reason}" if @report.reason
|
|
23
|
+
lines << if @report.ready?
|
|
24
|
+
"A connected profile is proven ready. NO REPOSITORY WRITES WERE REQUIRED."
|
|
25
|
+
else
|
|
26
|
+
"No repository writes were made."
|
|
27
|
+
end
|
|
28
|
+
lines.join("\n")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def objective_line
|
|
34
|
+
transition = [@report.pre_state, @report.post_state].compact.map { |s| s.to_s.tr("_", " ") }.join(" → ")
|
|
35
|
+
"Objective: #{@report.objective}#{" (#{transition})" unless transition.empty?}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def telemetry_line
|
|
39
|
+
"Cost: $#{format("%.2f", @report.cost_usd)} Latency: #{@report.latency_s}s " \
|
|
40
|
+
"Tools: #{present(@report.tool_use)} Network: #{present(@report.network)}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def present(list)
|
|
44
|
+
(list.nil? || list.empty?) ? "none" : list.join(", ")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Reports
|
|
5
|
+
# Renders an effective contract for review: the resolved value of every
|
|
6
|
+
# decision, where it came from, and what is protected. Section 07 requires
|
|
7
|
+
# the resolved value, its source, and any elevated-review surfaces to be
|
|
8
|
+
# visible - this is the "minimal view" of the policy explorer.
|
|
9
|
+
class Contract
|
|
10
|
+
def initialize(contract)
|
|
11
|
+
@contract = contract
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render
|
|
15
|
+
[header, autonomy_section, security_section, criteria_section,
|
|
16
|
+
protected_section, notes_section].compact.join("\n\n")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def header
|
|
22
|
+
[
|
|
23
|
+
"Effective contract — #{@contract.repository.full_name}",
|
|
24
|
+
"Intent: #{@contract.intent} (#{@contract.intent_confirmed? ? "confirmed" : "provisional"})",
|
|
25
|
+
"Profile: #{@contract.profile.id} (#{@contract.profile_source}) — #{@contract.profile.summary}",
|
|
26
|
+
"Policy version: #{@contract.policy_version}"
|
|
27
|
+
].join("\n")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def autonomy_section
|
|
31
|
+
pairs = @contract.autonomy.map { |k, v| "#{k}=#{v}" }.join(" ")
|
|
32
|
+
"Autonomy: #{pairs}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Section 12 insists the posture is named and explained in plain language
|
|
36
|
+
# rather than hidden in settings, so it gets its own block: what it is,
|
|
37
|
+
# what it means, and exactly which controls it makes binding.
|
|
38
|
+
def security_section
|
|
39
|
+
posture = @contract.security_posture
|
|
40
|
+
[
|
|
41
|
+
"Security posture: #{posture}",
|
|
42
|
+
" #{posture.description}",
|
|
43
|
+
" Required: #{SecurityControl.labels(posture.required_controls)}",
|
|
44
|
+
" Advisory: #{SecurityControl.labels(posture.advisory_controls)}"
|
|
45
|
+
].join("\n")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def criteria_section
|
|
49
|
+
lines = @contract.criteria.map do |c|
|
|
50
|
+
mark = c.required? ? "*" : " "
|
|
51
|
+
elevated = c.elevated? ? " [elevated review]" : ""
|
|
52
|
+
how = c.run || c.evaluator
|
|
53
|
+
" #{mark} #{c.id}: #{c.claim} — #{how}#{elevated}"
|
|
54
|
+
end
|
|
55
|
+
(["Criteria (* = required):"] + lines).join("\n")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def protected_section
|
|
59
|
+
(["Protected paths (elevated review to change):"] +
|
|
60
|
+
@contract.protected_paths.map { |p| " - #{p}" }).join("\n")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def notes_section
|
|
64
|
+
return if @contract.notes.empty?
|
|
65
|
+
|
|
66
|
+
(["Notes:"] + @contract.notes.map { |n| " - #{n}" }).join("\n")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Reports
|
|
5
|
+
# Renders discovered facts grouped by detector category, each with its
|
|
6
|
+
# confidence and provenance. This is the contract-review surface's core
|
|
7
|
+
# promise from section 06: the report can always answer "Why does
|
|
8
|
+
# Reanimator believe this?"
|
|
9
|
+
class Discovery
|
|
10
|
+
def initialize(discovery)
|
|
11
|
+
@discovery = discovery
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render
|
|
15
|
+
sections = [suggested_profile]
|
|
16
|
+
@discovery.by_category.each do |category, facts|
|
|
17
|
+
sections << category_section(category, facts)
|
|
18
|
+
end
|
|
19
|
+
sections << "No facts discovered." if @discovery.facts.empty?
|
|
20
|
+
sections.join("\n\n")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def suggested_profile
|
|
26
|
+
s = @discovery.suggestion
|
|
27
|
+
"Suggested profile: #{s.profile} (#{s.confidence}) — #{s.reason}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def category_section(category, facts)
|
|
31
|
+
lines = facts.map do |fact|
|
|
32
|
+
" [#{fact.confidence}] #{fact.claim} ← #{fact.provenance.explanation}"
|
|
33
|
+
end
|
|
34
|
+
(["#{category.to_s.tr("_", " ")}:"] + lines).join("\n")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Reports
|
|
5
|
+
# Renders configuration drift (specification section 06) so the two kinds of
|
|
6
|
+
# change never blur together: policy changes come first, under a heading
|
|
7
|
+
# that says a human is needed, and benign changes follow as information.
|
|
8
|
+
class Drift
|
|
9
|
+
def initialize(report, record_with: "reanimator drift --record")
|
|
10
|
+
@report = report
|
|
11
|
+
@record_with = record_with
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render
|
|
15
|
+
return quiet unless @report.drifted?
|
|
16
|
+
|
|
17
|
+
([header] + sections + [verdict, next_step]).compact.join("\n\n")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def quiet
|
|
23
|
+
"#{header}\nNo drift — the contract still matches the recorded snapshot."
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def header
|
|
27
|
+
"Configuration drift — #{@report.repository}\nSince: #{@report.since}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def sections
|
|
31
|
+
[
|
|
32
|
+
section("Policy changes (review required)", @report.policy_changes, "!"),
|
|
33
|
+
section("Benign changes", @report.benign_changes, "·")
|
|
34
|
+
].compact
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def section(title, changes, marker)
|
|
38
|
+
return if changes.empty?
|
|
39
|
+
|
|
40
|
+
lines = ["#{title} — #{changes.size}"]
|
|
41
|
+
changes.each do |change|
|
|
42
|
+
lines << " #{marker} #{change.summary}"
|
|
43
|
+
lines << " #{change.explanation}"
|
|
44
|
+
end
|
|
45
|
+
lines.join("\n")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# A maintainer whose build just went red needs to know how to accept
|
|
49
|
+
# drift they meant to introduce; the report is the only place they are
|
|
50
|
+
# looking.
|
|
51
|
+
def next_step
|
|
52
|
+
"Intended? Re-record with: #{@record_with}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def verdict
|
|
56
|
+
return "No policy changes; nothing here needs a decision." unless @report.requires_review?
|
|
57
|
+
|
|
58
|
+
changes = Reports.count(@report.policy_changes.size, "policy change")
|
|
59
|
+
"REVIEW REQUIRED: #{changes} — less is being proven than before."
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Reports
|
|
5
|
+
# Renders a portfolio's compatibility graph (specification section 10):
|
|
6
|
+
# which repositories must stay alive together, what role each plays, and
|
|
7
|
+
# which edge each gate is there to prove.
|
|
8
|
+
class Families
|
|
9
|
+
def initialize(families)
|
|
10
|
+
@families = families
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def render
|
|
14
|
+
return "No repository families detected." if @families.empty?
|
|
15
|
+
|
|
16
|
+
([header] + @families.map { |family| section(family) }).join("\n\n")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def header
|
|
22
|
+
count = @families.size
|
|
23
|
+
"Repository families — #{Reports.count(count, "family", "families")} " \
|
|
24
|
+
"(#{@families.sum(&:size)} repositories that must stay alive together)"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def section(family)
|
|
28
|
+
lines = ["#{family.id}:"]
|
|
29
|
+
family.members.each do |member|
|
|
30
|
+
lines << " - #{member.full_name} (#{member.role.to_s.tr("_", " ")})"
|
|
31
|
+
end
|
|
32
|
+
family.relationships.each do |relationship|
|
|
33
|
+
lines << " edge: #{relationship.label}"
|
|
34
|
+
lines << " #{relationship.description}"
|
|
35
|
+
end
|
|
36
|
+
family.gates.each { |gate| lines << " gate: #{gate.label}" }
|
|
37
|
+
lines.join("\n")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Reports
|
|
5
|
+
# Renders cross-repository gate results. The point of section 10 is that a
|
|
6
|
+
# family failure names the *edge* that broke, so every line leads with the
|
|
7
|
+
# relationship rather than with whichever repository happened to run.
|
|
8
|
+
class FamilyGates
|
|
9
|
+
def initialize(family, results)
|
|
10
|
+
@family = family
|
|
11
|
+
@results = results
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render
|
|
15
|
+
([header] + lines + [footer]).compact.join("\n")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def header = "Family gates — #{@family.id}"
|
|
21
|
+
|
|
22
|
+
def lines
|
|
23
|
+
return [" (no gates declared or inferred)"] if @results.empty?
|
|
24
|
+
|
|
25
|
+
@results.map do |result|
|
|
26
|
+
detail = result.detail ? " — #{result.detail}" : ""
|
|
27
|
+
timing = result.duration ? " (#{result.duration}s)" : ""
|
|
28
|
+
" #{Reports.glyph(result.status)} #{result.edge}#{detail}#{timing}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def footer
|
|
33
|
+
broken = @results.select(&:failed?)
|
|
34
|
+
return "All family edges hold." if broken.empty? && @results.any?(&:verified?)
|
|
35
|
+
return nil if broken.empty?
|
|
36
|
+
|
|
37
|
+
"BROKEN EDGE#{"S" unless broken.size == 1}: #{broken.map(&:edge).join("; ")}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Reports
|
|
5
|
+
# Renders a repository's vitality as the GitHub Check surface from section
|
|
6
|
+
# 15 - the compact, scannable artifact a maintainer sees on a commit. A
|
|
7
|
+
# healthy baseline that changed nothing says so explicitly: healthy silence
|
|
8
|
+
# made visible.
|
|
9
|
+
class GitHubCheck
|
|
10
|
+
def initialize(baseline)
|
|
11
|
+
@baseline = baseline
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render
|
|
15
|
+
lines = []
|
|
16
|
+
lines << "REANIMATOR / VITALITY"
|
|
17
|
+
lines << @baseline.state.to_s.upcase.tr("_", " ")
|
|
18
|
+
lines << "Verified: #{@baseline.generated_at.strftime("%Y-%m-%d %H:%M %Z")}"
|
|
19
|
+
lines << "Contract: #{@baseline.policy_version}"
|
|
20
|
+
lines << "Agent: none required"
|
|
21
|
+
@baseline.results.each do |result|
|
|
22
|
+
lines << "#{Reports.glyph(result.status)} #{result.criterion.claim}"
|
|
23
|
+
end
|
|
24
|
+
lines << checks_summary
|
|
25
|
+
lines.compact.join("\n")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def checks_summary
|
|
31
|
+
if unproven_or_failed.positive?
|
|
32
|
+
"Attention needed: #{unproven_or_failed} required check(s) are not yet proven."
|
|
33
|
+
elsif @baseline.executed?
|
|
34
|
+
"No maintenance change was required."
|
|
35
|
+
else
|
|
36
|
+
"No source changes were made. Evidence is read-only; run with execution to confirm."
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def unproven_or_failed
|
|
41
|
+
@baseline.unresolved_required.size
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Reports
|
|
5
|
+
# Renders a classified portfolio: repositories grouped by suggested cohort,
|
|
6
|
+
# each with the strongest evidence behind the grouping (section 04). It
|
|
7
|
+
# leads with the small recommended set, honoring the rule never to
|
|
8
|
+
# preselect everything.
|
|
9
|
+
class Inventory
|
|
10
|
+
def initialize(portfolio)
|
|
11
|
+
@portfolio = portfolio
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render
|
|
15
|
+
[header, cohort_sections].join("\n\n")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def header
|
|
21
|
+
recommended = @portfolio.recommended.map(&:name)
|
|
22
|
+
[
|
|
23
|
+
"Portfolio inventory — #{Reports.count(@portfolio.size, "repository", "repositories")}",
|
|
24
|
+
"Recommended for onboarding: #{recommended.empty? ? "(none yet)" : recommended.join(", ")}"
|
|
25
|
+
].join("\n")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def cohort_sections
|
|
29
|
+
@portfolio.by_cohort.map do |key, entries|
|
|
30
|
+
cohort = Cohort.fetch(key)
|
|
31
|
+
lines = entries.map { |e| " - #{e.name} — #{e.classification.reason}" }
|
|
32
|
+
(["#{cohort.title} → suggest #{cohort.default_intent}:"] + lines).join("\n")
|
|
33
|
+
end.join("\n\n")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Reports
|
|
5
|
+
# Renders the resolved autonomy for a repository across every task class
|
|
6
|
+
# (specification section 11). It shows how the conservative defaults, the
|
|
7
|
+
# repository's enabled autonomy, and the safety floor combine into what an
|
|
8
|
+
# agent may actually do - and flags where elevated review applies.
|
|
9
|
+
class Permissions
|
|
10
|
+
def initialize(contract)
|
|
11
|
+
@contract = contract
|
|
12
|
+
@model = PermissionModel.new(contract)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render
|
|
16
|
+
header = "Permissions — #{@contract.repository.full_name} " \
|
|
17
|
+
"(autonomy: #{@contract.autonomy.map { |k, v| "#{k}=#{v}" }.join(" ")})"
|
|
18
|
+
rows = TaskClass.keys.map { |key| row(TaskClass.fetch(key)) }
|
|
19
|
+
[header, "", *rows].join("\n")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def row(task_class)
|
|
25
|
+
resolution = @model.resolve(task_class)
|
|
26
|
+
actions = AutonomyLevel.keys.map { |level| "#{level}=#{resolution.actions[level]}" }.join(" ")
|
|
27
|
+
elevated = resolution.elevated_review? ? " [elevated review]" : ""
|
|
28
|
+
" #{task_class.title}: max=#{resolution.max_action} | #{actions}#{elevated}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Reports
|
|
5
|
+
# The tangible artifact onboarding must produce (Appendix A): a portfolio
|
|
6
|
+
# baseline that groups repositories by proof-of-life state and, for the ones
|
|
7
|
+
# with gaps, names exactly what remains unproven. It ends by stating that no
|
|
8
|
+
# source changed and no agent was required - the whole point of the first
|
|
9
|
+
# run.
|
|
10
|
+
class PortfolioBaseline
|
|
11
|
+
# State display order: proven first, then attention, then dormant.
|
|
12
|
+
STATE_ORDER = %i[alive at_risk needs_you unproven recovering diverged sleeping retired].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(baselines, name: "portfolio", generated_at: nil)
|
|
15
|
+
@baselines = baselines
|
|
16
|
+
@name = name
|
|
17
|
+
@generated_at = generated_at || @baselines.map(&:generated_at).max || Time.now
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render
|
|
21
|
+
([banner] + state_groups + [footer]).join("\n\n")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def banner
|
|
27
|
+
[
|
|
28
|
+
"REANIMATOR BASELINE",
|
|
29
|
+
"Portfolio: #{@name}",
|
|
30
|
+
"Generated: #{@generated_at.strftime("%Y-%m-%d %H:%M %Z")}",
|
|
31
|
+
"Mode: read-only",
|
|
32
|
+
"",
|
|
33
|
+
"SELECTED FOR CONTINUOUS CARE",
|
|
34
|
+
Reports.count(@baselines.size, "repository", "repositories")
|
|
35
|
+
].join("\n")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def state_groups
|
|
39
|
+
grouped = @baselines.group_by { |b| b.state.to_sym }
|
|
40
|
+
STATE_ORDER.filter_map do |state|
|
|
41
|
+
entries = grouped[state]
|
|
42
|
+
next unless entries
|
|
43
|
+
|
|
44
|
+
([state.to_s.upcase.tr("_", " ")] + entries.flat_map { |b| repository_lines(b) }).join("\n")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def repository_lines(baseline)
|
|
49
|
+
lines = [" #{baseline.repository.name}"]
|
|
50
|
+
gaps(baseline).each { |gap| lines << " - #{gap}" }
|
|
51
|
+
lines
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# For a repository that is not fully Alive, the required criteria that are
|
|
55
|
+
# not yet verified, with their reason.
|
|
56
|
+
def gaps(baseline)
|
|
57
|
+
return [] if baseline.state.alive?
|
|
58
|
+
|
|
59
|
+
baseline.unresolved_required
|
|
60
|
+
.map { |r| r.detail || "#{r.criterion.id} #{Reports.status_label(r.status)}" }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def footer
|
|
64
|
+
["NO SOURCE CHANGES WERE MADE", "No coding agent was required."].join("\n")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|