DEV Community

ZebedeeHolloway9023
ZebedeeHolloway9023

Posted on

Node.js Moderation Governance: Use JSON Schema for Structured Summarization Contracts

TL;DR: Treat a JSON Schema for moderation summarization as a versioned governance contract, not as a formatting hint. The Node.js worker should send the report as data, validate the returned object against the exact contract, and record the schema version, input digest, and review transition before anything reaches a human queue.

The decisive boundary is migration. A summary that is valid under moderation-summary.v1 is not automatically valid evidence for v2, and a valid object is still only a recommendation until local policy accepts it. In a support operation, this distinction prevents a schema edit from silently becoming a moderation-policy edit.

Version first.

Then validate.

What does the contract govern?

Start with the decision record, not the endpoint. The target JSON Schema follows the 2020-12 specification, and the contract governs shape, vocabulary, provenance, and the permitted workflow transition. It does not govern the model's private reasoning, and it does not turn a generated classification into an authoritative finding.

Keep it boring.

For each report, I would make these invariants explicit:

Invariant Failure boundary Governance response
One source report has one logical decision record Queue redelivery or worker retry Derive an idempotency key from the source and enforce uniqueness in storage
Every accepted field belongs to the published schema Missing property, unknown property, or wrong enum Reject the candidate and retain a controlled audit reference
Evidence remains linked to the source Summary is plausible but cannot identify supporting text Hold the item for review; do not infer evidence after generation
A schema change is distinguishable from a policy change Enum or required-field edit Publish a new version and run fixtures before switching writers
Human status changes remain reconstructable Retry overwrites an earlier event Append events and derive current queue state from accepted events

This is the same exactly-once mindset used in ledger work: network delivery is at-least-once, while the business effect is made idempotent. The database uniqueness constraint matters because a read followed by a write is not atomic when two workers race. Auditability matters because a later reviewer must be able to answer which contract produced the item, not merely inspect the latest summary.

How can Node.js use JSON Schema for structured summarization?

Keep the adapter small. The transport layer sends a stable request envelope; the validation layer parses JSON and applies the selected JSON Schema; the repository owns the state transition. A JSON-shaped prompt alone cannot guarantee this boundary. The application still needs to reject unknown properties, check evidence linkage, and apply its own escalation policy. An OpenAI GPT endpoint is one possible transport for this structured request, but the same validation boundary must hold for any endpoint.

The following Go example shows the critical path without tying it to a commercial endpoint. A Node.js implementation should preserve the same ordering and transaction boundary.

package moderation

import (
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
)

type Request struct {
    ReportID       string `json:"report_id"`
    SchemaVersion  string `json:"schema_version"`
    Text           string `json:"text"`
    IdempotencyKey string `json:"idempotency_key"`
}

func NewRequest(reportID, text string) (Request, error) {
    if reportID == "" || text == "" {
        return Request{}, fmt.Errorf("report id and text are required")
    }
    digest := sha256.Sum256([]byte(reportID + "\x00" + text))
    return Request{
        ReportID: reportID,
        SchemaVersion: "moderation-summary.v1",
        Text: text,
        IdempotencyKey: hex.EncodeToString(digest[:]),
    }, nil
}

func Encode(r Request) ([]byte, error) { return json.Marshal(r) }
Enter fullscreen mode Exit fullscreen mode

The digest is created before generation, so a retry does not depend on generated wording. The input is also clearly data. A report may contain quoted text that looks like an instruction; the worker must not let that text redefine the output contract. OWASP lists prompt injection and insecure output handling among the risks that need application-level controls, which is directly relevant to a moderation queue.

The Node.js service should treat a timeout as an unknown execution result, not as a negative classification. It can retry using the same idempotency key, but it should not create a second review item merely because the first request had no response. The provider-facing request and the local decision write are separate failure domains; no endpoint can make that distributed sequence a single transaction.

The trade-off is deliberate: schema-constrained structured output reduces parsing ambiguity, but it adds migration work and does not prove that a summary is substantively correct. For a disposable internal report, that governance overhead may be excessive. A human-review queue has the opposite constraint.

Which schema changes require a new version?

The practical test is semantic compatibility. Adding an optional descriptive property may be readable by old consumers, while changing an enum, tightening a string constraint, or making a property required can alter workflow behavior. The exact answer depends on the consumers, so the compatibility decision belongs in the architecture record and the schema repository, not in an unreviewed prompt edit.

