master-zapier-plan-draft/scripts/gen-model-docs.py
George Lambert 10c663cc0c Milestone 2: module/model docs (MD, Sphinx RST, PDF) and work log
74 source modules documented with extracted signatures, JSDoc params,
imports, and call graphs. 10 first-class models (PriceRule through
StatusResponse) have their own MD/RST/PDF. Sphinx HTML builds from
docs/sphinx. Per-module PDFs in docs/modules-pdf and docs/models-pdf.

Middleware gates 2–6 and 10 pass on MOCK_VERAE without NATS.

Learned: RST includes are relative to the RST file; keep one PDF per
module; Add Numbers remains the only push required tomorrow.
2026-09-09 02:44:51 -04:00

194 lines
7.5 KiB
Python

#!/usr/bin/env python3
"""Write first-class data-model MD + RST + PDF sheets."""
from __future__ import annotations
from pathlib import Path
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer
ROOT = Path(__file__).resolve().parents[1]
MD = ROOT / "docs" / "models"
RST = ROOT / "docs" / "sphinx" / "models"
PDF = ROOT / "docs" / "models-pdf"
MODELS = {
"PriceRule": {
"src": "packages/zappier/src/pricing.ts",
"kind": "discriminated union",
"fields": [
("kind", "'free' | 'fixed' | 'variable'", "Which list-price formula"),
("fixedCents", "number?", "When kind=fixed, cents per call"),
("baseCents", "number?", "When kind=variable, base cents"),
("perKbCents", "number?", "When kind=variable, per KB metadata"),
("perMbCents", "number?", "When kind=variable, per MB attachments"),
],
"used_by": ["quoteCall", "meter", "admin rate card UI", "portal API & pricing tab"],
"returns_in": "RateCard.endpoints values",
},
"TierConfig": {
"src": "packages/zappier/src/pricing.ts",
"kind": "interface",
"fields": [
("id", "string", "free | pro | business or custom"),
("name", "string", "Display name"),
("multiplier", "number", "Applied once to list cents"),
("monthlyCreditCents", "number", "Included usage before Stripe/invoice"),
("defaultRule", "PriceRule?", "Price for unlisted operationIds"),
],
"used_by": ["quoteCall", "applyMonthlyCredit", "admin tiers", "portal"],
"returns_in": "PricingStore.getTiers()",
},
"RateCard": {
"src": "packages/zappier/src/pricing.ts",
"kind": "interface",
"fields": [("endpoints", "Record<string, PriceRule>", "Keys are OpenAPI operationIds")],
"used_by": ["quoteCall", "admin PUT /admin/api/endpoints/:id"],
"returns_in": "PricingStore.getRateCard()",
},
"Quote": {
"src": "packages/zappier/src/pricing.ts",
"kind": "interface",
"fields": [
("endpointId", "string", "operationId priced"),
("listCents", "number", "Before multiplier"),
("totalCents", "number", "round(list * multiplier)"),
("breakdown.baseCents", "number", "Variable base"),
("breakdown.metadataCents", "number", "KB component"),
("breakdown.attachmentCents", "number", "MB component"),
],
"used_by": ["meter() sets res.locals.quote; JSON body.quote on /v1/*"],
"returns_in": "quoteCall(...) → Quote",
},
"CallUsage": {
"src": "packages/zappier/src/pricing.ts",
"kind": "interface",
"fields": [
("metadataBytes", "number", "UTF-8 length of metadata JSON"),
("attachmentBytes", "number", "Sum of uploaded file sizes"),
],
"used_by": ["meter()", "quoteCall()"],
"returns_in": "constructed in meter middleware",
},
"Customer": {
"src": "packages/zappier/src/auth.ts",
"kind": "interface",
"fields": [
("id", "string", "cust_…"),
("name", "string", "Display"),
("tierId", "string", "Matches TierConfig.id"),
("apiKey", "string", "x-api-key value"),
("multiplierOverride", "number?", "Beats tier multiplier"),
("email", "string?", "Portal login"),
],
"used_by": ["apiKeyAuth", "portal", "invoices", "usage.summaryFor"],
"returns_in": "CustomerRepo.findByApiKey / findById",
},
"AddNumbersResult": {
"src": "packages/verae-activate/creates/add_numbers.js",
"kind": "Zapier create output",
"fields": [
("number1", "number", "First addend (finite)"),
("number2", "number", "Second addend (finite)"),
("sum", "number", "number1 + number2"),
("mode", "'local' | 'hosted'", "local = Zapier cloud arithmetic; hosted = POST /v1/add"),
],
"used_by": ["Zapier Add Numbers action", "optional zappier POST /v1/add"],
"returns_in": "perform(z, bundle) → single object (creates never return arrays)",
},
"TimestampRequest": {
"src": "docs/00-sources/veraetime-openapi.yaml",
"kind": "OpenAPI schema",
"fields": [
("data", "string (required)", "Payload to hash/timestamp"),
("hashAlg", "string?", "Defaults SHA256"),
],
"used_by": ["POST /api/timestamp", "middleware timestampService.create"],
"returns_in": "request body only",
},
"TimestampResponse": {
"src": "docs/00-sources/veraetime-openapi.yaml",
"kind": "OpenAPI schema",
"fields": [("jobId", "string", "UUID of async job")],
"used_by": ["Zapier Create Timestamp"],
"returns_in": "HTTP 202 JSON",
},
"StatusResponse": {
"src": "docs/00-sources/veraetime-openapi.yaml",
"kind": "OpenAPI schema",
"fields": [
("id", "string", "jobId"),
("status", "'pending' | 'completed' | 'failed'", "Job state"),
("error", "string?", "If failed"),
("result", "string?", "Certificate if completed"),
("completedAt", "string date-time?", ""),
("metadata", "object?", "blockIndex, timestamp, certificate, block, chain"),
],
"used_by": ["GET /api/status/{jobId}", "Find Job Status search"],
"returns_in": "HTTP 200 JSON",
},
}
def body(name: str, spec: dict) -> str:
rows = "\n".join(f"| `{n}` | `{t}` | {d} |" for n, t, d in spec["fields"])
return f"""# {name}
**Kind:** {spec['kind']}
**Defined in:** `{spec['src']}`
## Fields
| Field | Type | Meaning |
|-------|------|---------|
{rows}
## Who constructs it
{spec['returns_in']}
## Who consumes it
{spec['used_by'] if isinstance(spec['used_by'], str) else ', '.join(f'`{x}`' for x in spec['used_by'])}
## Related modules
See `docs/modules/` for the functions that take these types as parameters and the values they return.
"""
def main() -> None:
MD.mkdir(parents=True, exist_ok=True)
RST.mkdir(parents=True, exist_ok=True)
PDF.mkdir(parents=True, exist_ok=True)
styles = getSampleStyleSheet()
for name, spec in MODELS.items():
text = body(name, spec)
(MD / f"{name}.md").write_text(text, encoding="utf-8")
(RST / f"{name}.rst").write_text(
f"{name}\n{'=' * len(name)}\n\n.. include:: {name}.md\n :literal:\n",
encoding="utf-8",
)
(RST / f"{name}.md").write_text(text, encoding="utf-8")
pdf_path = PDF / f"{name}.pdf"
doc = SimpleDocTemplate(str(pdf_path), pagesize=letter, leftMargin=0.7 * inch, rightMargin=0.7 * inch)
story = []
for line in text.splitlines():
if line.startswith("# "):
story.append(Paragraph(line[2:].replace("&", "&amp;"), styles["Title"]))
elif line.startswith("## "):
story.append(Spacer(1, 8))
story.append(Paragraph(line[3:].replace("&", "&amp;"), styles["Heading2"]))
elif line.strip():
story.append(Paragraph(line.replace("&", "&amp;").replace("|", " · ").replace("`", ""), styles["BodyText"]))
else:
story.append(Spacer(1, 4))
doc.build(story)
print(f"wrote {len(MODELS)} models")
if __name__ == "__main__":
main()