Skip to content

feat(lint): warn on 204-dropped outputs, with generated-lint and go-oracle exactness riders#525

Merged
HardMax71 merged 4 commits into
mainfrom
fix/hardening-batch
Jul 9, 2026
Merged

feat(lint): warn on 204-dropped outputs, with generated-lint and go-oracle exactness riders#525
HardMax71 merged 4 commits into
mainfrom
fix/hardening-batch

Conversation

@HardMax71

@HardMax71 HardMax71 commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Three hardening items left over from the last arcs, none changing behavior for the current fixture suite.

L07: declared outputs dropped by a 204

The RemoveLineItem seam in #524 showed the failure class: an operation declaring outputs while its success status resolves to 204 No Content loses the payload, fatally on python (FastAPI refuses to register a 204 route with a response body) and silently on go and ts. The new lint pass resolves the effective status through the same convention functions the endpoint derivation uses (classification, method override, resolveStatus), warns naming the dropped outputs, and points at the http_status_success override. A lone Bool output stays exempt, since a success flag feeding 204-versus-404 semantics is the designed pattern. The lint module gains a dependency on convention for the classification, asserted in ArchitectureTest; all four fixture specs stay silent under the rule (RemoveLineItem now overrides to 200), and the docs' lint tables grow the L07 row.

Generated python stays ruff-clean

Ecommerce's scale pushed two generated lines past the 100-character limit: the admin reset endpoint's three-counter values(...) call and the seven-variant enum Literal alias, the only ruff findings in any generated app. Both emitters now wrap when the single-line form exceeds the limit and stay single-line otherwise, so the other specs' generated output is byte-identical. Verified by before/after generation diffs across all four specs, ruff and mypy strict on the results, and the ecommerce smoke suite (124 passing) against the wrapped app.

Go conformance oracle compares integers exactly

The deferred follow-up from #518's review: the generated go test client decoded JSON through float64, so integer comparisons lost exactness above 2^53 and adjacent ids could conflate. The client now decodes with UseNumber, equality and ordering take an exact int64 path when both sides are integral, and map-key lookups and set hashing canonicalize integral values through one string form so json.Number, int64, and integral float64 agree; fractional values keep the float64 fallback and the arithmetic helpers stay float64 by design. A standalone probe shows the old path conflating 9007199254740993 with its neighbor and the new helpers distinguishing them in equality, ordering, string form, and hashes. All four go conformance suites pass unchanged.

Validation

Each slice was validated in isolation (lint 40/40 with the end-to-end CLI warning surfaced on a trap spec, python before/after diffs plus conformance, go suites on all four specs plus the exactness probe), and the assembled branch passes the full sbt suite (12 modules, zero failures).


Summary by cubic

Adds L07 lint to catch operations that resolve to 204 No Content and would drop outputs. Also keeps generated Python ruff-clean and makes Go conformance tests compare integers exactly to avoid precision loss.

  • New Features

    • L07 warns when an operation with outputs resolves to 204; resolves status via conventions; single Bool output is exempt. Suggests setting .http_status_success = 200.
  • Bug Fixes

    • Python codegen: wrap long counter values(...) and wide enum Literal[...] aliases when lines exceed 100 chars; other specs remain byte-identical.
    • Go conformance runtime: decode JSON with UseNumber, compare integers exactly as int64, and canonicalize numeric keys/hashes; fractional numbers still use float64.
    • Docs: list L01–L07 in lint tables and fix the L07 table row formatting.

Written for commit 1216daf. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added warning L07 for operations that resolve to 204 No Content while declaring outputs that can never be returned.
  • Bug Fixes
    • Improved numeric handling in generated test/runtime helpers for more consistent decoding, comparisons, hashing, and ordering.
  • Documentation
    • Updated docs and verification/structural lint coverage to include the new L07 check.
  • Improvements
    • Enhanced generated code formatting (Python enum aliases and SQLAlchemy seed resets) to better respect line-length limits.

HardMax71 added 3 commits July 9, 2026 02:07
The generated go test client decoded JSON through float64, so integer
comparisons lost exactness above 2 to the 53rd and adjacent ids could
conflate. The client now decodes with UseNumber, equality and ordering
take an exact int64 path when both sides are integral, and map-key
lookups and set hashing canonicalize integral values through one string
form so json.Number, int64, and integral float64 agree. Fractional
values keep the float64 fallback, and arithmetic helpers stay float64 by
design.
The admin reset endpoint's counter update and the enum Literal alias grew
past 100 characters on ecommerce (three counters, seven variants), the
only two ruff findings in any generated app. Both sites now wrap when the
single-line form exceeds the limit and stay single-line otherwise, so the
other specs' output is byte-identical.
An operation whose outputs go beyond a single Bool flag while its success
status resolves to 204 No Content loses its payload: FastAPI refuses to
register the route and the go and ts targets silently drop the body,
which is exactly how ecommerce RemoveLineItem failed before its override.
L07 resolves the effective status through the same convention functions
the endpoint derivation uses and points at the http_status_success fix.
A lone Bool output stays exempt as the designed 204-versus-404 selector,
and the lint module now depends on convention for the classification.
Copilot AI review requested due to automatic review settings July 9, 2026 00:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new L07 lint for outputs dropped by 204 responses, wires it into lint execution and docs, adds line-length-aware wrapping for generated Python output, and updates Go test-runtime numeric handling to preserve integer precision and canonical comparisons.

