DEV Community

Avery Lin
Avery Lin

Posted on

Freeze Semantic JSON Pointers Before Generating Field Descriptions

Generated API field copy stays trustworthy when every JSON Pointer is classified before any model runs. Continuous integration should reject any draft sentence that lands on a path classified as human-owned. Heading-level draft permits still let a model invent meaning inside an otherwise allowed section. A field-path ledger binds each leaf to a restatable schema fact or a human semantic claim.

The failure mode this map targets

Reference pages usually group many properties under one heading such as Request body or Status fields. A section-level permit then authorizes a model to write every sentence beneath that heading. Types, enums, and required flags are recoverable, but polling advice and retention promises are not. When those two kinds of claims share a paragraph, reviewers cannot tell which sentences are grounded.

JSON Schema and OpenAPI already name leaves with pointers such as /properties/retry/properties/backoff. A restatement of type, minimum, and enum from those nodes is recoverable work with a cited fragment. A sentence that tells callers to prefer exponential over linear is a product judgment, not a type. Classification at the pointer level keeps those claims from sharing a generated paragraph.

Two pointer classes

Give each documented JSON Pointer exactly one class before any generation job is scheduled. The restate class marks copy that must be recoverable from schema fragments, fixtures, or tests. The human class marks copy that states intent, tradeoffs, support posture, or unencoded behavior. Pointers with no class are build errors, because missing rows are not an invitation to improvise.

Nested objects inherit nothing from a parent pointer, including object nodes that receive their own description block. Extension fields such as x-when-to-poll can exist only in the ownership map when they never appear in the schema. Restate rows must list at least one source fragment that a hash can pin. Human rows must list none, so a later check can forbid invented citations on semantic paths.

Decision table for classification

Use this table as the working artifact beside the map, not as a style guide for prose.

Evidence present in the repository Class Model may draft
Schema type, enum, required, pattern, minimum restate Yes, citing the pointer
Field presence in a hashed example payload restate Yes, citing the fixture
Header name asserted by a contract test restate Yes, citing the test
Why a default value was chosen human No
How long clients should retry or poll human No
Compatibility, deprecation, or retention promises human No
Token lifetime, rotation, or legal processing terms human No

If a property needs both a type restatement and a semantic note, split it into two pointers rather than mixing classes. Put recoverable facts on the schema path, and put judgment on an explicit x- pointer that stays blank until a human writes it. The generator below treats a mixed paragraph as a failed freeze, not as a review comment.

Artifact: the ownership map

Keep a YAML ledger next to the schema so refactors update classification in the same pull request. The schema_sha256 value is filled by the checker, not by hand, after the file is read.

# docs/field-ownership.yaml — example map, not a live service description
schema: schemas/orders.v1.json
schema_sha256: pending
pointers:
  - path: /properties/id
    class: restate
    sources:
      - schemas/orders.v1.json#/properties/id
  - path: /properties/status
    class: restate
    sources:
      - schemas/orders.v1.json#/properties/status
  - path: /properties/status/x-when-to-poll
    class: human
    sources: []
  - path: /properties/idempotency_key
    class: restate
    sources:
      - schemas/orders.v1.json#/properties/idempotency_key
  - path: /properties/idempotency_window
    class: human
    sources: []
Enter fullscreen mode Exit fullscreen mode

The sample schema only needs enough shape for the checker to resolve restatable leaves. Treat the document as an example fixture, not as a published API.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["id", "status", "idempotency_key"],
  "properties": {
    "id": {"type": "string", "format": "uuid"},
    "status": {"type": "string", "enum": ["queued", "open", "closed"]},
    "idempotency_key": {"type": "string", "minLength": 8, "maxLength": 64}
  }
}
Enter fullscreen mode Exit fullscreen mode

Numbered workflow

  1. Inventory every pointer that will receive a description, including x- semantic rows with no schema node.
  2. Classify each pointer using the decision table, and refuse parent-to-child inheritance while filling the map.
  3. Hash the schema file and every restate source so later copy can be compared against the same bytes.
  4. Send only restate fragments to a model session; write human rows as freeze stubs with no prose.
  5. Render Markdown that tags each paragraph with its pointer, class, and source hash or human-owned.
  6. Fail CI when a human pointer gains model text, a restate pointer lacks a hash, or an unknown pointer appears.

Step 4 is the only place a hosted model needs to run, because the payload is schema leaves rather than a full guide. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option can host that restatement pass while the freeze file still blocks semantic pointers. The checker does not call a vendor API; it only validates files that a session wrote into docs/generated/.

Example generator and freeze checker

The following Python module is a local, runnable example. It hashes sources, writes stubs for human rows, and accepts restatements only when a matching pointer exists. Label any model output as untrusted until the freeze function returns without raising.

# field_freeze.py — example checker; run locally against fixtures
from __future__ import annotations

import hashlib
import json
import re
from pathlib import Path

import yaml

POINTER_BLOCK = re.compile(
    r"<!-- pointer:(?P<path>\S+) class:(?P<cls>restate|human) "
    r"src:(?P<src>\S+) -->\n(?P<body>.*?)(?=\n<!-- pointer:|\Z)",
    re.S,
)