Change Same version? Reason
Add an optional field ignored by old consumers Usually possible Existing valid records retain their meaning
Rename severity to risk_level No Readers cannot infer the old field's semantics safely
Add an allowed classification value Usually no Queue rules may not define handling for the new value
Make evidence required No Previously accepted records may no longer satisfy the contract
Change only documentation wording Only after review Documentation can still alter a human's interpretation

Run both old and new fixtures during migration. Read historical records with their stored version; write new records with the selected version. A dual-read period is useful when consumers migrate at different times, but it should have an owner and an expiry condition. Otherwise compatibility code becomes a permanent second policy.

The migration review has 3 separate questions: can the validator read the record, does the vocabulary still map to a current moderation rule, and can a reviewer reconstruct the source and contract version? Passing the first question says little about the other two. A field can remain syntactically valid while its meaning changes, so required-property and enum edits need fixture evidence and policy-owner sign-off. That review is slower than editing a prompt, but it prevents old queue items from becoming uninterpretable after deployment.

I initially treated the schema as an output convenience. I now keep three artifacts together: the schema, its fixture set, and the policy note that explains each workflow-sensitive field. The difficult part is that it is also a release artifact: it determines which records can enter a workflow and what an auditor can reconstruct months later. That is a governance concern before it is an inference concern.

What should happen when replay finds a bad record?

Replaying a report should reproduce the input digest and validate against the recorded schema version. It should not silently validate old output with the newest rules. If the old schema is unavailable, the replay is incomplete and belongs in an inspection queue.

package moderation

import "fmt"

type Decision struct {
    ReportID         string   `json:"report_id"`
    SchemaVersion    string   `json:"schema_version"`
    Classification   string   `json:"classification"`
    Summary          string   `json:"summary"`
    Evidence         []string `json:"evidence"`
    NeedsHumanReview bool     `json:"needs_human_review"`
}

type Store interface {
    FindByKey(key string) (Decision, bool, error)
    AppendAccepted(key string, decision Decision) error
    AppendHold(key string, reason string) error
}

func Commit(key string, raw []byte, store Store) error {
    if _, found, err := store.FindByKey(key); err != nil {
        return err
    } else if found {
        return nil
    }
    decision, err := DecodeAndValidateAgainstRecordedSchema(raw)
    if err != nil {
        return store.AppendHold(key, "invalid output for recorded schema")
    }
    if decision.ReportID == "" || len(decision.Evidence) == 0 {
        return store.AppendHold(key, "missing traceable evidence")
    }
    if err := store.AppendAccepted(key, decision); err != nil {
        return fmt.Errorf("append accepted decision: %w", err)
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

DecodeAndValidateAgainstRecordedSchema represents a standards-compliant JSON parser followed by JSON Schema validation. It is not a regular expression over response text. The repository must enforce uniqueness as well, because two workers can pass the initial lookup before either append completes.

The rejected option is automatic repair: take malformed output, fill missing fields, and enqueue the repaired object. That can be acceptable for exploratory analytics where every row is visibly provisional. It is the wrong boundary for moderation review, because a repair can erase the difference between what the model returned and what the application decided. Hold the record, preserve the linkage, and make a human-visible transition explicit.

How do correctness and compliance meet in operations?

Test the contract with fixtures for malformed JSON, missing fields, unknown properties, wrong scalar types, unsupported enum values, empty evidence, and quoted prompt-injection text. Add replay tests for duplicate delivery and for a record whose schema version is no longer deployed. These tests exercise the migration surface, which is more valuable than another happy-path request test.

Record schema version, input digest, correlation identifier, validation outcome, retry count, and latency bucket. Avoid placing complete support reports in ordinary logs by default; they may contain personal or sensitive material. Retention, deletion, access, and residency obligations vary by jurisdiction and organization, so the compliance owner must decide how long the audit linkage and source payload are kept. JSON Schema cannot satisfy those obligations by itself.

The stopping rule is concrete: if a malformed, duplicate, or version-ambiguous result can advance the human-review queue without an auditable event, the architecture is unfinished. Keep the model call replaceable, keep the policy local, and make the accepted record explainable.

References

Top comments (0)