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.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +207 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +483 -0
  5. data/SECURITY.md +32 -0
  6. data/exe/reanimator +6 -0
  7. data/lib/reanimator/acceptance_criterion.rb +75 -0
  8. data/lib/reanimator/agent_connection.rb +62 -0
  9. data/lib/reanimator/agent_profile.rb +51 -0
  10. data/lib/reanimator/agents/adapter.rb +49 -0
  11. data/lib/reanimator/agents/capability.rb +29 -0
  12. data/lib/reanimator/agents/constraints.rb +57 -0
  13. data/lib/reanimator/agents/hosted.rb +38 -0
  14. data/lib/reanimator/agents/local.rb +46 -0
  15. data/lib/reanimator/agents/patch.rb +60 -0
  16. data/lib/reanimator/agents/repair_task.rb +51 -0
  17. data/lib/reanimator/agents/result.rb +42 -0
  18. data/lib/reanimator/agents/strategies/arithmetic_fix.rb +51 -0
  19. data/lib/reanimator/agents/strategies/base.rb +26 -0
  20. data/lib/reanimator/agents.rb +38 -0
  21. data/lib/reanimator/autonomy_level.rb +41 -0
  22. data/lib/reanimator/baseline.rb +106 -0
  23. data/lib/reanimator/baseline_runner.rb +81 -0
  24. data/lib/reanimator/bounded_command.rb +87 -0
  25. data/lib/reanimator/classifier.rb +89 -0
  26. data/lib/reanimator/cli.rb +273 -0
  27. data/lib/reanimator/cohort.rb +61 -0
  28. data/lib/reanimator/commissioning/fixture.rb +69 -0
  29. data/lib/reanimator/commissioning/report.rb +45 -0
  30. data/lib/reanimator/commissioning/test.rb +145 -0
  31. data/lib/reanimator/confidence.rb +66 -0
  32. data/lib/reanimator/configuration.rb +35 -0
  33. data/lib/reanimator/contract_file.rb +116 -0
  34. data/lib/reanimator/contract_snapshot.rb +188 -0
  35. data/lib/reanimator/criteria_stack.rb +91 -0
  36. data/lib/reanimator/criterion_result.rb +70 -0
  37. data/lib/reanimator/detectors/base.rb +49 -0
  38. data/lib/reanimator/detectors/bundler.rb +68 -0
  39. data/lib/reanimator/detectors/commands.rb +112 -0
  40. data/lib/reanimator/detectors/fork.rb +25 -0
  41. data/lib/reanimator/detectors/github_actions.rb +68 -0
  42. data/lib/reanimator/detectors/operational_proof.rb +48 -0
  43. data/lib/reanimator/detectors/rails_app.rb +51 -0
  44. data/lib/reanimator/detectors/relationships.rb +46 -0
  45. data/lib/reanimator/detectors/ruby_gem.rb +40 -0
  46. data/lib/reanimator/detectors/security_controls.rb +36 -0
  47. data/lib/reanimator/discovered_fact.rb +99 -0
  48. data/lib/reanimator/discovery.rb +48 -0
  49. data/lib/reanimator/discovery_result.rb +89 -0
  50. data/lib/reanimator/drift_detector.rb +227 -0
  51. data/lib/reanimator/drift_report.rb +86 -0
  52. data/lib/reanimator/effective_contract.rb +108 -0
  53. data/lib/reanimator/effective_policy_resolver.rb +116 -0
  54. data/lib/reanimator/errors.rb +21 -0
  55. data/lib/reanimator/evaluators/base.rb +55 -0
  56. data/lib/reanimator/evaluators/command.rb +34 -0
  57. data/lib/reanimator/evaluators/documentation_check.rb +25 -0
  58. data/lib/reanimator/evaluators/http_smoke.rb +19 -0
  59. data/lib/reanimator/evaluators/repository_integrity.rb +22 -0
  60. data/lib/reanimator/evaluators/security_evidence.rb +24 -0
  61. data/lib/reanimator/evaluators/support_matrix.rb +25 -0
  62. data/lib/reanimator/evaluators.rb +41 -0
  63. data/lib/reanimator/family/gate.rb +40 -0
  64. data/lib/reanimator/family/gate_result.rb +46 -0
  65. data/lib/reanimator/family/gate_runner.rb +91 -0
  66. data/lib/reanimator/family.rb +89 -0
  67. data/lib/reanimator/family_inference.rb +113 -0
  68. data/lib/reanimator/git.rb +72 -0
  69. data/lib/reanimator/isolation.rb +36 -0
  70. data/lib/reanimator/language_inference.rb +43 -0
  71. data/lib/reanimator/lifecycle_intent.rb +93 -0
  72. data/lib/reanimator/obligation.rb +35 -0
  73. data/lib/reanimator/permission_model.rb +61 -0
  74. data/lib/reanimator/portfolio.rb +68 -0
  75. data/lib/reanimator/portfolio_drift.rb +120 -0
  76. data/lib/reanimator/profile_inference.rb +37 -0
  77. data/lib/reanimator/profiles.rb +110 -0
  78. data/lib/reanimator/proof_of_life.rb +45 -0
  79. data/lib/reanimator/proposed_change.rb +55 -0
  80. data/lib/reanimator/provenance.rb +39 -0
  81. data/lib/reanimator/relationship.rb +62 -0
  82. data/lib/reanimator/repair_run.rb +146 -0
  83. data/lib/reanimator/reports/agents.rb +28 -0
  84. data/lib/reanimator/reports/baseline.rb +73 -0
  85. data/lib/reanimator/reports/commissioning.rb +48 -0
  86. data/lib/reanimator/reports/contract.rb +70 -0
  87. data/lib/reanimator/reports/discovery.rb +38 -0
  88. data/lib/reanimator/reports/drift.rb +63 -0
  89. data/lib/reanimator/reports/families.rb +41 -0
  90. data/lib/reanimator/reports/family_gates.rb +41 -0
  91. data/lib/reanimator/reports/github_check.rb +45 -0
  92. data/lib/reanimator/reports/inventory.rb +37 -0
  93. data/lib/reanimator/reports/permissions.rb +32 -0
  94. data/lib/reanimator/reports/portfolio_baseline.rb +68 -0
  95. data/lib/reanimator/reports/portfolio_drift.rb +107 -0
  96. data/lib/reanimator/reports/run.rb +52 -0
  97. data/lib/reanimator/reports/snapshot.rb +58 -0
  98. data/lib/reanimator/reports.rb +36 -0
  99. data/lib/reanimator/repository.rb +96 -0
  100. data/lib/reanimator/repository_metadata.rb +78 -0
  101. data/lib/reanimator/repository_state.rb +48 -0
  102. data/lib/reanimator/routing/decision.rb +35 -0
  103. data/lib/reanimator/routing/router.rb +122 -0
  104. data/lib/reanimator/run.rb +48 -0
  105. data/lib/reanimator/safety_floor.rb +81 -0
  106. data/lib/reanimator/security_control.rb +87 -0
  107. data/lib/reanimator/security_posture.rb +80 -0
  108. data/lib/reanimator/snapshot_store.rb +78 -0
  109. data/lib/reanimator/task_class.rb +63 -0
  110. data/lib/reanimator/version.rb +5 -0
  111. data/lib/reanimator/vitality_profile.rb +44 -0
  112. data/lib/reanimator/waiver.rb +74 -0
  113. data/lib/reanimator.rb +214 -0
  114. 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