Changes

DroppedOutputs (L07) Lint

Layer / File(s) Summary
DroppedOutputs lint pass
modules/lint/src/main/scala/specrest/lint/DroppedOutputs.scala
Defines lint code L07, checks operation outputs against resolved 204 success status, and exempts the single-Bool output pattern.
Lint wiring
modules/lint/src/main/scala/specrest/lint/Lint.scala, modules/arch/src/test/scala/specrest/arch/ArchitectureTest.scala, build.sbt
Adds DroppedOutputs to lint execution, allows lint access to the convention layer, and adds the lint module dependency on convention.
DroppedOutputs tests
modules/lint/src/test/scala/specrest/lint/DroppedOutputsTest.scala
Adds test coverage for emitted diagnostics, suppressed cases, fixture behavior, and diagnostic contents for L07.
L07 documentation updates
docs/content/docs/design/architecture.mdx, docs/content/docs/roadmap.mdx, docs/content/docs/spec-language.mdx, docs/content/docs/research/spec_language_design/developer-experience.md
Updates lint counts and documents the new L07 warning and its status-resolution rules.

Python codegen line-length wrapping

Layer / File(s) Summary
Conditional multiline formatting
modules/codegen/src/main/scala/specrest/codegen/python/EmitPython.scala, modules/codegen/src/main/scala/specrest/codegen/python/AdminRouter.scala, modules/codegen/src/test/scala/specrest/codegen/EmitTest.scala, modules/testgen/src/test/scala/specrest/testgen/AdminRouterTest.scala
Formats long enum Literal aliases and ServiceState reset statements across multiple lines when the single-line form would exceed the threshold, with tests asserting wrapped output.

Go test runtime numeric handling

Layer / File(s) Summary
JSON decode with UseNumber
modules/testgen/src/main/resources/testgen-templates/go/chi/tests/conf_client.go
clientResponse.JSON() now decodes through json.Decoder with UseNumber instead of json.Unmarshal.
Numeric coercion and comparison helpers
modules/testgen/src/main/resources/testgen-templates/go/chi/tests/conf_runtime.go
Updates runtime helpers to handle json.Number, add exact integer coercion, revise equality and comparison ordering, and hash canonical string forms.

Estimated code review effort: 3 (Moderate) | ~30 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main lint addition and also mentions the codegen and Go exactness hardening work.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/hardening-batch

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
modules/codegen/src/main/scala/specrest/codegen/python/EmitPython.scala (1)

476-483: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Wrapped Literal[...] alias can still exceed the 100-char limit for larger enums.

Unlike AdminRouter's scalar-reset wrapping (which places each seed on its own line), this only does a single-level wrap: all values land on one indented line (" $values"). For an enum with many members or long member names, that line itself can exceed 100 chars, silently defeating the stated Ruff-compliance goal for enums not covered by current fixtures.

♻️ Suggested per-value wrapping fallback
   private def enumAliasDef(ir: ServiceIRFull, typeName: String): Option[String] =
     svcEnums(ir)
       .find(e => enumNameFull(e) == typeName)
       .map: e =>
