Class: Ruact::Doctor
- Inherits:
-
Object
- Object
- Ruact::Doctor
- Defined in:
- lib/ruact/doctor.rb
Overview
Runs a suite of installation health checks and prints ✓/✗ per check. Extracted from the ruact:doctor Rake task for direct testability (FR27).
Constant Summary collapse
- CHECKS =
%i[manifest vite controller layout streaming legacy_constant serialize_only flight_middleware].freeze
- LEGACY_CONST =
Built via Array#join so the gem-CI
name-propagationguard does not match these literals against itself (Story 5.1 review F4 — the doctor file participates in the guard with no exclusion). %w[Rails Rsc].join
- LEGACY_GEM =
%w[rails rsc].join("_")
- LEGACY_CONSTANT_RE =
/(?<![A-Za-z_])(?:#{LEGACY_CONST}|#{LEGACY_GEM})(?![A-Za-z_])/- LEGACY_SCAN_GLOBS =
["config/initializers/**/*.rb", "app/**/*.rb"].freeze
- RENAME_DOC_URL =
"https://github.com/luizcg/ruact/blob/main/CHANGELOG.md#renamed"- DESERIALIZE_SIGNALS =
--- Story 13.1: serialize-only invariant tripwire (FR97) ---------------
ruact EMITS React Flight (
text/x-component) but must never DESERIALIZE externally-supplied Flight into live Ruby objects — that keeps it outside the React2Shell / CVE-2025-55182 class (a Flight-deserialization RCE). See the ADR addendum in docs/internal/decisions/server-functions-api.md.The signal literals are assembled from fragments via Array#join so this file does NOT itself contain the matched strings (mirrors the LEGACY_CONST F4 lesson).
doctor.rbis also excluded from the scan as defense in depth. Only structural inbound-deserialization signals are used; the rawtext/x-componenttoken is deliberately NOT a signal because the gem legitimately EMITS that media type (controller.rb / server.rb) — matching it would false-fail the current, invariant-holding tree. [ # a `Deserializer` constant reference (FlightDeserializer, Flight::Deserializer, …); # matched as a substring so both the joined and namespaced forms trip it /#{%w[Deser ializer].join}/, # methods that turn inbound Flight into Ruby objects /\b#{%w[deserialize flight].join('_')}\b/, /\b#{%w[from flight].join('_')}\b/, /\b#{%w[parse flight].join('_')}\b/, /\b#{%w[decode flight].join('_')}\b/, # React Flight reader entry points invoked from Ruby (NOT createFromFlightPayload, # which is the client/browser deserializing the server's own trusted payload) /\b#{%w[create From].join}(?:NodeStream|ReadableStream|Fetch)\b/ ].freeze
- DESERIALIZE_SIGNAL_RE =
Regexp.union(DESERIALIZE_SIGNALS)
- ALLOW_FLIGHT_DESERIALIZATION =
A line carrying this annotation is a deliberate, reviewed deserializer and is treated as guarded (the check is a guard, not a blanket ban).
["# ruact:allow", "flight", "deserialization"].join("-")
- DOCTOR_FILE =
Exclude THIS file by its exact path (not basename) — its comments contain the literal
Deserializerexample, so it must not match its own scan; but a differently-located future file also nameddoctor.rbmust still be scanned (review finding R1 — basename exclusion was too broad). File.(__FILE__)
- SERIALIZE_ONLY_DOC =
"docs/internal/decisions/server-functions-api.md (serialize-only invariant, FR97)"- RESPONSE_TRANSFORMING_MIDDLEWARE =
Response-transforming middleware that can mutate/recompress a streamed
text/x-componentFlight body and break the wire contract (React-on-Rails ops lesson). Matched by class name so the check needs no hard dependency. %w[Rack::Deflater].freeze
- SUCCESS_STATUSES =
Statuses that do NOT fail the run. Anything else (including a malformed / future status) is treated as a failure (review finding R1).
%i[pass warn].freeze
Class Method Summary collapse
-
.run ⇒ Object
Runs all checks, prints results, returns true if none FAIL.
Instance Method Summary collapse
-
#initialize(serialize_only_root: File.join(Ruact.gem_path, "lib")) ⇒ Doctor
constructor
A new instance of Doctor.
- #run ⇒ Object
Constructor Details
Class Method Details
.run ⇒ Object
Runs all checks, prints results, returns true if none FAIL.
75 76 77 |
# File 'lib/ruact/doctor.rb', line 75 def self.run new.run end |
Instance Method Details
#run ⇒ Object
79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/ruact/doctor.rb', line 79 def run puts "[ruact] Health check" results = CHECKS.map { |check| send(:"check_#{check}") } results.each { |status, | puts format_result(status, ) } # A :warn must NOT fail the run (Story 13.1 AC3); only :pass / :warn are # success. An unexpected status (rendered `✗`) fails loudly rather than # being silently treated as a pass (review finding R1). passed = results.all? { |status, _| SUCCESS_STATUSES.include?(status) } puts "Run rails generate ruact:install to fix configuration issues" unless passed passed end |