DEV Community

Avery Lin
Avery Lin

Posted on

Bind Reference Summaries to Extracted Signatures Before Drafted Prose Ships

Published reference pages stay trustworthy only when extracted code facts and human-owned guarantees remain in separate fields. A documentation model may draft a narrative summary after a local extractor freezes signatures, defaults, and raised exception types. That same model may not invent compatibility windows, latency bounds, retry rules, or support promises of any kind. The workflow below enforces the split with a field ledger, a local validator, and a mandatory human review gate.

Why a single generated docstring is a weak contract

A mixed docstring hides the difference between a symbol the compiler can see and a promise only a maintainer can make. Reviewers then argue about tone while a silent sentence adds a retry rule that nobody on the team approved. Static extraction cannot recover intent, and a fluent model cannot recover missing policy from parameter names. Treating every sentence as editable prose therefore produces pages that look complete and still fail incident review.

Teams that already generate API listings from source still need a second control for narrative fields. The public symbol listing can be regenerated on each commit without taking on new editorial risk. The narrative cannot be regenerated that way, because adjectives such as stable, safe, and fast change the support burden. This workflow assigns each field a writer, a source, and a publish rule before any model call starts.

What the model may draft and what a human must own

The model may draft only the summary field, and only from a frozen fact bundle produced on the maintainer machine. It may restate parameter names, default literals, and exception types that already appear in that frozen bundle. It may also propose headings that mirror the public function list, provided every heading names an extracted symbol. It may not add fields, delete extracted facts, or describe behavior that the bundle does not contain.

A human must own side effects, compatibility, performance, security posture, and any example whose output readers might treat as a guarantee. A human must also confirm exception lists when the function calls helpers that this extractor cannot follow. Empty owned fields must block publication rather than invite the model to fill the gap with plausible prose. The table below is the ownership contract for this workflow, and it should be checked into the repository.

Field Allowed writer Required source If missing at publish
symbol, signature, defaults extractor only syntax tree of one module fail the job
raises extractor, then human confirm explicit raise names plus review fail when unresolved
summary model, then human review fact bundle only fail the job
side effects, compatibility human only maintainer note in the ledger fail the job
performance, retry, support human only approved policy note omit the section
examples human or frozen test output checked-in transcript omit the section

Step 1: Freeze extracted facts before any draft

Run the extractor against one module and write a JSON bundle that later steps must not edit. The script below targets a current CPython 3 release and has not been executed on a production repository here. It records function names, argument defaults, and explicit raise types taken from the module syntax tree. It deliberately ignores docstrings already present in the file, so earlier prose cannot re-enter the fact bundle as if it were extracted.

import ast
import json
import sys
from pathlib import Path

def literal(node):
    if node is None:
        return None
    try:
        value = ast.literal_eval(node)
    except (ValueError, TypeError, SyntaxError):
        return "<non-literal>"
    try:
        json.dumps(value)
    except TypeError:
        return repr(value)
    return value

def raise_name(exc):
    # Simple names and calls only. Attribute raises stay unresolved.
    if isinstance(exc, ast.Name):
        return exc.id
    if isinstance(exc, ast.Call) and isinstance(exc.func, ast.Name):
        return exc.func.id
    return None

def extract(path):
    tree = ast.parse(Path(path).read_text(encoding="utf-8"), filename=str(path))
    facts = []
    for node in tree.body:
        if not isinstance(node, ast.FunctionDef):
            continue
        if node.args.posonlyargs or node.args.kwonlyargs:
            facts.append({
                "symbol": node.name,
                "skipped": "positional-only or keyword-only arguments are outside this extractor",
            })
            continue
        raises = set()
        for child in ast.walk(node):
            if isinstance(child, ast.Raise):
                name = raise_name(child.exc)
                if name:
                    raises.add(name)
        args = node.args.args
        defaults = [None] * (len(args) - len(node.args.defaults))
        defaults += [literal(item) for item in node.args.defaults]
        calls = sorted({
            child.func.id
            for child in ast.walk(node)
            if isinstance(child, ast.Call) and isinstance(child.func, ast.Name)
        })
        facts.append({
            "symbol": node.name,
            "signature": [arg.arg for arg in args],
            "defaults": defaults,
            "raises": sorted(raises),
            "called_names": calls,
        })
    return {"module": str(path), "facts": facts}

