#!/usr/bin/env python3 """Build the Noema documentation section as static HTML. Usage: python3 docs/site/build.py OUTPUT_DIR Sources: the repository's Markdown documents, the OpenAPI document, the UI screenshots, and the authored pages in docs/site/pages/*.md. A page file may pull a repository document in with a line `{{include path [demote]}}`. The output follows the dixon.cx page skeleton; the section is deliberately not linked from the site navigation and is marked noindex. """ import html import json import os import re import shutil import sys import markdown ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) PAGES = os.path.join(ROOT, "docs", "site", "pages") BASE = "/noema/" SITE = "https://dixon.cx" # Order, slug, title, summary. The page body comes from pages/.md. CHAPTERS = [ ("index", "Overview", "What Noema is, what it refuses to claim, and how the pieces fit."), ("concepts", "Concepts and vocabulary", "Minds, events, epistemic kinds, memory kinds, beliefs, drives and the interpretation boundary."), ("architecture", "Architecture", "The daemon, the cognitive cycle, the storage model and the boundaries between deterministic code and language models."), ("getting-started", "Getting started", "From an empty machine to a running mind you can talk to, inspect and replay."), ("configuration", "Configuration", "Environment, config file, secret references, runtime settings and the per-mind cognitive configuration."), ("personality", "Identity and personality", "Versioned personalities, templates, traits and how they modulate attention and voice."), ("cognition", "The cognitive cycle", "Processes, attention scoring, the global workspace, executive dispatch, modes and metacognition."), ("memory", "Memory", "Episodic, semantic, procedural and autobiographical memory; retrieval, decay, consolidation and provenance."), ("beliefs", "Beliefs and evidence", "Evidence-derived confidence, statuses, contradictions, alternatives and operator corrections."), ("conversation", "Conversation", "How a message becomes an observation, a perception, a response intent and finally language."), ("motivation", "Goals, drives and affect", "The goal tree, the closed drive set, rule-driven affect and how they bias attention."), ("predictions", "Predictions and calibration", "Explicit predictions with criteria, deterministic resolution, prediction error and calibration."), ("graph", "Relationship graph", "Entities, evidenced edges, merges and the canvas explorer."), ("llm-providers", "Language models", "Roles, providers, routing, budgets, recorded calls, replay and versioned prompts."), ("capabilities", "Capabilities and policy", "The only path to action: ALLOW / ASK / DENY, approvals, locked capabilities and tool output as data."), ("integrations", "Integrations", "Research folder, git mirrors, Prometheus and signed webhooks as provenance-tagged observations."), ("dreaming", "Dreaming", "The opt-in idle mode: replay, association and speculative hypotheses that can never act."), ("snapshots-replay", "Snapshots, forks, replay and experiments", "Capturing state, branching minds, re-running cognition and running controlled experiments."), ("ui-tour", "Interface tour", "Every screen of the web interface, what it shows and what an operator does there."), ("api", "HTTP API", "Authentication, roles, the error envelope, streaming and every route."), ("cli", "Command line", "noemactl: every command with examples."), ("security", "Security and threat model", "Trust boundaries, secret handling, sessions, two-factor authentication and the threat-by-threat verification table."), ("operations", "Operating Noema", "Deployment, backups, scheduled jobs, metrics, audit, performance and upgrades."), ("decisions", "Architecture decision records", "The recorded decisions and the alternatives they rejected."), ("development", "Development and history", "Contributing, testing, the phased plan, changelog and release notes."), ("glossary", "Glossary", "Short definitions of every term used across the documentation."), ] STYLE = """ """ HEAD = """ {title} | Noema | Andy Dixon {style}
Phone:

Hidden from the page source until you click: friction against scrapers, not a guarantee.

Email:

web@dixon.cx