def sha256_file(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def load_map(map_path: Path) -> dict:
    payload = yaml.safe_load(map_path.read_text())
    schema_path = (map_path.parent.parent / payload["schema"]).resolve()
    payload["_schema_path"] = schema_path
    payload["_schema"] = json.loads(schema_path.read_text())
    payload["_schema_sha"] = sha256_file(schema_path)
    return payload


def render_stubs(ownership: dict) -> str:
    chunks = []
    for row in ownership["pointers"]:
        src = "human-owned"
        body = "_Human-owned pointer. Leave blank until an owner writes this row._\n"
        if row["class"] == "restate":
            src = ",".join(row["sources"]) + f"@{ownership['_schema_sha'][:12]}"
            body = "_Pending restatement from cited schema leaves._\n"
        chunks.append(
            f"<!-- pointer:{row['path']} class:{row['class']} src:{src} -->\n{body}"
        )
    return "\n".join(chunks)


def freeze(markdown: str, ownership: dict) -> None:
    allowed = {row["path"]: row for row in ownership["pointers"]}
    found = list(POINTER_BLOCK.finditer(markdown))
    if not found:
        raise AssertionError("generated markdown has no pointer markers")
    seen = set()
    for match in found:
        path = match.group("path")
        cls = match.group("cls")
        src = match.group("src")
        body = match.group("body").strip()
        if path not in allowed:
            raise AssertionError(f"unknown pointer {path}")
        row = allowed[path]
        if cls != row["class"]:
            raise AssertionError(f"class mismatch for {path}")
        if path in seen:
            raise AssertionError(f"duplicate pointer {path}")
        seen.add(path)
        if row["class"] == "human":
            if src != "human-owned" or "Pending restatement" in body:
                raise AssertionError(f"model text on human pointer {path}")
            if body and not body.startswith("_Human-owned pointer."):
                raise AssertionError(f"semantic copy on frozen pointer {path}")
        else:
            if ownership["_schema_sha"][:12] not in src:
                raise AssertionError(f"stale schema hash on {path}")
            if not body or body.startswith("_Human-owned pointer."):
                raise AssertionError(f"empty restatement for {path}")
    missing = set(allowed) - seen
    if missing:
        raise AssertionError(f"missing pointers: {sorted(missing)}")
Enter fullscreen mode Exit fullscreen mode

Wire a small test module so the freeze rule fails on a mixed paragraph before anyone reviews tone. The tests below are executable examples against the sample map, not measurements from a production docs corpus.

# test_field_freeze.py
from pathlib import Path

from field_freeze import freeze, load_map, render_stubs

ROOT = Path(__file__).parent


def test_stubs_keep_human_rows_blank():
    ownership = load_map(ROOT / "docs" / "field-ownership.yaml")
    markdown = render_stubs(ownership)
    freeze(markdown.replace(
        "_Pending restatement from cited schema leaves._",
        "`status` is a string enum of queued, open, and closed.",
    ), ownership)


def test_rejects_prose_on_human_pointer():
    ownership = load_map(ROOT / "docs" / "field-ownership.yaml")
    markdown = render_stubs(ownership).replace(
        "_Human-owned pointer. Leave blank until an owner writes this row._",
        "Poll every 250ms until status is closed.",
        1,
    )
    try:
        freeze(markdown, ownership)
    except AssertionError as exc:
        assert "semantic copy" in str(exc)
    else:
        raise AssertionError("expected freeze to fail")
Enter fullscreen mode Exit fullscreen mode

Run the example with a locked schema file and the map from version control.

python -m pip install pyyaml pytest
python -m pytest test_field_freeze.py -q
Enter fullscreen mode Exit fullscreen mode

After a restatement session, replace only the restate stub bodies, then run the same freeze before merging. Do not let a formatter strip the HTML comment markers, because the checker treats unmarked paragraphs as unknown pointers. If a schema refactor renames a field, the missing-pointer assertion is the intended failure, not a prompt to guess the new path.

What the model may draft, and what a human must own

On restate pointers, a model may paraphrase schema keywords into a short field description that a reader can verify against the cited fragment. It may list enum members, note required flags, and restate numeric bounds that appear in the hashed schema. It may not add rationale, recommended client intervals, or compatibility language, even when those sentences would read more helpfully.

On human pointers, a person owns polling cadence, idempotency windows, error recovery, and any claim about what the service will still do next quarter. Those rows stay as stubs in generated output until an owner replaces the placeholder in a separate commit. Reviewers then read a small semantic diff instead of a regenerated reference page that mixed types with promises.

Limitations and who should not use this

The map does not classify narrative tutorials, architecture decision records, or status-page copy, because those documents are not pointer-addressable schema leaves. Teams without a machine-readable schema should not adopt the freeze file, since restatement would have no recoverable source. Legal, pricing, and support-commitment pages should stay outside the generator entirely, even when a model could imitate the house style.

The checker also cannot see runtime behavior that never landed in schema, fixtures, or tests. Undocumented side effects remain human work, and marking them restate would only launder guesses. Large allOf and oneOf graphs need extra inventory work, because a pointer that exists in only one branch can still be emitted as if it were universal. If your descriptions must stay bilingual, freeze each language as a distinct output file rather than asking a model to translate human-owned rows.

Pointer maps go stale when properties move and the YAML is not updated in the same change. Treat an unknown pointer as a broken build, then add a classified row before regenerating. That operational cost is the point of the workflow: generation stays cheap only on leaves that already have evidence.

Top comments (0)