DEV Community

Avery Lin
Avery Lin

Posted on

Split Config Migration Pages Into an Owned Schema Pin and a Drafted Essay

A migration page is trustworthy only when commands, keys, and defaults come from a reviewed schema diff. Drafted prose may explain those pinned facts, but it must not add a flag, a path, or a version. The workflow below stores the diff as a JSON pin, marks narrative sections, and fails when a command is absent from the pin. Teams without a versioned schema, and teams writing security advisories, should not use this split at all.

Where generated upgrade notes usually break

Generated upgrade notes often mix a real diff with a plausible command that never existed in the repository. Reviewers then approve the tone and miss a wrong default, a renamed key, or a rollback step that cannot run. Separating the pin from the essay turns that class of error into a mechanical mismatch instead of a careful reading exercise. The pin is the only place where versions, keys, types, defaults, and shell commands are allowed to appear as facts.

The same split also limits how much context a drafting model needs before it writes the essay. A short pin is easier to review than a full schema history pasted into a chat window. It gives the checker an allowlist, and it gives the human a single file to approve when the code change lands. When the pin is wrong, the page is wrong, which is a better failure mode than a fluent page that cannot be traced.

Facts a human must own

A human owner records the source schema path, the target schema path, and the exact key set that the note is allowed to cite. That owner also records each command as a literal string, including the working directory assumption and the expected exit condition. Ownership includes the rollback decision, because a draft can describe a reverse migration that the schema change does not support. The owner name and the review date stay in the pin, so a later edit cannot hide who accepted the facts.

Human ownership of the pin stops at facts that a schema diff can actually prove on its own. Impact adjectives, audience guesses, and broad most-teams claims are not made true by a green checker. Those sentences can remain in the drafted essay, but a reviewer still has to reject unsupported scope. The checker described in the procedure only enforces keys, commands, version strings, and the rollback flag.

Prose a model may draft

The model may draft why a key moved, which workloads notice the change, and how to read the command the pin already lists. It may also draft a short sequencing paragraph, provided every command string in that paragraph already exists in the pin. It may not draft new flags, sample values that contradict the recorded default, or dates that are absent from the pin. If a sentence needs a fact that is not pinned, the draft stops and the human extends the pin before any prose continues.

Keep the drafted region small on purpose so the essay cannot quietly replace the generated fact table. A useful essay is three or four paragraphs that point at the generated fact table, not a second copy of the table in sentences. Repeating the default in prose is how drift starts when someone updates the pin and forgets the paragraph. Prefer a cross-reference such as the default recorded for pool.timeout_ms over a restated number in prose.

Use a free draft pass without making it authoritative

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access can draft the marked narrative sections when the pin file is the only context you paste. The free server option can run the same local checker and a static preview you start yourself, so review needs no paid host. Neither option is the source of truth, and the pin plus the checker stay authoritative if that hosted environment is unavailable.

Confirm current access terms in the product documentation before you depend on either option, because this article does not claim quotas, hardware, or permanence. Do not paste secrets, customer data, or unreleased credentials into a hosted draft even when the schema itself looks harmless. If the free path is down, write the essay locally and still run the same checker on the branch. The procedure does not require a hosted model in order to reject an unowned command during review.

Procedure

1. Capture the two schema versions that the note covers

Store the old and new config schemas next to the code change that introduced them in git. Prefer the files already reviewed in the pull request, rather than a schema reconstructed from memory. Record those paths in the pin so a later reader can regenerate the diff without guessing. If the change is not representable as a schema diff, stop and write the page under a different review process.

2. Write the pin and review it like production code

Create the pin file with versions, an owner, keys, and the literal commands reviewers expect operators to run. Review that file in the same pull request as the schema change, before any essay exists. Treat a pin edit as a behavior change, because the published commands will be copied from it. A sample pin is included below so you can run the checker without a private repository.

3. Render the fact table from the pin

Generate the fact section with a script so the model never types keys or shell commands. Commit the generator output, or regenerate it in continuous integration and fail on a dirty diff. The essay then links to that section instead of restating the same keys in surrounding prose. This generated section remains the practical boundary between human-owned facts and every later drafted explanation on the page.

4. Draft only inside the narrative markers

Paste the pin, not the whole repository, into the drafting session as the only factual source. Ask only for prose that stays strictly between the narrative draft markers shown in the sample page. Reject any response that adds a fenced shell block you did not already supply in the pin. If you start a static preview on the free server, compare that page with the pin and look for unknown keys.

python3 -m http.server 8080 --directory docs
Enter fullscreen mode Exit fullscreen mode

5. Run the local allowlist checker

