Build immutable policy publication artifact

This commit is contained in:
tegwick 2026-08-18 12:16:04 +02:00
parent cac0301866
commit e8035f3887
22 changed files with 2375 additions and 452 deletions

View file

@ -19,7 +19,7 @@ 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> \\
python3 tools/render.py <source.md> --output <page.html> \\
[--title "Name"] [--subtitle "..."]
"""
@ -39,7 +39,7 @@ 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)\*\*$")
LEVEL_CELL = re.compile(r"^\*\*([IAEPRV])(\d)\*\*$")
DECISION = re.compile(r"^\*\*(Decision [\d.]+[^*]*)\*\*(.*)$", re.S)
@ -91,10 +91,10 @@ def parse_table(lines: list[str], start: int) -> tuple[list[list[str]], int]:
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)
axis = LEVEL_CELL.match(body[0][0]).group(1)
names = {
"I": "Identity", "A": "Authorization", "E": "Enforcement",
"P": "Placement", "R": "Retention",
"P": "Placement", "R": "Retention", "V": "Availability",
}
rungs = []
for cells in body:
@ -104,15 +104,15 @@ def render_ladder(rows: list[list[str]]) -> str:
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'<div class="rung r{n}"><span class="code">{axis}{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>')
f'<span class="txt">Ladder ends at {axis}{len(rungs) - 1}.</span></div>')
return (
'<div class="ladder"><div class="pname">'
f'{names.get(plane, plane)}<span>plane {plane}</span></div>'
f'{names.get(axis, axis)}<span>axis {axis}</span></div>'
f'<div class="rungs">{"".join(rungs)}</div></div>'
)
@ -326,25 +326,30 @@ def render_body(markdown: str) -> tuple[str, list[tuple[str, str, str]]]:
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())
def render_page(
source: pathlib.Path,
*,
title: str | None = None,
subtitle: str = "",
publication: dict[str, str] | None = None,
) -> tuple[str, dict, int]:
meta, markdown = split_frontmatter(source.read_text(encoding="utf-8"))
body, rail = render_body(markdown)
title = args.title or meta.get("title", args.source.stem)
display = title.split(":")[0].strip()
resolved_title = title or meta.get("title", source.stem)
display = resolved_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", "")),
("owner", meta.get("owner", "")),
(
"review",
f"reviewed {meta.get('last_reviewed', '')}"
if meta.get("last_reviewed")
else "",
),
)
if v
)
@ -353,23 +358,103 @@ def main() -> int:
for a, n, t in rail
)
publication = publication or {}
source_note = " · ".join(
item
for item in (
publication.get("source_repo", ""),
publication.get("source_path", ""),
publication.get("source_revision", ""),
)
if item
)
source_line = (
f'<p class="sub">Source: <code>{html.escape(source_note)}</code></p>'
if source_note
else ""
)
review_due = publication.get("review_due", "")
review_line = (
f'<p class="sub">Review due: {html.escape(review_due)}</p>'
if review_due
else ""
)
lifecycle = publication.get("lifecycle", "active")
successor = publication.get("successor", "")
lifecycle_notice = ""
if lifecycle == "superseded":
successor_link = (
f' <a href="{html.escape(successor, quote=True)}">Read its successor.</a>'
if successor
else ""
)
lifecycle_notice = (
'<div class="rule-quote"><p><strong>Superseded.</strong>'
f" This address is retained as part of the policy record.{successor_link}</p></div>"
)
elif lifecycle == "withdrawn":
lifecycle_notice = (
'<div class="rule-quote"><p><strong>Withdrawn.</strong> '
"This document is retained for historical reference and is not current policy."
"</p></div>"
)
currency_notice = (
'<div class="rule-quote"><p><strong>Review overdue.</strong> '
f"This document was due for review on {html.escape(review_due)}.</p></div>"
if review_due and publication.get("stale") == "true"
else ""
)
source_revision_meta = (
f'<meta name="policy-source-revision" content="'
f'{html.escape(publication.get("source_revision", ""), quote=True)}">\n'
if publication.get("source_revision")
else ""
)
source_digest_meta = (
f'<meta name="policy-source-digest" content="'
f'{html.escape(publication.get("source_digest", ""), quote=True)}">\n'
if publication.get("source_digest")
else ""
)
page = (
f"<title>{html.escape(display)}</title>\n"
"<!doctype html>\n<html lang=\"en\"><meta charset=\"utf-8\">\n"
+ source_revision_meta
+ source_digest_meta
+ 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'<div class="eyebrow">{eyebrow}<span>generated from canonical source — do not edit</span></div>'
f"<h1>{html.escape(display)}</h1>"
+ (f'<p class="sub">{html.escape(args.subtitle)}</p>' if args.subtitle else "")
+ (f'<p class="sub">{html.escape(subtitle)}</p>' if subtitle else "")
+ source_line
+ review_line
+ '</header><div class="layout">'
f'<nav class="rail" aria-label="Sections"><ol>{rail_html}</ol></nav>'
f"<main>{body}"
f"<main>{lifecycle_notice}{currency_notice}{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"
f"<span>{html.escape(source_note or 'generated from canonical source')}</span></footer>"
"</main></div></div></html>\n"
)
args.output.write_text(page)
print(f"{args.output}: {len(rail)} sections, {len(page)} bytes")
return page, meta, len(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()
page, _meta, section_count = render_page(
args.source,
title=args.title,
subtitle=args.subtitle,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(page, encoding="utf-8")
print(f"{args.output}: {section_count} sections, {len(page)} bytes")
return 0