Module: SpecGuard::RSpec::IngestReporter

Defined in:
lib/specguard/rspec/ingest_reporter.rb

Overview

specguard-ingest --json: the machine-readable renderer over the same per-line facts the human report is built from.

Why a second renderer, on the one command that most needed it

An HTTP 400 is the only permanent verdict in this command's contract (SpecGuard::RSpec::IngestCLI::CONTENT_REFUSAL_CODES) — a refused line is refused every time it is offered — so the only way to land the run is to learn which specs the platform objected to and fix the payload. The platform sends one error per offending spec, each naming it by index, file and line (Ingest::Payload#label, and Api::BaseController#render_bad_request puts every one of them on the wire). Transport::Result keeps the whole array, and then Transport::Result#reason renders three of them, truncated to 300 characters, because it is built for the one stderr line an in-run CI warning is allowed.

That cap is right where it was set and is not touched here. It is a cap on a line, and this document is not a line: specguard-ingest already prints a row per line, a summary and its folding observations, out of band and nowhere near a CI log. So the refusal's own grounds do not reach a second channel, and this is that channel — the command whose entire job is to fix and re-send a refused run can now show you all of why it was refused.

This is SPGD-305 done again for the gem's other executable, and it is a RENDERER: it reads the SpecGuard::RSpec::IngestCLI::LineResults the delivery already produced and the SpecGuard::RSpec::IngestCLI::ListedLines the listing already extracted, and decides nothing. The exit code, the statuses, the selection and the cap are all upstream of here and identical on both paths.

Why it is NOT json_reporter.rb

JSONReporter is the lint document over Linter::Result, and it mirrors validate-intent --json --source key for key on purpose: the gem consumes that document, so the two must not need two parsers. This one is about deliveries, not annotations. It therefore does not claim that document's "schema" => "open-test-intent.v1.json" or its "mode" => "source" — naming a schema this document has nothing to do with would assert a conformance it cannot have. "tool" says what wrote it instead.

There is no ok and no exit_code in the document

Deliberately, and this is the one place the two renderers differ in kind rather than in form. JSONReporter carries an ok because a linter's verdict is a boolean. This command's is not: 0, 1 and 2 mean "accepted", "the platform refused content" and "this tool could not do its job", and collapsing that to a boolean would have to pick which of 1 and 2 counts as false — the exact confusion IngestCLI's class comment is arranged against. Restating the integer here would be a second copy of a fact the process already exits with, free to drift from it. So the exit status stays the one carrier of the verdict, unchanged by this flag, and the document carries the facts it is computed from.

Which runs emit a document

By cause, not by exit code. JSONReporter's rule — a run that checked nothing must not emit {"findings": []}, because that is how a gate that checked nothing gets mistaken for one that found nothing — transfers, but it does not transfer as "no exit-2 run emits a document": this command reaches 2 with a real report on a file whose lines were delivered and one of them never arrived.

So the line is drawn where the file was: a run that got as far as reading emits a document, whatever its exit code and even when the file held nothing to deliver ("lines": [] next to a summary of zeroes is a true statement about an empty file, and the warning naming why it was empty is on stderr either way). A run that never got that far — a bad flag, --from-line with --lines, no endpoint or API key, a file that cannot be read — emits prose on stderr and nothing at all on stdout, because there is nothing yet to be a document about.

The counts are handed in, not recomputed

summary's status counts are the ones IngestCLI computes once for whichever renderer runs, for JSONReporter's reason: two renderers of one result list that can disagree about how much of a file was delivered are worse than prose alone, because the disagreement is unfalsifiable from outside the process. That holds on the listing path too, where the only count that can be positive is unparseable and the text summary states no counterpart to disagree with — it is handed in anyway, so the discipline is structural at both entry points rather than true of one by luck. The folding observations are the same shape of thing — one grouping, rendered here as data and there as a sentence.

Constant Summary collapse

TOOL =

What wrote the document. Not a schema id: see the class comment.

"specguard-ingest"
MODE_DELIVER =

Whether lines were sent or only shown. The distinction a consumer most needs, because it decides whether the status counts below are verdicts or zeroes — see #summary's attempted.

"deliver"
MODE_LIST =
"list"
STATUS_LISTED =

A listed line that is a run. Its delivery-side counterparts are SpecGuard::RSpec::IngestCLI::STATUS_LABELS' keys, rendered by name (undelivered, not the prose renderer's not delivered) so a consumer branches on the tool's vocabulary rather than on its wording.

"listed"
STATUS_UNPARSEABLE =
"unparseable"

Class Method Summary collapse

Class Method Details

.render_delivery(source:, results:, counts:, foldings:) ⇒ String

Returns one JSON document, without a trailing newline.

Parameters:

Returns:

  • (String)

    one JSON document, without a trailing newline



116
117
118
119
120
121
122
123
124
# File 'lib/specguard/rspec/ingest_reporter.rb', line 116

def self.render_delivery(source:, results:, counts:, foldings:)
  document(
    mode: MODE_DELIVER,
    source: source,
    lines: results.map { |result| delivered(result) },
    summary: summary(source, lines: results.length, counts: counts, attempted: attempted(counts)),
    foldings: foldings.map { |folding| folded(folding) }
  )
end

.render_listing(source:, lines:, counts:) ⇒ String

Returns one JSON document, without a trailing newline.

Parameters:

Returns:

  • (String)

    one JSON document, without a trailing newline



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/specguard/rspec/ingest_reporter.rb', line 133

def self.render_listing(source:, lines:, counts:)
  # Every delivery status is 0 and `attempted` is 0, which is this
  # document's way of saying what the text listing says in words:
  # nothing was delivered. `unparseable` is the one that can be positive,
  # because a line that is not a run is knowable without sending it — and
  # it is stated for the reason the text row states it, so a preview
  # cannot under-report what the delivery would do.
  document(
    mode: MODE_LIST,
    source: source,
    lines: lines.map { |line| listed(line) },
    summary: summary(source, lines: lines.length, attempted: 0, counts: counts),
    foldings: []
  )
end