""" LINK_MAP = { "ARCHITECTURE.md": "architecture", "SECURITY.md": "security", "THREAT_MODEL.md": "security", "PLAN.md": "development", "CONTRIBUTING.md": "development", "CHANGELOG.md": "development", "README.md": "index", "docs/interpretation.md": "concepts", "docs/configuration.md": "configuration", "docs/ENVIRONMENT.md": "configuration", "docs/personality.md": "personality", "docs/cognition.md": "cognition", "docs/memory.md": "memory", "docs/beliefs.md": "beliefs", "docs/conversation.md": "conversation", "docs/motivation.md": "motivation", "docs/predictions.md": "predictions", "docs/graph.md": "graph", "docs/llm-providers.md": "llm-providers", "docs/capabilities.md": "capabilities", "docs/integrations.md": "integrations", "docs/dreaming.md": "dreaming", "docs/snapshots-replay.md": "snapshots-replay", "docs/performance.md": "operations", "docs/RELEASE_NOTES.md": "development", "docs/openapi.json": "api", "docs/adr": "decisions", } def read(path): with open(os.path.join(ROOT, path), encoding="utf-8") as f: return f.read() def demote(md, levels): out = [] fence = False for line in md.split("\n"): if line.startswith("```"): fence = not fence if not fence and line.startswith("#"): line = "#" * levels + line out.append(line) return "\n".join(out) def strip_h1(md): lines = md.split("\n") for i, l in enumerate(lines): if l.startswith("# "): return "\n".join(lines[:i] + lines[i + 1:]) return md def expand(md): """Resolve {{include path [demote N] [keep-title]}} and {{openapi}} directives.""" def inc(m): parts = m.group(1).split() path = parts[0] body = read(path) d = 0 keep = False for p in parts[1:]: if p.startswith("demote"): d = int(p.split("=")[1]) if "=" in p else 1 if p == "keep-title": keep = True if not keep: body = strip_h1(body) if d: body = demote(body, d) return body md = re.sub(r"\{\{include ([^}]+)\}\}", inc, md) md = md.replace("{{openapi}}", openapi_markdown()) return md def rewrite_links(html_text): def fix(m): href = m.group(1) if href.startswith(("http://", "https://", "#", "mailto:", "/")): return m.group(0) anchor = "" if "#" in href: href, anchor = href.split("#", 1) anchor = "#" + anchor if href.startswith("docs/screenshots/"): return 'href="' + BASE + "img/" + os.path.basename(href) + '"' for k, v in LINK_MAP.items(): if href == k or href.rstrip("/") == k or href.startswith(k + "/"): return 'href="' + BASE + v + anchor + '"' if href.endswith(".md"): slug = os.path.splitext(os.path.basename(href))[0] return 'href="' + BASE + slug + anchor + '"' return m.group(0) html_text = re.sub(r'href="([^"]+)"', fix, html_text) html_text = re.sub(r'src="docs/screenshots/([^"]+)"', r'src="' + BASE + r'img/\1"', html_text) return html_text def openapi_markdown(): doc = json.loads(read("internal/api/openapi.json")) groups = {} for path, ops in doc["paths"].items(): for method, op in ops.items(): tag = (op.get("tags") or ["root"])[0] groups.setdefault(tag, []).append((path, method.upper(), op)) out = [] for tag in sorted(groups): out.append(f"\n### {tag}\n") out.append("| Method | Path | Minimum role | Operation |\n|---|---|---|---|") for path, method, op in sorted(groups[tag], key=lambda x: (x[0], x[1])): role = str(op.get("x-required-role", "")).replace("(", "").replace(")", "") summary = op.get("summary", op.get("operationId", "")) out.append(f"| `{method}` | `{path}` | {role} | {summary} |") return "\n".join(out) + "\n" def render_page(idx, slug, title, summary, md): md = expand(md) conv = markdown.Markdown(extensions=["tables", "fenced_code", "toc", "sane_lists", "attr_list", "def_list"], extension_configs={"toc": {"toc_depth": "2-3"}}) body = conv.convert(md) body = rewrite_links(body) body = re.sub(r"", '
', body).replace("
", "") # Split into section boxes at h2 boundaries. parts = re.split(r"(?=

(.*?)

', sec, re.S) if not m: boxes.append('
' + sec + "
") continue hid, htext = m.group(1), m.group(2) headings.append((hid, re.sub("<[^>]+>", "", htext))) rest = sec[m.end():] boxes.append(f'

{htext}

{rest}
') contents = "" if headings: contents = '

On this page

    ' + "".join( f'
  1. {t}
  2. ' for h, t in headings) + "
" prev_link = next_link = "" if idx > 0: ps, pt, _ = CHAPTERS[idx - 1] prev_link = f'← {html.escape(pt)}' if idx + 1 < len(CHAPTERS): ns, nt, _ = CHAPTERS[idx + 1] next_link = f'{html.escape(nt)} →' nav_box = f'''
{prev_link or ""}All sections{next_link or ""}
''' head_box = f'''
Noema documentation

{html.escape(title)}

{html.escape(summary)}

{intro} {contents}
''' canonical = SITE + BASE + ("" if slug == "index" else slug) page = HEAD.format(title=html.escape(title), desc=html.escape(summary), canonical=canonical, style=STYLE) + head_box + "\n".join(boxes) + nav_box + TAIL return page def main(): if len(sys.argv) != 2: sys.exit("usage: build.py OUTPUT_DIR") out = sys.argv[1] os.makedirs(os.path.join(out, "img"), exist_ok=True) for f in os.listdir(os.path.join(ROOT, "docs", "screenshots")): if f.endswith(".png"): shutil.copy(os.path.join(ROOT, "docs", "screenshots", f), os.path.join(out, "img", f)) shutil.copy(os.path.join(ROOT, "internal", "api", "openapi.json"), os.path.join(out, "openapi.json")) for i, (slug, title, summary) in enumerate(CHAPTERS): with open(os.path.join(PAGES, slug + ".md"), encoding="utf-8") as f: md = f.read() page = render_page(i, slug, title, summary, md) with open(os.path.join(out, slug + ".html"), "w", encoding="utf-8") as f: f.write(page) print("wrote", slug + ".html", len(page) // 1024, "KB") if __name__ == "__main__": main()