Generated configuration pages fail when they copy parser defaults without a signed owner for units, restarts, and secrets. A useful pipeline extracts flag and environment bindings from source, then splits drafting work from publication authority. Models may regroup keys, rewrite help text, and propose example files from already extracted facts. Humans must still sign every default, unit, restart requirement, and secret classification before the page ships.
This article describes a documentation-generation workflow built around that split, not around chat-authored manuals. The original artifact is a small extractor plus a review ledger that refuses unsigned claims. The method stays useful if any particular coding assistant is removed from the drafting step. It also stays honest about the facts that free model access cannot verify from source.
Core conclusion and scope
Parser registrations are a better source for a config catalog than a blank prompt or a remembered README. Help strings already encode names, types, and sometimes default literals that a script can harvest. Those literals are still not publication-grade, because production meaning lives in operators, units, and failure modes. The catalog should therefore compile keys from code and leave semantic claims in a human signature lane.
The workflow below targets command-line services that bind flags and environment variables in one Python module. It does not replace OpenAPI generation, changelog authorship, or runbook writing. Teams that scatter configuration across Helm values, feature-flag consoles, and remote stores need extra extractors. Treat the sample worker as a labeled example, not as a production benchmark.
What a model may draft versus what a reviewer must own
Keep a decision table next to the facts file so drafting never silently becomes authority. Rows below are publication claims, not model quality scores, and they stay independent of any vendor.
| Claim on the page | May a model draft it? | Who publishes it? | Evidence required |
|---|---|---|---|
| Flag name, env name, parser type | No drafting needed | Extractor output | AST or registration call |
| Section grouping and heading copy | Yes, from extracted keys | Docs editor | Facts file only |
| Example YAML or dotenv layout | Yes, using extracted names | Docs editor | No invented keys |
| Default value as shipped behavior | No | Service owner | Release tag or config test |
| Unit, range, and empty-value meaning | No | Service owner | Code path or runbook |
| Restart required after change | No | On-call owner | Process model |
| Secret versus public classification | No | Security reviewer | Threat notes |
| Deprecation and support window | No | Maintainer | Version policy |
Unsigned rows must not reach the published reference, even when the prose looks complete. Editors may accept regrouped sections after a diff against the facts file. They must reject any default, unit, or restart sentence that lacks a named reviewer. That rule is the entire method; the extractor only makes violations visible.
Artifact: extract registrations into a facts file
The following sample module is labeled example code for a fictional queue-drain worker. It is not a claim about any live fleet, quota, or latency number. The registrations are the only facts the later catalog is allowed to trust.
# labeled example: queue_drain/settings.py
import argparse
import os
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="queue-drain")
parser.add_argument(
"--broker-url",
dest="broker_url",
default=os.environ.get("QUEUE_BROKER_URL", "amqp://guest@localhost:5672//"),
help="AMQP URL for the work queue",
)
parser.add_argument(
"--prefetch",
dest="prefetch",
type=int,
default=int(os.environ.get("QUEUE_PREFETCH", "32")),
help="Unacked messages held per consumer",
)
parser.add_argument(
"--ack-timeout-ms",
dest="ack_timeout_ms",
type=int,
default=int(os.environ.get("QUEUE_ACK_TIMEOUT_MS", "15000")),
help="Milliseconds before an unacked delivery is retried",
)
parser.add_argument(
"--lease-ttl-seconds",
dest="lease_ttl_seconds",
type=int,
default=int(os.environ.get("QUEUE_LEASE_TTL_SECONDS", "120")),
help="Seconds a worker may hold a lease",
)
parser.add_argument(
"--metrics-addr",
dest="metrics_addr",
default=os.environ.get("QUEUE_METRICS_ADDR", "127.0.0.1:9102"),
help="Bind address for Prometheus metrics",
)
return parser
A small AST walker can compile names, destinations, types, help strings, and default expressions without executing the process. Execution would pull live environment values and contaminate the catalog with a laptop's shell. The extractor therefore reads source text only and records default expressions as unevaluated strings.
# labeled example: tools/extract_argparse_facts.py
from __future__ import annotations
import argparse
import ast
import json
from pathlib import Path
class FlagVisitor(ast.NodeVisitor):
def __init__(self) -> None:
self.rows: list[dict[str, str]] = []
def visit_Call(self, node: ast.Call) -> None:
func = node.func
name = getattr(func, "attr", None)
if name != "add_argument":
self.generic_visit(node)
return
flag = ""
if node.args and isinstance(node.args[0], ast.Constant):
flag = str(node.args[0].value)
fields = {kw.arg: kw.value for kw in node.keywords if kw.arg}
dest = self._const(fields.get("dest")) or flag.lstrip("-").replace("-", "_")
help_text = self._const(fields.get("help"))
type_name = self._name(fields.get("type")) or "str"
default_src = ast.unparse(fields["default"]) if "default" in fields else ""
env_name = self._env_name(fields.get("default"))
self.rows.append(
{
"flag": flag,
"dest": dest,
"parser_type": type_name,
"help": help_text,
"default_expr": default_src,
"env_name": env_name,
}
)
self.generic_visit(node)
def _const(self, node: ast.AST | None) -> str:
if isinstance(node, ast.Constant):
return str(node.value)
return ""
def _name(self, node: ast.AST | None) -> str:
if isinstance(node, ast.Name):
return node.id
return ""
def _env_name(self, node: ast.AST | None) -> str:
text = ast.unparse(node) if node is not None else ""
marker = "os.environ.get("
if marker not in text:
return ""
start = text.find(marker) + len(marker)
chunk = text[start:].lstrip("\"'")
return chunk.split("\"")[0].split("'")[0]
def extract(path: Path) -> list[dict[str, str]]:
tree = ast.parse(path.read_text(encoding="utf-8"))
visitor = FlagVisitor()
visitor.visit(tree)
return visitor.rows
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("source", type=Path)
parser.add_argument("--out", type=Path, default=Path("config-facts.json"))
args = parser.parse_args()
rows = extract(args.source)
args.out.write_text(json.dumps(rows, indent=2) + "\n", encoding="utf-8")
print(f"wrote {len(rows)} keys to {args.out}")
if __name__ == "__main__":
main()
Run the extractor against the sample module and keep the JSON file in review, not in generated site output.
python tools/extract_argparse_facts.py queue_drain/settings.py --out config-facts.json
A facts file for the sample should resemble the following object list. Default expressions remain source text, including os.environ.get calls, so later prose cannot pretend a laptop export is the shipped default.
[
{
"flag": "--prefetch",
"dest": "prefetch",
"parser_type": "int",
"help": "Unacked messages held per consumer",
"default_expr": "int(os.environ.get(\"QUEUE_PREFETCH\", \"32\"))",
"env_name": "QUEUE_PREFETCH"
}
]
Numbered workflow
Follow the steps in order. Skipping the signature ledger is how unsigned defaults leak into customer-facing pages.
- Freeze the source revision that owns configuration registrations, and record the commit in the facts file header. Documentation generated from mixed branches will disagree with the binary operators actually run.
- Run the extractor in CI as a check that fails when keys appear or disappear without a catalog diff. Do not evaluate defaults during that job, because environment injection is not a documentation source.
- Emit a skeleton Markdown table with one row per key, leaving
default_signed,unit,restart, andsecretcolumns empty. Empty cells are cheaper to review than fluent paragraphs that hide missing owners. - Optionally draft grouping, heading copy, and example dotenv files from the facts file alone. Paste no extra product claims into that prompt, and reject any key the JSON does not list.
- Require named reviewers to fill the empty columns using tests, runbooks, or release tags. A missing signature blocks merge, even when the drafted prose is grammatically complete.
- Publish only the signed table plus examples that still match extracted names. Archive the facts file beside the page so later diffs can prove the catalog still tracks source.
Step four is the only place a coding model should enter the pipeline. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option can run that drafting job against the facts file when a team wants the grouping pass off a laptop. The model still must not invent defaults, units, restart rules, or secret labels, because those claims are not in the extracted JSON.
A constrained drafting prompt keeps the boundary visible. The text below is a labeled template, not a recorded production prompt with measured yield.
You receive config-facts.json only.
Group keys into at most four sections.
Write a short heading and a dotenv example using extracted env_name values.
Do not invent keys.
Do not state a numeric default, unit, restart rule, or secret classification.
Leave those cells as TODO_SIGNATURE.
If the drafting host is unavailable, skip step four and write headings by hand from the same JSON. The catalog remains valid because authority never lived in the model output. That substitution is the test that reader value does not depend on a vendor remaining in the loop.
Signature ledger and refusal rules
Store signatures beside the generated table, not inside model output. The ledger below is a labeled schema for queue-drain documentation review. Replace reviewer names with the people who actually own the process; do not invent affiliations.
| dest | default_signed | unit | restart_required | secret | reviewer | evidence |
| --- | --- | --- | --- | --- | --- | --- |
| prefetch | 32 | messages | no | no | TODO | config test |
| ack_timeout_ms | 15000 | milliseconds | no | no | TODO | worker loop |
| lease_ttl_seconds | 120 | seconds | no | no | TODO | lease renew |
| broker_url | unset in docs | URL | yes | yes | TODO | secret store |
| metrics_addr | 127.0.0.1:9102 | host:port | yes | no | TODO | bind test |
Refusal rules belong in CI so a fluent draft cannot override them. Compare these checks to the facts file after every catalog change.
python tools/check_config_catalog.py \
--facts config-facts.json \
--catalog docs/config-reference.md \
--ledger docs/config-signatures.yml
# labeled example: tools/check_config_catalog.py (core assertions)
REQUIRED_COLUMNS = ("default_signed", "unit", "restart_required", "secret", "reviewer")
BLOCKLIST = ("probably", "usually", "should be safe", "default is fine")
def assert_catalog(facts, catalog_text, ledger) -> None:
fact_dests = {row["dest"] for row in facts}
ledger_dests = {row["dest"] for row in ledger}
if fact_dests != ledger_dests:
raise SystemExit("ledger dests must match extracted dests")
for row in ledger:
for col in REQUIRED_COLUMNS:
if not str(row.get(col, "")).strip() or row.get(col) == "TODO":
raise SystemExit(f"unsigned column {col} for {row.get('dest')}")
lower = catalog_text.lower()
for token in BLOCKLIST:
if token in lower:
raise SystemExit(f"unsigned hedge in catalog: {token}")
broker_url illustrates why help-string defaults are unsafe to publish. The parser default may be a local guest URL, which is a development convenience and a credential leak if copied into public docs. The human owner records the secret classification and points operators at a secret store instead of echoing the literal. That decision cannot be recovered from AST alone.
Units produce a second class of silent errors. ack_timeout_ms and lease_ttl_seconds share an integer parser type, yet mixing them in one sentence creates a thousand-fold operational bug. The extractor records parser_type as int; only a reviewer may write milliseconds or seconds onto the page. Models that normalize both fields to "timeout" should be treated as failed drafts.
Limitations
The AST visitor understands a narrow add_argument shape and a single os.environ.get pattern. Wrappers, decorator-based parsers, and dynamically built flags will be missed until the extractor is extended. Dual sources of truth, such as a Helm chart that overrides the same keys, are outside this catalog unless a second extractor feeds the facts file.
Default expressions that call functions at import time cannot be signed from source text. Those values need a test that prints the parsed namespace under a clean environment. Feature flags delivered by a remote console are not configuration registrations and should not be mixed into this table. Support windows and deprecation dates also stay out of model drafts, because they are policy, not parser metadata.
Who should not use this approach
Do not use this pipeline if the published page is allowed to define production defaults without a reviewer. Regulated systems that require signed operational documents still need the human ledger, and they should not treat drafted grouping as evidence. Teams without a freeze on the settings module will generate catalogs that drift from running binaries. If configuration lives only in a vendor dashboard, extract from that API instead of argparse.
Skip the optional drafting host when the facts file contains secrets, customer hostnames, or unpublished product names. A free server option is still a network boundary, and the catalog's value is the signed table, not the generated headings. Operators who need air-gapped review should run the extractor locally and fill the ledger without a model in the loop.
The durable output is a config catalog whose keys compile from parser registrations and whose operational claims carry names. Keep drafting optional, keep signatures mandatory, and refuse any page that fills TODO with fluent guesses. If you already draft against MonkeyCode free models on the free server option, point that job at config-facts.json and leave the ledger in human review.
Top comments (0)