diff --git a/.gitignore b/.gitignore index fdc57c2..194e5ae 100644 --- a/.gitignore +++ b/.gitignore @@ -44,9 +44,9 @@ Thumbs.db # Working notes and local tooling, not part of the published source .claude/ -docs/project-memory.md -docs/assistant-store-implementation-note.md -docs/assistant-ui-guide.md -docs/bias-impact-experiments-plan.md -docs/vault-credential-gate-frontend.md -docs/vault-credential-gate-tasks.md +# In docs/ only the user guide is published; notes, the scripts that build the guide and its +# screenshots stay local. +docs/* +!docs/user-guide.md +!docs/user-guide.pdf +!docs/user-guide.docx diff --git a/docs/render-user-guide.py b/docs/render-user-guide.py deleted file mode 100644 index 0d2d908..0000000 --- a/docs/render-user-guide.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env python3 -"""Render the user guide to a print-ready HTML page. - -From web/, to regenerate docs/user-guide.pdf after editing docs/user-guide.md: - - python3 docs/render-user-guide.py docs/user-guide.md /tmp/ug.html \ - "HumAIn Flow" "User Guide" "Version of 16 September 2026" - npx playwright pdf file:///tmp/ug.html docs/user-guide.pdf - -Deliberately not a general Markdown implementation: it handles the constructs the -guide actually uses, so the output is predictable and the print styling can be -tuned for it (page breaks per chapter, a real table of contents, no orphan -headings). Pandoc is not installed here and pulling one in for one document costs -more than it saves. -""" -import html -import re -import sys - -INLINE = [ - (re.compile(r"`([^`]+)`"), r"\1"), - (re.compile(r"\*\*([^*]+)\*\*"), r"\1"), - (re.compile(r"(?\1"), - (re.compile(r"\[([^\]]+)\]\(([^)]+)\)"), r'\1'), -] - - -def inline(text): - out = html.escape(text) - for pattern, replacement in INLINE: - out = pattern.sub(replacement, out) - return out - - -def convert(markdown): - lines = markdown.split("\n") - out, toc = [], [] - i = 0 - list_stack = [] - - def close_lists(to_depth=0): - while len(list_stack) > to_depth: - out.append(f"") - - while i < len(lines): - line = lines[i] - stripped = line.strip() - - if not stripped: - close_lists() - i += 1 - continue - - if stripped.startswith("```"): - close_lists() - i += 1 - block = [] - while i < len(lines) and not lines[i].strip().startswith("```"): - block.append(html.escape(lines[i])) - i += 1 - i += 1 - out.append("
" + "\n".join(block) + "
") - continue - - heading = re.match(r"^(#{1,4})\s+(.*)$", stripped) - if heading: - close_lists() - level = len(heading.group(1)) - text = heading.group(2) - anchor = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-") - # Chapters are h2 and sections h3, so the contents list is those two levels. - if 2 <= level <= 3: - toc.append((level - 1, text, anchor)) - out.append(f'{inline(text)}') - i += 1 - continue - - if stripped.startswith("|") and i + 1 < len(lines) and re.match(r"^\|[\s:|-]+\|$", lines[i + 1].strip()): - close_lists() - header = [c.strip() for c in stripped.strip("|").split("|")] - i += 2 - rows = [] - while i < len(lines) and lines[i].strip().startswith("|"): - rows.append([c.strip() for c in lines[i].strip().strip("|").split("|")]) - i += 1 - head = "".join(f"{inline(c)}" for c in header) - body = "".join("" + "".join(f"{inline(c)}" for c in r) + "" for r in rows) - out.append(f"{head}{body}
") - continue - - if stripped in ("---", "***"): - close_lists() - out.append('
') - i += 1 - continue - - bullet = re.match(r"^(\s*)[-*]\s+(.*)$", line) - number = re.match(r"^(\s*)\d+\.\s+(.*)$", line) - if bullet or number: - match = bullet or number - tag = "ul" if bullet else "ol" - depth = len(match.group(1)) // 2 + 1 - while len(list_stack) > depth: - out.append(f"") - while len(list_stack) < depth: - out.append(f"<{tag}>") - list_stack.append(tag) - out.append(f"
  • {inline(match.group(2))}
  • ") - i += 1 - continue - - if stripped.startswith("> "): - close_lists() - out.append(f'

    {inline(stripped[2:])}

    ') - i += 1 - continue - - close_lists() - paragraph = [stripped] - i += 1 - while i < len(lines) and lines[i].strip() and not re.match(r"^\s*([-*]|\d+\.|#|\||>|```)", lines[i]): - paragraph.append(lines[i].strip()) - i += 1 - out.append(f"

    {inline(' '.join(paragraph))}

    ") - - close_lists() - return "\n".join(out), toc - - -CSS = """ -@page { size: A4; margin: 18mm 16mm 20mm; } -* { box-sizing: border-box; } -body { font: 10.5pt/1.55 -apple-system, "Segoe UI", Helvetica, Arial, sans-serif; - color: #1e293b; margin: 0; } -h1 { font-size: 21pt; color: #0f172a; margin: 0 0 6pt; line-height: 1.25; } -h2 { font-size: 19pt; color: #0f172a; page-break-before: always; page-break-after: avoid; - border-top: 2px solid #2563eb; padding-top: 6pt; margin: 0 0 10pt; line-height: 1.25; } -h3 { font-size: 12.5pt; color: #1d4ed8; margin: 16pt 0 5pt; page-break-after: avoid; } -h4 { font-size: 10.8pt; color: #0f172a; margin: 12pt 0 3pt; page-break-after: avoid; } -p { margin: 0 0 7pt; orphans: 2; widows: 2; } -ul, ol { margin: 0 0 8pt; padding-left: 18pt; } -li { margin-bottom: 2.5pt; } -code { font-family: "SF Mono", Menlo, Consolas, monospace; font-size: 9pt; - background: #f1f5f9; border: 1px solid #e2e8f0; border-radius: 3px; padding: 0 3px; } -pre { background: #f8fafc; border: 1px solid #e2e8f0; border-left: 3px solid #94a3b8; - border-radius: 4px; padding: 8pt 10pt; overflow-x: auto; page-break-inside: avoid; } -pre code { background: none; border: none; padding: 0; font-size: 8.5pt; line-height: 1.45; } -table { width: 100%; border-collapse: collapse; margin: 0 0 10pt; font-size: 9.5pt; - page-break-inside: avoid; } -th { background: #eff6ff; color: #1e3a8a; text-align: left; font-weight: 600; } -th, td { border: 1px solid #cbd5e1; padding: 4pt 6pt; vertical-align: top; } -.note { border-left: 3px solid #f59e0b; background: #fffbeb; padding: 6pt 9pt; - margin: 0 0 8pt; page-break-inside: avoid; } -hr.page-break { border: 0; page-break-after: always; } -.cover { height: 247mm; display: flex; flex-direction: column; justify-content: center; } -.cover-title { font-size: 34pt; border: 0; padding: 0; } -.cover-sub { font-size: 13pt; color: #475569; margin-top: 2pt; } -.cover-meta { margin-top: 26pt; font-size: 10pt; color: #64748b; } -.toc { page-break-after: always; } -.toc-title { font-size: 19pt; color: #0f172a; border-top: 2px solid #2563eb; - padding-top: 6pt; margin: 0 0 10pt; } -.toc ol { list-style: none; padding-left: 0; counter-reset: chapter; } -.toc li.l1 { counter-increment: chapter; font-weight: 600; margin-top: 6pt; } -.toc li.l1::before { content: counter(chapter) ". "; color: #2563eb; } -.toc li.l2 { padding-left: 16pt; font-weight: 400; font-size: 9.5pt; color: #475569; } -.toc a { color: inherit; text-decoration: none; } -a { color: #1d4ed8; } -""" - - -def main(md_path, html_path, title, subtitle, meta): - # The cover carries the document title, so the file's own leading H1 would only repeat it. - source = re.sub(r"\A\s*#\s+.*\n", "", open(md_path).read()) - body, toc = convert(source) - items = "".join( - f'
  • {html.escape(text)}
  • ' - for level, text, anchor in toc - ) - page = f"""{html.escape(title)} - -

    {html.escape(title)}

    -
    {html.escape(subtitle)}
    -
    {html.escape(meta)}
    -
    Contents
      {items}
    -{body}""" - open(html_path, "w").write(page) - print(f"html written: {html_path} ({len(page)} bytes, {len(toc)} toc entries)") - - -if __name__ == "__main__": - main(*sys.argv[1:]) diff --git a/docs/user-guide.docx b/docs/user-guide.docx new file mode 100644 index 0000000..1535a64 Binary files /dev/null and b/docs/user-guide.docx differ diff --git a/docs/user-guide.pdf b/docs/user-guide.pdf index 2ba0ca7..fc14a6f 100644 Binary files a/docs/user-guide.pdf and b/docs/user-guide.pdf differ