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 =
rubocop:disable Metrics/ClassLength
%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
- SCHEMA_VERSION =
Story 15.3 (FR107) — version of the machine-readable
ruact:doctor --jsondocument (see #as_json). This is an EXPERIMENTAL / UNSTABLE contract:0signals the shape may change without a major version bump while the agent-facing introspection surface is iterated. Gate any parser on it. Distinct from ServerFunctions::Snapshot::VERSION_V2 (the internal codegen bridge version) — do not conflate the two. 0
Class Method Summary collapse
-
.run ⇒ Object
Runs all checks, prints results, returns true if none FAIL.
Instance Method Summary collapse
-
#as_json ⇒ Hash
Story 15.3 (FR107) — the machine-readable
ruact:doctor --jsondocument. -
#initialize(serialize_only_root: File.join(Ruact.gem_path, "lib")) ⇒ Doctor
constructor
A new instance of Doctor.
-
#passed?(computed = results) ⇒ Boolean
True when NO check failed — only
:pass/:warnare success (mirrors #run's rule; an unexpected status fails). -
#results ⇒ Array<Array>
Runs every check ONCE and returns the raw result tuples, index-aligned with CHECKS.
- #run ⇒ Object
Constructor Details
Class Method Details
.run ⇒ Object
Runs all checks, prints results, returns true if none FAIL.
83 84 85 |
# File 'lib/ruact/doctor.rb', line 83 def self.run new.run end |
Instance Method Details
#as_json ⇒ Hash
Story 15.3 (FR107) — the machine-readable ruact:doctor --json document.
Reuses the SAME #results tuples the human path prints (no double-run), so
the JSON never disagrees with the ✓/⚠/✗ output. EXPERIMENTAL shape — gate
parsers on schema_version (see SCHEMA_VERSION). Emits NO prose — the
rake task prints ONLY this document in JSON mode.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/ruact/doctor.rb', line 127 def as_json computed = results { "schema_version" => SCHEMA_VERSION, "status" => passed?(computed) ? "pass" : "fail", "checks" => CHECKS.each_index.map do |i| status, , remediation = computed[i] { "name" => CHECKS[i].to_s, "status" => status.to_s, "message" => , "remediation" => remediation } end } end |
#passed?(computed = results) ⇒ Boolean
Returns true when NO check failed — only :pass/:warn are
success (mirrors #run's rule; an unexpected status fails).
114 115 116 |
# File 'lib/ruact/doctor.rb', line 114 def passed?(computed = results) computed.all? { |status, _| SUCCESS_STATUSES.include?(status) } end |
#results ⇒ Array<Array>
Runs every check ONCE and returns the raw result tuples, index-aligned with
CHECKS. Each tuple is [status, message] or [status, message, remediation] (the optional 3rd element is a machine-readable fix string,
nil when the check has no cleanly separable remediation). Shared by #run
(human path) and #as_json (JSON path) so a check like check_vite — which
opens a socket — never runs twice. (Story 15.3)
107 108 109 |
# File 'lib/ruact/doctor.rb', line 107 def results CHECKS.map { |check| send(:"check_#{check}") } end |
#run ⇒ Object
87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/ruact/doctor.rb', line 87 def run puts "[ruact] Health check" computed = results computed.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 = passed?(computed) puts "Run rails generate ruact:install to fix configuration issues" unless passed passed end |