DEV Community

YukiKobayashi880
YukiKobayashi880

Posted on

Freight Patch Findings: Three-Class Structured Summarization with JSON Schema in Node.js

Short answer: Use a provider-neutral JSON Schema contract in the Node.js review service, retain three bounded finding classes, and expire verbose model output on a separate audit schedule.

Keep it small.

A logistics code-review service should retain the small structured finding set, not every generated explanation. The least complex design is a provider-neutral JSON Schema contract with three finding classes: correctness, safety, and maintainability. Persist the contract result and a short evidence reference; put the full prompt, response, and source diff behind a time-bounded audit policy.

The bill is usually driven by retained payload volume and repeated review work, not by the schema declaration. A useful first estimate is simple: 10,000 changes per month x 3 findings x 700 bytes of normalized JSON is about 21 MB before indexes and audit metadata. Keeping a 20 KB explanation beside each finding turns that into roughly 600 MB for the same workload. The exact storage bill depends on the provider and retention policy, but the dominant term is visible before choosing either.

The limitation is clear: this design is a poor fit for teams that must retain complete source material for legal discovery or regulated replay. They should choose a longer, access-controlled evidence store and accept the added retention and redaction work.

What should a review result keep?

A finding needs enough evidence to be actionable after the model call is gone. I keep a stable identifier, severity, file and line span, a short message, a confidence value, and an evidence locator. I do not make the prose transcript the system of record. That choice matters in logistics because a patch may touch routing, label generation, or carrier handoff code; an auditor needs to find the relevant change, while a developer needs a bounded list that can be sorted and rechecked.

The retention boundary is a deliberate trade. Keeping normalized findings for 90 days supports trend analysis and re-review. Keeping the original diff and model output for 14 days supports dispute investigation. After that, a digest and a content hash can preserve correlation without retaining every shipment-related detail that appeared in the diff. When an incident crosses the boundary, the missing transcript is a real cost: reconstruction requires the source revision, the prompt version, and whatever deterministic fixtures were available. Retention is therefore an operational control, not a storage cleanup task.

Why does the schema have to be stricter than the prompt?

A prompt can ask for JSON; it cannot by itself enforce that an array contains only review findings, that a line number is an integer, or that an unexpected field is ignored rather than silently trusted. The schema is the boundary between an untrusted generation and a workflow that may open a ticket or block a deployment. OWASP describes output handling and excessive agency as LLM application risks, which is the right mental model here: validation must happen before the result acquires authority.

Here is a provider-neutral validator shape. The application still needs a JSON Schema implementation and a policy decision for every validation error; parsing JSON is not validation.

from jsonschema import Draft202012Validator

FINDINGS_SCHEMA = {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": False,
    "required": ["review_id", "findings"],
    "properties": {
        "review_id": {"type": "string", "minLength": 1},
        "findings": {
            "type": "array",
            "maxItems": 50,
            "items": {
                "type": "object",
                "additionalProperties": False,
                "required": ["class", "severity", "message", "evidence"],
                "properties": {
                    "class": {
                        "enum": ["correctness", "safety", "maintainability"]
                    },
                    "severity": {"enum": ["low", "medium", "high"]},
                    "message": {"type": "string", "maxLength": 500},
                    "evidence": {"type": "string", "maxLength": 300}
                }
            }
        }
    }
}

def validate_review(payload: dict) -> None:
    errors = sorted(
        Draft202012Validator(FINDINGS_SCHEMA).iter_errors(payload),
        key=lambda error: list(error.path),
    )
    if errors:
        raise ValueError("invalid review contract: " + "; ".join(e.message for e in errors))
Enter fullscreen mode Exit fullscreen mode

The three-class limit is intentionally boring. A reviewer can add a new class later through a versioned contract, but an unbounded taxonomy becomes a retention problem and a reporting problem at the same time. A 50-item ceiling also gives the caller a predictable upper bound; it is a guardrail against a malformed response becoming a large stored object.

Can Node.js use JSON Schema for structured summarization?

The runtime can use Node.js to assemble the prompt and call an endpoint, while the storage adapter receives only a validated object. Keep those responsibilities separate: an adapter may translate a schema declaration into a provider's request shape, but it must return the same internal result or an explicit typed error. The persistence layer should never inspect provider-specific response fields.

For a minimal contract test, the model call can be represented by a function returning parsed JSON. The important part is the seam after parsing and before retention.

def review_change(call_model, diff_text: str, schema: dict) -> dict:
    raw = call_model(
        instruction=(
            "Review this logistics patch. Return only an object that matches the supplied schema."
        ),
        input_text=diff_text,
        response_schema=schema,
    )
    payload = raw if isinstance(raw, dict) else None
    if payload is None:
        raise ValueError("model response was not a JSON object")
    validate_review(payload)
    return payload


def retention_record(review: dict, diff_hash: str, schema_version: str) -> dict:
    return {
        "review_id": review["review_id"],
        "findings": review["findings"],
        "diff_hash": diff_hash,
        "schema_version": schema_version,
    }
Enter fullscreen mode Exit fullscreen mode

This example is Python because the contract and validation rules are easier to inspect without hiding them behind an SDK. In a Node.js service, the same boundary should be represented by a JSON Schema validator and a small interface such as callModel(input, responseSchema), with the returned object checked before it reaches a queue or database. The provider call is replaceable; the contract is not.

Do not retry every failure. A timeout or transport interruption can be retried with an idempotency key tied to the review revision. A schema violation should be counted separately, captured with the schema version, and sent to a bounded repair or human-review path. Repeating the same invalid request can multiply cost while producing no better evidence.

What does portability require beyond swapping endpoints?

Portability is often reduced to changing a URL. That misses the expensive parts: structured-output semantics differ, refusal and truncation need distinct handling, token limits differ, and a provider may interpret a schema feature differently. The portable unit is the acceptance test, not the request body.

For each supported adapter, run fixtures containing an empty diff, a three-finding patch, a malformed line reference, sensitive text, and a response that exceeds the item limit. Assert the same outcomes: accepted, rejected, retried, or escalated. Record latency and token counts as operational metrics, but do not let a cheaper response format bypass the contract.

A small decision table keeps the boundary honest:

Decision Retain Discard or expire Reason
Normal review Validated findings, schema version, diff hash Raw response after 14 days Supports triage and correlation
Validation failure Error class, request ID, redacted sample Full payload under the same policy Enables adapter diagnosis
Escalated review Findings plus source revision Duplicate attempts Preserves the human decision path
Trend reporting Aggregates by class and severity File contents and prose Limits sensitive data exposure

A migration then becomes measurable. Shadow a second adapter on a fixed fixture set, compare acceptance and classification outcomes, and switch only after the difference has an owner. The database does not need to know which adapter produced a valid result.

What I would delete first?

I would delete unbounded explanation fields before deleting evidence locators. Explanations are useful during review, but they are the first place where sensitive shipment details, duplicated diff text, and uncontrolled verbosity accumulate. A 500-character message, a bounded evidence string, and a hash provide a workable default.

The deliberate loss is investigative detail after the raw-output window closes. That is acceptable only if the source revision, prompt or policy version, schema version, request identifier, and validation outcome remain queryable. Without those fields, a short retained result is cheap but not auditable. With them, the review record remains useful across endpoint changes and storage migrations.

The decision rule is straightforward: keep what lets an engineer reproduce the classification and explain the action; expire what merely repeats the input in more words. A schema is valuable here because it makes that retention decision enforceable at the storage boundary.

Further reading

Top comments (0)