if __name__ == "__main__":
    print(json.dumps(extract(sys.argv[1]), indent=2))
Enter fullscreen mode Exit fullscreen mode
python extract_facts.py pkg/public_api.py > build/facts.json
python -c "import hashlib, pathlib; print(hashlib.sha256(pathlib.Path('build/facts.json').read_bytes()).hexdigest())"
Enter fullscreen mode Exit fullscreen mode

Store the digest beside the bundle so a later draft cannot claim a different fact set. If the digest changes, discard the summary and start the draft again from the new bundle. Do not hand-edit the bundle to improve grammar, because grammar is not a fact source in this workflow.

Step 2: Attach an ownership ledger to the same digest

Create a ledger file that names the fact digest, the allowed draft field, and the human owner for each promise field. The model receives the facts and the allowed field name, and it does not receive credentials, customer tickets, or incident notes. The sample below is illustrative structure, not a record copied from a running service or a customer system. Keep the ledger next to the module so reviewers can see ownership without opening a separate policy wiki.

{
  "facts_sha256": "<digest printed in step 1>",
  "draftable": ["summary"],
  "owners": {
    "side_effects": "module-maintainer",
    "compatibility": "module-maintainer",
    "performance": "unset",
    "retry": "unset"
  },
  "promise_terms_denied_in_summary": [
    "always",
    "never",
    "guaranteed",
    "stable",
    "idempotent",
    "thread-safe",
    "backward compatible",
    "milliseconds",
    "retry"
  ]
}
Enter fullscreen mode Exit fullscreen mode

A continuous integration job should fail when any required owner field still contains the string unset. Optional sections such as performance stay absent until a named person replaces unset with a reviewed note. This rule is stricter than a style linter, and that strictness is the point of the gate. A green documentation build without an owner name is treated as an incomplete contract, not as a successful publish.

Step 3: Draft the summary from the bundle alone

Send the fact bundle, the ledger, and a short instruction that forbids new claims outside the bundle. Ask for one paragraph per symbol, with every concrete noun limited to names present in the bundle. Reject responses that add keys beyond summary or that mention files the extractor never opened for this job. Keep the repository checkout available to the extractor, but do not mount secrets into the draft environment.

The product boundary for this draft step is narrow, and it should stay narrower than the documentation pipeline around it. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode free model access can run this constrained summary draft using only the frozen fact bundle. The free server option can host that same job away from a laptop that also stores production credentials.

This article does not assert model names, quotas, hardware sizes, session duration, or benchmark results of any kind. A local prompt file keeps the constraint visible during review, and the following text is a template rather than a live transcript. Replace the runner with the client your team already trusts, and do not treat the sample as a measured run. The draft output should be JSON only, so the validator can reject prose that arrives without a symbol key.

Draft only JSON objects with a symbol and a summary.
Use only names and literals from the attached fact bundle.
Do not mention compatibility, speed, retries, or safety.
If a fact is missing, write "not stated in the fact bundle."
Enter fullscreen mode Exit fullscreen mode
Input: build/facts.json and build/ledger.json
Output: build/draft.json with keys limited to symbol and summary
Runner: the free-model client the team already operates
Excluded: environment files, tokens, tickets, and the full repository
Enter fullscreen mode Exit fullscreen mode

Step 4: Reject promise language before review

Even a constrained prompt can slip a guarantee into fluent prose, so the validator must run locally after every draft. The checker below compares symbols to the fact bundle, refuses extra keys, and flags every denied term it finds. It is a proposal and a starting regression test, not a complete proof about natural language meaning. Reviewers should still read the summary, because a paraphrase can promise safety without using a listed word.

import json
import re
import sys

def load(path):
    with open(path, encoding="utf-8") as handle:
        return json.load(handle)

