Files
pablan/docs/authoring-templates.md
T
ProfessorNovaandClaude Opus 5 97dbff309c Pablan, as it stands
Self-hosted knowledge management for SMEs: a split-screen Markdown editor
whose sections an LLM refines while you write, and RAG question answering
over the documents that result. FastAPI + Postgres/pgvector on the back,
SvelteKit on the front, everything OpenAI-compatible and self-hostable.

Squashed into a single commit; the development history stays local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
2026-09-04 08:36:17 +02:00

12 KiB

Authoring templates

A template is not an interview guide. It is a Markdown skeleton — a starting document with headings the author fills in — plus a persona and optional per-section hints that steer the section-refinement model. Templates are declarative configuration, not code: a new document type (a machine write-up, a decision record, a process) is a new YAML file, not a new feature.

templates/ ships a catalog of blueprints that an admin adds from — see "Catalog and lifecycle" below; the templates table holds only what a customer actually uses, and every row in it is editable. The full template is stored in templates.config (JSONB) and validated on load against AuthoringTemplate (app/authoring/schema.py, schema version 1.0). The schema is versioned (version field) and expected to evolve after real use.

Format

id: prozess
name: "Ablauf: wie wir das machen"
version: "1.0"
kind: authoring
locale: de
description: >
  Ein wiederkehrender Ablauf, Schritt für Schritt — so, dass jemand anderes
  ihn allein schafft.

model:
  temperature: 0.4
  min_class_hint: "12b"        # UI warning if the endpoint is weaker

persona: |
  Du bist ein präziser Fachredakteur für Arbeitsanweisungen. Du schreibst
  sachlich, in vollständigen Sätzen, und machst aus einer Abfolge eine
  nummerierte Liste. Zahlen, Fristen, Systemnamen und Zuständigkeiten
  behältst du exakt bei und erfindest keine dazu.

title_template: "Neuer Ablauf"

skeleton: |
  ## Wann das gilt

  ## Schritt für Schritt

  ## Wenn es klemmt

  ## Wer zuständig ist

sections:
  - heading: "Wann das gilt"
    hint: >
      Der Auslöser: in welcher Situation dieser Ablauf greift, und wo er
      nicht gilt.
  - heading: "Schritt für Schritt"
    hint: >
      Die Schritte in ihrer Reihenfolge, jeder als eine Handlung — mit den
      Systemen, Formularen und Fristen, die dazugehören.
  - heading: "Wenn es klemmt"
    hint: >
      Die Sonderfälle und die Stellen, an denen es erfahrungsgemäß hakt.
  - heading: "Wer zuständig ist"
    hint: >
      Wer den Ablauf verantwortet und wen man bei Rückfragen anspricht.

metadata:
  visibility: department

The headings are the questions a colleague actually asks. That is the whole craft in a template: "Wann das gilt / Schritt für Schritt / Wenn es klemmt" gets filled in, "Worum es geht / Details / Was andere wissen müssen" does not — it is a blank page wearing a structure. An skeleton: "" with no sections is a legitimate template (notiz), and better than headings that ask for nothing in particular.

Fields

Field Meaning
id Config id, stable across edits; the catalog and the picker key on it. Not the row UUID.
name Human title in the picker.
version Revision string, raised by hand when the skeleton or hints change. Read as a string even when the YAML looks like a float (1.0).
kind Always authoring. Replaces the old mode: interview — it distinguishes an authoring template from any legacy config still shaped like an interview.
locale The language this template's content is written in (de | en | null), not a UI string. The picker lists templates in the reader's language first.
description One line shown in the picker and catalog.
model.temperature Sampling temperature for the refinement call (default 0.4).
model.min_class_hint Advisory model class (e.g. "12b"); the UI warns when the configured endpoint looks weaker.
persona The editor voice — how the refinement model should write. Injected as the system persona for every section of this document.
skeleton The Markdown the editor opens with: headings the author fills in. This IS the starting content, not a description of it. An empty skeleton yields a blank document.
sections Per-heading hints: [{heading, hint}].
title_template Draft title, with {{user.name}} and {{date}} substituted at creation.
metadata.visibility Default visibility of the resulting document (public | department | restricted), changeable in the editor before publishing.

How a template drives the editor

  1. Creating a draft. POST /api/documents {template_id} loads the AuthoringTemplate, renders the skeleton (render_skeleton, verbatim with a single trailing newline) and the title (render_title, with {{user.name}} / {{date}} substituted), and writes a draft document authored by the user (app/authoring/document.py). A draft is author-only and never indexed — see data-model.md.
  2. Writing. The editor opens on the skeleton. The author writes Markdown directly; Markdown is the source of truth (rule 1), so there is no derived render to watch.
  3. Section refinement. After a typing pause the client asks the model to mature the section at the cursor (POST /api/documents/{id}/refine, SSE). The whole document travels as prefix/suffix context, but the model regenerates only that section (FIM-style) so a large document is never re-emitted whole. The prompt (app/authoring/prompts.py, natural language — rule 7) is persona + the matching section hint + a fixed rule block ("return only the refined section, keep the heading, invent no facts").