Run the Python checker from the repository root against the pin file and the Markdown page. The checker extracts fenced commands and dotted backtick tokens from the narrative region only, not the fact table. It fails when a command is not an exact argv string, or when a dotted token is not listed in keys. Fix the pin or delete the sentence, and do not weaken the checker to accept a fluent mismatch.

6. Read the claims the checker cannot see

After a green run, a human still reads impact, ordering advice, and any sentence that says who is affected. The checker does not know whether background workers are the right audience for that timeout key. It also does not execute the commands, so a pinned command can still be operationally wrong. Schedule a dry run of each pinned command in a disposable environment before you call the page done.

Sample pin

{
  "from_version": "1.4",
  "to_version": "1.5",
  "owner": "config-reviewer",
  "reviewed_on": "2026-10-09",
  "source_schema": "schema/config-1.4.json",
  "target_schema": "schema/config-1.5.json",
  "keys": {
    "pool.timeout_ms": {"change": "default", "from": 1000, "to": 2500},
    "pool.max_idle": {"change": "removed"}
  },
  "commands": [
    {
      "argv": "python scripts/migrate_config.py --from 1.4 --to 1.5",
      "expects": "exit 0 and a rewritten config file"
    }
  ],
  "rollback_supported": false
}
Enter fullscreen mode Exit fullscreen mode

The numbers in that pin are sample data for the checker, not measurements from a running service. Replace them with the defaults your schema review actually accepted. Keep reviewed_on equal to the review date you can defend, and do not copy this date forward. A pin that cannot point at real schema paths should not be used to publish a page.

Sample page shape

## Facts

| Key | Owned change |
| --- | --- |
| `pool.timeout_ms` | default from 1000 to 2500 |
| `pool.max_idle` | removed |

Enter fullscreen mode Exit fullscreen mode


bash
python scripts/migrate_config.py --from 1.4 --to 1.5


Expected: exit 0 and a rewritten config file

Rollback: not supported by this pin.

<!-- narrative:draft -->
Workers that set `pool.timeout_ms` explicitly keep their value, and only omitted keys receive the new default recorded in the fact table. There is no rollback command in the pin, so this essay does not describe a reverse migration. Run the listed command once in a staging copy, then compare the rewritten file with the 1.5 schema.
<!-- /narrative:draft -->
Enter fullscreen mode Exit fullscreen mode


python

The fact table above is illustrative and should be emitted by your generator, not typed by the model. The narrative mentions the timeout key, which is allowlisted, and it avoids adding a second shell fence. A sentence that added an unknown dotted token would fail, because that token is absent from keys. That failure is the behavior you want when a draft tries to document a setting the schema diff never contained.

Local checker

#!/usr/bin/env python3
"""Fail a migration essay when narrative invents a key or command outside the pin.

This file is a local proposal, and it does not call a model or record hosted metrics.
"""

import json
import re
import sys
from pathlib import Path

FENCE_MARK = "`" * 3
FENCE = re.compile(
    FENCE_MARK + r"(?:bash|sh|shell)?\n(.*?)" + FENCE_MARK,
    re.S,
)
NARRATIVE = re.compile(
    r"<!-- narrative:draft -->(.*?)<!-- /narrative:draft -->",
    re.S,
)
TOKEN = re.compile(r"`([^`\n]+)`")


def load_pin(path: Path) -> dict:
    pin = json.loads(path.read_text(encoding="utf-8"))
    required = ["from_version", "to_version", "owner", "keys", "commands"]
    missing = [name for name in required if name not in pin]
    if missing:
        raise SystemExit("pin missing fields: " + ", ".join(missing))
    return pin


def render_facts(pin: dict) -> str:
    lines = ["## Facts", "", "| Key | Owned change |", "| --- | --- |"]
    keys = pin["keys"]
    items = keys.items() if isinstance(keys, dict) else [(key, {}) for key in keys]
    for key, meta in items:
        change = meta.get("change", "cited") if isinstance(meta, dict) else "cited"
        detail = change
        if isinstance(meta, dict) and "from" in meta and "to" in meta:
            detail = change + " from " + str(meta["from"]) + " to " + str(meta["to"])
        lines.append("| `" + key + "` | " + detail + " |")
    lines.append("")
    for item in pin["commands"]:
        lines.append(FENCE_MARK + "bash")
        lines.append(item["argv"])
        lines.append(FENCE_MARK)
        lines.append("")
        lines.append("Expected: " + item["expects"])
    if pin.get("rollback_supported") is False:
        lines.append("")
        lines.append("Rollback: not supported by this pin.")
    return "\n".join(lines)