def validate(facts, ledger, draft):
    allowed = {
        item["symbol"]
        for item in facts["facts"]
        if "skipped" not in item
    }
    denied = [term.lower() for term in ledger["promise_terms_denied_in_summary"]]
    errors = []
    if ledger.get("facts_sha256") in (None, "", "<digest printed in step 1>"):
        errors.append("ledger digest is still a placeholder")
    for row in draft:
        if set(row) - {"symbol", "summary"}:
            errors.append(f"extra keys: {row.get('symbol')}")
        if row.get("symbol") not in allowed:
            errors.append(f"unknown symbol: {row.get('symbol')}")
        text = str(row.get("summary", "")).lower()
        hits = [term for term in denied if re.search(rf"\b{re.escape(term)}\b", text)]
        if hits:
            errors.append(f"{row.get('symbol')}: denied terms {hits}")
    return errors

if __name__ == "__main__":
    errors = validate(load(sys.argv[1]), load(sys.argv[2]), load(sys.argv[3]))
    print("\n".join(errors) if errors else "ok")
    sys.exit(1 if errors else 0)
Enter fullscreen mode Exit fullscreen mode
python validate_draft.py build/facts.json build/ledger.json build/draft.json
python validate_draft.py build/facts.json build/ledger.json tests/fixtures/denied_summary.json
echo "denied fixture exit=$?"
Enter fullscreen mode Exit fullscreen mode
[
  {
    "symbol": "load_config",
    "summary": "This function is guaranteed to retry forever."
  }
]
Enter fullscreen mode Exit fullscreen mode

Add a fixture that contains the word guaranteed and assert that the process exits with a non-zero status. Add another fixture that only restates parameter names and assert that the process exits with status zero. Those two cases define the minimum regression set for this gate, and both should run in continuous integration. Expand the denied list when review finds a new promise verb, and record that addition in the same pull request.

Step 5: Write owned fields and then render

Open the draft only after the validator prints ok, then write side effects and compatibility into the ledger. Do not paste those owned statements back into the summary, or the next draft will blur the field boundary again. Render the page by joining extracted facts, the reviewed summary, and every owned field that is not unset. If performance or retry remains unset, omit those headings instead of printing a placeholder that readers might quote.

Publish the fact digest in a footer so a later reader can see which snapshot the prose actually describes. Numbered review checks keep the gate boring, repeatable, and independent of who happens to be on call. The four checks below are the minimum bar before the reference site job is allowed to upload HTML. A failed check returns the page to the maintainer, and it does not authorize another unsupervised model pass.

  1. Confirm the published digest matches build/facts.json for the same commit that the site job is publishing.
  2. Confirm every summary noun is a symbol, parameter, default, or raised type taken from that extracted file.
  3. Confirm owned fields were edited by the named owner, not by the draft job that produced the summary text.
  4. Confirm omitted sections are absent from the HTML, not filled with guessed bounds or softened promises.

Limitations and teams that should skip this

The extractor reads top-level functions, literal defaults, and simple raise names, but it skips keyword-only arguments, class methods, and attribute raises. Called helpers, decorators, and native extensions can change behavior without any of those gaps appearing in the bundle. A denied-term list cannot catch a paraphrase that promises the same outcome with different and unlisted words. A green validator is evidence of lexical compliance only, and it is not evidence that the summary is true.

Do not use this approach for security advisories, legal terms, incident communications, or regulated customer instructions. Do not use it when nobody is assigned to own compatibility, because the ledger will block every release or be bypassed. Do not point the draft job at a full repository when the only intended input is the frozen fact bundle. Teams that need attested wording should keep their existing approval system and treat these scripts as a pre-check only.

Keep the fence smaller than the repository

Reference quality improves when the model is fenced to a summary and the maintainer remains the only writer of guarantees. The artifact is small enough to live beside the module it documents, and the digest makes stale prose visible at review time. If a free MonkeyCode workspace already runs your constrained drafts, send it only the fact bundle from this workflow. Keep the validator, the ledger, and the publish decision inside the repository pipeline you already review.

Top comments (0)