policy-nexus/tools/render.py
tegwick 06be56fdff Take over the renderer; defer controlled disclosure to a risk service
The renderer and its stylesheet moved in from the-custodian with a make build
target, so T02 generalises something that works rather than starting from
scratch. Publication tooling belongs to the repo that owns publication.

Disclosure is resolved for now: full public is fine in build mode, where there
are no users to expose and no attacker with anything to gain. Recorded as
deferred rather than closed, because it stops being true at production - the
same blast-radius disclosure that a consumer must read becomes a map once real
tenant data exists.

Controlled disclosure is deliberately not this repo's job. Publication is about
permanence and currency; embargo is about severity, remediation and timing, and
building it here would put risk judgement in the repo least qualified to make
it. It likely wants a service of its own - a risk-nexus - with this repo as its
publication surface rather than its brain.

The only cost today is one line in T01: the addressing scheme must not assume
every document is public from birth, so that adding an embargo state later is a
new status rather than a URL migration.

First publication retargeted - the framework relocated to NetKingdom canon and
is now tenancy-posture_v0.1, five axes rather than five planes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 15:42:00 +02:00

377 lines
14 KiB
Python

#!/usr/bin/env python3
"""Render a canon markdown document into a styled, self-contained artifact page.
Single source of truth: the markdown. The page is generated, never hand-edited,
so the two cannot diverge.
Design devices are recognised from conventions already present in the markdown
rather than from extra markup, so the source stays a readable document:
* a table whose first column is `**X0**`/`**X1**`... renders as a level ladder
* a table whose first header cell is `Threat` renders as a threat matrix
* a table with a `Kind` column renders with mechanical/adversarial chips
* a table whose first header cell is `E \\ P` renders as the E x P matrix
* a blockquote renders as a pull quote
* `**Decision N.N...**` at the start of a paragraph renders as a decision
* `## N. Title` headings build the section rail
Stdlib only, per the estate's structure-not-tooling stance: a publishing step
that needs its own toolchain is a publishing step that stops being run.
Usage:
python3 tools/render-artifact.py <source.md> --output <page.html> \\
[--title "Name"] [--subtitle "..."]
"""
from __future__ import annotations
import argparse
import html
import pathlib
import re
import sys
STYLE = pathlib.Path(__file__).parent / "style.css"
INLINE_CODE = re.compile(r"`([^`]+)`")
BOLD = re.compile(r"\*\*([^*]+)\*\*")
EM = re.compile(r"(?<![*\w])\*([^*]+)\*(?!\*)")
LINK = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
HEADING = re.compile(r"^(#{1,4})\s+(.*)$")
SECTION_NO = re.compile(r"^(\d+)\.\s+(.*)$")
LEVEL_CELL = re.compile(r"^\*\*([IAEPR])(\d)\*\*$")
DECISION = re.compile(r"^\*\*(Decision [\d.]+[^*]*)\*\*(.*)$", re.S)
def inline(text: str) -> str:
"""Escape, then apply inline markdown. Order matters: code first."""
slots: list[str] = []
def stash(rendered: str) -> str:
slots.append(rendered)
return f"\x00{len(slots) - 1}\x00"
text = INLINE_CODE.sub(lambda m: stash(f"<code>{html.escape(m.group(1))}</code>"), text)
text = html.escape(text, quote=False)
text = LINK.sub(
lambda m: f'<a href="{html.escape(m.group(2), quote=True)}">{m.group(1)}</a>', text
)
text = BOLD.sub(r"<strong>\1</strong>", text)
text = EM.sub(r"<em>\1</em>", text)
for i, rendered in enumerate(slots):
text = text.replace(f"\x00{i}\x00", rendered)
return text
def split_frontmatter(source: str) -> tuple[dict, str]:
if not source.startswith("---\n"):
return {}, source
end = source.index("\n---\n", 4)
meta = {}
for line in source[4:end].splitlines():
if ":" in line and not line.startswith((" ", "-")):
key, _, value = line.partition(":")
meta[key.strip()] = value.strip().strip('"')
return meta, source[end + 5 :]
def parse_table(lines: list[str], start: int) -> tuple[list[list[str]], int]:
rows, i = [], start
while i < len(lines) and lines[i].lstrip().startswith("|"):
cells = [c.strip() for c in lines[i].strip().strip("|").split("|")]
if not all(set(c) <= set("-: ") for c in cells):
rows.append(cells)
i += 1
return rows, i
# --- table renderers -------------------------------------------------------
def render_ladder(rows: list[list[str]]) -> str:
"""A level table becomes a stepped scale. Colour depth encodes strength."""
body = rows[1:]
plane = LEVEL_CELL.match(body[0][0]).group(1)
names = {
"I": "Identity", "A": "Authorization", "E": "Enforcement",
"P": "Placement", "R": "Retention",
}
rungs = []
for cells in body:
match = LEVEL_CELL.match(cells[0])
if not match:
continue
n = int(match.group(2))
text = cells[1] if len(cells) > 1 else ""
rungs.append(
f'<div class="rung r{n}"><span class="code">{plane}{n}</span>'
f'<span class="txt">{inline(text)}</span></div>'
)
while len(rungs) < 5:
rungs.append('<div class="rung na"><span class="code">—</span>'
f'<span class="txt">Ladder ends at {plane}{len(rungs) - 1}.</span></div>')
return (
'<div class="ladder"><div class="pname">'
f'{names.get(plane, plane)}<span>plane {plane}</span></div>'
f'<div class="rungs">{"".join(rungs)}</div></div>'
)
def render_threat(rows: list[list[str]]) -> str:
head = "".join(f"<th>{inline(c)}</th>" for c in rows[0])
body = []
for cells in rows[1:]:
tds = [f"<td>{inline(cells[0])}</td>"]
for c in cells[1:]:
cls = "yes" if "" in c else "no" if "" in c else ""
tds.append(f'<td class="{cls}">{inline(c)}</td>')
body.append(f"<tr>{''.join(tds)}</tr>")
return (f'<div class="scroll"><table class="tm"><thead><tr>{head}</tr></thead>'
f'<tbody>{"".join(body)}</tbody></table></div>')
def render_matrix(rows: list[list[str]]) -> str:
"""`E \\ P` table becomes the two-axis grid. Cells hold pins or markers."""
cols = rows[0][1:]
cells = ['<div class="mgrid">']
for cells_row in rows[1:]:
e = cells_row[0].strip("*")
level = int(e[1]) if len(e) > 1 and e[1].isdigit() else 0
cells.append(f'<div class="rlab">{html.escape(e)}</div>')
for value in cells_row[1:]:
v = value.strip()
if v == "":
cells.append('<div class="mcell void"></div>')
continue
tint = f" tint{level}" if level else ""
pins = ""
for entry in (p.strip() for p in v.split("<br>") if p.strip()):
ghost = " ghost" if entry.startswith("(") else ""
pins += f'<span class="pin{ghost}">{inline(entry.strip("()"))}</span>'
cells.append(f'<div class="mcell{tint}">{pins}</div>')
cells.append('<div class="rlab"></div>')
cells.extend(f'<div class="clab">{html.escape(c)}</div>' for c in cols)
cells.append("</div>")
return (
'<div class="matrix-shell"><div class="ylab">Enforcement →</div>'
+ "".join(cells)
+ "</div>"
'<div class="mnote">'
'<span class="k"><span class="sw"></span>Where a service sits today</span>'
'<span class="k"><span class="sw g"></span>Target or default</span>'
'<span class="k"><span class="sw v"></span>Unreachable at this placement</span>'
"</div>"
)
def render_table(rows: list[list[str]]) -> str:
if not rows:
return ""
header = [c.strip() for c in rows[0]]
first = header[0].lower()
if first.replace(" ", "") in {"e\\p", "e\\p"}:
return render_matrix(rows)
if first == "threat":
return render_threat(rows)
kind_col = header.index("Kind") if "Kind" in header else None
# A level table is a ladder. The evidence table also leads with `Level` but
# carries a `Kind` column, and is a table of artifacts, not of rungs.
if first == "level" and kind_col is None and len(rows) > 1 and LEVEL_CELL.match(rows[1][0]):
return render_ladder(rows)
head = "".join(f"<th>{inline(c)}</th>" for c in header)
body = []
for cells in rows[1:]:
tds = []
for i, c in enumerate(cells):
if i == kind_col:
adv = "adv" if "adversarial" in c.lower() else ""
label = re.sub(r"[*_]", "", c).strip()
tds.append(f'<td><span class="kind {adv}">{html.escape(label)}</span></td>')
else:
tds.append(f"<td>{inline(c)}</td>")
body.append(f"<tr>{''.join(tds)}</tr>")
return (f'<div class="scroll"><table><thead><tr>{head}</tr></thead>'
f'<tbody>{"".join(body)}</tbody></table></div>')
# --- document ---------------------------------------------------------------
def render_body(markdown: str) -> tuple[str, list[tuple[str, str, str]]]:
lines = markdown.splitlines()
out: list[str] = []
rail: list[tuple[str, str, str]] = []
open_section = False
ladders_open = False
i = 0
def close_ladders() -> None:
nonlocal ladders_open
if ladders_open:
out.append("</div></div>")
ladders_open = False
while i < len(lines):
line = lines[i]
stripped = line.strip()
if not stripped:
i += 1
continue
heading = HEADING.match(stripped)
if heading:
level, text = len(heading.group(1)), heading.group(2)
if level == 1:
i += 1
continue
if level == 2:
close_ladders()
if open_section:
out.append("</section>")
match = SECTION_NO.match(text)
if match:
num, title = match.group(1), match.group(2)
anchor = f"s{num}"
rail.append((anchor, num, title))
out.append(
f'<section id="{anchor}"><h2>'
f'<span class="sn">{int(num):02d}</span>{inline(title)}</h2>'
)
else:
anchor = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
rail.append((anchor, "·", text))
out.append(f'<section id="{anchor}"><h2>{inline(text)}</h2>')
open_section = True
else:
close_ladders()
out.append(f"<h3>{inline(text)}</h3>")
i += 1
continue
if stripped.startswith("|"):
rows, i = parse_table(lines, i)
rendered = render_table(rows)
if 'class="ladder"' in rendered:
if not ladders_open:
out.append('<div class="breakout"><div class="ladders">')
ladders_open = True
out.append(rendered)
else:
close_ladders()
out.append(rendered)
continue
close_ladders()
if stripped.startswith("```"):
block = []
i += 1
while i < len(lines) and not lines[i].strip().startswith("```"):
block.append(lines[i])
i += 1
out.append(f"<pre>{html.escape(chr(10).join(block))}</pre>")
i += 1
continue
if stripped.startswith(">"):
quote = []
while i < len(lines) and lines[i].strip().startswith(">"):
quote.append(lines[i].strip().lstrip(">").strip())
i += 1
out.append(f'<div class="rule-quote"><p>{inline(" ".join(quote))}</p></div>')
continue
if re.match(r"^[-*]\s+", stripped) or re.match(r"^\d+\.\s+", stripped):
ordered = bool(re.match(r"^\d+\.\s+", stripped))
items = []
while i < len(lines):
s = lines[i].strip()
if re.match(r"^[-*]\s+", s) or re.match(r"^\d+\.\s+", s):
items.append(re.sub(r"^([-*]|\d+\.)\s+", "", s))
elif s and lines[i].startswith((" ", "\t")) and items:
items[-1] += " " + s
else:
break
i += 1
tag = "ol" if ordered else "ul"
body = "".join(f"<li>{inline(t)}</li>" for t in items)
out.append(f"<{tag}>{body}</{tag}>")
continue
if set(stripped) <= set("-") and len(stripped) >= 3:
i += 1
continue
para = [stripped]
i += 1
while i < len(lines) and lines[i].strip() and not re.match(
r"^(\||>|```|#{1,4}\s|[-*]\s|\d+\.\s|---)", lines[i].strip()
):
para.append(lines[i].strip())
i += 1
text = " ".join(para)
decision = DECISION.match(text)
if decision:
out.append(
f'<p><span class="dec">{inline(decision.group(1))}</span>'
f"{inline(decision.group(2))}</p>"
)
else:
out.append(f"<p>{inline(text)}</p>")
close_ladders()
if open_section:
out.append("</section>")
return "\n".join(out), rail
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("source", type=pathlib.Path)
ap.add_argument("--output", required=True, type=pathlib.Path)
ap.add_argument("--title", default=None)
ap.add_argument("--subtitle", default="")
args = ap.parse_args()
meta, markdown = split_frontmatter(args.source.read_text())
body, rail = render_body(markdown)
title = args.title or meta.get("title", args.source.stem)
display = title.split(":")[0].strip()
eyebrow = " ".join(
f"<span{' class=\"stat\"' if k == 'status' else ''}>{html.escape(v)}</span>"
for k, v in (
("id", meta.get("id", "")),
("status", f"{meta.get('status', '')} · {meta.get('revision', '')}".strip(" ·")),
("date", meta.get("date", "")),
)
if v
)
rail_html = "".join(
f'<li><a href="#{a}"><span class="n">{n}</span>{html.escape(t)}</a></li>'
for a, n, t in rail
)
page = (
f"<title>{html.escape(display)}</title>\n"
f"<style>\n{STYLE.read_text()}\n</style>\n"
'<div class="wrap"><header>'
f'<div class="eyebrow">{eyebrow}<span>generated from canon — do not edit</span></div>'
f"<h1>{html.escape(display)}</h1>"
+ (f'<p class="sub">{html.escape(args.subtitle)}</p>' if args.subtitle else "")
+ '</header><div class="layout">'
f'<nav class="rail" aria-label="Sections"><ol>{rail_html}</ol></nav>'
f"<main>{body}"
f'<footer><span>{html.escape(meta.get("id", ""))} · '
f'{html.escape(meta.get("revision", ""))} · {html.escape(meta.get("status", ""))}</span>'
"<span>generated from the-custodian/canon</span></footer>"
"</main></div></div>\n"
)
args.output.write_text(page)
print(f"{args.output}: {len(rail)} sections, {len(page)} bytes")
return 0
if __name__ == "__main__":
raise SystemExit(main())