def check(pin: dict, markdown: str) -> list[str]:
    errors = []
    blocks = NARRATIVE.findall(markdown)
    if not blocks:
        return ["no narrative:draft region found"]
    body = "\n".join(blocks)
    allowed_argv = {item["argv"] for item in pin["commands"]}
    allowed_keys = set(pin["keys"])
    for fence in FENCE.findall(body):
        command = fence.strip()
        if command not in allowed_argv:
            errors.append("unowned command: " + command)
    for token in TOKEN.findall(body):
        if token in allowed_keys or token in allowed_argv:
            continue
        if "." in token and re.fullmatch(r"[A-Za-z0-9_.-]+", token):
            errors.append("unowned key token: " + token)
    for version in (pin["from_version"], pin["to_version"]):
        if version not in markdown:
            errors.append("missing version string: " + version)
    if pin.get("rollback_supported") is False and "rollback" in body.lower():
        allowed_phrase = (
            "no rollback" in body.lower()
            or "not describe a reverse" in body.lower()
        )
        if not allowed_phrase:
            errors.append("rollback mentioned but pin sets rollback_supported false")
    return errors


def main() -> int:
    args = sys.argv[1:]
    render = False
    if args and args[0] == "--render":
        render = True
        args = args[1:]
    if render:
        if len(args) != 1:
            raise SystemExit("usage: check_migration_pin.py --render PIN.json")
        print(render_facts(load_pin(Path(args[0]))))
        return 0
    if len(args) != 2:
        raise SystemExit("usage: check_migration_pin.py PIN.json PAGE.md")
    pin = load_pin(Path(args[0]))
    page = Path(args[1]).read_text(encoding="utf-8")
    errors = check(pin, page)
    if errors:
        print("\n".join(errors))
        return 1
    print("pin check passed")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
Enter fullscreen mode Exit fullscreen mode
python3 scripts/check_migration_pin.py --render \
  docs/migrations/pins/1.4-to-1.5.json \
  > docs/migrations/partials/1.4-to-1.5.facts.md

python3 scripts/check_migration_pin.py \
  docs/migrations/pins/1.4-to-1.5.json \
  docs/migrations/1.4-to-1.5.md
Enter fullscreen mode Exit fullscreen mode

A render pass prints the fact section from the pin and does not read the essay at all. A check pass prints pin check passed and exits with status 0 when the narrative stays inside the allowlist. Any unowned command prints the offending string and exits with status 1 so the branch fails. The static server command is only a local preview you start, and it is not a benchmark of the free server option.

Decision table

Situation Model may draft Human must own Checker result
Key default changed in schema Why callers notice Old default, new default, key name Fail if the essay cites an unknown dotted token
Command already stored in the pin When to run it Exact argv and expected exit Fail if a narrative fence differs from argv
Rollback marked unsupported A warning that no reverse step exists rollback_supported: false Fail if the essay invents a reverse command
Behavior change outside the schema Nothing, until a different review path is chosen Decision not to force a pin Do not bypass the checker to publish anyway

Read the table as a routing rule, not as a suggestion that every row deserves a generated page. Rows without a schema diff have no pin, so they never reach the drafting step described above. Rows with a pin still need the dry run, because an exact argv string can point at a script that fails on real files. Use the checker column to decide what automation may block, and leave audience claims to the human review in step 6.

Limitations

The allowlist is intentionally narrow, and several real defects still pass through it without a warning. A pinned command can be outdated if the owner copies the wrong script path and nobody executes it. A dotted token rule misses bare words, so a model can still invent a flag that is not wrapped in backticks. Version presence and the rollback phrase check are heuristics, not proof that tags or reverse migrations were reviewed.

Hosted drafting adds a second class of limitation that the local checker is not able to measure. Free model access and the free server option can speed review, but either one can be unavailable or unfit for private schemas. This article states those two availability options as operator-supplied claims, not as measured capacity or uptime. Keep a local editor path so a documentation change is not blocked when the hosted preview is down.

Who should skip this workflow

Skip this split when the change has no schema, because the pin would be a story rather than a diff. Skip it for security advisories, legal terms, pricing, and uptime promises, where every sentence needs specialist review rather than an allowlist. Skip it if your reviewers will not dry-run commands, since a green checker can still ship a broken migration. Skip it in environments that forbid sending schema excerpts to a hosted model, and write the essay offline instead.

The useful outcome is a page whose commands trace to a reviewed file and whose essay can be regenerated cleanly. The essay can change voice without forcing a second review of every argv string already stored in the pin. If the next upgrade already has two schema files in git, draft the marked essay from the pin alone. Run the checker on the free server when that option is currently available, and merge only after it exits cleanly.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.