sections[].heading must match a skeleton heading exactly. The server finds the section the cursor sits in (app/authoring/sections.py::active_section, sharing the heading/fence logic with rag/chunking.py), reads the heading text the section starts with, and looks up its hint by exact string match (AuthoringTemplate.hint_for). A hint whose heading does not appear in the skeleton simply never reaches the model. A section with no hint still gets refined — persona and the rule block are enough.

Language policy

Shipped template content — persona, hints, descriptions, title templates, skeleton headings — is German. Templates are product content for the German market, exactly like the fixture corpus, NOT UI copy. Schema keys, structure and section ids stay English. File naming is <id>.<locale>.yaml (prozess.de.yaml); a file with no locale suffix belongs to the default locale, so a customer can drop their own YAML in without learning the convention.

The catalog

templates/ ships blueprints for common SME situations. A blueprint is inert: it has no row in templates and cannot be used until an admin adds it.

id Purpose Visibility of the result Starter
notiz Anything at all — no skeleton, an empty document department yes
prozess How something is done here, step by step, with what snags department yes
stoerung What broke, what caused it, what fixed it department yes
person What someone does and what to ask them about public yes
anlage A machine: running it, maintaining it, its quirks department
entscheidung What was decided, why, and what follows department
projekt-debrief What a project taught, in three questions department

Two of these deserve their reasoning written down:

  • notiz has no skeleton on purpose. Three generic headings ("What this is about / Details / What others need to know") are what a blank page looks like when it is trying to be helpful, and nobody writes a document that way. Someone reaching for the open template already knows what they want to say.
  • person is written by the person themselves (the profile page starts it, api/account.py PERSONAL_BLUEPRINT) and is public: a directory that half the company cannot read answers nobody's "who knows about X?". It replaced an "onboarding" blueprint somebody else was supposed to fill in FOR a new colleague — nobody does that, and the colleague knows the answers.

A draft stays private to its author until they publish it, so anything sensitive can be removed first.

Catalog and lifecycle

A fresh instance seeds four starter blueprintsnotiz, prozess, stoerung, person (STARTER_TEMPLATE_IDS in app/template_catalog.py) — and only while the templates table is empty. They are the four occasions on which anyone actually writes something down: write it down, how we do this, what broke, who you are. Everything more specific — a machine, a decision, a project review — is a deliberate add from the catalog, because a picker of ten options is a picker nobody reads. seed_starter_templates is the ONLY automatic write to the table, and it runs exclusively against an empty table; a startup upsert would silently discard an admin's edits.

Nothing in templates is read-only. A template describes how a company documents its own knowledge, so the company owns it — including the seeded ones and every blueprint added later. Admins can edit the YAML, duplicate (fork gets <id>-kopie), and delete; deleting is safe because the catalog can always supply the blueprint again, and documents created from the template are independent and survive.

This is the opposite of the built-in help documents (app/help_import.py), which are re-imported on every start and refuse edits (409 builtin_readonly). The rule behind both: content that describes how Pablan works belongs to the product; content that describes how this company works belongs to the customer.

Adding a blueprint twice is refused (409 already_added) rather than overwriting — the second add would silently discard the admin's edits.

Versioning stays manual: the version string in the YAML identifies a revision, and the editor reminds the admin to raise it when the skeleton or hints change. There is no version history table — a template is content under the customer's control, not an audit trail.

Validation approach

Cheap schema validation before any UI work: create a draft from the template, write a rough section under each heading, and check that refinement matures the prose without inventing facts, keeps the heading, and answers in the section's language. A broken blueprint is logged and skipped on load rather than taking the catalog down (catalog_invalid). The refinement prompt is covered by an eval in tests/evals; extend it whenever the prompt or the rule block changes.

Open problem: where built-in content lives

Pablan ships two kinds of content it did not get from the customer: the help documents and the template catalog. The current split (help documents re-imported and locked, templates offered and owned) is a decision we are comfortable defending, but the surrounding questions are open:

  • How much should a fresh instance start with? We seed four templates and a handful of help documents. Should the seed be larger (a demo document set that shows what a good captured document looks like), configurable at install time, or nothing at all?
  • Where should the shipped content live? Today: templates/ and help/ as files in the repo, imported at startup. Files-in-repo is right for two developers; it is not obviously right once the catalog has thirty entries in several languages.
  • How does catalog content reach existing instances? A blueprint improved in a later release does not reach anyone who already added it, by design. There is no "update available" signal — a diff view against the current blueprint is the obvious feature, and it is not built.
  • Should deleting the last template be possible? It is today. A member who opens "Capture knowledge" in that state sees an empty picker.

This is deliberately recorded, not solved. Decide it with a real customer install in front of us rather than from first principles.