-        val values = enumValuesFull(e).map(v => s"\"$v\"").mkString(", ")
-        val single = s"${enumAliasName(typeName)} = Literal[$values]"
-        if single.length <= 100 then single
-        else s"${enumAliasName(typeName)} = Literal[\n    $values\n]"
+        val quoted = enumValuesFull(e).map(v => s"\"$v\"")
+        val values = quoted.mkString(", ")
+        val single = s"${enumAliasName(typeName)} = Literal[$values]"
+        val oneWrap = s"${enumAliasName(typeName)} = Literal[\n    $values\n]"
+        if single.length <= 100 then single
+        else if oneWrap.linesIterator.forall(_.length <= 100) then oneWrap
+        else
+          (s"${enumAliasName(typeName)} = Literal[" ::
+            quoted.map(v => s"    $v,") :::
+            List("]")).mkString("\n")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/codegen/src/main/scala/specrest/codegen/python/EmitPython.scala`
around lines 476 - 483, The wrapped enum alias in enumAliasDef can still violate
the 100-character limit because all enum values are emitted on one indented
line. Update enumAliasDef in EmitPython so that when the single-line Literal
assignment is too long, it falls back to per-value wrapping across multiple
lines rather than a single joined values line. Use the existing
enumAliasName/typeName logic and keep the Literal[...] output Ruff-compliant for
larger enums.
modules/lint/src/main/scala/specrest/lint/DroppedOutputs.scala (1)

20-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider reusing Path's resolution logic instead of duplicating it.

Lines 20-28 inline the same getConventionresolveMethod/resolveStatus sequence that Path already encapsulates in its private resolveMethod and resolveStatus methods. If the resolution logic in Path changes, this lint won't automatically follow, creating a risk of divergence between the lint and actual endpoint derivation.

Exposing a single public method on Path (e.g., successStatus) would eliminate the duplication and keep the comparison type-consistent (Int vs String).

♻️ Proposed refactor

In Path.scala (add a public wrapper around the existing private methods):

def successStatus(c: operation_classification, conv: Option[conventions_decl]): Int =
  val method = resolveMethod(c, conv)
  resolveStatus(c, conv, method)

Then in DroppedOutputs.scala:

       val opName         = classificationOperationName(classification)
-      val method = resolveMethod(
-        Path.getConvention(conv, opName, "http_method").flatMap(parseHttpMethod),
-        classificationMethod(classification)
-      )
-      val status = resolveStatus(
-        Path.getConvention(conv, opName, "http_status_success"),
-        method,
-        classificationKind(classification)
-      )
-      if status == "204" then
+      val status = Path.successStatus(classification, conv)
+      if status == 204 then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/lint/src/main/scala/specrest/lint/DroppedOutputs.scala` around lines
20 - 28, Refactor DroppedOutputs to stop duplicating Path’s endpoint resolution
logic by calling a single public helper on Path instead of inlining
getConvention, parseHttpMethod, resolveMethod, and resolveStatus. Add a public
wrapper on Path (for example, successStatus) that delegates to the existing
private resolveMethod and resolveStatus methods, then update DroppedOutputs to
use that helper so the lint stays aligned with Path’s behavior and keeps the
comparison type-consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/content/docs/research/spec_language_design/developer-experience.md`:
- Line 22: Clarify the L07 lint rule wording in the spec table to match the
simpler subject-predicate style used by the other rows. Update the description
in the developer experience docs so the entry for the rule currently described
as “an operation whose outputs a resolved `204 No Content` success status would
drop” reads more directly and scannably, while preserving the same meaning and
keeping the L07 row aligned with the surrounding lint descriptions.

---

Nitpick comments:
In `@modules/codegen/src/main/scala/specrest/codegen/python/EmitPython.scala`:
- Around line 476-483: The wrapped enum alias in enumAliasDef can still violate
the 100-character limit because all enum values are emitted on one indented
line. Update enumAliasDef in EmitPython so that when the single-line Literal
assignment is too long, it falls back to per-value wrapping across multiple
lines rather than a single joined values line. Use the existing
enumAliasName/typeName logic and keep the Literal[...] output Ruff-compliant for
larger enums.

In `@modules/lint/src/main/scala/specrest/lint/DroppedOutputs.scala`:
- Around line 20-28: Refactor DroppedOutputs to stop duplicating Path’s endpoint
resolution logic by calling a single public helper on Path instead of inlining
getConvention, parseHttpMethod, resolveMethod, and resolveStatus. Add a public
wrapper on Path (for example, successStatus) that delegates to the existing
private resolveMethod and resolveStatus methods, then update DroppedOutputs to
use that helper so the lint stays aligned with Path’s behavior and keeps the
comparison type-consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e7960822-7c9d-499a-bd48-311c7f78133d

📥 Commits

Reviewing files that changed from the base of the PR and between 41a7d45 and 12bade1.

📒 Files selected for processing (15)
  • build.sbt
  • docs/content/docs/design/architecture.mdx
  • docs/content/docs/research/spec_language_design/developer-experience.md
  • docs/content/docs/roadmap.mdx
  • docs/content/docs/spec-language.mdx
  • modules/arch/src/test/scala/specrest/arch/ArchitectureTest.scala
  • modules/codegen/src/main/scala/specrest/codegen/python/AdminRouter.scala
  • modules/codegen/src/main/scala/specrest/codegen/python/EmitPython.scala
  • modules/codegen/src/test/scala/specrest/codegen/EmitTest.scala
  • modules/lint/src/main/scala/specrest/lint/DroppedOutputs.scala
  • modules/lint/src/main/scala/specrest/lint/Lint.scala
  • modules/lint/src/test/scala/specrest/lint/DroppedOutputsTest.scala
  • modules/testgen/src/main/resources/testgen-templates/go/chi/tests/conf_client.go
  • modules/testgen/src/main/resources/testgen-templates/go/chi/tests/conf_runtime.go
  • modules/testgen/src/test/scala/specrest/testgen/AdminRouterTest.scala

Comment thread docs/content/docs/research/spec_language_design/developer-experience.md Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 15 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread docs/content/docs/research/spec_language_design/developer-experience.md Outdated
@HardMax71 HardMax71 merged commit be12ff4 into main Jul 9, 2026
73 of 74 checks passed
@HardMax71 HardMax71 deleted the fix/hardening-batch branch July 9, 2026 10:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants