<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-GB">
	<id>https://yusupov.cloud/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Mvuijlst</id>
	<title>Yusupov&#039;s House - User contributions [en-gb]</title>
	<link rel="self" type="application/atom+xml" href="https://yusupov.cloud/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Mvuijlst"/>
	<link rel="alternate" type="text/html" href="https://yusupov.cloud/wiki/Special:Contributions/Mvuijlst"/>
	<updated>2026-05-16T10:04:48Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.44.0</generator>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Inrik_%C3%9Cksk%C3%BCla&amp;diff=441</id>
		<title>Inrik Üksküla</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Inrik_%C3%9Cksk%C3%BCla&amp;diff=441"/>
		<updated>2026-05-06T13:37:51Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Inrik Üksküla&#039;&#039;&#039; (born 1979) is an Estonian linguist and semiotician, currently unaffiliated. He studied theoretical linguistics and logic at the University of Tartu, where he subsequently taught for several years before leaving academic employment in 2018. He has described his departure as voluntary, citing a preference for &amp;quot;work that doesn&#039;t require committee approval.&amp;quot; He has since published independently, maintaining a particular interest in formal models of small, restricted corpora.&lt;br /&gt;
&lt;br /&gt;
Üksküla&#039;s approach is characterised by a willingness to develop competing hypotheses in parallel rather than committing prematurely to a single reading. He has credited the Tartu tradition of semiotics associated with Juri Lotman as an influence on his view that a corpus should be allowed to generate its own interpretive possibilities before external frameworks are applied.&lt;br /&gt;
&lt;br /&gt;
==Work==&lt;br /&gt;
Üksküla&#039;s preprint &amp;quot;The Clan of Zagi: Numeric Calculus or Genealogical Primer? A Structural Analysis of the Kristiansen Cuneiform Corpus&amp;quot; (2024) analysed 104 short, highly formulaic sentences in the [[Zagi Tablets]]—a corpus of clay tablets inscribed in the [[Kristiansen coding system]] and headed in Akkadian as &#039;&#039;imri Zagi-ak&#039;&#039; (&amp;quot;the clan of Zagi&amp;quot;). The study identified a small set of structural pivots and a paradigm of four ordinal or cardinal markers, and developed two interpretive hypotheses: a numeric calculus reading, in which the corpus functions as a didactic arithmetic system, and a genealogical primer reading, in which the same structures encode a kinship model for the named clan. Üksküla evaluated both against the corpus and concluded that the available evidence did not decisively favour either, proposing a hybrid in which formal numeric notation is used to model the internal structure of a specific kin-group.&lt;br /&gt;
&lt;br /&gt;
The Akkadian heading &#039;&#039;imri Zagi-ak&#039;&#039; attracted particular attention as the first named attribution in the broader [[Kristiansen corpus]]. Üksküla noted that the name Zagi does not appear in any known Akkadian administrative context and may be a transliteration of a name from an unrelated language.&lt;br /&gt;
&lt;br /&gt;
[[Ginevra Rubergskier|Rubergskier]] has observed that the AND_PLUS element in the Zagi corpus shares its collocational profile with the addition operator she identified in the [[Dozenal Primer Inscription]], a correspondence Üksküla discusses briefly in his preprint. [[Marie Roelandt]] subsequently developed Üksküla&#039;s genealogical hypothesis further in two posts on her blog [[Klema Field Notes]], arguing that the UNIT sign should be read as the kin term CHILD.&lt;br /&gt;
&lt;br /&gt;
==See also==&lt;br /&gt;
* [[Zagi Tablets]]&lt;br /&gt;
* [[Scapula Glyph Inscription]]&lt;br /&gt;
* [[Jan-Tage Kristiansen]]&lt;br /&gt;
* [[Marie Roelandt]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Researchers]][[Category:Epigraphy]][[Category:Kristiansen corpus]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=A_Cabinet_of_Brief_Curiosities&amp;diff=415</id>
		<title>A Cabinet of Brief Curiosities</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=A_Cabinet_of_Brief_Curiosities&amp;diff=415"/>
		<updated>2026-04-23T15:24:51Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| 01_name         = A Cabinet of Brief Curiosities&lt;br /&gt;
| 02_url          = https://acbc.yusupov.cloud&lt;br /&gt;
| 03_developer    = Michel Vuijlsteke&lt;br /&gt;
| 04_released     = 2025&lt;br /&gt;
| 05_genre        = AI-generated short-fiction application&lt;br /&gt;
| 06_language     = Python&lt;br /&gt;
| 07_framework    = [[Flask]] 3.0&lt;br /&gt;
| 08_license      = MIT&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;A Cabinet of Brief Curiosities&#039;&#039;&#039; (abbreviated &#039;&#039;&#039;acbc&#039;&#039;&#039;) is a web application hosted at &amp;lt;code&amp;gt;acbc.yusupov.cloud&amp;lt;/code&amp;gt; that generates illustrated three-sentence short stories in the style of [[H. P. Lovecraft]]. Each story is composed by a large language model from a structured set of randomised &amp;quot;knobs&amp;quot; and optional user-supplied seed words, and is paired with a black-and-white illustration produced by a generative image model and styled to resemble a 19th-century engraved book plate. The site&#039;s rotating tagline — drawn every thirty minutes from a pool of forty-five variants — frames the act of generation in deliberately archaic terms; the canonical opening reads &amp;quot;Dredge slivers of impossible worlds from the black gulfs of imagination and press them into trembling mortal words.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Flask 3.0 with [[SQLite]] as its database, accessed through [[SQLAlchemy]].&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt in the project repository &amp;lt;/ref&amp;gt; Authentication is handled by Flask-Login and cross-site request forgery protection by Flask-WTF. It is deployed on an Ubuntu VPS behind [[Nginx]] with [[Gunicorn]] running over a Unix socket under a dedicated &amp;lt;code&amp;gt;django&amp;lt;/code&amp;gt; system user, supervised by systemd. Additional dependencies include the [[OpenAI]] Python client for both text and image generation, [[Pillow (imaging library)|Pillow]] for image post-processing, [[python-dotenv]] for environment configuration, and [[httpx]] as the underlying HTTP transport. The front end is rendered from Jinja templates using a small Bootstrap-derived stylesheet (&amp;lt;code&amp;gt;static/style.css&amp;lt;/code&amp;gt;) and is registered as an installable progressive web app via &amp;lt;code&amp;gt;static/manifest.json&amp;lt;/code&amp;gt; and a service worker (&amp;lt;code&amp;gt;static/sw.js&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
The database contains two tables.&lt;br /&gt;
&lt;br /&gt;
=== User ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; stores a single bootstrapped administrator account with an e-mail address, a Werkzeug password hash, and a creation timestamp. Sign-ups are disabled at the route level: the &amp;lt;code&amp;gt;/signup&amp;lt;/code&amp;gt; endpoint flashes a notice and redirects to login. The administrator is created on application start from the &amp;lt;code&amp;gt;ADMIN_EMAIL&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ADMIN_PASSWORD&amp;lt;/code&amp;gt; environment variables if no matching row exists.&lt;br /&gt;
&lt;br /&gt;
=== Story ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;Story&amp;lt;/code&amp;gt; stores the generated short fiction:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;id&#039;&#039; (primary key), an optional &#039;&#039;user_id&#039;&#039; foreign key (null for guest submissions), the three-sentence &#039;&#039;story_text&#039;&#039;, the optional &#039;&#039;mood&#039;&#039;, &#039;&#039;nouns&#039;&#039; and &#039;&#039;verbs&#039;&#039; seeds supplied by the requester, an optional &#039;&#039;image_path&#039;&#039; (relative to the static folder, e.g. &amp;lt;code&amp;gt;images/story_42.png&amp;lt;/code&amp;gt;), the originating &#039;&#039;ip_address&#039;&#039; (indexed), an indexed &#039;&#039;created_at&#039;&#039; timestamp, and a JSON-encoded &#039;&#039;knobs_json&#039;&#039; column that records the parameter set the model was given.&lt;br /&gt;
&lt;br /&gt;
The schema is created on first run by SQLAlchemy. A small startup routine inspects the live table and issues an &amp;lt;code&amp;gt;ALTER TABLE&amp;lt;/code&amp;gt; if a newer column (such as &amp;lt;code&amp;gt;knobs_json&amp;lt;/code&amp;gt;) is missing, providing a lightweight migration path without an external migration framework. The same routine sets &amp;lt;code&amp;gt;PRAGMA journal_mode=WAL&amp;lt;/code&amp;gt; on the SQLite database to allow concurrent reads while a background image-generation thread writes.&lt;br /&gt;
&lt;br /&gt;
== Story generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Story generation is initiated by a POST to &amp;lt;code&amp;gt;/generate&amp;lt;/code&amp;gt; and proceeds through several deterministic-and-random stages before the OpenAI call.&lt;br /&gt;
&lt;br /&gt;
=== Seeds and knobs ===&lt;br /&gt;
&lt;br /&gt;
A request may carry up to three optional free-text fields — &#039;&#039;noun&#039;&#039;, &#039;&#039;verb&#039;&#039;, and &#039;&#039;mood&#039;&#039; — which are passed to the model as &amp;quot;seeds&amp;quot; and validated for presence in the output. Around the seeds, the application constructs a compact JSON &#039;&#039;knobs&#039;&#039; object that nudges the model along several axes:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Dimension !! Pool size !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| Perspective || 4 || first, second, third, omniscient&lt;br /&gt;
|-&lt;br /&gt;
| Structure || 8 || Discovery → Investigation → Revelation; Object → Rumor → Catastrophe; Signal → Interpretation → Realization; …&lt;br /&gt;
|-&lt;br /&gt;
| Time || 8 (forced &#039;&#039;ambiguous&#039;&#039;) || Victorian era, distant past, mythic period, interwar, near-future of obsolete technology&lt;br /&gt;
|-&lt;br /&gt;
| Location || 50 || lighthouse, salt marsh, foundry, signal box, scriptorium, observatory, shipbreaker&#039;s yard, …&lt;br /&gt;
|-&lt;br /&gt;
| Situation || 32 || during a storm, at low tide, during a blackout, on the eve of demolition, while the clock refuses to strike, …&lt;br /&gt;
|-&lt;br /&gt;
| Lexical palette || 53 || nautical, horological, astronomical, archival, cartographic, mycological, glaciological, heraldic, …&lt;br /&gt;
|-&lt;br /&gt;
| Style constraint || 5 || include exactly one short line of dialogue; include a question; avoid the words &amp;quot;shadow&amp;quot; and &amp;quot;dim&amp;quot;; …&lt;br /&gt;
|-&lt;br /&gt;
| Sentence pattern || 3 || short → long → medium; medium → short → long; long → medium → short&lt;br /&gt;
|-&lt;br /&gt;
| Theme (only when no seeds) || 18 || forbidden knowledge, cosmic entities, inherited curses, mathematical theorems, astronomical observations, …&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The knobs object always carries the seeds and at most two additional non-seed dimensions. Three of the knobs — &#039;&#039;perspective&#039;&#039;, &#039;&#039;structure&#039;&#039;, and &#039;&#039;time&#039;&#039; — are selected &#039;&#039;deterministically&#039;&#039; from a SHA-256 hash of a salt composed of the current 30-minute time bucket, the requester&#039;s IP address, and the seed words; this guarantees that the same client requesting the same seeds inside the same half-hour window draws the same priority knobs. The previous story&#039;s knobs for that IP are read from the &amp;lt;code&amp;gt;knobs_json&amp;lt;/code&amp;gt; column (falling back to the JSONL choices log for older rows) and avoided where possible. &#039;&#039;Time&#039;&#039; is hard-pinned to &amp;lt;code&amp;gt;ambiguous&amp;lt;/code&amp;gt; to suppress dated or era-specific references in the output. Lexicon and constraint values are sampled with the regular pseudo-random generator. Once all priority knobs are forced into the object, any non-priority extras are dropped at random until at most two non-seed knobs remain.&lt;br /&gt;
&lt;br /&gt;
=== System rules ===&lt;br /&gt;
&lt;br /&gt;
The system prompt is composed entirely of positive instructions: it asks for exactly three sentences in a grave Lovecraftian register, with no titles, lists, numbering, or blank lines, and embeds two short hand-written exemplar stories so the model can imitate length, cadence, and concreteness rather than reason in the negative. The application then validates the response against a separate banned-word list rather than naming forbidden terms in the prompt itself, in order to avoid the well-documented tendency of language models to reproduce items they are explicitly told to avoid.&lt;br /&gt;
&lt;br /&gt;
=== Model selection ===&lt;br /&gt;
&lt;br /&gt;
The default text model is GPT-4o (configurable via &amp;lt;code&amp;gt;OPENAI_MODEL&amp;lt;/code&amp;gt;) with a configurable fallback (&amp;lt;code&amp;gt;OPENAI_FALLBACK_MODEL&amp;lt;/code&amp;gt;, also GPT-4o by default). A helper function inspects the installed OpenAI SDK at request time: if the configured model is in the &amp;lt;code&amp;gt;gpt-5&amp;lt;/code&amp;gt; family but the SDK does not expose the Responses API, the call silently falls back to a chat-compatible model. When the Responses API is available it is preferred for all models, with the system rules supplied via the explicit &amp;lt;code&amp;gt;instructions&amp;lt;/code&amp;gt; field. For &amp;lt;code&amp;gt;gpt-5&amp;lt;/code&amp;gt; family models the call additionally injects a &amp;lt;code&amp;gt;reasoning&amp;lt;/code&amp;gt; object (defaulting to &#039;&#039;effort: low&#039;&#039; but configurable via &amp;lt;code&amp;gt;OPENAI_REASONING_EFFORT&amp;lt;/code&amp;gt;), drops the &amp;lt;code&amp;gt;temperature&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;top_p&amp;lt;/code&amp;gt; parameters (which those models reject), and grants a higher &amp;lt;code&amp;gt;max_output_tokens&amp;lt;/code&amp;gt; budget (1600 by default, raised to 2400 on retry) to absorb reasoning-token consumption. For non-reasoning models, &#039;&#039;temperature&#039;&#039; is sampled uniformly from a narrow [0.78, 0.92] interval and &#039;&#039;top_p&#039;&#039; is fixed at 0.9, deliberately tighter than typical creative-writing defaults to keep tone consistent. Where the Responses API is unavailable, the call degrades to Chat Completions.&lt;br /&gt;
&lt;br /&gt;
=== Validation and retry ===&lt;br /&gt;
&lt;br /&gt;
After the call returns, the output is validated by a checker that enforces:&lt;br /&gt;
&lt;br /&gt;
* Exactly three non-empty lines.&lt;br /&gt;
* No line beginning with any of a list of banned opening prefixes (&amp;quot;In the&amp;quot;, &amp;quot;Beneath&amp;quot;, &amp;quot;Under the&amp;quot;, &amp;quot;Within the&amp;quot;, and so on).&lt;br /&gt;
* Absence of a curated list of overused or out-of-register words and phrases (&amp;quot;eldritch&amp;quot;, &amp;quot;cyclopean&amp;quot;, &amp;quot;loathsome&amp;quot;, any inflection of &#039;&#039;tentacle&#039;&#039;, and similar).&lt;br /&gt;
* Absence of anachronisms unsuited to the desired ambiguous-historical tone (kilometres, kilograms, GPS coordinates, e-mail addresses, four-digit years, references to Wi-Fi).&lt;br /&gt;
* A per-sentence word count in the [8, 60] range.&lt;br /&gt;
* One terminal punctuation mark per line.&lt;br /&gt;
&lt;br /&gt;
If the request supplied seeds, a second pass verifies that the noun appears in the text — with allowance for irregular plurals — and that the verb appears in any common inflection.&lt;br /&gt;
&lt;br /&gt;
A separate near-duplicate check computes a 64-bit SimHash of the candidate text and rejects it if the Hamming distance to any of the most recent fifty stored stories is below a small threshold, so the corpus does not collapse into thematic loops.&lt;br /&gt;
&lt;br /&gt;
When validation fails, a single targeted retry is issued with a corrective prompt that quotes the failing lines back at the model verbatim, names the specific failures, lowers the temperature slightly, and reuses the same knobs JSON. The retry result is accepted if it passes, or if it fails with strictly fewer issues than the original. If the model call itself fails — including the specific case where a &amp;lt;code&amp;gt;gpt-5&amp;lt;/code&amp;gt; response is marked &#039;&#039;incomplete&#039;&#039; due to &amp;lt;code&amp;gt;max_output_tokens&amp;lt;/code&amp;gt; — the pipeline retries once with a higher token budget, then attempts the configured fallback model, and finally falls back to a fixed three-sentence placeholder. All failures surface to the user as Flask flash messages with appropriate severity.&lt;br /&gt;
&lt;br /&gt;
=== Persistence and logging ===&lt;br /&gt;
&lt;br /&gt;
The validated story text is written to a new &amp;lt;code&amp;gt;Story&amp;lt;/code&amp;gt; row together with the seeds, the originating IP, and the JSON-encoded knobs object. Image generation is then dispatched in a background daemon thread so the HTTP response returns immediately. Two structured logs are appended in JSON-Lines format under the Flask instance folder:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;instance/choices.log.jsonl&amp;lt;/code&amp;gt; records, per story, the seeds, the knobs JSON, the selected temperature and top_p, the model actually used, the configured model, and the validation outcome (including reasons and whether a retry was attempted).&lt;br /&gt;
* &amp;lt;code&amp;gt;instance/image.log.jsonl&amp;lt;/code&amp;gt; records each image generation attempt with status (&#039;&#039;success&#039;&#039;, &#039;&#039;retry&#039;&#039;, &#039;&#039;failed&#039;&#039;, &#039;&#039;skipped&#039;&#039;), error text, attempt count, image size, image model, and post-processing diagnostics such as the standard deviation of the output&#039;s luminance.&lt;br /&gt;
&lt;br /&gt;
Both logs are rotated to a single &#039;&#039;&amp;lt;file&amp;gt;.1&#039;&#039; backup once they exceed a configurable byte threshold (default 2 MiB) so that long-running instances cannot fill the disk.&lt;br /&gt;
&lt;br /&gt;
== Image generation ==&lt;br /&gt;
&lt;br /&gt;
Each story is paired with a square 1024×1024 illustration generated through the OpenAI image API.&lt;br /&gt;
&lt;br /&gt;
=== Style library ===&lt;br /&gt;
&lt;br /&gt;
The application maintains a curated library of fifteen monochrome illustration styles, each anchored to one or more named historical artists or movements — for example wood engravings in the manner of Gustave Doré or Thomas Bewick, pen-and-ink in the manner of Aubrey Beardsley or Edward Gorey, drypoint in the manner of Käthe Kollwitz, aquatint in the manner of Goya&#039;s &#039;&#039;Caprichos&#039;&#039;, a Victorian scientific plate after Ernst Haeckel, relief woodcut after Lynd Ward, silhouette cuts after Lotte Reiniger, and symbolist pen-and-ink after Alfred Kubin. Anchoring each entry to a real reference reliably moves the model closer to the desired register. The style for a given story is selected by hashing its primary key, so adjacent stories on the home grid do not visually collide and a regenerated image for the same story can vary by perturbing the salt. A legacy global counter file at &amp;lt;code&amp;gt;instance/image_style_cycle.txt&amp;lt;/code&amp;gt; is retained for callers without a story id.&lt;br /&gt;
&lt;br /&gt;
=== Prompt construction ===&lt;br /&gt;
&lt;br /&gt;
To keep the image prompt concrete enough for a diffusion model to act on, the three-sentence story is first reduced to a one- or two-sentence visual scene description by a cheap secondary call to a smaller model (&amp;lt;code&amp;gt;OPENAI_SCENE_MODEL&amp;lt;/code&amp;gt;, default &amp;lt;code&amp;gt;gpt-4o-mini&amp;lt;/code&amp;gt;); the helper names a single subject, a single setting, one source of light, and one telling object, and discards mood words and metaphor. The final image prompt is then assembled from a hard full-bleed directive, the selected style sentence, the scene description, and an avoid clause that explicitly forbids colour, painterly shading, photorealism, captions, watermarks, signatures, and any kind of border, frame, or decorative edge.&lt;br /&gt;
&lt;br /&gt;
=== Image API and retries ===&lt;br /&gt;
&lt;br /&gt;
The configured image model defaults to &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt;; common typos such as &amp;lt;code&amp;gt;gpt-image-1.5&amp;lt;/code&amp;gt; are normalised back to &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt; at start-up. The generation size is configurable through &amp;lt;code&amp;gt;OPENAI_IMAGE_GEN_SIZE&amp;lt;/code&amp;gt; (default 1024×1024) and the post-processed final size is fixed at 1024×1024. Each generation tolerates up to three attempts (configurable via &amp;lt;code&amp;gt;IMAGE_RETRIES&amp;lt;/code&amp;gt;) with exponential backoff starting at 1.5 seconds and capped at 10 seconds. If all attempts fail — or if a returned image is blank or near-uniform — a small inline SVG placeholder is written to disk and recorded as the story&#039;s image path so the front end always has something to display. A few legacy stories carry a one-pixel GIF placeholder of the same purpose; the application detects either by the presence of the substring &amp;lt;code&amp;gt;_placeholder.&amp;lt;/code&amp;gt; in the path.&lt;br /&gt;
&lt;br /&gt;
=== Post-processing ===&lt;br /&gt;
&lt;br /&gt;
Successfully returned PNG bytes are passed through a Pillow-based post-processor that:&lt;br /&gt;
&lt;br /&gt;
* Estimates the background colour from the four corner patches.&lt;br /&gt;
* Computes a difference image against a uniform background of that colour, raises its contrast, and finds the bounding box of meaningful content.&lt;br /&gt;
* Crops away any uniform border of more than ten pixels on any side, padded by two pixels to avoid clipping ink.&lt;br /&gt;
* Resizes the result back to the final size using Lanczos resampling and re-encodes as optimised PNG.&lt;br /&gt;
* Computes the standard deviation of the resulting image&#039;s luminance; if the value falls below a small threshold, the image is treated as blank and the generation attempt is failed so a retry can be issued.&lt;br /&gt;
&lt;br /&gt;
If Pillow is unavailable or any step fails, the original bytes are written through unchanged.&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Home ===&lt;br /&gt;
&lt;br /&gt;
The home page (&amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt;) renders the generation form together with a gallery of the five most recent stories that successfully produced an image. For unauthenticated visitors, the form is hidden and replaced by a rotating notice (one of twenty-two phrasings, selected from a SHA-256 hash of the visitor&#039;s IP and a six-hour bucket) when the visitor has already generated a story in the last 24 hours or when the site-wide guest cap has been reached. A footer-level statistics block displays the number of guest stories in the last 24 hours, the number of stories created by signed-in users in the same window, the all-time total, and the time elapsed since the most recent story.&lt;br /&gt;
&lt;br /&gt;
=== Story detail ===&lt;br /&gt;
&lt;br /&gt;
Each story is reachable at &amp;lt;code&amp;gt;/story/&amp;lt;id&amp;gt;&amp;lt;/code&amp;gt; and is publicly viewable regardless of authorship. The page presents the three sentences alongside the illustration; for the administrator, controls are exposed to regenerate the image (&amp;lt;code&amp;gt;/regenerate-image/&amp;lt;id&amp;gt;&amp;lt;/code&amp;gt;) or delete the story (&amp;lt;code&amp;gt;/story/&amp;lt;id&amp;gt;/delete&amp;lt;/code&amp;gt;). A polling endpoint at &amp;lt;code&amp;gt;/api/story/&amp;lt;id&amp;gt;/status&amp;lt;/code&amp;gt; returns a JSON document indicating whether a real image has yet been written, the URL of the image (or placeholder), and a flag distinguishing the two; the front-end script polls with exponential backoff capped at eight seconds, gives up after a hard timeout, and writes status updates into an &amp;lt;code&amp;gt;aria-live&amp;lt;/code&amp;gt; region for screen-reader accessibility. The story page also overrides the base template&#039;s [[Open Graph protocol|Open Graph]] block to advertise per-story metadata, including the generated illustration, when the page is shared on social media.&lt;br /&gt;
&lt;br /&gt;
=== Archive ===&lt;br /&gt;
&lt;br /&gt;
The archive (&amp;lt;code&amp;gt;/archive&amp;lt;/code&amp;gt;) is a paginated chronological listing of all stories, nine per page, with previous/next navigation. There is no per-user filter — all stories are visible. The archive page sets &amp;lt;code&amp;gt;&amp;amp;lt;meta name=&amp;quot;robots&amp;quot; content=&amp;quot;noindex,follow&amp;quot;&amp;amp;gt;&amp;lt;/code&amp;gt; to keep the long tail out of search-engine indexes while leaving individual story pages indexable.&lt;br /&gt;
&lt;br /&gt;
=== Health endpoint ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;/healthz&amp;lt;/code&amp;gt; returns a small JSON document and an HTTP 200 when the application is fully configured and able to query the database, or HTTP 503 when the OpenAI API key is not set or the database round-trip fails. It is intended for use by uptime monitors and load balancers.&lt;br /&gt;
&lt;br /&gt;
=== Authentication ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; accepts only the e-mail address configured in &amp;lt;code&amp;gt;ADMIN_EMAIL&amp;lt;/code&amp;gt; and verifies the password against the stored Werkzeug hash. &amp;lt;code&amp;gt;/logout&amp;lt;/code&amp;gt; ends the session. &amp;lt;code&amp;gt;/signup&amp;lt;/code&amp;gt; is intentionally disabled. All form-bearing pages carry a CSRF token rendered from the Flask-WTF helper, and an expired token is recovered with a flash message and a redirect rather than an HTTP error.&lt;br /&gt;
&lt;br /&gt;
=== Tagline rotation ===&lt;br /&gt;
&lt;br /&gt;
The base template injects a &amp;quot;current tagline&amp;quot; string into every response. The selection is deterministic on the current 30-minute interval since the Unix epoch: the interval timestamp is used as a seed for the standard library random module, which then picks one of forty-five variant phrasings. All visitors served within the same half-hour see the same tagline; the tagline rotates without any database state.&lt;br /&gt;
&lt;br /&gt;
== Rate limiting ==&lt;br /&gt;
&lt;br /&gt;
Rate limiting is enforced for unauthenticated visitors only:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Per-IP daily limit&#039;&#039;&#039;: any IP that has produced a story within the last 24 hours is blocked from generating another (one story per visitor per day).&lt;br /&gt;
* &#039;&#039;&#039;Site-wide guest cap&#039;&#039;&#039;: the total number of guest-authored stories in the last 24 hours must not exceed &amp;lt;code&amp;gt;GUEST_DAILY_CAP&amp;lt;/code&amp;gt; (default 24). When the cap is reached, the form is hidden for all guests until older stories age out.&lt;br /&gt;
&lt;br /&gt;
Both limits are evaluated at form render time (to hide the form) and at form submission time (to short-circuit the POST with a flashed warning and a redirect). Authenticated users have no rate limits and no per-IP enforcement.&lt;br /&gt;
&lt;br /&gt;
The originating IP for limit accounting and for the &amp;lt;code&amp;gt;Story.ip_address&amp;lt;/code&amp;gt; column is read from the &amp;lt;code&amp;gt;X-Forwarded-For&amp;lt;/code&amp;gt; request header only when the immediate peer is in a configurable trusted-proxy allow-list (&amp;lt;code&amp;gt;TRUSTED_PROXIES&amp;lt;/code&amp;gt;, default loopback); otherwise the application uses &amp;lt;code&amp;gt;request.remote_addr&amp;lt;/code&amp;gt;. This prevents trivial spoofing of the originating address when the application is exposed without a reverse proxy in front of it.&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
The production deployment is an Ubuntu VPS running a systemd unit (&amp;lt;code&amp;gt;acbc.service&amp;lt;/code&amp;gt;) that launches Gunicorn with three workers, bound to a Unix domain socket under the project directory. Nginx terminates HTTPS (provisioned by [[Let&#039;s Encrypt]] via Certbot), serves the &amp;lt;code&amp;gt;static/&amp;lt;/code&amp;gt; directory directly with a one-year immutable cache header, and proxies all other requests to the Gunicorn socket. The service runs as the &amp;lt;code&amp;gt;django&amp;lt;/code&amp;gt; system user out of &amp;lt;code&amp;gt;/home/django/acbc&amp;lt;/code&amp;gt;. The application reads its configuration from environment variables loaded via python-dotenv.&lt;br /&gt;
&lt;br /&gt;
== Security and authorisation ==&lt;br /&gt;
&lt;br /&gt;
* All POST endpoints require a CSRF token issued by Flask-WTF.&lt;br /&gt;
* Sign-ups are disabled; only the bootstrapped administrator account can authenticate.&lt;br /&gt;
* Login attempts for any e-mail other than &amp;lt;code&amp;gt;ADMIN_EMAIL&amp;lt;/code&amp;gt; are rejected without a database query.&lt;br /&gt;
* The image-deletion helper refuses to remove any path that does not begin with &amp;lt;code&amp;gt;images/&amp;lt;/code&amp;gt; or that, after canonicalisation, falls outside the configured upload folder, providing protection against path-traversal in stored data.&lt;br /&gt;
* Story deletion requires either the administrator session or ownership of the row; image regeneration requires the administrator session.&lt;br /&gt;
* The story-status JSON endpoint returns no user-identifying information beyond the public fields rendered on the story page.&lt;br /&gt;
* The originating-IP determination only honours &amp;lt;code&amp;gt;X-Forwarded-For&amp;lt;/code&amp;gt; from trusted proxies.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
* [[Flask (web framework)]]&lt;br /&gt;
* [[H. P. Lovecraft]]&lt;br /&gt;
* [[Computational creativity]]&lt;br /&gt;
* [[Generative artificial intelligence]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=A_Cabinet_of_Brief_Curiosities&amp;diff=414</id>
		<title>A Cabinet of Brief Curiosities</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=A_Cabinet_of_Brief_Curiosities&amp;diff=414"/>
		<updated>2026-04-23T15:23:51Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| 01_name         = A Cabinet of Brief Curiosities&lt;br /&gt;
| 02_url          = https://acbc.yusupov.cloud&lt;br /&gt;
| 03_developer    = Michel Vuijlsteke&lt;br /&gt;
| 04_released     = 2025&lt;br /&gt;
| 05_genre        = AI-generated short-fiction application&lt;br /&gt;
| 06_language     = Python&lt;br /&gt;
| 07_framework    = [[Flask]] 3.0&lt;br /&gt;
| 08_license      = MIT&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;A Cabinet of Brief Curiosities&#039;&#039;&#039; (abbreviated &#039;&#039;&#039;acbc&#039;&#039;&#039;) is a web application hosted at &amp;lt;code&amp;gt;acbc.yusupov.cloud&amp;lt;/code&amp;gt; that generates illustrated three-sentence short stories in the style of [[H. P. Lovecraft]]. Each story is composed by a large language model from a structured set of randomised &amp;quot;knobs&amp;quot; and optional user-supplied seed words, and is paired with a black-and-white illustration produced by a generative image model and styled to resemble a 19th-century engraved book plate. The site&#039;s rotating tagline — drawn every thirty minutes from a pool of forty-five variants — frames the act of generation in deliberately archaic terms; the canonical opening reads &amp;quot;Dredge slivers of impossible worlds from the black gulfs of imagination and press them into trembling mortal words.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Flask 3.0 with [[SQLite]] as its database, accessed through [[SQLAlchemy]] (Flask-SQLAlchemy 3.1).&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt in the project repository pins Flask 3.0.3, Flask-Login 0.6.3, Flask-SQLAlchemy 3.1.1, Flask-WTF ≥1.2, python-dotenv 1.0.1, openai ≥1.50, httpx 0.27.2, and Pillow ≥10.&amp;lt;/ref&amp;gt; Authentication is handled by Flask-Login and cross-site request forgery protection by Flask-WTF. It is deployed on an Ubuntu VPS behind [[Nginx]] with [[Gunicorn]] running over a Unix socket under a dedicated &amp;lt;code&amp;gt;django&amp;lt;/code&amp;gt; system user, supervised by systemd. Additional dependencies include the [[OpenAI]] Python client for both text and image generation, [[Pillow (imaging library)|Pillow]] for image post-processing, [[python-dotenv]] for environment configuration, and [[httpx]] as the underlying HTTP transport. The front end is rendered from Jinja templates using a small Bootstrap-derived stylesheet (&amp;lt;code&amp;gt;static/style.css&amp;lt;/code&amp;gt;) and is registered as an installable progressive web app via &amp;lt;code&amp;gt;static/manifest.json&amp;lt;/code&amp;gt; and a service worker (&amp;lt;code&amp;gt;static/sw.js&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
The database contains two tables.&lt;br /&gt;
&lt;br /&gt;
=== User ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; stores a single bootstrapped administrator account with an e-mail address, a Werkzeug password hash, and a creation timestamp. Sign-ups are disabled at the route level: the &amp;lt;code&amp;gt;/signup&amp;lt;/code&amp;gt; endpoint flashes a notice and redirects to login. The administrator is created on application start from the &amp;lt;code&amp;gt;ADMIN_EMAIL&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ADMIN_PASSWORD&amp;lt;/code&amp;gt; environment variables if no matching row exists.&lt;br /&gt;
&lt;br /&gt;
=== Story ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;Story&amp;lt;/code&amp;gt; stores the generated short fiction:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;id&#039;&#039; (primary key), an optional &#039;&#039;user_id&#039;&#039; foreign key (null for guest submissions), the three-sentence &#039;&#039;story_text&#039;&#039;, the optional &#039;&#039;mood&#039;&#039;, &#039;&#039;nouns&#039;&#039; and &#039;&#039;verbs&#039;&#039; seeds supplied by the requester, an optional &#039;&#039;image_path&#039;&#039; (relative to the static folder, e.g. &amp;lt;code&amp;gt;images/story_42.png&amp;lt;/code&amp;gt;), the originating &#039;&#039;ip_address&#039;&#039; (indexed), an indexed &#039;&#039;created_at&#039;&#039; timestamp, and a JSON-encoded &#039;&#039;knobs_json&#039;&#039; column that records the parameter set the model was given.&lt;br /&gt;
&lt;br /&gt;
The schema is created on first run by SQLAlchemy. A small startup routine inspects the live table and issues an &amp;lt;code&amp;gt;ALTER TABLE&amp;lt;/code&amp;gt; if a newer column (such as &amp;lt;code&amp;gt;knobs_json&amp;lt;/code&amp;gt;) is missing, providing a lightweight migration path without an external migration framework. The same routine sets &amp;lt;code&amp;gt;PRAGMA journal_mode=WAL&amp;lt;/code&amp;gt; on the SQLite database to allow concurrent reads while a background image-generation thread writes.&lt;br /&gt;
&lt;br /&gt;
== Story generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Story generation is initiated by a POST to &amp;lt;code&amp;gt;/generate&amp;lt;/code&amp;gt; and proceeds through several deterministic-and-random stages before the OpenAI call.&lt;br /&gt;
&lt;br /&gt;
=== Seeds and knobs ===&lt;br /&gt;
&lt;br /&gt;
A request may carry up to three optional free-text fields — &#039;&#039;noun&#039;&#039;, &#039;&#039;verb&#039;&#039;, and &#039;&#039;mood&#039;&#039; — which are passed to the model as &amp;quot;seeds&amp;quot; and validated for presence in the output. Around the seeds, the application constructs a compact JSON &#039;&#039;knobs&#039;&#039; object that nudges the model along several axes:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Dimension !! Pool size !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| Perspective || 4 || first, second, third, omniscient&lt;br /&gt;
|-&lt;br /&gt;
| Structure || 8 || Discovery → Investigation → Revelation; Object → Rumor → Catastrophe; Signal → Interpretation → Realization; …&lt;br /&gt;
|-&lt;br /&gt;
| Time || 8 (forced &#039;&#039;ambiguous&#039;&#039;) || Victorian era, distant past, mythic period, interwar, near-future of obsolete technology&lt;br /&gt;
|-&lt;br /&gt;
| Location || 50 || lighthouse, salt marsh, foundry, signal box, scriptorium, observatory, shipbreaker&#039;s yard, …&lt;br /&gt;
|-&lt;br /&gt;
| Situation || 32 || during a storm, at low tide, during a blackout, on the eve of demolition, while the clock refuses to strike, …&lt;br /&gt;
|-&lt;br /&gt;
| Lexical palette || 53 || nautical, horological, astronomical, archival, cartographic, mycological, glaciological, heraldic, …&lt;br /&gt;
|-&lt;br /&gt;
| Style constraint || 5 || include exactly one short line of dialogue; include a question; avoid the words &amp;quot;shadow&amp;quot; and &amp;quot;dim&amp;quot;; …&lt;br /&gt;
|-&lt;br /&gt;
| Sentence pattern || 3 || short → long → medium; medium → short → long; long → medium → short&lt;br /&gt;
|-&lt;br /&gt;
| Theme (only when no seeds) || 18 || forbidden knowledge, cosmic entities, inherited curses, mathematical theorems, astronomical observations, …&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The knobs object always carries the seeds and at most two additional non-seed dimensions. Three of the knobs — &#039;&#039;perspective&#039;&#039;, &#039;&#039;structure&#039;&#039;, and &#039;&#039;time&#039;&#039; — are selected &#039;&#039;deterministically&#039;&#039; from a SHA-256 hash of a salt composed of the current 30-minute time bucket, the requester&#039;s IP address, and the seed words; this guarantees that the same client requesting the same seeds inside the same half-hour window draws the same priority knobs. The previous story&#039;s knobs for that IP are read from the &amp;lt;code&amp;gt;knobs_json&amp;lt;/code&amp;gt; column (falling back to the JSONL choices log for older rows) and avoided where possible. &#039;&#039;Time&#039;&#039; is hard-pinned to &amp;lt;code&amp;gt;ambiguous&amp;lt;/code&amp;gt; to suppress dated or era-specific references in the output. Lexicon and constraint values are sampled with the regular pseudo-random generator. Once all priority knobs are forced into the object, any non-priority extras are dropped at random until at most two non-seed knobs remain.&lt;br /&gt;
&lt;br /&gt;
=== System rules ===&lt;br /&gt;
&lt;br /&gt;
The system prompt is composed entirely of positive instructions: it asks for exactly three sentences in a grave Lovecraftian register, with no titles, lists, numbering, or blank lines, and embeds two short hand-written exemplar stories so the model can imitate length, cadence, and concreteness rather than reason in the negative. The application then validates the response against a separate banned-word list rather than naming forbidden terms in the prompt itself, in order to avoid the well-documented tendency of language models to reproduce items they are explicitly told to avoid.&lt;br /&gt;
&lt;br /&gt;
=== Model selection ===&lt;br /&gt;
&lt;br /&gt;
The default text model is GPT-4o (configurable via &amp;lt;code&amp;gt;OPENAI_MODEL&amp;lt;/code&amp;gt;) with a configurable fallback (&amp;lt;code&amp;gt;OPENAI_FALLBACK_MODEL&amp;lt;/code&amp;gt;, also GPT-4o by default). A helper function inspects the installed OpenAI SDK at request time: if the configured model is in the &amp;lt;code&amp;gt;gpt-5&amp;lt;/code&amp;gt; family but the SDK does not expose the Responses API, the call silently falls back to a chat-compatible model. When the Responses API is available it is preferred for all models, with the system rules supplied via the explicit &amp;lt;code&amp;gt;instructions&amp;lt;/code&amp;gt; field. For &amp;lt;code&amp;gt;gpt-5&amp;lt;/code&amp;gt; family models the call additionally injects a &amp;lt;code&amp;gt;reasoning&amp;lt;/code&amp;gt; object (defaulting to &#039;&#039;effort: low&#039;&#039; but configurable via &amp;lt;code&amp;gt;OPENAI_REASONING_EFFORT&amp;lt;/code&amp;gt;), drops the &amp;lt;code&amp;gt;temperature&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;top_p&amp;lt;/code&amp;gt; parameters (which those models reject), and grants a higher &amp;lt;code&amp;gt;max_output_tokens&amp;lt;/code&amp;gt; budget (1600 by default, raised to 2400 on retry) to absorb reasoning-token consumption. For non-reasoning models, &#039;&#039;temperature&#039;&#039; is sampled uniformly from a narrow [0.78, 0.92] interval and &#039;&#039;top_p&#039;&#039; is fixed at 0.9, deliberately tighter than typical creative-writing defaults to keep tone consistent. Where the Responses API is unavailable, the call degrades to Chat Completions.&lt;br /&gt;
&lt;br /&gt;
=== Validation and retry ===&lt;br /&gt;
&lt;br /&gt;
After the call returns, the output is validated by a checker that enforces:&lt;br /&gt;
&lt;br /&gt;
* Exactly three non-empty lines.&lt;br /&gt;
* No line beginning with any of a list of banned opening prefixes (&amp;quot;In the&amp;quot;, &amp;quot;Beneath&amp;quot;, &amp;quot;Under the&amp;quot;, &amp;quot;Within the&amp;quot;, and so on).&lt;br /&gt;
* Absence of a curated list of overused or out-of-register words and phrases (&amp;quot;eldritch&amp;quot;, &amp;quot;cyclopean&amp;quot;, &amp;quot;loathsome&amp;quot;, any inflection of &#039;&#039;tentacle&#039;&#039;, and similar).&lt;br /&gt;
* Absence of anachronisms unsuited to the desired ambiguous-historical tone (kilometres, kilograms, GPS coordinates, e-mail addresses, four-digit years, references to Wi-Fi).&lt;br /&gt;
* A per-sentence word count in the [8, 60] range.&lt;br /&gt;
* One terminal punctuation mark per line.&lt;br /&gt;
&lt;br /&gt;
If the request supplied seeds, a second pass verifies that the noun appears in the text — with allowance for irregular plurals — and that the verb appears in any common inflection.&lt;br /&gt;
&lt;br /&gt;
A separate near-duplicate check computes a 64-bit SimHash of the candidate text and rejects it if the Hamming distance to any of the most recent fifty stored stories is below a small threshold, so the corpus does not collapse into thematic loops.&lt;br /&gt;
&lt;br /&gt;
When validation fails, a single targeted retry is issued with a corrective prompt that quotes the failing lines back at the model verbatim, names the specific failures, lowers the temperature slightly, and reuses the same knobs JSON. The retry result is accepted if it passes, or if it fails with strictly fewer issues than the original. If the model call itself fails — including the specific case where a &amp;lt;code&amp;gt;gpt-5&amp;lt;/code&amp;gt; response is marked &#039;&#039;incomplete&#039;&#039; due to &amp;lt;code&amp;gt;max_output_tokens&amp;lt;/code&amp;gt; — the pipeline retries once with a higher token budget, then attempts the configured fallback model, and finally falls back to a fixed three-sentence placeholder. All failures surface to the user as Flask flash messages with appropriate severity.&lt;br /&gt;
&lt;br /&gt;
=== Persistence and logging ===&lt;br /&gt;
&lt;br /&gt;
The validated story text is written to a new &amp;lt;code&amp;gt;Story&amp;lt;/code&amp;gt; row together with the seeds, the originating IP, and the JSON-encoded knobs object. Image generation is then dispatched in a background daemon thread so the HTTP response returns immediately. Two structured logs are appended in JSON-Lines format under the Flask instance folder:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;instance/choices.log.jsonl&amp;lt;/code&amp;gt; records, per story, the seeds, the knobs JSON, the selected temperature and top_p, the model actually used, the configured model, and the validation outcome (including reasons and whether a retry was attempted).&lt;br /&gt;
* &amp;lt;code&amp;gt;instance/image.log.jsonl&amp;lt;/code&amp;gt; records each image generation attempt with status (&#039;&#039;success&#039;&#039;, &#039;&#039;retry&#039;&#039;, &#039;&#039;failed&#039;&#039;, &#039;&#039;skipped&#039;&#039;), error text, attempt count, image size, image model, and post-processing diagnostics such as the standard deviation of the output&#039;s luminance.&lt;br /&gt;
&lt;br /&gt;
Both logs are rotated to a single &#039;&#039;&amp;lt;file&amp;gt;.1&#039;&#039; backup once they exceed a configurable byte threshold (default 2 MiB) so that long-running instances cannot fill the disk.&lt;br /&gt;
&lt;br /&gt;
== Image generation ==&lt;br /&gt;
&lt;br /&gt;
Each story is paired with a square 1024×1024 illustration generated through the OpenAI image API.&lt;br /&gt;
&lt;br /&gt;
=== Style library ===&lt;br /&gt;
&lt;br /&gt;
The application maintains a curated library of fifteen monochrome illustration styles, each anchored to one or more named historical artists or movements — for example wood engravings in the manner of Gustave Doré or Thomas Bewick, pen-and-ink in the manner of Aubrey Beardsley or Edward Gorey, drypoint in the manner of Käthe Kollwitz, aquatint in the manner of Goya&#039;s &#039;&#039;Caprichos&#039;&#039;, a Victorian scientific plate after Ernst Haeckel, relief woodcut after Lynd Ward, silhouette cuts after Lotte Reiniger, and symbolist pen-and-ink after Alfred Kubin. Anchoring each entry to a real reference reliably moves the model closer to the desired register. The style for a given story is selected by hashing its primary key, so adjacent stories on the home grid do not visually collide and a regenerated image for the same story can vary by perturbing the salt. A legacy global counter file at &amp;lt;code&amp;gt;instance/image_style_cycle.txt&amp;lt;/code&amp;gt; is retained for callers without a story id.&lt;br /&gt;
&lt;br /&gt;
=== Prompt construction ===&lt;br /&gt;
&lt;br /&gt;
To keep the image prompt concrete enough for a diffusion model to act on, the three-sentence story is first reduced to a one- or two-sentence visual scene description by a cheap secondary call to a smaller model (&amp;lt;code&amp;gt;OPENAI_SCENE_MODEL&amp;lt;/code&amp;gt;, default &amp;lt;code&amp;gt;gpt-4o-mini&amp;lt;/code&amp;gt;); the helper names a single subject, a single setting, one source of light, and one telling object, and discards mood words and metaphor. The final image prompt is then assembled from a hard full-bleed directive, the selected style sentence, the scene description, and an avoid clause that explicitly forbids colour, painterly shading, photorealism, captions, watermarks, signatures, and any kind of border, frame, or decorative edge.&lt;br /&gt;
&lt;br /&gt;
=== Image API and retries ===&lt;br /&gt;
&lt;br /&gt;
The configured image model defaults to &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt;; common typos such as &amp;lt;code&amp;gt;gpt-image-1.5&amp;lt;/code&amp;gt; are normalised back to &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt; at start-up. The generation size is configurable through &amp;lt;code&amp;gt;OPENAI_IMAGE_GEN_SIZE&amp;lt;/code&amp;gt; (default 1024×1024) and the post-processed final size is fixed at 1024×1024. Each generation tolerates up to three attempts (configurable via &amp;lt;code&amp;gt;IMAGE_RETRIES&amp;lt;/code&amp;gt;) with exponential backoff starting at 1.5 seconds and capped at 10 seconds. If all attempts fail — or if a returned image is blank or near-uniform — a small inline SVG placeholder is written to disk and recorded as the story&#039;s image path so the front end always has something to display. A few legacy stories carry a one-pixel GIF placeholder of the same purpose; the application detects either by the presence of the substring &amp;lt;code&amp;gt;_placeholder.&amp;lt;/code&amp;gt; in the path.&lt;br /&gt;
&lt;br /&gt;
=== Post-processing ===&lt;br /&gt;
&lt;br /&gt;
Successfully returned PNG bytes are passed through a Pillow-based post-processor that:&lt;br /&gt;
&lt;br /&gt;
* Estimates the background colour from the four corner patches.&lt;br /&gt;
* Computes a difference image against a uniform background of that colour, raises its contrast, and finds the bounding box of meaningful content.&lt;br /&gt;
* Crops away any uniform border of more than ten pixels on any side, padded by two pixels to avoid clipping ink.&lt;br /&gt;
* Resizes the result back to the final size using Lanczos resampling and re-encodes as optimised PNG.&lt;br /&gt;
* Computes the standard deviation of the resulting image&#039;s luminance; if the value falls below a small threshold, the image is treated as blank and the generation attempt is failed so a retry can be issued.&lt;br /&gt;
&lt;br /&gt;
If Pillow is unavailable or any step fails, the original bytes are written through unchanged.&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Home ===&lt;br /&gt;
&lt;br /&gt;
The home page (&amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt;) renders the generation form together with a gallery of the five most recent stories that successfully produced an image. For unauthenticated visitors, the form is hidden and replaced by a rotating notice (one of twenty-two phrasings, selected from a SHA-256 hash of the visitor&#039;s IP and a six-hour bucket) when the visitor has already generated a story in the last 24 hours or when the site-wide guest cap has been reached. A footer-level statistics block displays the number of guest stories in the last 24 hours, the number of stories created by signed-in users in the same window, the all-time total, and the time elapsed since the most recent story.&lt;br /&gt;
&lt;br /&gt;
=== Story detail ===&lt;br /&gt;
&lt;br /&gt;
Each story is reachable at &amp;lt;code&amp;gt;/story/&amp;lt;id&amp;gt;&amp;lt;/code&amp;gt; and is publicly viewable regardless of authorship. The page presents the three sentences alongside the illustration; for the administrator, controls are exposed to regenerate the image (&amp;lt;code&amp;gt;/regenerate-image/&amp;lt;id&amp;gt;&amp;lt;/code&amp;gt;) or delete the story (&amp;lt;code&amp;gt;/story/&amp;lt;id&amp;gt;/delete&amp;lt;/code&amp;gt;). A polling endpoint at &amp;lt;code&amp;gt;/api/story/&amp;lt;id&amp;gt;/status&amp;lt;/code&amp;gt; returns a JSON document indicating whether a real image has yet been written, the URL of the image (or placeholder), and a flag distinguishing the two; the front-end script polls with exponential backoff capped at eight seconds, gives up after a hard timeout, and writes status updates into an &amp;lt;code&amp;gt;aria-live&amp;lt;/code&amp;gt; region for screen-reader accessibility. The story page also overrides the base template&#039;s [[Open Graph protocol|Open Graph]] block to advertise per-story metadata, including the generated illustration, when the page is shared on social media.&lt;br /&gt;
&lt;br /&gt;
=== Archive ===&lt;br /&gt;
&lt;br /&gt;
The archive (&amp;lt;code&amp;gt;/archive&amp;lt;/code&amp;gt;) is a paginated chronological listing of all stories, nine per page, with previous/next navigation. There is no per-user filter — all stories are visible. The archive page sets &amp;lt;code&amp;gt;&amp;amp;lt;meta name=&amp;quot;robots&amp;quot; content=&amp;quot;noindex,follow&amp;quot;&amp;amp;gt;&amp;lt;/code&amp;gt; to keep the long tail out of search-engine indexes while leaving individual story pages indexable.&lt;br /&gt;
&lt;br /&gt;
=== Health endpoint ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;/healthz&amp;lt;/code&amp;gt; returns a small JSON document and an HTTP 200 when the application is fully configured and able to query the database, or HTTP 503 when the OpenAI API key is not set or the database round-trip fails. It is intended for use by uptime monitors and load balancers.&lt;br /&gt;
&lt;br /&gt;
=== Authentication ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; accepts only the e-mail address configured in &amp;lt;code&amp;gt;ADMIN_EMAIL&amp;lt;/code&amp;gt; and verifies the password against the stored Werkzeug hash. &amp;lt;code&amp;gt;/logout&amp;lt;/code&amp;gt; ends the session. &amp;lt;code&amp;gt;/signup&amp;lt;/code&amp;gt; is intentionally disabled. All form-bearing pages carry a CSRF token rendered from the Flask-WTF helper, and an expired token is recovered with a flash message and a redirect rather than an HTTP error.&lt;br /&gt;
&lt;br /&gt;
=== Tagline rotation ===&lt;br /&gt;
&lt;br /&gt;
The base template injects a &amp;quot;current tagline&amp;quot; string into every response. The selection is deterministic on the current 30-minute interval since the Unix epoch: the interval timestamp is used as a seed for the standard library random module, which then picks one of forty-five variant phrasings. All visitors served within the same half-hour see the same tagline; the tagline rotates without any database state.&lt;br /&gt;
&lt;br /&gt;
== Rate limiting ==&lt;br /&gt;
&lt;br /&gt;
Rate limiting is enforced for unauthenticated visitors only:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Per-IP daily limit&#039;&#039;&#039;: any IP that has produced a story within the last 24 hours is blocked from generating another (one story per visitor per day).&lt;br /&gt;
* &#039;&#039;&#039;Site-wide guest cap&#039;&#039;&#039;: the total number of guest-authored stories in the last 24 hours must not exceed &amp;lt;code&amp;gt;GUEST_DAILY_CAP&amp;lt;/code&amp;gt; (default 24). When the cap is reached, the form is hidden for all guests until older stories age out.&lt;br /&gt;
&lt;br /&gt;
Both limits are evaluated at form render time (to hide the form) and at form submission time (to short-circuit the POST with a flashed warning and a redirect). Authenticated users have no rate limits and no per-IP enforcement.&lt;br /&gt;
&lt;br /&gt;
The originating IP for limit accounting and for the &amp;lt;code&amp;gt;Story.ip_address&amp;lt;/code&amp;gt; column is read from the &amp;lt;code&amp;gt;X-Forwarded-For&amp;lt;/code&amp;gt; request header only when the immediate peer is in a configurable trusted-proxy allow-list (&amp;lt;code&amp;gt;TRUSTED_PROXIES&amp;lt;/code&amp;gt;, default loopback); otherwise the application uses &amp;lt;code&amp;gt;request.remote_addr&amp;lt;/code&amp;gt;. This prevents trivial spoofing of the originating address when the application is exposed without a reverse proxy in front of it.&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
The production deployment is an Ubuntu VPS running a systemd unit (&amp;lt;code&amp;gt;acbc.service&amp;lt;/code&amp;gt;) that launches Gunicorn with three workers, bound to a Unix domain socket under the project directory. Nginx terminates HTTPS (provisioned by [[Let&#039;s Encrypt]] via Certbot), serves the &amp;lt;code&amp;gt;static/&amp;lt;/code&amp;gt; directory directly with a one-year immutable cache header, and proxies all other requests to the Gunicorn socket. The service runs as the &amp;lt;code&amp;gt;django&amp;lt;/code&amp;gt; system user out of &amp;lt;code&amp;gt;/home/django/acbc&amp;lt;/code&amp;gt;. The application reads its configuration from environment variables loaded via python-dotenv.&lt;br /&gt;
&lt;br /&gt;
== Security and authorisation ==&lt;br /&gt;
&lt;br /&gt;
* All POST endpoints require a CSRF token issued by Flask-WTF.&lt;br /&gt;
* Sign-ups are disabled; only the bootstrapped administrator account can authenticate.&lt;br /&gt;
* Login attempts for any e-mail other than &amp;lt;code&amp;gt;ADMIN_EMAIL&amp;lt;/code&amp;gt; are rejected without a database query.&lt;br /&gt;
* The image-deletion helper refuses to remove any path that does not begin with &amp;lt;code&amp;gt;images/&amp;lt;/code&amp;gt; or that, after canonicalisation, falls outside the configured upload folder, providing protection against path-traversal in stored data.&lt;br /&gt;
* Story deletion requires either the administrator session or ownership of the row; image regeneration requires the administrator session.&lt;br /&gt;
* The story-status JSON endpoint returns no user-identifying information beyond the public fields rendered on the story page.&lt;br /&gt;
* The originating-IP determination only honours &amp;lt;code&amp;gt;X-Forwarded-For&amp;lt;/code&amp;gt; from trusted proxies.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
* [[Flask (web framework)]]&lt;br /&gt;
* [[H. P. Lovecraft]]&lt;br /&gt;
* [[Computational creativity]]&lt;br /&gt;
* [[Generative artificial intelligence]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=A_Cabinet_of_Brief_Curiosities&amp;diff=413</id>
		<title>A Cabinet of Brief Curiosities</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=A_Cabinet_of_Brief_Curiosities&amp;diff=413"/>
		<updated>2026-04-23T14:50:46Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Created page with &amp;quot;{{Infobox | 01_name         = A Cabinet of Brief Curiosities | 02_url          = https://acbc.yusupov.cloud | 03_developer    = Michel Vuijlsteke | 04_released     = 2025 | 05_genre        = AI-generated short-fiction application | 06_language     = Python | 07_framework    = Flask 3.0 | 08_license      = MIT }}  &amp;#039;&amp;#039;&amp;#039;A Cabinet of Brief Curiosities&amp;#039;&amp;#039;&amp;#039; (abbreviated &amp;#039;&amp;#039;&amp;#039;acbc&amp;#039;&amp;#039;&amp;#039;) is a web application hosted at &amp;lt;code&amp;gt;acbc.yusupov.cloud&amp;lt;/code&amp;gt; that generates illustrated thre...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| 01_name         = A Cabinet of Brief Curiosities&lt;br /&gt;
| 02_url          = https://acbc.yusupov.cloud&lt;br /&gt;
| 03_developer    = Michel Vuijlsteke&lt;br /&gt;
| 04_released     = 2025&lt;br /&gt;
| 05_genre        = AI-generated short-fiction application&lt;br /&gt;
| 06_language     = Python&lt;br /&gt;
| 07_framework    = [[Flask]] 3.0&lt;br /&gt;
| 08_license      = MIT&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;A Cabinet of Brief Curiosities&#039;&#039;&#039; (abbreviated &#039;&#039;&#039;acbc&#039;&#039;&#039;) is a web application hosted at &amp;lt;code&amp;gt;acbc.yusupov.cloud&amp;lt;/code&amp;gt; that generates illustrated three-sentence short stories in the style of [[H. P. Lovecraft]]. Each story is composed by a large language model from a structured set of randomised &amp;quot;knobs&amp;quot; and optional user-supplied seed words, and is paired with a black-and-white illustration designed to resemble a 19th-century engraved book plate. The site&#039;s rotating tagline — drawn every thirty minutes from a pool of forty-five variants — frames the act of generation in deliberately archaic terms; the canonical opening reads &amp;quot;Dredge slivers of impossible worlds from the black gulfs of imagination and press them into trembling mortal words.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Flask 3.0 with [[SQLite]] as its database, accessed through [[SQLAlchemy]] (Flask-SQLAlchemy 3.1).&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt in the project repository pins Flask 3.0.3, Flask-Login 0.6.3, Flask-SQLAlchemy 3.1.1, python-dotenv 1.0.1, openai ≥1.50, httpx 0.27.2, and Pillow ≥10.&amp;lt;/ref&amp;gt; Authentication is handled by Flask-Login. It is deployed on an Ubuntu VPS behind [[Nginx]] with [[Gunicorn]] running over a Unix socket under a dedicated &amp;lt;code&amp;gt;django&amp;lt;/code&amp;gt; system user, supervised by systemd. Additional dependencies include the [[OpenAI]] Python client for both text and image generation, [[Pillow (imaging library)|Pillow]] for image post-processing, [[python-dotenv]] for environment configuration, and [[httpx]] as the underlying HTTP transport. The front end is rendered from Jinja templates using a small Bootstrap-derived stylesheet (&amp;lt;code&amp;gt;static/style.css&amp;lt;/code&amp;gt;) and is registered as an installable progressive web app via &amp;lt;code&amp;gt;static/manifest.json&amp;lt;/code&amp;gt; and a service worker (&amp;lt;code&amp;gt;static/sw.js&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
The database contains two tables.&lt;br /&gt;
&lt;br /&gt;
=== User ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; stores a single bootstrapped administrator account with an e-mail address, a Werkzeug password hash, and a creation timestamp. Sign-ups are disabled at the route level: the &amp;lt;code&amp;gt;/signup&amp;lt;/code&amp;gt; endpoint flashes a notice and redirects to login. The administrator is created on application start from the &amp;lt;code&amp;gt;ADMIN_EMAIL&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ADMIN_PASSWORD&amp;lt;/code&amp;gt; environment variables if no matching row exists.&lt;br /&gt;
&lt;br /&gt;
=== Story ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;Story&amp;lt;/code&amp;gt; stores the generated short fiction:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;id&#039;&#039; (primary key), an optional &#039;&#039;user_id&#039;&#039; foreign key (null for guest submissions), the three-sentence &#039;&#039;story_text&#039;&#039;, the optional &#039;&#039;mood&#039;&#039;, &#039;&#039;nouns&#039;&#039; and &#039;&#039;verbs&#039;&#039; seeds supplied by the requester, an optional &#039;&#039;image_path&#039;&#039; (relative to the static folder, e.g. &amp;lt;code&amp;gt;images/story_42.png&amp;lt;/code&amp;gt;), the originating &#039;&#039;ip_address&#039;&#039; (indexed), and an indexed &#039;&#039;created_at&#039;&#039; timestamp.&lt;br /&gt;
&lt;br /&gt;
== Story generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Story generation is initiated by a POST to &amp;lt;code&amp;gt;/generate&amp;lt;/code&amp;gt; and proceeds through several deterministic-and-random stages before the OpenAI call.&lt;br /&gt;
&lt;br /&gt;
=== Seeds and knobs ===&lt;br /&gt;
&lt;br /&gt;
A request may carry up to three optional free-text fields — &#039;&#039;noun&#039;&#039;, &#039;&#039;verb&#039;&#039;, and &#039;&#039;mood&#039;&#039; — which are passed to the model as &amp;quot;seeds&amp;quot; and validated for presence in the output. Around the seeds, the application constructs a compact JSON &#039;&#039;knobs&#039;&#039; object that nudges the model along several axes:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Dimension !! Pool size !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| Perspective || 4 || first, second, third, omniscient&lt;br /&gt;
|-&lt;br /&gt;
| Structure || 8 || Discovery → Investigation → Revelation; Object → Rumor → Catastrophe; Signal → Interpretation → Realization; …&lt;br /&gt;
|-&lt;br /&gt;
| Time || 8 (forced &#039;&#039;ambiguous&#039;&#039;) || Victorian era, distant past, mythic period, interwar, near-future of obsolete technology&lt;br /&gt;
|-&lt;br /&gt;
| Location || 50 || lighthouse, salt marsh, foundry, signal box, scriptorium, observatory, shipbreaker&#039;s yard, …&lt;br /&gt;
|-&lt;br /&gt;
| Situation || 32 || during a storm, at low tide, during a blackout, on the eve of demolition, while the clock refuses to strike, …&lt;br /&gt;
|-&lt;br /&gt;
| Lexical palette || 53 || nautical, horological, astronomical, archival, cartographic, mycological, glaciological, heraldic, …&lt;br /&gt;
|-&lt;br /&gt;
| Style constraint || 5 || include exactly one short line of dialogue; include a question; avoid the words &amp;quot;shadow&amp;quot; and &amp;quot;dim&amp;quot;; …&lt;br /&gt;
|-&lt;br /&gt;
| Sentence pattern || 3 || short → long → medium; medium → short → long; long → medium → short&lt;br /&gt;
|-&lt;br /&gt;
| Theme (only when no seeds) || 18 || forbidden knowledge, cosmic entities, inherited curses, mathematical theorems, astronomical observations, …&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The knobs object always carries the seeds and at most three additional dimensions. Three of the knobs — &#039;&#039;perspective&#039;&#039;, &#039;&#039;structure&#039;&#039;, and &#039;&#039;time&#039;&#039; — are selected &#039;&#039;deterministically&#039;&#039; from a SHA-256 hash of a salt composed of the current 30-minute time bucket, the requester&#039;s IP address, and the seed words; this guarantees that the same client requesting the same seeds inside the same half-hour window draws the same priority knobs. The previous story&#039;s knobs for that IP are read from the choices log and avoided where possible. &#039;&#039;Time&#039;&#039; is hard-pinned to &amp;lt;code&amp;gt;ambiguous&amp;lt;/code&amp;gt; to suppress dated or era-specific references in the output. Lexicon and constraint values are sampled with the regular pseudo-random generator. Once all priority knobs are forced into the object, any non-priority extras are dropped at random until at most three non-seed knobs remain.&lt;br /&gt;
&lt;br /&gt;
=== System rules ===&lt;br /&gt;
&lt;br /&gt;
The system prompt instructs the model to produce a coherent short story of exactly three sentences in the style of H. P. Lovecraft, with no titles, lists, numbering, or blank lines. The tone must be grave, ominous, and unsettling, and never humorous. The model is forbidden to use the words &amp;quot;eldritch&amp;quot;, &amp;quot;cyclopean&amp;quot;, or &amp;quot;loathsome&amp;quot;, and is forbidden to begin any line with one of six banned openings — &amp;quot;In the&amp;quot;, &amp;quot;Beneath&amp;quot;, &amp;quot;Under the&amp;quot;, &amp;quot;Within the&amp;quot;, &amp;quot;In the dim&amp;quot;, and &amp;quot;In the shadow&amp;quot;. Whimsical seeds are to be adapted with synonyms that preserve tone.&lt;br /&gt;
&lt;br /&gt;
=== Model selection ===&lt;br /&gt;
&lt;br /&gt;
The default text model is GPT-4o (configurable via &amp;lt;code&amp;gt;OPENAI_MODEL&amp;lt;/code&amp;gt;) with a configurable fallback (&amp;lt;code&amp;gt;OPENAI_FALLBACK_MODEL&amp;lt;/code&amp;gt;, also GPT-4o by default). A helper function inspects the installed OpenAI SDK at request time: if the configured model is in the &amp;lt;code&amp;gt;gpt-5&amp;lt;/code&amp;gt; family but the SDK does not expose the Responses API, the call silently falls back to a chat-compatible model. When the Responses API is available it is preferred for all models, with the system rules supplied via the explicit &amp;lt;code&amp;gt;instructions&amp;lt;/code&amp;gt; field. For &amp;lt;code&amp;gt;gpt-5&amp;lt;/code&amp;gt; family models the call additionally injects a &amp;lt;code&amp;gt;reasoning&amp;lt;/code&amp;gt; object (defaulting to &#039;&#039;effort: low&#039;&#039; but configurable via &amp;lt;code&amp;gt;OPENAI_REASONING_EFFORT&amp;lt;/code&amp;gt;), drops the &amp;lt;code&amp;gt;temperature&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;top_p&amp;lt;/code&amp;gt; parameters (which those models reject), and grants a higher &amp;lt;code&amp;gt;max_output_tokens&amp;lt;/code&amp;gt; budget (1600 by default, raised to 2400 on retry) to absorb reasoning-token consumption. For non-reasoning models, &#039;&#039;temperature&#039;&#039; is sampled uniformly from [0.88, 1.05] and &#039;&#039;top_p&#039;&#039; is fixed at 0.92. Where the Responses API is unavailable, the call degrades to Chat Completions.&lt;br /&gt;
&lt;br /&gt;
=== Validation and retry ===&lt;br /&gt;
&lt;br /&gt;
After the call returns, the output is validated by a lightweight checker that enforces:&lt;br /&gt;
&lt;br /&gt;
* Exactly three non-empty lines.&lt;br /&gt;
* No line beginning with any of the banned opening prefixes.&lt;br /&gt;
* Absence of the banned words.&lt;br /&gt;
* One terminal punctuation mark per line (no missing terminator, no more than two strong delimiters).&lt;br /&gt;
&lt;br /&gt;
If the request supplied seeds, a second pass verifies that the noun appears in the text and that the verb (or a simple inflection thereof) appears as a whole word.&lt;br /&gt;
&lt;br /&gt;
When validation fails, a single targeted retry is issued with a corrective prompt that names the specific failures, lowers the temperature slightly, and reuses the same knobs JSON. The retry result is accepted if it passes, or if it fails with strictly fewer issues than the original. If the model call itself fails — including the specific case where a &amp;lt;code&amp;gt;gpt-5&amp;lt;/code&amp;gt; response is marked &#039;&#039;incomplete&#039;&#039; due to &amp;lt;code&amp;gt;max_output_tokens&amp;lt;/code&amp;gt; — the pipeline retries once with a higher token budget, then attempts the configured fallback model, and finally falls back to a fixed three-sentence placeholder (&amp;quot;The moon borrowed a suitcase from a bewildered pigeon. …&amp;quot;). All failures surface to the user as Flask flash messages with appropriate severity.&lt;br /&gt;
&lt;br /&gt;
=== Persistence and logging ===&lt;br /&gt;
&lt;br /&gt;
The validated story text is written to a new &amp;lt;code&amp;gt;Story&amp;lt;/code&amp;gt; row together with the seeds and originating IP. Image generation is then dispatched in a background daemon thread so the HTTP response returns immediately. Two structured logs are appended in JSON-Lines format under the Flask instance folder:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;instance/choices.log.jsonl&amp;lt;/code&amp;gt; records, per story, the seeds, the knobs JSON, the selected temperature and top_p, the model actually used, the configured model, and the validation outcome (including reasons and whether a retry was attempted).&lt;br /&gt;
* &amp;lt;code&amp;gt;instance/image.log.jsonl&amp;lt;/code&amp;gt; records each image generation attempt with status (&#039;&#039;success&#039;&#039;, &#039;&#039;retry&#039;&#039;, &#039;&#039;failed&#039;&#039;, &#039;&#039;skipped&#039;&#039;), error text, attempt count, image size, and image model.&lt;br /&gt;
&lt;br /&gt;
== Image generation ==&lt;br /&gt;
&lt;br /&gt;
Each story is paired with a square 1024×1024 illustration generated through the OpenAI image API.&lt;br /&gt;
&lt;br /&gt;
=== Style cycling ===&lt;br /&gt;
&lt;br /&gt;
The application maintains a counter file at &amp;lt;code&amp;gt;instance/image_style_cycle.txt&amp;lt;/code&amp;gt; that walks deterministically through a library of fifteen monochrome engraving styles, including Victorian wood engraving, antique grimoire pen-and-ink, penny-dreadful frontispiece, steel engraving, copperplate etching, drypoint, mezzotint, aquatint-grained etching, scratchboard, Victorian scientific plate, woodcut, scanned 1860s book plate, fin-de-siècle symbolism, and Victorian reportage sketch. A threading lock guards the read-modify-write of the counter so that concurrent generations do not collide on the same style. If the counter file is missing or unreadable, a random style is chosen instead.&lt;br /&gt;
&lt;br /&gt;
=== Prompt construction ===&lt;br /&gt;
&lt;br /&gt;
The image prompt is assembled from four blocks:&lt;br /&gt;
&lt;br /&gt;
# A composition directive specifying a square 1:1 aspect ratio, full-bleed framing with no borders or margins, and a centred subject.&lt;br /&gt;
# The selected style sentence from the cycling library.&lt;br /&gt;
# A &amp;quot;scanned 1860s book plate&amp;quot; line that asks for slight ink unevenness and faint paper texture.&lt;br /&gt;
# The story text itself, truncated to 900 characters with an ellipsis if longer.&lt;br /&gt;
# A trailing &amp;quot;Avoid:&amp;quot; clause that explicitly forbids colour, grayscale wash, painterly shading, photorealism, 3D rendering, borders, frames, mats, vignettes, modern comic or anime style, halftone dots, captions, readable text, watermarks, and signatures.&lt;br /&gt;
&lt;br /&gt;
=== Image API and retries ===&lt;br /&gt;
&lt;br /&gt;
The configured image model defaults to &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt;; common typos such as &amp;lt;code&amp;gt;gpt-image-1.5&amp;lt;/code&amp;gt; are normalised back to &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt; at start-up. The output size is fixed at 1024×1024. Each generation tolerates up to three attempts (configurable via &amp;lt;code&amp;gt;IMAGE_RETRIES&amp;lt;/code&amp;gt;) with exponential backoff starting at 1.5 seconds and capped at 10 seconds. If all attempts fail, a tiny placeholder GIF is written to disk under the name &amp;lt;code&amp;gt;story_&amp;lt;id&amp;gt;_placeholder.gif&amp;lt;/code&amp;gt; and recorded as the story&#039;s image path so the front end always has something to display.&lt;br /&gt;
&lt;br /&gt;
=== Post-processing ===&lt;br /&gt;
&lt;br /&gt;
Successfully returned PNG bytes are passed through a Pillow-based post-processor that:&lt;br /&gt;
&lt;br /&gt;
* Estimates the background colour from the four corner patches.&lt;br /&gt;
* Computes a difference image against a uniform background of that colour, raises its contrast, and finds the bounding box of meaningful content.&lt;br /&gt;
* Crops away any uniform border of more than ten pixels on any side, padded by two pixels to avoid clipping ink.&lt;br /&gt;
* Resizes the result back to 1024×1024 using Lanczos resampling and re-encodes as optimised PNG.&lt;br /&gt;
&lt;br /&gt;
If Pillow is unavailable or any step fails, the original bytes are written through unchanged.&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Home ===&lt;br /&gt;
&lt;br /&gt;
The home page (&amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt;) renders the generation form together with a gallery of the five most recent stories that successfully produced an image. For unauthenticated visitors, the form is hidden and replaced by a rotating notice (one of twenty-two phrasings, selected from a SHA-256 hash of the visitor&#039;s IP and a six-hour bucket) when the visitor has already generated a story in the last 24 hours or when the site-wide guest cap has been reached. A footer-level statistics block displays the number of guest stories in the last 24 hours, the number of stories created by signed-in users in the same window, the all-time total, and the time elapsed since the most recent story.&lt;br /&gt;
&lt;br /&gt;
=== Story detail ===&lt;br /&gt;
&lt;br /&gt;
Each story is reachable at &amp;lt;code&amp;gt;/story/&amp;lt;id&amp;gt;&amp;lt;/code&amp;gt; and is publicly viewable regardless of authorship. The page presents the three sentences alongside the illustration; for the administrator, controls are exposed to regenerate the image (&amp;lt;code&amp;gt;/regenerate-image/&amp;lt;id&amp;gt;&amp;lt;/code&amp;gt;) or delete the story (&amp;lt;code&amp;gt;/story/&amp;lt;id&amp;gt;/delete&amp;lt;/code&amp;gt;). A polling endpoint at &amp;lt;code&amp;gt;/api/story/&amp;lt;id&amp;gt;/status&amp;lt;/code&amp;gt; returns a JSON document indicating whether a real image has yet been written, the URL of the image (or placeholder), and a flag distinguishing the two; the front end uses this to swap a placeholder GIF for the final image once background generation completes.&lt;br /&gt;
&lt;br /&gt;
=== Archive ===&lt;br /&gt;
&lt;br /&gt;
The archive (&amp;lt;code&amp;gt;/archive&amp;lt;/code&amp;gt;) is a paginated chronological listing of all stories, nine per page, with previous/next navigation. There is no per-user filter — all stories are visible.&lt;br /&gt;
&lt;br /&gt;
=== Authentication ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; accepts only the e-mail address configured in &amp;lt;code&amp;gt;ADMIN_EMAIL&amp;lt;/code&amp;gt; and verifies the password against the stored Werkzeug hash. &amp;lt;code&amp;gt;/logout&amp;lt;/code&amp;gt; ends the session. &amp;lt;code&amp;gt;/signup&amp;lt;/code&amp;gt; is intentionally disabled.&lt;br /&gt;
&lt;br /&gt;
=== Tagline rotation ===&lt;br /&gt;
&lt;br /&gt;
The base template injects a &amp;quot;current tagline&amp;quot; string into every response. The selection is deterministic on the current 30-minute interval since the Unix epoch: the interval timestamp is used as a seed for the standard library random module, which then picks one of forty-five variant phrasings. All visitors served within the same half-hour see the same tagline; the tagline rotates without any database state.&lt;br /&gt;
&lt;br /&gt;
== Rate limiting ==&lt;br /&gt;
&lt;br /&gt;
Rate limiting is enforced for unauthenticated visitors only:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Per-IP daily limit&#039;&#039;&#039;: any IP that has produced a story within the last 24 hours is blocked from generating another (one story per visitor per day).&lt;br /&gt;
* &#039;&#039;&#039;Site-wide guest cap&#039;&#039;&#039;: the total number of guest-authored stories in the last 24 hours must not exceed &amp;lt;code&amp;gt;GUEST_DAILY_CAP&amp;lt;/code&amp;gt; (default 24). When the cap is reached, the form is hidden for all guests until older stories age out.&lt;br /&gt;
&lt;br /&gt;
Both limits are evaluated at form render time (to hide the form) and at form submission time (to short-circuit the POST with a flashed warning and a redirect). Authenticated users have no rate limits and no per-IP enforcement.&lt;br /&gt;
&lt;br /&gt;
The originating IP for limit accounting and for the &amp;lt;code&amp;gt;Story.ip_address&amp;lt;/code&amp;gt; column is read from &amp;lt;code&amp;gt;X-Forwarded-For&amp;lt;/code&amp;gt; when the request carries it (taking the first comma-separated value), and from &amp;lt;code&amp;gt;request.remote_addr&amp;lt;/code&amp;gt; otherwise.&lt;br /&gt;
&lt;br /&gt;
The application reads its configuration from environment variables loaded via python-dotenv:&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
The production deployment is an Ubuntu VPS running a systemd unit (&amp;lt;code&amp;gt;acbc.service&amp;lt;/code&amp;gt;) that launches Gunicorn with three workers, bound to a Unix domain socket under the project directory. Nginx terminates HTTPS (provisioned by [[Let&#039;s Encrypt]] via Certbot), serves the &amp;lt;code&amp;gt;static/&amp;lt;/code&amp;gt; directory directly with a one-year immutable cache header, and proxies all other requests to the Gunicorn socket. The service runs as the &amp;lt;code&amp;gt;django&amp;lt;/code&amp;gt; system user out of &amp;lt;code&amp;gt;/home/django/acbc&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Security and authorisation ==&lt;br /&gt;
&lt;br /&gt;
* Sign-ups are disabled; only the bootstrapped administrator account can authenticate.&lt;br /&gt;
* Login attempts for any e-mail other than &amp;lt;code&amp;gt;ADMIN_EMAIL&amp;lt;/code&amp;gt; are rejected without a database query.&lt;br /&gt;
* The image-deletion helper refuses to remove any path that does not begin with &amp;lt;code&amp;gt;images/&amp;lt;/code&amp;gt; or that, after canonicalisation, falls outside the configured upload folder, providing protection against path-traversal in stored data.&lt;br /&gt;
* Story deletion requires either the administrator session or ownership of the row; image regeneration requires the administrator session.&lt;br /&gt;
* The story-status JSON endpoint returns no user-identifying information beyond the public fields rendered on the story page.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
* [[Flask (web framework)]]&lt;br /&gt;
* [[H. P. Lovecraft]]&lt;br /&gt;
* [[Computational creativity]]&lt;br /&gt;
* [[Generative artificial intelligence]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=412</id>
		<title>Yusupov.cloud</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=412"/>
		<updated>2026-04-23T14:42:57Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Subdomains and projects */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name           = yusupov.cloud&lt;br /&gt;
| 1 url            = https://yusupov.cloud&lt;br /&gt;
| 2 type           = Personal web sites&lt;br /&gt;
| 3 owner          = [[Michel Vuijlsteke]]&lt;br /&gt;
| 4 launched       = 2025&lt;br /&gt;
| 5 current_status = Online&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;yusupov.cloud&#039;&#039;&#039; is a personal domain and virtual private server operated by Belgian technologist [[Michel Vuijlsteke]]. It hosts multiple small web applications on subdomains and at the apex domain. One of these is a MediaWiki installation titled “Yusupov’s House.” The setup is presented as a web-era continuation of the do-it-yourself ethos of Vuijlsteke’s 1990s BBS of the same name.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot;&amp;gt;“Yusupov’s House,” &#039;&#039;yusupov.cloud&#039;&#039; (wiki), accessed 10 October 2025, https://yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
Among the projects on the domain is a MediaWiki (at the apex, &#039;&#039;yusupov.cloud&#039;&#039;) running MediaWiki 1.44.0 with PHP 8.3.6 (FPM) and SQLite, using the Vector skin and core extensions for citations and template scripting.&amp;lt;ref name=&amp;quot;version&amp;quot;&amp;gt;‘‘Special:Version’’ page, &#039;&#039;yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://yusupov.cloud/wiki/Special:Version&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Subdomains and projects ==&lt;br /&gt;
Publicly visible projects include:&lt;br /&gt;
&lt;br /&gt;
* [https://acbc.yusupov.cloud acbc.yusupov.cloud] — &#039;&#039;[[A Cabinet of Brief Curiosities]]&#039;&#039;, generating tiny three-sentence surreal/horror micro-stories with an hourly cadence and an archive. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;acbc-home&amp;quot;&amp;gt;A Cabinet of Brief Curiosities (home), &#039;&#039;acbc.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://acbc.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://agenda.yusupov.cloud agenda.yusupov.cloud] — &#039;&#039;A Life in Planners&#039;&#039;, a structured journal chronicling the final years of the operator’s mother, with calendar, food, medications, measurements, and statistics views (multilingual UI).&amp;lt;ref name=&amp;quot;agenda&amp;quot;&amp;gt;“A life in planners,” &#039;&#039;agenda.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://agenda.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://cloud.yusupov.cloud cloud.yusupov.cloud] — a series of static html creative coding experiments, simulations, and games including: timebeat, fire and snake simulations, biomass metaballs, cs3, &#039;&#039;Cross&#039;&#039; crossword puzzle game, image dithering tool, books, &#039;&#039;Elite Galaxy Explorer&#039;&#039;, ZX Spectrum loading screen simulator, Carcassonne, 3D boids flocking algorithm, physarum slime mold simulation, temps temperature visualization, &#039;&#039;The Chronicle of Hamurabi&#039;&#039; ancient Sumeria resource management game, and gatekeeper.&amp;lt;ref name=&amp;quot;cloud-home&amp;quot;&amp;gt;&amp;quot;cloud,&amp;quot; &#039;&#039;cloud.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://cloud.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://digest.yusupov.cloud digest.yusupov.cloud] — &#039;&#039;[[Digest]]&#039;&#039;, daily seasonal AI-assisted recipes inspired by current events, browsable by meal type and ingredients.&amp;lt;ref name=&amp;quot;digest-home&amp;quot;&amp;gt;“Digest — Daily recipes inspired by the news,” &#039;&#039;digest.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://digest.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://echoes.yusupov.cloud echoes.yusupov.cloud] — &#039;&#039;[[Echoes of What Wasn&#039;t]]&#039;&#039;, an AI-generated alternate-history newspaper presenting richly detailed articles about historical events as if they had unfolded differently. A pipeline scrapes real events from multilingual Wikipedia, uses OpenAI to craft a divergent narrative with period-appropriate prose and DALL-E imagery, and publishes via a REST API. Features article browsing by month, a &amp;quot;Where/When&amp;quot; interactive map-and-timeline view using Leaflet, and a picture desk. (Built with Wagtail 7/Django 5 per operator.)&amp;lt;ref name=&amp;quot;echoes-home&amp;quot;&amp;gt;&amp;quot;Echoes — Dispatches from Histories That Never Were,&amp;quot; &#039;&#039;echoes.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://echoes.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://nomos.yusupov.cloud nomos.yusupov.cloud] — &#039;&#039;[[Nomos]]&#039;&#039;, a daily-generating archive of fictional Flemish administrative legislation, where real Vlaamse Codex documents are semantically shifted by GPT-5 into structurally faithful but entirely invented decrees, orders, and circulars. (Built with Wagtail 7/Django 6 per operator.)&lt;br /&gt;
* [https://quidlibet.yusupov.cloud quidlibet.yusupov.cloud] — &#039;&#039;[[Quidlibet]]&#039;&#039;, an app that generates fictional books complete with synopsis, author bio, and faux reviews; includes genre and author archives. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;quidlibet-home&amp;quot;&amp;gt;“Quidlibet — Book Generator,” &#039;&#039;quidlibet.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://quidlibet.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://tyov-web.yusupov.cloud tyov-web.yusupov.cloud] — a web implementation of the solo RPG &#039;&#039;[[Thousand Year Old Vampire]]&#039;&#039;, with Django 5 backend and Vue 3 frontend. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
Operator-reported (not publicly discoverable at time of writing):&lt;br /&gt;
&lt;br /&gt;
* skills.yusupov.cloud — a skills matrix application. (Per operator.)&lt;br /&gt;
* resources.yusupov.cloud — a simple resource planning calendar. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
== Technology ==&lt;br /&gt;
The wiki stack is documented on &#039;&#039;Special:Version&#039;&#039;. Individual apps are described by the operator as Flask (&#039;&#039;acbc&#039;&#039;, &#039;&#039;quidlibet&#039;&#039;) and Django 5 + Vue 3 (&#039;&#039;tyov-web&#039;&#039;).&amp;lt;ref name=&amp;quot;version&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Relation to the BBS ==&lt;br /&gt;
The project name references Vuijlsteke’s single-line BBS (FidoNet 2:291/1925) active between 1990 and 1995. While the VPS is not a BBS, its single-admin, self-maintained hosting reprises the early DIY approach.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;nodehist&amp;quot;&amp;gt;“Nodelist history search: History of node 2:291/1925,” NodeHist, accessed 10 October 2025, https://nodehist.fidonet.org.ua/?address=2%3A291%2F1925&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Yusupov&#039;s House]] (1990s BBS)&lt;br /&gt;
* [[Michel Vuijlsteke]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Personal websites]]&lt;br /&gt;
[[Category:Belgian websites]]&lt;br /&gt;
[[Category:2025 establishments in Belgium]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Nomos&amp;diff=411</id>
		<title>Nomos</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Nomos&amp;diff=411"/>
		<updated>2026-04-20T15:42:17Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| 01_name         = Nomos&lt;br /&gt;
| 02_url          = https://nomos.yusupov.cloud&lt;br /&gt;
| 03_developer    = Michel Vuijlsteke&lt;br /&gt;
| 04_released     = 2026&lt;br /&gt;
| 05_genre        = AI-generated legal document archive&lt;br /&gt;
| 06_language     = Python&lt;br /&gt;
| 07_framework    = [[Django]] / [[Wagtail]]&lt;br /&gt;
| 08_license      = Proprietary&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nomos&#039;&#039;&#039; is a web application hosted at &amp;lt;code&amp;gt;nomos.yusupov.cloud&amp;lt;/code&amp;gt; that generates and publishes AI-created Flemish administrative legislation. Each day, the system fetches a real document from the Vlaamse Codex open-data API, performs a semantic shift on its subject matter using a large language model, and produces a structurally faithful but entirely fictional legal text — a decree, ministerial order, or circular — that reads as if it were published in the &#039;&#039;Belgisch Staatsblad&#039;&#039;. The generated documents are stored in a [[Wagtail (CMS)|Wagtail]] content management system and presented through a GOV.UK-inspired Dutch-language public interface. The name &#039;&#039;Nomos&#039;&#039; derives from the [[Ancient Greek|Greek]] word νόμος, meaning &amp;quot;law.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Django with Wagtail as its content management framework and [[SQLite]] as its development database. Additional dependencies include the [[OpenAI]] Python client for language-model calls, [[Requests (software)|Requests]] for HTTP communication with the Vlaamse Codex API, [[Beautiful Soup (HTML parser)|Beautiful Soup]] for scraping title inspiration from the Belgisch Staatsblad, [[Pillow (imaging library)|Pillow]] as a Wagtail dependency, and [[python-dotenv]] for environment configuration. The front end uses [[Bootstrap]] loaded from the jsDelivr CDN with subresource integrity hashes. All user interface text is in Dutch. The planned production deployment targets a Hetzner VPS behind [[Nginx]] with [[Gunicorn]] and [[PostgreSQL]].&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
The data model uses Wagtail&#039;s page tree. A singleton &amp;lt;code&amp;gt;DecreeIndexPage&amp;lt;/code&amp;gt; (limited to &amp;lt;code&amp;gt;max_count = 1&amp;lt;/code&amp;gt;) serves as the parent of all generated documents. Each document is a &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; with the following fields:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;instrument&#039;&#039; — one of four types: Decreet, Besluit, Omzendbrief, or Reglement.&lt;br /&gt;
* &#039;&#039;full_title&#039;&#039; (&amp;lt;code&amp;gt;TextField&amp;lt;/code&amp;gt;) — the complete title, unlimited in length. The standard Wagtail &#039;&#039;title&#039;&#039; field (255-character limit) holds a truncated copy for internal use.&lt;br /&gt;
* &#039;&#039;body&#039;&#039; (&amp;lt;code&amp;gt;RichTextField&amp;lt;/code&amp;gt;) — the full HTML text of the generated legislation.&lt;br /&gt;
* &#039;&#039;publication_date&#039;&#039; (&amp;lt;code&amp;gt;DateField&amp;lt;/code&amp;gt;) — set to the generation date.&lt;br /&gt;
* &#039;&#039;status&#039;&#039; — either &#039;&#039;Geldig&#039;&#039; (valid) or &#039;&#039;Gearchiveerd&#039;&#039; (archived); defaults to Geldig.&lt;br /&gt;
* &#039;&#039;seed_id&#039;&#039; (&amp;lt;code&amp;gt;IntegerField&amp;lt;/code&amp;gt;, unique, nullable) — the numeric ID of the source document in the Vlaamse Codex, used for deduplication to ensure no seed is used twice.&lt;br /&gt;
* &#039;&#039;seed_reference&#039;&#039; (&amp;lt;code&amp;gt;URLField&amp;lt;/code&amp;gt;) — direct API link to the source document.&lt;br /&gt;
* &#039;&#039;seed_document&#039;&#039; (&amp;lt;code&amp;gt;TextField&amp;lt;/code&amp;gt;) — human-readable label recording the type and title of the source document.&lt;br /&gt;
* &#039;&#039;generation_notes&#039;&#039; (&amp;lt;code&amp;gt;TextField&amp;lt;/code&amp;gt;) — an LLM-generated human-readable summary describing how the generated document differs from its source.&lt;br /&gt;
* &#039;&#039;revision_notes&#039;&#039; (&amp;lt;code&amp;gt;TextField&amp;lt;/code&amp;gt;) — an LLM-generated summary of the subtle details introduced during the revision stage (see below); empty if the revision was rejected or produced no changes.&lt;br /&gt;
&lt;br /&gt;
Full-text search is indexed on &#039;&#039;full_title&#039;&#039; and &#039;&#039;body&#039;&#039; via Wagtail&#039;s database search backend.&lt;br /&gt;
&lt;br /&gt;
== Generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Generation is driven by the &amp;lt;code&amp;gt;generate_decree&amp;lt;/code&amp;gt; management command, which invokes a multi-stage pipeline implemented in &amp;lt;code&amp;gt;nomos/services/generator.py&amp;lt;/code&amp;gt;. The pipeline retries up to three times if validation fails.&lt;br /&gt;
&lt;br /&gt;
=== Stage 1: Structural sourcing ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;pick_random_seed()&amp;lt;/code&amp;gt; function in &amp;lt;code&amp;gt;nomos/services/codex.py&amp;lt;/code&amp;gt; fetches up to 200 recent documents from the Vlaamse Codex open-data API (&amp;lt;code&amp;gt;codex.opendata.api.vlaanderen.be&amp;lt;/code&amp;gt;). Candidates are filtered to four allowed document types:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Codex type !! Mapped instrument&lt;br /&gt;
|-&lt;br /&gt;
| Decreet || DECREET&lt;br /&gt;
|-&lt;br /&gt;
| Besluit van de Vlaamse Regering || BESLUIT&lt;br /&gt;
|-&lt;br /&gt;
| Ministerieel besluit || BESLUIT&lt;br /&gt;
|-&lt;br /&gt;
| Omzendbrief || OMZENDBRIEF&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Documents whose &amp;lt;code&amp;gt;seed_id&amp;lt;/code&amp;gt; already exists in the database are excluded. The remaining candidates are grouped by type, and a weighted random selection favours types that have been used less frequently in the preceding seven days. For each recently used instrument, the selection weight is reduced by two per occurrence (minimum weight of 1). Once a seed is selected, the system fetches its full detail and chapter/section structure from the API.&lt;br /&gt;
&lt;br /&gt;
=== Topic inspiration ===&lt;br /&gt;
&lt;br /&gt;
Before generating, the pipeline scrapes the &#039;&#039;Belgisch Staatsblad&#039;&#039; website (&amp;lt;code&amp;gt;ejustice.just.fgov.be&amp;lt;/code&amp;gt;) for a random document title to use as thematic inspiration. To avoid reusing the same inspiration across runs, the scraper selects a random publication date from the past 90 days rather than always fetching the current edition. Federal and national references in the scraped title are replaced with Flemish equivalents using a table of 17 substitution pairs — for example, &amp;quot;Federale Overheidsdienst&amp;quot; becomes &amp;quot;Vlaamse overheidsdienst,&amp;quot; &amp;quot;Koninklijk besluit&amp;quot; becomes &amp;quot;Besluit van de Vlaamse Regering,&amp;quot; and &amp;quot;België&amp;quot; becomes &amp;quot;Vlaanderen.&amp;quot; If the scrape fails, generation proceeds without a topic hint.&lt;br /&gt;
&lt;br /&gt;
=== Stage 2: Technical mutation ===&lt;br /&gt;
&lt;br /&gt;
The seed document&#039;s title (&#039;&#039;opschrift&#039;&#039;) is sent to the OpenAI Chat Completions API (the model is configurable via the &amp;lt;code&amp;gt;OPENAI_MODEL&amp;lt;/code&amp;gt; environment variable). The system prompt instructs the model to behave as an expert in Flemish legislation and to perform a semantic shift: replace the core subject with a plausible but fictional technical equivalent while preserving the exact grammatical structure and bureaucratic tone. If a topic hint was obtained from the Belgisch Staatsblad, it is included as thematic guidance.&lt;br /&gt;
&lt;br /&gt;
=== Stage 3: Administrative drafting ===&lt;br /&gt;
&lt;br /&gt;
The tilted title, the seed document&#039;s full text (up to 4,000 characters), and its structural outline are sent to a second API call. The system prompt instructs the model to act as a legislative jurist of the Flemish government and to rewrite the source document about the new subject. Strict structural parity rules are enforced:&lt;br /&gt;
&lt;br /&gt;
* The output must contain the exact same number of chapters (&#039;&#039;hoofdstukken&#039;&#039;), sections (&#039;&#039;afdelingen&#039;&#039;), and articles (&#039;&#039;artikelen&#039;&#039;) as the source.&lt;br /&gt;
* If the source has no chapter divisions, the output must not introduce them.&lt;br /&gt;
* The total length must be comparable to the source.&lt;br /&gt;
* Content must be entirely original — only the form is emulated.&lt;br /&gt;
&lt;br /&gt;
The output is requested as clean HTML using &amp;lt;code&amp;gt;&amp;amp;lt;h2&amp;amp;gt;&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;&amp;amp;lt;h3&amp;amp;gt;&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;&amp;amp;lt;p&amp;amp;gt;&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;&amp;amp;lt;ol&amp;amp;gt;&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;&amp;amp;lt;li&amp;amp;gt;&amp;lt;/code&amp;gt; elements, without a top-level heading (which is rendered separately on the page).&lt;br /&gt;
&lt;br /&gt;
=== Stage 3b: Subtle revision ===&lt;br /&gt;
&lt;br /&gt;
After drafting, the full text is sent to an additional API call that introduces &#039;&#039;defamiliarisation through precision&#039;&#039;: the model is instructed to locate 3 to 5 passages dealing with execution, control, materials, or conditions, and to make a single local detail in each slightly more specific or procedural than necessary — for example, adding an unexpectedly precise measurement, a format requirement, or a procedural substep. The changes must be strictly additive; no text may be removed or truncated. Two programmatic guards run before the revision is accepted: an identity check rejects revisions that return the text unchanged, and a length check rejects revisions where the word count drops below 95% of the original (indicating deleted content). If a revision fails either guard, it is retried once. A separate validation call (the &#039;&#039;revision scrub&#039;&#039;) then checks whether the revision shifted the main subject, introduced too many or contextually inappropriate details, or deleted content. If the revision is rejected or produced no changes, the unrevised text is used.&lt;br /&gt;
&lt;br /&gt;
=== Stage 4: Juridical scrub ===&lt;br /&gt;
&lt;br /&gt;
The generated HTML is submitted to a validation call in which the model acts as a quality controller. It checks for the presence of narrative, poetic, or metaphorical language; references to fiction, imagination, or art; and humor or irony. Documents that do not read as authentic administrative texts are rejected with a reason. If all three attempts fail validation, the pipeline raises an error.&lt;br /&gt;
&lt;br /&gt;
=== Stage 5: Generation notes ===&lt;br /&gt;
&lt;br /&gt;
After successful validation, an API call generates a human-readable summary of the transformation. The model is instructed to act as an archivist and to describe in one or two plain-language sentences how the new document&#039;s subject differs from the original, without technical jargon. If the subtle revision was accepted, a second call compares the pre- and post-revision texts word by word and produces a bullet-point list of the specific passages that were changed, stored as &#039;&#039;revision_notes&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== Text sanitisation ===&lt;br /&gt;
&lt;br /&gt;
All generated text is passed through a sanitisation function that strips Unicode control characters (C0/C1 range, excluding newlines and tabs), applies NFC normalisation, removes empty list items and orphaned list wrappers from the HTML, and cleans whitespace artifacts. This addresses a known issue where some language models occasionally emit ASCII control characters in place of Unicode punctuation.&lt;br /&gt;
&lt;br /&gt;
=== Persistence ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;save_decree()&amp;lt;/code&amp;gt; function in &amp;lt;code&amp;gt;nomos/services/storage.py&amp;lt;/code&amp;gt; creates a &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; as a child of the &amp;lt;code&amp;gt;DecreeIndexPage&amp;lt;/code&amp;gt;. The slug is derived from the title (truncated to 200 characters) and made unique by appending a numeric suffix if necessary. The page is published immediately via Wagtail&#039;s &amp;lt;code&amp;gt;save_revision().publish()&amp;lt;/code&amp;gt; mechanism. The publication date is set to the current date and the status defaults to &#039;&#039;Geldig&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
== Anti-sameness system ==&lt;br /&gt;
&lt;br /&gt;
To prevent the archive from becoming repetitive, the system employs two mechanisms:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Seed deduplication&#039;&#039;&#039;: the &amp;lt;code&amp;gt;seed_id&amp;lt;/code&amp;gt; field (unique integer) ensures that no Vlaamse Codex document is used as a source more than once. Before selecting a seed, the pipeline queries all existing &amp;lt;code&amp;gt;seed_id&amp;lt;/code&amp;gt; values and excludes them from the candidate pool.&lt;br /&gt;
* &#039;&#039;&#039;Type-weighted selection&#039;&#039;&#039;: the &amp;lt;code&amp;gt;_get_recent_type_counts()&amp;lt;/code&amp;gt; function counts how many times each instrument type has appeared in the last seven days. Types with higher recent counts receive proportionally lower selection weights, encouraging the system to alternate between decrees, orders, and circulars.&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Index page ===&lt;br /&gt;
&lt;br /&gt;
The index page lists all published &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; children ordered by publication date (newest first). A search bar and an instrument type dropdown filter are provided. Search uses Wagtail&#039;s database search backend, querying the &#039;&#039;full_title&#039;&#039; and &#039;&#039;body&#039;&#039; fields. The type filter applies an exact match on the &#039;&#039;instrument&#039;&#039; field. Each list entry displays the full title, publication date, instrument type label, and a colour-coded status tag (green for &#039;&#039;Geldig&#039;&#039;, grey for &#039;&#039;Gearchiveerd&#039;&#039;).&lt;br /&gt;
&lt;br /&gt;
=== Document detail page ===&lt;br /&gt;
&lt;br /&gt;
Each decree page displays:&lt;br /&gt;
&lt;br /&gt;
* A breadcrumb navigation link back to the index page.&lt;br /&gt;
* The full title as a top-level heading.&lt;br /&gt;
* A GOV.UK-style summary list with key/value rows for document type, publication date, and status (rendered as a tag badge).&lt;br /&gt;
* The full decree body rendered as rich text.&lt;br /&gt;
&lt;br /&gt;
=== Visual design ===&lt;br /&gt;
&lt;br /&gt;
The interface is very loosely inspired by the [[GOV.UK Design System]]. The base template features a dark masthead with a yellow (&amp;lt;code&amp;gt;#ffe615&amp;lt;/code&amp;gt;) accent border, the site name &amp;quot;Nomos&amp;quot; as a navigation link, and a dark/light mode toggle button. The toggle uses CSS custom properties for theming and persists the user&#039;s preference in &amp;lt;code&amp;gt;localStorage&amp;lt;/code&amp;gt;. In dark mode, the masthead shifts to &amp;lt;code&amp;gt;#1a1a1a&amp;lt;/code&amp;gt;, links become lighter, and status tag colours are adjusted for contrast. A responsive layout collapses the summary list key/value pairs into a single column below 576px.&lt;br /&gt;
&lt;br /&gt;
== Administration ==&lt;br /&gt;
&lt;br /&gt;
The application uses the standard Wagtail admin interface. &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; content panels expose the instrument type, full title, body, publication date, and status. A separate &amp;quot;Generatie-informatie&amp;quot; settings panel groups the seed ID, seed reference URL, seed document description, generation notes, and revision notes — metadata that is recorded automatically during generation and is accessible to editors but not displayed on the public site.&lt;br /&gt;
&lt;br /&gt;
The Django admin is available for lower-level database access.&lt;br /&gt;
&lt;br /&gt;
== Logging ==&lt;br /&gt;
&lt;br /&gt;
Application logging is configured with two handlers: console output and a rotating file log (&amp;lt;code&amp;gt;nomos.log&amp;lt;/code&amp;gt; in the project root). The &amp;lt;code&amp;gt;nomos&amp;lt;/code&amp;gt; logger is set to &amp;lt;code&amp;gt;INFO&amp;lt;/code&amp;gt; level and records each pipeline stage (seed selection, title tilting, drafting, validation, publication) with document identifiers and truncated titles.&lt;br /&gt;
&lt;br /&gt;
== Management commands ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Command !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;generate_decree&amp;lt;/code&amp;gt; || Run the full generation pipeline: fetch seed, tilt title, draft decree, apply subtle revision, validate, generate notes, and publish as a Wagtail page&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;setup_index_page&amp;lt;/code&amp;gt; || Create the &amp;lt;code&amp;gt;DecreeIndexPage&amp;lt;/code&amp;gt; as the site root (idempotent); removes the default Wagtail welcome page if present&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
The planned production deployment targets &amp;lt;code&amp;gt;nomos.yusupov.cloud&amp;lt;/code&amp;gt; on a Hetzner VPS running [[Nginx]] as a reverse proxy, [[Gunicorn]] as the WSGI application server, and [[PostgreSQL]] as the production database (replacing SQLite). TLS is to be provided by [[Let&#039;s Encrypt]] via Certbot. Daily generation is to be scheduled via cron calling &amp;lt;code&amp;gt;python manage.py generate_decree&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Wagtail (CMS)]]&lt;br /&gt;
* [[Vlaamse Codex]]&lt;br /&gt;
* [[Computational creativity]]&lt;br /&gt;
* [[Procedural generation]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Nomos&amp;diff=410</id>
		<title>Nomos</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Nomos&amp;diff=410"/>
		<updated>2026-04-20T15:41:00Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Technology stack */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| 01_name         = Nomos&lt;br /&gt;
| 02_url          = https://nomos.yusupov.cloud&lt;br /&gt;
| 03_developer    = Michel Vuijlsteke&lt;br /&gt;
| 04_released     = 2026&lt;br /&gt;
| 05_genre        = AI-generated legal document archive&lt;br /&gt;
| 06_language     = Python&lt;br /&gt;
| 07_framework    = [[Django]] 6.0 / [[Wagtail]] 7.3&lt;br /&gt;
| 08_license      = Proprietary&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nomos&#039;&#039;&#039; is a web application hosted at &amp;lt;code&amp;gt;nomos.yusupov.cloud&amp;lt;/code&amp;gt; that generates and publishes AI-created Flemish administrative legislation. Each day, the system fetches a real document from the Vlaamse Codex open-data API, performs a semantic shift on its subject matter using a large language model, and produces a structurally faithful but entirely fictional legal text — a decree, ministerial order, or circular — that reads as if it were published in the &#039;&#039;Belgisch Staatsblad&#039;&#039;. The generated documents are stored in a [[Wagtail (CMS)|Wagtail]] content management system and presented through a GOV.UK-inspired Dutch-language public interface. The name &#039;&#039;Nomos&#039;&#039; derives from the [[Ancient Greek|Greek]] word νόμος, meaning &amp;quot;law.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Django 6.0.4 with Wagtail 7.3.1 as its content management framework and [[SQLite]] as its development database.&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt lists Django 6.0.4, wagtail 7.3.1, openai 2.32.0, requests 2.33.1, python-dotenv 1.2.2, beautifulsoup4 4.14.3, and Pillow 12.2.0.&amp;lt;/ref&amp;gt; Additional dependencies include the [[OpenAI]] Python client for language-model calls, [[Requests (software)|Requests]] for HTTP communication with the Vlaamse Codex API, [[Beautiful Soup (HTML parser)|Beautiful Soup]] for scraping title inspiration from the Belgisch Staatsblad, [[Pillow (imaging library)|Pillow]] as a Wagtail dependency, and [[python-dotenv]] for environment configuration. The front end uses [[Bootstrap]] 5.3.8 loaded from the jsDelivr CDN with subresource integrity hashes. All user interface text is in Dutch.&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
The data model uses Wagtail&#039;s page tree. A singleton &amp;lt;code&amp;gt;DecreeIndexPage&amp;lt;/code&amp;gt; (limited to &amp;lt;code&amp;gt;max_count = 1&amp;lt;/code&amp;gt;) serves as the parent of all generated documents. Each document is a &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; with the following fields:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;instrument&#039;&#039; — one of four types: Decreet, Besluit, Omzendbrief, or Reglement.&lt;br /&gt;
* &#039;&#039;full_title&#039;&#039; (&amp;lt;code&amp;gt;TextField&amp;lt;/code&amp;gt;) — the complete title, unlimited in length. The standard Wagtail &#039;&#039;title&#039;&#039; field (255-character limit) holds a truncated copy for internal use.&lt;br /&gt;
* &#039;&#039;body&#039;&#039; (&amp;lt;code&amp;gt;RichTextField&amp;lt;/code&amp;gt;) — the full HTML text of the generated legislation.&lt;br /&gt;
* &#039;&#039;publication_date&#039;&#039; (&amp;lt;code&amp;gt;DateField&amp;lt;/code&amp;gt;) — set to the generation date.&lt;br /&gt;
* &#039;&#039;status&#039;&#039; — either &#039;&#039;Geldig&#039;&#039; (valid) or &#039;&#039;Gearchiveerd&#039;&#039; (archived); defaults to Geldig.&lt;br /&gt;
* &#039;&#039;seed_id&#039;&#039; (&amp;lt;code&amp;gt;IntegerField&amp;lt;/code&amp;gt;, unique, nullable) — the numeric ID of the source document in the Vlaamse Codex, used for deduplication to ensure no seed is used twice.&lt;br /&gt;
* &#039;&#039;seed_reference&#039;&#039; (&amp;lt;code&amp;gt;URLField&amp;lt;/code&amp;gt;) — direct API link to the source document.&lt;br /&gt;
* &#039;&#039;seed_document&#039;&#039; (&amp;lt;code&amp;gt;TextField&amp;lt;/code&amp;gt;) — human-readable label recording the type and title of the source document.&lt;br /&gt;
* &#039;&#039;generation_notes&#039;&#039; (&amp;lt;code&amp;gt;TextField&amp;lt;/code&amp;gt;) — an LLM-generated human-readable summary describing how the generated document differs from its source.&lt;br /&gt;
* &#039;&#039;revision_notes&#039;&#039; (&amp;lt;code&amp;gt;TextField&amp;lt;/code&amp;gt;) — an LLM-generated summary of the subtle details introduced during the revision stage (see below); empty if the revision was rejected or produced no changes.&lt;br /&gt;
&lt;br /&gt;
Full-text search is indexed on &#039;&#039;full_title&#039;&#039; and &#039;&#039;body&#039;&#039; via Wagtail&#039;s database search backend.&lt;br /&gt;
&lt;br /&gt;
== Generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Generation is driven by the &amp;lt;code&amp;gt;generate_decree&amp;lt;/code&amp;gt; management command, which invokes a multi-stage pipeline implemented in &amp;lt;code&amp;gt;nomos/services/generator.py&amp;lt;/code&amp;gt;. The pipeline retries up to three times if validation fails.&lt;br /&gt;
&lt;br /&gt;
=== Stage 1: Structural sourcing ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;pick_random_seed()&amp;lt;/code&amp;gt; function in &amp;lt;code&amp;gt;nomos/services/codex.py&amp;lt;/code&amp;gt; fetches up to 200 recent documents from the Vlaamse Codex open-data API (&amp;lt;code&amp;gt;codex.opendata.api.vlaanderen.be&amp;lt;/code&amp;gt;). Candidates are filtered to four allowed document types:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Codex type !! Mapped instrument&lt;br /&gt;
|-&lt;br /&gt;
| Decreet || DECREET&lt;br /&gt;
|-&lt;br /&gt;
| Besluit van de Vlaamse Regering || BESLUIT&lt;br /&gt;
|-&lt;br /&gt;
| Ministerieel besluit || BESLUIT&lt;br /&gt;
|-&lt;br /&gt;
| Omzendbrief || OMZENDBRIEF&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Documents whose &amp;lt;code&amp;gt;seed_id&amp;lt;/code&amp;gt; already exists in the database are excluded. The remaining candidates are grouped by type, and a weighted random selection favours types that have been used less frequently in the preceding seven days. For each recently used instrument, the selection weight is reduced by two per occurrence (minimum weight of 1). Once a seed is selected, the system fetches its full detail and chapter/section structure from the API.&lt;br /&gt;
&lt;br /&gt;
=== Topic inspiration ===&lt;br /&gt;
&lt;br /&gt;
Before generating, the pipeline scrapes the &#039;&#039;Belgisch Staatsblad&#039;&#039; website (&amp;lt;code&amp;gt;ejustice.just.fgov.be&amp;lt;/code&amp;gt;) for a random document title to use as thematic inspiration. To avoid reusing the same inspiration across runs, the scraper selects a random publication date from the past 90 days rather than always fetching the current edition. Federal and national references in the scraped title are replaced with Flemish equivalents using a table of 17 substitution pairs — for example, &amp;quot;Federale Overheidsdienst&amp;quot; becomes &amp;quot;Vlaamse overheidsdienst,&amp;quot; &amp;quot;Koninklijk besluit&amp;quot; becomes &amp;quot;Besluit van de Vlaamse Regering,&amp;quot; and &amp;quot;België&amp;quot; becomes &amp;quot;Vlaanderen.&amp;quot; If the scrape fails, generation proceeds without a topic hint.&lt;br /&gt;
&lt;br /&gt;
=== Stage 2: Technical mutation ===&lt;br /&gt;
&lt;br /&gt;
The seed document&#039;s title (&#039;&#039;opschrift&#039;&#039;) is sent to the OpenAI Chat Completions API (default model: GPT-5, configurable via the &amp;lt;code&amp;gt;OPENAI_MODEL&amp;lt;/code&amp;gt; environment variable). The system prompt instructs the model to behave as an expert in Flemish legislation and to perform a semantic shift: replace the core subject with a plausible but fictional technical equivalent while preserving the exact grammatical structure and bureaucratic tone. If a topic hint was obtained from the Belgisch Staatsblad, it is included as thematic guidance.&amp;lt;ref name=&amp;quot;temperature&amp;quot;&amp;gt;GPT-5 does not support custom temperature values. All API calls use the model&#039;s default temperature (1).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Stage 3: Administrative drafting ===&lt;br /&gt;
&lt;br /&gt;
The tilted title, the seed document&#039;s full text (up to 4,000 characters), and its structural outline are sent to a second API call. The system prompt instructs the model to act as a legislative jurist of the Flemish government and to rewrite the source document about the new subject. Strict structural parity rules are enforced:&lt;br /&gt;
&lt;br /&gt;
* The output must contain the exact same number of chapters (&#039;&#039;hoofdstukken&#039;&#039;), sections (&#039;&#039;afdelingen&#039;&#039;), and articles (&#039;&#039;artikelen&#039;&#039;) as the source.&lt;br /&gt;
* If the source has no chapter divisions, the output must not introduce them.&lt;br /&gt;
* The total length must be comparable to the source.&lt;br /&gt;
* Content must be entirely original — only the form is emulated.&lt;br /&gt;
&lt;br /&gt;
The output is requested as clean HTML using &amp;lt;code&amp;gt;&amp;amp;lt;h2&amp;amp;gt;&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;&amp;amp;lt;h3&amp;amp;gt;&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;&amp;amp;lt;p&amp;amp;gt;&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;&amp;amp;lt;ol&amp;amp;gt;&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;&amp;amp;lt;li&amp;amp;gt;&amp;lt;/code&amp;gt; elements, without a top-level heading (which is rendered separately on the page).&lt;br /&gt;
&lt;br /&gt;
=== Stage 3b: Subtle revision ===&lt;br /&gt;
&lt;br /&gt;
After drafting, the full text is sent to an additional API call that introduces &#039;&#039;defamiliarisation through precision&#039;&#039;: the model is instructed to locate 3 to 5 passages dealing with execution, control, materials, or conditions, and to make a single local detail in each slightly more specific or procedural than necessary — for example, adding an unexpectedly precise measurement, a format requirement, or a procedural substep. The changes must be strictly additive; no text may be removed or truncated. Two programmatic guards run before the revision is accepted: an identity check rejects revisions that return the text unchanged, and a length check rejects revisions where the word count drops below 95% of the original (indicating deleted content). If a revision fails either guard, it is retried once. A separate validation call (the &#039;&#039;revision scrub&#039;&#039;) then checks whether the revision shifted the main subject, introduced too many or contextually inappropriate details, or deleted content. If the revision is rejected or produced no changes, the unrevised text is used.&lt;br /&gt;
&lt;br /&gt;
=== Stage 4: Juridical scrub ===&lt;br /&gt;
&lt;br /&gt;
The generated HTML is submitted to a validation call in which the model acts as a quality controller. It checks for the presence of narrative, poetic, or metaphorical language; references to fiction, imagination, or art; and humor or irony. Documents that do not read as authentic administrative texts are rejected with a reason. If all three attempts fail validation, the pipeline raises an error.&lt;br /&gt;
&lt;br /&gt;
=== Stage 5: Generation notes ===&lt;br /&gt;
&lt;br /&gt;
After successful validation, an API call generates a human-readable summary of the transformation. The model is instructed to act as an archivist and to describe in one or two plain-language sentences how the new document&#039;s subject differs from the original, without technical jargon. If the subtle revision was accepted, a second call compares the pre- and post-revision texts word by word and produces a bullet-point list of the specific passages that were changed, stored as &#039;&#039;revision_notes&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== Text sanitisation ===&lt;br /&gt;
&lt;br /&gt;
All generated text is passed through a sanitisation function that strips Unicode control characters (C0/C1 range, excluding newlines and tabs), applies NFC normalisation, removes empty list items and orphaned list wrappers from the HTML, and cleans whitespace artifacts. This addresses a known issue where GPT-5 occasionally emits ASCII control characters in place of Unicode punctuation.&lt;br /&gt;
&lt;br /&gt;
=== Persistence ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;save_decree()&amp;lt;/code&amp;gt; function in &amp;lt;code&amp;gt;nomos/services/storage.py&amp;lt;/code&amp;gt; creates a &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; as a child of the &amp;lt;code&amp;gt;DecreeIndexPage&amp;lt;/code&amp;gt;. The slug is derived from the title (truncated to 200 characters) and made unique by appending a numeric suffix if necessary. The page is published immediately via Wagtail&#039;s &amp;lt;code&amp;gt;save_revision().publish()&amp;lt;/code&amp;gt; mechanism. The publication date is set to the current date and the status defaults to &#039;&#039;Geldig&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
== Anti-sameness system ==&lt;br /&gt;
&lt;br /&gt;
To prevent the archive from becoming repetitive, the system employs two mechanisms:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Seed deduplication&#039;&#039;&#039;: the &amp;lt;code&amp;gt;seed_id&amp;lt;/code&amp;gt; field (unique integer) ensures that no Vlaamse Codex document is used as a source more than once. Before selecting a seed, the pipeline queries all existing &amp;lt;code&amp;gt;seed_id&amp;lt;/code&amp;gt; values and excludes them from the candidate pool.&lt;br /&gt;
* &#039;&#039;&#039;Type-weighted selection&#039;&#039;&#039;: the &amp;lt;code&amp;gt;_get_recent_type_counts()&amp;lt;/code&amp;gt; function counts how many times each instrument type has appeared in the last seven days. Types with higher recent counts receive proportionally lower selection weights, encouraging the system to alternate between decrees, orders, and circulars.&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Index page ===&lt;br /&gt;
&lt;br /&gt;
The index page lists all published &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; children ordered by publication date (newest first). A search bar and an instrument type dropdown filter are provided. Search uses Wagtail&#039;s database search backend, querying the &#039;&#039;full_title&#039;&#039; and &#039;&#039;body&#039;&#039; fields. The type filter applies an exact match on the &#039;&#039;instrument&#039;&#039; field. Each list entry displays the full title, publication date, instrument type label, and a colour-coded status tag (green for &#039;&#039;Geldig&#039;&#039;, grey for &#039;&#039;Gearchiveerd&#039;&#039;).&lt;br /&gt;
&lt;br /&gt;
=== Document detail page ===&lt;br /&gt;
&lt;br /&gt;
Each decree page displays:&lt;br /&gt;
&lt;br /&gt;
* A breadcrumb navigation link back to the index page.&lt;br /&gt;
* The full title as a top-level heading.&lt;br /&gt;
* A GOV.UK-style summary list with key/value rows for document type, publication date, and status (rendered as a tag badge).&lt;br /&gt;
* The full decree body rendered as rich text.&lt;br /&gt;
&lt;br /&gt;
=== Visual design ===&lt;br /&gt;
&lt;br /&gt;
The interface is very loosely inspired by the [[GOV.UK Design System]]. The base template features a dark masthead with a yellow (&amp;lt;code&amp;gt;#ffe615&amp;lt;/code&amp;gt;) accent border, the site name &amp;quot;Nomos&amp;quot; as a navigation link, and a dark/light mode toggle button. The toggle uses CSS custom properties for theming and persists the user&#039;s preference in &amp;lt;code&amp;gt;localStorage&amp;lt;/code&amp;gt;. In dark mode, the masthead shifts to &amp;lt;code&amp;gt;#1a1a1a&amp;lt;/code&amp;gt;, links become lighter, and status tag colours are adjusted for contrast. A responsive layout collapses the summary list key/value pairs into a single column below 576px.&lt;br /&gt;
&lt;br /&gt;
== Administration ==&lt;br /&gt;
&lt;br /&gt;
The application uses the standard Wagtail admin interface. &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; content panels expose the instrument type, full title, body, publication date, and status. A separate &amp;quot;Generatie-informatie&amp;quot; settings panel groups the seed ID, seed reference URL, seed document description, generation notes, and revision notes — metadata that is recorded automatically during generation and is accessible to editors but not displayed on the public site.&lt;br /&gt;
&lt;br /&gt;
The Django admin is available for lower-level database access.&lt;br /&gt;
&lt;br /&gt;
== Logging ==&lt;br /&gt;
&lt;br /&gt;
Application logging is configured with two handlers: console output and a rotating file log (&amp;lt;code&amp;gt;nomos.log&amp;lt;/code&amp;gt; in the project root). The &amp;lt;code&amp;gt;nomos&amp;lt;/code&amp;gt; logger is set to &amp;lt;code&amp;gt;INFO&amp;lt;/code&amp;gt; level and records each pipeline stage (seed selection, title tilting, drafting, validation, publication) with document identifiers and truncated titles.&lt;br /&gt;
&lt;br /&gt;
== Management commands ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Command !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;generate_decree&amp;lt;/code&amp;gt; || Run the full generation pipeline: fetch seed, tilt title, draft decree, apply subtle revision, validate, generate notes, and publish as a Wagtail page&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;setup_index_page&amp;lt;/code&amp;gt; || Create the &amp;lt;code&amp;gt;DecreeIndexPage&amp;lt;/code&amp;gt; as the site root (idempotent); removes the default Wagtail welcome page if present&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
The planned production deployment targets &amp;lt;code&amp;gt;nomos.yusupov.cloud&amp;lt;/code&amp;gt; on a Hetzner VPS running [[Nginx]] as a reverse proxy, [[Gunicorn]] as the WSGI application server, and [[PostgreSQL]] as the production database (replacing SQLite). TLS is to be provided by [[Let&#039;s Encrypt]] via Certbot. Daily generation is to be scheduled via cron calling &amp;lt;code&amp;gt;python manage.py generate_decree&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Wagtail (CMS)]]&lt;br /&gt;
* [[Vlaamse Codex]]&lt;br /&gt;
* [[Computational creativity]]&lt;br /&gt;
* [[Procedural generation]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Nomos&amp;diff=409</id>
		<title>Nomos</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Nomos&amp;diff=409"/>
		<updated>2026-04-20T15:40:05Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| 01_name         = Nomos&lt;br /&gt;
| 02_url          = https://nomos.yusupov.cloud&lt;br /&gt;
| 03_developer    = Michel Vuijlsteke&lt;br /&gt;
| 04_released     = 2026&lt;br /&gt;
| 05_genre        = AI-generated legal document archive&lt;br /&gt;
| 06_language     = Python&lt;br /&gt;
| 07_framework    = [[Django]] 6.0 / [[Wagtail]] 7.3&lt;br /&gt;
| 08_license      = Proprietary&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nomos&#039;&#039;&#039; is a web application hosted at &amp;lt;code&amp;gt;nomos.yusupov.cloud&amp;lt;/code&amp;gt; that generates and publishes AI-created Flemish administrative legislation. Each day, the system fetches a real document from the Vlaamse Codex open-data API, performs a semantic shift on its subject matter using a large language model, and produces a structurally faithful but entirely fictional legal text — a decree, ministerial order, or circular — that reads as if it were published in the &#039;&#039;Belgisch Staatsblad&#039;&#039;. The generated documents are stored in a [[Wagtail (CMS)|Wagtail]] content management system and presented through a GOV.UK-inspired Dutch-language public interface. The name &#039;&#039;Nomos&#039;&#039; derives from the [[Ancient Greek|Greek]] word νόμος, meaning &amp;quot;law.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Django 6.0.4 with Wagtail 7.3.1 as its content management framework and [[SQLite]] as its development database.&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt lists Django 6.0.4, wagtail 7.3.1, openai 2.32.0, requests 2.33.1, python-dotenv 1.2.2, beautifulsoup4 4.14.3, and Pillow 12.2.0.&amp;lt;/ref&amp;gt; Additional dependencies include the [[OpenAI]] Python client for language-model calls, [[Requests (software)|Requests]] for HTTP communication with the Vlaamse Codex API, [[Beautiful Soup (HTML parser)|Beautiful Soup]] for scraping title inspiration from the Belgisch Staatsblad, [[Pillow (imaging library)|Pillow]] as a Wagtail dependency, and [[python-dotenv]] for environment configuration. The front end uses [[Bootstrap]] 5.3.8 loaded from the jsDelivr CDN with subresource integrity hashes. All user interface text is in Dutch. The planned production deployment targets a Hetzner VPS behind [[Nginx]] with [[Gunicorn]] and [[PostgreSQL]].&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
The data model uses Wagtail&#039;s page tree. A singleton &amp;lt;code&amp;gt;DecreeIndexPage&amp;lt;/code&amp;gt; (limited to &amp;lt;code&amp;gt;max_count = 1&amp;lt;/code&amp;gt;) serves as the parent of all generated documents. Each document is a &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; with the following fields:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;instrument&#039;&#039; — one of four types: Decreet, Besluit, Omzendbrief, or Reglement.&lt;br /&gt;
* &#039;&#039;full_title&#039;&#039; (&amp;lt;code&amp;gt;TextField&amp;lt;/code&amp;gt;) — the complete title, unlimited in length. The standard Wagtail &#039;&#039;title&#039;&#039; field (255-character limit) holds a truncated copy for internal use.&lt;br /&gt;
* &#039;&#039;body&#039;&#039; (&amp;lt;code&amp;gt;RichTextField&amp;lt;/code&amp;gt;) — the full HTML text of the generated legislation.&lt;br /&gt;
* &#039;&#039;publication_date&#039;&#039; (&amp;lt;code&amp;gt;DateField&amp;lt;/code&amp;gt;) — set to the generation date.&lt;br /&gt;
* &#039;&#039;status&#039;&#039; — either &#039;&#039;Geldig&#039;&#039; (valid) or &#039;&#039;Gearchiveerd&#039;&#039; (archived); defaults to Geldig.&lt;br /&gt;
* &#039;&#039;seed_id&#039;&#039; (&amp;lt;code&amp;gt;IntegerField&amp;lt;/code&amp;gt;, unique, nullable) — the numeric ID of the source document in the Vlaamse Codex, used for deduplication to ensure no seed is used twice.&lt;br /&gt;
* &#039;&#039;seed_reference&#039;&#039; (&amp;lt;code&amp;gt;URLField&amp;lt;/code&amp;gt;) — direct API link to the source document.&lt;br /&gt;
* &#039;&#039;seed_document&#039;&#039; (&amp;lt;code&amp;gt;TextField&amp;lt;/code&amp;gt;) — human-readable label recording the type and title of the source document.&lt;br /&gt;
* &#039;&#039;generation_notes&#039;&#039; (&amp;lt;code&amp;gt;TextField&amp;lt;/code&amp;gt;) — an LLM-generated human-readable summary describing how the generated document differs from its source.&lt;br /&gt;
* &#039;&#039;revision_notes&#039;&#039; (&amp;lt;code&amp;gt;TextField&amp;lt;/code&amp;gt;) — an LLM-generated summary of the subtle details introduced during the revision stage (see below); empty if the revision was rejected or produced no changes.&lt;br /&gt;
&lt;br /&gt;
Full-text search is indexed on &#039;&#039;full_title&#039;&#039; and &#039;&#039;body&#039;&#039; via Wagtail&#039;s database search backend.&lt;br /&gt;
&lt;br /&gt;
== Generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Generation is driven by the &amp;lt;code&amp;gt;generate_decree&amp;lt;/code&amp;gt; management command, which invokes a multi-stage pipeline implemented in &amp;lt;code&amp;gt;nomos/services/generator.py&amp;lt;/code&amp;gt;. The pipeline retries up to three times if validation fails.&lt;br /&gt;
&lt;br /&gt;
=== Stage 1: Structural sourcing ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;pick_random_seed()&amp;lt;/code&amp;gt; function in &amp;lt;code&amp;gt;nomos/services/codex.py&amp;lt;/code&amp;gt; fetches up to 200 recent documents from the Vlaamse Codex open-data API (&amp;lt;code&amp;gt;codex.opendata.api.vlaanderen.be&amp;lt;/code&amp;gt;). Candidates are filtered to four allowed document types:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Codex type !! Mapped instrument&lt;br /&gt;
|-&lt;br /&gt;
| Decreet || DECREET&lt;br /&gt;
|-&lt;br /&gt;
| Besluit van de Vlaamse Regering || BESLUIT&lt;br /&gt;
|-&lt;br /&gt;
| Ministerieel besluit || BESLUIT&lt;br /&gt;
|-&lt;br /&gt;
| Omzendbrief || OMZENDBRIEF&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Documents whose &amp;lt;code&amp;gt;seed_id&amp;lt;/code&amp;gt; already exists in the database are excluded. The remaining candidates are grouped by type, and a weighted random selection favours types that have been used less frequently in the preceding seven days. For each recently used instrument, the selection weight is reduced by two per occurrence (minimum weight of 1). Once a seed is selected, the system fetches its full detail and chapter/section structure from the API.&lt;br /&gt;
&lt;br /&gt;
=== Topic inspiration ===&lt;br /&gt;
&lt;br /&gt;
Before generating, the pipeline scrapes the &#039;&#039;Belgisch Staatsblad&#039;&#039; website (&amp;lt;code&amp;gt;ejustice.just.fgov.be&amp;lt;/code&amp;gt;) for a random document title to use as thematic inspiration. To avoid reusing the same inspiration across runs, the scraper selects a random publication date from the past 90 days rather than always fetching the current edition. Federal and national references in the scraped title are replaced with Flemish equivalents using a table of 17 substitution pairs — for example, &amp;quot;Federale Overheidsdienst&amp;quot; becomes &amp;quot;Vlaamse overheidsdienst,&amp;quot; &amp;quot;Koninklijk besluit&amp;quot; becomes &amp;quot;Besluit van de Vlaamse Regering,&amp;quot; and &amp;quot;België&amp;quot; becomes &amp;quot;Vlaanderen.&amp;quot; If the scrape fails, generation proceeds without a topic hint.&lt;br /&gt;
&lt;br /&gt;
=== Stage 2: Technical mutation ===&lt;br /&gt;
&lt;br /&gt;
The seed document&#039;s title (&#039;&#039;opschrift&#039;&#039;) is sent to the OpenAI Chat Completions API (default model: GPT-5, configurable via the &amp;lt;code&amp;gt;OPENAI_MODEL&amp;lt;/code&amp;gt; environment variable). The system prompt instructs the model to behave as an expert in Flemish legislation and to perform a semantic shift: replace the core subject with a plausible but fictional technical equivalent while preserving the exact grammatical structure and bureaucratic tone. If a topic hint was obtained from the Belgisch Staatsblad, it is included as thematic guidance.&amp;lt;ref name=&amp;quot;temperature&amp;quot;&amp;gt;GPT-5 does not support custom temperature values. All API calls use the model&#039;s default temperature (1).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Stage 3: Administrative drafting ===&lt;br /&gt;
&lt;br /&gt;
The tilted title, the seed document&#039;s full text (up to 4,000 characters), and its structural outline are sent to a second API call. The system prompt instructs the model to act as a legislative jurist of the Flemish government and to rewrite the source document about the new subject. Strict structural parity rules are enforced:&lt;br /&gt;
&lt;br /&gt;
* The output must contain the exact same number of chapters (&#039;&#039;hoofdstukken&#039;&#039;), sections (&#039;&#039;afdelingen&#039;&#039;), and articles (&#039;&#039;artikelen&#039;&#039;) as the source.&lt;br /&gt;
* If the source has no chapter divisions, the output must not introduce them.&lt;br /&gt;
* The total length must be comparable to the source.&lt;br /&gt;
* Content must be entirely original — only the form is emulated.&lt;br /&gt;
&lt;br /&gt;
The output is requested as clean HTML using &amp;lt;code&amp;gt;&amp;amp;lt;h2&amp;amp;gt;&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;&amp;amp;lt;h3&amp;amp;gt;&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;&amp;amp;lt;p&amp;amp;gt;&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;&amp;amp;lt;ol&amp;amp;gt;&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;&amp;amp;lt;li&amp;amp;gt;&amp;lt;/code&amp;gt; elements, without a top-level heading (which is rendered separately on the page).&lt;br /&gt;
&lt;br /&gt;
=== Stage 3b: Subtle revision ===&lt;br /&gt;
&lt;br /&gt;
After drafting, the full text is sent to an additional API call that introduces &#039;&#039;defamiliarisation through precision&#039;&#039;: the model is instructed to locate 3 to 5 passages dealing with execution, control, materials, or conditions, and to make a single local detail in each slightly more specific or procedural than necessary — for example, adding an unexpectedly precise measurement, a format requirement, or a procedural substep. The changes must be strictly additive; no text may be removed or truncated. Two programmatic guards run before the revision is accepted: an identity check rejects revisions that return the text unchanged, and a length check rejects revisions where the word count drops below 95% of the original (indicating deleted content). If a revision fails either guard, it is retried once. A separate validation call (the &#039;&#039;revision scrub&#039;&#039;) then checks whether the revision shifted the main subject, introduced too many or contextually inappropriate details, or deleted content. If the revision is rejected or produced no changes, the unrevised text is used.&lt;br /&gt;
&lt;br /&gt;
=== Stage 4: Juridical scrub ===&lt;br /&gt;
&lt;br /&gt;
The generated HTML is submitted to a validation call in which the model acts as a quality controller. It checks for the presence of narrative, poetic, or metaphorical language; references to fiction, imagination, or art; and humor or irony. Documents that do not read as authentic administrative texts are rejected with a reason. If all three attempts fail validation, the pipeline raises an error.&lt;br /&gt;
&lt;br /&gt;
=== Stage 5: Generation notes ===&lt;br /&gt;
&lt;br /&gt;
After successful validation, an API call generates a human-readable summary of the transformation. The model is instructed to act as an archivist and to describe in one or two plain-language sentences how the new document&#039;s subject differs from the original, without technical jargon. If the subtle revision was accepted, a second call compares the pre- and post-revision texts word by word and produces a bullet-point list of the specific passages that were changed, stored as &#039;&#039;revision_notes&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== Text sanitisation ===&lt;br /&gt;
&lt;br /&gt;
All generated text is passed through a sanitisation function that strips Unicode control characters (C0/C1 range, excluding newlines and tabs), applies NFC normalisation, removes empty list items and orphaned list wrappers from the HTML, and cleans whitespace artifacts. This addresses a known issue where GPT-5 occasionally emits ASCII control characters in place of Unicode punctuation.&lt;br /&gt;
&lt;br /&gt;
=== Persistence ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;save_decree()&amp;lt;/code&amp;gt; function in &amp;lt;code&amp;gt;nomos/services/storage.py&amp;lt;/code&amp;gt; creates a &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; as a child of the &amp;lt;code&amp;gt;DecreeIndexPage&amp;lt;/code&amp;gt;. The slug is derived from the title (truncated to 200 characters) and made unique by appending a numeric suffix if necessary. The page is published immediately via Wagtail&#039;s &amp;lt;code&amp;gt;save_revision().publish()&amp;lt;/code&amp;gt; mechanism. The publication date is set to the current date and the status defaults to &#039;&#039;Geldig&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
== Anti-sameness system ==&lt;br /&gt;
&lt;br /&gt;
To prevent the archive from becoming repetitive, the system employs two mechanisms:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Seed deduplication&#039;&#039;&#039;: the &amp;lt;code&amp;gt;seed_id&amp;lt;/code&amp;gt; field (unique integer) ensures that no Vlaamse Codex document is used as a source more than once. Before selecting a seed, the pipeline queries all existing &amp;lt;code&amp;gt;seed_id&amp;lt;/code&amp;gt; values and excludes them from the candidate pool.&lt;br /&gt;
* &#039;&#039;&#039;Type-weighted selection&#039;&#039;&#039;: the &amp;lt;code&amp;gt;_get_recent_type_counts()&amp;lt;/code&amp;gt; function counts how many times each instrument type has appeared in the last seven days. Types with higher recent counts receive proportionally lower selection weights, encouraging the system to alternate between decrees, orders, and circulars.&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Index page ===&lt;br /&gt;
&lt;br /&gt;
The index page lists all published &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; children ordered by publication date (newest first). A search bar and an instrument type dropdown filter are provided. Search uses Wagtail&#039;s database search backend, querying the &#039;&#039;full_title&#039;&#039; and &#039;&#039;body&#039;&#039; fields. The type filter applies an exact match on the &#039;&#039;instrument&#039;&#039; field. Each list entry displays the full title, publication date, instrument type label, and a colour-coded status tag (green for &#039;&#039;Geldig&#039;&#039;, grey for &#039;&#039;Gearchiveerd&#039;&#039;).&lt;br /&gt;
&lt;br /&gt;
=== Document detail page ===&lt;br /&gt;
&lt;br /&gt;
Each decree page displays:&lt;br /&gt;
&lt;br /&gt;
* A breadcrumb navigation link back to the index page.&lt;br /&gt;
* The full title as a top-level heading.&lt;br /&gt;
* A GOV.UK-style summary list with key/value rows for document type, publication date, and status (rendered as a tag badge).&lt;br /&gt;
* The full decree body rendered as rich text.&lt;br /&gt;
&lt;br /&gt;
=== Visual design ===&lt;br /&gt;
&lt;br /&gt;
The interface is very loosely inspired by the [[GOV.UK Design System]]. The base template features a dark masthead with a yellow (&amp;lt;code&amp;gt;#ffe615&amp;lt;/code&amp;gt;) accent border, the site name &amp;quot;Nomos&amp;quot; as a navigation link, and a dark/light mode toggle button. The toggle uses CSS custom properties for theming and persists the user&#039;s preference in &amp;lt;code&amp;gt;localStorage&amp;lt;/code&amp;gt;. In dark mode, the masthead shifts to &amp;lt;code&amp;gt;#1a1a1a&amp;lt;/code&amp;gt;, links become lighter, and status tag colours are adjusted for contrast. A responsive layout collapses the summary list key/value pairs into a single column below 576px.&lt;br /&gt;
&lt;br /&gt;
== Administration ==&lt;br /&gt;
&lt;br /&gt;
The application uses the standard Wagtail admin interface. &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; content panels expose the instrument type, full title, body, publication date, and status. A separate &amp;quot;Generatie-informatie&amp;quot; settings panel groups the seed ID, seed reference URL, seed document description, generation notes, and revision notes — metadata that is recorded automatically during generation and is accessible to editors but not displayed on the public site.&lt;br /&gt;
&lt;br /&gt;
The Django admin is available for lower-level database access.&lt;br /&gt;
&lt;br /&gt;
== Logging ==&lt;br /&gt;
&lt;br /&gt;
Application logging is configured with two handlers: console output and a rotating file log (&amp;lt;code&amp;gt;nomos.log&amp;lt;/code&amp;gt; in the project root). The &amp;lt;code&amp;gt;nomos&amp;lt;/code&amp;gt; logger is set to &amp;lt;code&amp;gt;INFO&amp;lt;/code&amp;gt; level and records each pipeline stage (seed selection, title tilting, drafting, validation, publication) with document identifiers and truncated titles.&lt;br /&gt;
&lt;br /&gt;
== Management commands ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Command !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;generate_decree&amp;lt;/code&amp;gt; || Run the full generation pipeline: fetch seed, tilt title, draft decree, apply subtle revision, validate, generate notes, and publish as a Wagtail page&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;setup_index_page&amp;lt;/code&amp;gt; || Create the &amp;lt;code&amp;gt;DecreeIndexPage&amp;lt;/code&amp;gt; as the site root (idempotent); removes the default Wagtail welcome page if present&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
The planned production deployment targets &amp;lt;code&amp;gt;nomos.yusupov.cloud&amp;lt;/code&amp;gt; on a Hetzner VPS running [[Nginx]] as a reverse proxy, [[Gunicorn]] as the WSGI application server, and [[PostgreSQL]] as the production database (replacing SQLite). TLS is to be provided by [[Let&#039;s Encrypt]] via Certbot. Daily generation is to be scheduled via cron calling &amp;lt;code&amp;gt;python manage.py generate_decree&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Wagtail (CMS)]]&lt;br /&gt;
* [[Vlaamse Codex]]&lt;br /&gt;
* [[Computational creativity]]&lt;br /&gt;
* [[Procedural generation]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Nomos&amp;diff=408</id>
		<title>Nomos</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Nomos&amp;diff=408"/>
		<updated>2026-04-19T23:18:18Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Created page with &amp;quot;{{Infobox | 01_name         = Nomos | 02_url          = https://nomos.yusupov.cloud | 03_developer    = Michel Vuijlsteke | 04_released     = 2026 | 05_genre        = AI-generated legal document archive | 06_language     = Python | 07_framework    = Django 6.0 / Wagtail 7.3 | 08_license      = Proprietary }}  &amp;#039;&amp;#039;&amp;#039;Nomos&amp;#039;&amp;#039;&amp;#039; is a web application hosted at &amp;lt;code&amp;gt;nomos.yusupov.cloud&amp;lt;/code&amp;gt; that generates and publishes AI-created Flemish administrative legislation. Each...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| 01_name         = Nomos&lt;br /&gt;
| 02_url          = https://nomos.yusupov.cloud&lt;br /&gt;
| 03_developer    = Michel Vuijlsteke&lt;br /&gt;
| 04_released     = 2026&lt;br /&gt;
| 05_genre        = AI-generated legal document archive&lt;br /&gt;
| 06_language     = Python&lt;br /&gt;
| 07_framework    = [[Django]] 6.0 / [[Wagtail]] 7.3&lt;br /&gt;
| 08_license      = Proprietary&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nomos&#039;&#039;&#039; is a web application hosted at &amp;lt;code&amp;gt;nomos.yusupov.cloud&amp;lt;/code&amp;gt; that generates and publishes AI-created Flemish administrative legislation. Each day, the system fetches a real document from the Vlaamse Codex open-data API, performs a semantic shift on its subject matter using a large language model, and produces a structurally faithful but entirely fictional legal text — a decree, ministerial order, or circular — that reads as if it were published in the &#039;&#039;Belgisch Staatsblad&#039;&#039;. The generated documents are stored in a [[Wagtail (CMS)|Wagtail]] content management system and presented through a GOV.UK-inspired Dutch-language public interface. The name &#039;&#039;Nomos&#039;&#039; derives from the [[Ancient Greek|Greek]] word νόμος, meaning &amp;quot;law.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Django 6.0.4 with Wagtail 7.3.1 as its content management framework and [[SQLite]] as its development database.&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt lists Django 6.0.4, wagtail 7.3.1, openai 2.32.0, requests 2.33.1, python-dotenv 1.2.2, beautifulsoup4 4.14.3, and Pillow 12.2.0.&amp;lt;/ref&amp;gt; Additional dependencies include the [[OpenAI]] Python client for language-model calls, [[Requests (software)|Requests]] for HTTP communication with the Vlaamse Codex API, [[Beautiful Soup (HTML parser)|Beautiful Soup]] for scraping title inspiration from the Belgisch Staatsblad, [[Pillow (imaging library)|Pillow]] as a Wagtail dependency, and [[python-dotenv]] for environment configuration. The front end uses [[Bootstrap]] 5.3.8 loaded from the jsDelivr CDN with subresource integrity hashes. All user interface text is in Dutch. The planned production deployment targets a Hetzner VPS behind [[Nginx]] with [[Gunicorn]] and [[PostgreSQL]].&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
The data model uses Wagtail&#039;s page tree. A singleton &amp;lt;code&amp;gt;DecreeIndexPage&amp;lt;/code&amp;gt; (limited to &amp;lt;code&amp;gt;max_count = 1&amp;lt;/code&amp;gt;) serves as the parent of all generated documents. Each document is a &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; with the following fields:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;instrument&#039;&#039; — one of four types: Decreet, Besluit, Omzendbrief, or Reglement.&lt;br /&gt;
* &#039;&#039;full_title&#039;&#039; (&amp;lt;code&amp;gt;TextField&amp;lt;/code&amp;gt;) — the complete title, unlimited in length. The standard Wagtail &#039;&#039;title&#039;&#039; field (255-character limit) holds a truncated copy for internal use.&lt;br /&gt;
* &#039;&#039;body&#039;&#039; (&amp;lt;code&amp;gt;RichTextField&amp;lt;/code&amp;gt;) — the full HTML text of the generated legislation.&lt;br /&gt;
* &#039;&#039;publication_date&#039;&#039; (&amp;lt;code&amp;gt;DateField&amp;lt;/code&amp;gt;) — set to the generation date.&lt;br /&gt;
* &#039;&#039;status&#039;&#039; — either &#039;&#039;Geldig&#039;&#039; (valid) or &#039;&#039;Gearchiveerd&#039;&#039; (archived); defaults to Geldig.&lt;br /&gt;
* &#039;&#039;seed_id&#039;&#039; (&amp;lt;code&amp;gt;IntegerField&amp;lt;/code&amp;gt;, unique, nullable) — the numeric ID of the source document in the Vlaamse Codex, used for deduplication to ensure no seed is used twice.&lt;br /&gt;
* &#039;&#039;seed_reference&#039;&#039; (&amp;lt;code&amp;gt;URLField&amp;lt;/code&amp;gt;) — direct API link to the source document.&lt;br /&gt;
* &#039;&#039;seed_document&#039;&#039; (&amp;lt;code&amp;gt;TextField&amp;lt;/code&amp;gt;) — human-readable label recording the type and title of the source document.&lt;br /&gt;
* &#039;&#039;generation_notes&#039;&#039; (&amp;lt;code&amp;gt;TextField&amp;lt;/code&amp;gt;) — an LLM-generated human-readable summary describing how the generated document differs from its source.&lt;br /&gt;
&lt;br /&gt;
Full-text search is indexed on &#039;&#039;full_title&#039;&#039; and &#039;&#039;body&#039;&#039; via Wagtail&#039;s database search backend.&lt;br /&gt;
&lt;br /&gt;
== Generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Generation is driven by the &amp;lt;code&amp;gt;generate_decree&amp;lt;/code&amp;gt; management command, which invokes a five-stage pipeline implemented in &amp;lt;code&amp;gt;nomos/services/generator.py&amp;lt;/code&amp;gt;. The pipeline retries up to three times if validation fails.&lt;br /&gt;
&lt;br /&gt;
=== Stage 1: Structural sourcing ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;pick_random_seed()&amp;lt;/code&amp;gt; function in &amp;lt;code&amp;gt;nomos/services/codex.py&amp;lt;/code&amp;gt; fetches up to 200 recent documents from the Vlaamse Codex open-data API (&amp;lt;code&amp;gt;codex.opendata.api.vlaanderen.be&amp;lt;/code&amp;gt;). Candidates are filtered to four allowed document types:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Codex type !! Mapped instrument&lt;br /&gt;
|-&lt;br /&gt;
| Decreet || DECREET&lt;br /&gt;
|-&lt;br /&gt;
| Besluit van de Vlaamse Regering || BESLUIT&lt;br /&gt;
|-&lt;br /&gt;
| Ministerieel besluit || BESLUIT&lt;br /&gt;
|-&lt;br /&gt;
| Omzendbrief || OMZENDBRIEF&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Documents whose &amp;lt;code&amp;gt;seed_id&amp;lt;/code&amp;gt; already exists in the database are excluded. The remaining candidates are grouped by type, and a weighted random selection favours types that have been used less frequently in the preceding seven days. For each recently used instrument, the selection weight is reduced by two per occurrence (minimum weight of 1). Once a seed is selected, the system fetches its full detail and chapter/section structure from the API.&lt;br /&gt;
&lt;br /&gt;
=== Topic inspiration ===&lt;br /&gt;
&lt;br /&gt;
Before generating, the pipeline scrapes the &#039;&#039;Belgisch Staatsblad&#039;&#039; website (&amp;lt;code&amp;gt;ejustice.just.fgov.be&amp;lt;/code&amp;gt;) for a random document title to use as thematic inspiration. Federal and national references in the scraped title are replaced with Flemish equivalents using a table of 17 substitution pairs — for example, &amp;quot;Federale Overheidsdienst&amp;quot; becomes &amp;quot;Vlaamse overheidsdienst,&amp;quot; &amp;quot;Koninklijk besluit&amp;quot; becomes &amp;quot;Besluit van de Vlaamse Regering,&amp;quot; and &amp;quot;België&amp;quot; becomes &amp;quot;Vlaanderen.&amp;quot; If the scrape fails, generation proceeds without a topic hint.&lt;br /&gt;
&lt;br /&gt;
=== Stage 2: Technical mutation ===&lt;br /&gt;
&lt;br /&gt;
The seed document&#039;s title (&#039;&#039;opschrift&#039;&#039;) is sent to the OpenAI Chat Completions API (default model: GPT-5, configurable via the &amp;lt;code&amp;gt;OPENAI_MODEL&amp;lt;/code&amp;gt; environment variable). The system prompt instructs the model to behave as an expert in Flemish legislation and to perform a semantic shift: replace the core subject with a plausible but fictional technical equivalent while preserving the exact grammatical structure and bureaucratic tone. If a topic hint was obtained from the Belgisch Staatsblad, it is included as thematic guidance.&amp;lt;ref name=&amp;quot;temperature&amp;quot;&amp;gt;GPT-5 does not support custom temperature values. All API calls use the model&#039;s default temperature (1).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Stage 3: Administrative drafting ===&lt;br /&gt;
&lt;br /&gt;
The tilted title, the seed document&#039;s full text (up to 4,000 characters), and its structural outline are sent to a second API call. The system prompt instructs the model to act as a legislative jurist of the Flemish government and to rewrite the source document about the new subject. Strict structural parity rules are enforced:&lt;br /&gt;
&lt;br /&gt;
* The output must contain the exact same number of chapters (&#039;&#039;hoofdstukken&#039;&#039;), sections (&#039;&#039;afdelingen&#039;&#039;), and articles (&#039;&#039;artikelen&#039;&#039;) as the source.&lt;br /&gt;
* If the source has no chapter divisions, the output must not introduce them.&lt;br /&gt;
* The total length must be comparable to the source.&lt;br /&gt;
* Content must be entirely original — only the form is emulated.&lt;br /&gt;
&lt;br /&gt;
The output is requested as clean HTML using &amp;lt;code&amp;gt;&amp;amp;lt;h2&amp;amp;gt;&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;&amp;amp;lt;h3&amp;amp;gt;&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;&amp;amp;lt;p&amp;amp;gt;&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;&amp;amp;lt;ol&amp;amp;gt;&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;&amp;amp;lt;li&amp;amp;gt;&amp;lt;/code&amp;gt; elements, without a top-level heading (which is rendered separately on the page).&lt;br /&gt;
&lt;br /&gt;
=== Stage 4: Juridical scrub ===&lt;br /&gt;
&lt;br /&gt;
The generated HTML is submitted to a validation call in which the model acts as a quality controller. It checks for the presence of narrative, poetic, or metaphorical language; references to fiction, imagination, or art; and humor or irony. Documents that do not read as authentic administrative texts are rejected with a reason. If all three attempts fail validation, the pipeline raises an error.&lt;br /&gt;
&lt;br /&gt;
=== Stage 5: Generation notes ===&lt;br /&gt;
&lt;br /&gt;
After successful validation, a final API call generates a human-readable summary of the transformation. The model is instructed to act as an archivist and to describe in one or two plain-language sentences how the new document&#039;s subject differs from the original, without technical jargon.&lt;br /&gt;
&lt;br /&gt;
=== Text sanitisation ===&lt;br /&gt;
&lt;br /&gt;
All generated text is passed through a sanitisation function that strips Unicode control characters (C0/C1 range, excluding newlines and tabs), applies NFC normalisation, removes empty list items and orphaned list wrappers from the HTML, and cleans whitespace artifacts. This addresses a known issue where GPT-5 occasionally emits ASCII control characters in place of Unicode punctuation.&lt;br /&gt;
&lt;br /&gt;
=== Persistence ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;save_decree()&amp;lt;/code&amp;gt; function in &amp;lt;code&amp;gt;nomos/services/storage.py&amp;lt;/code&amp;gt; creates a &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; as a child of the &amp;lt;code&amp;gt;DecreeIndexPage&amp;lt;/code&amp;gt;. The slug is derived from the title (truncated to 200 characters) and made unique by appending a numeric suffix if necessary. The page is published immediately via Wagtail&#039;s &amp;lt;code&amp;gt;save_revision().publish()&amp;lt;/code&amp;gt; mechanism. The publication date is set to the current date and the status defaults to &#039;&#039;Geldig&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
== Anti-sameness system ==&lt;br /&gt;
&lt;br /&gt;
To prevent the archive from becoming repetitive, the system employs two mechanisms:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Seed deduplication&#039;&#039;&#039;: the &amp;lt;code&amp;gt;seed_id&amp;lt;/code&amp;gt; field (unique integer) ensures that no Vlaamse Codex document is used as a source more than once. Before selecting a seed, the pipeline queries all existing &amp;lt;code&amp;gt;seed_id&amp;lt;/code&amp;gt; values and excludes them from the candidate pool.&lt;br /&gt;
* &#039;&#039;&#039;Type-weighted selection&#039;&#039;&#039;: the &amp;lt;code&amp;gt;_get_recent_type_counts()&amp;lt;/code&amp;gt; function counts how many times each instrument type has appeared in the last seven days. Types with higher recent counts receive proportionally lower selection weights, encouraging the system to alternate between decrees, orders, and circulars.&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Index page ===&lt;br /&gt;
&lt;br /&gt;
The index page lists all published &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; children ordered by publication date (newest first). A search bar and an instrument type dropdown filter are provided. Search uses Wagtail&#039;s database search backend, querying the &#039;&#039;full_title&#039;&#039; and &#039;&#039;body&#039;&#039; fields. The type filter applies an exact match on the &#039;&#039;instrument&#039;&#039; field. Each list entry displays the full title, publication date, instrument type label, and a colour-coded status tag (green for &#039;&#039;Geldig&#039;&#039;, grey for &#039;&#039;Gearchiveerd&#039;&#039;).&lt;br /&gt;
&lt;br /&gt;
=== Document detail page ===&lt;br /&gt;
&lt;br /&gt;
Each decree page displays:&lt;br /&gt;
&lt;br /&gt;
* A breadcrumb navigation link back to the index page.&lt;br /&gt;
* The full title as a top-level heading.&lt;br /&gt;
* A GOV.UK-style summary list with key/value rows for document type, publication date, and status (rendered as a tag badge).&lt;br /&gt;
* The full decree body rendered as rich text.&lt;br /&gt;
&lt;br /&gt;
=== Visual design ===&lt;br /&gt;
&lt;br /&gt;
The interface is inspired by the [[GOV.UK Design System]]. The base template features a dark masthead with a yellow (&amp;lt;code&amp;gt;#ffe615&amp;lt;/code&amp;gt;) accent border, the site name &amp;quot;Nomos&amp;quot; as a navigation link, and a dark/light mode toggle button. The toggle uses CSS custom properties for theming and persists the user&#039;s preference in &amp;lt;code&amp;gt;localStorage&amp;lt;/code&amp;gt;. In dark mode, the masthead shifts to &amp;lt;code&amp;gt;#1a1a1a&amp;lt;/code&amp;gt;, links become lighter, and status tag colours are adjusted for contrast. A responsive layout collapses the summary list key/value pairs into a single column below 576px.&lt;br /&gt;
&lt;br /&gt;
== Administration ==&lt;br /&gt;
&lt;br /&gt;
The application uses the standard Wagtail admin interface at &amp;lt;code&amp;gt;/admin/&amp;lt;/code&amp;gt;. &amp;lt;code&amp;gt;DecreePage&amp;lt;/code&amp;gt; content panels expose the instrument type, full title, body, publication date, and status. A separate &amp;quot;Generatie-informatie&amp;quot; settings panel groups the seed ID, seed reference URL, seed document description, and generation notes — metadata that is recorded automatically during generation and is accessible to editors but not displayed on the public site.&lt;br /&gt;
&lt;br /&gt;
The Django admin is available at &amp;lt;code&amp;gt;/django-admin/&amp;lt;/code&amp;gt; for lower-level database access.&lt;br /&gt;
&lt;br /&gt;
== Logging ==&lt;br /&gt;
&lt;br /&gt;
Application logging is configured with two handlers: console output and a rotating file log (&amp;lt;code&amp;gt;nomos.log&amp;lt;/code&amp;gt; in the project root). The &amp;lt;code&amp;gt;nomos&amp;lt;/code&amp;gt; logger is set to &amp;lt;code&amp;gt;INFO&amp;lt;/code&amp;gt; level and records each pipeline stage (seed selection, title tilting, drafting, validation, publication) with document identifiers and truncated titles.&lt;br /&gt;
&lt;br /&gt;
== Management commands ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Command !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;generate_decree&amp;lt;/code&amp;gt; || Run the full five-stage pipeline: fetch seed, tilt title, draft decree, validate, generate notes, and publish as a Wagtail page&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;setup_index_page&amp;lt;/code&amp;gt; || Create the &amp;lt;code&amp;gt;DecreeIndexPage&amp;lt;/code&amp;gt; as the site root (idempotent); removes the default Wagtail welcome page if present&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
The planned production deployment targets &amp;lt;code&amp;gt;nomos.yusupov.cloud&amp;lt;/code&amp;gt; on a Hetzner VPS running [[Nginx]] as a reverse proxy, [[Gunicorn]] as the WSGI application server, and [[PostgreSQL]] as the production database (replacing SQLite). TLS is to be provided by [[Let&#039;s Encrypt]] via Certbot. Daily generation is to be scheduled via cron calling &amp;lt;code&amp;gt;python manage.py generate_decree&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Wagtail (CMS)]]&lt;br /&gt;
* [[Vlaamse Codex]]&lt;br /&gt;
* [[Computational creativity]]&lt;br /&gt;
* [[Procedural generation]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=407</id>
		<title>Yusupov.cloud</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=407"/>
		<updated>2026-04-19T23:18:06Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Subdomains and projects */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name           = yusupov.cloud&lt;br /&gt;
| 1 url            = https://yusupov.cloud&lt;br /&gt;
| 2 type           = Personal web sites&lt;br /&gt;
| 3 owner          = [[Michel Vuijlsteke]]&lt;br /&gt;
| 4 launched       = 2025&lt;br /&gt;
| 5 current_status = Online&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;yusupov.cloud&#039;&#039;&#039; is a personal domain and virtual private server operated by Belgian technologist [[Michel Vuijlsteke]]. It hosts multiple small web applications on subdomains and at the apex domain. One of these is a MediaWiki installation titled “Yusupov’s House.” The setup is presented as a web-era continuation of the do-it-yourself ethos of Vuijlsteke’s 1990s BBS of the same name.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot;&amp;gt;“Yusupov’s House,” &#039;&#039;yusupov.cloud&#039;&#039; (wiki), accessed 10 October 2025, https://yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
Among the projects on the domain is a MediaWiki (at the apex, &#039;&#039;yusupov.cloud&#039;&#039;) running MediaWiki 1.44.0 with PHP 8.3.6 (FPM) and SQLite, using the Vector skin and core extensions for citations and template scripting.&amp;lt;ref name=&amp;quot;version&amp;quot;&amp;gt;‘‘Special:Version’’ page, &#039;&#039;yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://yusupov.cloud/wiki/Special:Version&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Subdomains and projects ==&lt;br /&gt;
Publicly visible projects include:&lt;br /&gt;
&lt;br /&gt;
* [https://acbc.yusupov.cloud acbc.yusupov.cloud] — &#039;&#039;A Cabinet of Brief Curiosities&#039;&#039;, generating tiny three-sentence surreal/horror micro-stories with an hourly cadence and an archive. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;acbc-home&amp;quot;&amp;gt;A Cabinet of Brief Curiosities (home), &#039;&#039;acbc.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://acbc.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://agenda.yusupov.cloud agenda.yusupov.cloud] — &#039;&#039;A Life in Planners&#039;&#039;, a structured journal chronicling the final years of the operator’s mother, with calendar, food, medications, measurements, and statistics views (multilingual UI).&amp;lt;ref name=&amp;quot;agenda&amp;quot;&amp;gt;“A life in planners,” &#039;&#039;agenda.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://agenda.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://cloud.yusupov.cloud cloud.yusupov.cloud] — a series of static html creative coding experiments, simulations, and games including: timebeat, fire and snake simulations, biomass metaballs, cs3, &#039;&#039;Cross&#039;&#039; crossword puzzle game, image dithering tool, books, &#039;&#039;Elite Galaxy Explorer&#039;&#039;, ZX Spectrum loading screen simulator, Carcassonne, 3D boids flocking algorithm, physarum slime mold simulation, temps temperature visualization, &#039;&#039;The Chronicle of Hamurabi&#039;&#039; ancient Sumeria resource management game, and gatekeeper.&amp;lt;ref name=&amp;quot;cloud-home&amp;quot;&amp;gt;&amp;quot;cloud,&amp;quot; &#039;&#039;cloud.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://cloud.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://digest.yusupov.cloud digest.yusupov.cloud] — &#039;&#039;[[Digest]]&#039;&#039;, daily seasonal AI-assisted recipes inspired by current events, browsable by meal type and ingredients.&amp;lt;ref name=&amp;quot;digest-home&amp;quot;&amp;gt;“Digest — Daily recipes inspired by the news,” &#039;&#039;digest.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://digest.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://echoes.yusupov.cloud echoes.yusupov.cloud] — &#039;&#039;[[Echoes of What Wasn&#039;t]]&#039;&#039;, an AI-generated alternate-history newspaper presenting richly detailed articles about historical events as if they had unfolded differently. A pipeline scrapes real events from multilingual Wikipedia, uses OpenAI to craft a divergent narrative with period-appropriate prose and DALL-E imagery, and publishes via a REST API. Features article browsing by month, a &amp;quot;Where/When&amp;quot; interactive map-and-timeline view using Leaflet, and a picture desk. (Built with Wagtail 7/Django 5 per operator.)&amp;lt;ref name=&amp;quot;echoes-home&amp;quot;&amp;gt;&amp;quot;Echoes — Dispatches from Histories That Never Were,&amp;quot; &#039;&#039;echoes.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://echoes.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://nomos.yusupov.cloud nomos.yusupov.cloud] — &#039;&#039;[[Nomos]]&#039;&#039;, a daily-generating archive of fictional Flemish administrative legislation, where real Vlaamse Codex documents are semantically shifted by GPT-5 into structurally faithful but entirely invented decrees, orders, and circulars. (Built with Wagtail 7/Django 6 per operator.)&lt;br /&gt;
* [https://quidlibet.yusupov.cloud quidlibet.yusupov.cloud] — &#039;&#039;[[Quidlibet]]&#039;&#039;, an app that generates fictional books complete with synopsis, author bio, and faux reviews; includes genre and author archives. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;quidlibet-home&amp;quot;&amp;gt;“Quidlibet — Book Generator,” &#039;&#039;quidlibet.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://quidlibet.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://tyov-web.yusupov.cloud tyov-web.yusupov.cloud] — a web implementation of the solo RPG &#039;&#039;[[Thousand Year Old Vampire]]&#039;&#039;, with Django 5 backend and Vue 3 frontend. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
Operator-reported (not publicly discoverable at time of writing):&lt;br /&gt;
&lt;br /&gt;
* skills.yusupov.cloud — a skills matrix application. (Per operator.)&lt;br /&gt;
* resources.yusupov.cloud — a simple resource planning calendar. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
== Technology ==&lt;br /&gt;
The wiki stack is documented on &#039;&#039;Special:Version&#039;&#039;. Individual apps are described by the operator as Flask (&#039;&#039;acbc&#039;&#039;, &#039;&#039;quidlibet&#039;&#039;) and Django 5 + Vue 3 (&#039;&#039;tyov-web&#039;&#039;).&amp;lt;ref name=&amp;quot;version&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Relation to the BBS ==&lt;br /&gt;
The project name references Vuijlsteke’s single-line BBS (FidoNet 2:291/1925) active between 1990 and 1995. While the VPS is not a BBS, its single-admin, self-maintained hosting reprises the early DIY approach.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;nodehist&amp;quot;&amp;gt;“Nodelist history search: History of node 2:291/1925,” NodeHist, accessed 10 October 2025, https://nodehist.fidonet.org.ua/?address=2%3A291%2F1925&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Yusupov&#039;s House]] (1990s BBS)&lt;br /&gt;
* [[Michel Vuijlsteke]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Personal websites]]&lt;br /&gt;
[[Category:Belgian websites]]&lt;br /&gt;
[[Category:2025 establishments in Belgium]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Michel_Vuijlsteke&amp;diff=406</id>
		<title>Michel Vuijlsteke</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Michel_Vuijlsteke&amp;diff=406"/>
		<updated>2026-04-19T21:15:40Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Volunteering */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name         = Michel Vuijlsteke&lt;br /&gt;
| image        = Michel Vuijlsteke, Gentse Feesten 2025.jpg&lt;br /&gt;
| 01 Born         = 27 August 1970, Ghent, Belgium&lt;br /&gt;
| 03 Occupation   = User experience architect; writer; former BBS/Internet early adopter&lt;br /&gt;
| 04 Active = 1970–present&lt;br /&gt;
| 05 Employer(s)  = Ghent University, [[Albania for King Zog Committee]]&lt;br /&gt;
| 06 Website      = [https://blog.zog.org/ blog.zog.org]&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Michel Vuijlsteke&#039;&#039;&#039; (born 27 August 1970) is a Belgian user experience architect, writer and former Internet early adopter. &lt;br /&gt;
&lt;br /&gt;
== Biography ==&lt;br /&gt;
Vuijlsteke was born in Ghent and studied law, but did not pursue a legal career.&amp;lt;ref name=&amp;quot;cv&amp;quot;&amp;gt;Michel Vuijlsteke, &#039;&#039;CV Michel Vuijlsteke&#039;&#039;, 2025.&amp;lt;/ref&amp;gt; In 1992 he participated in the pilot program of the [[Central European University]] in Prague, an experience he later summarized as leaving “mainly culinary impressions.”&amp;lt;ref name=&amp;quot;cv&amp;quot; /&amp;gt; In 2016 he obtained a cooking diploma at the [[Spermalie Hotel School]] in Ghent, formalising a long-standing interest in food.&amp;lt;ref name=&amp;quot;cv&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
He lives in Ghent, is married and has four children; two cats also feature prominently in the household logistics.&amp;lt;ref name=&amp;quot;cv&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Career ==&lt;br /&gt;
Vuijlsteke co-founded [[Netpoint]] in 1995, one of the early Internet companies in Belgium, which built websites for a variety of clients.&amp;lt;ref name=&amp;quot;cv&amp;quot; /&amp;gt; After Netpoint’s acquisition by [[Unit4Agresso]] and subsequent renaming to &#039;&#039;Amercom België&#039;&#039; in 2000, he served as managing director.&amp;lt;ref name=&amp;quot;cv&amp;quot; /&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
He later worked at the [[College of Europe]] and joined [[Namahn]] (2006–2010), contributing to projects in information architecture for clients including SD Worx, Belgacom and KBC.&amp;lt;ref name=&amp;quot;cv&amp;quot; /&amp;gt; From 2010 to 2018 he was product owner of [[Adhese]], an online advertising platform operating in the European ad-tech niche.&amp;lt;ref name=&amp;quot;cv&amp;quot; /&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
Between 2018 and 2022 he returned to [[Namahn]], mainly working on user experience architecture at [[EUROCONTROL]], where he contributed to applications used in European airspace management.&amp;lt;ref name=&amp;quot;cv&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;linkedin&amp;quot;&amp;gt;&amp;quot;Michel Vuijlsteke – LinkedIn profile&amp;quot;. Available at: https://www.linkedin.com/in/mvuijlst/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vuijlsteke is currently employed at [[Ghent University]], where he heads the team responsible for user experience and interface development across university-wide web platforms. His role combines coordination of front-end development with UX design leadership.&amp;lt;ref name=&amp;quot;cv&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Volunteering ==&lt;br /&gt;
* At [[cirQ]] he contributed to productions such as &#039;&#039;Datakamp&#039;&#039; (2017), &#039;&#039;Batacratie&#039;&#039; (2018), &#039;&#039;Batastunt&#039;&#039; (2019) and &#039;&#039;cirQ TV&#039;&#039; during the COVID lockdown. He returned to cirQ in 2025 for &#039;&#039;Bataknar 2.0&#039;&#039;&amp;lt;ref name=&amp;quot;cv&amp;quot; /&amp;gt;&lt;br /&gt;
* At [[Companie Cordial]] (then &#039;&#039;Refu Interim&#039;&#039;) he built a volunteer-management application and is a member of the Board&amp;lt;ref name=&amp;quot;cv&amp;quot; /&amp;gt;&lt;br /&gt;
* For [[Gentblogt]] he wrote articles and took photos for many years, later serving as chair of the non-profit.&amp;lt;ref name=&amp;quot;cv&amp;quot; /&amp;gt;&lt;br /&gt;
* For [[Doorway Productions]] he worked as a screenwriter and script doctor.&amp;lt;ref name=&amp;quot;cv&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Online presence ==&lt;br /&gt;
Since 2002 Vuijlsteke writes at least daily posts on blog.zog.org, mixing technology, personal notes and commentary, which he once called “a diary that got out of hand.”&amp;lt;ref name=&amp;quot;about&amp;quot;&amp;gt;&amp;quot;Over mezelf&amp;quot;, Zog.org. Available at: https://blog.zog.org/about&amp;lt;/ref&amp;gt; Beyond the blog, he operates personal and experimental websites across multiple domains, among others: [[yusupov.cloud]], [https://moosedept.org moosedept.org], [[otlet.cloud]], [https://blaffeture.net blaffeture.net], [https://tsuk.org tsuk.org], [https://genea.cloud genea.cloud], and [https://vuijlsteke.net vuijlsteke.net].&lt;br /&gt;
&lt;br /&gt;
== Association with the Moose Dept. ==&lt;br /&gt;
In addition to his professional and personal writing, Vuijlsteke has been informally connected with the [[Moose Dept.]], a subdivision of the [[Albania for King Zog Committee]]. Since at least 1989 he has occasionally signed correspondence as “Grand Mufti, Moose Dept.,” a title referenced in later interviews.&amp;lt;ref&amp;gt;Barker, W., interview with M. Vuijlsteke, 1998, privately circulated typescript.&amp;lt;/ref&amp;gt;&amp;lt;ref&amp;gt;Anonymous interview with M. Vuijlsteke, 2007, recorded in the AKZ Oral Archive, Ghent.&amp;lt;/ref&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
The relationship between these playful titles and his broader digital projects has been the subject of some scholarly commentary.&amp;lt;ref&amp;gt;Duquesne, M., &#039;&#039;Domestic Esoterica: Everyday Surfaces of Secret Societies&#039;&#039; (&#039;&#039;Proceedings of Contemporary Folklore&#039;&#039;, vol. 7, 2018), pp. 52–67.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Languages ==&lt;br /&gt;
Vuijlsteke speaks Dutch and French as mother tongues, English fluently, and has working knowledge of German, Italian and Spanish.&amp;lt;ref name=&amp;quot;cv&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&amp;lt;references /&amp;gt;&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Digest&amp;diff=405</id>
		<title>Digest</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Digest&amp;diff=405"/>
		<updated>2026-04-19T20:28:35Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| 01_name         = Digest&lt;br /&gt;
| 02_url          = https://digest.yusupov.cloud&lt;br /&gt;
| 03_developer    = Michel Vuijlsteke&lt;br /&gt;
| 04_released     = 2025&lt;br /&gt;
| 05_genre        = AI-generated recipe application&lt;br /&gt;
| 06_language     = Python&lt;br /&gt;
| 07_framework    = [[Django]] 5.2&lt;br /&gt;
| 08_license      = Proprietary&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Digest&#039;&#039;&#039; is a web application hosted at &amp;lt;code&amp;gt;digest.yusupov.cloud&amp;lt;/code&amp;gt; that generates and publishes AI-created recipes inspired by current news articles. Each day, the system fetches articles from the &#039;&#039;New York Times&#039;&#039; RSS feed, selects two at random, and uses them as creative prompts for a large language model to invent a realistic, seasonally appropriate recipe. A companion image-generation pipeline produces editorial-quality food photography for each recipe using a composition diversity system that enforces visual variety across generations. The site&#039;s tagline is &amp;quot;Daily recipes inspired by the news.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Django 5.2 with [[SQLite]] as its database.&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt in the project repository lists Django ≥5.2, Pillow ≥10, openai ≥1.40, python-dotenv ≥1.0, gunicorn ≥21, requests ≥2.31, beautifulsoup4 ≥4.12, and markdown ≥3.5.&amp;lt;/ref&amp;gt; It is deployed behind [[Nginx]] with [[Gunicorn]] on an Ubuntu server running two synchronous workers on port 8001. Additional dependencies include [[Pillow (imaging library)|Pillow]] for image processing and optimisation, the [[OpenAI]] Python client for language-model and image-generation calls, [[Beautiful Soup (HTML parser)|Beautiful Soup]] for scraping recipe inspiration from external sites, [[Markdown (markup language)|python-markdown]] for rendering recipe inspiration text, and [[python-dotenv]] for environment configuration. The front end uses [[Bootstrap]] 5.3.3 with Bootstrap Icons, and typography is set in Roboto (body), Roboto Serif (headings), and Roboto Slab (accents), loaded from Google Fonts.&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
=== Recipe ===&lt;br /&gt;
&lt;br /&gt;
The central model is &amp;lt;code&amp;gt;Recipe&amp;lt;/code&amp;gt;, which stores:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;name&#039;&#039;, &#039;&#039;slug&#039;&#039; (auto-generated), &#039;&#039;intro&#039;&#039; (short paragraph), &#039;&#039;inspiration&#039;&#039; (120–200-word Markdown text linking to the source articles), and &#039;&#039;visual_description&#039;&#039; (a single sentence describing the plated dish for use as an image-generation prompt).&lt;br /&gt;
* &#039;&#039;hero_image&#039;&#039; (uploaded to &amp;lt;code&amp;gt;recipes/hero/&amp;lt;/code&amp;gt;), &#039;&#039;base_servings&#039;&#039; (default 4), &#039;&#039;prep_time_minutes&#039;&#039;, &#039;&#039;cook_time_minutes&#039;&#039;, &#039;&#039;is_featured&#039;&#039; (boolean), and automatic &#039;&#039;created_at&#039;&#039; / &#039;&#039;updated_at&#039;&#039; timestamps.&lt;br /&gt;
* A many-to-many relationship to &amp;lt;code&amp;gt;RecipeType&amp;lt;/code&amp;gt; (one of ten canonical types: Main, Salad, One-pan, Starter, Cold dish, Soup, Side dish, Dessert, Breakfast, or Snack).&lt;br /&gt;
&lt;br /&gt;
=== Structured ingredients ===&lt;br /&gt;
&lt;br /&gt;
Ingredients are organised into optional titled groups (e.g. &amp;quot;Dough&amp;quot;, &amp;quot;Sauce&amp;quot;) via &amp;lt;code&amp;gt;IngredientGroup&amp;lt;/code&amp;gt;. Each &amp;lt;code&amp;gt;Ingredient&amp;lt;/code&amp;gt; within a group has:&lt;br /&gt;
&lt;br /&gt;
* A foreign key to &amp;lt;code&amp;gt;IngredientName&amp;lt;/code&amp;gt; (a canonical master list of deduplicated ingredient names, each with a slug and an optional foreign key to &amp;lt;code&amp;gt;IngredientCategory&amp;lt;/code&amp;gt;).&lt;br /&gt;
* A &#039;&#039;quantity&#039;&#039; (decimal), a foreign key to &amp;lt;code&amp;gt;Unit&amp;lt;/code&amp;gt; (with &#039;&#039;name_singular&#039;&#039; and &#039;&#039;name_plural&#039;&#039;, e.g. &amp;quot;cup&amp;quot;/&amp;quot;cups&amp;quot;, &amp;quot;clove&amp;quot;/&amp;quot;cloves&amp;quot;, &amp;quot;g&amp;quot;/&amp;quot;g&amp;quot;), an optional &#039;&#039;note&#039;&#039; (e.g. &amp;quot;finely diced&amp;quot;, &amp;quot;zested then juiced&amp;quot;), and an &#039;&#039;order&#039;&#039; field.&lt;br /&gt;
²&lt;br /&gt;
&amp;lt;code&amp;gt;IngredientCategory&amp;lt;/code&amp;gt; supports arbitrary hierarchy via a self-referencing parent foreign key, enabling structures such as Fruit → Citrus → Lemon.&lt;br /&gt;
&lt;br /&gt;
=== Instructions ===&lt;br /&gt;
&lt;br /&gt;
Recipe steps are stored as &amp;lt;code&amp;gt;Direction&amp;lt;/code&amp;gt; rows, optionally grouped into titled &amp;lt;code&amp;gt;InstructionGroup&amp;lt;/code&amp;gt; sections. Both models carry an &#039;&#039;order&#039;&#039; field for sequencing.&lt;br /&gt;
&lt;br /&gt;
=== Image prompt history ===&lt;br /&gt;
&lt;br /&gt;
Every AI image generation attempt is recorded in &amp;lt;code&amp;gt;RecipeImagePrompt&amp;lt;/code&amp;gt;, which stores the full prompt text, the visual description, five composition dimension values (camera angle, framing, setting, lighting style, mood), a &#039;&#039;was_centered&#039;&#039; boolean, and a creation timestamp. This history is queried by the composition diversity system to enforce variety across successive hero images.&lt;br /&gt;
&lt;br /&gt;
== Recipe generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Recipe generation is driven by Django management commands and a service layer in &amp;lt;code&amp;gt;recipes/services/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Inspiration sourcing ===&lt;br /&gt;
&lt;br /&gt;
Two management commands gather external inspiration:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;fetch_nyt_inspiration&amp;lt;/code&amp;gt; downloads the current &#039;&#039;New York Times&#039;&#039; front page image (saved to &amp;lt;code&amp;gt;static/img/nyt/&amp;lt;/code&amp;gt;) and fetches articles from the NYT homepage RSS feed. It also manages a weighted variation-rotation state file (&amp;lt;code&amp;gt;data/nyt_variation_state.json&amp;lt;/code&amp;gt;) that tracks which recipe types, cooking techniques, proteins, bases, and connection styles have been used recently.&lt;br /&gt;
* &amp;lt;code&amp;gt;fetch_recipe_inspiration&amp;lt;/code&amp;gt; scrapes the &#039;&#039;NYT Cooking&#039;&#039; and &#039;&#039;Dagelijkse Kost&#039;&#039; (VRT) websites for reference recipes, extracting titles, descriptions, and ingredient lists from embedded JSON-LD &amp;lt;code&amp;gt;Recipe&amp;lt;/code&amp;gt; markup. Results are written to &amp;lt;code&amp;gt;data/inspiration.json&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
A PowerShell script (&amp;lt;code&amp;gt;scripts/schedule_inspiration_refresh.ps1&amp;lt;/code&amp;gt;) automates the inspiration refresh on a three-day cycle via Windows Task Scheduler.&lt;br /&gt;
&lt;br /&gt;
=== Variation rotation ===&lt;br /&gt;
&lt;br /&gt;
To prevent repetitive output, the system maintains weighted &amp;quot;draw bags&amp;quot; for five dimensions of recipe variety:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Dimension !! Pool size !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| Recipe type || 10 || Main (weight 7), One-pan (3), Soup (3), Salad (2), …&lt;br /&gt;
|-&lt;br /&gt;
| Technique || 13 || pan-searing (3), roasting (3), stir-frying (2), sous vide (2), …&lt;br /&gt;
|-&lt;br /&gt;
| Protein || 9 || chicken (3), beef (3), fish (2), shellfish (2), mushrooms (2), …&lt;br /&gt;
|-&lt;br /&gt;
| Base || 8 || rice (3), pasta (3), potatoes (3), flatbread (2), noodles (2), …&lt;br /&gt;
|-&lt;br /&gt;
| Connection style || 10 || geographical (10), seasonal (4), historical (3), narrative (3), …&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Each dimension uses a weighted bag: items are added with multiplicity proportional to their weight, and one is drawn per run without replacement. When the bag empties it is refilled. Immediate repeats of the same pick are avoided when alternatives exist. State is persisted to JSON between runs.&lt;br /&gt;
&lt;br /&gt;
=== Prompt construction ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;gen_recipe_from_nyt&amp;lt;/code&amp;gt; command orchestrates the full pipeline:&lt;br /&gt;
&lt;br /&gt;
# Two articles are selected from the RSS feed, filtered against a usage log (&amp;lt;code&amp;gt;data/inspiration_usage.json&amp;lt;/code&amp;gt;) to avoid reuse.&lt;br /&gt;
# Variation picks are drawn for recipe type, technique, protein, base, and connection style.&lt;br /&gt;
# A structured prompt is assembled, instructing the model to act as a professional recipe developer, invent one realistic recipe that a home cook in Belgium can make using seasonally appropriate ingredients, stay within 75 minutes total time, and use the drawn picks. The prompt includes both article summaries and one extra recipe reference from the inspiration pool (with a 2:1 preference for VRT over NYT sources).&lt;br /&gt;
# An ingredient diversity constraint is injected: the system queries the last seven recipes&#039; ingredients and flags any non-staple ingredient that appears in three or more of them. Flagged ingredients are added to the prompt as a hard exclusion list (&amp;quot;MUST NOT be used in this recipe&amp;quot;). A curated set of approximately 40 pantry staples — salt, oil, butter, garlic, onion, flour, water, lemon, parsley, and similar basics — is exempt from this check. The constraint is self-correcting: once a previously overused ingredient drops below the threshold it is automatically unblocked.&lt;br /&gt;
# The prompt is appended with strict JSON schema instructions specifying the output format: name, intro, inspiration, visual description, servings, times, typed ingredient groups with quantities and units, titled instruction groups, and a recipe type from the canonical list.&lt;br /&gt;
&lt;br /&gt;
=== Text generation ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;OpenAIRecipeGenerator&amp;lt;/code&amp;gt; service class sends the prompt to the OpenAI Responses API. The default model is GPT-5, configurable via environment variable. The system prompt instructs the model to behave as a seasoned chef with Michelin-starred and home-cooking experience, and enforces detailed culinary realism rules: precise quantities (&amp;quot;325g not 300g&amp;quot;), specified heat levels and sensory cues, timing ranges, prep details in ingredient notes, assertive seasoning with salt quantities, and instructions that read like a tested recipe.&amp;lt;ref name=&amp;quot;system-prompt&amp;quot;&amp;gt;The system prompt in &amp;lt;code&amp;gt;recipes/services/openai_recipes.py&amp;lt;/code&amp;gt; specifies: &amp;quot;You are a seasoned chef and food editor with 20 years of Michelin-starred and home-cooking experience.&amp;quot;&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The API call requests JSON object output format. If the model or SDK does not support the &amp;lt;code&amp;gt;text&amp;lt;/code&amp;gt; format parameter, the call falls back to unformatted output. Similarly, if the model does not support the &amp;lt;code&amp;gt;temperature&amp;lt;/code&amp;gt; parameter (as is the case with GPT-5), the call automatically retries without it.&lt;br /&gt;
&lt;br /&gt;
=== Validation ===&lt;br /&gt;
&lt;br /&gt;
After generation, the recipe JSON is passed through a self-critique step: a second, cheaper API call instructs the model to review the recipe for culinary realism — checking that quantities make sense for the serving count, that cook times are realistic for the described technique, that no ingredients mentioned in the instructions are missing from the ingredient list, and that prep notes match the steps. The model returns a corrected JSON object or the original if no issues are found. If the critique call fails, the original is used unchanged.&lt;br /&gt;
&lt;br /&gt;
=== Inspiration rewriting ===&lt;br /&gt;
&lt;br /&gt;
After generation, the pipeline inspects the &amp;lt;code&amp;gt;inspiration&amp;lt;/code&amp;gt; field. If it is shorter than 120 words, contains the forbidden words &amp;quot;article&amp;quot; or &amp;quot;news,&amp;quot; or lacks Markdown anchor links to both source articles, a rewrite call asks the model to produce a 120–200-word vivid text weaving concrete ideas from both sources, ending with properly formatted attribution links.&lt;br /&gt;
&lt;br /&gt;
=== Persistence ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;save_recipe_from_data&amp;lt;/code&amp;gt; function maps the generated JSON to Django models within a single transaction:&lt;br /&gt;
&lt;br /&gt;
* Creates the &amp;lt;code&amp;gt;Recipe&amp;lt;/code&amp;gt; row with all scalar fields.&lt;br /&gt;
* Resolves recipe types against the canonical allowed list (by slug); creates missing &amp;lt;code&amp;gt;RecipeType&amp;lt;/code&amp;gt; rows only for canonical names.&lt;br /&gt;
* Creates &amp;lt;code&amp;gt;IngredientGroup&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Ingredient&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;IngredientName&amp;lt;/code&amp;gt; (get-or-create by slug), and &amp;lt;code&amp;gt;Unit&amp;lt;/code&amp;gt; (get-or-create by singular name) rows.&lt;br /&gt;
* Normalises instruction groups: merges untitled single-step groups into one, handles multiple JSON shapes (list of strings, list of dicts with &amp;lt;code&amp;gt;step&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;text&amp;lt;/code&amp;gt; keys, plain text blobs), and filters artifacts shorter than three characters.&lt;br /&gt;
* Triggers hero image generation (non-fatal by default; controlled by the &amp;lt;code&amp;gt;OPENAI_IMAGE_REQUIRED&amp;lt;/code&amp;gt; setting).&lt;br /&gt;
&lt;br /&gt;
=== Text sanitisation ===&lt;br /&gt;
&lt;br /&gt;
All generated text fields are passed through a sanitisation function before database persistence. This addresses a known issue where certain language models (notably GPT-5) emit ASCII control characters in place of Unicode punctuation — for example, &amp;lt;code&amp;gt;U+0019&amp;lt;/code&amp;gt; followed by a digit where an en-dash should appear, or &amp;lt;code&amp;gt;U+0019&amp;lt;/code&amp;gt; followed by &amp;lt;code&amp;gt;s&amp;lt;/code&amp;gt; where a right single quotation mark is intended. The sanitisation function applies pattern-based corrections for these control character sequences, normalises Unicode dashes and quotation marks to their ASCII equivalents, and strips any remaining C0 control characters. The function is applied to the recipe name, intro, inspiration, visual description, all ingredient names and notes, and all instruction steps and group titles.&lt;br /&gt;
&lt;br /&gt;
== Hero image generation ==&lt;br /&gt;
&lt;br /&gt;
Image generation is a three-layer system: a low-level OpenAI client, a composition diversity engine, and a high-level orchestrator.&lt;br /&gt;
&lt;br /&gt;
=== Image API client ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;OpenAIImageGenerator&amp;lt;/code&amp;gt; calls the OpenAI image generation endpoint (model &amp;lt;code&amp;gt;gpt-image-1.5&amp;lt;/code&amp;gt; by default, configurable via &amp;lt;code&amp;gt;OPENAI_IMAGE_MODEL&amp;lt;/code&amp;gt;) and returns raw PNG bytes decoded from the base64 API response. The default output size is 1536×1024 (landscape), matching typical food photography framing.&amp;lt;ref name=&amp;quot;image-size&amp;quot;&amp;gt;The default size is set in &amp;lt;code&amp;gt;recipes/services/openai_images.py&amp;lt;/code&amp;gt; and can be overridden via the &amp;lt;code&amp;gt;OPENAI_IMAGE_SIZE&amp;lt;/code&amp;gt; setting.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Composition diversity ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;FoodPhotoPromptGenerator&amp;lt;/code&amp;gt; enforces visual variety across successive hero images by selecting compositions from the Cartesian product of five dimensions:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Dimension !! Count !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| Camera angle || 8 || overhead flat lay, 45° oblique, eye-level, low side, macro detail, handheld documentary, three-quarter view, tilted dutch angle&lt;br /&gt;
|-&lt;br /&gt;
| Framing || 11 || wide with negative space, tight crop, off-center, partial out-of-frame, action/utensil-in-motion, ingredient close-up, shallow depth-of-field, symmetrical centered, diagonal composition, …&lt;br /&gt;
|-&lt;br /&gt;
| Setting || 11 || white studio, stainless pro kitchen, home counter, picnic/outdoor, dark restaurant table, marble surface, wooden cutting board, concrete backdrop, vintage tile, slate serving board, …&lt;br /&gt;
|-&lt;br /&gt;
| Lighting || 12 || hard sun, soft window, dramatic low-key, warm tungsten, backlit steam, golden hour, bounce fill, rim lighting, diffused overhead, candlelight, …&lt;br /&gt;
|-&lt;br /&gt;
| Mood || 12 || modern editorial, rustic farmhouse, minimalist, street-food, celebratory, cozy homestyle, vibrant colorful, moody atmospheric, fresh healthy, indulgent decadent, …&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
This yields approximately 13,000 possible combinations. The selection algorithm:&lt;br /&gt;
&lt;br /&gt;
# Generates all combinations and shuffles them using a cryptographically secure random source (&amp;lt;code&amp;gt;secrets.SystemRandom&amp;lt;/code&amp;gt;).&lt;br /&gt;
# Filters candidates against: angle+setting pairs used in the last 30 days (banned), recent compositions within the history window (must differ in at least 3 of 5 dimensions), and a centering constraint (no centered composition if the previous image was centered).&lt;br /&gt;
# Falls back to progressively relaxed criteria if no perfect match exists.&lt;br /&gt;
&lt;br /&gt;
=== Prompt construction ===&lt;br /&gt;
&lt;br /&gt;
The selected composition is assembled into a detailed image prompt that specifies:&lt;br /&gt;
&lt;br /&gt;
* The dish name and a one-sentence visual description.&lt;br /&gt;
* A style directive requesting &amp;quot;high-end editorial food photography for a cookbook or food magazine,&amp;quot; with natural imperfections — &amp;quot;slight char marks, a drip of sauce, steam rising, herbs slightly wilted from heat.&amp;quot;&lt;br /&gt;
* The five composition dimensions.&lt;br /&gt;
* Food styling details: realistic portion sizes, contextual props (linen napkin, scattered herbs, olive oil drizzle), visible textures (crispy skin, glossy glaze, charred edges), and a natural, non-oversaturated colour palette.&lt;br /&gt;
* Hard constraints: photorealistic only (no illustrations), no text or watermarks, no human faces or hands, and avoidance of rustic wood unless specified.&lt;br /&gt;
&lt;br /&gt;
=== Persistence and attachment ===&lt;br /&gt;
&lt;br /&gt;
Each generation creates a &amp;lt;code&amp;gt;RecipeImagePrompt&amp;lt;/code&amp;gt; record storing all composition metadata. The generated PNG is attached to the recipe&#039;s &amp;lt;code&amp;gt;hero_image&amp;lt;/code&amp;gt; field via Django&#039;s file storage. On-demand regeneration is available through the upload-image view, which also accepts manual image uploads (validated for JPEG, PNG, or WebP content type, maximum 20 MB, downscaled to 2048px and converted to JPEG at 85% quality).&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Home page ===&lt;br /&gt;
&lt;br /&gt;
The home page displays a featured recipe (hero image with title, intro, and call-to-action) and a grid of the seven most recent recipes as cards with 1:1 hero images. The layout is responsive: seven columns on desktop, three on tablet, two on mobile.&lt;br /&gt;
&lt;br /&gt;
=== Recipe detail ===&lt;br /&gt;
&lt;br /&gt;
Each recipe page (&amp;lt;code&amp;gt;/r/&amp;lt;slug&amp;gt;/&amp;lt;/code&amp;gt;) features:&lt;br /&gt;
&lt;br /&gt;
* A hero section with the image (left, 1:1 aspect ratio, rounded shadow) and title, intro, recipe type badges (linked to type pages), and time estimates (prep, cook, total) with ingredient count.&lt;br /&gt;
* A sticky sidebar with the ingredient list, organised by groups, with a servings control (−/+ buttons and direct input). Ingredient quantities are scaled client-side using the formula &amp;lt;code&amp;gt;scaled_qty = base_qty × (servings / base_servings)&amp;lt;/code&amp;gt;, with unit pluralisation logic handling irregular forms (cup/cups, clove/cloves).&lt;br /&gt;
* Numbered instructions below, optionally grouped by titled sections.&lt;br /&gt;
* For authenticated users: edit, delete, and regenerate-image controls, plus a collapsible display of the raw image generation prompt.&lt;br /&gt;
&lt;br /&gt;
The page emits [[Schema.org]] &amp;lt;code&amp;gt;Recipe&amp;lt;/code&amp;gt; JSON-LD markup (name, image, prep/cook times in ISO 8601 duration format, yield, ingredient list, and &amp;lt;code&amp;gt;HowToStep&amp;lt;/code&amp;gt; instructions), Open Graph meta tags, and a canonical URL.&lt;br /&gt;
&lt;br /&gt;
=== Browsing ===&lt;br /&gt;
&lt;br /&gt;
The site provides several browsing views, all with file-based caching:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;Meals&#039;&#039; (&amp;lt;code&amp;gt;/types/&amp;lt;/code&amp;gt;): lists recipe types with the latest example recipe for each, linking to paginated type detail pages (12 recipes per page).&lt;br /&gt;
* &#039;&#039;Ingredients&#039;&#039; (&amp;lt;code&amp;gt;/ingredients/&amp;lt;/code&amp;gt;): a master ingredient index with recipe usage counts, linking to paginated ingredient detail pages (12 recipes per page).&lt;br /&gt;
* &#039;&#039;Categories&#039;&#039; (&amp;lt;code&amp;gt;/categories/&amp;lt;/code&amp;gt;): a hierarchical ingredient category tree with subcategories and ingredient counts, linking to paginated category detail pages (24 items per page).&lt;br /&gt;
* &#039;&#039;Calendar&#039;&#039; (&amp;lt;code&amp;gt;/year/&amp;lt;/code&amp;gt;): a year-view calendar displaying all twelve months in a responsive grid. Days on which at least one recipe was published (across all years) are rendered as hyperlinks to a day detail page (&amp;lt;code&amp;gt;/year/MM/DD/&amp;lt;/code&amp;gt;), which lists all recipes for that month-and-day combination regardless of year, using the same card layout and pagination as the type detail pages.&lt;br /&gt;
* &#039;&#039;About&#039;&#039; (&amp;lt;code&amp;gt;/about/&amp;lt;/code&amp;gt;): a static information page.&lt;br /&gt;
&lt;br /&gt;
=== Theme ===&lt;br /&gt;
&lt;br /&gt;
The site supports light and dark colour themes, toggled via a moon/sun icon button in the navigation bar and persisted in &amp;lt;code&amp;gt;localStorage&amp;lt;/code&amp;gt;. Dark mode uses a &amp;lt;code&amp;gt;#121212&amp;lt;/code&amp;gt; background, &amp;lt;code&amp;gt;#e6e6e6&amp;lt;/code&amp;gt; text, and yellow (&amp;lt;code&amp;gt;rgb(255, 193, 7)&amp;lt;/code&amp;gt;) accent links, with high-contrast card and list-group styling.&lt;br /&gt;
&lt;br /&gt;
== Administration ==&lt;br /&gt;
&lt;br /&gt;
The Django admin interface is customised with:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;RecipeAdmin&amp;lt;/code&amp;gt;: list display with name, servings, times, creation date, and featured flag; search across name, intro, and inspiration; inline editing of instruction groups and directions; horizontal filter for recipe types.&lt;br /&gt;
* &amp;lt;code&amp;gt;IngredientNameAdmin&amp;lt;/code&amp;gt;: list with category and recipe count; bulk actions for assigning or removing categories, and for merging duplicate ingredient names (which transfers all ingredient references to the merge target).&lt;br /&gt;
* &amp;lt;code&amp;gt;IngredientCategoryAdmin&amp;lt;/code&amp;gt;: hierarchical display with direct and recursive ingredient counts; inline subcategory editing; a custom overview view showing category statistics; and an action for moving ingredients between subcategories.&lt;br /&gt;
&lt;br /&gt;
Custom admin templates provide forms for category assignment (&amp;lt;code&amp;gt;assign_category.html&amp;lt;/code&amp;gt;), category overview (&amp;lt;code&amp;gt;category_overview.html&amp;lt;/code&amp;gt;), ingredient merging (&amp;lt;code&amp;gt;merge_ingredients.html&amp;lt;/code&amp;gt;), and ingredient moving (&amp;lt;code&amp;gt;move_ingredients.html&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
== SEO and caching ==&lt;br /&gt;
&lt;br /&gt;
Four sitemap classes are registered at &amp;lt;code&amp;gt;/sitemap.xml&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Sitemap !! Items !! Priority !! Frequency&lt;br /&gt;
|-&lt;br /&gt;
| Recipes || All recipes || 0.9 || daily&lt;br /&gt;
|-&lt;br /&gt;
| Recipe types || All types || 0.6 || weekly&lt;br /&gt;
|-&lt;br /&gt;
| Ingredients || All ingredient names || 0.5 || weekly&lt;br /&gt;
|-&lt;br /&gt;
| Static pages || home, about, types index, ingredients index || 0.4 || weekly&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
A &amp;lt;code&amp;gt;robots.txt&amp;lt;/code&amp;gt; view allows all crawlers and includes the sitemap URL. Django&#039;s file-based cache is used throughout, with cache timeouts ranging from 5 minutes (recipe detail for anonymous users) to 1 hour (ingredient and category indexes). Cache is invalidated wholesale via &amp;lt;code&amp;gt;post_save&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;post_delete&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;m2m_changed&amp;lt;/code&amp;gt; signals on the &amp;lt;code&amp;gt;Recipe&amp;lt;/code&amp;gt; model.&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
The application is deployed on an Ubuntu VPS behind Nginx, which serves static files and media directly and proxies application requests to Gunicorn on port 8001. The Gunicorn configuration specifies two synchronous workers, a 180-second request timeout (to accommodate long-running OpenAI image generation calls), and request-count-based worker recycling (1000 requests with ±100 jitter). Nginx is configured with &amp;lt;code&amp;gt;proxy_read_timeout 300s&amp;lt;/code&amp;gt; for the same reason. TLS is provided by [[Let&#039;s Encrypt]] via Certbot.&lt;br /&gt;
&lt;br /&gt;
== Middleware ==&lt;br /&gt;
&lt;br /&gt;
A custom &amp;lt;code&amp;gt;ClacksOverheadMiddleware&amp;lt;/code&amp;gt; adds the &amp;lt;code&amp;gt;X-Clacks-Overhead: GNU Terry Pratchett&amp;lt;/code&amp;gt; header to all responses, as a tribute to [[Terry Pratchett]].&lt;br /&gt;
&lt;br /&gt;
== Management commands ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Command !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;gen_recipe&amp;lt;/code&amp;gt; || Generate a single recipe from a free-text topic&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;gen_recipe_from_file&amp;lt;/code&amp;gt; || Create a recipe from a JSON file matching the output schema&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;gen_recipe_from_nyt&amp;lt;/code&amp;gt; || Full pipeline: fetch NYT articles, build prompt with variation rotation, generate recipe, optionally save to database&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;fix_garbled_text&amp;lt;/code&amp;gt; || One-time cleanup of control-character artifacts in existing recipes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;fetch_nyt_inspiration&amp;lt;/code&amp;gt; || Download NYT front page and fetch RSS articles; manage variation state&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;fetch_recipe_inspiration&amp;lt;/code&amp;gt; || Scrape NYT Cooking and Dagelijkse Kost for reference recipes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;create_ingredient_categories&amp;lt;/code&amp;gt; || Seed the hierarchical ingredient category tree&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;auto_categorize_ingredients&amp;lt;/code&amp;gt; || Pattern-matching assignment of categories to uncategorised ingredients&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;find_duplicate_ingredients&amp;lt;/code&amp;gt; || Detect and merge duplicate ingredient names&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;normalize_instructions&amp;lt;/code&amp;gt; || Post-migration cleanup for instruction group schema&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;seed_recipes&amp;lt;/code&amp;gt; || Populate the database with sample recipes&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Artificial intelligence art]]&lt;br /&gt;
* [[Computational creativity]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Digest&amp;diff=404</id>
		<title>Digest</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Digest&amp;diff=404"/>
		<updated>2026-04-13T21:25:12Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Created page with &amp;quot;{{Infobox | 01_name         = Digest | 02_url          = https://digest.yusupov.cloud | 03_developer    = Michel Vuijlsteke | 04_released     = 2025 | 05_genre        = AI-generated recipe application | 06_language     = Python | 07_framework    = Django 5.2 | 08_license      = Proprietary }}  &amp;#039;&amp;#039;&amp;#039;Digest&amp;#039;&amp;#039;&amp;#039; is a web application hosted at &amp;lt;code&amp;gt;digest.yusupov.cloud&amp;lt;/code&amp;gt; that generates and publishes AI-created recipes inspired by current news articles. Each day, the s...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| 01_name         = Digest&lt;br /&gt;
| 02_url          = https://digest.yusupov.cloud&lt;br /&gt;
| 03_developer    = Michel Vuijlsteke&lt;br /&gt;
| 04_released     = 2025&lt;br /&gt;
| 05_genre        = AI-generated recipe application&lt;br /&gt;
| 06_language     = Python&lt;br /&gt;
| 07_framework    = [[Django]] 5.2&lt;br /&gt;
| 08_license      = Proprietary&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Digest&#039;&#039;&#039; is a web application hosted at &amp;lt;code&amp;gt;digest.yusupov.cloud&amp;lt;/code&amp;gt; that generates and publishes AI-created recipes inspired by current news articles. Each day, the system fetches articles from the &#039;&#039;New York Times&#039;&#039; RSS feed, selects two at random, and uses them as creative prompts for a large language model to invent a realistic, seasonally appropriate recipe. A companion image-generation pipeline produces editorial-quality food photography for each recipe using a composition diversity system that enforces visual variety across generations. The site&#039;s tagline is &amp;quot;Daily recipes inspired by the news.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Django 5.2 with [[SQLite]] as its database.&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt in the project repository lists Django ≥5.2, Pillow ≥10, openai ≥1.40, python-dotenv ≥1.0, gunicorn ≥21, requests ≥2.31, beautifulsoup4 ≥4.12, and markdown ≥3.5.&amp;lt;/ref&amp;gt; It is deployed behind [[Nginx]] with [[Gunicorn]] on an Ubuntu server running two synchronous workers on port 8001. Additional dependencies include [[Pillow (imaging library)|Pillow]] for image processing and optimisation, the [[OpenAI]] Python client for language-model and image-generation calls, [[Beautiful Soup (HTML parser)|Beautiful Soup]] for scraping recipe inspiration from external sites, [[Markdown (markup language)|python-markdown]] for rendering recipe inspiration text, and [[python-dotenv]] for environment configuration. The front end uses [[Bootstrap]] 5.3.3 with Bootstrap Icons, and typography is set in Roboto (body), Roboto Serif (headings), and Roboto Slab (accents), loaded from Google Fonts.&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
=== Recipe ===&lt;br /&gt;
&lt;br /&gt;
The central model is &amp;lt;code&amp;gt;Recipe&amp;lt;/code&amp;gt;, which stores:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;name&#039;&#039;, &#039;&#039;slug&#039;&#039; (auto-generated), &#039;&#039;intro&#039;&#039; (short paragraph), &#039;&#039;inspiration&#039;&#039; (120–200-word Markdown text linking to the source articles), and &#039;&#039;visual_description&#039;&#039; (a single sentence describing the plated dish for use as an image-generation prompt).&lt;br /&gt;
* &#039;&#039;hero_image&#039;&#039; (uploaded to &amp;lt;code&amp;gt;recipes/hero/&amp;lt;/code&amp;gt;), &#039;&#039;base_servings&#039;&#039; (default 4), &#039;&#039;prep_time_minutes&#039;&#039;, &#039;&#039;cook_time_minutes&#039;&#039;, &#039;&#039;is_featured&#039;&#039; (boolean), and automatic &#039;&#039;created_at&#039;&#039; / &#039;&#039;updated_at&#039;&#039; timestamps.&lt;br /&gt;
* A many-to-many relationship to &amp;lt;code&amp;gt;RecipeType&amp;lt;/code&amp;gt; (one of ten canonical types: Main, Salad, One-pan, Starter, Cold dish, Soup, Side dish, Dessert, Breakfast, or Snack).&lt;br /&gt;
&lt;br /&gt;
=== Structured ingredients ===&lt;br /&gt;
&lt;br /&gt;
Ingredients are organised into optional titled groups (e.g. &amp;quot;Dough&amp;quot;, &amp;quot;Sauce&amp;quot;) via &amp;lt;code&amp;gt;IngredientGroup&amp;lt;/code&amp;gt;. Each &amp;lt;code&amp;gt;Ingredient&amp;lt;/code&amp;gt; within a group has:&lt;br /&gt;
&lt;br /&gt;
* A foreign key to &amp;lt;code&amp;gt;IngredientName&amp;lt;/code&amp;gt; (a canonical master list of deduplicated ingredient names, each with a slug and an optional foreign key to &amp;lt;code&amp;gt;IngredientCategory&amp;lt;/code&amp;gt;).&lt;br /&gt;
* A &#039;&#039;quantity&#039;&#039; (decimal), a foreign key to &amp;lt;code&amp;gt;Unit&amp;lt;/code&amp;gt; (with &#039;&#039;name_singular&#039;&#039; and &#039;&#039;name_plural&#039;&#039;, e.g. &amp;quot;cup&amp;quot;/&amp;quot;cups&amp;quot;, &amp;quot;clove&amp;quot;/&amp;quot;cloves&amp;quot;, &amp;quot;g&amp;quot;/&amp;quot;g&amp;quot;), an optional &#039;&#039;note&#039;&#039; (e.g. &amp;quot;finely diced&amp;quot;, &amp;quot;zested then juiced&amp;quot;), and an &#039;&#039;order&#039;&#039; field.&lt;br /&gt;
²&lt;br /&gt;
&amp;lt;code&amp;gt;IngredientCategory&amp;lt;/code&amp;gt; supports arbitrary hierarchy via a self-referencing parent foreign key, enabling structures such as Fruit → Citrus → Lemon.&lt;br /&gt;
&lt;br /&gt;
=== Instructions ===&lt;br /&gt;
&lt;br /&gt;
Recipe steps are stored as &amp;lt;code&amp;gt;Direction&amp;lt;/code&amp;gt; rows, optionally grouped into titled &amp;lt;code&amp;gt;InstructionGroup&amp;lt;/code&amp;gt; sections. Both models carry an &#039;&#039;order&#039;&#039; field for sequencing.&lt;br /&gt;
&lt;br /&gt;
=== Image prompt history ===&lt;br /&gt;
&lt;br /&gt;
Every AI image generation attempt is recorded in &amp;lt;code&amp;gt;RecipeImagePrompt&amp;lt;/code&amp;gt;, which stores the full prompt text, the visual description, five composition dimension values (camera angle, framing, setting, lighting style, mood), a &#039;&#039;was_centered&#039;&#039; boolean, and a creation timestamp. This history is queried by the composition diversity system to enforce variety across successive hero images.&lt;br /&gt;
&lt;br /&gt;
== Recipe generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Recipe generation is driven by Django management commands and a service layer in &amp;lt;code&amp;gt;recipes/services/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Inspiration sourcing ===&lt;br /&gt;
&lt;br /&gt;
Two management commands gather external inspiration:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;fetch_nyt_inspiration&amp;lt;/code&amp;gt; downloads the current &#039;&#039;New York Times&#039;&#039; front page image (saved to &amp;lt;code&amp;gt;static/img/nyt/&amp;lt;/code&amp;gt;) and fetches articles from the NYT homepage RSS feed. It also manages a weighted variation-rotation state file (&amp;lt;code&amp;gt;data/nyt_variation_state.json&amp;lt;/code&amp;gt;) that tracks which recipe types, cooking techniques, proteins, bases, and connection styles have been used recently.&lt;br /&gt;
* &amp;lt;code&amp;gt;fetch_recipe_inspiration&amp;lt;/code&amp;gt; scrapes the &#039;&#039;NYT Cooking&#039;&#039; and &#039;&#039;Dagelijkse Kost&#039;&#039; (VRT) websites for reference recipes, extracting titles, descriptions, and ingredient lists from embedded JSON-LD &amp;lt;code&amp;gt;Recipe&amp;lt;/code&amp;gt; markup. Results are written to &amp;lt;code&amp;gt;data/inspiration.json&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
A PowerShell script (&amp;lt;code&amp;gt;scripts/schedule_inspiration_refresh.ps1&amp;lt;/code&amp;gt;) automates the inspiration refresh on a three-day cycle via Windows Task Scheduler.&lt;br /&gt;
&lt;br /&gt;
=== Variation rotation ===&lt;br /&gt;
&lt;br /&gt;
To prevent repetitive output, the system maintains weighted &amp;quot;draw bags&amp;quot; for five dimensions of recipe variety:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Dimension !! Pool size !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| Recipe type || 10 || Main (weight 7), One-pan (3), Soup (3), Salad (2), …&lt;br /&gt;
|-&lt;br /&gt;
| Technique || 13 || pan-searing (3), roasting (3), stir-frying (2), sous vide (2), …&lt;br /&gt;
|-&lt;br /&gt;
| Protein || 9 || chicken (3), beef (3), fish (2), shellfish (2), mushrooms (2), …&lt;br /&gt;
|-&lt;br /&gt;
| Base || 8 || rice (3), pasta (3), potatoes (3), flatbread (2), noodles (2), …&lt;br /&gt;
|-&lt;br /&gt;
| Connection style || 10 || geographical (10), seasonal (4), historical (3), narrative (3), …&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Each dimension uses a weighted bag: items are added with multiplicity proportional to their weight, and one is drawn per run without replacement. When the bag empties it is refilled. Immediate repeats of the same pick are avoided when alternatives exist. State is persisted to JSON between runs.&lt;br /&gt;
&lt;br /&gt;
=== Prompt construction ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;gen_recipe_from_nyt&amp;lt;/code&amp;gt; command orchestrates the full pipeline:&lt;br /&gt;
&lt;br /&gt;
# Two articles are selected from the RSS feed, filtered against a usage log (&amp;lt;code&amp;gt;data/inspiration_usage.json&amp;lt;/code&amp;gt;) to avoid reuse.&lt;br /&gt;
# Variation picks are drawn for recipe type, technique, protein, base, and connection style.&lt;br /&gt;
# A structured prompt is assembled, instructing the model to act as a professional recipe developer, invent one realistic recipe that a home cook in Belgium can make using seasonally appropriate ingredients, stay within 75 minutes total time, and use the drawn picks. The prompt includes both article summaries and one extra recipe reference from the inspiration pool (with a 2:1 preference for VRT over NYT sources).&lt;br /&gt;
# The prompt is appended with strict JSON schema instructions specifying the output format: name, intro, inspiration, visual description, servings, times, typed ingredient groups with quantities and units, titled instruction groups, and a recipe type from the canonical list.&lt;br /&gt;
&lt;br /&gt;
=== Text generation ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;OpenAIRecipeGenerator&amp;lt;/code&amp;gt; service class sends the prompt to the OpenAI Responses API. The default model is GPT-5, configurable via environment variable. The system prompt instructs the model to behave as a seasoned chef with Michelin-starred and home-cooking experience, and enforces detailed culinary realism rules: precise quantities (&amp;quot;325g not 300g&amp;quot;), specified heat levels and sensory cues, timing ranges, prep details in ingredient notes, assertive seasoning with salt quantities, and instructions that read like a tested recipe.&amp;lt;ref name=&amp;quot;system-prompt&amp;quot;&amp;gt;The system prompt in &amp;lt;code&amp;gt;recipes/services/openai_recipes.py&amp;lt;/code&amp;gt; specifies: &amp;quot;You are a seasoned chef and food editor with 20 years of Michelin-starred and home-cooking experience.&amp;quot;&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The API call requests JSON object output format. If the model or SDK does not support the &amp;lt;code&amp;gt;text&amp;lt;/code&amp;gt; format parameter, the call falls back to unformatted output. Similarly, if the model does not support the &amp;lt;code&amp;gt;temperature&amp;lt;/code&amp;gt; parameter (as is the case with GPT-5), the call automatically retries without it.&lt;br /&gt;
&lt;br /&gt;
=== Validation ===&lt;br /&gt;
&lt;br /&gt;
After generation, the recipe JSON is passed through a self-critique step: a second, cheaper API call instructs the model to review the recipe for culinary realism — checking that quantities make sense for the serving count, that cook times are realistic for the described technique, that no ingredients mentioned in the instructions are missing from the ingredient list, and that prep notes match the steps. The model returns a corrected JSON object or the original if no issues are found. If the critique call fails, the original is used unchanged.&lt;br /&gt;
&lt;br /&gt;
=== Inspiration rewriting ===&lt;br /&gt;
&lt;br /&gt;
After generation, the pipeline inspects the &amp;lt;code&amp;gt;inspiration&amp;lt;/code&amp;gt; field. If it is shorter than 120 words, contains the forbidden words &amp;quot;article&amp;quot; or &amp;quot;news,&amp;quot; or lacks Markdown anchor links to both source articles, a rewrite call asks the model to produce a 120–200-word vivid text weaving concrete ideas from both sources, ending with properly formatted attribution links.&lt;br /&gt;
&lt;br /&gt;
=== Persistence ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;save_recipe_from_data&amp;lt;/code&amp;gt; function maps the generated JSON to Django models within a single transaction:&lt;br /&gt;
&lt;br /&gt;
* Creates the &amp;lt;code&amp;gt;Recipe&amp;lt;/code&amp;gt; row with all scalar fields.&lt;br /&gt;
* Resolves recipe types against the canonical allowed list (by slug); creates missing &amp;lt;code&amp;gt;RecipeType&amp;lt;/code&amp;gt; rows only for canonical names.&lt;br /&gt;
* Creates &amp;lt;code&amp;gt;IngredientGroup&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Ingredient&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;IngredientName&amp;lt;/code&amp;gt; (get-or-create by slug), and &amp;lt;code&amp;gt;Unit&amp;lt;/code&amp;gt; (get-or-create by singular name) rows.&lt;br /&gt;
* Normalises instruction groups: merges untitled single-step groups into one, handles multiple JSON shapes (list of strings, list of dicts with &amp;lt;code&amp;gt;step&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;text&amp;lt;/code&amp;gt; keys, plain text blobs), and filters artifacts shorter than three characters.&lt;br /&gt;
* Triggers hero image generation (non-fatal by default; controlled by the &amp;lt;code&amp;gt;OPENAI_IMAGE_REQUIRED&amp;lt;/code&amp;gt; setting).&lt;br /&gt;
&lt;br /&gt;
== Hero image generation ==&lt;br /&gt;
&lt;br /&gt;
Image generation is a three-layer system: a low-level OpenAI client, a composition diversity engine, and a high-level orchestrator.&lt;br /&gt;
&lt;br /&gt;
=== Image API client ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;OpenAIImageGenerator&amp;lt;/code&amp;gt; calls the OpenAI image generation endpoint (model &amp;lt;code&amp;gt;gpt-image-1.5&amp;lt;/code&amp;gt; by default, configurable via &amp;lt;code&amp;gt;OPENAI_IMAGE_MODEL&amp;lt;/code&amp;gt;) and returns raw PNG bytes decoded from the base64 API response. The default output size is 1536×1024 (landscape), matching typical food photography framing.&amp;lt;ref name=&amp;quot;image-size&amp;quot;&amp;gt;The default size is set in &amp;lt;code&amp;gt;recipes/services/openai_images.py&amp;lt;/code&amp;gt; and can be overridden via the &amp;lt;code&amp;gt;OPENAI_IMAGE_SIZE&amp;lt;/code&amp;gt; setting.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Composition diversity ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;FoodPhotoPromptGenerator&amp;lt;/code&amp;gt; enforces visual variety across successive hero images by selecting compositions from the Cartesian product of five dimensions:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Dimension !! Count !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| Camera angle || 8 || overhead flat lay, 45° oblique, eye-level, low side, macro detail, handheld documentary, three-quarter view, tilted dutch angle&lt;br /&gt;
|-&lt;br /&gt;
| Framing || 11 || wide with negative space, tight crop, off-center, partial out-of-frame, action/utensil-in-motion, ingredient close-up, shallow depth-of-field, symmetrical centered, diagonal composition, …&lt;br /&gt;
|-&lt;br /&gt;
| Setting || 11 || white studio, stainless pro kitchen, home counter, picnic/outdoor, dark restaurant table, marble surface, wooden cutting board, concrete backdrop, vintage tile, slate serving board, …&lt;br /&gt;
|-&lt;br /&gt;
| Lighting || 12 || hard sun, soft window, dramatic low-key, warm tungsten, backlit steam, golden hour, bounce fill, rim lighting, diffused overhead, candlelight, …&lt;br /&gt;
|-&lt;br /&gt;
| Mood || 12 || modern editorial, rustic farmhouse, minimalist, street-food, celebratory, cozy homestyle, vibrant colorful, moody atmospheric, fresh healthy, indulgent decadent, …&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
This yields approximately 13,000 possible combinations. The selection algorithm:&lt;br /&gt;
&lt;br /&gt;
# Generates all combinations and shuffles them using a cryptographically secure random source (&amp;lt;code&amp;gt;secrets.SystemRandom&amp;lt;/code&amp;gt;).&lt;br /&gt;
# Filters candidates against: angle+setting pairs used in the last 30 days (banned), recent compositions within the history window (must differ in at least 3 of 5 dimensions), and a centering constraint (no centered composition if the previous image was centered).&lt;br /&gt;
# Falls back to progressively relaxed criteria if no perfect match exists.&lt;br /&gt;
&lt;br /&gt;
=== Prompt construction ===&lt;br /&gt;
&lt;br /&gt;
The selected composition is assembled into a detailed image prompt that specifies:&lt;br /&gt;
&lt;br /&gt;
* The dish name and a one-sentence visual description.&lt;br /&gt;
* A style directive requesting &amp;quot;high-end editorial food photography for a cookbook or food magazine,&amp;quot; with natural imperfections — &amp;quot;slight char marks, a drip of sauce, steam rising, herbs slightly wilted from heat.&amp;quot;&lt;br /&gt;
* The five composition dimensions.&lt;br /&gt;
* Food styling details: realistic portion sizes, contextual props (linen napkin, scattered herbs, olive oil drizzle), visible textures (crispy skin, glossy glaze, charred edges), and a natural, non-oversaturated colour palette.&lt;br /&gt;
* Hard constraints: photorealistic only (no illustrations), no text or watermarks, no human faces or hands, and avoidance of rustic wood unless specified.&lt;br /&gt;
&lt;br /&gt;
=== Persistence and attachment ===&lt;br /&gt;
&lt;br /&gt;
Each generation creates a &amp;lt;code&amp;gt;RecipeImagePrompt&amp;lt;/code&amp;gt; record storing all composition metadata. The generated PNG is attached to the recipe&#039;s &amp;lt;code&amp;gt;hero_image&amp;lt;/code&amp;gt; field via Django&#039;s file storage. On-demand regeneration is available through the upload-image view, which also accepts manual image uploads (validated for JPEG, PNG, or WebP content type, maximum 20 MB, downscaled to 2048px and converted to JPEG at 85% quality).&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Home page ===&lt;br /&gt;
&lt;br /&gt;
The home page displays a featured recipe (hero image with title, intro, and call-to-action) and a grid of the seven most recent recipes as cards with 1:1 hero images. The layout is responsive: seven columns on desktop, three on tablet, two on mobile.&lt;br /&gt;
&lt;br /&gt;
=== Recipe detail ===&lt;br /&gt;
&lt;br /&gt;
Each recipe page (&amp;lt;code&amp;gt;/r/&amp;lt;slug&amp;gt;/&amp;lt;/code&amp;gt;) features:&lt;br /&gt;
&lt;br /&gt;
* A hero section with the image (left, 1:1 aspect ratio, rounded shadow) and title, intro, recipe type badges (linked to type pages), and time estimates (prep, cook, total) with ingredient count.&lt;br /&gt;
* A sticky sidebar with the ingredient list, organised by groups, with a servings control (−/+ buttons and direct input). Ingredient quantities are scaled client-side using the formula &amp;lt;code&amp;gt;scaled_qty = base_qty × (servings / base_servings)&amp;lt;/code&amp;gt;, with unit pluralisation logic handling irregular forms (cup/cups, clove/cloves).&lt;br /&gt;
* Numbered instructions below, optionally grouped by titled sections.&lt;br /&gt;
* For authenticated users: edit, delete, and regenerate-image controls, plus a collapsible display of the raw image generation prompt.&lt;br /&gt;
&lt;br /&gt;
The page emits [[Schema.org]] &amp;lt;code&amp;gt;Recipe&amp;lt;/code&amp;gt; JSON-LD markup (name, image, prep/cook times in ISO 8601 duration format, yield, ingredient list, and &amp;lt;code&amp;gt;HowToStep&amp;lt;/code&amp;gt; instructions), Open Graph meta tags, and a canonical URL.&lt;br /&gt;
&lt;br /&gt;
=== Browsing ===&lt;br /&gt;
&lt;br /&gt;
The site provides several browsing views, all with file-based caching:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;Meals&#039;&#039; (&amp;lt;code&amp;gt;/types/&amp;lt;/code&amp;gt;): lists recipe types with the latest example recipe for each, linking to paginated type detail pages (12 recipes per page).&lt;br /&gt;
* &#039;&#039;Ingredients&#039;&#039; (&amp;lt;code&amp;gt;/ingredients/&amp;lt;/code&amp;gt;): a master ingredient index with recipe usage counts, linking to paginated ingredient detail pages (12 recipes per page).&lt;br /&gt;
* &#039;&#039;Categories&#039;&#039; (&amp;lt;code&amp;gt;/categories/&amp;lt;/code&amp;gt;): a hierarchical ingredient category tree with subcategories and ingredient counts, linking to paginated category detail pages (24 items per page).&lt;br /&gt;
* &#039;&#039;About&#039;&#039; (&amp;lt;code&amp;gt;/about/&amp;lt;/code&amp;gt;): a static information page.&lt;br /&gt;
&lt;br /&gt;
=== Theme ===&lt;br /&gt;
&lt;br /&gt;
The site supports light and dark colour themes, toggled via a moon/sun icon button in the navigation bar and persisted in &amp;lt;code&amp;gt;localStorage&amp;lt;/code&amp;gt;. Dark mode uses a &amp;lt;code&amp;gt;#121212&amp;lt;/code&amp;gt; background, &amp;lt;code&amp;gt;#e6e6e6&amp;lt;/code&amp;gt; text, and yellow (&amp;lt;code&amp;gt;rgb(255, 193, 7)&amp;lt;/code&amp;gt;) accent links, with high-contrast card and list-group styling.&lt;br /&gt;
&lt;br /&gt;
== Administration ==&lt;br /&gt;
&lt;br /&gt;
The Django admin interface is customised with:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;RecipeAdmin&amp;lt;/code&amp;gt;: list display with name, servings, times, creation date, and featured flag; search across name, intro, and inspiration; inline editing of instruction groups and directions; horizontal filter for recipe types.&lt;br /&gt;
* &amp;lt;code&amp;gt;IngredientNameAdmin&amp;lt;/code&amp;gt;: list with category and recipe count; bulk actions for assigning or removing categories, and for merging duplicate ingredient names (which transfers all ingredient references to the merge target).&lt;br /&gt;
* &amp;lt;code&amp;gt;IngredientCategoryAdmin&amp;lt;/code&amp;gt;: hierarchical display with direct and recursive ingredient counts; inline subcategory editing; a custom overview view showing category statistics; and an action for moving ingredients between subcategories.&lt;br /&gt;
&lt;br /&gt;
Custom admin templates provide forms for category assignment (&amp;lt;code&amp;gt;assign_category.html&amp;lt;/code&amp;gt;), category overview (&amp;lt;code&amp;gt;category_overview.html&amp;lt;/code&amp;gt;), ingredient merging (&amp;lt;code&amp;gt;merge_ingredients.html&amp;lt;/code&amp;gt;), and ingredient moving (&amp;lt;code&amp;gt;move_ingredients.html&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
== SEO and caching ==&lt;br /&gt;
&lt;br /&gt;
Four sitemap classes are registered at &amp;lt;code&amp;gt;/sitemap.xml&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Sitemap !! Items !! Priority !! Frequency&lt;br /&gt;
|-&lt;br /&gt;
| Recipes || All recipes || 0.9 || daily&lt;br /&gt;
|-&lt;br /&gt;
| Recipe types || All types || 0.6 || weekly&lt;br /&gt;
|-&lt;br /&gt;
| Ingredients || All ingredient names || 0.5 || weekly&lt;br /&gt;
|-&lt;br /&gt;
| Static pages || home, about, types index, ingredients index || 0.4 || weekly&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
A &amp;lt;code&amp;gt;robots.txt&amp;lt;/code&amp;gt; view allows all crawlers and includes the sitemap URL. Django&#039;s file-based cache is used throughout, with cache timeouts ranging from 5 minutes (recipe detail for anonymous users) to 1 hour (ingredient and category indexes). Cache is invalidated wholesale via &amp;lt;code&amp;gt;post_save&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;post_delete&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;m2m_changed&amp;lt;/code&amp;gt; signals on the &amp;lt;code&amp;gt;Recipe&amp;lt;/code&amp;gt; model.&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
The application is deployed on an Ubuntu VPS behind Nginx, which serves static files and media directly and proxies application requests to Gunicorn on port 8001. The Gunicorn configuration specifies two synchronous workers, a 180-second request timeout (to accommodate long-running OpenAI image generation calls), and request-count-based worker recycling (1000 requests with ±100 jitter). Nginx is configured with &amp;lt;code&amp;gt;proxy_read_timeout 300s&amp;lt;/code&amp;gt; for the same reason. TLS is provided by [[Let&#039;s Encrypt]] via Certbot.&lt;br /&gt;
&lt;br /&gt;
== Middleware ==&lt;br /&gt;
&lt;br /&gt;
A custom &amp;lt;code&amp;gt;ClacksOverheadMiddleware&amp;lt;/code&amp;gt; adds the &amp;lt;code&amp;gt;X-Clacks-Overhead: GNU Terry Pratchett&amp;lt;/code&amp;gt; header to all responses, as a tribute to [[Terry Pratchett]].&lt;br /&gt;
&lt;br /&gt;
== Management commands ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Command !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;gen_recipe&amp;lt;/code&amp;gt; || Generate a single recipe from a free-text topic&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;gen_recipe_from_file&amp;lt;/code&amp;gt; || Create a recipe from a JSON file matching the output schema&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;gen_recipe_from_nyt&amp;lt;/code&amp;gt; || Full pipeline: fetch NYT articles, build prompt with variation rotation, generate recipe, optionally save to database&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;fetch_nyt_inspiration&amp;lt;/code&amp;gt; || Download NYT front page and fetch RSS articles; manage variation state&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;fetch_recipe_inspiration&amp;lt;/code&amp;gt; || Scrape NYT Cooking and Dagelijkse Kost for reference recipes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;create_ingredient_categories&amp;lt;/code&amp;gt; || Seed the hierarchical ingredient category tree&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;auto_categorize_ingredients&amp;lt;/code&amp;gt; || Pattern-matching assignment of categories to uncategorised ingredients&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;find_duplicate_ingredients&amp;lt;/code&amp;gt; || Detect and merge duplicate ingredient names&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;normalize_instructions&amp;lt;/code&amp;gt; || Post-migration cleanup for instruction group schema&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;seed_recipes&amp;lt;/code&amp;gt; || Populate the database with sample recipes&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Artificial intelligence art]]&lt;br /&gt;
* [[Computational creativity]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=403</id>
		<title>Yusupov.cloud</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=403"/>
		<updated>2026-04-13T21:25:00Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name           = yusupov.cloud&lt;br /&gt;
| 1 url            = https://yusupov.cloud&lt;br /&gt;
| 2 type           = Personal web sites&lt;br /&gt;
| 3 owner          = [[Michel Vuijlsteke]]&lt;br /&gt;
| 4 launched       = 2025&lt;br /&gt;
| 5 current_status = Online&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;yusupov.cloud&#039;&#039;&#039; is a personal domain and virtual private server operated by Belgian technologist [[Michel Vuijlsteke]]. It hosts multiple small web applications on subdomains and at the apex domain. One of these is a MediaWiki installation titled “Yusupov’s House.” The setup is presented as a web-era continuation of the do-it-yourself ethos of Vuijlsteke’s 1990s BBS of the same name.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot;&amp;gt;“Yusupov’s House,” &#039;&#039;yusupov.cloud&#039;&#039; (wiki), accessed 10 October 2025, https://yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
Among the projects on the domain is a MediaWiki (at the apex, &#039;&#039;yusupov.cloud&#039;&#039;) running MediaWiki 1.44.0 with PHP 8.3.6 (FPM) and SQLite, using the Vector skin and core extensions for citations and template scripting.&amp;lt;ref name=&amp;quot;version&amp;quot;&amp;gt;‘‘Special:Version’’ page, &#039;&#039;yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://yusupov.cloud/wiki/Special:Version&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Subdomains and projects ==&lt;br /&gt;
Publicly visible projects include:&lt;br /&gt;
&lt;br /&gt;
* [https://acbc.yusupov.cloud acbc.yusupov.cloud] — &#039;&#039;A Cabinet of Brief Curiosities&#039;&#039;, generating tiny three-sentence surreal/horror micro-stories with an hourly cadence and an archive. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;acbc-home&amp;quot;&amp;gt;A Cabinet of Brief Curiosities (home), &#039;&#039;acbc.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://acbc.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://agenda.yusupov.cloud agenda.yusupov.cloud] — &#039;&#039;A Life in Planners&#039;&#039;, a structured journal chronicling the final years of the operator’s mother, with calendar, food, medications, measurements, and statistics views (multilingual UI).&amp;lt;ref name=&amp;quot;agenda&amp;quot;&amp;gt;“A life in planners,” &#039;&#039;agenda.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://agenda.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://cloud.yusupov.cloud cloud.yusupov.cloud] — a series of static html creative coding experiments, simulations, and games including: timebeat, fire and snake simulations, biomass metaballs, cs3, &#039;&#039;Cross&#039;&#039; crossword puzzle game, image dithering tool, books, &#039;&#039;Elite Galaxy Explorer&#039;&#039;, ZX Spectrum loading screen simulator, Carcassonne, 3D boids flocking algorithm, physarum slime mold simulation, temps temperature visualization, &#039;&#039;The Chronicle of Hamurabi&#039;&#039; ancient Sumeria resource management game, and gatekeeper.&amp;lt;ref name=&amp;quot;cloud-home&amp;quot;&amp;gt;&amp;quot;cloud,&amp;quot; &#039;&#039;cloud.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://cloud.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://digest.yusupov.cloud digest.yusupov.cloud] — &#039;&#039;[[Digest]]&#039;&#039;, daily seasonal AI-assisted recipes inspired by current events, browsable by meal type and ingredients.&amp;lt;ref name=&amp;quot;digest-home&amp;quot;&amp;gt;“Digest — Daily recipes inspired by the news,” &#039;&#039;digest.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://digest.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://echoes.yusupov.cloud echoes.yusupov.cloud] — &#039;&#039;[[Echoes of What Wasn&#039;t]]&#039;&#039;, an AI-generated alternate-history newspaper presenting richly detailed articles about historical events as if they had unfolded differently. A pipeline scrapes real events from multilingual Wikipedia, uses OpenAI to craft a divergent narrative with period-appropriate prose and DALL-E imagery, and publishes via a REST API. Features article browsing by month, a &amp;quot;Where/When&amp;quot; interactive map-and-timeline view using Leaflet, and a picture desk. (Built with Wagtail 7/Django 5 per operator.)&amp;lt;ref name=&amp;quot;echoes-home&amp;quot;&amp;gt;&amp;quot;Echoes — Dispatches from Histories That Never Were,&amp;quot; &#039;&#039;echoes.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://echoes.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://quidlibet.yusupov.cloud quidlibet.yusupov.cloud] — &#039;&#039;[[Quidlibet]]&#039;&#039;, an app that generates fictional books complete with synopsis, author bio, and faux reviews; includes genre and author archives. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;quidlibet-home&amp;quot;&amp;gt;“Quidlibet — Book Generator,” &#039;&#039;quidlibet.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://quidlibet.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://tyov-web.yusupov.cloud tyov-web.yusupov.cloud] — a web implementation of the solo RPG &#039;&#039;[[Thousand Year Old Vampire]]&#039;&#039;, with Django 5 backend and Vue 3 frontend. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
Operator-reported (not publicly discoverable at time of writing):&lt;br /&gt;
&lt;br /&gt;
* skills.yusupov.cloud — a skills matrix application. (Per operator.)&lt;br /&gt;
* resources.yusupov.cloud — a simple resource plannign calendar. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
== Technology ==&lt;br /&gt;
The wiki stack is documented on &#039;&#039;Special:Version&#039;&#039;. Individual apps are described by the operator as Flask (&#039;&#039;acbc&#039;&#039;, &#039;&#039;quidlibet&#039;&#039;) and Django 5 + Vue 3 (&#039;&#039;tyov-web&#039;&#039;).&amp;lt;ref name=&amp;quot;version&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Relation to the BBS ==&lt;br /&gt;
The project name references Vuijlsteke’s single-line BBS (FidoNet 2:291/1925) active between 1990 and 1995. While the VPS is not a BBS, its single-admin, self-maintained hosting reprises the early DIY approach.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;nodehist&amp;quot;&amp;gt;“Nodelist history search: History of node 2:291/1925,” NodeHist, accessed 10 October 2025, https://nodehist.fidonet.org.ua/?address=2%3A291%2F1925&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Yusupov&#039;s House]] (1990s BBS)&lt;br /&gt;
* [[Michel Vuijlsteke]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Personal websites]]&lt;br /&gt;
[[Category:Belgian websites]]&lt;br /&gt;
[[Category:2025 establishments in Belgium]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Quidlibet&amp;diff=402</id>
		<title>Quidlibet</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Quidlibet&amp;diff=402"/>
		<updated>2026-04-13T13:50:01Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| 01_name         = Quidlibet&lt;br /&gt;
| 02_url          = https://quidlibet.yusupov.cloud&lt;br /&gt;
| 03_developer    = Michel Vuijlsteke&lt;br /&gt;
| 04_released     = 2025&lt;br /&gt;
| 05_genre        = AI-generated fictional book library&lt;br /&gt;
| 06_language     = Python&lt;br /&gt;
| 07_framework    = [[Flask]] 3.0&lt;br /&gt;
| 08_license      = Proprietary&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Quidlibet&#039;&#039;&#039; (Latin for &amp;quot;anything whatsoever&amp;quot;) is a web application hosted at &amp;lt;code&amp;gt;quidlibet.yusupov.cloud&amp;lt;/code&amp;gt; that generates and catalogues entirely fictional books. Each book is a complete literary artefact: a plausible title, a named author with a biographical sketch and portrait, a Markdown-formatted synopsis, an AI-generated cover in genre-appropriate style, a publication date and page count, and between four and eight reader reviews with individually calibrated star ratings. The site presents itself as a browsable library catalogue under the heading &amp;quot;Possible Books,&amp;quot; inviting visitors to enter a title and optionally an author and genre for &amp;quot;a book that you&#039;d like to read but that doesn&#039;t actually exist.&amp;quot; In addition to on-demand generation, an automated pipeline publishes up to twelve new books per day on a cron schedule.&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Flask 3.0 with Flask-SQLAlchemy as its ORM and Flask-Login for authentication.&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt in the project repository lists Flask 3.0.3, Flask-SQLAlchemy 3.1.1, Flask-Login 0.6.3, and Flask-Caching 2.3.0.&amp;lt;/ref&amp;gt; It uses [[SQLite]] as its database and is deployed behind [[Nginx]] on a Linux VPS. Additional dependencies include [[Pillow (imaging library)|Pillow]] for image processing, the Python &amp;lt;code&amp;gt;markdown&amp;lt;/code&amp;gt; library for rich-text rendering, [[Beautiful Soup (HTML parser)|Beautiful Soup]] for web scraping, &amp;lt;code&amp;gt;python-dotenv&amp;lt;/code&amp;gt; for configuration, and the [[OpenAI]] Python client for all language-model and image-generation calls. The front end uses Bootstrap 5.3 with a custom CSS layer, Google Fonts (Roboto Serif, Roboto Slab, Roboto), and Bootstrap Icons. The site is installable as a [[progressive web application]] via a Web App Manifest and a minimal service worker.&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
=== Books ===&lt;br /&gt;
&lt;br /&gt;
Each book record carries a &#039;&#039;title&#039;&#039;, a foreign key to an &#039;&#039;Author&#039;&#039;, a foreign key to a &#039;&#039;Genre&#039;&#039; (with a legacy genre-name string for backward compatibility), a Markdown &#039;&#039;synopsis&#039;&#039;, a &#039;&#039;num_pages&#039;&#039; integer, an ISO 8601 &#039;&#039;publication_date&#039;&#039;, a &#039;&#039;cover_image&#039;&#039; path, an &#039;&#039;avg_rating&#039;&#039; and &#039;&#039;rating_count&#039;&#039; (derived from reviews), and the &#039;&#039;ip_address&#039;&#039; and timestamp of the generation request. Books are addressed by URL slug in the form &amp;lt;code&amp;gt;/book/{id}-{title-slug}&amp;lt;/code&amp;gt;, where the leading integer guarantees uniqueness and the slug provides readability.&lt;br /&gt;
&lt;br /&gt;
=== Authors ===&lt;br /&gt;
&lt;br /&gt;
Authors are stored with a &#039;&#039;name&#039;&#039; in &amp;quot;Lastname, Firstname&amp;quot; format (rendered as &amp;quot;Firstname Lastname&amp;quot; in the interface via a Jinja display-name filter), a Markdown-formatted &#039;&#039;bio&#039;&#039;, and an optional &#039;&#039;author_image&#039;&#039; path. Author names are matched case-insensitively using Python&#039;s &amp;lt;code&amp;gt;SequenceMatcher&amp;lt;/code&amp;gt; with a similarity threshold, so that minor variations do not create duplicate records.&lt;br /&gt;
&lt;br /&gt;
=== Genres ===&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;Genre&#039;&#039; model supports hierarchical categorisation via a self-referential &#039;&#039;parent_id&#039;&#039; foreign key. Each genre has a &#039;&#039;name&#039;&#039;, an optional &#039;&#039;description&#039;&#039;, a hex &#039;&#039;color&#039;&#039; code for UI tags, an &#039;&#039;icon&#039;&#039; class, a &#039;&#039;display_order&#039;&#039;, and an &#039;&#039;is_active&#039;&#039; flag.&lt;br /&gt;
&lt;br /&gt;
=== Reviews ===&lt;br /&gt;
&lt;br /&gt;
Each review belongs to a book and carries a &#039;&#039;reviewer_name&#039;&#039;, a &#039;&#039;review_date&#039;&#039; (in-universe), Markdown &#039;&#039;review_text&#039;&#039;, and a &#039;&#039;stars&#039;&#039; rating between one and five. Reviews are cascade-deleted when their parent book is removed.&lt;br /&gt;
&lt;br /&gt;
=== Supporting models ===&lt;br /&gt;
&lt;br /&gt;
A &#039;&#039;GenerationJob&#039;&#039; tracks the asynchronous lifecycle of each generation request (pending → processing → completed or failed), with a &#039;&#039;progress_message&#039;&#039; field polled by the front end. A &#039;&#039;GenerationLog&#039;&#039; provides an audit trail of every generation attempt — successful or otherwise — including the model used, the API surface called, and any error message. A &#039;&#039;GenerationQueueItem&#039;&#039; implements a database-backed seed queue as an alternative to the file-based daily queue.&lt;br /&gt;
&lt;br /&gt;
== Book generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Book generation is performed in the main Flask application via a multi-step pipeline of OpenAI API calls. The default text model is GPT-5; the image model is &amp;lt;code&amp;gt;gpt-image-1.5&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Title and metadata ===&lt;br /&gt;
&lt;br /&gt;
When a visitor submits a title (and optionally an author and genre), the application creates a &#039;&#039;GenerationJob&#039;&#039; and redirects to a status page that polls for progress. On the status page, while the visitor waits, a rotating display of faux-library-science progress messages — &amp;quot;Reticulating splines…,&amp;quot; &amp;quot;Reconciling author names against authority files…,&amp;quot; &amp;quot;De-duplicating near-identical editions and printings…,&amp;quot; &amp;quot;Calibrating star ratings to review sentiment…&amp;quot; — plays at random intervals for entertainment. Behind the scenes, the pipeline sends a structured prompt to the text model asking it to return a JSON object with the book&#039;s synopsis, page count, publication date, author name (invented if not supplied), and an author biography. The prompt instructs the model to write the synopsis in Markdown with bold and italic formatting, to keep it under two paragraphs of 120 words each, and to anchor it with concrete names, places, and objects rather than vague abstractions.&lt;br /&gt;
&lt;br /&gt;
=== Author reuse and continuity ===&lt;br /&gt;
&lt;br /&gt;
Before generating a new author, the pipeline queries the database for existing authors whose names are at least 80% similar (by sequence-matching across four name-format permutations). If a match is found — which happens for roughly a quarter of all generated books — the pipeline reuses the existing author record. It supplies the model with a context dossier listing the author&#039;s existing books, genres, and publication dates, and instructs it to revise the biography so that it naturally covers the author&#039;s expanded body of work. A hard constraint requires that any new publication date differ from the author&#039;s existing dates by at least six months. Biographies are post-processed by a helper that detects and corrects any instance of the &amp;quot;Lastname, Firstname&amp;quot; storage format that may have leaked into the prose.&lt;br /&gt;
&lt;br /&gt;
When the model must invent a new author, the prompt includes a dynamically generated blocklist of first and last names that already appear two or more times in the database, preventing the library from accumulating clusters of identically named authors. The blocklist is rebuilt from the live database on every generation run.&lt;br /&gt;
&lt;br /&gt;
=== Synopses and formatting ===&lt;br /&gt;
&lt;br /&gt;
Author biographies are written in Markdown: the author&#039;s name is &#039;&#039;&#039;bold&#039;&#039;&#039; on first mention and for notable awards, book titles are &#039;&#039;italicised&#039;&#039;, and paragraphs are separated by blank lines. Synopses follow similar conventions. All Markdown content is rendered through a Jinja filter backed by the Python &amp;lt;code&amp;gt;markdown&amp;lt;/code&amp;gt; library (with extensions for line breaks, fenced code, and tables). On grid pages where biographies appear as truncated previews, the Markdown is first rendered to HTML, then stripped of tags, so that formatting tokens do not leak into the plaintext snippet.&lt;br /&gt;
&lt;br /&gt;
=== Review generation and rating profiles ===&lt;br /&gt;
&lt;br /&gt;
Reviews are generated in a separate API call. Before prompting, the pipeline constructs a deterministic &#039;&#039;rating profile&#039;&#039; by seeding a random number generator with the SHA-256 hash of the book&#039;s title, synopsis, and publication date. This seed selects one of four rating clusters — low (18% probability, mean 2.2–3.0), mid (34%, mean 3.2–3.9), high (26%, mean 4.0–4.6), or polarised (22%, mean 3.0–3.8 with high spread) — and derives a target mean, standard deviation, skew direction, and a rant count (zero to two extended reviews of either one–two or five stars).&lt;br /&gt;
&lt;br /&gt;
The model is asked to produce four to eight reviews, each assigned a distinct &#039;&#039;stylistic palette&#039;&#039; from a rotating set of eight: crisp capsule (one or two punchy sentences), craft critique (prose, structure, pacing), character study (interiority, dialogue, motives), worldbuilding lens (setting, atmosphere, rules), theme tracer (motifs and subtext), comparative take (comparisons to two non-celebrity authors), sceptic&#039;s ledger (bullet-point pros and cons), and librarian angle (audience notes). Each review also rotates through a primary focus — characters, plot, prose, world, themes, or audience — so that no two reviews in a set read the same way. Review dates must fall between the book&#039;s publication date and the present day. The prompt bans stock phrases (&amp;quot;page-turner,&amp;quot; &amp;quot;unputdownable&amp;quot;) and requires diverse, plausible reviewer names.&lt;br /&gt;
&lt;br /&gt;
After creation, the pipeline calculates the book&#039;s average rating and review count directly from the database and stores them on the book record.&lt;br /&gt;
&lt;br /&gt;
=== Cover generation ===&lt;br /&gt;
&lt;br /&gt;
Cover images are generated via OpenAI&#039;s &amp;lt;code&amp;gt;gpt-image-1.5&amp;lt;/code&amp;gt; model at 1024 × 1536 pixels (2:3 portrait aspect). The prompt is tailored to the book&#039;s genre through a weighted style-selection system: cookbooks receive an eightfold bias toward photographic covers; graphic novels toward vector/graphic styles; science fiction, fantasy, and horror toward illustration; biography and history toward photography; and romance toward an even mix. The prompt specifies the exact title and author name as cover typography and bans watermarks. Generated images are decoded from base64, converted to optimised progressive JPEG via Pillow (quality 92), and saved as &amp;lt;code&amp;gt;book_{id}.jpg&amp;lt;/code&amp;gt;. The pipeline allows two attempts; if both fail, the entire book-generation transaction is rolled back so that no coverless book can enter the database.&lt;br /&gt;
&lt;br /&gt;
=== Author-photo generation ===&lt;br /&gt;
&lt;br /&gt;
After a successful cover, the pipeline checks whether the author already has a portrait. If not, it sends a prompt to &amp;lt;code&amp;gt;gpt-image-1.5&amp;lt;/code&amp;gt; for a photorealistic head-and-shoulders portrait at 1024 × 1024 pixels. The result is centre-cropped to a 512 × 512 square JPEG and saved as &amp;lt;code&amp;gt;author_{id}.jpg&amp;lt;/code&amp;gt;. Unlike cover generation, photo generation is non-blocking: failures are logged but do not abort the job. In the interface, authors without photos receive a CSS-generated avatar showing their initials.&lt;br /&gt;
&lt;br /&gt;
=== Error handling and transaction safety ===&lt;br /&gt;
&lt;br /&gt;
If any exception occurs during book creation, review insertion, or image generation, the pipeline issues an immediate &amp;lt;code&amp;gt;db.session.rollback()&amp;lt;/code&amp;gt; to discard all uncommitted data. The &#039;&#039;GenerationJob&#039;&#039; and &#039;&#039;GenerationLog&#039;&#039; records — committed in an earlier transaction — survive, so the failure is auditable. The job is marked as failed with a descriptive error message. In templates, all cover &amp;lt;code&amp;gt;&amp;amp;lt;img&amp;amp;gt;&amp;lt;/code&amp;gt; tags include an &amp;lt;code&amp;gt;onerror&amp;lt;/code&amp;gt; handler that replaces a broken image with a CSS gradient placeholder bearing the book&#039;s title, as a defence against stale database references to missing files.&lt;br /&gt;
&lt;br /&gt;
== Daily batch generation ==&lt;br /&gt;
&lt;br /&gt;
A standalone script, &amp;lt;code&amp;gt;fast_generator.py&amp;lt;/code&amp;gt;, generates twelve book seeds (title, author, genre) in a single GPT-5 API call lasting roughly twelve seconds. The batch pipeline incorporates several layers of anti-repetition machinery.&lt;br /&gt;
&lt;br /&gt;
=== Goodreads title inspiration ===&lt;br /&gt;
&lt;br /&gt;
Before prompting, the script scrapes two to three randomly chosen genre pages on [[Goodreads]] (from a pool of eight: historical fiction, history, fantasy, science fiction, romance, cookbooks, general fiction, and horror). It extracts book titles from &amp;lt;code&amp;gt;div.bookBox img[alt]&amp;lt;/code&amp;gt; elements, cleans them of series numbers, author suffixes, and emoji, and presents a sample as &amp;quot;inspiration titles — for tone and flavour only, not to be reproduced.&amp;quot; If Goodreads is unreachable, the script falls back to a curated JSON file of titles harvested from the [[Internet Archive]] and [[Open Library]].&lt;br /&gt;
&lt;br /&gt;
=== Anti-sameness system ===&lt;br /&gt;
&lt;br /&gt;
To prevent the model from gravitating toward a narrow band of title shapes, the script enforces structural diversity through five mechanisms:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Title-class rotation.&#039;&#039;&#039; Ten structural classes — concrete object, place or institution, named person, event or incident, documentary phrase, odd juxtaposition, fragment or question, subtitle-led, idiomatic phrase, and one-word punch — are allocated evenly across the batch so that each slot has a designated shape.&lt;br /&gt;
# &#039;&#039;&#039;Avoidance guidance.&#039;&#039;&#039; The script analyses a rolling memory of up to 2,000 previously generated titles (persisted in a JSON file) plus all titles already in the database. It identifies the twenty most overused content words, the most common title frames (e.g., &amp;quot;The Last ___,&amp;quot; &amp;quot;Beyond the ___&amp;quot;), the most frequent opening words, and the most frequent closing nouns, and encodes these as explicit &amp;quot;do not use&amp;quot; directives in the prompt.&lt;br /&gt;
# &#039;&#039;&#039;Batch validator.&#039;&#039;&#039; After generation, each title in the batch is scored for repetitiveness: collisions on first word, end noun, or structural frame within the batch incur penalties of 0.5–1.5 points; matches against historical overuse patterns add further penalties; and two-word titles composed entirely of generic mood words score a 2.0 penalty. Titles exceeding a cumulative penalty of 3.0 are rejected and regenerated.&lt;br /&gt;
# &#039;&#039;&#039;Title memory.&#039;&#039;&#039; Accepted titles are appended to the rolling memory file (capped at 2,000 entries, oldest dropped first) and checked against the database to prevent exact duplicates.&lt;br /&gt;
# &#039;&#039;&#039;Database deduplication.&#039;&#039;&#039; Each final title is compared case-insensitively to every existing book in the database; duplicates are silently skipped.&lt;br /&gt;
&lt;br /&gt;
A parallel system applies to author names. The prompt includes a dynamically built blocklist of first and last names that already appear two or more times across the library&#039;s author table, and requires that no two authors within a single batch share a first name. The local fallback generator (used when the OpenAI API is unavailable) enforces the same within-batch uniqueness constraint on first names.&lt;br /&gt;
&lt;br /&gt;
The output is written to &amp;lt;code&amp;gt;daily_books.json&amp;lt;/code&amp;gt;, a flat array of title/author/genre objects with generation timestamps.&lt;br /&gt;
&lt;br /&gt;
=== Hourly processing ===&lt;br /&gt;
&lt;br /&gt;
A companion script, &amp;lt;code&amp;gt;simple_hourly_processor.py&amp;lt;/code&amp;gt;, runs once per hour via cron. It selects one book from the daily JSON file — indexed by a hash of the current date and hour — and triggers its full generation (metadata, reviews, cover, author photo) by calling the application&#039;s &amp;lt;code&amp;gt;/cron/hourly_book_full&amp;lt;/code&amp;gt; endpoint. If all twelve daily seeds have already been processed, the hour is skipped. In production, a typical crontab runs the batch generator daily at midnight and the hourly processor every other hour, yielding up to twelve new books per day.&lt;br /&gt;
&lt;br /&gt;
== Cron security ==&lt;br /&gt;
&lt;br /&gt;
Cron endpoints are protected by a localhost check: the request&#039;s client IP must be &amp;lt;code&amp;gt;127.0.0.1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;::1&amp;lt;/code&amp;gt;, or &amp;lt;code&amp;gt;localhost&amp;lt;/code&amp;gt;. For deployments where the cron job calls the public URL rather than the loopback address, two overrides are available: an &amp;lt;code&amp;gt;X-API-Key&amp;lt;/code&amp;gt; header (or &amp;lt;code&amp;gt;api_key&amp;lt;/code&amp;gt; query parameter) checked against an environment variable, or a global &amp;lt;code&amp;gt;DISABLE_CRON_LOCALHOST_CHECK&amp;lt;/code&amp;gt; flag.&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Home page ===&lt;br /&gt;
&lt;br /&gt;
The home page displays a hero section with the title &amp;quot;Possible Books,&amp;quot; a rotating tagline (changed every thirty minutes, seeded by the current UTC half-hour), and a generation form with an autocomplete datalist of existing authors. Below the form, a grid of the twelve most recent books (with cover images or gradient fallbacks) appears alongside the twenty-five most recent reviews, sorted by review date.&lt;br /&gt;
&lt;br /&gt;
=== Book pages ===&lt;br /&gt;
&lt;br /&gt;
Each book page shows the cover image (or fallback), title, author byline linked to the author&#039;s page, genre tag, page count, publication date, and average star rating. The synopsis and author biography are rendered from Markdown. Below, reviews are listed with reviewer name, star display (filled and hollow star characters), review date, and Markdown-formatted review text. Administrators see additional controls: edit book, regenerate cover, delete book, regenerate reviews, and recalculate rating.&lt;br /&gt;
&lt;br /&gt;
=== Author pages ===&lt;br /&gt;
&lt;br /&gt;
The author page displays the portrait (or a CSS initials placeholder), the full Markdown biography, and a grid of the author&#039;s books. The authors index lists all authors sorted alphabetically by surname, with a plaintext preview of each biography (Markdown rendered, tags stripped, truncated to 100 characters).&lt;br /&gt;
&lt;br /&gt;
=== Archive ===&lt;br /&gt;
&lt;br /&gt;
The archive page presents a paginated, filterable grid of all books. A sidebar offers filters by publication-year range (dynamically bucketed), minimum average rating (one to five stars), and genre (checkboxes, grouped by popularity). Filters are applied via form auto-submission on change; an explicit &amp;quot;clear filters&amp;quot; link resets the view.&lt;br /&gt;
&lt;br /&gt;
=== Genres ===&lt;br /&gt;
&lt;br /&gt;
The genres page lists all active genres alphabetically with book counts.&lt;br /&gt;
&lt;br /&gt;
=== Theme ===&lt;br /&gt;
&lt;br /&gt;
The site defaults to a dark colour scheme (&amp;lt;code&amp;gt;#111315&amp;lt;/code&amp;gt; background, &amp;lt;code&amp;gt;#e6e6e6&amp;lt;/code&amp;gt; text) and supports a light mode toggled via a moon-icon button in the masthead, persisted in &amp;lt;code&amp;gt;localStorage&amp;lt;/code&amp;gt;. The toggle respects the operating system&#039;s &amp;lt;code&amp;gt;prefers-color-scheme&amp;lt;/code&amp;gt; setting as a default. Typography uses Roboto Serif for display headings and Roboto for body text and UI elements.&lt;br /&gt;
&lt;br /&gt;
== Progressive web application ==&lt;br /&gt;
&lt;br /&gt;
The site ships a Web App Manifest declaring standalone display mode, dark background and theme colours, and maskable icons at 192 and 512 pixels. A service worker registers at the root scope but operates in passthrough mode — all fetch events are returned unmodified — to satisfy PWA installability requirements without risking stale cached pages in a database-backed application that publishes new content hourly.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[Procedural generation]]&lt;br /&gt;
* [[Flask (web framework)]]&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Quidlibet&amp;diff=401</id>
		<title>Quidlibet</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Quidlibet&amp;diff=401"/>
		<updated>2026-04-13T13:49:12Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| 01_name         = Quidlibet&lt;br /&gt;
| 02_url          = https://quidlibet.yusupov.cloud&lt;br /&gt;
| 03_developer    = Michel Vuijlsteke&lt;br /&gt;
| 04_released     = 2025&lt;br /&gt;
| 05_genre        = AI-generated fictional book library&lt;br /&gt;
| 06_language     = Python&lt;br /&gt;
| 07_framework    = [[Flask]] 3.0&lt;br /&gt;
| 08_license      = Proprietary&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Quidlibet&#039;&#039;&#039; (Latin for &amp;quot;anything whatsoever&amp;quot;) is a web application hosted at &amp;lt;code&amp;gt;quidlibet.yusupov.cloud&amp;lt;/code&amp;gt; that generates and catalogues entirely fictional books. Each book is a complete literary artefact: a plausible title, a named author with a biographical sketch and portrait, a Markdown-formatted synopsis, an AI-generated cover in genre-appropriate style, a publication date and page count, and between four and eight reader reviews with individually calibrated star ratings. The site presents itself as a browsable library catalogue under the heading &amp;quot;Possible Books,&amp;quot; inviting visitors to enter a title — and optionally an author and genre — for &amp;quot;a book that you&#039;d like to read but that doesn&#039;t actually exist.&amp;quot; In addition to on-demand generation, an automated pipeline publishes up to twelve new books per day on a cron schedule.&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Flask 3.0 with Flask-SQLAlchemy as its ORM and Flask-Login for authentication.&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt in the project repository lists Flask 3.0.3, Flask-SQLAlchemy 3.1.1, Flask-Login 0.6.3, and Flask-Caching 2.3.0.&amp;lt;/ref&amp;gt; It uses [[SQLite]] as its database and is deployed behind [[Nginx]] on a Linux VPS. Additional dependencies include [[Pillow (imaging library)|Pillow]] for image processing, the Python &amp;lt;code&amp;gt;markdown&amp;lt;/code&amp;gt; library for rich-text rendering, [[Beautiful Soup (HTML parser)|Beautiful Soup]] for web scraping, &amp;lt;code&amp;gt;python-dotenv&amp;lt;/code&amp;gt; for configuration, and the [[OpenAI]] Python client for all language-model and image-generation calls. The front end uses Bootstrap 5.3 with a custom CSS layer, Google Fonts (Roboto Serif, Roboto Slab, Roboto), and Bootstrap Icons. The site is installable as a [[progressive web application]] via a Web App Manifest and a minimal service worker.&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
=== Books ===&lt;br /&gt;
&lt;br /&gt;
Each book record carries a &#039;&#039;title&#039;&#039;, a foreign key to an &#039;&#039;Author&#039;&#039;, a foreign key to a &#039;&#039;Genre&#039;&#039; (with a legacy genre-name string for backward compatibility), a Markdown &#039;&#039;synopsis&#039;&#039;, a &#039;&#039;num_pages&#039;&#039; integer, an ISO 8601 &#039;&#039;publication_date&#039;&#039;, a &#039;&#039;cover_image&#039;&#039; path, an &#039;&#039;avg_rating&#039;&#039; and &#039;&#039;rating_count&#039;&#039; (derived from reviews), and the &#039;&#039;ip_address&#039;&#039; and timestamp of the generation request. Books are addressed by URL slug in the form &amp;lt;code&amp;gt;/book/{id}-{title-slug}&amp;lt;/code&amp;gt;, where the leading integer guarantees uniqueness and the slug provides readability.&lt;br /&gt;
&lt;br /&gt;
=== Authors ===&lt;br /&gt;
&lt;br /&gt;
Authors are stored with a &#039;&#039;name&#039;&#039; in &amp;quot;Lastname, Firstname&amp;quot; format (rendered as &amp;quot;Firstname Lastname&amp;quot; in the interface via a Jinja display-name filter), a Markdown-formatted &#039;&#039;bio&#039;&#039;, and an optional &#039;&#039;author_image&#039;&#039; path. Author names are matched case-insensitively using Python&#039;s &amp;lt;code&amp;gt;SequenceMatcher&amp;lt;/code&amp;gt; with a similarity threshold, so that minor variations do not create duplicate records.&lt;br /&gt;
&lt;br /&gt;
=== Genres ===&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;Genre&#039;&#039; model supports hierarchical categorisation via a self-referential &#039;&#039;parent_id&#039;&#039; foreign key. Each genre has a &#039;&#039;name&#039;&#039;, an optional &#039;&#039;description&#039;&#039;, a hex &#039;&#039;color&#039;&#039; code for UI tags, an &#039;&#039;icon&#039;&#039; class, a &#039;&#039;display_order&#039;&#039;, and an &#039;&#039;is_active&#039;&#039; flag.&lt;br /&gt;
&lt;br /&gt;
=== Reviews ===&lt;br /&gt;
&lt;br /&gt;
Each review belongs to a book and carries a &#039;&#039;reviewer_name&#039;&#039;, a &#039;&#039;review_date&#039;&#039; (in-universe), Markdown &#039;&#039;review_text&#039;&#039;, and a &#039;&#039;stars&#039;&#039; rating between one and five. Reviews are cascade-deleted when their parent book is removed.&lt;br /&gt;
&lt;br /&gt;
=== Supporting models ===&lt;br /&gt;
&lt;br /&gt;
A &#039;&#039;GenerationJob&#039;&#039; tracks the asynchronous lifecycle of each generation request (pending → processing → completed or failed), with a &#039;&#039;progress_message&#039;&#039; field polled by the front end. A &#039;&#039;GenerationLog&#039;&#039; provides an audit trail of every generation attempt — successful or otherwise — including the model used, the API surface called, and any error message. A &#039;&#039;GenerationQueueItem&#039;&#039; implements a database-backed seed queue as an alternative to the file-based daily queue.&lt;br /&gt;
&lt;br /&gt;
== Book generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Book generation is performed in the main Flask application via a multi-step pipeline of OpenAI API calls. The default text model is GPT-5; the image model is &amp;lt;code&amp;gt;gpt-image-1.5&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Title and metadata ===&lt;br /&gt;
&lt;br /&gt;
When a visitor submits a title (and optionally an author and genre), the application creates a &#039;&#039;GenerationJob&#039;&#039; and redirects to a status page that polls for progress. On the status page, while the visitor waits, a rotating display of faux-library-science progress messages — &amp;quot;Reticulating splines…,&amp;quot; &amp;quot;Reconciling author names against authority files…,&amp;quot; &amp;quot;De-duplicating near-identical editions and printings…,&amp;quot; &amp;quot;Calibrating star ratings to review sentiment…&amp;quot; — plays at random intervals for entertainment. Behind the scenes, the pipeline sends a structured prompt to the text model asking it to return a JSON object with the book&#039;s synopsis, page count, publication date, author name (invented if not supplied), and an author biography. The prompt instructs the model to write the synopsis in Markdown with bold and italic formatting, to keep it under two paragraphs of 120 words each, and to anchor it with concrete names, places, and objects rather than vague abstractions.&lt;br /&gt;
&lt;br /&gt;
=== Author reuse and continuity ===&lt;br /&gt;
&lt;br /&gt;
Before generating a new author, the pipeline queries the database for existing authors whose names are at least 80% similar (by sequence-matching across four name-format permutations). If a match is found — which happens for roughly a quarter of all generated books — the pipeline reuses the existing author record. It supplies the model with a context dossier listing the author&#039;s existing books, genres, and publication dates, and instructs it to revise the biography so that it naturally covers the author&#039;s expanded body of work. A hard constraint requires that any new publication date differ from the author&#039;s existing dates by at least six months. Biographies are post-processed by a helper that detects and corrects any instance of the &amp;quot;Lastname, Firstname&amp;quot; storage format that may have leaked into the prose.&lt;br /&gt;
&lt;br /&gt;
When the model must invent a new author, the prompt includes a dynamically generated blocklist of first and last names that already appear two or more times in the database, preventing the library from accumulating clusters of identically named authors. The blocklist is rebuilt from the live database on every generation run.&lt;br /&gt;
&lt;br /&gt;
=== Synopses and formatting ===&lt;br /&gt;
&lt;br /&gt;
Author biographies are written in Markdown: the author&#039;s name is &#039;&#039;&#039;bold&#039;&#039;&#039; on first mention and for notable awards, book titles are &#039;&#039;italicised&#039;&#039;, and paragraphs are separated by blank lines. Synopses follow similar conventions. All Markdown content is rendered through a Jinja filter backed by the Python &amp;lt;code&amp;gt;markdown&amp;lt;/code&amp;gt; library (with extensions for line breaks, fenced code, and tables). On grid pages where biographies appear as truncated previews, the Markdown is first rendered to HTML, then stripped of tags, so that formatting tokens do not leak into the plaintext snippet.&lt;br /&gt;
&lt;br /&gt;
=== Review generation and rating profiles ===&lt;br /&gt;
&lt;br /&gt;
Reviews are generated in a separate API call. Before prompting, the pipeline constructs a deterministic &#039;&#039;rating profile&#039;&#039; by seeding a random number generator with the SHA-256 hash of the book&#039;s title, synopsis, and publication date. This seed selects one of four rating clusters — low (18% probability, mean 2.2–3.0), mid (34%, mean 3.2–3.9), high (26%, mean 4.0–4.6), or polarised (22%, mean 3.0–3.8 with high spread) — and derives a target mean, standard deviation, skew direction, and a rant count (zero to two extended reviews of either one–two or five stars).&lt;br /&gt;
&lt;br /&gt;
The model is asked to produce four to eight reviews, each assigned a distinct &#039;&#039;stylistic palette&#039;&#039; from a rotating set of eight: crisp capsule (one or two punchy sentences), craft critique (prose, structure, pacing), character study (interiority, dialogue, motives), worldbuilding lens (setting, atmosphere, rules), theme tracer (motifs and subtext), comparative take (comparisons to two non-celebrity authors), sceptic&#039;s ledger (bullet-point pros and cons), and librarian angle (audience notes). Each review also rotates through a primary focus — characters, plot, prose, world, themes, or audience — so that no two reviews in a set read the same way. Review dates must fall between the book&#039;s publication date and the present day. The prompt bans stock phrases (&amp;quot;page-turner,&amp;quot; &amp;quot;unputdownable&amp;quot;) and requires diverse, plausible reviewer names.&lt;br /&gt;
&lt;br /&gt;
After creation, the pipeline calculates the book&#039;s average rating and review count directly from the database and stores them on the book record.&lt;br /&gt;
&lt;br /&gt;
=== Cover generation ===&lt;br /&gt;
&lt;br /&gt;
Cover images are generated via OpenAI&#039;s &amp;lt;code&amp;gt;gpt-image-1.5&amp;lt;/code&amp;gt; model at 1024 × 1536 pixels (2:3 portrait aspect). The prompt is tailored to the book&#039;s genre through a weighted style-selection system: cookbooks receive an eightfold bias toward photographic covers; graphic novels toward vector/graphic styles; science fiction, fantasy, and horror toward illustration; biography and history toward photography; and romance toward an even mix. The prompt specifies the exact title and author name as cover typography and bans watermarks. Generated images are decoded from base64, converted to optimised progressive JPEG via Pillow (quality 92), and saved as &amp;lt;code&amp;gt;book_{id}.jpg&amp;lt;/code&amp;gt;. The pipeline allows two attempts; if both fail, the entire book-generation transaction is rolled back so that no coverless book can enter the database.&lt;br /&gt;
&lt;br /&gt;
=== Author-photo generation ===&lt;br /&gt;
&lt;br /&gt;
After a successful cover, the pipeline checks whether the author already has a portrait. If not, it sends a prompt to &amp;lt;code&amp;gt;gpt-image-1.5&amp;lt;/code&amp;gt; for a photorealistic head-and-shoulders portrait at 1024 × 1024 pixels. The result is centre-cropped to a 512 × 512 square JPEG and saved as &amp;lt;code&amp;gt;author_{id}.jpg&amp;lt;/code&amp;gt;. Unlike cover generation, photo generation is non-blocking: failures are logged but do not abort the job. In the interface, authors without photos receive a CSS-generated avatar showing their initials.&lt;br /&gt;
&lt;br /&gt;
=== Error handling and transaction safety ===&lt;br /&gt;
&lt;br /&gt;
If any exception occurs during book creation, review insertion, or image generation, the pipeline issues an immediate &amp;lt;code&amp;gt;db.session.rollback()&amp;lt;/code&amp;gt; to discard all uncommitted data. The &#039;&#039;GenerationJob&#039;&#039; and &#039;&#039;GenerationLog&#039;&#039; records — committed in an earlier transaction — survive, so the failure is auditable. The job is marked as failed with a descriptive error message. In templates, all cover &amp;lt;code&amp;gt;&amp;amp;lt;img&amp;amp;gt;&amp;lt;/code&amp;gt; tags include an &amp;lt;code&amp;gt;onerror&amp;lt;/code&amp;gt; handler that replaces a broken image with a CSS gradient placeholder bearing the book&#039;s title, as a defence against stale database references to missing files.&lt;br /&gt;
&lt;br /&gt;
== Daily batch generation ==&lt;br /&gt;
&lt;br /&gt;
A standalone script, &amp;lt;code&amp;gt;fast_generator.py&amp;lt;/code&amp;gt;, generates twelve book seeds (title, author, genre) in a single GPT-5 API call lasting roughly twelve seconds. The batch pipeline incorporates several layers of anti-repetition machinery.&lt;br /&gt;
&lt;br /&gt;
=== Goodreads title inspiration ===&lt;br /&gt;
&lt;br /&gt;
Before prompting, the script scrapes two to three randomly chosen genre pages on [[Goodreads]] (from a pool of eight: historical fiction, history, fantasy, science fiction, romance, cookbooks, general fiction, and horror). It extracts book titles from &amp;lt;code&amp;gt;div.bookBox img[alt]&amp;lt;/code&amp;gt; elements, cleans them of series numbers, author suffixes, and emoji, and presents a sample as &amp;quot;inspiration titles — for tone and flavour only, not to be reproduced.&amp;quot; If Goodreads is unreachable, the script falls back to a curated JSON file of titles harvested from the [[Internet Archive]] and [[Open Library]].&lt;br /&gt;
&lt;br /&gt;
=== Anti-sameness system ===&lt;br /&gt;
&lt;br /&gt;
To prevent the model from gravitating toward a narrow band of title shapes, the script enforces structural diversity through five mechanisms:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Title-class rotation.&#039;&#039;&#039; Ten structural classes — concrete object, place or institution, named person, event or incident, documentary phrase, odd juxtaposition, fragment or question, subtitle-led, idiomatic phrase, and one-word punch — are allocated evenly across the batch so that each slot has a designated shape.&lt;br /&gt;
# &#039;&#039;&#039;Avoidance guidance.&#039;&#039;&#039; The script analyses a rolling memory of up to 2,000 previously generated titles (persisted in a JSON file) plus all titles already in the database. It identifies the twenty most overused content words, the most common title frames (e.g., &amp;quot;The Last ___,&amp;quot; &amp;quot;Beyond the ___&amp;quot;), the most frequent opening words, and the most frequent closing nouns, and encodes these as explicit &amp;quot;do not use&amp;quot; directives in the prompt.&lt;br /&gt;
# &#039;&#039;&#039;Batch validator.&#039;&#039;&#039; After generation, each title in the batch is scored for repetitiveness: collisions on first word, end noun, or structural frame within the batch incur penalties of 0.5–1.5 points; matches against historical overuse patterns add further penalties; and two-word titles composed entirely of generic mood words score a 2.0 penalty. Titles exceeding a cumulative penalty of 3.0 are rejected and regenerated.&lt;br /&gt;
# &#039;&#039;&#039;Title memory.&#039;&#039;&#039; Accepted titles are appended to the rolling memory file (capped at 2,000 entries, oldest dropped first) and checked against the database to prevent exact duplicates.&lt;br /&gt;
# &#039;&#039;&#039;Database deduplication.&#039;&#039;&#039; Each final title is compared case-insensitively to every existing book in the database; duplicates are silently skipped.&lt;br /&gt;
&lt;br /&gt;
A parallel system applies to author names. The prompt includes a dynamically built blocklist of first and last names that already appear two or more times across the library&#039;s author table, and requires that no two authors within a single batch share a first name. The local fallback generator (used when the OpenAI API is unavailable) enforces the same within-batch uniqueness constraint on first names.&lt;br /&gt;
&lt;br /&gt;
The output is written to &amp;lt;code&amp;gt;daily_books.json&amp;lt;/code&amp;gt;, a flat array of title/author/genre objects with generation timestamps.&lt;br /&gt;
&lt;br /&gt;
=== Hourly processing ===&lt;br /&gt;
&lt;br /&gt;
A companion script, &amp;lt;code&amp;gt;simple_hourly_processor.py&amp;lt;/code&amp;gt;, runs once per hour via cron. It selects one book from the daily JSON file — indexed by a hash of the current date and hour — and triggers its full generation (metadata, reviews, cover, author photo) by calling the application&#039;s &amp;lt;code&amp;gt;/cron/hourly_book_full&amp;lt;/code&amp;gt; endpoint. If all twelve daily seeds have already been processed, the hour is skipped. In production, a typical crontab runs the batch generator daily at midnight and the hourly processor every other hour, yielding up to twelve new books per day.&lt;br /&gt;
&lt;br /&gt;
== Cron security ==&lt;br /&gt;
&lt;br /&gt;
Cron endpoints are protected by a localhost check: the request&#039;s client IP must be &amp;lt;code&amp;gt;127.0.0.1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;::1&amp;lt;/code&amp;gt;, or &amp;lt;code&amp;gt;localhost&amp;lt;/code&amp;gt;. For deployments where the cron job calls the public URL rather than the loopback address, two overrides are available: an &amp;lt;code&amp;gt;X-API-Key&amp;lt;/code&amp;gt; header (or &amp;lt;code&amp;gt;api_key&amp;lt;/code&amp;gt; query parameter) checked against an environment variable, or a global &amp;lt;code&amp;gt;DISABLE_CRON_LOCALHOST_CHECK&amp;lt;/code&amp;gt; flag.&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Home page ===&lt;br /&gt;
&lt;br /&gt;
The home page displays a hero section with the title &amp;quot;Possible Books,&amp;quot; a rotating tagline (changed every thirty minutes, seeded by the current UTC half-hour), and a generation form with an autocomplete datalist of existing authors. Below the form, a grid of the twelve most recent books (with cover images or gradient fallbacks) appears alongside the twenty-five most recent reviews, sorted by review date.&lt;br /&gt;
&lt;br /&gt;
=== Book pages ===&lt;br /&gt;
&lt;br /&gt;
Each book page shows the cover image (or fallback), title, author byline linked to the author&#039;s page, genre tag, page count, publication date, and average star rating. The synopsis and author biography are rendered from Markdown. Below, reviews are listed with reviewer name, star display (filled and hollow star characters), review date, and Markdown-formatted review text. Administrators see additional controls: edit book, regenerate cover, delete book, regenerate reviews, and recalculate rating.&lt;br /&gt;
&lt;br /&gt;
=== Author pages ===&lt;br /&gt;
&lt;br /&gt;
The author page displays the portrait (or a CSS initials placeholder), the full Markdown biography, and a grid of the author&#039;s books. The authors index lists all authors sorted alphabetically by surname, with a plaintext preview of each biography (Markdown rendered, tags stripped, truncated to 100 characters).&lt;br /&gt;
&lt;br /&gt;
=== Archive ===&lt;br /&gt;
&lt;br /&gt;
The archive page presents a paginated, filterable grid of all books. A sidebar offers filters by publication-year range (dynamically bucketed), minimum average rating (one to five stars), and genre (checkboxes, grouped by popularity). Filters are applied via form auto-submission on change; an explicit &amp;quot;clear filters&amp;quot; link resets the view.&lt;br /&gt;
&lt;br /&gt;
=== Genres ===&lt;br /&gt;
&lt;br /&gt;
The genres page lists all active genres alphabetically with book counts.&lt;br /&gt;
&lt;br /&gt;
=== Theme ===&lt;br /&gt;
&lt;br /&gt;
The site defaults to a dark colour scheme (&amp;lt;code&amp;gt;#111315&amp;lt;/code&amp;gt; background, &amp;lt;code&amp;gt;#e6e6e6&amp;lt;/code&amp;gt; text) and supports a light mode toggled via a moon-icon button in the masthead, persisted in &amp;lt;code&amp;gt;localStorage&amp;lt;/code&amp;gt;. The toggle respects the operating system&#039;s &amp;lt;code&amp;gt;prefers-color-scheme&amp;lt;/code&amp;gt; setting as a default. Typography uses Roboto Serif for display headings and Roboto for body text and UI elements.&lt;br /&gt;
&lt;br /&gt;
== Progressive web application ==&lt;br /&gt;
&lt;br /&gt;
The site ships a Web App Manifest declaring standalone display mode, dark background and theme colours, and maskable icons at 192 and 512 pixels. A service worker registers at the root scope but operates in passthrough mode — all fetch events are returned unmodified — to satisfy PWA installability requirements without risking stale cached pages in a database-backed application that publishes new content hourly.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[Procedural generation]]&lt;br /&gt;
* [[Flask (web framework)]]&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Quidlibet&amp;diff=400</id>
		<title>Quidlibet</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Quidlibet&amp;diff=400"/>
		<updated>2026-04-13T13:24:04Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| 01_name         = Quidlibet&lt;br /&gt;
| 02_url          = https://quidlibet.yusupov.cloud&lt;br /&gt;
| 03_developer    = Michel Vuijlsteke&lt;br /&gt;
| 04_released     = 2025&lt;br /&gt;
| 05_genre        = AI-generated fictional book library&lt;br /&gt;
| 06_language     = Python&lt;br /&gt;
| 07_framework    = [[Flask]] 3.0&lt;br /&gt;
| 08_license      = Proprietary&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Quidlibet&#039;&#039;&#039; (Latin for &amp;quot;anything whatsoever&amp;quot;) is a web application hosted at &amp;lt;code&amp;gt;quidlibet.yusupov.cloud&amp;lt;/code&amp;gt; that generates and catalogues entirely fictional books. Each book is a complete literary artefact: a plausible title, a named author with a biographical sketch and portrait, a Markdown-formatted synopsis, an AI-generated cover in genre-appropriate style, a publication date and page count, and between four and eight reader reviews with individually calibrated star ratings. The site presents itself as a browsable library catalogue under the heading &amp;quot;Possible Books,&amp;quot; inviting visitors to enter a title and optionally an author and genre for &amp;quot;a book that you&#039;d like to read but that doesn&#039;t actually exist.&amp;quot; In addition to on-demand generation, an automated pipeline publishes up to twelve new books per day on a cron schedule.&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Flask 3.0 with Flask-SQLAlchemy as its ORM and Flask-Login for authentication.&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt in the project repository lists Flask 3.0.3, Flask-SQLAlchemy 3.1.1, Flask-Login 0.6.3, and Flask-Caching 2.3.0.&amp;lt;/ref&amp;gt; It uses [[SQLite]] as its database and is deployed behind [[Nginx]] on a Linux VPS. Additional dependencies include [[Pillow (imaging library)|Pillow]] for image processing, the Python &amp;lt;code&amp;gt;markdown&amp;lt;/code&amp;gt; library for rich-text rendering, [[Beautiful Soup (HTML parser)|Beautiful Soup]] for web scraping, &amp;lt;code&amp;gt;python-dotenv&amp;lt;/code&amp;gt; for configuration, and the [[OpenAI]] Python client for all language-model and image-generation calls. The front end uses Bootstrap 5.3 with a custom CSS layer, Google Fonts (Roboto Serif, Roboto Slab, Roboto), and Bootstrap Icons. The site is installable as a [[progressive web application]] via a Web App Manifest and a minimal service worker.&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
=== Books ===&lt;br /&gt;
&lt;br /&gt;
Each book record carries a &#039;&#039;title&#039;&#039;, a foreign key to an &#039;&#039;Author&#039;&#039;, a foreign key to a &#039;&#039;Genre&#039;&#039; (with a legacy genre-name string for backward compatibility), a Markdown &#039;&#039;synopsis&#039;&#039;, a &#039;&#039;num_pages&#039;&#039; integer, an ISO 8601 &#039;&#039;publication_date&#039;&#039;, a &#039;&#039;cover_image&#039;&#039; path, an &#039;&#039;avg_rating&#039;&#039; and &#039;&#039;rating_count&#039;&#039; (derived from reviews), and the &#039;&#039;ip_address&#039;&#039; and timestamp of the generation request. Books are addressed by URL slug in the form &amp;lt;code&amp;gt;/book/{id}-{title-slug}&amp;lt;/code&amp;gt;, where the leading integer guarantees uniqueness and the slug provides readability.&lt;br /&gt;
&lt;br /&gt;
=== Authors ===&lt;br /&gt;
&lt;br /&gt;
Authors are stored with a &#039;&#039;name&#039;&#039; in &amp;quot;Lastname, Firstname&amp;quot; format (rendered as &amp;quot;Firstname Lastname&amp;quot; in the interface via a Jinja display-name filter), a Markdown-formatted &#039;&#039;bio&#039;&#039;, and an optional &#039;&#039;author_image&#039;&#039; path. Author names are matched case-insensitively using Python&#039;s &amp;lt;code&amp;gt;SequenceMatcher&amp;lt;/code&amp;gt; with a similarity threshold, so that minor variations do not create duplicate records.&lt;br /&gt;
&lt;br /&gt;
=== Genres ===&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;Genre&#039;&#039; model supports hierarchical categorisation via a self-referential &#039;&#039;parent_id&#039;&#039; foreign key. Each genre has a &#039;&#039;name&#039;&#039;, an optional &#039;&#039;description&#039;&#039;, a hex &#039;&#039;color&#039;&#039; code for UI tags, an &#039;&#039;icon&#039;&#039; class, a &#039;&#039;display_order&#039;&#039;, and an &#039;&#039;is_active&#039;&#039; flag.&lt;br /&gt;
&lt;br /&gt;
=== Reviews ===&lt;br /&gt;
&lt;br /&gt;
Each review belongs to a book and carries a &#039;&#039;reviewer_name&#039;&#039;, a &#039;&#039;review_date&#039;&#039; (in-universe), Markdown &#039;&#039;review_text&#039;&#039;, and a &#039;&#039;stars&#039;&#039; rating between one and five. Reviews are cascade-deleted when their parent book is removed.&lt;br /&gt;
&lt;br /&gt;
=== Supporting models ===&lt;br /&gt;
&lt;br /&gt;
A &#039;&#039;GenerationJob&#039;&#039; tracks the asynchronous lifecycle of each generation request (pending → processing → completed or failed), with a &#039;&#039;progress_message&#039;&#039; field polled by the front end. A &#039;&#039;GenerationLog&#039;&#039; provides an audit trail of every generation attempt — successful or otherwise — including the model used, the API surface called, and any error message. A &#039;&#039;GenerationQueueItem&#039;&#039; implements a database-backed seed queue as an alternative to the file-based daily queue.&lt;br /&gt;
&lt;br /&gt;
== Book generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Book generation is performed in the main Flask application via a multi-step pipeline of OpenAI API calls. The default text model is GPT-5; the image model is &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Title and metadata ===&lt;br /&gt;
&lt;br /&gt;
When a visitor submits a title (and optionally an author and genre), the application creates a &#039;&#039;GenerationJob&#039;&#039; and redirects to a status page that polls for progress. On the status page, while the visitor waits, a rotating display of faux-library-science progress messages — &amp;quot;Reticulating splines…,&amp;quot; &amp;quot;Reconciling author names against authority files…,&amp;quot; &amp;quot;De-duplicating near-identical editions and printings…,&amp;quot; &amp;quot;Calibrating star ratings to review sentiment…&amp;quot; — plays at random intervals for entertainment. Behind the scenes, the pipeline sends a structured prompt to the text model asking it to return a JSON object with the book&#039;s synopsis, page count, publication date, author name (invented if not supplied), and an author biography. The prompt instructs the model to write the synopsis in Markdown with bold and italic formatting, to keep it under two paragraphs of 120 words each, and to anchor it with concrete names, places, and objects rather than vague abstractions.&lt;br /&gt;
&lt;br /&gt;
=== Author reuse and continuity ===&lt;br /&gt;
&lt;br /&gt;
Before generating a new author, the pipeline queries the database for existing authors whose names are at least 80% similar (by sequence-matching across four name-format permutations). If a match is found — which happens for roughly a quarter of all generated books — the pipeline reuses the existing author record. It supplies the model with a context dossier listing the author&#039;s existing books, genres, and publication dates, and instructs it to revise the biography so that it naturally covers the author&#039;s expanded body of work. A hard constraint requires that any new publication date differ from the author&#039;s existing dates by at least six months. Biographies are post-processed by a helper that detects and corrects any instance of the &amp;quot;Lastname, Firstname&amp;quot; storage format that may have leaked into the prose.&lt;br /&gt;
&lt;br /&gt;
=== Synopses and formatting ===&lt;br /&gt;
&lt;br /&gt;
Author biographies are written in Markdown: the author&#039;s name is &#039;&#039;&#039;bold&#039;&#039;&#039; on first mention and for notable awards, book titles are &#039;&#039;italicised&#039;&#039;, and paragraphs are separated by blank lines. Synopses follow similar conventions. All Markdown content is rendered through a Jinja filter backed by the Python &amp;lt;code&amp;gt;markdown&amp;lt;/code&amp;gt; library (with extensions for line breaks, fenced code, and tables). On grid pages where biographies appear as truncated previews, the Markdown is first rendered to HTML, then stripped of tags, so that formatting tokens do not leak into the plaintext snippet.&lt;br /&gt;
&lt;br /&gt;
=== Review generation and rating profiles ===&lt;br /&gt;
&lt;br /&gt;
Reviews are generated in a separate API call. Before prompting, the pipeline constructs a deterministic &#039;&#039;rating profile&#039;&#039; by seeding a random number generator with the SHA-256 hash of the book&#039;s title, synopsis, and publication date. This seed selects one of four rating clusters — low (18% probability, mean 2.2–3.0), mid (34%, mean 3.2–3.9), high (26%, mean 4.0–4.6), or polarised (22%, mean 3.0–3.8 with high spread) — and derives a target mean, standard deviation, skew direction, and a rant count (zero to two extended reviews of either one–two or five stars).&lt;br /&gt;
&lt;br /&gt;
The model is asked to produce four to eight reviews, each assigned a distinct &#039;&#039;stylistic palette&#039;&#039; from a rotating set of eight: crisp capsule (one or two punchy sentences), craft critique (prose, structure, pacing), character study (interiority, dialogue, motives), worldbuilding lens (setting, atmosphere, rules), theme tracer (motifs and subtext), comparative take (comparisons to two non-celebrity authors), sceptic&#039;s ledger (bullet-point pros and cons), and librarian angle (audience notes). Each review also rotates through a primary focus — characters, plot, prose, world, themes, or audience — so that no two reviews in a set read the same way. Review dates must fall between the book&#039;s publication date and the present day. The prompt bans stock phrases (&amp;quot;page-turner,&amp;quot; &amp;quot;unputdownable&amp;quot;) and requires diverse, plausible reviewer names.&lt;br /&gt;
&lt;br /&gt;
After creation, the pipeline calculates the book&#039;s average rating and review count directly from the database and stores them on the book record.&lt;br /&gt;
&lt;br /&gt;
=== Cover generation ===&lt;br /&gt;
&lt;br /&gt;
Cover images are generated via OpenAI&#039;s &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt; model at 1024 × 1536 pixels (2:3 portrait aspect). The prompt is tailored to the book&#039;s genre through a weighted style-selection system: cookbooks receive an eightfold bias toward photographic covers; graphic novels toward vector/graphic styles; science fiction, fantasy, and horror toward illustration; biography and history toward photography; and romance toward an even mix. The prompt specifies the exact title and author name as cover typography and bans watermarks. Generated images are decoded from base64, converted to optimised progressive JPEG via Pillow (quality 92), and saved as &amp;lt;code&amp;gt;book_{id}.jpg&amp;lt;/code&amp;gt;. The pipeline allows two attempts; if both fail, the entire book-generation transaction is rolled back so that no coverless book can enter the database.&lt;br /&gt;
&lt;br /&gt;
=== Author-photo generation ===&lt;br /&gt;
&lt;br /&gt;
After a successful cover, the pipeline checks whether the author already has a portrait. If not, it sends a prompt to &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt; for a photorealistic head-and-shoulders portrait at 1024 × 1024 pixels. The result is centre-cropped to a 512 × 512 square JPEG and saved as &amp;lt;code&amp;gt;author_{id}.jpg&amp;lt;/code&amp;gt;. Unlike cover generation, photo generation is non-blocking: failures are logged but do not abort the job. In the interface, authors without photos receive a CSS-generated avatar showing their initials.&lt;br /&gt;
&lt;br /&gt;
=== Error handling and transaction safety ===&lt;br /&gt;
&lt;br /&gt;
If any exception occurs during book creation, review insertion, or image generation, the pipeline issues an immediate &amp;lt;code&amp;gt;db.session.rollback()&amp;lt;/code&amp;gt; to discard all uncommitted data. The &#039;&#039;GenerationJob&#039;&#039; and &#039;&#039;GenerationLog&#039;&#039; records — committed in an earlier transaction — survive, so the failure is auditable. The job is marked as failed with a descriptive error message. In templates, all cover &amp;lt;code&amp;gt;&amp;amp;lt;img&amp;amp;gt;&amp;lt;/code&amp;gt; tags include an &amp;lt;code&amp;gt;onerror&amp;lt;/code&amp;gt; handler that replaces a broken image with a CSS gradient placeholder bearing the book&#039;s title, as a defence against stale database references to missing files.&lt;br /&gt;
&lt;br /&gt;
== Daily batch generation ==&lt;br /&gt;
&lt;br /&gt;
A standalone script, &amp;lt;code&amp;gt;fast_generator.py&amp;lt;/code&amp;gt;, generates twelve book seeds (title, author, genre) in a single GPT-5 API call lasting roughly twelve seconds. The batch pipeline incorporates several layers of anti-repetition machinery.&lt;br /&gt;
&lt;br /&gt;
=== Goodreads title inspiration ===&lt;br /&gt;
&lt;br /&gt;
Before prompting, the script scrapes two to three randomly chosen genre pages on [[Goodreads]] (from a pool of eight: historical fiction, history, fantasy, science fiction, romance, cookbooks, general fiction, and horror). It extracts book titles from &amp;lt;code&amp;gt;div.bookBox img[alt]&amp;lt;/code&amp;gt; elements, cleans them of series numbers, author suffixes, and emoji, and presents a sample as &amp;quot;inspiration titles — for tone and flavour only, not to be reproduced.&amp;quot; If Goodreads is unreachable, the script falls back to a curated JSON file of titles harvested from the [[Internet Archive]] and [[Open Library]].&lt;br /&gt;
&lt;br /&gt;
=== Anti-sameness system ===&lt;br /&gt;
&lt;br /&gt;
To prevent the model from gravitating toward a narrow band of title shapes, the script enforces structural diversity through five mechanisms:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Title-class rotation.&#039;&#039;&#039; Ten structural classes — concrete object, place or institution, named person, event or incident, documentary phrase, odd juxtaposition, fragment or question, subtitle-led, idiomatic phrase, and one-word punch — are allocated evenly across the batch so that each slot has a designated shape.&lt;br /&gt;
# &#039;&#039;&#039;Avoidance guidance.&#039;&#039;&#039; The script analyses a rolling memory of up to 2,000 previously generated titles (persisted in a JSON file) plus all titles already in the database. It identifies the twenty most overused content words, the most common title frames (e.g., &amp;quot;The Last ___,&amp;quot; &amp;quot;Beyond the ___&amp;quot;), the most frequent opening words, and the most frequent closing nouns, and encodes these as explicit &amp;quot;do not use&amp;quot; directives in the prompt.&lt;br /&gt;
# &#039;&#039;&#039;Batch validator.&#039;&#039;&#039; After generation, each title in the batch is scored for repetitiveness: collisions on first word, end noun, or structural frame within the batch incur penalties of 0.5–1.5 points; matches against historical overuse patterns add further penalties; and two-word titles composed entirely of generic mood words score a 2.0 penalty. Titles exceeding a cumulative penalty of 3.0 are rejected and regenerated.&lt;br /&gt;
# &#039;&#039;&#039;Title memory.&#039;&#039;&#039; Accepted titles are appended to the rolling memory file (capped at 2,000 entries, oldest dropped first) and checked against the database to prevent exact duplicates.&lt;br /&gt;
# &#039;&#039;&#039;Database deduplication.&#039;&#039;&#039; Each final title is compared case-insensitively to every existing book in the database; duplicates are silently skipped.&lt;br /&gt;
&lt;br /&gt;
The output is written to &amp;lt;code&amp;gt;daily_books.json&amp;lt;/code&amp;gt;, a flat array of title/author/genre objects with generation timestamps.&lt;br /&gt;
&lt;br /&gt;
=== Hourly processing ===&lt;br /&gt;
&lt;br /&gt;
A companion script, &amp;lt;code&amp;gt;simple_hourly_processor.py&amp;lt;/code&amp;gt;, runs once per hour via cron. It selects one book from the daily JSON file — indexed by a hash of the current date and hour — and triggers its full generation (metadata, reviews, cover, author photo) by calling the application&#039;s &amp;lt;code&amp;gt;/cron/hourly_book_full&amp;lt;/code&amp;gt; endpoint. If all twelve daily seeds have already been processed, the hour is skipped. In production, a typical crontab runs the batch generator daily at midnight and the hourly processor every other hour, yielding up to twelve new books per day.&lt;br /&gt;
&lt;br /&gt;
== Cron security ==&lt;br /&gt;
&lt;br /&gt;
Cron endpoints are protected by a localhost check: the request&#039;s client IP must be &amp;lt;code&amp;gt;127.0.0.1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;::1&amp;lt;/code&amp;gt;, or &amp;lt;code&amp;gt;localhost&amp;lt;/code&amp;gt;. For deployments where the cron job calls the public URL rather than the loopback address, two overrides are available: an &amp;lt;code&amp;gt;X-API-Key&amp;lt;/code&amp;gt; header (or &amp;lt;code&amp;gt;api_key&amp;lt;/code&amp;gt; query parameter) checked against an environment variable, or a global &amp;lt;code&amp;gt;DISABLE_CRON_LOCALHOST_CHECK&amp;lt;/code&amp;gt; flag.&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Home page ===&lt;br /&gt;
&lt;br /&gt;
The home page displays a hero section with the title &amp;quot;Possible Books,&amp;quot; a rotating tagline (changed every thirty minutes, seeded by the current UTC half-hour), and a generation form with an autocomplete datalist of existing authors. Below the form, a grid of the twelve most recent books (with cover images or gradient fallbacks) appears alongside the twenty-five most recent reviews, sorted by review date.&lt;br /&gt;
&lt;br /&gt;
=== Book pages ===&lt;br /&gt;
&lt;br /&gt;
Each book page shows the cover image (or fallback), title, author byline linked to the author&#039;s page, genre tag, page count, publication date, and average star rating. The synopsis and author biography are rendered from Markdown. Below, reviews are listed with reviewer name, star display (filled and hollow star characters), review date, and Markdown-formatted review text. Administrators see additional controls: edit book, regenerate cover, delete book, regenerate reviews, and recalculate rating.&lt;br /&gt;
&lt;br /&gt;
=== Author pages ===&lt;br /&gt;
&lt;br /&gt;
The author page displays the portrait (or a CSS initials placeholder), the full Markdown biography, and a grid of the author&#039;s books. The authors index lists all authors sorted alphabetically by surname, with a plaintext preview of each biography (Markdown rendered, tags stripped, truncated to 100 characters).&lt;br /&gt;
&lt;br /&gt;
=== Archive ===&lt;br /&gt;
&lt;br /&gt;
The archive page presents a paginated, filterable grid of all books. A sidebar offers filters by publication-year range (dynamically bucketed), minimum average rating (one to five stars), and genre (checkboxes, grouped by popularity). Filters are applied via form auto-submission on change; an explicit &amp;quot;clear filters&amp;quot; link resets the view.&lt;br /&gt;
&lt;br /&gt;
=== Genres ===&lt;br /&gt;
&lt;br /&gt;
The genres page lists all active genres alphabetically with book counts.&lt;br /&gt;
&lt;br /&gt;
=== Theme ===&lt;br /&gt;
&lt;br /&gt;
The site defaults to a dark colour scheme (&amp;lt;code&amp;gt;#111315&amp;lt;/code&amp;gt; background, &amp;lt;code&amp;gt;#e6e6e6&amp;lt;/code&amp;gt; text) and supports a light mode toggled via a moon-icon button in the masthead, persisted in &amp;lt;code&amp;gt;localStorage&amp;lt;/code&amp;gt;. The toggle respects the operating system&#039;s &amp;lt;code&amp;gt;prefers-color-scheme&amp;lt;/code&amp;gt; setting as a default. Typography uses Roboto Serif for display headings and Roboto for body text and UI elements.&lt;br /&gt;
&lt;br /&gt;
== Progressive web application ==&lt;br /&gt;
&lt;br /&gt;
The site ships a Web App Manifest declaring standalone display mode, dark background and theme colours, and maskable icons at 192 and 512 pixels. A service worker registers at the root scope but operates in passthrough mode — all fetch events are returned unmodified — to satisfy PWA installability requirements without risking stale cached pages in a database-backed application that publishes new content hourly.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[Procedural generation]]&lt;br /&gt;
* [[Flask (web framework)]]&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Quidlibet&amp;diff=399</id>
		<title>Quidlibet</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Quidlibet&amp;diff=399"/>
		<updated>2026-04-13T13:15:04Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Created page with &amp;quot;{{Infobox | 01_name         = Quidlibet | 02_url          = https://quidlibet.yusupov.cloud | 03_developer    = Michel Vuijlsteke | 04_released     = 2025 | 05_genre        = AI-generated fictional book library | 06_language     = Python | 07_framework    = Flask 3.0 | 08_license      = Proprietary }}  &amp;#039;&amp;#039;&amp;#039;Quidlibet&amp;#039;&amp;#039;&amp;#039; (Latin for &amp;quot;anything whatsoever&amp;quot;) is a web application hosted at &amp;lt;code&amp;gt;quidlibet.yusupov.cloud&amp;lt;/code&amp;gt; that generates and catalogues entirely fictional...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| 01_name         = Quidlibet&lt;br /&gt;
| 02_url          = https://quidlibet.yusupov.cloud&lt;br /&gt;
| 03_developer    = Michel Vuijlsteke&lt;br /&gt;
| 04_released     = 2025&lt;br /&gt;
| 05_genre        = AI-generated fictional book library&lt;br /&gt;
| 06_language     = Python&lt;br /&gt;
| 07_framework    = [[Flask]] 3.0&lt;br /&gt;
| 08_license      = Proprietary&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Quidlibet&#039;&#039;&#039; (Latin for &amp;quot;anything whatsoever&amp;quot;) is a web application hosted at &amp;lt;code&amp;gt;quidlibet.yusupov.cloud&amp;lt;/code&amp;gt; that generates and catalogues entirely fictional books. Each book is a complete literary artefact: a plausible title, a named author with a biographical sketch and portrait, a Markdown-formatted synopsis, an AI-generated cover in genre-appropriate style, a publication date and page count, and between four and eight reader reviews with individually calibrated star ratings. The site presents itself as a browsable library catalogue under the heading &amp;quot;Possible Books,&amp;quot; inviting visitors to enter a title — and optionally an author and genre — for &amp;quot;a book that you&#039;d like to read but that doesn&#039;t actually exist.&amp;quot; In addition to on-demand generation, an automated pipeline publishes up to twelve new books per day on a cron schedule.&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Flask 3.0 with Flask-SQLAlchemy as its ORM and Flask-Login for authentication.&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt in the project repository lists Flask 3.0.3, Flask-SQLAlchemy 3.1.1, Flask-Login 0.6.3, and Flask-Caching 2.3.0.&amp;lt;/ref&amp;gt; It uses [[SQLite]] as its database and is deployed behind [[Nginx]] on a Linux VPS. Additional dependencies include [[Pillow (imaging library)|Pillow]] for image processing, the Python &amp;lt;code&amp;gt;markdown&amp;lt;/code&amp;gt; library for rich-text rendering, [[Beautiful Soup (HTML parser)|Beautiful Soup]] for web scraping, &amp;lt;code&amp;gt;python-dotenv&amp;lt;/code&amp;gt; for configuration, and the [[OpenAI]] Python client for all language-model and image-generation calls. The front end uses Bootstrap 5.3 with a custom CSS layer, Google Fonts (Roboto Serif, Roboto Slab, Roboto), and Bootstrap Icons. The site is installable as a [[progressive web application]] via a Web App Manifest and a minimal service worker.&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
=== Books ===&lt;br /&gt;
&lt;br /&gt;
Each book record carries a &#039;&#039;title&#039;&#039;, a foreign key to an &#039;&#039;Author&#039;&#039;, a foreign key to a &#039;&#039;Genre&#039;&#039; (with a legacy genre-name string for backward compatibility), a Markdown &#039;&#039;synopsis&#039;&#039;, a &#039;&#039;num_pages&#039;&#039; integer, an ISO 8601 &#039;&#039;publication_date&#039;&#039;, a &#039;&#039;cover_image&#039;&#039; path, an &#039;&#039;avg_rating&#039;&#039; and &#039;&#039;rating_count&#039;&#039; (derived from reviews), and the &#039;&#039;ip_address&#039;&#039; and timestamp of the generation request. Books are addressed by URL slug in the form &amp;lt;code&amp;gt;/book/{id}-{title-slug}&amp;lt;/code&amp;gt;, where the leading integer guarantees uniqueness and the slug provides readability.&lt;br /&gt;
&lt;br /&gt;
=== Authors ===&lt;br /&gt;
&lt;br /&gt;
Authors are stored with a &#039;&#039;name&#039;&#039; in &amp;quot;Lastname, Firstname&amp;quot; format (rendered as &amp;quot;Firstname Lastname&amp;quot; in the interface via a Jinja display-name filter), a Markdown-formatted &#039;&#039;bio&#039;&#039;, and an optional &#039;&#039;author_image&#039;&#039; path. Author names are matched case-insensitively using Python&#039;s &amp;lt;code&amp;gt;SequenceMatcher&amp;lt;/code&amp;gt; with a similarity threshold, so that minor variations do not create duplicate records.&lt;br /&gt;
&lt;br /&gt;
=== Genres ===&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;Genre&#039;&#039; model supports hierarchical categorisation via a self-referential &#039;&#039;parent_id&#039;&#039; foreign key. Each genre has a &#039;&#039;name&#039;&#039;, an optional &#039;&#039;description&#039;&#039;, a hex &#039;&#039;color&#039;&#039; code for UI tags, an &#039;&#039;icon&#039;&#039; class, a &#039;&#039;display_order&#039;&#039;, and an &#039;&#039;is_active&#039;&#039; flag.&lt;br /&gt;
&lt;br /&gt;
=== Reviews ===&lt;br /&gt;
&lt;br /&gt;
Each review belongs to a book and carries a &#039;&#039;reviewer_name&#039;&#039;, a &#039;&#039;review_date&#039;&#039; (in-universe), Markdown &#039;&#039;review_text&#039;&#039;, and a &#039;&#039;stars&#039;&#039; rating between one and five. Reviews are cascade-deleted when their parent book is removed.&lt;br /&gt;
&lt;br /&gt;
=== Supporting models ===&lt;br /&gt;
&lt;br /&gt;
A &#039;&#039;GenerationJob&#039;&#039; tracks the asynchronous lifecycle of each generation request (pending → processing → completed or failed), with a &#039;&#039;progress_message&#039;&#039; field polled by the front end. A &#039;&#039;GenerationLog&#039;&#039; provides an audit trail of every generation attempt — successful or otherwise — including the model used, the API surface called, and any error message. A &#039;&#039;GenerationQueueItem&#039;&#039; implements a database-backed seed queue as an alternative to the file-based daily queue.&lt;br /&gt;
&lt;br /&gt;
== Book generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Book generation is performed in the main Flask application via a multi-step pipeline of OpenAI API calls. The default text model is GPT-5; the image model is &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Title and metadata ===&lt;br /&gt;
&lt;br /&gt;
When a visitor submits a title (and optionally an author and genre), the application creates a &#039;&#039;GenerationJob&#039;&#039; and redirects to a status page that polls for progress. On the status page, while the visitor waits, a rotating display of faux-library-science progress messages — &amp;quot;Reticulating splines…,&amp;quot; &amp;quot;Reconciling author names against authority files…,&amp;quot; &amp;quot;De-duplicating near-identical editions and printings…,&amp;quot; &amp;quot;Calibrating star ratings to review sentiment…&amp;quot; — plays at random intervals for entertainment. Behind the scenes, the pipeline sends a structured prompt to the text model asking it to return a JSON object with the book&#039;s synopsis, page count, publication date, author name (invented if not supplied), and an author biography. The prompt instructs the model to write the synopsis in Markdown with bold and italic formatting, to keep it under two paragraphs of 120 words each, and to anchor it with concrete names, places, and objects rather than vague abstractions.&lt;br /&gt;
&lt;br /&gt;
=== Author reuse and continuity ===&lt;br /&gt;
&lt;br /&gt;
Before generating a new author, the pipeline queries the database for existing authors whose names are at least 80% similar (by sequence-matching across four name-format permutations). If a match is found — which happens for roughly a quarter of all generated books — the pipeline reuses the existing author record. It supplies the model with a context dossier listing the author&#039;s existing books, genres, and publication dates, and instructs it to revise the biography so that it naturally covers the author&#039;s expanded body of work. A hard constraint requires that any new publication date differ from the author&#039;s existing dates by at least six months. Biographies are post-processed by a helper that detects and corrects any instance of the &amp;quot;Lastname, Firstname&amp;quot; storage format that may have leaked into the prose.&lt;br /&gt;
&lt;br /&gt;
=== Synopses and formatting ===&lt;br /&gt;
&lt;br /&gt;
Author biographies are written in Markdown: the author&#039;s name is &#039;&#039;&#039;bold&#039;&#039;&#039; on first mention and for notable awards, book titles are &#039;&#039;italicised&#039;&#039;, and paragraphs are separated by blank lines. Synopses follow similar conventions. All Markdown content is rendered through a Jinja filter backed by the Python &amp;lt;code&amp;gt;markdown&amp;lt;/code&amp;gt; library (with extensions for line breaks, fenced code, and tables). On grid pages where biographies appear as truncated previews, the Markdown is first rendered to HTML, then stripped of tags, so that formatting tokens do not leak into the plaintext snippet.&lt;br /&gt;
&lt;br /&gt;
=== Review generation and rating profiles ===&lt;br /&gt;
&lt;br /&gt;
Reviews are generated in a separate API call. Before prompting, the pipeline constructs a deterministic &#039;&#039;rating profile&#039;&#039; by seeding a random number generator with the SHA-256 hash of the book&#039;s title, synopsis, and publication date. This seed selects one of four rating clusters — low (18% probability, mean 2.2–3.0), mid (34%, mean 3.2–3.9), high (26%, mean 4.0–4.6), or polarised (22%, mean 3.0–3.8 with high spread) — and derives a target mean, standard deviation, skew direction, and a rant count (zero to two extended reviews of either one–two or five stars).&lt;br /&gt;
&lt;br /&gt;
The model is asked to produce four to eight reviews, each assigned a distinct &#039;&#039;stylistic palette&#039;&#039; from a rotating set of eight: crisp capsule (one or two punchy sentences), craft critique (prose, structure, pacing), character study (interiority, dialogue, motives), worldbuilding lens (setting, atmosphere, rules), theme tracer (motifs and subtext), comparative take (comparisons to two non-celebrity authors), sceptic&#039;s ledger (bullet-point pros and cons), and librarian angle (audience notes). Each review also rotates through a primary focus — characters, plot, prose, world, themes, or audience — so that no two reviews in a set read the same way. Review dates must fall between the book&#039;s publication date and the present day. The prompt bans stock phrases (&amp;quot;page-turner,&amp;quot; &amp;quot;unputdownable&amp;quot;) and requires diverse, plausible reviewer names.&lt;br /&gt;
&lt;br /&gt;
After creation, the pipeline calculates the book&#039;s average rating and review count directly from the database and stores them on the book record.&lt;br /&gt;
&lt;br /&gt;
=== Cover generation ===&lt;br /&gt;
&lt;br /&gt;
Cover images are generated via OpenAI&#039;s &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt; model at 1024 × 1536 pixels (2:3 portrait aspect). The prompt is tailored to the book&#039;s genre through a weighted style-selection system: cookbooks receive an eightfold bias toward photographic covers; graphic novels toward vector/graphic styles; science fiction, fantasy, and horror toward illustration; biography and history toward photography; and romance toward an even mix. The prompt specifies the exact title and author name as cover typography and bans watermarks. Generated images are decoded from base64, converted to optimised progressive JPEG via Pillow (quality 92), and saved as &amp;lt;code&amp;gt;book_{id}.jpg&amp;lt;/code&amp;gt;. The pipeline allows two attempts; if both fail, the entire book-generation transaction is rolled back so that no coverless book can enter the database.&lt;br /&gt;
&lt;br /&gt;
=== Author-photo generation ===&lt;br /&gt;
&lt;br /&gt;
After a successful cover, the pipeline checks whether the author already has a portrait. If not, it sends a prompt to &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt; for a photorealistic head-and-shoulders portrait at 1024 × 1024 pixels. The result is centre-cropped to a 512 × 512 square JPEG and saved as &amp;lt;code&amp;gt;author_{id}.jpg&amp;lt;/code&amp;gt;. Unlike cover generation, photo generation is non-blocking: failures are logged but do not abort the job. In the interface, authors without photos receive a CSS-generated avatar showing their initials.&lt;br /&gt;
&lt;br /&gt;
=== Error handling and transaction safety ===&lt;br /&gt;
&lt;br /&gt;
If any exception occurs during book creation, review insertion, or image generation, the pipeline issues an immediate &amp;lt;code&amp;gt;db.session.rollback()&amp;lt;/code&amp;gt; to discard all uncommitted data. The &#039;&#039;GenerationJob&#039;&#039; and &#039;&#039;GenerationLog&#039;&#039; records — committed in an earlier transaction — survive, so the failure is auditable. The job is marked as failed with a descriptive error message. In templates, all cover &amp;lt;code&amp;gt;&amp;amp;lt;img&amp;amp;gt;&amp;lt;/code&amp;gt; tags include an &amp;lt;code&amp;gt;onerror&amp;lt;/code&amp;gt; handler that replaces a broken image with a CSS gradient placeholder bearing the book&#039;s title, as a defence against stale database references to missing files.&lt;br /&gt;
&lt;br /&gt;
== Daily batch generation ==&lt;br /&gt;
&lt;br /&gt;
A standalone script, &amp;lt;code&amp;gt;fast_generator.py&amp;lt;/code&amp;gt;, generates twelve book seeds (title, author, genre) in a single GPT-5 API call lasting roughly twelve seconds. The batch pipeline incorporates several layers of anti-repetition machinery.&lt;br /&gt;
&lt;br /&gt;
=== Goodreads title inspiration ===&lt;br /&gt;
&lt;br /&gt;
Before prompting, the script scrapes two to three randomly chosen genre pages on [[Goodreads]] (from a pool of eight: historical fiction, history, fantasy, science fiction, romance, cookbooks, general fiction, and horror). It extracts book titles from &amp;lt;code&amp;gt;div.bookBox img[alt]&amp;lt;/code&amp;gt; elements, cleans them of series numbers, author suffixes, and emoji, and presents a sample as &amp;quot;inspiration titles — for tone and flavour only, not to be reproduced.&amp;quot; If Goodreads is unreachable, the script falls back to a curated JSON file of titles harvested from the [[Internet Archive]] and [[Open Library]].&lt;br /&gt;
&lt;br /&gt;
=== Anti-sameness system ===&lt;br /&gt;
&lt;br /&gt;
To prevent the model from gravitating toward a narrow band of title shapes, the script enforces structural diversity through five mechanisms:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Title-class rotation.&#039;&#039;&#039; Ten structural classes — concrete object, place or institution, named person, event or incident, documentary phrase, odd juxtaposition, fragment or question, subtitle-led, idiomatic phrase, and one-word punch — are allocated evenly across the batch so that each slot has a designated shape.&lt;br /&gt;
# &#039;&#039;&#039;Avoidance guidance.&#039;&#039;&#039; The script analyses a rolling memory of up to 2,000 previously generated titles (persisted in a JSON file) plus all titles already in the database. It identifies the twenty most overused content words, the most common title frames (e.g., &amp;quot;The Last ___,&amp;quot; &amp;quot;Beyond the ___&amp;quot;), the most frequent opening words, and the most frequent closing nouns, and encodes these as explicit &amp;quot;do not use&amp;quot; directives in the prompt.&lt;br /&gt;
# &#039;&#039;&#039;Batch validator.&#039;&#039;&#039; After generation, each title in the batch is scored for repetitiveness: collisions on first word, end noun, or structural frame within the batch incur penalties of 0.5–1.5 points; matches against historical overuse patterns add further penalties; and two-word titles composed entirely of generic mood words score a 2.0 penalty. Titles exceeding a cumulative penalty of 3.0 are rejected and regenerated.&lt;br /&gt;
# &#039;&#039;&#039;Title memory.&#039;&#039;&#039; Accepted titles are appended to the rolling memory file (capped at 2,000 entries, oldest dropped first) and checked against the database to prevent exact duplicates.&lt;br /&gt;
# &#039;&#039;&#039;Database deduplication.&#039;&#039;&#039; Each final title is compared case-insensitively to every existing book in the database; duplicates are silently skipped.&lt;br /&gt;
&lt;br /&gt;
The output is written to &amp;lt;code&amp;gt;daily_books.json&amp;lt;/code&amp;gt;, a flat array of title/author/genre objects with generation timestamps.&lt;br /&gt;
&lt;br /&gt;
=== Hourly processing ===&lt;br /&gt;
&lt;br /&gt;
A companion script, &amp;lt;code&amp;gt;simple_hourly_processor.py&amp;lt;/code&amp;gt;, runs once per hour via cron. It selects one book from the daily JSON file — indexed by a hash of the current date and hour — and triggers its full generation (metadata, reviews, cover, author photo) by calling the application&#039;s &amp;lt;code&amp;gt;/cron/hourly_book_full&amp;lt;/code&amp;gt; endpoint. If all twelve daily seeds have already been processed, the hour is skipped. In production, a typical crontab runs the batch generator daily at midnight and the hourly processor every other hour, yielding up to twelve new books per day.&lt;br /&gt;
&lt;br /&gt;
== Cron security ==&lt;br /&gt;
&lt;br /&gt;
Cron endpoints are protected by a localhost check: the request&#039;s client IP must be &amp;lt;code&amp;gt;127.0.0.1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;::1&amp;lt;/code&amp;gt;, or &amp;lt;code&amp;gt;localhost&amp;lt;/code&amp;gt;. For deployments where the cron job calls the public URL rather than the loopback address, two overrides are available: an &amp;lt;code&amp;gt;X-API-Key&amp;lt;/code&amp;gt; header (or &amp;lt;code&amp;gt;api_key&amp;lt;/code&amp;gt; query parameter) checked against an environment variable, or a global &amp;lt;code&amp;gt;DISABLE_CRON_LOCALHOST_CHECK&amp;lt;/code&amp;gt; flag.&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Home page ===&lt;br /&gt;
&lt;br /&gt;
The home page displays a hero section with the title &amp;quot;Possible Books,&amp;quot; a rotating tagline (changed every thirty minutes, seeded by the current UTC half-hour), and a generation form with an autocomplete datalist of existing authors. Below the form, a grid of the twelve most recent books (with cover images or gradient fallbacks) appears alongside the twenty-five most recent reviews, sorted by review date.&lt;br /&gt;
&lt;br /&gt;
=== Book pages ===&lt;br /&gt;
&lt;br /&gt;
Each book page shows the cover image (or fallback), title, author byline linked to the author&#039;s page, genre tag, page count, publication date, and average star rating. The synopsis and author biography are rendered from Markdown. Below, reviews are listed with reviewer name, star display (filled and hollow star characters), review date, and Markdown-formatted review text. Administrators see additional controls: edit book, regenerate cover, delete book, regenerate reviews, and recalculate rating.&lt;br /&gt;
&lt;br /&gt;
=== Author pages ===&lt;br /&gt;
&lt;br /&gt;
The author page displays the portrait (or a CSS initials placeholder), the full Markdown biography, and a grid of the author&#039;s books. The authors index lists all authors sorted alphabetically by surname, with a plaintext preview of each biography (Markdown rendered, tags stripped, truncated to 100 characters).&lt;br /&gt;
&lt;br /&gt;
=== Archive ===&lt;br /&gt;
&lt;br /&gt;
The archive page presents a paginated, filterable grid of all books. A sidebar offers filters by publication-year range (dynamically bucketed), minimum average rating (one to five stars), and genre (checkboxes, grouped by popularity). Filters are applied via form auto-submission on change; an explicit &amp;quot;clear filters&amp;quot; link resets the view.&lt;br /&gt;
&lt;br /&gt;
=== Genres ===&lt;br /&gt;
&lt;br /&gt;
The genres page lists all active genres alphabetically with book counts.&lt;br /&gt;
&lt;br /&gt;
=== Theme ===&lt;br /&gt;
&lt;br /&gt;
The site defaults to a dark colour scheme (&amp;lt;code&amp;gt;#111315&amp;lt;/code&amp;gt; background, &amp;lt;code&amp;gt;#e6e6e6&amp;lt;/code&amp;gt; text) and supports a light mode toggled via a moon-icon button in the masthead, persisted in &amp;lt;code&amp;gt;localStorage&amp;lt;/code&amp;gt;. The toggle respects the operating system&#039;s &amp;lt;code&amp;gt;prefers-color-scheme&amp;lt;/code&amp;gt; setting as a default. Typography uses Roboto Serif for display headings and Roboto for body text and UI elements.&lt;br /&gt;
&lt;br /&gt;
== Progressive web application ==&lt;br /&gt;
&lt;br /&gt;
The site ships a Web App Manifest declaring standalone display mode, dark background and theme colours, and maskable icons at 192 and 512 pixels. A service worker registers at the root scope but operates in passthrough mode — all fetch events are returned unmodified — to satisfy PWA installability requirements without risking stale cached pages in a database-backed application that publishes new content hourly.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[Procedural generation]]&lt;br /&gt;
* [[Flask (web framework)]]&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=398</id>
		<title>Yusupov.cloud</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=398"/>
		<updated>2026-04-13T13:05:02Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Subdomains and projects */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name           = yusupov.cloud&lt;br /&gt;
| 1 url            = https://yusupov.cloud&lt;br /&gt;
| 2 type           = Personal web sites&lt;br /&gt;
| 3 owner          = [[Michel Vuijlsteke]]&lt;br /&gt;
| 4 launched       = 2025&lt;br /&gt;
| 5 current_status = Online&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;yusupov.cloud&#039;&#039;&#039; is a personal domain and virtual private server operated by Belgian technologist [[Michel Vuijlsteke]]. It hosts multiple small web applications on subdomains and at the apex domain. One of these is a MediaWiki installation titled “Yusupov’s House.” The setup is presented as a web-era continuation of the do-it-yourself ethos of Vuijlsteke’s 1990s BBS of the same name.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot;&amp;gt;“Yusupov’s House,” &#039;&#039;yusupov.cloud&#039;&#039; (wiki), accessed 10 October 2025, https://yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
Among the projects on the domain is a MediaWiki (at the apex, &#039;&#039;yusupov.cloud&#039;&#039;) running MediaWiki 1.44.0 with PHP 8.3.6 (FPM) and SQLite, using the Vector skin and core extensions for citations and template scripting.&amp;lt;ref name=&amp;quot;version&amp;quot;&amp;gt;‘‘Special:Version’’ page, &#039;&#039;yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://yusupov.cloud/wiki/Special:Version&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Subdomains and projects ==&lt;br /&gt;
Publicly visible projects include:&lt;br /&gt;
&lt;br /&gt;
* [https://acbc.yusupov.cloud acbc.yusupov.cloud] — &#039;&#039;A Cabinet of Brief Curiosities&#039;&#039;, generating tiny three-sentence surreal/horror micro-stories with an hourly cadence and an archive. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;acbc-home&amp;quot;&amp;gt;A Cabinet of Brief Curiosities (home), &#039;&#039;acbc.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://acbc.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://agenda.yusupov.cloud agenda.yusupov.cloud] — &#039;&#039;A Life in Planners&#039;&#039;, a structured journal chronicling the final years of the operator’s mother, with calendar, food, medications, measurements, and statistics views (multilingual UI).&amp;lt;ref name=&amp;quot;agenda&amp;quot;&amp;gt;“A life in planners,” &#039;&#039;agenda.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://agenda.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://cloud.yusupov.cloud cloud.yusupov.cloud] — a series of static html creative coding experiments, simulations, and games including: timebeat, fire and snake simulations, biomass metaballs, cs3, &#039;&#039;Cross&#039;&#039; crossword puzzle game, image dithering tool, books, &#039;&#039;Elite Galaxy Explorer&#039;&#039;, ZX Spectrum loading screen simulator, Carcassonne, 3D boids flocking algorithm, physarum slime mold simulation, temps temperature visualization, &#039;&#039;The Chronicle of Hamurabi&#039;&#039; ancient Sumeria resource management game, and gatekeeper.&amp;lt;ref name=&amp;quot;cloud-home&amp;quot;&amp;gt;&amp;quot;cloud,&amp;quot; &#039;&#039;cloud.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://cloud.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://digest.yusupov.cloud digest.yusupov.cloud] — &#039;&#039;Digest&#039;&#039;, daily seasonal AI-assisted recipes inspired by current events, browsable by meal type and ingredients.&amp;lt;ref name=&amp;quot;digest-home&amp;quot;&amp;gt;“Digest — Daily recipes inspired by the news,” &#039;&#039;digest.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://digest.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://echoes.yusupov.cloud echoes.yusupov.cloud] — &#039;&#039;[[Echoes of What Wasn&#039;t]]&#039;&#039;, an AI-generated alternate-history newspaper presenting richly detailed articles about historical events as if they had unfolded differently. A pipeline scrapes real events from multilingual Wikipedia, uses OpenAI to craft a divergent narrative with period-appropriate prose and DALL-E imagery, and publishes via a REST API. Features article browsing by month, a &amp;quot;Where/When&amp;quot; interactive map-and-timeline view using Leaflet, and a picture desk. (Built with Wagtail 7/Django 5 per operator.)&amp;lt;ref name=&amp;quot;echoes-home&amp;quot;&amp;gt;&amp;quot;Echoes — Dispatches from Histories That Never Were,&amp;quot; &#039;&#039;echoes.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://echoes.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://quidlibet.yusupov.cloud quidlibet.yusupov.cloud] — &#039;&#039;[[Quidlibet]]&#039;&#039;, an app that generates fictional books complete with synopsis, author bio, and faux reviews; includes genre and author archives. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;quidlibet-home&amp;quot;&amp;gt;“Quidlibet — Book Generator,” &#039;&#039;quidlibet.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://quidlibet.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://tyov-web.yusupov.cloud tyov-web.yusupov.cloud] — a web implementation of the solo RPG &#039;&#039;[[Thousand Year Old Vampire]]&#039;&#039;, with Django 5 backend and Vue 3 frontend. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
Operator-reported (not publicly discoverable at time of writing):&lt;br /&gt;
&lt;br /&gt;
* skills.yusupov.cloud — a skills matrix application. (Per operator.)&lt;br /&gt;
* resources.yusupov.cloud — a simple resource plannign calendar. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
== Technology ==&lt;br /&gt;
The wiki stack is documented on &#039;&#039;Special:Version&#039;&#039;. Individual apps are described by the operator as Flask (&#039;&#039;acbc&#039;&#039;, &#039;&#039;quidlibet&#039;&#039;) and Django 5 + Vue 3 (&#039;&#039;tyov-web&#039;&#039;).&amp;lt;ref name=&amp;quot;version&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Relation to the BBS ==&lt;br /&gt;
The project name references Vuijlsteke’s single-line BBS (FidoNet 2:291/1925) active between 1990 and 1995. While the VPS is not a BBS, its single-admin, self-maintained hosting reprises the early DIY approach.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;nodehist&amp;quot;&amp;gt;“Nodelist history search: History of node 2:291/1925,” NodeHist, accessed 10 October 2025, https://nodehist.fidonet.org.ua/?address=2%3A291%2F1925&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Yusupov&#039;s House]] (1990s BBS)&lt;br /&gt;
* [[Michel Vuijlsteke]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Personal websites]]&lt;br /&gt;
[[Category:Belgian websites]]&lt;br /&gt;
[[Category:2025 establishments in Belgium]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=397</id>
		<title>Yusupov.cloud</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=397"/>
		<updated>2026-04-13T09:30:34Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Subdomains and projects */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name           = yusupov.cloud&lt;br /&gt;
| 1 url            = https://yusupov.cloud&lt;br /&gt;
| 2 type           = Personal web sites&lt;br /&gt;
| 3 owner          = [[Michel Vuijlsteke]]&lt;br /&gt;
| 4 launched       = 2025&lt;br /&gt;
| 5 current_status = Online&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;yusupov.cloud&#039;&#039;&#039; is a personal domain and virtual private server operated by Belgian technologist [[Michel Vuijlsteke]]. It hosts multiple small web applications on subdomains and at the apex domain. One of these is a MediaWiki installation titled “Yusupov’s House.” The setup is presented as a web-era continuation of the do-it-yourself ethos of Vuijlsteke’s 1990s BBS of the same name.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot;&amp;gt;“Yusupov’s House,” &#039;&#039;yusupov.cloud&#039;&#039; (wiki), accessed 10 October 2025, https://yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
Among the projects on the domain is a MediaWiki (at the apex, &#039;&#039;yusupov.cloud&#039;&#039;) running MediaWiki 1.44.0 with PHP 8.3.6 (FPM) and SQLite, using the Vector skin and core extensions for citations and template scripting.&amp;lt;ref name=&amp;quot;version&amp;quot;&amp;gt;‘‘Special:Version’’ page, &#039;&#039;yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://yusupov.cloud/wiki/Special:Version&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Subdomains and projects ==&lt;br /&gt;
Publicly visible projects include:&lt;br /&gt;
&lt;br /&gt;
* [https://acbc.yusupov.cloud acbc.yusupov.cloud] — &#039;&#039;A Cabinet of Brief Curiosities&#039;&#039;, generating tiny three-sentence surreal/horror micro-stories with an hourly cadence and an archive. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;acbc-home&amp;quot;&amp;gt;A Cabinet of Brief Curiosities (home), &#039;&#039;acbc.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://acbc.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://agenda.yusupov.cloud agenda.yusupov.cloud] — &#039;&#039;A Life in Planners&#039;&#039;, a structured journal chronicling the final years of the operator’s mother, with calendar, food, medications, measurements, and statistics views (multilingual UI).&amp;lt;ref name=&amp;quot;agenda&amp;quot;&amp;gt;“A life in planners,” &#039;&#039;agenda.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://agenda.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://cloud.yusupov.cloud cloud.yusupov.cloud] — a series of static html creative coding experiments, simulations, and games including: timebeat, fire and snake simulations, biomass metaballs, cs3, &#039;&#039;Cross&#039;&#039; crossword puzzle game, image dithering tool, books, &#039;&#039;Elite Galaxy Explorer&#039;&#039;, ZX Spectrum loading screen simulator, Carcassonne, 3D boids flocking algorithm, physarum slime mold simulation, temps temperature visualization, &#039;&#039;The Chronicle of Hamurabi&#039;&#039; ancient Sumeria resource management game, and gatekeeper.&amp;lt;ref name=&amp;quot;cloud-home&amp;quot;&amp;gt;&amp;quot;cloud,&amp;quot; &#039;&#039;cloud.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://cloud.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://digest.yusupov.cloud digest.yusupov.cloud] — &#039;&#039;Digest&#039;&#039;, daily seasonal AI-assisted recipes inspired by current events, browsable by meal type and ingredients.&amp;lt;ref name=&amp;quot;digest-home&amp;quot;&amp;gt;“Digest — Daily recipes inspired by the news,” &#039;&#039;digest.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://digest.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://echoes.yusupov.cloud echoes.yusupov.cloud] — &#039;&#039;[[Echoes of What Wasn&#039;t]]&#039;&#039;, an AI-generated alternate-history newspaper presenting richly detailed articles about historical events as if they had unfolded differently. A pipeline scrapes real events from multilingual Wikipedia, uses OpenAI to craft a divergent narrative with period-appropriate prose and DALL-E imagery, and publishes via a REST API. Features article browsing by month, a &amp;quot;Where/When&amp;quot; interactive map-and-timeline view using Leaflet, and a picture desk. (Built with Wagtail 7/Django 5 per operator.)&amp;lt;ref name=&amp;quot;echoes-home&amp;quot;&amp;gt;&amp;quot;Echoes — Dispatches from Histories That Never Were,&amp;quot; &#039;&#039;echoes.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://echoes.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://quidlibet.yusupov.cloud quidlibet.yusupov.cloud] — &#039;&#039;Quidlibet&#039;&#039;, an app that generates fictional books complete with synopsis, author bio, and faux reviews; includes genre and author archives. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;quidlibet-home&amp;quot;&amp;gt;“Quidlibet — Book Generator,” &#039;&#039;quidlibet.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://quidlibet.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://tyov-web.yusupov.cloud tyov-web.yusupov.cloud] — a web implementation of the solo RPG &#039;&#039;[[Thousand Year Old Vampire]]&#039;&#039;, with Django 5 backend and Vue 3 frontend. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
Operator-reported (not publicly discoverable at time of writing):&lt;br /&gt;
&lt;br /&gt;
* skills.yusupov.cloud — a skills matrix application. (Per operator.)&lt;br /&gt;
* resources.yusupov.cloud — a simple resource plannign calendar. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
== Technology ==&lt;br /&gt;
The wiki stack is documented on &#039;&#039;Special:Version&#039;&#039;. Individual apps are described by the operator as Flask (&#039;&#039;acbc&#039;&#039;, &#039;&#039;quidlibet&#039;&#039;) and Django 5 + Vue 3 (&#039;&#039;tyov-web&#039;&#039;).&amp;lt;ref name=&amp;quot;version&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Relation to the BBS ==&lt;br /&gt;
The project name references Vuijlsteke’s single-line BBS (FidoNet 2:291/1925) active between 1990 and 1995. While the VPS is not a BBS, its single-admin, self-maintained hosting reprises the early DIY approach.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;nodehist&amp;quot;&amp;gt;“Nodelist history search: History of node 2:291/1925,” NodeHist, accessed 10 October 2025, https://nodehist.fidonet.org.ua/?address=2%3A291%2F1925&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Yusupov&#039;s House]] (1990s BBS)&lt;br /&gt;
* [[Michel Vuijlsteke]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Personal websites]]&lt;br /&gt;
[[Category:Belgian websites]]&lt;br /&gt;
[[Category:2025 establishments in Belgium]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=File:Tyov-story.jpg&amp;diff=396</id>
		<title>File:Tyov-story.jpg</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=File:Tyov-story.jpg&amp;diff=396"/>
		<updated>2026-04-12T11:46:41Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=File:Tyov-prompt.jpg&amp;diff=395</id>
		<title>File:Tyov-prompt.jpg</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=File:Tyov-prompt.jpg&amp;diff=395"/>
		<updated>2026-04-12T11:45:53Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=File:Tyov-home.jpg&amp;diff=394</id>
		<title>File:Tyov-home.jpg</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=File:Tyov-home.jpg&amp;diff=394"/>
		<updated>2026-04-12T11:45:22Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=393</id>
		<title>Thousand Year Old Vampire</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=393"/>
		<updated>2026-04-12T11:45:01Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox &lt;br /&gt;
| 01_name = Thousand Year Old Vampire - Web Helper&lt;br /&gt;
| 02_developer = Michel Vuijlsteke&lt;br /&gt;
| 03_programming language = Python, TypeScript&lt;br /&gt;
| 04_operating system = Cross-platform (Web)&lt;br /&gt;
| 05_genre = Solo RPG digital companion&lt;br /&gt;
| 06_license = Custom&lt;br /&gt;
| 07_website = &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Thousand Year Old Vampire - Web Helper&#039;&#039;&#039; (&#039;&#039;&#039;TYOV-Web&#039;&#039;&#039;) is a browser-based adaptation of the solo role-playing game &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings. Rather than presenting the game as a digital board game or a rules spreadsheet, it treats a campaign as a long, accumulating chronicle: a vampire begins with a handful of memories, gains new experiences, loses old ones, and leaves behind a life story made of fragments, scars, names, and vanished centuries. The application is built as a &#039;&#039;&#039;Django&#039;&#039;&#039; backend serving a &#039;&#039;&#039;Vue&#039;&#039;&#039; single-page frontend, but its real achievement is literary rather than merely technical: it turns bookkeeping-heavy solo play into something that feels legible, mournful, and continuous.&lt;br /&gt;
&lt;br /&gt;
[[File:tyov-home.jpg|thumb|center|900px|The TYOV-Web home page, where players can browse existing chronicles, continue unfinished character creation, or begin a new vampire.]]&lt;br /&gt;
&lt;br /&gt;
== Premise ==&lt;br /&gt;
&lt;br /&gt;
In &#039;&#039;Thousand Year Old Vampire&#039;&#039;, the player does not manage hit points, armies, or territory. Instead, the player manages &#039;&#039;&#039;memory&#039;&#039;&#039;. A vampire may survive for centuries, but cannot perfectly retain those centuries. New experiences force older ones aside. Friends die. Languages decay. Identities are traded, abandoned, or forgotten. The drama comes from watching the self narrow and distort over time.&lt;br /&gt;
&lt;br /&gt;
TYOV-Web preserves that central idea. A player creates a vampire from mortal beginnings, plays through prompts generated by dice rolls, writes narrative responses to those prompts, and organises the resulting fragments into memories. Over time the character collects skills, resources, marks, and known companions, but those are unstable possessions. The most important question is never simply &#039;&#039;what happened?&#039;&#039; It is &#039;&#039;what can still be remembered?&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== How Play Works ==&lt;br /&gt;
&lt;br /&gt;
The application follows the structure of the tabletop game closely, but presents it as a guided story engine. Character creation unfolds in six stages: mortal life, mortal relationships, skills, resources, a defining early experience, and finally the turning into vampirism. Once play begins, each turn revolves around a prompt. The player reads the current prompt, writes at least one experience in response, manages any consequences on the character sheet, then rolls a ten-sided die and a six-sided die to discover the next destination in the prompt list.&lt;br /&gt;
&lt;br /&gt;
[[File:tyov-prompt.jpg|thumb|right|500px|A prompt page during play. The current prompt, experience form, memories, characters, and skills are visible at the same time, emphasizing both story writing and memory pressure.]]&lt;br /&gt;
&lt;br /&gt;
That movement is what gives the game its broken historical rhythm. A positive result on D10 minus D6 pushes the vampire forward; a negative result drags the story backward into earlier themes or alternative branches. Prompts are also divided into variations, so revisiting a number does not necessarily mean revisiting the same moment in the same way.&lt;br /&gt;
&lt;br /&gt;
The memory system gives the application its emotional pressure. A character can normally hold only &#039;&#039;&#039;five active memories&#039;&#039;&#039;, and each memory can contain no more than &#039;&#039;&#039;three experiences&#039;&#039;&#039;. When that limit is reached, the player must decide what to lose. Some memories can be moved into a &#039;&#039;&#039;diary&#039;&#039;&#039;, which acts as an external repository, but diaries themselves are only resources, and resources can be lost. The result is a game about narrative curation: the player is constantly deciding which version of the vampire&#039;s past deserves to remain visible.&lt;br /&gt;
&lt;br /&gt;
Special prompts can alter that rhythm. Some disable experience-writing for a turn. Others allow the vampire&#039;s name to change, permit the editing of old experiences, reduce or increase permanent memory capacity, or end the chronicle entirely with an epilogue.&lt;br /&gt;
&lt;br /&gt;
== An Example Unlife: Karmis ==&lt;br /&gt;
&lt;br /&gt;
The most compelling way to understand the application is through one of its stored characters. One campaign in the database follows &#039;&#039;&#039;Karmis&#039;&#039;&#039;, formerly &#039;&#039;&#039;Ekurzu&#039;&#039;&#039; and before that &#039;&#039;&#039;Naram&#039;&#039;&#039;, across roughly three and a half thousand years.&lt;br /&gt;
&lt;br /&gt;
[[File:tyov-story.jpg|thumb|right|500px|The story overview view presents a character&#039;s life as a readable timeline, combining narrative entries with a compact summary of memories, experiences, characters, skills, resources, and marks.]]&lt;br /&gt;
&lt;br /&gt;
He begins in Bronze Age Mari as a temple scribe: precise, observant, and deeply embedded in the religious and political machinery of the city. His mortal life is defined by clay tablets, whispered intelligence, and courtly danger. He falls in love with Hessa, is shaped by the mentorship of the high priest Ibbi-Zamri, and is eventually turned by the ancient vampire Ashurban the Veiled. In one of the campaign&#039;s defining cruelties, Ibbi-Zamri becomes the first person Karmis kills after the turning.&lt;br /&gt;
&lt;br /&gt;
The early centuries of his unlife are marked by exile and shame. He flees with Ennatum, struggles to master the hunger, tries to imagine restraint as redemption, and fails. One of the campaign&#039;s most effective lines comes from this period: he builds walls and plays flutes in the wilderness, but it is &amp;quot;never penance, only a cage with softer bars.&amp;quot; Eventually he devours Ennatum during a sandstorm and carries that loss into the long middle stretch of the story.&lt;br /&gt;
&lt;br /&gt;
From there the campaign moves through one civilisation after another. Karmis passes through Akhenaten&#039;s Egypt, the imperial roads of the Achaemenids, the scholarly world of Alexandria, Roman frontiers, monastic Europe, Mediterranean trade routes, and the violence of the early modern Atlantic world. His roles change less than the world around him does. He remains a scribe, witness, forger, archivist, and careful observer of institutions that never last as long as he does.&lt;br /&gt;
&lt;br /&gt;
The campaign also shows how the game&#039;s systems create character rather than merely recording it. Karmis gains skills such as &#039;&#039;Silent Cartography&#039;&#039;, the ability to trace unseen paths of influence, and &#039;&#039;Ash-Tongue&#039;&#039;, a talent for insinuation and social survival. But he loses others as time damages his identity. He once knew how to decipher ancient texts; later he no longer can. He once believed he could control the beast within; later even that confidence disappears. The mechanics translate immortality into erosion.&lt;br /&gt;
&lt;br /&gt;
One of the campaign&#039;s richest relationships is with &#039;&#039;&#039;Thooni&#039;&#039;&#039;, a scholar who recognises the name &#039;&#039;Ekurzu&#039;&#039; not as legend but as continuity. After her mortal death, the relationship persists in altered form: a spectral Thooni begins appearing in the margins of Karmis&#039;s diary, leaving fragments in languages he can no longer read. TYOV-Web handles this elegantly because its model of a character is not limited to a living cast list. People can be present, absent, lost, remembered, or transformed into symbols the protagonist can no longer fully interpret.&lt;br /&gt;
&lt;br /&gt;
By the late Roman and early medieval portions of the chronicle, the cost of the memory rules becomes brutal. Karmis loses so many memories that even the event of his own turning is no longer available to him. He relies on diaries and external records to stabilise himself, yet those too are vulnerable. A carved chamber called &#039;&#039;The Walls of Forgetting&#039;&#039; and a later monastery diary both disappear. The application does not treat this as simple inventory loss; it feels instead like a second death for pieces of the self.&lt;br /&gt;
&lt;br /&gt;
In the final act, Karmis witnesses the Atlantic slave trade and begins keeping a ledger of names drawn from shipping manifests and punishment records. A later response captures the moral exhaustion of the character with unusual sharpness: he no longer imagines himself purified by restraint, only redirected by purpose. He feeds on slavers and calls it justice, yet recognises that the blood tastes the same. This is the kind of line the application is built to preserve: not just event logs, but accumulations of ethical fatigue.&lt;br /&gt;
&lt;br /&gt;
His last great companion is &#039;&#039;&#039;Mirelde of Bracha&#039;&#039;&#039;, who becomes mirror, partner, and eventually historian. By the time the epilogue arrives, the story is no longer told in Karmis&#039;s own voice. Mirelde writes the closing judgment on him: not as a monster laid to rest, but as &amp;quot;just a man who remembered too much, for too long, and chose, in the end, peace.&amp;quot; That shift in narrator is exactly the sort of late-game turn TYOV-Web makes easy to sustain, because it keeps the campaign legible even after the protagonist himself has become unreliable.&lt;br /&gt;
&lt;br /&gt;
== Why the Application Works ==&lt;br /&gt;
&lt;br /&gt;
What makes TYOV-Web effective is not that it digitises every rule, though it largely does. Its strength is that it understands which parts of the original game are dramatically important and places those at the centre of the interface.&lt;br /&gt;
&lt;br /&gt;
The main play screen is organised around the prompt, the character sheet, and the memory display. The prompt is always the immediate literary provocation. The memory display constantly reminds the player of pressure and loss: active memories, diary memories, and forgotten memories sit in visible relation to one another. Skills, resources, marks, and known characters are editable in place, which means the character sheet grows and collapses alongside the fiction instead of feeling like a separate administrative surface.&lt;br /&gt;
&lt;br /&gt;
The result is a frontend that behaves less like a dashboard and more like a writing desk. Players move between the prompt text, the experience form, and the fragments of the vampire&#039;s surviving life. Even when the application is doing something purely mechanical, such as validating whether the player may continue, the effect is narrative: it is asking whether the current moment of the story has been fully accounted for.&lt;br /&gt;
&lt;br /&gt;
== The Frontend as Chronicle ==&lt;br /&gt;
&lt;br /&gt;
The client side of TYOV-Web is a &#039;&#039;&#039;Vue 3&#039;&#039;&#039; single-page application written in &#039;&#039;&#039;TypeScript&#039;&#039;&#039;. In practical terms, this means the entire experience unfolds without page reloads; in literary terms, it allows the game to feel continuous. The player logs in, sees a home screen of active and archived characters, enters a creation wizard for new vampires, and then spends most of the campaign inside a single evolving game view.&lt;br /&gt;
&lt;br /&gt;
That view is supported by a set of focused screens: a login page, a home dashboard, the six-step character creation flow, the main game interface, an ending screen for completed chronicles, an archive view, and a story view that presents the campaign as a timeline. Around those are a collection of smaller windows and dialogs for editing characters, skills, resources, memories, and experiences. These are important not because there are many of them, but because they preserve the sense that a campaign is always open to revision, annotation, and reinterpretation.&lt;br /&gt;
&lt;br /&gt;
State is held in Pinia stores for authentication, gameplay, character creation, and theme. That is an implementation detail, but in this case it serves a visible purpose: it keeps the player&#039;s character, current prompt, pending changes, and validation state coherent at all times. The interface never feels as if it has forgotten where the player is in the life of the vampire.&lt;br /&gt;
&lt;br /&gt;
== The Backend as Memory Keeper ==&lt;br /&gt;
&lt;br /&gt;
Behind the interface is a &#039;&#039;&#039;Django 5&#039;&#039;&#039; application with a REST API built on &#039;&#039;&#039;Django REST Framework&#039;&#039;&#039;. The backend models the campaign in the same terms the game itself cares about: &#039;&#039;&#039;Character&#039;&#039;&#039;, &#039;&#039;&#039;Memory&#039;&#039;&#039;, &#039;&#039;&#039;Experience&#039;&#039;&#039;, &#039;&#039;&#039;Skill&#039;&#039;&#039;, &#039;&#039;&#039;Resource&#039;&#039;&#039;, &#039;&#039;&#039;Mark&#039;&#039;&#039;, &#039;&#039;&#039;GameCharacter&#039;&#039;&#039;, &#039;&#039;&#039;Prompt&#039;&#039;&#039;, and &#039;&#039;&#039;PendingChange&#039;&#039;&#039;. This is not especially glamorous architecture, but it is disciplined. The data model is close to the language of play, which makes the whole system easier to reason about.&lt;br /&gt;
&lt;br /&gt;
The most interesting design choice is the use of &#039;&#039;&#039;pending changes&#039;&#039;&#039;. TYOV-Web does not immediately finalise every action the player takes. Instead, new experiences and character-sheet adjustments can accumulate as provisional edits until the player chooses to continue the story. Only then are they committed atomically. This makes the system feel less like a CRUD application and more like a drafting process. A turn can be assembled, reconsidered, and only then sealed into the chronicle.&lt;br /&gt;
&lt;br /&gt;
Authentication is handled by JWT tokens, and character data is scoped to the authenticated user. The backend exposes endpoints for the game state itself, dice rolling, story continuation, and CRUD operations on memories, experiences, skills, resources, marks, and known characters. In the article, however, the important point is not the shape of the URLs. It is that the backend has been designed to support a narrative workflow rather than a generic records system.&lt;br /&gt;
&lt;br /&gt;
== Audit Trail ==&lt;br /&gt;
&lt;br /&gt;
One of the application&#039;s strongest features is its audit trail. Each time the player advances to a new prompt, TYOV-Web captures a complete snapshot of the game state: prompt transition, dice roll, character sheet, memories, and the set of changes applied on that turn. In effect, the application keeps both the &#039;&#039;current&#039;&#039; vampire and the &#039;&#039;history of how that vampire became current&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
This matters for more than debugging. In a game built around forgetting, a turn-by-turn archive becomes part of the theme. The player may choose to lose a memory inside the fiction, but the application still preserves the fact that the loss occurred. The vampire forgets; the chronicle does not.&lt;br /&gt;
&lt;br /&gt;
== Development and Deployment ==&lt;br /&gt;
&lt;br /&gt;
TYOV-Web is developed as a two-part web project: Django on the server side, Vue on the client side. In local development the repository can be started with a PowerShell bootstrap script, which creates the Python environment, installs dependencies, runs migrations, and launches both servers. In production, the frontend is built with Vite and the backend is configured through the usual Django environment settings, including a production mode selected with &amp;lt;code&amp;gt;DJANGO_ENV=production&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Testing exists on both sides of the stack. The backend is covered with &#039;&#039;&#039;pytest&#039;&#039;&#039; and &#039;&#039;&#039;pytest-django&#039;&#039;&#039;; the frontend uses &#039;&#039;&#039;Playwright&#039;&#039;&#039; for end-to-end coverage of the creation flow and core game loop. Those details are reassuring, but secondary. The more meaningful point is that the project has been built as an actual maintained application rather than a one-off prototype.&lt;br /&gt;
&lt;br /&gt;
== Reception Within the Project ==&lt;br /&gt;
&lt;br /&gt;
As a fan-made implementation, TYOV-Web is best understood as an act of interpretation. It does not try to replace the tabletop experience with spectacle. Instead it asks what a web application can do well for this particular game: preserve long-form writing, reduce clerical overhead, make memory pressure visible, and retain the full history of a life that the protagonist can no longer fully remember. In that sense it is not merely a helper app. It is a machine for turning play into biography.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Vue.js]]&lt;br /&gt;
* [[Single-page application]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Web applications]]&lt;br /&gt;
[[Category:Role-playing video games]]&lt;br /&gt;
[[Category:Django (web framework) applications]]&lt;br /&gt;
[[Category:Vue.js applications]]&lt;br /&gt;
[[Category:Solo role-playing games]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=392</id>
		<title>Thousand Year Old Vampire</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=392"/>
		<updated>2026-04-12T11:34:54Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* How Play Works */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox &lt;br /&gt;
| 01_name = Thousand Year Old Vampire - Web Helper&lt;br /&gt;
| 02_developer = Michel Vuijlsteke&lt;br /&gt;
| 03_programming language = Python, TypeScript&lt;br /&gt;
| 04_operating system = Cross-platform (Web)&lt;br /&gt;
| 05_genre = Solo RPG digital companion&lt;br /&gt;
| 06_license = Custom&lt;br /&gt;
| 07_website = &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Thousand Year Old Vampire - Web Helper&#039;&#039;&#039; (&#039;&#039;&#039;TYOV-Web&#039;&#039;&#039;) is a browser-based adaptation of the solo role-playing game &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings. Rather than presenting the game as a digital board game or a rules spreadsheet, it treats a campaign as a long, accumulating chronicle: a vampire begins with a handful of memories, gains new experiences, loses old ones, and leaves behind a life story made of fragments, scars, names, and vanished centuries. The application is built as a &#039;&#039;&#039;Django&#039;&#039;&#039; backend serving a &#039;&#039;&#039;Vue&#039;&#039;&#039; single-page frontend, but its real achievement is literary rather than merely technical: it turns bookkeeping-heavy solo play into something that feels legible, mournful, and continuous.&lt;br /&gt;
&lt;br /&gt;
== Premise ==&lt;br /&gt;
&lt;br /&gt;
In &#039;&#039;Thousand Year Old Vampire&#039;&#039;, the player does not manage hit points, armies, or territory. Instead, the player manages &#039;&#039;&#039;memory&#039;&#039;&#039;. A vampire may survive for centuries, but cannot perfectly retain those centuries. New experiences force older ones aside. Friends die. Languages decay. Identities are traded, abandoned, or forgotten. The drama comes from watching the self narrow and distort over time.&lt;br /&gt;
&lt;br /&gt;
TYOV-Web preserves that central idea. A player creates a vampire from mortal beginnings, plays through prompts generated by dice rolls, writes narrative responses to those prompts, and organises the resulting fragments into memories. Over time the character collects skills, resources, marks, and known companions, but those are unstable possessions. The most important question is never simply &#039;&#039;what happened?&#039;&#039; It is &#039;&#039;what can still be remembered?&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== How Play Works ==&lt;br /&gt;
&lt;br /&gt;
The application follows the structure of the tabletop game closely, but presents it as a guided story engine. Character creation unfolds in six stages: mortal life, mortal relationships, skills, resources, a defining early experience, and finally the turning into vampirism. Once play begins, each turn revolves around a prompt. The player reads the current prompt, writes at least one experience in response, manages any consequences on the character sheet, then rolls a ten-sided die and a six-sided die to discover the next destination in the prompt list.&lt;br /&gt;
&lt;br /&gt;
That movement is what gives the game its broken historical rhythm. A positive result on D10 minus D6 pushes the vampire forward; a negative result drags the story backward into earlier themes or alternative branches. Prompts are also divided into variations, so revisiting a number does not necessarily mean revisiting the same moment in the same way.&lt;br /&gt;
&lt;br /&gt;
The memory system gives the application its emotional pressure. A character can normally hold only &#039;&#039;&#039;five active memories&#039;&#039;&#039;, and each memory can contain no more than &#039;&#039;&#039;three experiences&#039;&#039;&#039;. When that limit is reached, the player must decide what to lose. Some memories can be moved into a &#039;&#039;&#039;diary&#039;&#039;&#039;, which acts as an external repository, but diaries themselves are only resources, and resources can be lost. The result is a game about narrative curation: the player is constantly deciding which version of the vampire&#039;s past deserves to remain visible.&lt;br /&gt;
&lt;br /&gt;
Special prompts can alter that rhythm. Some disable experience-writing for a turn. Others allow the vampire&#039;s name to change, permit the editing of old experiences, reduce or increase permanent memory capacity, or end the chronicle entirely with an epilogue.&lt;br /&gt;
&lt;br /&gt;
== An Example Unlife: Karmis ==&lt;br /&gt;
&lt;br /&gt;
The most compelling way to understand the application is through one of its stored characters. One campaign in the database follows &#039;&#039;&#039;Karmis&#039;&#039;&#039;, formerly &#039;&#039;&#039;Ekurzu&#039;&#039;&#039; and before that &#039;&#039;&#039;Naram&#039;&#039;&#039;, across roughly three and a half thousand years.&lt;br /&gt;
&lt;br /&gt;
He begins in Bronze Age Mari as a temple scribe: precise, observant, and deeply embedded in the religious and political machinery of the city. His mortal life is defined by clay tablets, whispered intelligence, and courtly danger. He falls in love with Hessa, is shaped by the mentorship of the high priest Ibbi-Zamri, and is eventually turned by the ancient vampire Ashurban the Veiled. In one of the campaign&#039;s defining cruelties, Ibbi-Zamri becomes the first person Karmis kills after the turning.&lt;br /&gt;
&lt;br /&gt;
The early centuries of his unlife are marked by exile and shame. He flees with Ennatum, struggles to master the hunger, tries to imagine restraint as redemption, and fails. One of the campaign&#039;s most effective lines comes from this period: he builds walls and plays flutes in the wilderness, but it is &amp;quot;never penance, only a cage with softer bars.&amp;quot; Eventually he devours Ennatum during a sandstorm and carries that loss into the long middle stretch of the story.&lt;br /&gt;
&lt;br /&gt;
From there the campaign moves through one civilisation after another. Karmis passes through Akhenaten&#039;s Egypt, the imperial roads of the Achaemenids, the scholarly world of Alexandria, Roman frontiers, monastic Europe, Mediterranean trade routes, and the violence of the early modern Atlantic world. His roles change less than the world around him does. He remains a scribe, witness, forger, archivist, and careful observer of institutions that never last as long as he does.&lt;br /&gt;
&lt;br /&gt;
The campaign also shows how the game&#039;s systems create character rather than merely recording it. Karmis gains skills such as &#039;&#039;Silent Cartography&#039;&#039;, the ability to trace unseen paths of influence, and &#039;&#039;Ash-Tongue&#039;&#039;, a talent for insinuation and social survival. But he loses others as time damages his identity. He once knew how to decipher ancient texts; later he no longer can. He once believed he could control the beast within; later even that confidence disappears. The mechanics translate immortality into erosion.&lt;br /&gt;
&lt;br /&gt;
One of the campaign&#039;s richest relationships is with &#039;&#039;&#039;Thooni&#039;&#039;&#039;, a scholar who recognises the name &#039;&#039;Ekurzu&#039;&#039; not as legend but as continuity. After her mortal death, the relationship persists in altered form: a spectral Thooni begins appearing in the margins of Karmis&#039;s diary, leaving fragments in languages he can no longer read. TYOV-Web handles this elegantly because its model of a character is not limited to a living cast list. People can be present, absent, lost, remembered, or transformed into symbols the protagonist can no longer fully interpret.&lt;br /&gt;
&lt;br /&gt;
By the late Roman and early medieval portions of the chronicle, the cost of the memory rules becomes brutal. Karmis loses so many memories that even the event of his own turning is no longer available to him. He relies on diaries and external records to stabilise himself, yet those too are vulnerable. A carved chamber called &#039;&#039;The Walls of Forgetting&#039;&#039; and a later monastery diary both disappear. The application does not treat this as simple inventory loss; it feels instead like a second death for pieces of the self.&lt;br /&gt;
&lt;br /&gt;
In the final act, Karmis witnesses the Atlantic slave trade and begins keeping a ledger of names drawn from shipping manifests and punishment records. A later response captures the moral exhaustion of the character with unusual sharpness: he no longer imagines himself purified by restraint, only redirected by purpose. He feeds on slavers and calls it justice, yet recognises that the blood tastes the same. This is the kind of line the application is built to preserve: not just event logs, but accumulations of ethical fatigue.&lt;br /&gt;
&lt;br /&gt;
His last great companion is &#039;&#039;&#039;Mirelde of Bracha&#039;&#039;&#039;, who becomes mirror, partner, and eventually historian. By the time the epilogue arrives, the story is no longer told in Karmis&#039;s own voice. Mirelde writes the closing judgment on him: not as a monster laid to rest, but as &amp;quot;just a man who remembered too much, for too long, and chose, in the end, peace.&amp;quot; That shift in narrator is exactly the sort of late-game turn TYOV-Web makes easy to sustain, because it keeps the campaign legible even after the protagonist himself has become unreliable.&lt;br /&gt;
&lt;br /&gt;
== Why the Application Works ==&lt;br /&gt;
&lt;br /&gt;
What makes TYOV-Web effective is not that it digitises every rule, though it largely does. Its strength is that it understands which parts of the original game are dramatically important and places those at the centre of the interface.&lt;br /&gt;
&lt;br /&gt;
The main play screen is organised around the prompt, the character sheet, and the memory display. The prompt is always the immediate literary provocation. The memory display constantly reminds the player of pressure and loss: active memories, diary memories, and forgotten memories sit in visible relation to one another. Skills, resources, marks, and known characters are editable in place, which means the character sheet grows and collapses alongside the fiction instead of feeling like a separate administrative surface.&lt;br /&gt;
&lt;br /&gt;
The result is a frontend that behaves less like a dashboard and more like a writing desk. Players move between the prompt text, the experience form, and the fragments of the vampire&#039;s surviving life. Even when the application is doing something purely mechanical, such as validating whether the player may continue, the effect is narrative: it is asking whether the current moment of the story has been fully accounted for.&lt;br /&gt;
&lt;br /&gt;
== The Frontend as Chronicle ==&lt;br /&gt;
&lt;br /&gt;
The client side of TYOV-Web is a &#039;&#039;&#039;Vue 3&#039;&#039;&#039; single-page application written in &#039;&#039;&#039;TypeScript&#039;&#039;&#039;. In practical terms, this means the entire experience unfolds without page reloads; in literary terms, it allows the game to feel continuous. The player logs in, sees a home screen of active and archived characters, enters a creation wizard for new vampires, and then spends most of the campaign inside a single evolving game view.&lt;br /&gt;
&lt;br /&gt;
That view is supported by a set of focused screens: a login page, a home dashboard, the six-step character creation flow, the main game interface, an ending screen for completed chronicles, an archive view, and a story view that presents the campaign as a timeline. Around those are a collection of smaller windows and dialogs for editing characters, skills, resources, memories, and experiences. These are important not because there are many of them, but because they preserve the sense that a campaign is always open to revision, annotation, and reinterpretation.&lt;br /&gt;
&lt;br /&gt;
State is held in Pinia stores for authentication, gameplay, character creation, and theme. That is an implementation detail, but in this case it serves a visible purpose: it keeps the player&#039;s character, current prompt, pending changes, and validation state coherent at all times. The interface never feels as if it has forgotten where the player is in the life of the vampire.&lt;br /&gt;
&lt;br /&gt;
== The Backend as Memory Keeper ==&lt;br /&gt;
&lt;br /&gt;
Behind the interface is a &#039;&#039;&#039;Django 5&#039;&#039;&#039; application with a REST API built on &#039;&#039;&#039;Django REST Framework&#039;&#039;&#039;. The backend models the campaign in the same terms the game itself cares about: &#039;&#039;&#039;Character&#039;&#039;&#039;, &#039;&#039;&#039;Memory&#039;&#039;&#039;, &#039;&#039;&#039;Experience&#039;&#039;&#039;, &#039;&#039;&#039;Skill&#039;&#039;&#039;, &#039;&#039;&#039;Resource&#039;&#039;&#039;, &#039;&#039;&#039;Mark&#039;&#039;&#039;, &#039;&#039;&#039;GameCharacter&#039;&#039;&#039;, &#039;&#039;&#039;Prompt&#039;&#039;&#039;, and &#039;&#039;&#039;PendingChange&#039;&#039;&#039;. This is not especially glamorous architecture, but it is disciplined. The data model is close to the language of play, which makes the whole system easier to reason about.&lt;br /&gt;
&lt;br /&gt;
The most interesting design choice is the use of &#039;&#039;&#039;pending changes&#039;&#039;&#039;. TYOV-Web does not immediately finalise every action the player takes. Instead, new experiences and character-sheet adjustments can accumulate as provisional edits until the player chooses to continue the story. Only then are they committed atomically. This makes the system feel less like a CRUD application and more like a drafting process. A turn can be assembled, reconsidered, and only then sealed into the chronicle.&lt;br /&gt;
&lt;br /&gt;
Authentication is handled by JWT tokens, and character data is scoped to the authenticated user. The backend exposes endpoints for the game state itself, dice rolling, story continuation, and CRUD operations on memories, experiences, skills, resources, marks, and known characters. In the article, however, the important point is not the shape of the URLs. It is that the backend has been designed to support a narrative workflow rather than a generic records system.&lt;br /&gt;
&lt;br /&gt;
== Audit Trail ==&lt;br /&gt;
&lt;br /&gt;
One of the application&#039;s strongest features is its audit trail. Each time the player advances to a new prompt, TYOV-Web captures a complete snapshot of the game state: prompt transition, dice roll, character sheet, memories, and the set of changes applied on that turn. In effect, the application keeps both the &#039;&#039;current&#039;&#039; vampire and the &#039;&#039;history of how that vampire became current&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
This matters for more than debugging. In a game built around forgetting, a turn-by-turn archive becomes part of the theme. The player may choose to lose a memory inside the fiction, but the application still preserves the fact that the loss occurred. The vampire forgets; the chronicle does not.&lt;br /&gt;
&lt;br /&gt;
== Development and Deployment ==&lt;br /&gt;
&lt;br /&gt;
TYOV-Web is developed as a two-part web project: Django on the server side, Vue on the client side. In local development the repository can be started with a PowerShell bootstrap script, which creates the Python environment, installs dependencies, runs migrations, and launches both servers. In production, the frontend is built with Vite and the backend is configured through the usual Django environment settings, including a production mode selected with &amp;lt;code&amp;gt;DJANGO_ENV=production&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Testing exists on both sides of the stack. The backend is covered with &#039;&#039;&#039;pytest&#039;&#039;&#039; and &#039;&#039;&#039;pytest-django&#039;&#039;&#039;; the frontend uses &#039;&#039;&#039;Playwright&#039;&#039;&#039; for end-to-end coverage of the creation flow and core game loop. Those details are reassuring, but secondary. The more meaningful point is that the project has been built as an actual maintained application rather than a one-off prototype.&lt;br /&gt;
&lt;br /&gt;
== Reception Within the Project ==&lt;br /&gt;
&lt;br /&gt;
As a fan-made implementation, TYOV-Web is best understood as an act of interpretation. It does not try to replace the tabletop experience with spectacle. Instead it asks what a web application can do well for this particular game: preserve long-form writing, reduce clerical overhead, make memory pressure visible, and retain the full history of a life that the protagonist can no longer fully remember. In that sense it is not merely a helper app. It is a machine for turning play into biography.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Vue.js]]&lt;br /&gt;
* [[Single-page application]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Web applications]]&lt;br /&gt;
[[Category:Role-playing video games]]&lt;br /&gt;
[[Category:Django (web framework) applications]]&lt;br /&gt;
[[Category:Vue.js applications]]&lt;br /&gt;
[[Category:Solo role-playing games]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=391</id>
		<title>Thousand Year Old Vampire</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=391"/>
		<updated>2026-04-12T11:34:07Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox &lt;br /&gt;
| 01_name = Thousand Year Old Vampire - Web Helper&lt;br /&gt;
| 02_developer = Michel Vuijlsteke&lt;br /&gt;
| 03_programming language = Python, TypeScript&lt;br /&gt;
| 04_operating system = Cross-platform (Web)&lt;br /&gt;
| 05_genre = Solo RPG digital companion&lt;br /&gt;
| 06_license = Custom&lt;br /&gt;
| 07_website = &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Thousand Year Old Vampire - Web Helper&#039;&#039;&#039; (&#039;&#039;&#039;TYOV-Web&#039;&#039;&#039;) is a browser-based adaptation of the solo role-playing game &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings. Rather than presenting the game as a digital board game or a rules spreadsheet, it treats a campaign as a long, accumulating chronicle: a vampire begins with a handful of memories, gains new experiences, loses old ones, and leaves behind a life story made of fragments, scars, names, and vanished centuries. The application is built as a &#039;&#039;&#039;Django&#039;&#039;&#039; backend serving a &#039;&#039;&#039;Vue&#039;&#039;&#039; single-page frontend, but its real achievement is literary rather than merely technical: it turns bookkeeping-heavy solo play into something that feels legible, mournful, and continuous.&lt;br /&gt;
&lt;br /&gt;
== Premise ==&lt;br /&gt;
&lt;br /&gt;
In &#039;&#039;Thousand Year Old Vampire&#039;&#039;, the player does not manage hit points, armies, or territory. Instead, the player manages &#039;&#039;&#039;memory&#039;&#039;&#039;. A vampire may survive for centuries, but cannot perfectly retain those centuries. New experiences force older ones aside. Friends die. Languages decay. Identities are traded, abandoned, or forgotten. The drama comes from watching the self narrow and distort over time.&lt;br /&gt;
&lt;br /&gt;
TYOV-Web preserves that central idea. A player creates a vampire from mortal beginnings, plays through prompts generated by dice rolls, writes narrative responses to those prompts, and organises the resulting fragments into memories. Over time the character collects skills, resources, marks, and known companions, but those are unstable possessions. The most important question is never simply &#039;&#039;what happened?&#039;&#039; It is &#039;&#039;what can still be remembered?&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== How Play Works ==&lt;br /&gt;
&lt;br /&gt;
The application follows the structure of the tabletop game closely, but presents it as a guided story engine. Character creation unfolds in six stages: mortal life, mortal relationships, skills, resources, a defining early experience, and finally the turning into vampirism. Once play begins, each turn revolves around a prompt. The player reads the current prompt, writes at least one experience in response, manages any consequences on the character sheet, then rolls a ten-sided die and a six-sided die to discover the next destination in the prompt list.&lt;br /&gt;
&lt;br /&gt;
That movement is what gives the game its broken historical rhythm. A positive result on D10 minus D6 pushes the vampire forward; a negative result drags the story backward into earlier themes or alternative branches. Prompts are also divided into variations, so revisiting a number does not necessarily mean revisiting the same moment in the same way.&lt;br /&gt;
&lt;br /&gt;
The memory system gives the application its emotional pressure. A character can normally hold only &#039;&#039;&#039;five active memories&#039;&#039;&#039;, and each memory can contain no more than &#039;&#039;&#039;three experiences&#039;&#039;&#039;. When that limit is reached, the player must decide what to lose. Some memories can be moved into a &#039;&#039;&#039;diary&#039;&#039;&#039;, which acts as an external repository, but diaries themselves are only resources, and resources can be lost. The result is a game about narrative curation: the player is constantly deciding which version of the vampire&#039;s past deserves to remain visible.&lt;br /&gt;
&lt;br /&gt;
Special prompts can alter that rhythm. Some disable experience-writing for a turn. Others allow the vampire&#039;s name to change, permit the editing of old experiences, reduce or increase permanent memory capacity, or end the chronicle entirely with an epilogue. TYOV-Web exposes these rules clearly during play, but the rules never overwhelm the fiction. They exist to sharpen it.&lt;br /&gt;
&lt;br /&gt;
== An Example Unlife: Karmis ==&lt;br /&gt;
&lt;br /&gt;
The most compelling way to understand the application is through one of its stored characters. One campaign in the database follows &#039;&#039;&#039;Karmis&#039;&#039;&#039;, formerly &#039;&#039;&#039;Ekurzu&#039;&#039;&#039; and before that &#039;&#039;&#039;Naram&#039;&#039;&#039;, across roughly three and a half thousand years.&lt;br /&gt;
&lt;br /&gt;
He begins in Bronze Age Mari as a temple scribe: precise, observant, and deeply embedded in the religious and political machinery of the city. His mortal life is defined by clay tablets, whispered intelligence, and courtly danger. He falls in love with Hessa, is shaped by the mentorship of the high priest Ibbi-Zamri, and is eventually turned by the ancient vampire Ashurban the Veiled. In one of the campaign&#039;s defining cruelties, Ibbi-Zamri becomes the first person Karmis kills after the turning.&lt;br /&gt;
&lt;br /&gt;
The early centuries of his unlife are marked by exile and shame. He flees with Ennatum, struggles to master the hunger, tries to imagine restraint as redemption, and fails. One of the campaign&#039;s most effective lines comes from this period: he builds walls and plays flutes in the wilderness, but it is &amp;quot;never penance, only a cage with softer bars.&amp;quot; Eventually he devours Ennatum during a sandstorm and carries that loss into the long middle stretch of the story.&lt;br /&gt;
&lt;br /&gt;
From there the campaign moves through one civilisation after another. Karmis passes through Akhenaten&#039;s Egypt, the imperial roads of the Achaemenids, the scholarly world of Alexandria, Roman frontiers, monastic Europe, Mediterranean trade routes, and the violence of the early modern Atlantic world. His roles change less than the world around him does. He remains a scribe, witness, forger, archivist, and careful observer of institutions that never last as long as he does.&lt;br /&gt;
&lt;br /&gt;
The campaign also shows how the game&#039;s systems create character rather than merely recording it. Karmis gains skills such as &#039;&#039;Silent Cartography&#039;&#039;, the ability to trace unseen paths of influence, and &#039;&#039;Ash-Tongue&#039;&#039;, a talent for insinuation and social survival. But he loses others as time damages his identity. He once knew how to decipher ancient texts; later he no longer can. He once believed he could control the beast within; later even that confidence disappears. The mechanics translate immortality into erosion.&lt;br /&gt;
&lt;br /&gt;
One of the campaign&#039;s richest relationships is with &#039;&#039;&#039;Thooni&#039;&#039;&#039;, a scholar who recognises the name &#039;&#039;Ekurzu&#039;&#039; not as legend but as continuity. After her mortal death, the relationship persists in altered form: a spectral Thooni begins appearing in the margins of Karmis&#039;s diary, leaving fragments in languages he can no longer read. TYOV-Web handles this elegantly because its model of a character is not limited to a living cast list. People can be present, absent, lost, remembered, or transformed into symbols the protagonist can no longer fully interpret.&lt;br /&gt;
&lt;br /&gt;
By the late Roman and early medieval portions of the chronicle, the cost of the memory rules becomes brutal. Karmis loses so many memories that even the event of his own turning is no longer available to him. He relies on diaries and external records to stabilise himself, yet those too are vulnerable. A carved chamber called &#039;&#039;The Walls of Forgetting&#039;&#039; and a later monastery diary both disappear. The application does not treat this as simple inventory loss; it feels instead like a second death for pieces of the self.&lt;br /&gt;
&lt;br /&gt;
In the final act, Karmis witnesses the Atlantic slave trade and begins keeping a ledger of names drawn from shipping manifests and punishment records. A later response captures the moral exhaustion of the character with unusual sharpness: he no longer imagines himself purified by restraint, only redirected by purpose. He feeds on slavers and calls it justice, yet recognises that the blood tastes the same. This is the kind of line the application is built to preserve: not just event logs, but accumulations of ethical fatigue.&lt;br /&gt;
&lt;br /&gt;
His last great companion is &#039;&#039;&#039;Mirelde of Bracha&#039;&#039;&#039;, who becomes mirror, partner, and eventually historian. By the time the epilogue arrives, the story is no longer told in Karmis&#039;s own voice. Mirelde writes the closing judgment on him: not as a monster laid to rest, but as &amp;quot;just a man who remembered too much, for too long, and chose, in the end, peace.&amp;quot; That shift in narrator is exactly the sort of late-game turn TYOV-Web makes easy to sustain, because it keeps the campaign legible even after the protagonist himself has become unreliable.&lt;br /&gt;
&lt;br /&gt;
== Why the Application Works ==&lt;br /&gt;
&lt;br /&gt;
What makes TYOV-Web effective is not that it digitises every rule, though it largely does. Its strength is that it understands which parts of the original game are dramatically important and places those at the centre of the interface.&lt;br /&gt;
&lt;br /&gt;
The main play screen is organised around the prompt, the character sheet, and the memory display. The prompt is always the immediate literary provocation. The memory display constantly reminds the player of pressure and loss: active memories, diary memories, and forgotten memories sit in visible relation to one another. Skills, resources, marks, and known characters are editable in place, which means the character sheet grows and collapses alongside the fiction instead of feeling like a separate administrative surface.&lt;br /&gt;
&lt;br /&gt;
The result is a frontend that behaves less like a dashboard and more like a writing desk. Players move between the prompt text, the experience form, and the fragments of the vampire&#039;s surviving life. Even when the application is doing something purely mechanical, such as validating whether the player may continue, the effect is narrative: it is asking whether the current moment of the story has been fully accounted for.&lt;br /&gt;
&lt;br /&gt;
== The Frontend as Chronicle ==&lt;br /&gt;
&lt;br /&gt;
The client side of TYOV-Web is a &#039;&#039;&#039;Vue 3&#039;&#039;&#039; single-page application written in &#039;&#039;&#039;TypeScript&#039;&#039;&#039;. In practical terms, this means the entire experience unfolds without page reloads; in literary terms, it allows the game to feel continuous. The player logs in, sees a home screen of active and archived characters, enters a creation wizard for new vampires, and then spends most of the campaign inside a single evolving game view.&lt;br /&gt;
&lt;br /&gt;
That view is supported by a set of focused screens: a login page, a home dashboard, the six-step character creation flow, the main game interface, an ending screen for completed chronicles, an archive view, and a story view that presents the campaign as a timeline. Around those are a collection of smaller windows and dialogs for editing characters, skills, resources, memories, and experiences. These are important not because there are many of them, but because they preserve the sense that a campaign is always open to revision, annotation, and reinterpretation.&lt;br /&gt;
&lt;br /&gt;
State is held in Pinia stores for authentication, gameplay, character creation, and theme. That is an implementation detail, but in this case it serves a visible purpose: it keeps the player&#039;s character, current prompt, pending changes, and validation state coherent at all times. The interface never feels as if it has forgotten where the player is in the life of the vampire.&lt;br /&gt;
&lt;br /&gt;
== The Backend as Memory Keeper ==&lt;br /&gt;
&lt;br /&gt;
Behind the interface is a &#039;&#039;&#039;Django 5&#039;&#039;&#039; application with a REST API built on &#039;&#039;&#039;Django REST Framework&#039;&#039;&#039;. The backend models the campaign in the same terms the game itself cares about: &#039;&#039;&#039;Character&#039;&#039;&#039;, &#039;&#039;&#039;Memory&#039;&#039;&#039;, &#039;&#039;&#039;Experience&#039;&#039;&#039;, &#039;&#039;&#039;Skill&#039;&#039;&#039;, &#039;&#039;&#039;Resource&#039;&#039;&#039;, &#039;&#039;&#039;Mark&#039;&#039;&#039;, &#039;&#039;&#039;GameCharacter&#039;&#039;&#039;, &#039;&#039;&#039;Prompt&#039;&#039;&#039;, and &#039;&#039;&#039;PendingChange&#039;&#039;&#039;. This is not especially glamorous architecture, but it is disciplined. The data model is close to the language of play, which makes the whole system easier to reason about.&lt;br /&gt;
&lt;br /&gt;
The most interesting design choice is the use of &#039;&#039;&#039;pending changes&#039;&#039;&#039;. TYOV-Web does not immediately finalise every action the player takes. Instead, new experiences and character-sheet adjustments can accumulate as provisional edits until the player chooses to continue the story. Only then are they committed atomically. This makes the system feel less like a CRUD application and more like a drafting process. A turn can be assembled, reconsidered, and only then sealed into the chronicle.&lt;br /&gt;
&lt;br /&gt;
Authentication is handled by JWT tokens, and character data is scoped to the authenticated user. The backend exposes endpoints for the game state itself, dice rolling, story continuation, and CRUD operations on memories, experiences, skills, resources, marks, and known characters. In the article, however, the important point is not the shape of the URLs. It is that the backend has been designed to support a narrative workflow rather than a generic records system.&lt;br /&gt;
&lt;br /&gt;
== Audit Trail ==&lt;br /&gt;
&lt;br /&gt;
One of the application&#039;s strongest features is its audit trail. Each time the player advances to a new prompt, TYOV-Web captures a complete snapshot of the game state: prompt transition, dice roll, character sheet, memories, and the set of changes applied on that turn. In effect, the application keeps both the &#039;&#039;current&#039;&#039; vampire and the &#039;&#039;history of how that vampire became current&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
This matters for more than debugging. In a game built around forgetting, a turn-by-turn archive becomes part of the theme. The player may choose to lose a memory inside the fiction, but the application still preserves the fact that the loss occurred. The vampire forgets; the chronicle does not.&lt;br /&gt;
&lt;br /&gt;
== Development and Deployment ==&lt;br /&gt;
&lt;br /&gt;
TYOV-Web is developed as a two-part web project: Django on the server side, Vue on the client side. In local development the repository can be started with a PowerShell bootstrap script, which creates the Python environment, installs dependencies, runs migrations, and launches both servers. In production, the frontend is built with Vite and the backend is configured through the usual Django environment settings, including a production mode selected with &amp;lt;code&amp;gt;DJANGO_ENV=production&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Testing exists on both sides of the stack. The backend is covered with &#039;&#039;&#039;pytest&#039;&#039;&#039; and &#039;&#039;&#039;pytest-django&#039;&#039;&#039;; the frontend uses &#039;&#039;&#039;Playwright&#039;&#039;&#039; for end-to-end coverage of the creation flow and core game loop. Those details are reassuring, but secondary. The more meaningful point is that the project has been built as an actual maintained application rather than a one-off prototype.&lt;br /&gt;
&lt;br /&gt;
== Reception Within the Project ==&lt;br /&gt;
&lt;br /&gt;
As a fan-made implementation, TYOV-Web is best understood as an act of interpretation. It does not try to replace the tabletop experience with spectacle. Instead it asks what a web application can do well for this particular game: preserve long-form writing, reduce clerical overhead, make memory pressure visible, and retain the full history of a life that the protagonist can no longer fully remember. In that sense it is not merely a helper app. It is a machine for turning play into biography.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Vue.js]]&lt;br /&gt;
* [[Single-page application]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Web applications]]&lt;br /&gt;
[[Category:Role-playing video games]]&lt;br /&gt;
[[Category:Django (web framework) applications]]&lt;br /&gt;
[[Category:Vue.js applications]]&lt;br /&gt;
[[Category:Solo role-playing games]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=390</id>
		<title>Thousand Year Old Vampire</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=390"/>
		<updated>2026-04-12T11:33:32Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox software&lt;br /&gt;
| name = Thousand Year Old Vampire - Web Helper&lt;br /&gt;
| developer = Michel Vuijlsteke&lt;br /&gt;
| programming language = Python, TypeScript&lt;br /&gt;
| operating system = Cross-platform (Web)&lt;br /&gt;
| genre = Solo RPG digital companion&lt;br /&gt;
| license = Custom&lt;br /&gt;
| website = &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Thousand Year Old Vampire - Web Helper&#039;&#039;&#039; (&#039;&#039;&#039;TYOV-Web&#039;&#039;&#039;) is a browser-based adaptation of the solo role-playing game &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings. Rather than presenting the game as a digital board game or a rules spreadsheet, it treats a campaign as a long, accumulating chronicle: a vampire begins with a handful of memories, gains new experiences, loses old ones, and leaves behind a life story made of fragments, scars, names, and vanished centuries. The application is built as a &#039;&#039;&#039;Django&#039;&#039;&#039; backend serving a &#039;&#039;&#039;Vue&#039;&#039;&#039; single-page frontend, but its real achievement is literary rather than merely technical: it turns bookkeeping-heavy solo play into something that feels legible, mournful, and continuous.&lt;br /&gt;
&lt;br /&gt;
== Premise ==&lt;br /&gt;
&lt;br /&gt;
In &#039;&#039;Thousand Year Old Vampire&#039;&#039;, the player does not manage hit points, armies, or territory. Instead, the player manages &#039;&#039;&#039;memory&#039;&#039;&#039;. A vampire may survive for centuries, but cannot perfectly retain those centuries. New experiences force older ones aside. Friends die. Languages decay. Identities are traded, abandoned, or forgotten. The drama comes from watching the self narrow and distort over time.&lt;br /&gt;
&lt;br /&gt;
TYOV-Web preserves that central idea. A player creates a vampire from mortal beginnings, plays through prompts generated by dice rolls, writes narrative responses to those prompts, and organises the resulting fragments into memories. Over time the character collects skills, resources, marks, and known companions, but those are unstable possessions. The most important question is never simply &#039;&#039;what happened?&#039;&#039; It is &#039;&#039;what can still be remembered?&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== How Play Works ==&lt;br /&gt;
&lt;br /&gt;
The application follows the structure of the tabletop game closely, but presents it as a guided story engine. Character creation unfolds in six stages: mortal life, mortal relationships, skills, resources, a defining early experience, and finally the turning into vampirism. Once play begins, each turn revolves around a prompt. The player reads the current prompt, writes at least one experience in response, manages any consequences on the character sheet, then rolls a ten-sided die and a six-sided die to discover the next destination in the prompt list.&lt;br /&gt;
&lt;br /&gt;
That movement is what gives the game its broken historical rhythm. A positive result on D10 minus D6 pushes the vampire forward; a negative result drags the story backward into earlier themes or alternative branches. Prompts are also divided into variations, so revisiting a number does not necessarily mean revisiting the same moment in the same way.&lt;br /&gt;
&lt;br /&gt;
The memory system gives the application its emotional pressure. A character can normally hold only &#039;&#039;&#039;five active memories&#039;&#039;&#039;, and each memory can contain no more than &#039;&#039;&#039;three experiences&#039;&#039;&#039;. When that limit is reached, the player must decide what to lose. Some memories can be moved into a &#039;&#039;&#039;diary&#039;&#039;&#039;, which acts as an external repository, but diaries themselves are only resources, and resources can be lost. The result is a game about narrative curation: the player is constantly deciding which version of the vampire&#039;s past deserves to remain visible.&lt;br /&gt;
&lt;br /&gt;
Special prompts can alter that rhythm. Some disable experience-writing for a turn. Others allow the vampire&#039;s name to change, permit the editing of old experiences, reduce or increase permanent memory capacity, or end the chronicle entirely with an epilogue. TYOV-Web exposes these rules clearly during play, but the rules never overwhelm the fiction. They exist to sharpen it.&lt;br /&gt;
&lt;br /&gt;
== An Example Unlife: Karmis ==&lt;br /&gt;
&lt;br /&gt;
The most compelling way to understand the application is through one of its stored characters. One campaign in the database follows &#039;&#039;&#039;Karmis&#039;&#039;&#039;, formerly &#039;&#039;&#039;Ekurzu&#039;&#039;&#039; and before that &#039;&#039;&#039;Naram&#039;&#039;&#039;, across roughly three and a half thousand years.&lt;br /&gt;
&lt;br /&gt;
He begins in Bronze Age Mari as a temple scribe: precise, observant, and deeply embedded in the religious and political machinery of the city. His mortal life is defined by clay tablets, whispered intelligence, and courtly danger. He falls in love with Hessa, is shaped by the mentorship of the high priest Ibbi-Zamri, and is eventually turned by the ancient vampire Ashurban the Veiled. In one of the campaign&#039;s defining cruelties, Ibbi-Zamri becomes the first person Karmis kills after the turning.&lt;br /&gt;
&lt;br /&gt;
The early centuries of his unlife are marked by exile and shame. He flees with Ennatum, struggles to master the hunger, tries to imagine restraint as redemption, and fails. One of the campaign&#039;s most effective lines comes from this period: he builds walls and plays flutes in the wilderness, but it is &amp;quot;never penance, only a cage with softer bars.&amp;quot; Eventually he devours Ennatum during a sandstorm and carries that loss into the long middle stretch of the story.&lt;br /&gt;
&lt;br /&gt;
From there the campaign moves through one civilisation after another. Karmis passes through Akhenaten&#039;s Egypt, the imperial roads of the Achaemenids, the scholarly world of Alexandria, Roman frontiers, monastic Europe, Mediterranean trade routes, and the violence of the early modern Atlantic world. His roles change less than the world around him does. He remains a scribe, witness, forger, archivist, and careful observer of institutions that never last as long as he does.&lt;br /&gt;
&lt;br /&gt;
The campaign also shows how the game&#039;s systems create character rather than merely recording it. Karmis gains skills such as &#039;&#039;Silent Cartography&#039;&#039;, the ability to trace unseen paths of influence, and &#039;&#039;Ash-Tongue&#039;&#039;, a talent for insinuation and social survival. But he loses others as time damages his identity. He once knew how to decipher ancient texts; later he no longer can. He once believed he could control the beast within; later even that confidence disappears. The mechanics translate immortality into erosion.&lt;br /&gt;
&lt;br /&gt;
One of the campaign&#039;s richest relationships is with &#039;&#039;&#039;Thooni&#039;&#039;&#039;, a scholar who recognises the name &#039;&#039;Ekurzu&#039;&#039; not as legend but as continuity. After her mortal death, the relationship persists in altered form: a spectral Thooni begins appearing in the margins of Karmis&#039;s diary, leaving fragments in languages he can no longer read. TYOV-Web handles this elegantly because its model of a character is not limited to a living cast list. People can be present, absent, lost, remembered, or transformed into symbols the protagonist can no longer fully interpret.&lt;br /&gt;
&lt;br /&gt;
By the late Roman and early medieval portions of the chronicle, the cost of the memory rules becomes brutal. Karmis loses so many memories that even the event of his own turning is no longer available to him. He relies on diaries and external records to stabilise himself, yet those too are vulnerable. A carved chamber called &#039;&#039;The Walls of Forgetting&#039;&#039; and a later monastery diary both disappear. The application does not treat this as simple inventory loss; it feels instead like a second death for pieces of the self.&lt;br /&gt;
&lt;br /&gt;
In the final act, Karmis witnesses the Atlantic slave trade and begins keeping a ledger of names drawn from shipping manifests and punishment records. A later response captures the moral exhaustion of the character with unusual sharpness: he no longer imagines himself purified by restraint, only redirected by purpose. He feeds on slavers and calls it justice, yet recognises that the blood tastes the same. This is the kind of line the application is built to preserve: not just event logs, but accumulations of ethical fatigue.&lt;br /&gt;
&lt;br /&gt;
His last great companion is &#039;&#039;&#039;Mirelde of Bracha&#039;&#039;&#039;, who becomes mirror, partner, and eventually historian. By the time the epilogue arrives, the story is no longer told in Karmis&#039;s own voice. Mirelde writes the closing judgment on him: not as a monster laid to rest, but as &amp;quot;just a man who remembered too much, for too long, and chose, in the end, peace.&amp;quot; That shift in narrator is exactly the sort of late-game turn TYOV-Web makes easy to sustain, because it keeps the campaign legible even after the protagonist himself has become unreliable.&lt;br /&gt;
&lt;br /&gt;
== Why the Application Works ==&lt;br /&gt;
&lt;br /&gt;
What makes TYOV-Web effective is not that it digitises every rule, though it largely does. Its strength is that it understands which parts of the original game are dramatically important and places those at the centre of the interface.&lt;br /&gt;
&lt;br /&gt;
The main play screen is organised around the prompt, the character sheet, and the memory display. The prompt is always the immediate literary provocation. The memory display constantly reminds the player of pressure and loss: active memories, diary memories, and forgotten memories sit in visible relation to one another. Skills, resources, marks, and known characters are editable in place, which means the character sheet grows and collapses alongside the fiction instead of feeling like a separate administrative surface.&lt;br /&gt;
&lt;br /&gt;
The result is a frontend that behaves less like a dashboard and more like a writing desk. Players move between the prompt text, the experience form, and the fragments of the vampire&#039;s surviving life. Even when the application is doing something purely mechanical, such as validating whether the player may continue, the effect is narrative: it is asking whether the current moment of the story has been fully accounted for.&lt;br /&gt;
&lt;br /&gt;
== The Frontend as Chronicle ==&lt;br /&gt;
&lt;br /&gt;
The client side of TYOV-Web is a &#039;&#039;&#039;Vue 3&#039;&#039;&#039; single-page application written in &#039;&#039;&#039;TypeScript&#039;&#039;&#039;. In practical terms, this means the entire experience unfolds without page reloads; in literary terms, it allows the game to feel continuous. The player logs in, sees a home screen of active and archived characters, enters a creation wizard for new vampires, and then spends most of the campaign inside a single evolving game view.&lt;br /&gt;
&lt;br /&gt;
That view is supported by a set of focused screens: a login page, a home dashboard, the six-step character creation flow, the main game interface, an ending screen for completed chronicles, an archive view, and a story view that presents the campaign as a timeline. Around those are a collection of smaller windows and dialogs for editing characters, skills, resources, memories, and experiences. These are important not because there are many of them, but because they preserve the sense that a campaign is always open to revision, annotation, and reinterpretation.&lt;br /&gt;
&lt;br /&gt;
State is held in Pinia stores for authentication, gameplay, character creation, and theme. That is an implementation detail, but in this case it serves a visible purpose: it keeps the player&#039;s character, current prompt, pending changes, and validation state coherent at all times. The interface never feels as if it has forgotten where the player is in the life of the vampire.&lt;br /&gt;
&lt;br /&gt;
== The Backend as Memory Keeper ==&lt;br /&gt;
&lt;br /&gt;
Behind the interface is a &#039;&#039;&#039;Django 5&#039;&#039;&#039; application with a REST API built on &#039;&#039;&#039;Django REST Framework&#039;&#039;&#039;. The backend models the campaign in the same terms the game itself cares about: &#039;&#039;&#039;Character&#039;&#039;&#039;, &#039;&#039;&#039;Memory&#039;&#039;&#039;, &#039;&#039;&#039;Experience&#039;&#039;&#039;, &#039;&#039;&#039;Skill&#039;&#039;&#039;, &#039;&#039;&#039;Resource&#039;&#039;&#039;, &#039;&#039;&#039;Mark&#039;&#039;&#039;, &#039;&#039;&#039;GameCharacter&#039;&#039;&#039;, &#039;&#039;&#039;Prompt&#039;&#039;&#039;, and &#039;&#039;&#039;PendingChange&#039;&#039;&#039;. This is not especially glamorous architecture, but it is disciplined. The data model is close to the language of play, which makes the whole system easier to reason about.&lt;br /&gt;
&lt;br /&gt;
The most interesting design choice is the use of &#039;&#039;&#039;pending changes&#039;&#039;&#039;. TYOV-Web does not immediately finalise every action the player takes. Instead, new experiences and character-sheet adjustments can accumulate as provisional edits until the player chooses to continue the story. Only then are they committed atomically. This makes the system feel less like a CRUD application and more like a drafting process. A turn can be assembled, reconsidered, and only then sealed into the chronicle.&lt;br /&gt;
&lt;br /&gt;
Authentication is handled by JWT tokens, and character data is scoped to the authenticated user. The backend exposes endpoints for the game state itself, dice rolling, story continuation, and CRUD operations on memories, experiences, skills, resources, marks, and known characters. In the article, however, the important point is not the shape of the URLs. It is that the backend has been designed to support a narrative workflow rather than a generic records system.&lt;br /&gt;
&lt;br /&gt;
== Audit Trail ==&lt;br /&gt;
&lt;br /&gt;
One of the application&#039;s strongest features is its audit trail. Each time the player advances to a new prompt, TYOV-Web captures a complete snapshot of the game state: prompt transition, dice roll, character sheet, memories, and the set of changes applied on that turn. In effect, the application keeps both the &#039;&#039;current&#039;&#039; vampire and the &#039;&#039;history of how that vampire became current&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
This matters for more than debugging. In a game built around forgetting, a turn-by-turn archive becomes part of the theme. The player may choose to lose a memory inside the fiction, but the application still preserves the fact that the loss occurred. The vampire forgets; the chronicle does not.&lt;br /&gt;
&lt;br /&gt;
== Development and Deployment ==&lt;br /&gt;
&lt;br /&gt;
TYOV-Web is developed as a two-part web project: Django on the server side, Vue on the client side. In local development the repository can be started with a PowerShell bootstrap script, which creates the Python environment, installs dependencies, runs migrations, and launches both servers. In production, the frontend is built with Vite and the backend is configured through the usual Django environment settings, including a production mode selected with &amp;lt;code&amp;gt;DJANGO_ENV=production&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Testing exists on both sides of the stack. The backend is covered with &#039;&#039;&#039;pytest&#039;&#039;&#039; and &#039;&#039;&#039;pytest-django&#039;&#039;&#039;; the frontend uses &#039;&#039;&#039;Playwright&#039;&#039;&#039; for end-to-end coverage of the creation flow and core game loop. Those details are reassuring, but secondary. The more meaningful point is that the project has been built as an actual maintained application rather than a one-off prototype.&lt;br /&gt;
&lt;br /&gt;
== Reception Within the Project ==&lt;br /&gt;
&lt;br /&gt;
As a fan-made implementation, TYOV-Web is best understood as an act of interpretation. It does not try to replace the tabletop experience with spectacle. Instead it asks what a web application can do well for this particular game: preserve long-form writing, reduce clerical overhead, make memory pressure visible, and retain the full history of a life that the protagonist can no longer fully remember. In that sense it is not merely a helper app. It is a machine for turning play into biography.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Vue.js]]&lt;br /&gt;
* [[Single-page application]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Web applications]]&lt;br /&gt;
[[Category:Role-playing video games]]&lt;br /&gt;
[[Category:Django (web framework) applications]]&lt;br /&gt;
[[Category:Vue.js applications]]&lt;br /&gt;
[[Category:Solo role-playing games]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=389</id>
		<title>Thousand Year Old Vampire</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=389"/>
		<updated>2026-04-12T11:26:18Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* A Life in Summary */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox &lt;br /&gt;
| 01_name = Thousand Year Old Vampire – Web Helper&lt;br /&gt;
| 02_logo =&lt;br /&gt;
| 03_developer = Michel Vuijlsteke&lt;br /&gt;
| 04_programming language = Python (Django 5), TypeScript (Vue 3)&lt;br /&gt;
| 05_operating system = Cross-platform (Web)&lt;br /&gt;
| 06_genre = Solo RPG Digital Companion&lt;br /&gt;
| 07_license = custom&lt;br /&gt;
| 08_website = &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Thousand Year Old Vampire – Web Helper&#039;&#039;&#039; (&#039;&#039;&#039;TYOV-Web&#039;&#039;&#039;) is a modern web-based implementation of the solo tabletop role-playing game &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings. The application digitises the game&#039;s complex mechanics of memory, experience, and character development, allowing players to create and guide a vampire character across centuries of unlife through an interactive browser interface. The backend is built with &#039;&#039;&#039;Django 5&#039;&#039;&#039; and &#039;&#039;&#039;Django REST Framework&#039;&#039;&#039;; the frontend is a &#039;&#039;&#039;Vue 3&#039;&#039;&#039; single-page application.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Thousand Year Old Vampire&#039;&#039; is a solo storytelling RPG in which a player creates a vampire character and guides it through an increasingly fragmented existence spanning hundreds or thousands of years. The central tension lies in the vampire&#039;s deteriorating memory: as an immortal being it accumulates experiences, but its ancient mind can only retain a limited number of memories at any time. Players must constantly choose what to remember and what to forget.&lt;br /&gt;
&lt;br /&gt;
TYOV-Web fully implements the game&#039;s rules—character creation, dice-driven prompt navigation, memory management, diary storage, skills, resources, marks, known characters, and game-ending conditions—in a reactive web interface with persistent server-side storage and a complete audit trail.&lt;br /&gt;
&lt;br /&gt;
== Game Rules ==&lt;br /&gt;
&lt;br /&gt;
=== Core Concept ===&lt;br /&gt;
&lt;br /&gt;
The player creates a vampire and progresses through numbered &#039;&#039;&#039;prompts&#039;&#039;&#039; (story scenarios). Each prompt asks the player to narrate what happens to the vampire during a particular era. The game is non-linear: dice rolls determine which prompt to visit next, and most prompts have multiple &#039;&#039;&#039;variations&#039;&#039;&#039; (A, B, C) to prevent repetition.&lt;br /&gt;
&lt;br /&gt;
=== Character Creation ===&lt;br /&gt;
&lt;br /&gt;
Character creation follows a guided six-step wizard:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Step !! Description&lt;br /&gt;
|-&lt;br /&gt;
| 1. Mortal Life || Establish the vampire&#039;s name, background, and mortal existence&lt;br /&gt;
|-&lt;br /&gt;
| 2. Mortal Characters || Define 2–4 NPCs from the vampire&#039;s human life (friends, mentors, rivals, lovers)&lt;br /&gt;
|-&lt;br /&gt;
| 3. Skills || Choose 2–3 starting skills representing mortal expertise&lt;br /&gt;
|-&lt;br /&gt;
| 4. Resources || Select initial possessions and locations&lt;br /&gt;
|-&lt;br /&gt;
| 5. Combination Experience || Write a pivotal mortal-life experience that ties characters, skills, and resources together&lt;br /&gt;
|-&lt;br /&gt;
| 6. The Turning || Narrate how and why the character was turned into a vampire&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Memory System ===&lt;br /&gt;
&lt;br /&gt;
The memory system is the heart of the game:&lt;br /&gt;
&lt;br /&gt;
* A character may hold a maximum of &#039;&#039;&#039;5 active memories&#039;&#039;&#039; (adjustable by prompt rules and permanent effects).&lt;br /&gt;
* Each memory can contain up to &#039;&#039;&#039;3 experiences&#039;&#039;&#039; (individual narrative entries).&lt;br /&gt;
* When the limit is exceeded, the player must &#039;&#039;&#039;forget&#039;&#039;&#039; (permanently lose) a memory, or &#039;&#039;&#039;move&#039;&#039;&#039; one to a &#039;&#039;&#039;diary&#039;&#039;&#039; (if the character possesses one).&lt;br /&gt;
* A &#039;&#039;&#039;diary&#039;&#039;&#039; is a special resource that stores up to &#039;&#039;&#039;4 additional memories&#039;&#039;&#039; outside the active limit.&lt;br /&gt;
* If the diary resource is lost, all memories stored in it are also lost.&lt;br /&gt;
* Some prompts grant or remove permanent memory slots (&amp;lt;code&amp;gt;memory_slot_gain&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;memory_slot_loss&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
=== Dice Rolling and Prompt Navigation ===&lt;br /&gt;
&lt;br /&gt;
Each turn the player rolls two dice:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;D10&#039;&#039;&#039; (1–10) minus &#039;&#039;&#039;D6&#039;&#039;&#039; (1–6) = result (range: −5 to +9).&lt;br /&gt;
* A &#039;&#039;&#039;positive&#039;&#039;&#039; result moves forward that many prompts.&lt;br /&gt;
* A &#039;&#039;&#039;negative&#039;&#039;&#039; result moves backward.&lt;br /&gt;
* A result of &#039;&#039;&#039;zero&#039;&#039;&#039; stays at the current prompt number but selects the next unused variation.&lt;br /&gt;
&lt;br /&gt;
Variations are visited in order A → B → C. If all variations of a prompt have been used, the system finds the next available variation in prompt order.&lt;br /&gt;
&lt;br /&gt;
=== Turn Structure ===&lt;br /&gt;
&lt;br /&gt;
Each game turn follows this sequence:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Prompt Presentation&#039;&#039;&#039; – The current prompt text and any special rules are displayed.&lt;br /&gt;
# &#039;&#039;&#039;Dice Roll&#039;&#039;&#039; – The player rolls D10 − D6 to determine the next prompt.&lt;br /&gt;
# &#039;&#039;&#039;Experience Creation&#039;&#039;&#039; – The player writes at least one narrative experience for the current prompt.&lt;br /&gt;
# &#039;&#039;&#039;Character Updates&#039;&#039;&#039; – Skills, resources, marks, and known characters may be added, edited, checked, or removed.&lt;br /&gt;
# &#039;&#039;&#039;Memory Management&#039;&#039;&#039; – The player resolves any memory overflow (forget or move to diary).&lt;br /&gt;
# &#039;&#039;&#039;Validation&#039;&#039;&#039; – The system verifies all rules are satisfied (at least one experience added, memory limits respected, etc.).&lt;br /&gt;
# &#039;&#039;&#039;Continue&#039;&#039;&#039; – All pending changes are atomically committed and the game advances to the next prompt.&lt;br /&gt;
&lt;br /&gt;
=== Character Attributes ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Attribute !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Skills&#039;&#039;&#039; || Learned abilities; can be &#039;&#039;normal&#039;&#039; (latent), &#039;&#039;checked&#039;&#039; (used/active), or &#039;&#039;lost&#039;&#039; (permanently removed).&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Resources&#039;&#039;&#039; || Possessions or locations; typed as &#039;&#039;portable&#039;&#039; or &#039;&#039;stationary&#039;&#039;. One resource may be flagged as a &#039;&#039;diary&#039;&#039;.&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Marks&#039;&#039;&#039; || Physical or psychological scars that accumulate over time.&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Known Characters&#039;&#039;&#039; || NPCs the vampire has encountered; typed as &#039;&#039;mortal&#039;&#039; or &#039;&#039;immortal&#039;&#039;; may be lost (dead, vanished, etc.).&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Special Prompt Rules ===&lt;br /&gt;
&lt;br /&gt;
Certain prompts carry special mechanics encoded as rules:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Rule !! Effect&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;no_experience&amp;lt;/code&amp;gt; || Experience creation is disabled for this turn&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;allow_name_change&amp;lt;/code&amp;gt; || The player may change the vampire&#039;s name&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_modification&amp;lt;/code&amp;gt; || The player may edit the text of existing experiences&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slot_loss&amp;lt;/code&amp;gt; || Permanent reduction of memory capacity by 1&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slot_gain&amp;lt;/code&amp;gt; || Permanent increase of memory capacity by 1&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;game_over&amp;lt;/code&amp;gt; || The game ends; the player writes an epilogue&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Game Ending ===&lt;br /&gt;
&lt;br /&gt;
The game ends when the player reaches a prompt with the &amp;lt;code&amp;gt;game_over&amp;lt;/code&amp;gt; rule (or under other conditions defined by the original game). The player writes an &#039;&#039;&#039;epilogue&#039;&#039;&#039;—a final reflection on the vampire&#039;s story—and the character is archived. A statistics summary shows prompt count, memories, experiences, skills, resources, marks, and characters accumulated during the playthrough.&lt;br /&gt;
&lt;br /&gt;
== Architecture ==&lt;br /&gt;
&lt;br /&gt;
=== Technology Stack ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Layer !! Technology !! Version&lt;br /&gt;
|-&lt;br /&gt;
| Backend framework || Django || 5.0.6&lt;br /&gt;
|-&lt;br /&gt;
| REST API || Django REST Framework || 3.15.1&lt;br /&gt;
|-&lt;br /&gt;
| Authentication || Simple JWT (custom) || 5.3.0&lt;br /&gt;
|-&lt;br /&gt;
| CORS || django-cors-headers || 4.3.1&lt;br /&gt;
|-&lt;br /&gt;
| Image handling || Pillow || 11.3.0&lt;br /&gt;
|-&lt;br /&gt;
| Production server || Gunicorn || 21.2.0&lt;br /&gt;
|-&lt;br /&gt;
| Frontend framework || Vue.js || 3.5&lt;br /&gt;
|-&lt;br /&gt;
| State management || Pinia || 3.0&lt;br /&gt;
|-&lt;br /&gt;
| Routing || Vue Router || 4.5&lt;br /&gt;
|-&lt;br /&gt;
| HTTP client || Axios || 1.10&lt;br /&gt;
|-&lt;br /&gt;
| CSS framework || Bootstrap || 5.3&lt;br /&gt;
|-&lt;br /&gt;
| Build tool || Vite || 7.0&lt;br /&gt;
|-&lt;br /&gt;
| Language || TypeScript || 5.8&lt;br /&gt;
|-&lt;br /&gt;
| Testing (backend) || pytest + pytest-django || 8.0 / 4.8&lt;br /&gt;
|-&lt;br /&gt;
| Testing (frontend) || Playwright || 1.53&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== High-Level Diagram ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
┌─────────────────────────────────────────────────┐&lt;br /&gt;
│               Vue 3 SPA (TypeScript)            │&lt;br /&gt;
│  ┌──────────┐ ┌──────────┐ ┌──────────────────┐│&lt;br /&gt;
│  │  Views   │ │Components│ │  Pinia Stores     ││&lt;br /&gt;
│  │ (7 pages)│ │ (14+)    │ │ auth/game/theme/  ││&lt;br /&gt;
│  │          │ │          │ │ characterCreation  ││&lt;br /&gt;
│  └────┬─────┘ └────┬─────┘ └────────┬─────────┘│&lt;br /&gt;
│       └─────────────┴────────────────┘          │&lt;br /&gt;
│                     │ Axios                     │&lt;br /&gt;
│                     ▼                           │&lt;br /&gt;
├─────────────────────────────────────────────────┤&lt;br /&gt;
│              Django REST API                    │&lt;br /&gt;
│  /api/auth/login    /api/auth/verify            │&lt;br /&gt;
│  /api/characters/   (CRUD + game_state,         │&lt;br /&gt;
│   roll_dice, continue_story, audit_trail)       │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/memories/                 │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/experiences/              │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/skills/                   │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/resources/                │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/marks/                    │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/characters/               │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/pending-changes/          │&lt;br /&gt;
│  /api/prompts/                                  │&lt;br /&gt;
├─────────────────────────────────────────────────┤&lt;br /&gt;
│          Django ORM / SQLite                    │&lt;br /&gt;
│  Character · Memory · Experience · Skill        │&lt;br /&gt;
│  Resource · Mark · GameCharacter · Prompt        │&lt;br /&gt;
│  PendingChange · GameStateSnapshot              │&lt;br /&gt;
└─────────────────────────────────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend ==&lt;br /&gt;
&lt;br /&gt;
=== Django Apps ===&lt;br /&gt;
&lt;br /&gt;
The project is organised into three Django apps plus a settings module:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! App !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;vampire&amp;lt;/code&amp;gt; || Core domain models: Character, Memory, Experience, Skill, Resource, Mark, GameCharacter, Prompt, PendingChange, GameStateSnapshot&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;tyov_api&amp;lt;/code&amp;gt; || REST API layer: serialisers, viewsets, URL routing, permissions, middleware&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;authentication&amp;lt;/code&amp;gt; || JWT-based login and token verification endpoints&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;tyov_backend&amp;lt;/code&amp;gt; || Django project settings, URL root, WSGI/ASGI configuration&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Data Models ===&lt;br /&gt;
&lt;br /&gt;
==== Character ====&lt;br /&gt;
&lt;br /&gt;
The central model, owned by a Django &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; via a ForeignKey:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Field !! Type !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt; || CharField(200) || Current name of the vampire&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt; || TextField || Background/appearance description&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;image&amp;lt;/code&amp;gt; || ImageField || Optional character portrait&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;current_prompt&amp;lt;/code&amp;gt; || CharField(10) || ID of the current prompt (e.g. &amp;quot;17a&amp;quot;)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;last_dice_roll&amp;lt;/code&amp;gt; || JSONField || Stores D10, D6, and result of the last roll&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;visited_prompts&amp;lt;/code&amp;gt; || JSONField(list) || List of all prompt IDs visited in order&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slots_lost&amp;lt;/code&amp;gt; || IntegerField || Permanent memory slot reductions&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slots_gained&amp;lt;/code&amp;gt; || IntegerField || Permanent memory slot increases&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;creation_step&amp;lt;/code&amp;gt; || IntegerField(1–6) || Tracks progress through the creation wizard&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;is_creation_complete&amp;lt;/code&amp;gt; || BooleanField || True when all six creation steps are finished&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;creation_data&amp;lt;/code&amp;gt; || JSONField || Temporary storage during creation&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;epilogue&amp;lt;/code&amp;gt; || TextField || Player&#039;s final reflection (set at game end)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;is_completed&amp;lt;/code&amp;gt; || BooleanField || Whether the story has ended&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;completed_at&amp;lt;/code&amp;gt; || DateTimeField || Timestamp of story completion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Memory ====&lt;br /&gt;
&lt;br /&gt;
A memory belongs to a Character and holds up to 3 Experiences:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; – descriptive name (unique per character)&lt;br /&gt;
* &amp;lt;code&amp;gt;in_diary&amp;lt;/code&amp;gt; – whether the memory is stored in the diary&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt; – whether the memory has been forgotten&lt;br /&gt;
&lt;br /&gt;
==== Experience ====&lt;br /&gt;
&lt;br /&gt;
An individual narrative entry within a Memory:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; – optional short description&lt;br /&gt;
* &amp;lt;code&amp;gt;content&amp;lt;/code&amp;gt; – the narrative text&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; – which prompt generated this experience&lt;br /&gt;
* &amp;lt;code&amp;gt;date_info&amp;lt;/code&amp;gt; – temporal context (e.g. &amp;quot;Winter, 431 CE&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
==== Skill ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;status&amp;lt;/code&amp;gt; – one of &amp;lt;code&amp;gt;normal&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;checked&amp;lt;/code&amp;gt;, or &amp;lt;code&amp;gt;lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Resource ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;resource_type&amp;lt;/code&amp;gt; – &amp;lt;code&amp;gt;portable&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;stationary&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;is_diary&amp;lt;/code&amp;gt; – flags the special diary resource (constrained to one active diary per character)&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Mark ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== GameCharacter ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;character_type&amp;lt;/code&amp;gt; – &amp;lt;code&amp;gt;mortal&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;immortal&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;relationship&amp;lt;/code&amp;gt; – e.g. friend, rival, mentor, love, enemy, neutral&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Prompt ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; – e.g. &amp;quot;17a&amp;quot;, &amp;quot;32b&amp;quot;&lt;br /&gt;
* &amp;lt;code&amp;gt;number&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;variation&amp;lt;/code&amp;gt; – parsed components for sorting&lt;br /&gt;
* &amp;lt;code&amp;gt;content&amp;lt;/code&amp;gt; – the scenario text&lt;br /&gt;
* &amp;lt;code&amp;gt;rules&amp;lt;/code&amp;gt; – special mechanics as comma-separated tokens (e.g. &amp;lt;code&amp;gt;no_experience, allow_name_change&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
==== PendingChange ====&lt;br /&gt;
&lt;br /&gt;
Temporary storage for changes before they are atomically committed on &amp;quot;Continue&amp;quot;:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;change_type&amp;lt;/code&amp;gt; – experience, memory, skill, resource, mark, or character&lt;br /&gt;
* &amp;lt;code&amp;gt;change_data&amp;lt;/code&amp;gt; – JSON payload of the change&lt;br /&gt;
&lt;br /&gt;
==== GameStateSnapshot (Audit Trail) ====&lt;br /&gt;
&lt;br /&gt;
Automatically created each turn to record the complete game state:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;turn_number&amp;lt;/code&amp;gt; – sequential, starting from 1&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;next_prompt_id&amp;lt;/code&amp;gt; – transition&lt;br /&gt;
* &amp;lt;code&amp;gt;dice_roll&amp;lt;/code&amp;gt; – raw dice data&lt;br /&gt;
* &amp;lt;code&amp;gt;game_state&amp;lt;/code&amp;gt; – full JSON snapshot of all memories, skills, resources, etc.&lt;br /&gt;
* &amp;lt;code&amp;gt;changes_applied&amp;lt;/code&amp;gt; – JSON array of pending changes that were committed&lt;br /&gt;
* &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; – timestamp&lt;br /&gt;
&lt;br /&gt;
=== API Endpoints ===&lt;br /&gt;
&lt;br /&gt;
All endpoints require JWT authentication (except login).&lt;br /&gt;
&lt;br /&gt;
==== Authentication ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/auth/login/&amp;lt;/code&amp;gt; || Authenticate with username/password; returns JWT token&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/auth/verify/&amp;lt;/code&amp;gt; || Verify current token and return user info&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Login is rate-limited to 5 requests per minute per IP.&lt;br /&gt;
&lt;br /&gt;
==== Characters ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/&amp;lt;/code&amp;gt; || List all characters for the authenticated user&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/&amp;lt;/code&amp;gt; || Create a new character&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Retrieve full character detail (prefetched relations)&lt;br /&gt;
|-&lt;br /&gt;
| PUT/PATCH || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Update character fields&lt;br /&gt;
|-&lt;br /&gt;
| DELETE || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Delete a character&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/game_state/&amp;lt;/code&amp;gt; || Complete game state including prompt, validation, dice info&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/{id}/roll_dice/&amp;lt;/code&amp;gt; || Roll D10 − D6 and store the result&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/{id}/continue_story/&amp;lt;/code&amp;gt; || Validate, commit pending changes, advance to next prompt&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/audit_trail/&amp;lt;/code&amp;gt; || View turn-by-turn audit history&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/game-state-changes/&amp;lt;/code&amp;gt; || Timeline of state changes between turns&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Nested Character Resources ====&lt;br /&gt;
&lt;br /&gt;
For each character, full CRUD is available on:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/memories/&amp;lt;/code&amp;gt; (plus &amp;lt;code&amp;gt;move_to_diary&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;restore_lost_memory&amp;lt;/code&amp;gt; actions)&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/experiences/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/skills/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/resources/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/marks/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/characters/&amp;lt;/code&amp;gt; (known NPCs)&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/pending-changes/&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Prompts ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/prompts/&amp;lt;/code&amp;gt; || List all game prompts&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/prompts/{prompt_id}/&amp;lt;/code&amp;gt; || Retrieve a single prompt by ID&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Permissions and Security ===&lt;br /&gt;
&lt;br /&gt;
* All character data is scoped to the authenticated user via &amp;lt;code&amp;gt;IsCharacterOwner&amp;lt;/code&amp;gt; permission.&lt;br /&gt;
* JWT tokens are sent as &amp;lt;code&amp;gt;Authorization: Bearer {token}&amp;lt;/code&amp;gt; headers.&lt;br /&gt;
* The login endpoint is throttled (5/minute) to prevent brute-force attacks.&lt;br /&gt;
* All pending changes are processed inside a database &#039;&#039;&#039;transaction&#039;&#039;&#039; to guarantee atomicity.&lt;br /&gt;
* CORS headers are configured via &amp;lt;code&amp;gt;django-cors-headers&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Frontend ==&lt;br /&gt;
&lt;br /&gt;
=== Views (Pages) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! View !! Route !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;LoginView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; || Authentication form; redirects authenticated users to home&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;HomeView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt; || Dashboard showing all active characters in a card grid&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharacterCreationView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/character-creation/:id&amp;lt;/code&amp;gt; || Six-step creation wizard&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/game/:characterId&amp;lt;/code&amp;gt; || Main game loop: prompt display, experience form, dice rolling, entity management, memory display&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameEndedView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/game/:characterId/ended&amp;lt;/code&amp;gt; || End-of-story screen with statistics and epilogue&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;RecordsView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/records&amp;lt;/code&amp;gt; || Archive of current and completed characters&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;StoryView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/story/:characterId&amp;lt;/code&amp;gt; || Narrative timeline with experience history, character stats sidebar, and epilogue&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
All routes except &amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; require authentication. The router guard redirects unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
=== Key Components ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Component !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PromptSection&#039;&#039;&#039; || Displays the current prompt text, special rules, and dice roll results&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ExperienceForm&#039;&#039;&#039; || Form for writing new experiences with title, date, content, and memory selection&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;MemoriesDisplay&#039;&#039;&#039; || Shows active, diary, and lost memories with experience counts; includes move-to-diary, delete, and restore actions&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameDataSection&#039;&#039;&#039; || Reusable list for skills, resources, or marks with add/edit/delete controls&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharactersSection&#039;&#039;&#039; || Grid of known NPCs with type badges and edit/delete&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;EntityCreationWindow&#039;&#039;&#039; || Draggable modal for creating/editing skills, resources, marks&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharacterCreationWindow&#039;&#039;&#039; || Draggable modal for creating/editing NPC characters&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ExperienceEditWindow&#039;&#039;&#039; || Window for editing existing experience text&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;DiaryCreationModal&#039;&#039;&#039; || Modal for creating a diary resource and moving a memory into it&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ConfirmationDialog&#039;&#039;&#039; || Generic confirmation dialog for destructive actions&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ValidationWarning&#039;&#039;&#039; || Displays validation errors when game rules are not satisfied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== State Management (Pinia) ===&lt;br /&gt;
&lt;br /&gt;
Four Pinia stores manage application state:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Store !! Responsibility&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;auth&#039;&#039;&#039; || JWT token, user object, login/logout, token expiry handling&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;game&#039;&#039;&#039; || Characters list, current character, game state, all CRUD actions for entities, dice rolling, story continuation&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;characterCreation&#039;&#039;&#039; || Multi-step wizard data, initial character creation&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;theme&#039;&#039;&#039; || Dark/light mode toggle with localStorage persistence&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== API Service Layer ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;api.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Axios instance with base URL auto-detection (development vs. production), request interceptor for JWT headers, response interceptor for 401/token-expiry handling.&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;gameService.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – All API call methods: authentication, character CRUD, dice rolling, story continuation, memory/experience/skill/resource/mark/character management.&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;entityService.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Generic &amp;lt;code&amp;gt;EntityService&amp;lt;/code&amp;gt; class instantiated for skills, resources, marks, and characters for DRY CRUD operations.&lt;br /&gt;
&lt;br /&gt;
=== Composables ===&lt;br /&gt;
&lt;br /&gt;
Vue composables encapsulate reusable business logic:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useGameData()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Computed properties derived from the game store (current character, memories, skills, resources, etc.)&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useExperienceForm()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Form state, validation, memory selection, and reset logic&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useEntityManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Generic CRUD operations with optional confirmation dialogs&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useSkillManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useResourceManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useMarkManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useMemoryManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Specialised wrappers&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useConfirmationDialog()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Promise-based modal confirmation&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useMemoryDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Display logic for memories including pending-change awareness&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useExperienceDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Markdown rendering for experience content&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;usePendingChangesDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Badge rendering for undo-able pending changes&lt;br /&gt;
&lt;br /&gt;
== Audit Trail ==&lt;br /&gt;
&lt;br /&gt;
The application includes an automatic audit trail that captures the complete game state every time the player advances to a new prompt.&lt;br /&gt;
&lt;br /&gt;
Each &#039;&#039;&#039;GameStateSnapshot&#039;&#039;&#039; records:&lt;br /&gt;
&lt;br /&gt;
* The turn number (sequential from 1)&lt;br /&gt;
* The prompt transition (e.g. 36a → 34a)&lt;br /&gt;
* The dice roll&lt;br /&gt;
* A complete JSON snapshot of all memories, skills, resources, marks, and known characters&lt;br /&gt;
* All pending changes that were committed&lt;br /&gt;
&lt;br /&gt;
The audit trail is accessible via:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;API&#039;&#039;&#039;: &amp;lt;code&amp;gt;GET /api/characters/{id}/audit_trail/&amp;lt;/code&amp;gt; with optional &amp;lt;code&amp;gt;?turn=N&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;?limit=N&amp;lt;/code&amp;gt; parameters&lt;br /&gt;
* &#039;&#039;&#039;Management command&#039;&#039;&#039;: &amp;lt;code&amp;gt;python manage.py view_audit_trail {id} [--summary] [--turn N] [--export file.json]&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;Timeline API&#039;&#039;&#039;: &amp;lt;code&amp;gt;GET /api/characters/{id}/game-state-changes/&amp;lt;/code&amp;gt; for a structured timeline of all state changes&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
=== Local Development ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
.\startup.ps1&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This script creates a virtual environment, installs dependencies, runs migrations, and starts both the Django backend (&amp;lt;code&amp;gt;http://localhost:8000&amp;lt;/code&amp;gt;) and the Vue dev server (&amp;lt;code&amp;gt;http://localhost:5173&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
=== Production ===&lt;br /&gt;
&lt;br /&gt;
* Set &amp;lt;code&amp;gt;DJANGO_ENV=production&amp;lt;/code&amp;gt;&lt;br /&gt;
* Build the frontend: &amp;lt;code&amp;gt;cd frontend &amp;amp;&amp;amp; npm run build&amp;lt;/code&amp;gt;&lt;br /&gt;
* Serve with Gunicorn behind a reverse proxy&lt;br /&gt;
* Static files are collected into the &amp;lt;code&amp;gt;static/&amp;lt;/code&amp;gt; directory&lt;br /&gt;
* The production frontend points to &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Example: A Finished Character ==&lt;br /&gt;
&lt;br /&gt;
The following is an example of a character from an actual playthrough stored in the application database. It demonstrates how the game&#039;s mechanics play out over an extended narrative.&lt;br /&gt;
&lt;br /&gt;
=== Karmiš (formerly Ekurzu / Naram) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Description:&#039;&#039;&#039; Naram was a temple scribe in the ancient city of Mari: meticulous, observant, and deeply entangled in the political and spiritual life of the city. His life was shaped by clay tablets, omens, and whispered secrets carried through palace corridors and temple courtyards. Behind his calm demeanor lies a mind constantly at work—recording, interpreting, and surviving.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Prompts visited:&#039;&#039;&#039; 34 prompts across variations, including: 1a, 4a, 11a–c, 12a, 17a, 20a, 24a, 25a, 32a–b, 34a–36a, 37a–41c, 43a–c, 47a, 52a, 57a, 64a, 66a, 68a, 71a, 73a.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Final prompt:&#039;&#039;&#039; 73a&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory slots lost:&#039;&#039;&#039; 1 · &#039;&#039;&#039;Memory slots gained:&#039;&#039;&#039; 1 (net effect: standard 5-memory limit)&lt;br /&gt;
&lt;br /&gt;
==== Epilogue ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;blockquote&amp;gt;&lt;br /&gt;
He has not died. Let me say that plainly. There will be no grave. He is alive, but quietly, deliberately, and utterly outside your reach.&lt;br /&gt;
&lt;br /&gt;
When I first met him, I did not understand what he was. He wore his years like dust on old vellum—thin, barely visible, but ever present. [...] Over time, I became his mirror, then his partner, and finally his historian.&lt;br /&gt;
&lt;br /&gt;
But someone has to begin the telling. Someone has to place the first stone. So this book begins in a house of quiet gardens and low ceilings, where he writes in the mornings and feeds only when he must. [...]&lt;br /&gt;
&lt;br /&gt;
Just a man who remembered too much, for too long. And chose, in the end, peace.&lt;br /&gt;
&lt;br /&gt;
— &#039;&#039;Mirelde&#039;&#039;&lt;br /&gt;
&amp;lt;/blockquote&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== A Life in Summary ====&lt;br /&gt;
&lt;br /&gt;
Karmiš&#039;s story spans roughly three and a half thousand years. It begins in Bronze Age Mesopotamia, where the mortal scribe Naram serves in the temple of Ishtar at Mari. He writes omens, navigatesg palace intrigue, and fals in love with a woman named Hessa who moves through the court &amp;quot;like smoke.&amp;quot; His mentor, the High Priest Ibbi-Zamri, is the first person he kills after being turned by the ancient vampire Ashurban the Veiled.&lt;br /&gt;
&lt;br /&gt;
The early centuries are defined by guilt and exile. Fleeing with his mortal companion Ennatum, the newly-made vampire (now calling himself Ekurzu) struggles to control his hunger. He builds walls and plays flutes in the wilderness, but it is &amp;quot;never penance, only a cage with softer bars.&amp;quot; Eventually the hunger wins: he devours Ennatum during a sandstorm and spends the following decades alone.&lt;br /&gt;
&lt;br /&gt;
The middle game carries the character through the great civilisations of antiquity: Amarna under Akhenaten, the Achaemenid Royal Road, the libraries of Alexandria—always in the role of scribe, forger, or quiet observer. He accumulates skills like &#039;&#039;Silent Cartography&#039;&#039; (&amp;quot;I trace the unseen paths between people, places, and power&amp;quot;) and &#039;&#039;Ash-Tongue&#039;&#039; (&amp;quot;I speak in soot and suggestion, in the cracks between laws&amp;quot;), but loses others as the centuries erode his identity: the ability to &#039;&#039;Decipher Ancient Texts&#039;&#039; and even &#039;&#039;I Control the Beast&#039;&#039; both slip away.&lt;br /&gt;
&lt;br /&gt;
A pivotal relationship develops with the scholar Thoöni, who recognises the name &#039;&#039;Ekurzu&#039;&#039; not as myth but as continuity. Later, after Thoöni&#039;s mortal death, her spectral echo begins appearing in the margins of his diary, scratching fragments in languages he can no longer read.&lt;br /&gt;
&lt;br /&gt;
By the late Roman period Karmiš has lost so many memories that he can barely hold a conversation. The game&#039;s memory mechanics enforce this viscerally: eight of his sixteen memories were permanently forgotten, including &#039;&#039;The Turning&#039;&#039; itself; the vampire no longer remembers how he was made. He compensates by keeping diaries, but even those are lost: &#039;&#039;The Walls of Forgetting&#039;&#039; (memory carvings in five scripts beneath a shrine near Carchemish) and &#039;&#039;The Monastery Diary&#039;&#039; both vanish, taking their stored memories with them.&lt;br /&gt;
&lt;br /&gt;
The final act takes place in early modern Europe. Karmiš witnesses the Atlantic slave trade first-hand and begins compiling &#039;&#039;The Margins of Manifest&#039;&#039;, a cipher-folio of names taken from shipping manifests and punishment ledgers. His response to prompt 66a captures the shift:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;blockquote&amp;gt;&lt;br /&gt;
&#039;&#039;It is no longer a question of restraint. That was a younger version of me, one who believed self-control made me different from the others. Now I am different in purpose, not method. I feed from slavers and call it justice, but the blood tastes the same.&#039;&#039;&lt;br /&gt;
&amp;lt;/blockquote&amp;gt;&lt;br /&gt;
&lt;br /&gt;
His companion in the final centuries is Mirelde of Bracha. She is an immortal who becomes his mirror, partner, and ultimately his historian. Together they rediscover a clay tablet bearing his original name in a Venetian bookseller&#039;s back room (prompt 68a, 1704 CE), and in a ruined estate outside Smyrna Mirelde finds the remains of a life Karmiš can no longer remember.&lt;br /&gt;
&lt;br /&gt;
At the story&#039;s end Karmiš retains only five active memories, three diary memories, two marks (&#039;&#039;The Unblinking Eye&#039;&#039;, a scar that burns in the presence of lies and &#039;&#039;The Gesture Forbidden&#039;&#039;, an involuntary court gesture mistaken for mockery), and three possessions: his diary &#039;&#039;Kept Against Forgetting&#039;&#039;, the clay tablet bearing his name, and the slave-trade folio. Of the eleven characters he encountered, only four remain: Ashurban (his distant maker), Mirelde (his companion), Thoöni&#039;s ghost, and a nameless figure known only as &#039;&#039;The One Who Does Not Answer&#039;&#039;. Over 34 prompts he accumulated 37 experiences, gained and lost a memory slot each, and burned through three separate diaries.&lt;br /&gt;
&lt;br /&gt;
The character&#039;s story is narrated in the epilogue not by Karmiš himself but by Mirelde, who insists: &amp;quot;He has not died. [...] Just a man who remembered too much, for too long. And chose, in the end, &#039;&#039;peace&#039;&#039;.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
Tests are run with &#039;&#039;&#039;pytest&#039;&#039;&#039; and &#039;&#039;&#039;pytest-django&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
pytest&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Test files include:&lt;br /&gt;
* &amp;lt;code&amp;gt;vampire/test_models.py&amp;lt;/code&amp;gt; – model unit tests&lt;br /&gt;
* &amp;lt;code&amp;gt;tyov_api/test_api.py&amp;lt;/code&amp;gt; – API endpoint integration tests&lt;br /&gt;
* &amp;lt;code&amp;gt;tyov_api/test_models.py&amp;lt;/code&amp;gt; – API model tests&lt;br /&gt;
* &amp;lt;code&amp;gt;authentication/test_views.py&amp;lt;/code&amp;gt; – authentication tests&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
End-to-end tests use &#039;&#039;&#039;Playwright&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
cd frontend&lt;br /&gt;
npx playwright test&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Test specs include:&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/character-creation-workflow.spec.ts&amp;lt;/code&amp;gt; – character creation wizard&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/tyov-game.spec.ts&amp;lt;/code&amp;gt; – game loop testing&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/vue.spec.ts&amp;lt;/code&amp;gt; – general Vue component tests&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings – the original tabletop game&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Vue.js]]&lt;br /&gt;
* [[Single-page application]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Web applications]]&lt;br /&gt;
[[Category:Role-playing video games]]&lt;br /&gt;
[[Category:Django (web framework) applications]]&lt;br /&gt;
[[Category:Vue.js applications]]&lt;br /&gt;
[[Category:Solo role-playing games]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=388</id>
		<title>Thousand Year Old Vampire</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=388"/>
		<updated>2026-04-12T11:22:40Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Epilogue */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox &lt;br /&gt;
| 01_name = Thousand Year Old Vampire – Web Helper&lt;br /&gt;
| 02_logo =&lt;br /&gt;
| 03_developer = Michel Vuijlsteke&lt;br /&gt;
| 04_programming language = Python (Django 5), TypeScript (Vue 3)&lt;br /&gt;
| 05_operating system = Cross-platform (Web)&lt;br /&gt;
| 06_genre = Solo RPG Digital Companion&lt;br /&gt;
| 07_license = custom&lt;br /&gt;
| 08_website = &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Thousand Year Old Vampire – Web Helper&#039;&#039;&#039; (&#039;&#039;&#039;TYOV-Web&#039;&#039;&#039;) is a modern web-based implementation of the solo tabletop role-playing game &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings. The application digitises the game&#039;s complex mechanics of memory, experience, and character development, allowing players to create and guide a vampire character across centuries of unlife through an interactive browser interface. The backend is built with &#039;&#039;&#039;Django 5&#039;&#039;&#039; and &#039;&#039;&#039;Django REST Framework&#039;&#039;&#039;; the frontend is a &#039;&#039;&#039;Vue 3&#039;&#039;&#039; single-page application.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Thousand Year Old Vampire&#039;&#039; is a solo storytelling RPG in which a player creates a vampire character and guides it through an increasingly fragmented existence spanning hundreds or thousands of years. The central tension lies in the vampire&#039;s deteriorating memory: as an immortal being it accumulates experiences, but its ancient mind can only retain a limited number of memories at any time. Players must constantly choose what to remember and what to forget.&lt;br /&gt;
&lt;br /&gt;
TYOV-Web fully implements the game&#039;s rules—character creation, dice-driven prompt navigation, memory management, diary storage, skills, resources, marks, known characters, and game-ending conditions—in a reactive web interface with persistent server-side storage and a complete audit trail.&lt;br /&gt;
&lt;br /&gt;
== Game Rules ==&lt;br /&gt;
&lt;br /&gt;
=== Core Concept ===&lt;br /&gt;
&lt;br /&gt;
The player creates a vampire and progresses through numbered &#039;&#039;&#039;prompts&#039;&#039;&#039; (story scenarios). Each prompt asks the player to narrate what happens to the vampire during a particular era. The game is non-linear: dice rolls determine which prompt to visit next, and most prompts have multiple &#039;&#039;&#039;variations&#039;&#039;&#039; (A, B, C) to prevent repetition.&lt;br /&gt;
&lt;br /&gt;
=== Character Creation ===&lt;br /&gt;
&lt;br /&gt;
Character creation follows a guided six-step wizard:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Step !! Description&lt;br /&gt;
|-&lt;br /&gt;
| 1. Mortal Life || Establish the vampire&#039;s name, background, and mortal existence&lt;br /&gt;
|-&lt;br /&gt;
| 2. Mortal Characters || Define 2–4 NPCs from the vampire&#039;s human life (friends, mentors, rivals, lovers)&lt;br /&gt;
|-&lt;br /&gt;
| 3. Skills || Choose 2–3 starting skills representing mortal expertise&lt;br /&gt;
|-&lt;br /&gt;
| 4. Resources || Select initial possessions and locations&lt;br /&gt;
|-&lt;br /&gt;
| 5. Combination Experience || Write a pivotal mortal-life experience that ties characters, skills, and resources together&lt;br /&gt;
|-&lt;br /&gt;
| 6. The Turning || Narrate how and why the character was turned into a vampire&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Memory System ===&lt;br /&gt;
&lt;br /&gt;
The memory system is the heart of the game:&lt;br /&gt;
&lt;br /&gt;
* A character may hold a maximum of &#039;&#039;&#039;5 active memories&#039;&#039;&#039; (adjustable by prompt rules and permanent effects).&lt;br /&gt;
* Each memory can contain up to &#039;&#039;&#039;3 experiences&#039;&#039;&#039; (individual narrative entries).&lt;br /&gt;
* When the limit is exceeded, the player must &#039;&#039;&#039;forget&#039;&#039;&#039; (permanently lose) a memory, or &#039;&#039;&#039;move&#039;&#039;&#039; one to a &#039;&#039;&#039;diary&#039;&#039;&#039; (if the character possesses one).&lt;br /&gt;
* A &#039;&#039;&#039;diary&#039;&#039;&#039; is a special resource that stores up to &#039;&#039;&#039;4 additional memories&#039;&#039;&#039; outside the active limit.&lt;br /&gt;
* If the diary resource is lost, all memories stored in it are also lost.&lt;br /&gt;
* Some prompts grant or remove permanent memory slots (&amp;lt;code&amp;gt;memory_slot_gain&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;memory_slot_loss&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
=== Dice Rolling and Prompt Navigation ===&lt;br /&gt;
&lt;br /&gt;
Each turn the player rolls two dice:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;D10&#039;&#039;&#039; (1–10) minus &#039;&#039;&#039;D6&#039;&#039;&#039; (1–6) = result (range: −5 to +9).&lt;br /&gt;
* A &#039;&#039;&#039;positive&#039;&#039;&#039; result moves forward that many prompts.&lt;br /&gt;
* A &#039;&#039;&#039;negative&#039;&#039;&#039; result moves backward.&lt;br /&gt;
* A result of &#039;&#039;&#039;zero&#039;&#039;&#039; stays at the current prompt number but selects the next unused variation.&lt;br /&gt;
&lt;br /&gt;
Variations are visited in order A → B → C. If all variations of a prompt have been used, the system finds the next available variation in prompt order.&lt;br /&gt;
&lt;br /&gt;
=== Turn Structure ===&lt;br /&gt;
&lt;br /&gt;
Each game turn follows this sequence:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Prompt Presentation&#039;&#039;&#039; – The current prompt text and any special rules are displayed.&lt;br /&gt;
# &#039;&#039;&#039;Dice Roll&#039;&#039;&#039; – The player rolls D10 − D6 to determine the next prompt.&lt;br /&gt;
# &#039;&#039;&#039;Experience Creation&#039;&#039;&#039; – The player writes at least one narrative experience for the current prompt.&lt;br /&gt;
# &#039;&#039;&#039;Character Updates&#039;&#039;&#039; – Skills, resources, marks, and known characters may be added, edited, checked, or removed.&lt;br /&gt;
# &#039;&#039;&#039;Memory Management&#039;&#039;&#039; – The player resolves any memory overflow (forget or move to diary).&lt;br /&gt;
# &#039;&#039;&#039;Validation&#039;&#039;&#039; – The system verifies all rules are satisfied (at least one experience added, memory limits respected, etc.).&lt;br /&gt;
# &#039;&#039;&#039;Continue&#039;&#039;&#039; – All pending changes are atomically committed and the game advances to the next prompt.&lt;br /&gt;
&lt;br /&gt;
=== Character Attributes ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Attribute !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Skills&#039;&#039;&#039; || Learned abilities; can be &#039;&#039;normal&#039;&#039; (latent), &#039;&#039;checked&#039;&#039; (used/active), or &#039;&#039;lost&#039;&#039; (permanently removed).&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Resources&#039;&#039;&#039; || Possessions or locations; typed as &#039;&#039;portable&#039;&#039; or &#039;&#039;stationary&#039;&#039;. One resource may be flagged as a &#039;&#039;diary&#039;&#039;.&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Marks&#039;&#039;&#039; || Physical or psychological scars that accumulate over time.&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Known Characters&#039;&#039;&#039; || NPCs the vampire has encountered; typed as &#039;&#039;mortal&#039;&#039; or &#039;&#039;immortal&#039;&#039;; may be lost (dead, vanished, etc.).&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Special Prompt Rules ===&lt;br /&gt;
&lt;br /&gt;
Certain prompts carry special mechanics encoded as rules:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Rule !! Effect&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;no_experience&amp;lt;/code&amp;gt; || Experience creation is disabled for this turn&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;allow_name_change&amp;lt;/code&amp;gt; || The player may change the vampire&#039;s name&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_modification&amp;lt;/code&amp;gt; || The player may edit the text of existing experiences&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slot_loss&amp;lt;/code&amp;gt; || Permanent reduction of memory capacity by 1&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slot_gain&amp;lt;/code&amp;gt; || Permanent increase of memory capacity by 1&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;game_over&amp;lt;/code&amp;gt; || The game ends; the player writes an epilogue&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Game Ending ===&lt;br /&gt;
&lt;br /&gt;
The game ends when the player reaches a prompt with the &amp;lt;code&amp;gt;game_over&amp;lt;/code&amp;gt; rule (or under other conditions defined by the original game). The player writes an &#039;&#039;&#039;epilogue&#039;&#039;&#039;—a final reflection on the vampire&#039;s story—and the character is archived. A statistics summary shows prompt count, memories, experiences, skills, resources, marks, and characters accumulated during the playthrough.&lt;br /&gt;
&lt;br /&gt;
== Architecture ==&lt;br /&gt;
&lt;br /&gt;
=== Technology Stack ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Layer !! Technology !! Version&lt;br /&gt;
|-&lt;br /&gt;
| Backend framework || Django || 5.0.6&lt;br /&gt;
|-&lt;br /&gt;
| REST API || Django REST Framework || 3.15.1&lt;br /&gt;
|-&lt;br /&gt;
| Authentication || Simple JWT (custom) || 5.3.0&lt;br /&gt;
|-&lt;br /&gt;
| CORS || django-cors-headers || 4.3.1&lt;br /&gt;
|-&lt;br /&gt;
| Image handling || Pillow || 11.3.0&lt;br /&gt;
|-&lt;br /&gt;
| Production server || Gunicorn || 21.2.0&lt;br /&gt;
|-&lt;br /&gt;
| Frontend framework || Vue.js || 3.5&lt;br /&gt;
|-&lt;br /&gt;
| State management || Pinia || 3.0&lt;br /&gt;
|-&lt;br /&gt;
| Routing || Vue Router || 4.5&lt;br /&gt;
|-&lt;br /&gt;
| HTTP client || Axios || 1.10&lt;br /&gt;
|-&lt;br /&gt;
| CSS framework || Bootstrap || 5.3&lt;br /&gt;
|-&lt;br /&gt;
| Build tool || Vite || 7.0&lt;br /&gt;
|-&lt;br /&gt;
| Language || TypeScript || 5.8&lt;br /&gt;
|-&lt;br /&gt;
| Testing (backend) || pytest + pytest-django || 8.0 / 4.8&lt;br /&gt;
|-&lt;br /&gt;
| Testing (frontend) || Playwright || 1.53&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== High-Level Diagram ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
┌─────────────────────────────────────────────────┐&lt;br /&gt;
│               Vue 3 SPA (TypeScript)            │&lt;br /&gt;
│  ┌──────────┐ ┌──────────┐ ┌──────────────────┐│&lt;br /&gt;
│  │  Views   │ │Components│ │  Pinia Stores     ││&lt;br /&gt;
│  │ (7 pages)│ │ (14+)    │ │ auth/game/theme/  ││&lt;br /&gt;
│  │          │ │          │ │ characterCreation  ││&lt;br /&gt;
│  └────┬─────┘ └────┬─────┘ └────────┬─────────┘│&lt;br /&gt;
│       └─────────────┴────────────────┘          │&lt;br /&gt;
│                     │ Axios                     │&lt;br /&gt;
│                     ▼                           │&lt;br /&gt;
├─────────────────────────────────────────────────┤&lt;br /&gt;
│              Django REST API                    │&lt;br /&gt;
│  /api/auth/login    /api/auth/verify            │&lt;br /&gt;
│  /api/characters/   (CRUD + game_state,         │&lt;br /&gt;
│   roll_dice, continue_story, audit_trail)       │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/memories/                 │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/experiences/              │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/skills/                   │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/resources/                │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/marks/                    │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/characters/               │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/pending-changes/          │&lt;br /&gt;
│  /api/prompts/                                  │&lt;br /&gt;
├─────────────────────────────────────────────────┤&lt;br /&gt;
│          Django ORM / SQLite                    │&lt;br /&gt;
│  Character · Memory · Experience · Skill        │&lt;br /&gt;
│  Resource · Mark · GameCharacter · Prompt        │&lt;br /&gt;
│  PendingChange · GameStateSnapshot              │&lt;br /&gt;
└─────────────────────────────────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend ==&lt;br /&gt;
&lt;br /&gt;
=== Django Apps ===&lt;br /&gt;
&lt;br /&gt;
The project is organised into three Django apps plus a settings module:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! App !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;vampire&amp;lt;/code&amp;gt; || Core domain models: Character, Memory, Experience, Skill, Resource, Mark, GameCharacter, Prompt, PendingChange, GameStateSnapshot&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;tyov_api&amp;lt;/code&amp;gt; || REST API layer: serialisers, viewsets, URL routing, permissions, middleware&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;authentication&amp;lt;/code&amp;gt; || JWT-based login and token verification endpoints&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;tyov_backend&amp;lt;/code&amp;gt; || Django project settings, URL root, WSGI/ASGI configuration&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Data Models ===&lt;br /&gt;
&lt;br /&gt;
==== Character ====&lt;br /&gt;
&lt;br /&gt;
The central model, owned by a Django &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; via a ForeignKey:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Field !! Type !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt; || CharField(200) || Current name of the vampire&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt; || TextField || Background/appearance description&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;image&amp;lt;/code&amp;gt; || ImageField || Optional character portrait&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;current_prompt&amp;lt;/code&amp;gt; || CharField(10) || ID of the current prompt (e.g. &amp;quot;17a&amp;quot;)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;last_dice_roll&amp;lt;/code&amp;gt; || JSONField || Stores D10, D6, and result of the last roll&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;visited_prompts&amp;lt;/code&amp;gt; || JSONField(list) || List of all prompt IDs visited in order&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slots_lost&amp;lt;/code&amp;gt; || IntegerField || Permanent memory slot reductions&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slots_gained&amp;lt;/code&amp;gt; || IntegerField || Permanent memory slot increases&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;creation_step&amp;lt;/code&amp;gt; || IntegerField(1–6) || Tracks progress through the creation wizard&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;is_creation_complete&amp;lt;/code&amp;gt; || BooleanField || True when all six creation steps are finished&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;creation_data&amp;lt;/code&amp;gt; || JSONField || Temporary storage during creation&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;epilogue&amp;lt;/code&amp;gt; || TextField || Player&#039;s final reflection (set at game end)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;is_completed&amp;lt;/code&amp;gt; || BooleanField || Whether the story has ended&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;completed_at&amp;lt;/code&amp;gt; || DateTimeField || Timestamp of story completion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Memory ====&lt;br /&gt;
&lt;br /&gt;
A memory belongs to a Character and holds up to 3 Experiences:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; – descriptive name (unique per character)&lt;br /&gt;
* &amp;lt;code&amp;gt;in_diary&amp;lt;/code&amp;gt; – whether the memory is stored in the diary&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt; – whether the memory has been forgotten&lt;br /&gt;
&lt;br /&gt;
==== Experience ====&lt;br /&gt;
&lt;br /&gt;
An individual narrative entry within a Memory:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; – optional short description&lt;br /&gt;
* &amp;lt;code&amp;gt;content&amp;lt;/code&amp;gt; – the narrative text&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; – which prompt generated this experience&lt;br /&gt;
* &amp;lt;code&amp;gt;date_info&amp;lt;/code&amp;gt; – temporal context (e.g. &amp;quot;Winter, 431 CE&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
==== Skill ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;status&amp;lt;/code&amp;gt; – one of &amp;lt;code&amp;gt;normal&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;checked&amp;lt;/code&amp;gt;, or &amp;lt;code&amp;gt;lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Resource ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;resource_type&amp;lt;/code&amp;gt; – &amp;lt;code&amp;gt;portable&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;stationary&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;is_diary&amp;lt;/code&amp;gt; – flags the special diary resource (constrained to one active diary per character)&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Mark ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== GameCharacter ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;character_type&amp;lt;/code&amp;gt; – &amp;lt;code&amp;gt;mortal&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;immortal&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;relationship&amp;lt;/code&amp;gt; – e.g. friend, rival, mentor, love, enemy, neutral&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Prompt ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; – e.g. &amp;quot;17a&amp;quot;, &amp;quot;32b&amp;quot;&lt;br /&gt;
* &amp;lt;code&amp;gt;number&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;variation&amp;lt;/code&amp;gt; – parsed components for sorting&lt;br /&gt;
* &amp;lt;code&amp;gt;content&amp;lt;/code&amp;gt; – the scenario text&lt;br /&gt;
* &amp;lt;code&amp;gt;rules&amp;lt;/code&amp;gt; – special mechanics as comma-separated tokens (e.g. &amp;lt;code&amp;gt;no_experience, allow_name_change&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
==== PendingChange ====&lt;br /&gt;
&lt;br /&gt;
Temporary storage for changes before they are atomically committed on &amp;quot;Continue&amp;quot;:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;change_type&amp;lt;/code&amp;gt; – experience, memory, skill, resource, mark, or character&lt;br /&gt;
* &amp;lt;code&amp;gt;change_data&amp;lt;/code&amp;gt; – JSON payload of the change&lt;br /&gt;
&lt;br /&gt;
==== GameStateSnapshot (Audit Trail) ====&lt;br /&gt;
&lt;br /&gt;
Automatically created each turn to record the complete game state:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;turn_number&amp;lt;/code&amp;gt; – sequential, starting from 1&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;next_prompt_id&amp;lt;/code&amp;gt; – transition&lt;br /&gt;
* &amp;lt;code&amp;gt;dice_roll&amp;lt;/code&amp;gt; – raw dice data&lt;br /&gt;
* &amp;lt;code&amp;gt;game_state&amp;lt;/code&amp;gt; – full JSON snapshot of all memories, skills, resources, etc.&lt;br /&gt;
* &amp;lt;code&amp;gt;changes_applied&amp;lt;/code&amp;gt; – JSON array of pending changes that were committed&lt;br /&gt;
* &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; – timestamp&lt;br /&gt;
&lt;br /&gt;
=== API Endpoints ===&lt;br /&gt;
&lt;br /&gt;
All endpoints require JWT authentication (except login).&lt;br /&gt;
&lt;br /&gt;
==== Authentication ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/auth/login/&amp;lt;/code&amp;gt; || Authenticate with username/password; returns JWT token&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/auth/verify/&amp;lt;/code&amp;gt; || Verify current token and return user info&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Login is rate-limited to 5 requests per minute per IP.&lt;br /&gt;
&lt;br /&gt;
==== Characters ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/&amp;lt;/code&amp;gt; || List all characters for the authenticated user&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/&amp;lt;/code&amp;gt; || Create a new character&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Retrieve full character detail (prefetched relations)&lt;br /&gt;
|-&lt;br /&gt;
| PUT/PATCH || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Update character fields&lt;br /&gt;
|-&lt;br /&gt;
| DELETE || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Delete a character&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/game_state/&amp;lt;/code&amp;gt; || Complete game state including prompt, validation, dice info&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/{id}/roll_dice/&amp;lt;/code&amp;gt; || Roll D10 − D6 and store the result&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/{id}/continue_story/&amp;lt;/code&amp;gt; || Validate, commit pending changes, advance to next prompt&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/audit_trail/&amp;lt;/code&amp;gt; || View turn-by-turn audit history&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/game-state-changes/&amp;lt;/code&amp;gt; || Timeline of state changes between turns&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Nested Character Resources ====&lt;br /&gt;
&lt;br /&gt;
For each character, full CRUD is available on:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/memories/&amp;lt;/code&amp;gt; (plus &amp;lt;code&amp;gt;move_to_diary&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;restore_lost_memory&amp;lt;/code&amp;gt; actions)&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/experiences/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/skills/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/resources/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/marks/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/characters/&amp;lt;/code&amp;gt; (known NPCs)&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/pending-changes/&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Prompts ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/prompts/&amp;lt;/code&amp;gt; || List all game prompts&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/prompts/{prompt_id}/&amp;lt;/code&amp;gt; || Retrieve a single prompt by ID&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Permissions and Security ===&lt;br /&gt;
&lt;br /&gt;
* All character data is scoped to the authenticated user via &amp;lt;code&amp;gt;IsCharacterOwner&amp;lt;/code&amp;gt; permission.&lt;br /&gt;
* JWT tokens are sent as &amp;lt;code&amp;gt;Authorization: Bearer {token}&amp;lt;/code&amp;gt; headers.&lt;br /&gt;
* The login endpoint is throttled (5/minute) to prevent brute-force attacks.&lt;br /&gt;
* All pending changes are processed inside a database &#039;&#039;&#039;transaction&#039;&#039;&#039; to guarantee atomicity.&lt;br /&gt;
* CORS headers are configured via &amp;lt;code&amp;gt;django-cors-headers&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Frontend ==&lt;br /&gt;
&lt;br /&gt;
=== Views (Pages) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! View !! Route !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;LoginView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; || Authentication form; redirects authenticated users to home&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;HomeView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt; || Dashboard showing all active characters in a card grid&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharacterCreationView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/character-creation/:id&amp;lt;/code&amp;gt; || Six-step creation wizard&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/game/:characterId&amp;lt;/code&amp;gt; || Main game loop: prompt display, experience form, dice rolling, entity management, memory display&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameEndedView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/game/:characterId/ended&amp;lt;/code&amp;gt; || End-of-story screen with statistics and epilogue&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;RecordsView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/records&amp;lt;/code&amp;gt; || Archive of current and completed characters&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;StoryView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/story/:characterId&amp;lt;/code&amp;gt; || Narrative timeline with experience history, character stats sidebar, and epilogue&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
All routes except &amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; require authentication. The router guard redirects unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
=== Key Components ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Component !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PromptSection&#039;&#039;&#039; || Displays the current prompt text, special rules, and dice roll results&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ExperienceForm&#039;&#039;&#039; || Form for writing new experiences with title, date, content, and memory selection&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;MemoriesDisplay&#039;&#039;&#039; || Shows active, diary, and lost memories with experience counts; includes move-to-diary, delete, and restore actions&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameDataSection&#039;&#039;&#039; || Reusable list for skills, resources, or marks with add/edit/delete controls&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharactersSection&#039;&#039;&#039; || Grid of known NPCs with type badges and edit/delete&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;EntityCreationWindow&#039;&#039;&#039; || Draggable modal for creating/editing skills, resources, marks&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharacterCreationWindow&#039;&#039;&#039; || Draggable modal for creating/editing NPC characters&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ExperienceEditWindow&#039;&#039;&#039; || Window for editing existing experience text&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;DiaryCreationModal&#039;&#039;&#039; || Modal for creating a diary resource and moving a memory into it&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ConfirmationDialog&#039;&#039;&#039; || Generic confirmation dialog for destructive actions&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ValidationWarning&#039;&#039;&#039; || Displays validation errors when game rules are not satisfied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== State Management (Pinia) ===&lt;br /&gt;
&lt;br /&gt;
Four Pinia stores manage application state:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Store !! Responsibility&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;auth&#039;&#039;&#039; || JWT token, user object, login/logout, token expiry handling&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;game&#039;&#039;&#039; || Characters list, current character, game state, all CRUD actions for entities, dice rolling, story continuation&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;characterCreation&#039;&#039;&#039; || Multi-step wizard data, initial character creation&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;theme&#039;&#039;&#039; || Dark/light mode toggle with localStorage persistence&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== API Service Layer ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;api.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Axios instance with base URL auto-detection (development vs. production), request interceptor for JWT headers, response interceptor for 401/token-expiry handling.&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;gameService.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – All API call methods: authentication, character CRUD, dice rolling, story continuation, memory/experience/skill/resource/mark/character management.&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;entityService.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Generic &amp;lt;code&amp;gt;EntityService&amp;lt;/code&amp;gt; class instantiated for skills, resources, marks, and characters for DRY CRUD operations.&lt;br /&gt;
&lt;br /&gt;
=== Composables ===&lt;br /&gt;
&lt;br /&gt;
Vue composables encapsulate reusable business logic:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useGameData()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Computed properties derived from the game store (current character, memories, skills, resources, etc.)&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useExperienceForm()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Form state, validation, memory selection, and reset logic&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useEntityManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Generic CRUD operations with optional confirmation dialogs&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useSkillManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useResourceManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useMarkManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useMemoryManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Specialised wrappers&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useConfirmationDialog()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Promise-based modal confirmation&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useMemoryDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Display logic for memories including pending-change awareness&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useExperienceDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Markdown rendering for experience content&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;usePendingChangesDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Badge rendering for undo-able pending changes&lt;br /&gt;
&lt;br /&gt;
== Audit Trail ==&lt;br /&gt;
&lt;br /&gt;
The application includes an automatic audit trail that captures the complete game state every time the player advances to a new prompt.&lt;br /&gt;
&lt;br /&gt;
Each &#039;&#039;&#039;GameStateSnapshot&#039;&#039;&#039; records:&lt;br /&gt;
&lt;br /&gt;
* The turn number (sequential from 1)&lt;br /&gt;
* The prompt transition (e.g. 36a → 34a)&lt;br /&gt;
* The dice roll&lt;br /&gt;
* A complete JSON snapshot of all memories, skills, resources, marks, and known characters&lt;br /&gt;
* All pending changes that were committed&lt;br /&gt;
&lt;br /&gt;
The audit trail is accessible via:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;API&#039;&#039;&#039;: &amp;lt;code&amp;gt;GET /api/characters/{id}/audit_trail/&amp;lt;/code&amp;gt; with optional &amp;lt;code&amp;gt;?turn=N&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;?limit=N&amp;lt;/code&amp;gt; parameters&lt;br /&gt;
* &#039;&#039;&#039;Management command&#039;&#039;&#039;: &amp;lt;code&amp;gt;python manage.py view_audit_trail {id} [--summary] [--turn N] [--export file.json]&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;Timeline API&#039;&#039;&#039;: &amp;lt;code&amp;gt;GET /api/characters/{id}/game-state-changes/&amp;lt;/code&amp;gt; for a structured timeline of all state changes&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
=== Local Development ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
.\startup.ps1&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This script creates a virtual environment, installs dependencies, runs migrations, and starts both the Django backend (&amp;lt;code&amp;gt;http://localhost:8000&amp;lt;/code&amp;gt;) and the Vue dev server (&amp;lt;code&amp;gt;http://localhost:5173&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
=== Production ===&lt;br /&gt;
&lt;br /&gt;
* Set &amp;lt;code&amp;gt;DJANGO_ENV=production&amp;lt;/code&amp;gt;&lt;br /&gt;
* Build the frontend: &amp;lt;code&amp;gt;cd frontend &amp;amp;&amp;amp; npm run build&amp;lt;/code&amp;gt;&lt;br /&gt;
* Serve with Gunicorn behind a reverse proxy&lt;br /&gt;
* Static files are collected into the &amp;lt;code&amp;gt;static/&amp;lt;/code&amp;gt; directory&lt;br /&gt;
* The production frontend points to &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Example: A Finished Character ==&lt;br /&gt;
&lt;br /&gt;
The following is an example of a character from an actual playthrough stored in the application database. It demonstrates how the game&#039;s mechanics play out over an extended narrative.&lt;br /&gt;
&lt;br /&gt;
=== Karmiš (formerly Ekurzu / Naram) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Description:&#039;&#039;&#039; Naram was a temple scribe in the ancient city of Mari: meticulous, observant, and deeply entangled in the political and spiritual life of the city. His life was shaped by clay tablets, omens, and whispered secrets carried through palace corridors and temple courtyards. Behind his calm demeanor lies a mind constantly at work—recording, interpreting, and surviving.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Prompts visited:&#039;&#039;&#039; 34 prompts across variations, including: 1a, 4a, 11a–c, 12a, 17a, 20a, 24a, 25a, 32a–b, 34a–36a, 37a–41c, 43a–c, 47a, 52a, 57a, 64a, 66a, 68a, 71a, 73a.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Final prompt:&#039;&#039;&#039; 73a&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory slots lost:&#039;&#039;&#039; 1 · &#039;&#039;&#039;Memory slots gained:&#039;&#039;&#039; 1 (net effect: standard 5-memory limit)&lt;br /&gt;
&lt;br /&gt;
==== Epilogue ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;blockquote&amp;gt;&lt;br /&gt;
He has not died. Let me say that plainly. There will be no grave. He is alive, but quietly, deliberately, and utterly outside your reach.&lt;br /&gt;
&lt;br /&gt;
When I first met him, I did not understand what he was. He wore his years like dust on old vellum—thin, barely visible, but ever present. [...] Over time, I became his mirror, then his partner, and finally his historian.&lt;br /&gt;
&lt;br /&gt;
But someone has to begin the telling. Someone has to place the first stone. So this book begins in a house of quiet gardens and low ceilings, where he writes in the mornings and feeds only when he must. [...]&lt;br /&gt;
&lt;br /&gt;
Just a man who remembered too much, for too long. And chose, in the end, peace.&lt;br /&gt;
&lt;br /&gt;
— &#039;&#039;Mirelde&#039;&#039;&lt;br /&gt;
&amp;lt;/blockquote&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== A Life in Summary ====&lt;br /&gt;
&lt;br /&gt;
Karmiš&#039;s story spans roughly three and a half thousand years. It begins in Bronze Age Mesopotamia, where the mortal scribe Naram serves in the temple of Ishtar at Mari—writing omens, navigating palace intrigue, and falling in love with a woman named Hessa who moves through the court &amp;quot;like smoke.&amp;quot; His mentor, the High Priest Ibbi-Zamri, is the first person he kills after being turned by the ancient vampire Ashurban the Veiled.&lt;br /&gt;
&lt;br /&gt;
The early centuries are defined by guilt and exile. Fleeing with his mortal companion Ennatum, the newly-made vampire—now calling himself Ekurzu—struggles to control his hunger. He builds walls and plays flutes in the wilderness, but it is &amp;quot;never penance, only a cage with softer bars.&amp;quot; Eventually the hunger wins: he devours Ennatum during a sandstorm and spends the following decades alone.&lt;br /&gt;
&lt;br /&gt;
The middle game carries the character through the great civilisations of antiquity—Amarna under Akhenaten, the Achaemenid Royal Road, the libraries of Alexandria—always in the role of scribe, forger, or quiet observer. He accumulates skills like &#039;&#039;Silent Cartography&#039;&#039; (&amp;quot;I trace the unseen paths between people, places, and power&amp;quot;) and &#039;&#039;Ash-Tongue&#039;&#039; (&amp;quot;I speak in soot and suggestion, in the cracks between laws&amp;quot;), but loses others as the centuries erode his identity: the ability to &#039;&#039;Decipher Ancient Texts&#039;&#039; and even &#039;&#039;I Control the Beast&#039;&#039; both slip away.&lt;br /&gt;
&lt;br /&gt;
A pivotal relationship develops with the scholar Thoöni, who recognises the name &#039;&#039;Ekurzu&#039;&#039; not as myth but as continuity. Later—after Thoöni&#039;s mortal death—her spectral echo begins appearing in the margins of his diary, scratching fragments in languages he can no longer read.&lt;br /&gt;
&lt;br /&gt;
By the late Roman period Karmiš has lost so many memories that he can barely hold a conversation. The game&#039;s memory mechanics enforce this viscerally: eight of his sixteen memories were permanently forgotten, including &#039;&#039;The Turning&#039;&#039; itself—the vampire no longer remembers how he was made. He compensates by keeping diaries, but even those are lost: &#039;&#039;The Walls of Forgetting&#039;&#039; (memory carvings in five scripts beneath a shrine near Carchemish) and &#039;&#039;The Monastery Diary&#039;&#039; both vanish, taking their stored memories with them.&lt;br /&gt;
&lt;br /&gt;
The final act takes place in early modern Europe. Karmiš witnesses the Atlantic slave trade first-hand and begins compiling &#039;&#039;The Margins of Manifest&#039;&#039;—a cipher-folio of names taken from shipping manifests and punishment ledgers. His response to prompt 66a captures the shift:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;blockquote&amp;gt;&lt;br /&gt;
&#039;&#039;It is no longer a question of restraint. That was a younger version of me—one who believed self-control made me different from the others. Now I am different in purpose, not method. I feed from slavers and call it justice, but the blood tastes the same.&#039;&#039;&lt;br /&gt;
&amp;lt;/blockquote&amp;gt;&lt;br /&gt;
&lt;br /&gt;
His companion in the final centuries is Mirelde of Bracha—an immortal who becomes his mirror, partner, and ultimately his historian. Together they rediscover a clay tablet bearing his original name in a Venetian bookseller&#039;s back room (prompt 68a, 1704 CE), and in a ruined estate outside Smyrna Mirelde finds the remains of a life Karmiš can no longer remember.&lt;br /&gt;
&lt;br /&gt;
At the story&#039;s end Karmiš retains only five active memories, three diary memories, two marks (&#039;&#039;The Unblinking Eye&#039;&#039;—a scar that burns in the presence of lies—and &#039;&#039;The Gesture Forbidden&#039;&#039;—an involuntary court gesture mistaken for mockery), and three possessions: his diary &#039;&#039;Kept Against Forgetting&#039;&#039;, the clay tablet bearing his name, and the slave-trade folio. Of the eleven characters he encountered, only four remain: Ashurban (his distant maker), Mirelde (his companion), Thoöni&#039;s ghost, and a nameless figure known only as &#039;&#039;The One Who Does Not Answer&#039;&#039;. Over 34 prompts he accumulated 37 experiences, gained and lost a memory slot each, and burned through three separate diaries.&lt;br /&gt;
&lt;br /&gt;
The character&#039;s story is narrated in the epilogue not by Karmiš himself but by Mirelde, who insists: &amp;quot;He has not died. [...] Just a man who remembered too much, for too long. And chose, in the end, &#039;&#039;peace&#039;&#039;.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
Tests are run with &#039;&#039;&#039;pytest&#039;&#039;&#039; and &#039;&#039;&#039;pytest-django&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
pytest&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Test files include:&lt;br /&gt;
* &amp;lt;code&amp;gt;vampire/test_models.py&amp;lt;/code&amp;gt; – model unit tests&lt;br /&gt;
* &amp;lt;code&amp;gt;tyov_api/test_api.py&amp;lt;/code&amp;gt; – API endpoint integration tests&lt;br /&gt;
* &amp;lt;code&amp;gt;tyov_api/test_models.py&amp;lt;/code&amp;gt; – API model tests&lt;br /&gt;
* &amp;lt;code&amp;gt;authentication/test_views.py&amp;lt;/code&amp;gt; – authentication tests&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
End-to-end tests use &#039;&#039;&#039;Playwright&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
cd frontend&lt;br /&gt;
npx playwright test&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Test specs include:&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/character-creation-workflow.spec.ts&amp;lt;/code&amp;gt; – character creation wizard&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/tyov-game.spec.ts&amp;lt;/code&amp;gt; – game loop testing&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/vue.spec.ts&amp;lt;/code&amp;gt; – general Vue component tests&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings – the original tabletop game&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Vue.js]]&lt;br /&gt;
* [[Single-page application]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Web applications]]&lt;br /&gt;
[[Category:Role-playing video games]]&lt;br /&gt;
[[Category:Django (web framework) applications]]&lt;br /&gt;
[[Category:Vue.js applications]]&lt;br /&gt;
[[Category:Solo role-playing games]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=387</id>
		<title>Thousand Year Old Vampire</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=387"/>
		<updated>2026-04-12T11:21:35Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox &lt;br /&gt;
| 01_name = Thousand Year Old Vampire – Web Helper&lt;br /&gt;
| 02_logo =&lt;br /&gt;
| 03_developer = Michel Vuijlsteke&lt;br /&gt;
| 04_programming language = Python (Django 5), TypeScript (Vue 3)&lt;br /&gt;
| 05_operating system = Cross-platform (Web)&lt;br /&gt;
| 06_genre = Solo RPG Digital Companion&lt;br /&gt;
| 07_license = custom&lt;br /&gt;
| 08_website = &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Thousand Year Old Vampire – Web Helper&#039;&#039;&#039; (&#039;&#039;&#039;TYOV-Web&#039;&#039;&#039;) is a modern web-based implementation of the solo tabletop role-playing game &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings. The application digitises the game&#039;s complex mechanics of memory, experience, and character development, allowing players to create and guide a vampire character across centuries of unlife through an interactive browser interface. The backend is built with &#039;&#039;&#039;Django 5&#039;&#039;&#039; and &#039;&#039;&#039;Django REST Framework&#039;&#039;&#039;; the frontend is a &#039;&#039;&#039;Vue 3&#039;&#039;&#039; single-page application.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Thousand Year Old Vampire&#039;&#039; is a solo storytelling RPG in which a player creates a vampire character and guides it through an increasingly fragmented existence spanning hundreds or thousands of years. The central tension lies in the vampire&#039;s deteriorating memory: as an immortal being it accumulates experiences, but its ancient mind can only retain a limited number of memories at any time. Players must constantly choose what to remember and what to forget.&lt;br /&gt;
&lt;br /&gt;
TYOV-Web fully implements the game&#039;s rules—character creation, dice-driven prompt navigation, memory management, diary storage, skills, resources, marks, known characters, and game-ending conditions—in a reactive web interface with persistent server-side storage and a complete audit trail.&lt;br /&gt;
&lt;br /&gt;
== Game Rules ==&lt;br /&gt;
&lt;br /&gt;
=== Core Concept ===&lt;br /&gt;
&lt;br /&gt;
The player creates a vampire and progresses through numbered &#039;&#039;&#039;prompts&#039;&#039;&#039; (story scenarios). Each prompt asks the player to narrate what happens to the vampire during a particular era. The game is non-linear: dice rolls determine which prompt to visit next, and most prompts have multiple &#039;&#039;&#039;variations&#039;&#039;&#039; (A, B, C) to prevent repetition.&lt;br /&gt;
&lt;br /&gt;
=== Character Creation ===&lt;br /&gt;
&lt;br /&gt;
Character creation follows a guided six-step wizard:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Step !! Description&lt;br /&gt;
|-&lt;br /&gt;
| 1. Mortal Life || Establish the vampire&#039;s name, background, and mortal existence&lt;br /&gt;
|-&lt;br /&gt;
| 2. Mortal Characters || Define 2–4 NPCs from the vampire&#039;s human life (friends, mentors, rivals, lovers)&lt;br /&gt;
|-&lt;br /&gt;
| 3. Skills || Choose 2–3 starting skills representing mortal expertise&lt;br /&gt;
|-&lt;br /&gt;
| 4. Resources || Select initial possessions and locations&lt;br /&gt;
|-&lt;br /&gt;
| 5. Combination Experience || Write a pivotal mortal-life experience that ties characters, skills, and resources together&lt;br /&gt;
|-&lt;br /&gt;
| 6. The Turning || Narrate how and why the character was turned into a vampire&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Memory System ===&lt;br /&gt;
&lt;br /&gt;
The memory system is the heart of the game:&lt;br /&gt;
&lt;br /&gt;
* A character may hold a maximum of &#039;&#039;&#039;5 active memories&#039;&#039;&#039; (adjustable by prompt rules and permanent effects).&lt;br /&gt;
* Each memory can contain up to &#039;&#039;&#039;3 experiences&#039;&#039;&#039; (individual narrative entries).&lt;br /&gt;
* When the limit is exceeded, the player must &#039;&#039;&#039;forget&#039;&#039;&#039; (permanently lose) a memory, or &#039;&#039;&#039;move&#039;&#039;&#039; one to a &#039;&#039;&#039;diary&#039;&#039;&#039; (if the character possesses one).&lt;br /&gt;
* A &#039;&#039;&#039;diary&#039;&#039;&#039; is a special resource that stores up to &#039;&#039;&#039;4 additional memories&#039;&#039;&#039; outside the active limit.&lt;br /&gt;
* If the diary resource is lost, all memories stored in it are also lost.&lt;br /&gt;
* Some prompts grant or remove permanent memory slots (&amp;lt;code&amp;gt;memory_slot_gain&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;memory_slot_loss&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
=== Dice Rolling and Prompt Navigation ===&lt;br /&gt;
&lt;br /&gt;
Each turn the player rolls two dice:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;D10&#039;&#039;&#039; (1–10) minus &#039;&#039;&#039;D6&#039;&#039;&#039; (1–6) = result (range: −5 to +9).&lt;br /&gt;
* A &#039;&#039;&#039;positive&#039;&#039;&#039; result moves forward that many prompts.&lt;br /&gt;
* A &#039;&#039;&#039;negative&#039;&#039;&#039; result moves backward.&lt;br /&gt;
* A result of &#039;&#039;&#039;zero&#039;&#039;&#039; stays at the current prompt number but selects the next unused variation.&lt;br /&gt;
&lt;br /&gt;
Variations are visited in order A → B → C. If all variations of a prompt have been used, the system finds the next available variation in prompt order.&lt;br /&gt;
&lt;br /&gt;
=== Turn Structure ===&lt;br /&gt;
&lt;br /&gt;
Each game turn follows this sequence:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Prompt Presentation&#039;&#039;&#039; – The current prompt text and any special rules are displayed.&lt;br /&gt;
# &#039;&#039;&#039;Dice Roll&#039;&#039;&#039; – The player rolls D10 − D6 to determine the next prompt.&lt;br /&gt;
# &#039;&#039;&#039;Experience Creation&#039;&#039;&#039; – The player writes at least one narrative experience for the current prompt.&lt;br /&gt;
# &#039;&#039;&#039;Character Updates&#039;&#039;&#039; – Skills, resources, marks, and known characters may be added, edited, checked, or removed.&lt;br /&gt;
# &#039;&#039;&#039;Memory Management&#039;&#039;&#039; – The player resolves any memory overflow (forget or move to diary).&lt;br /&gt;
# &#039;&#039;&#039;Validation&#039;&#039;&#039; – The system verifies all rules are satisfied (at least one experience added, memory limits respected, etc.).&lt;br /&gt;
# &#039;&#039;&#039;Continue&#039;&#039;&#039; – All pending changes are atomically committed and the game advances to the next prompt.&lt;br /&gt;
&lt;br /&gt;
=== Character Attributes ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Attribute !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Skills&#039;&#039;&#039; || Learned abilities; can be &#039;&#039;normal&#039;&#039; (latent), &#039;&#039;checked&#039;&#039; (used/active), or &#039;&#039;lost&#039;&#039; (permanently removed).&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Resources&#039;&#039;&#039; || Possessions or locations; typed as &#039;&#039;portable&#039;&#039; or &#039;&#039;stationary&#039;&#039;. One resource may be flagged as a &#039;&#039;diary&#039;&#039;.&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Marks&#039;&#039;&#039; || Physical or psychological scars that accumulate over time.&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Known Characters&#039;&#039;&#039; || NPCs the vampire has encountered; typed as &#039;&#039;mortal&#039;&#039; or &#039;&#039;immortal&#039;&#039;; may be lost (dead, vanished, etc.).&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Special Prompt Rules ===&lt;br /&gt;
&lt;br /&gt;
Certain prompts carry special mechanics encoded as rules:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Rule !! Effect&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;no_experience&amp;lt;/code&amp;gt; || Experience creation is disabled for this turn&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;allow_name_change&amp;lt;/code&amp;gt; || The player may change the vampire&#039;s name&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_modification&amp;lt;/code&amp;gt; || The player may edit the text of existing experiences&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slot_loss&amp;lt;/code&amp;gt; || Permanent reduction of memory capacity by 1&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slot_gain&amp;lt;/code&amp;gt; || Permanent increase of memory capacity by 1&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;game_over&amp;lt;/code&amp;gt; || The game ends; the player writes an epilogue&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Game Ending ===&lt;br /&gt;
&lt;br /&gt;
The game ends when the player reaches a prompt with the &amp;lt;code&amp;gt;game_over&amp;lt;/code&amp;gt; rule (or under other conditions defined by the original game). The player writes an &#039;&#039;&#039;epilogue&#039;&#039;&#039;—a final reflection on the vampire&#039;s story—and the character is archived. A statistics summary shows prompt count, memories, experiences, skills, resources, marks, and characters accumulated during the playthrough.&lt;br /&gt;
&lt;br /&gt;
== Architecture ==&lt;br /&gt;
&lt;br /&gt;
=== Technology Stack ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Layer !! Technology !! Version&lt;br /&gt;
|-&lt;br /&gt;
| Backend framework || Django || 5.0.6&lt;br /&gt;
|-&lt;br /&gt;
| REST API || Django REST Framework || 3.15.1&lt;br /&gt;
|-&lt;br /&gt;
| Authentication || Simple JWT (custom) || 5.3.0&lt;br /&gt;
|-&lt;br /&gt;
| CORS || django-cors-headers || 4.3.1&lt;br /&gt;
|-&lt;br /&gt;
| Image handling || Pillow || 11.3.0&lt;br /&gt;
|-&lt;br /&gt;
| Production server || Gunicorn || 21.2.0&lt;br /&gt;
|-&lt;br /&gt;
| Frontend framework || Vue.js || 3.5&lt;br /&gt;
|-&lt;br /&gt;
| State management || Pinia || 3.0&lt;br /&gt;
|-&lt;br /&gt;
| Routing || Vue Router || 4.5&lt;br /&gt;
|-&lt;br /&gt;
| HTTP client || Axios || 1.10&lt;br /&gt;
|-&lt;br /&gt;
| CSS framework || Bootstrap || 5.3&lt;br /&gt;
|-&lt;br /&gt;
| Build tool || Vite || 7.0&lt;br /&gt;
|-&lt;br /&gt;
| Language || TypeScript || 5.8&lt;br /&gt;
|-&lt;br /&gt;
| Testing (backend) || pytest + pytest-django || 8.0 / 4.8&lt;br /&gt;
|-&lt;br /&gt;
| Testing (frontend) || Playwright || 1.53&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== High-Level Diagram ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
┌─────────────────────────────────────────────────┐&lt;br /&gt;
│               Vue 3 SPA (TypeScript)            │&lt;br /&gt;
│  ┌──────────┐ ┌──────────┐ ┌──────────────────┐│&lt;br /&gt;
│  │  Views   │ │Components│ │  Pinia Stores     ││&lt;br /&gt;
│  │ (7 pages)│ │ (14+)    │ │ auth/game/theme/  ││&lt;br /&gt;
│  │          │ │          │ │ characterCreation  ││&lt;br /&gt;
│  └────┬─────┘ └────┬─────┘ └────────┬─────────┘│&lt;br /&gt;
│       └─────────────┴────────────────┘          │&lt;br /&gt;
│                     │ Axios                     │&lt;br /&gt;
│                     ▼                           │&lt;br /&gt;
├─────────────────────────────────────────────────┤&lt;br /&gt;
│              Django REST API                    │&lt;br /&gt;
│  /api/auth/login    /api/auth/verify            │&lt;br /&gt;
│  /api/characters/   (CRUD + game_state,         │&lt;br /&gt;
│   roll_dice, continue_story, audit_trail)       │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/memories/                 │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/experiences/              │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/skills/                   │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/resources/                │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/marks/                    │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/characters/               │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/pending-changes/          │&lt;br /&gt;
│  /api/prompts/                                  │&lt;br /&gt;
├─────────────────────────────────────────────────┤&lt;br /&gt;
│          Django ORM / SQLite                    │&lt;br /&gt;
│  Character · Memory · Experience · Skill        │&lt;br /&gt;
│  Resource · Mark · GameCharacter · Prompt        │&lt;br /&gt;
│  PendingChange · GameStateSnapshot              │&lt;br /&gt;
└─────────────────────────────────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend ==&lt;br /&gt;
&lt;br /&gt;
=== Django Apps ===&lt;br /&gt;
&lt;br /&gt;
The project is organised into three Django apps plus a settings module:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! App !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;vampire&amp;lt;/code&amp;gt; || Core domain models: Character, Memory, Experience, Skill, Resource, Mark, GameCharacter, Prompt, PendingChange, GameStateSnapshot&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;tyov_api&amp;lt;/code&amp;gt; || REST API layer: serialisers, viewsets, URL routing, permissions, middleware&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;authentication&amp;lt;/code&amp;gt; || JWT-based login and token verification endpoints&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;tyov_backend&amp;lt;/code&amp;gt; || Django project settings, URL root, WSGI/ASGI configuration&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Data Models ===&lt;br /&gt;
&lt;br /&gt;
==== Character ====&lt;br /&gt;
&lt;br /&gt;
The central model, owned by a Django &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; via a ForeignKey:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Field !! Type !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt; || CharField(200) || Current name of the vampire&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt; || TextField || Background/appearance description&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;image&amp;lt;/code&amp;gt; || ImageField || Optional character portrait&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;current_prompt&amp;lt;/code&amp;gt; || CharField(10) || ID of the current prompt (e.g. &amp;quot;17a&amp;quot;)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;last_dice_roll&amp;lt;/code&amp;gt; || JSONField || Stores D10, D6, and result of the last roll&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;visited_prompts&amp;lt;/code&amp;gt; || JSONField(list) || List of all prompt IDs visited in order&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slots_lost&amp;lt;/code&amp;gt; || IntegerField || Permanent memory slot reductions&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slots_gained&amp;lt;/code&amp;gt; || IntegerField || Permanent memory slot increases&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;creation_step&amp;lt;/code&amp;gt; || IntegerField(1–6) || Tracks progress through the creation wizard&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;is_creation_complete&amp;lt;/code&amp;gt; || BooleanField || True when all six creation steps are finished&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;creation_data&amp;lt;/code&amp;gt; || JSONField || Temporary storage during creation&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;epilogue&amp;lt;/code&amp;gt; || TextField || Player&#039;s final reflection (set at game end)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;is_completed&amp;lt;/code&amp;gt; || BooleanField || Whether the story has ended&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;completed_at&amp;lt;/code&amp;gt; || DateTimeField || Timestamp of story completion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Memory ====&lt;br /&gt;
&lt;br /&gt;
A memory belongs to a Character and holds up to 3 Experiences:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; – descriptive name (unique per character)&lt;br /&gt;
* &amp;lt;code&amp;gt;in_diary&amp;lt;/code&amp;gt; – whether the memory is stored in the diary&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt; – whether the memory has been forgotten&lt;br /&gt;
&lt;br /&gt;
==== Experience ====&lt;br /&gt;
&lt;br /&gt;
An individual narrative entry within a Memory:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; – optional short description&lt;br /&gt;
* &amp;lt;code&amp;gt;content&amp;lt;/code&amp;gt; – the narrative text&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; – which prompt generated this experience&lt;br /&gt;
* &amp;lt;code&amp;gt;date_info&amp;lt;/code&amp;gt; – temporal context (e.g. &amp;quot;Winter, 431 CE&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
==== Skill ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;status&amp;lt;/code&amp;gt; – one of &amp;lt;code&amp;gt;normal&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;checked&amp;lt;/code&amp;gt;, or &amp;lt;code&amp;gt;lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Resource ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;resource_type&amp;lt;/code&amp;gt; – &amp;lt;code&amp;gt;portable&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;stationary&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;is_diary&amp;lt;/code&amp;gt; – flags the special diary resource (constrained to one active diary per character)&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Mark ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== GameCharacter ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;character_type&amp;lt;/code&amp;gt; – &amp;lt;code&amp;gt;mortal&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;immortal&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;relationship&amp;lt;/code&amp;gt; – e.g. friend, rival, mentor, love, enemy, neutral&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Prompt ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; – e.g. &amp;quot;17a&amp;quot;, &amp;quot;32b&amp;quot;&lt;br /&gt;
* &amp;lt;code&amp;gt;number&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;variation&amp;lt;/code&amp;gt; – parsed components for sorting&lt;br /&gt;
* &amp;lt;code&amp;gt;content&amp;lt;/code&amp;gt; – the scenario text&lt;br /&gt;
* &amp;lt;code&amp;gt;rules&amp;lt;/code&amp;gt; – special mechanics as comma-separated tokens (e.g. &amp;lt;code&amp;gt;no_experience, allow_name_change&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
==== PendingChange ====&lt;br /&gt;
&lt;br /&gt;
Temporary storage for changes before they are atomically committed on &amp;quot;Continue&amp;quot;:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;change_type&amp;lt;/code&amp;gt; – experience, memory, skill, resource, mark, or character&lt;br /&gt;
* &amp;lt;code&amp;gt;change_data&amp;lt;/code&amp;gt; – JSON payload of the change&lt;br /&gt;
&lt;br /&gt;
==== GameStateSnapshot (Audit Trail) ====&lt;br /&gt;
&lt;br /&gt;
Automatically created each turn to record the complete game state:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;turn_number&amp;lt;/code&amp;gt; – sequential, starting from 1&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;next_prompt_id&amp;lt;/code&amp;gt; – transition&lt;br /&gt;
* &amp;lt;code&amp;gt;dice_roll&amp;lt;/code&amp;gt; – raw dice data&lt;br /&gt;
* &amp;lt;code&amp;gt;game_state&amp;lt;/code&amp;gt; – full JSON snapshot of all memories, skills, resources, etc.&lt;br /&gt;
* &amp;lt;code&amp;gt;changes_applied&amp;lt;/code&amp;gt; – JSON array of pending changes that were committed&lt;br /&gt;
* &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; – timestamp&lt;br /&gt;
&lt;br /&gt;
=== API Endpoints ===&lt;br /&gt;
&lt;br /&gt;
All endpoints require JWT authentication (except login).&lt;br /&gt;
&lt;br /&gt;
==== Authentication ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/auth/login/&amp;lt;/code&amp;gt; || Authenticate with username/password; returns JWT token&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/auth/verify/&amp;lt;/code&amp;gt; || Verify current token and return user info&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Login is rate-limited to 5 requests per minute per IP.&lt;br /&gt;
&lt;br /&gt;
==== Characters ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/&amp;lt;/code&amp;gt; || List all characters for the authenticated user&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/&amp;lt;/code&amp;gt; || Create a new character&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Retrieve full character detail (prefetched relations)&lt;br /&gt;
|-&lt;br /&gt;
| PUT/PATCH || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Update character fields&lt;br /&gt;
|-&lt;br /&gt;
| DELETE || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Delete a character&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/game_state/&amp;lt;/code&amp;gt; || Complete game state including prompt, validation, dice info&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/{id}/roll_dice/&amp;lt;/code&amp;gt; || Roll D10 − D6 and store the result&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/{id}/continue_story/&amp;lt;/code&amp;gt; || Validate, commit pending changes, advance to next prompt&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/audit_trail/&amp;lt;/code&amp;gt; || View turn-by-turn audit history&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/game-state-changes/&amp;lt;/code&amp;gt; || Timeline of state changes between turns&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Nested Character Resources ====&lt;br /&gt;
&lt;br /&gt;
For each character, full CRUD is available on:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/memories/&amp;lt;/code&amp;gt; (plus &amp;lt;code&amp;gt;move_to_diary&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;restore_lost_memory&amp;lt;/code&amp;gt; actions)&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/experiences/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/skills/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/resources/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/marks/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/characters/&amp;lt;/code&amp;gt; (known NPCs)&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/pending-changes/&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Prompts ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/prompts/&amp;lt;/code&amp;gt; || List all game prompts&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/prompts/{prompt_id}/&amp;lt;/code&amp;gt; || Retrieve a single prompt by ID&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Permissions and Security ===&lt;br /&gt;
&lt;br /&gt;
* All character data is scoped to the authenticated user via &amp;lt;code&amp;gt;IsCharacterOwner&amp;lt;/code&amp;gt; permission.&lt;br /&gt;
* JWT tokens are sent as &amp;lt;code&amp;gt;Authorization: Bearer {token}&amp;lt;/code&amp;gt; headers.&lt;br /&gt;
* The login endpoint is throttled (5/minute) to prevent brute-force attacks.&lt;br /&gt;
* All pending changes are processed inside a database &#039;&#039;&#039;transaction&#039;&#039;&#039; to guarantee atomicity.&lt;br /&gt;
* CORS headers are configured via &amp;lt;code&amp;gt;django-cors-headers&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Frontend ==&lt;br /&gt;
&lt;br /&gt;
=== Views (Pages) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! View !! Route !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;LoginView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; || Authentication form; redirects authenticated users to home&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;HomeView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt; || Dashboard showing all active characters in a card grid&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharacterCreationView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/character-creation/:id&amp;lt;/code&amp;gt; || Six-step creation wizard&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/game/:characterId&amp;lt;/code&amp;gt; || Main game loop: prompt display, experience form, dice rolling, entity management, memory display&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameEndedView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/game/:characterId/ended&amp;lt;/code&amp;gt; || End-of-story screen with statistics and epilogue&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;RecordsView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/records&amp;lt;/code&amp;gt; || Archive of current and completed characters&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;StoryView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/story/:characterId&amp;lt;/code&amp;gt; || Narrative timeline with experience history, character stats sidebar, and epilogue&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
All routes except &amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; require authentication. The router guard redirects unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
=== Key Components ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Component !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PromptSection&#039;&#039;&#039; || Displays the current prompt text, special rules, and dice roll results&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ExperienceForm&#039;&#039;&#039; || Form for writing new experiences with title, date, content, and memory selection&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;MemoriesDisplay&#039;&#039;&#039; || Shows active, diary, and lost memories with experience counts; includes move-to-diary, delete, and restore actions&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameDataSection&#039;&#039;&#039; || Reusable list for skills, resources, or marks with add/edit/delete controls&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharactersSection&#039;&#039;&#039; || Grid of known NPCs with type badges and edit/delete&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;EntityCreationWindow&#039;&#039;&#039; || Draggable modal for creating/editing skills, resources, marks&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharacterCreationWindow&#039;&#039;&#039; || Draggable modal for creating/editing NPC characters&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ExperienceEditWindow&#039;&#039;&#039; || Window for editing existing experience text&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;DiaryCreationModal&#039;&#039;&#039; || Modal for creating a diary resource and moving a memory into it&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ConfirmationDialog&#039;&#039;&#039; || Generic confirmation dialog for destructive actions&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ValidationWarning&#039;&#039;&#039; || Displays validation errors when game rules are not satisfied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== State Management (Pinia) ===&lt;br /&gt;
&lt;br /&gt;
Four Pinia stores manage application state:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Store !! Responsibility&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;auth&#039;&#039;&#039; || JWT token, user object, login/logout, token expiry handling&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;game&#039;&#039;&#039; || Characters list, current character, game state, all CRUD actions for entities, dice rolling, story continuation&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;characterCreation&#039;&#039;&#039; || Multi-step wizard data, initial character creation&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;theme&#039;&#039;&#039; || Dark/light mode toggle with localStorage persistence&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== API Service Layer ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;api.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Axios instance with base URL auto-detection (development vs. production), request interceptor for JWT headers, response interceptor for 401/token-expiry handling.&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;gameService.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – All API call methods: authentication, character CRUD, dice rolling, story continuation, memory/experience/skill/resource/mark/character management.&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;entityService.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Generic &amp;lt;code&amp;gt;EntityService&amp;lt;/code&amp;gt; class instantiated for skills, resources, marks, and characters for DRY CRUD operations.&lt;br /&gt;
&lt;br /&gt;
=== Composables ===&lt;br /&gt;
&lt;br /&gt;
Vue composables encapsulate reusable business logic:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useGameData()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Computed properties derived from the game store (current character, memories, skills, resources, etc.)&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useExperienceForm()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Form state, validation, memory selection, and reset logic&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useEntityManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Generic CRUD operations with optional confirmation dialogs&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useSkillManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useResourceManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useMarkManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useMemoryManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Specialised wrappers&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useConfirmationDialog()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Promise-based modal confirmation&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useMemoryDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Display logic for memories including pending-change awareness&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useExperienceDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Markdown rendering for experience content&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;usePendingChangesDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Badge rendering for undo-able pending changes&lt;br /&gt;
&lt;br /&gt;
== Audit Trail ==&lt;br /&gt;
&lt;br /&gt;
The application includes an automatic audit trail that captures the complete game state every time the player advances to a new prompt.&lt;br /&gt;
&lt;br /&gt;
Each &#039;&#039;&#039;GameStateSnapshot&#039;&#039;&#039; records:&lt;br /&gt;
&lt;br /&gt;
* The turn number (sequential from 1)&lt;br /&gt;
* The prompt transition (e.g. 36a → 34a)&lt;br /&gt;
* The dice roll&lt;br /&gt;
* A complete JSON snapshot of all memories, skills, resources, marks, and known characters&lt;br /&gt;
* All pending changes that were committed&lt;br /&gt;
&lt;br /&gt;
The audit trail is accessible via:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;API&#039;&#039;&#039;: &amp;lt;code&amp;gt;GET /api/characters/{id}/audit_trail/&amp;lt;/code&amp;gt; with optional &amp;lt;code&amp;gt;?turn=N&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;?limit=N&amp;lt;/code&amp;gt; parameters&lt;br /&gt;
* &#039;&#039;&#039;Management command&#039;&#039;&#039;: &amp;lt;code&amp;gt;python manage.py view_audit_trail {id} [--summary] [--turn N] [--export file.json]&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;Timeline API&#039;&#039;&#039;: &amp;lt;code&amp;gt;GET /api/characters/{id}/game-state-changes/&amp;lt;/code&amp;gt; for a structured timeline of all state changes&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
=== Local Development ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
.\startup.ps1&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This script creates a virtual environment, installs dependencies, runs migrations, and starts both the Django backend (&amp;lt;code&amp;gt;http://localhost:8000&amp;lt;/code&amp;gt;) and the Vue dev server (&amp;lt;code&amp;gt;http://localhost:5173&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
=== Production ===&lt;br /&gt;
&lt;br /&gt;
* Set &amp;lt;code&amp;gt;DJANGO_ENV=production&amp;lt;/code&amp;gt;&lt;br /&gt;
* Build the frontend: &amp;lt;code&amp;gt;cd frontend &amp;amp;&amp;amp; npm run build&amp;lt;/code&amp;gt;&lt;br /&gt;
* Serve with Gunicorn behind a reverse proxy&lt;br /&gt;
* Static files are collected into the &amp;lt;code&amp;gt;static/&amp;lt;/code&amp;gt; directory&lt;br /&gt;
* The production frontend points to &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Example: A Finished Character ==&lt;br /&gt;
&lt;br /&gt;
The following is an example of a character from an actual playthrough stored in the application database. It demonstrates how the game&#039;s mechanics play out over an extended narrative.&lt;br /&gt;
&lt;br /&gt;
=== Karmiš (formerly Ekurzu / Naram) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Description:&#039;&#039;&#039; Naram was a temple scribe in the ancient city of Mari: meticulous, observant, and deeply entangled in the political and spiritual life of the city. His life was shaped by clay tablets, omens, and whispered secrets carried through palace corridors and temple courtyards. Behind his calm demeanor lies a mind constantly at work—recording, interpreting, and surviving.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Prompts visited:&#039;&#039;&#039; 34 prompts across variations, including: 1a, 4a, 11a–c, 12a, 17a, 20a, 24a, 25a, 32a–b, 34a–36a, 37a–41c, 43a–c, 47a, 52a, 57a, 64a, 66a, 68a, 71a, 73a.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Final prompt:&#039;&#039;&#039; 73a&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory slots lost:&#039;&#039;&#039; 1 · &#039;&#039;&#039;Memory slots gained:&#039;&#039;&#039; 1 (net effect: standard 5-memory limit)&lt;br /&gt;
&lt;br /&gt;
==== Epilogue ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;blockquote&amp;gt;&lt;br /&gt;
He has not died. Let me say that plainly. There will be no grave, no crumbling tomb, no name worn smooth by time. He is alive—quietly, deliberately, and utterly outside your reach.&lt;br /&gt;
&lt;br /&gt;
This book is not a memorial. It is a reckoning.&lt;br /&gt;
&lt;br /&gt;
When I first met him, I did not understand what he was. He wore his years like dust on old vellum—thin, barely visible, but ever present. [...] Over time, I became his mirror, then his partner, and—finally—his historian.&lt;br /&gt;
&lt;br /&gt;
But someone has to begin the telling. Someone has to place the first stone. So this book begins in a house of quiet gardens and low ceilings, where he writes in the mornings and feeds only when he must. [...]&lt;br /&gt;
&lt;br /&gt;
Just a man who remembered too much, for too long. And chose, in the end, &#039;&#039;peace&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
— &#039;&#039;Mirelde&#039;&#039;&lt;br /&gt;
&amp;lt;/blockquote&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== A Life in Summary ====&lt;br /&gt;
&lt;br /&gt;
Karmiš&#039;s story spans roughly three and a half thousand years. It begins in Bronze Age Mesopotamia, where the mortal scribe Naram serves in the temple of Ishtar at Mari—writing omens, navigating palace intrigue, and falling in love with a woman named Hessa who moves through the court &amp;quot;like smoke.&amp;quot; His mentor, the High Priest Ibbi-Zamri, is the first person he kills after being turned by the ancient vampire Ashurban the Veiled.&lt;br /&gt;
&lt;br /&gt;
The early centuries are defined by guilt and exile. Fleeing with his mortal companion Ennatum, the newly-made vampire—now calling himself Ekurzu—struggles to control his hunger. He builds walls and plays flutes in the wilderness, but it is &amp;quot;never penance, only a cage with softer bars.&amp;quot; Eventually the hunger wins: he devours Ennatum during a sandstorm and spends the following decades alone.&lt;br /&gt;
&lt;br /&gt;
The middle game carries the character through the great civilisations of antiquity—Amarna under Akhenaten, the Achaemenid Royal Road, the libraries of Alexandria—always in the role of scribe, forger, or quiet observer. He accumulates skills like &#039;&#039;Silent Cartography&#039;&#039; (&amp;quot;I trace the unseen paths between people, places, and power&amp;quot;) and &#039;&#039;Ash-Tongue&#039;&#039; (&amp;quot;I speak in soot and suggestion, in the cracks between laws&amp;quot;), but loses others as the centuries erode his identity: the ability to &#039;&#039;Decipher Ancient Texts&#039;&#039; and even &#039;&#039;I Control the Beast&#039;&#039; both slip away.&lt;br /&gt;
&lt;br /&gt;
A pivotal relationship develops with the scholar Thoöni, who recognises the name &#039;&#039;Ekurzu&#039;&#039; not as myth but as continuity. Later—after Thoöni&#039;s mortal death—her spectral echo begins appearing in the margins of his diary, scratching fragments in languages he can no longer read.&lt;br /&gt;
&lt;br /&gt;
By the late Roman period Karmiš has lost so many memories that he can barely hold a conversation. The game&#039;s memory mechanics enforce this viscerally: eight of his sixteen memories were permanently forgotten, including &#039;&#039;The Turning&#039;&#039; itself—the vampire no longer remembers how he was made. He compensates by keeping diaries, but even those are lost: &#039;&#039;The Walls of Forgetting&#039;&#039; (memory carvings in five scripts beneath a shrine near Carchemish) and &#039;&#039;The Monastery Diary&#039;&#039; both vanish, taking their stored memories with them.&lt;br /&gt;
&lt;br /&gt;
The final act takes place in early modern Europe. Karmiš witnesses the Atlantic slave trade first-hand and begins compiling &#039;&#039;The Margins of Manifest&#039;&#039;—a cipher-folio of names taken from shipping manifests and punishment ledgers. His response to prompt 66a captures the shift:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;blockquote&amp;gt;&lt;br /&gt;
&#039;&#039;It is no longer a question of restraint. That was a younger version of me—one who believed self-control made me different from the others. Now I am different in purpose, not method. I feed from slavers and call it justice, but the blood tastes the same.&#039;&#039;&lt;br /&gt;
&amp;lt;/blockquote&amp;gt;&lt;br /&gt;
&lt;br /&gt;
His companion in the final centuries is Mirelde of Bracha—an immortal who becomes his mirror, partner, and ultimately his historian. Together they rediscover a clay tablet bearing his original name in a Venetian bookseller&#039;s back room (prompt 68a, 1704 CE), and in a ruined estate outside Smyrna Mirelde finds the remains of a life Karmiš can no longer remember.&lt;br /&gt;
&lt;br /&gt;
At the story&#039;s end Karmiš retains only five active memories, three diary memories, two marks (&#039;&#039;The Unblinking Eye&#039;&#039;—a scar that burns in the presence of lies—and &#039;&#039;The Gesture Forbidden&#039;&#039;—an involuntary court gesture mistaken for mockery), and three possessions: his diary &#039;&#039;Kept Against Forgetting&#039;&#039;, the clay tablet bearing his name, and the slave-trade folio. Of the eleven characters he encountered, only four remain: Ashurban (his distant maker), Mirelde (his companion), Thoöni&#039;s ghost, and a nameless figure known only as &#039;&#039;The One Who Does Not Answer&#039;&#039;. Over 34 prompts he accumulated 37 experiences, gained and lost a memory slot each, and burned through three separate diaries.&lt;br /&gt;
&lt;br /&gt;
The character&#039;s story is narrated in the epilogue not by Karmiš himself but by Mirelde, who insists: &amp;quot;He has not died. [...] Just a man who remembered too much, for too long. And chose, in the end, &#039;&#039;peace&#039;&#039;.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
Tests are run with &#039;&#039;&#039;pytest&#039;&#039;&#039; and &#039;&#039;&#039;pytest-django&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
pytest&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Test files include:&lt;br /&gt;
* &amp;lt;code&amp;gt;vampire/test_models.py&amp;lt;/code&amp;gt; – model unit tests&lt;br /&gt;
* &amp;lt;code&amp;gt;tyov_api/test_api.py&amp;lt;/code&amp;gt; – API endpoint integration tests&lt;br /&gt;
* &amp;lt;code&amp;gt;tyov_api/test_models.py&amp;lt;/code&amp;gt; – API model tests&lt;br /&gt;
* &amp;lt;code&amp;gt;authentication/test_views.py&amp;lt;/code&amp;gt; – authentication tests&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
End-to-end tests use &#039;&#039;&#039;Playwright&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
cd frontend&lt;br /&gt;
npx playwright test&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Test specs include:&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/character-creation-workflow.spec.ts&amp;lt;/code&amp;gt; – character creation wizard&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/tyov-game.spec.ts&amp;lt;/code&amp;gt; – game loop testing&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/vue.spec.ts&amp;lt;/code&amp;gt; – general Vue component tests&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings – the original tabletop game&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Vue.js]]&lt;br /&gt;
* [[Single-page application]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Web applications]]&lt;br /&gt;
[[Category:Role-playing video games]]&lt;br /&gt;
[[Category:Django (web framework) applications]]&lt;br /&gt;
[[Category:Vue.js applications]]&lt;br /&gt;
[[Category:Solo role-playing games]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=386</id>
		<title>Thousand Year Old Vampire</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=386"/>
		<updated>2026-04-12T11:18:24Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Example Experience: &amp;quot;The Bellies of Ships and Men&amp;quot; (Prompt 64a) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox &lt;br /&gt;
| 01_name = Thousand Year Old Vampire – Web Helper&lt;br /&gt;
| 02_logo =&lt;br /&gt;
| 03_developer = Michel Vuijlsteke&lt;br /&gt;
| 04_programming language = Python (Django 5), TypeScript (Vue 3)&lt;br /&gt;
| 05_operating system = Cross-platform (Web)&lt;br /&gt;
| 06_genre = Solo RPG Digital Companion&lt;br /&gt;
| 07_license = custom&lt;br /&gt;
| 08_website = &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Thousand Year Old Vampire – Web Helper&#039;&#039;&#039; (&#039;&#039;&#039;TYOV-Web&#039;&#039;&#039;) is a modern web-based implementation of the solo tabletop role-playing game &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings. The application digitises the game&#039;s complex mechanics of memory, experience, and character development, allowing players to create and guide a vampire character across centuries of unlife through an interactive browser interface. The backend is built with &#039;&#039;&#039;Django 5&#039;&#039;&#039; and &#039;&#039;&#039;Django REST Framework&#039;&#039;&#039;; the frontend is a &#039;&#039;&#039;Vue 3&#039;&#039;&#039; single-page application.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Thousand Year Old Vampire&#039;&#039; is a solo storytelling RPG in which a player creates a vampire character and guides it through an increasingly fragmented existence spanning hundreds or thousands of years. The central tension lies in the vampire&#039;s deteriorating memory: as an immortal being it accumulates experiences, but its ancient mind can only retain a limited number of memories at any time. Players must constantly choose what to remember and what to forget.&lt;br /&gt;
&lt;br /&gt;
TYOV-Web fully implements the game&#039;s rules—character creation, dice-driven prompt navigation, memory management, diary storage, skills, resources, marks, known characters, and game-ending conditions—in a reactive web interface with persistent server-side storage and a complete audit trail.&lt;br /&gt;
&lt;br /&gt;
== Game Rules ==&lt;br /&gt;
&lt;br /&gt;
=== Core Concept ===&lt;br /&gt;
&lt;br /&gt;
The player creates a vampire and progresses through numbered &#039;&#039;&#039;prompts&#039;&#039;&#039; (story scenarios). Each prompt asks the player to narrate what happens to the vampire during a particular era. The game is non-linear: dice rolls determine which prompt to visit next, and most prompts have multiple &#039;&#039;&#039;variations&#039;&#039;&#039; (A, B, C) to prevent repetition.&lt;br /&gt;
&lt;br /&gt;
=== Character Creation ===&lt;br /&gt;
&lt;br /&gt;
Character creation follows a guided six-step wizard:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Step !! Description&lt;br /&gt;
|-&lt;br /&gt;
| 1. Mortal Life || Establish the vampire&#039;s name, background, and mortal existence&lt;br /&gt;
|-&lt;br /&gt;
| 2. Mortal Characters || Define 2–4 NPCs from the vampire&#039;s human life (friends, mentors, rivals, lovers)&lt;br /&gt;
|-&lt;br /&gt;
| 3. Skills || Choose 2–3 starting skills representing mortal expertise&lt;br /&gt;
|-&lt;br /&gt;
| 4. Resources || Select initial possessions and locations&lt;br /&gt;
|-&lt;br /&gt;
| 5. Combination Experience || Write a pivotal mortal-life experience that ties characters, skills, and resources together&lt;br /&gt;
|-&lt;br /&gt;
| 6. The Turning || Narrate how and why the character was turned into a vampire&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Memory System ===&lt;br /&gt;
&lt;br /&gt;
The memory system is the heart of the game:&lt;br /&gt;
&lt;br /&gt;
* A character may hold a maximum of &#039;&#039;&#039;5 active memories&#039;&#039;&#039; (adjustable by prompt rules and permanent effects).&lt;br /&gt;
* Each memory can contain up to &#039;&#039;&#039;3 experiences&#039;&#039;&#039; (individual narrative entries).&lt;br /&gt;
* When the limit is exceeded, the player must &#039;&#039;&#039;forget&#039;&#039;&#039; (permanently lose) a memory, or &#039;&#039;&#039;move&#039;&#039;&#039; one to a &#039;&#039;&#039;diary&#039;&#039;&#039; (if the character possesses one).&lt;br /&gt;
* A &#039;&#039;&#039;diary&#039;&#039;&#039; is a special resource that stores up to &#039;&#039;&#039;4 additional memories&#039;&#039;&#039; outside the active limit.&lt;br /&gt;
* If the diary resource is lost, all memories stored in it are also lost.&lt;br /&gt;
* Some prompts grant or remove permanent memory slots (&amp;lt;code&amp;gt;memory_slot_gain&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;memory_slot_loss&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
=== Dice Rolling and Prompt Navigation ===&lt;br /&gt;
&lt;br /&gt;
Each turn the player rolls two dice:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;D10&#039;&#039;&#039; (1–10) minus &#039;&#039;&#039;D6&#039;&#039;&#039; (1–6) = result (range: −5 to +9).&lt;br /&gt;
* A &#039;&#039;&#039;positive&#039;&#039;&#039; result moves forward that many prompts.&lt;br /&gt;
* A &#039;&#039;&#039;negative&#039;&#039;&#039; result moves backward.&lt;br /&gt;
* A result of &#039;&#039;&#039;zero&#039;&#039;&#039; stays at the current prompt number but selects the next unused variation.&lt;br /&gt;
&lt;br /&gt;
Variations are visited in order A → B → C. If all variations of a prompt have been used, the system finds the next available variation in prompt order.&lt;br /&gt;
&lt;br /&gt;
=== Turn Structure ===&lt;br /&gt;
&lt;br /&gt;
Each game turn follows this sequence:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Prompt Presentation&#039;&#039;&#039; – The current prompt text and any special rules are displayed.&lt;br /&gt;
# &#039;&#039;&#039;Dice Roll&#039;&#039;&#039; – The player rolls D10 − D6 to determine the next prompt.&lt;br /&gt;
# &#039;&#039;&#039;Experience Creation&#039;&#039;&#039; – The player writes at least one narrative experience for the current prompt.&lt;br /&gt;
# &#039;&#039;&#039;Character Updates&#039;&#039;&#039; – Skills, resources, marks, and known characters may be added, edited, checked, or removed.&lt;br /&gt;
# &#039;&#039;&#039;Memory Management&#039;&#039;&#039; – The player resolves any memory overflow (forget or move to diary).&lt;br /&gt;
# &#039;&#039;&#039;Validation&#039;&#039;&#039; – The system verifies all rules are satisfied (at least one experience added, memory limits respected, etc.).&lt;br /&gt;
# &#039;&#039;&#039;Continue&#039;&#039;&#039; – All pending changes are atomically committed and the game advances to the next prompt.&lt;br /&gt;
&lt;br /&gt;
=== Character Attributes ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Attribute !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Skills&#039;&#039;&#039; || Learned abilities; can be &#039;&#039;normal&#039;&#039; (latent), &#039;&#039;checked&#039;&#039; (used/active), or &#039;&#039;lost&#039;&#039; (permanently removed).&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Resources&#039;&#039;&#039; || Possessions or locations; typed as &#039;&#039;portable&#039;&#039; or &#039;&#039;stationary&#039;&#039;. One resource may be flagged as a &#039;&#039;diary&#039;&#039;.&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Marks&#039;&#039;&#039; || Physical or psychological scars that accumulate over time.&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Known Characters&#039;&#039;&#039; || NPCs the vampire has encountered; typed as &#039;&#039;mortal&#039;&#039; or &#039;&#039;immortal&#039;&#039;; may be lost (dead, vanished, etc.).&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Special Prompt Rules ===&lt;br /&gt;
&lt;br /&gt;
Certain prompts carry special mechanics encoded as rules:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Rule !! Effect&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;no_experience&amp;lt;/code&amp;gt; || Experience creation is disabled for this turn&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;allow_name_change&amp;lt;/code&amp;gt; || The player may change the vampire&#039;s name&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_modification&amp;lt;/code&amp;gt; || The player may edit the text of existing experiences&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slot_loss&amp;lt;/code&amp;gt; || Permanent reduction of memory capacity by 1&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slot_gain&amp;lt;/code&amp;gt; || Permanent increase of memory capacity by 1&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;game_over&amp;lt;/code&amp;gt; || The game ends; the player writes an epilogue&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Game Ending ===&lt;br /&gt;
&lt;br /&gt;
The game ends when the player reaches a prompt with the &amp;lt;code&amp;gt;game_over&amp;lt;/code&amp;gt; rule (or under other conditions defined by the original game). The player writes an &#039;&#039;&#039;epilogue&#039;&#039;&#039;—a final reflection on the vampire&#039;s story—and the character is archived. A statistics summary shows prompt count, memories, experiences, skills, resources, marks, and characters accumulated during the playthrough.&lt;br /&gt;
&lt;br /&gt;
== Architecture ==&lt;br /&gt;
&lt;br /&gt;
=== Technology Stack ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Layer !! Technology !! Version&lt;br /&gt;
|-&lt;br /&gt;
| Backend framework || Django || 5.0.6&lt;br /&gt;
|-&lt;br /&gt;
| REST API || Django REST Framework || 3.15.1&lt;br /&gt;
|-&lt;br /&gt;
| Authentication || Simple JWT (custom) || 5.3.0&lt;br /&gt;
|-&lt;br /&gt;
| CORS || django-cors-headers || 4.3.1&lt;br /&gt;
|-&lt;br /&gt;
| Image handling || Pillow || 11.3.0&lt;br /&gt;
|-&lt;br /&gt;
| Production server || Gunicorn || 21.2.0&lt;br /&gt;
|-&lt;br /&gt;
| Frontend framework || Vue.js || 3.5&lt;br /&gt;
|-&lt;br /&gt;
| State management || Pinia || 3.0&lt;br /&gt;
|-&lt;br /&gt;
| Routing || Vue Router || 4.5&lt;br /&gt;
|-&lt;br /&gt;
| HTTP client || Axios || 1.10&lt;br /&gt;
|-&lt;br /&gt;
| CSS framework || Bootstrap || 5.3&lt;br /&gt;
|-&lt;br /&gt;
| Build tool || Vite || 7.0&lt;br /&gt;
|-&lt;br /&gt;
| Language || TypeScript || 5.8&lt;br /&gt;
|-&lt;br /&gt;
| Testing (backend) || pytest + pytest-django || 8.0 / 4.8&lt;br /&gt;
|-&lt;br /&gt;
| Testing (frontend) || Playwright || 1.53&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== High-Level Diagram ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
┌─────────────────────────────────────────────────┐&lt;br /&gt;
│               Vue 3 SPA (TypeScript)            │&lt;br /&gt;
│  ┌──────────┐ ┌──────────┐ ┌──────────────────┐│&lt;br /&gt;
│  │  Views   │ │Components│ │  Pinia Stores     ││&lt;br /&gt;
│  │ (7 pages)│ │ (14+)    │ │ auth/game/theme/  ││&lt;br /&gt;
│  │          │ │          │ │ characterCreation  ││&lt;br /&gt;
│  └────┬─────┘ └────┬─────┘ └────────┬─────────┘│&lt;br /&gt;
│       └─────────────┴────────────────┘          │&lt;br /&gt;
│                     │ Axios                     │&lt;br /&gt;
│                     ▼                           │&lt;br /&gt;
├─────────────────────────────────────────────────┤&lt;br /&gt;
│              Django REST API                    │&lt;br /&gt;
│  /api/auth/login    /api/auth/verify            │&lt;br /&gt;
│  /api/characters/   (CRUD + game_state,         │&lt;br /&gt;
│   roll_dice, continue_story, audit_trail)       │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/memories/                 │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/experiences/              │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/skills/                   │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/resources/                │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/marks/                    │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/characters/               │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/pending-changes/          │&lt;br /&gt;
│  /api/prompts/                                  │&lt;br /&gt;
├─────────────────────────────────────────────────┤&lt;br /&gt;
│          Django ORM / SQLite                    │&lt;br /&gt;
│  Character · Memory · Experience · Skill        │&lt;br /&gt;
│  Resource · Mark · GameCharacter · Prompt        │&lt;br /&gt;
│  PendingChange · GameStateSnapshot              │&lt;br /&gt;
└─────────────────────────────────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend ==&lt;br /&gt;
&lt;br /&gt;
=== Django Apps ===&lt;br /&gt;
&lt;br /&gt;
The project is organised into three Django apps plus a settings module:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! App !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;vampire&amp;lt;/code&amp;gt; || Core domain models: Character, Memory, Experience, Skill, Resource, Mark, GameCharacter, Prompt, PendingChange, GameStateSnapshot&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;tyov_api&amp;lt;/code&amp;gt; || REST API layer: serialisers, viewsets, URL routing, permissions, middleware&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;authentication&amp;lt;/code&amp;gt; || JWT-based login and token verification endpoints&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;tyov_backend&amp;lt;/code&amp;gt; || Django project settings, URL root, WSGI/ASGI configuration&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Data Models ===&lt;br /&gt;
&lt;br /&gt;
==== Character ====&lt;br /&gt;
&lt;br /&gt;
The central model, owned by a Django &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; via a ForeignKey:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Field !! Type !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt; || CharField(200) || Current name of the vampire&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt; || TextField || Background/appearance description&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;image&amp;lt;/code&amp;gt; || ImageField || Optional character portrait&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;current_prompt&amp;lt;/code&amp;gt; || CharField(10) || ID of the current prompt (e.g. &amp;quot;17a&amp;quot;)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;last_dice_roll&amp;lt;/code&amp;gt; || JSONField || Stores D10, D6, and result of the last roll&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;visited_prompts&amp;lt;/code&amp;gt; || JSONField(list) || List of all prompt IDs visited in order&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slots_lost&amp;lt;/code&amp;gt; || IntegerField || Permanent memory slot reductions&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slots_gained&amp;lt;/code&amp;gt; || IntegerField || Permanent memory slot increases&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;creation_step&amp;lt;/code&amp;gt; || IntegerField(1–6) || Tracks progress through the creation wizard&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;is_creation_complete&amp;lt;/code&amp;gt; || BooleanField || True when all six creation steps are finished&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;creation_data&amp;lt;/code&amp;gt; || JSONField || Temporary storage during creation&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;epilogue&amp;lt;/code&amp;gt; || TextField || Player&#039;s final reflection (set at game end)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;is_completed&amp;lt;/code&amp;gt; || BooleanField || Whether the story has ended&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;completed_at&amp;lt;/code&amp;gt; || DateTimeField || Timestamp of story completion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Memory ====&lt;br /&gt;
&lt;br /&gt;
A memory belongs to a Character and holds up to 3 Experiences:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; – descriptive name (unique per character)&lt;br /&gt;
* &amp;lt;code&amp;gt;in_diary&amp;lt;/code&amp;gt; – whether the memory is stored in the diary&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt; – whether the memory has been forgotten&lt;br /&gt;
&lt;br /&gt;
==== Experience ====&lt;br /&gt;
&lt;br /&gt;
An individual narrative entry within a Memory:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; – optional short description&lt;br /&gt;
* &amp;lt;code&amp;gt;content&amp;lt;/code&amp;gt; – the narrative text&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; – which prompt generated this experience&lt;br /&gt;
* &amp;lt;code&amp;gt;date_info&amp;lt;/code&amp;gt; – temporal context (e.g. &amp;quot;Winter, 431 CE&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
==== Skill ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;status&amp;lt;/code&amp;gt; – one of &amp;lt;code&amp;gt;normal&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;checked&amp;lt;/code&amp;gt;, or &amp;lt;code&amp;gt;lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Resource ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;resource_type&amp;lt;/code&amp;gt; – &amp;lt;code&amp;gt;portable&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;stationary&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;is_diary&amp;lt;/code&amp;gt; – flags the special diary resource (constrained to one active diary per character)&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Mark ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== GameCharacter ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;character_type&amp;lt;/code&amp;gt; – &amp;lt;code&amp;gt;mortal&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;immortal&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;relationship&amp;lt;/code&amp;gt; – e.g. friend, rival, mentor, love, enemy, neutral&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Prompt ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; – e.g. &amp;quot;17a&amp;quot;, &amp;quot;32b&amp;quot;&lt;br /&gt;
* &amp;lt;code&amp;gt;number&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;variation&amp;lt;/code&amp;gt; – parsed components for sorting&lt;br /&gt;
* &amp;lt;code&amp;gt;content&amp;lt;/code&amp;gt; – the scenario text&lt;br /&gt;
* &amp;lt;code&amp;gt;rules&amp;lt;/code&amp;gt; – special mechanics as comma-separated tokens (e.g. &amp;lt;code&amp;gt;no_experience, allow_name_change&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
==== PendingChange ====&lt;br /&gt;
&lt;br /&gt;
Temporary storage for changes before they are atomically committed on &amp;quot;Continue&amp;quot;:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;change_type&amp;lt;/code&amp;gt; – experience, memory, skill, resource, mark, or character&lt;br /&gt;
* &amp;lt;code&amp;gt;change_data&amp;lt;/code&amp;gt; – JSON payload of the change&lt;br /&gt;
&lt;br /&gt;
==== GameStateSnapshot (Audit Trail) ====&lt;br /&gt;
&lt;br /&gt;
Automatically created each turn to record the complete game state:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;turn_number&amp;lt;/code&amp;gt; – sequential, starting from 1&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;next_prompt_id&amp;lt;/code&amp;gt; – transition&lt;br /&gt;
* &amp;lt;code&amp;gt;dice_roll&amp;lt;/code&amp;gt; – raw dice data&lt;br /&gt;
* &amp;lt;code&amp;gt;game_state&amp;lt;/code&amp;gt; – full JSON snapshot of all memories, skills, resources, etc.&lt;br /&gt;
* &amp;lt;code&amp;gt;changes_applied&amp;lt;/code&amp;gt; – JSON array of pending changes that were committed&lt;br /&gt;
* &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; – timestamp&lt;br /&gt;
&lt;br /&gt;
=== API Endpoints ===&lt;br /&gt;
&lt;br /&gt;
All endpoints require JWT authentication (except login).&lt;br /&gt;
&lt;br /&gt;
==== Authentication ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/auth/login/&amp;lt;/code&amp;gt; || Authenticate with username/password; returns JWT token&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/auth/verify/&amp;lt;/code&amp;gt; || Verify current token and return user info&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Login is rate-limited to 5 requests per minute per IP.&lt;br /&gt;
&lt;br /&gt;
==== Characters ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/&amp;lt;/code&amp;gt; || List all characters for the authenticated user&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/&amp;lt;/code&amp;gt; || Create a new character&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Retrieve full character detail (prefetched relations)&lt;br /&gt;
|-&lt;br /&gt;
| PUT/PATCH || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Update character fields&lt;br /&gt;
|-&lt;br /&gt;
| DELETE || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Delete a character&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/game_state/&amp;lt;/code&amp;gt; || Complete game state including prompt, validation, dice info&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/{id}/roll_dice/&amp;lt;/code&amp;gt; || Roll D10 − D6 and store the result&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/{id}/continue_story/&amp;lt;/code&amp;gt; || Validate, commit pending changes, advance to next prompt&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/audit_trail/&amp;lt;/code&amp;gt; || View turn-by-turn audit history&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/game-state-changes/&amp;lt;/code&amp;gt; || Timeline of state changes between turns&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Nested Character Resources ====&lt;br /&gt;
&lt;br /&gt;
For each character, full CRUD is available on:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/memories/&amp;lt;/code&amp;gt; (plus &amp;lt;code&amp;gt;move_to_diary&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;restore_lost_memory&amp;lt;/code&amp;gt; actions)&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/experiences/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/skills/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/resources/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/marks/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/characters/&amp;lt;/code&amp;gt; (known NPCs)&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/pending-changes/&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Prompts ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/prompts/&amp;lt;/code&amp;gt; || List all game prompts&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/prompts/{prompt_id}/&amp;lt;/code&amp;gt; || Retrieve a single prompt by ID&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Permissions and Security ===&lt;br /&gt;
&lt;br /&gt;
* All character data is scoped to the authenticated user via &amp;lt;code&amp;gt;IsCharacterOwner&amp;lt;/code&amp;gt; permission.&lt;br /&gt;
* JWT tokens are sent as &amp;lt;code&amp;gt;Authorization: Bearer {token}&amp;lt;/code&amp;gt; headers.&lt;br /&gt;
* The login endpoint is throttled (5/minute) to prevent brute-force attacks.&lt;br /&gt;
* All pending changes are processed inside a database &#039;&#039;&#039;transaction&#039;&#039;&#039; to guarantee atomicity.&lt;br /&gt;
* CORS headers are configured via &amp;lt;code&amp;gt;django-cors-headers&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Frontend ==&lt;br /&gt;
&lt;br /&gt;
=== Views (Pages) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! View !! Route !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;LoginView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; || Authentication form; redirects authenticated users to home&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;HomeView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt; || Dashboard showing all active characters in a card grid&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharacterCreationView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/character-creation/:id&amp;lt;/code&amp;gt; || Six-step creation wizard&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/game/:characterId&amp;lt;/code&amp;gt; || Main game loop: prompt display, experience form, dice rolling, entity management, memory display&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameEndedView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/game/:characterId/ended&amp;lt;/code&amp;gt; || End-of-story screen with statistics and epilogue&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;RecordsView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/records&amp;lt;/code&amp;gt; || Archive of current and completed characters&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;StoryView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/story/:characterId&amp;lt;/code&amp;gt; || Narrative timeline with experience history, character stats sidebar, and epilogue&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
All routes except &amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; require authentication. The router guard redirects unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
=== Key Components ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Component !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PromptSection&#039;&#039;&#039; || Displays the current prompt text, special rules, and dice roll results&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ExperienceForm&#039;&#039;&#039; || Form for writing new experiences with title, date, content, and memory selection&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;MemoriesDisplay&#039;&#039;&#039; || Shows active, diary, and lost memories with experience counts; includes move-to-diary, delete, and restore actions&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameDataSection&#039;&#039;&#039; || Reusable list for skills, resources, or marks with add/edit/delete controls&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharactersSection&#039;&#039;&#039; || Grid of known NPCs with type badges and edit/delete&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;EntityCreationWindow&#039;&#039;&#039; || Draggable modal for creating/editing skills, resources, marks&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharacterCreationWindow&#039;&#039;&#039; || Draggable modal for creating/editing NPC characters&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ExperienceEditWindow&#039;&#039;&#039; || Window for editing existing experience text&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;DiaryCreationModal&#039;&#039;&#039; || Modal for creating a diary resource and moving a memory into it&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ConfirmationDialog&#039;&#039;&#039; || Generic confirmation dialog for destructive actions&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ValidationWarning&#039;&#039;&#039; || Displays validation errors when game rules are not satisfied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== State Management (Pinia) ===&lt;br /&gt;
&lt;br /&gt;
Four Pinia stores manage application state:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Store !! Responsibility&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;auth&#039;&#039;&#039; || JWT token, user object, login/logout, token expiry handling&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;game&#039;&#039;&#039; || Characters list, current character, game state, all CRUD actions for entities, dice rolling, story continuation&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;characterCreation&#039;&#039;&#039; || Multi-step wizard data, initial character creation&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;theme&#039;&#039;&#039; || Dark/light mode toggle with localStorage persistence&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== API Service Layer ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;api.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Axios instance with base URL auto-detection (development vs. production), request interceptor for JWT headers, response interceptor for 401/token-expiry handling.&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;gameService.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – All API call methods: authentication, character CRUD, dice rolling, story continuation, memory/experience/skill/resource/mark/character management.&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;entityService.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Generic &amp;lt;code&amp;gt;EntityService&amp;lt;/code&amp;gt; class instantiated for skills, resources, marks, and characters for DRY CRUD operations.&lt;br /&gt;
&lt;br /&gt;
=== Composables ===&lt;br /&gt;
&lt;br /&gt;
Vue composables encapsulate reusable business logic:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useGameData()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Computed properties derived from the game store (current character, memories, skills, resources, etc.)&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useExperienceForm()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Form state, validation, memory selection, and reset logic&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useEntityManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Generic CRUD operations with optional confirmation dialogs&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useSkillManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useResourceManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useMarkManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useMemoryManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Specialised wrappers&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useConfirmationDialog()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Promise-based modal confirmation&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useMemoryDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Display logic for memories including pending-change awareness&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useExperienceDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Markdown rendering for experience content&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;usePendingChangesDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Badge rendering for undo-able pending changes&lt;br /&gt;
&lt;br /&gt;
== Audit Trail ==&lt;br /&gt;
&lt;br /&gt;
The application includes an automatic audit trail that captures the complete game state every time the player advances to a new prompt.&lt;br /&gt;
&lt;br /&gt;
Each &#039;&#039;&#039;GameStateSnapshot&#039;&#039;&#039; records:&lt;br /&gt;
&lt;br /&gt;
* The turn number (sequential from 1)&lt;br /&gt;
* The prompt transition (e.g. 36a → 34a)&lt;br /&gt;
* The dice roll&lt;br /&gt;
* A complete JSON snapshot of all memories, skills, resources, marks, and known characters&lt;br /&gt;
* All pending changes that were committed&lt;br /&gt;
&lt;br /&gt;
The audit trail is accessible via:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;API&#039;&#039;&#039;: &amp;lt;code&amp;gt;GET /api/characters/{id}/audit_trail/&amp;lt;/code&amp;gt; with optional &amp;lt;code&amp;gt;?turn=N&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;?limit=N&amp;lt;/code&amp;gt; parameters&lt;br /&gt;
* &#039;&#039;&#039;Management command&#039;&#039;&#039;: &amp;lt;code&amp;gt;python manage.py view_audit_trail {id} [--summary] [--turn N] [--export file.json]&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;Timeline API&#039;&#039;&#039;: &amp;lt;code&amp;gt;GET /api/characters/{id}/game-state-changes/&amp;lt;/code&amp;gt; for a structured timeline of all state changes&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
=== Local Development ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
.\startup.ps1&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This script creates a virtual environment, installs dependencies, runs migrations, and starts both the Django backend (&amp;lt;code&amp;gt;http://localhost:8000&amp;lt;/code&amp;gt;) and the Vue dev server (&amp;lt;code&amp;gt;http://localhost:5173&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
=== Production ===&lt;br /&gt;
&lt;br /&gt;
* Set &amp;lt;code&amp;gt;DJANGO_ENV=production&amp;lt;/code&amp;gt;&lt;br /&gt;
* Build the frontend: &amp;lt;code&amp;gt;cd frontend &amp;amp;&amp;amp; npm run build&amp;lt;/code&amp;gt;&lt;br /&gt;
* Serve with Gunicorn behind a reverse proxy&lt;br /&gt;
* Static files are collected into the &amp;lt;code&amp;gt;static/&amp;lt;/code&amp;gt; directory&lt;br /&gt;
* The production frontend points to &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Example: A Finished Character ==&lt;br /&gt;
&lt;br /&gt;
The following is an example of a character from an actual playthrough stored in the application database. It demonstrates how the game&#039;s mechanics play out over an extended narrative.&lt;br /&gt;
&lt;br /&gt;
=== Karmiš (formerly Ekurzu / Naram) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Description:&#039;&#039;&#039; Naram was a temple scribe in the ancient city of Mari: meticulous, observant, and deeply entangled in the political and spiritual life of the city. His life was shaped by clay tablets, omens, and whispered secrets carried through palace corridors and temple courtyards. Behind his calm demeanor lies a mind constantly at work—recording, interpreting, and surviving.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Prompts visited:&#039;&#039;&#039; 34 prompts across variations, including: 1a, 4a, 11a–c, 12a, 17a, 20a, 24a, 25a, 32a–b, 34a–36a, 37a–41c, 43a–c, 47a, 52a, 57a, 64a, 66a, 68a, 71a, 73a.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Final prompt:&#039;&#039;&#039; 73a&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory slots lost:&#039;&#039;&#039; 1 · &#039;&#039;&#039;Memory slots gained:&#039;&#039;&#039; 1 (net effect: standard 5-memory limit)&lt;br /&gt;
&lt;br /&gt;
==== Epilogue ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;blockquote&amp;gt;&lt;br /&gt;
He has not died. Let me say that plainly. There will be no grave. He is alive, but quietly, deliberately, and utterly outside your reach.&lt;br /&gt;
&lt;br /&gt;
When I first met him, I did not understand what he was. He wore his years like dust on old vellum—thin, barely visible, but ever present. [...] Over time, I became his mirror, then his partner, and finally his historian.&lt;br /&gt;
&lt;br /&gt;
But someone has to begin the telling. Someone has to place the first stone. So this book begins in a house of quiet gardens and low ceilings, where he writes in the mornings and feeds only when he must. [...]&lt;br /&gt;
&lt;br /&gt;
Just a man who remembered too much, for too long. And chose, in the end, &#039;&#039;peace&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
— &#039;&#039;Mirelde&#039;&#039;&lt;br /&gt;
&amp;lt;/blockquote&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Statistics ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Metric !! Count&lt;br /&gt;
|-&lt;br /&gt;
| Total memories || 16&lt;br /&gt;
|-&lt;br /&gt;
| Active memories || 5&lt;br /&gt;
|-&lt;br /&gt;
| Diary memories || 3&lt;br /&gt;
|-&lt;br /&gt;
| Lost (forgotten) memories || 8&lt;br /&gt;
|-&lt;br /&gt;
| Total experiences || 37&lt;br /&gt;
|-&lt;br /&gt;
| Skills || 11 (3 lost during play)&lt;br /&gt;
|-&lt;br /&gt;
| Resources || 10 (6 lost during play)&lt;br /&gt;
|-&lt;br /&gt;
| Marks || 2&lt;br /&gt;
|-&lt;br /&gt;
| Known characters || 11 (6 lost during play)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Active Memories (at end of game) ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Memory Title !! Location !! Experiences&lt;br /&gt;
|-&lt;br /&gt;
| Memory 1 || Active || &amp;quot;Mortal life&amp;quot; (Initial), &amp;quot;The Name Given in Ash&amp;quot; (24a)&lt;br /&gt;
|-&lt;br /&gt;
| The Thorn and the Quill || Active || 1 experience (35a)&lt;br /&gt;
|-&lt;br /&gt;
| The Light That Does Not Burn || Active || &amp;quot;The Dawn at Mirelde&#039;s Fire&amp;quot; (52a), &amp;quot;The Dust in the Foundation&amp;quot; (57a)&lt;br /&gt;
|-&lt;br /&gt;
| We Were Not Counted Among the Cargo || Active || &amp;quot;The Bellies of Ships and Men&amp;quot; (64a), &amp;quot;Ink Without Meaning&amp;quot; (66a)&lt;br /&gt;
|-&lt;br /&gt;
| Echoes Before the Self Was Named || Active || &amp;quot;The Weight in the Smoke&amp;quot; (71a), &amp;quot;The Clay That Knew My Name&amp;quot; (68a)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Diary Memories ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Memory Title !! Experiences&lt;br /&gt;
|-&lt;br /&gt;
| The Hill That Waits || &amp;quot;The Hollow Beneath the Cairn&amp;quot; (40a), &amp;quot;The Ring Beneath the Clay&amp;quot; (43c), &amp;quot;The Face Returned Through Time&amp;quot; (44a)&lt;br /&gt;
|-&lt;br /&gt;
| Ash of the Ledger || &amp;quot;The Last Ledger Vanishes&amp;quot; (38a), &amp;quot;The Fog Beneath Names&amp;quot; (39b), &amp;quot;The Weight of Unspoken Words&amp;quot; (34a)&lt;br /&gt;
|-&lt;br /&gt;
| The Fever-Ledger || &amp;quot;The Illness in Karkheda&amp;quot; (36a), &amp;quot;The Name That Returns in Ash&amp;quot; (41c), &amp;quot;The Echo That Cannot Translate&amp;quot; (47a)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Lost Memories ====&lt;br /&gt;
&lt;br /&gt;
Eight memories were forgotten over the course of the game, including &amp;quot;The Forged Omen&amp;quot; (Memory 2), &amp;quot;Hessa&#039;s Last Message&amp;quot; (Memory 3), &amp;quot;Duel of Stars and Signs&amp;quot; (Memory 4), &amp;quot;The Turning&amp;quot;, &amp;quot;The Name That Cannot Burn&amp;quot;, &amp;quot;The Blood Between Accusations&amp;quot;, &amp;quot;Trade Beneath Empire&amp;quot;, and &amp;quot;What Was Meant to Last&amp;quot;. These losses illustrate the game&#039;s core mechanic: as new experiences accumulate, the vampire is forced to sacrifice older memories, creating a poignant sense of identity erosion across the centuries.&lt;br /&gt;
&lt;br /&gt;
==== Skills ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Skill !! Status !! Description (excerpt)&lt;br /&gt;
|-&lt;br /&gt;
| Ash-Tongue || {{Checked}} || &amp;quot;I speak in soot and suggestion, in the cracks between laws.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Bloodthirsty || {{Lost}} || &amp;quot;The scent of mortal blood calls to me like a hymn in the dark.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Ceremonial Composure || {{Checked}} || &amp;quot;In the presence of gods or kings, my face is unreadable.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Decipher Ancient Texts || {{Lost}} || &amp;quot;I can read languages long dead and spot forgeries.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| I Control the Beast || {{Lost}} || &amp;quot;When the thirst rises, I do not flinch. I meet the monster&#039;s gaze.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Ledger Without End || Normal || &amp;quot;Born from the quiet rituals of inventory and account.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Physick of the Pale Vein || Normal || &amp;quot;I feed gently, beneath the guise of care.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Remain in Unknowing || {{Checked}} || &amp;quot;I do not flee the edges of memory.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Silent Cartography || {{Checked}} || &amp;quot;I trace the unseen paths between people, places, and power.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Snare and Stillness || {{Checked}} || &amp;quot;I move with silence that makes mortals forget they heard me.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Tongue of the Unnamed || Normal || &amp;quot;I no longer understand the languages of my past, but I wear the sounds of others like masks.&amp;quot;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Resources ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Resource !! Type !! Status !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| Kept Against Forgetting || Portable / &#039;&#039;&#039;Diary&#039;&#039;&#039; || Active || The character&#039;s diary; written with Mirelde&lt;br /&gt;
|-&lt;br /&gt;
| Tablet Bearing My Name || Portable || Active || A clay tablet from the ancient past, rediscovered&lt;br /&gt;
|-&lt;br /&gt;
| The Margins of Manifest || Portable || Active || A cipher-folio cataloguing names of the enslaved&lt;br /&gt;
|-&lt;br /&gt;
| Burrowed Sanctum || Portable || Lost || An earthen chamber beneath a forgotten altar&lt;br /&gt;
|-&lt;br /&gt;
| Chamber of Whispers || Stationary || Lost || A hidden storeroom beneath the temple of Ishtar&lt;br /&gt;
|-&lt;br /&gt;
| Clay Tablets and Reed Stylus || Portable || Lost || Writing implements used by instinct, not understanding&lt;br /&gt;
|-&lt;br /&gt;
| Hidden Archive Tablet || Portable || Lost || A tablet inscribed with dynasty-breaking truths&lt;br /&gt;
|-&lt;br /&gt;
| The Monastery Diary || Portable / Diary || Lost || A former diary, lost when the monastery fell&lt;br /&gt;
|-&lt;br /&gt;
| The Silent Authority || Portable || Lost || A carved seal whose symbols can no longer be read&lt;br /&gt;
|-&lt;br /&gt;
| The Walls of Forgetting || Stationary / Diary || Lost || Memory carvings in five scripts beneath a forgotten shrine&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Known Characters ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Name !! Type !! Relationship !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Ashurban the Veiled || Immortal || Neutral || Active&lt;br /&gt;
|-&lt;br /&gt;
| Mirelde of Bracha || Immortal || Friend || Active&lt;br /&gt;
|-&lt;br /&gt;
| The One Who Does Not Answer || Mortal || Neutral || Active&lt;br /&gt;
|-&lt;br /&gt;
| Thoöni (spectral) || Immortal || Neutral || Active&lt;br /&gt;
|-&lt;br /&gt;
| Belatu || Mortal || Rival || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Ennatum || Mortal || Friend || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Hessa || Mortal || Love || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Ibbi-Zamri || Mortal || Mentor || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Mekha || Mortal || Enemy || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Pelagon || Mortal || Enemy || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Thoöni (scholar) || Mortal || Friend || Lost&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Marks ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Mark !! Description&lt;br /&gt;
|-&lt;br /&gt;
| The Unblinking Eye || An ancient eye-like scar below the collarbone; burns faintly in the presence of lies&lt;br /&gt;
|-&lt;br /&gt;
| The Gesture Forbidden || An involuntary three-fingered gesture of silence, often mistaken for mockery&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Example Experience: &amp;quot;The Bellies of Ships and Men&amp;quot; (Prompt 64a) ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;blockquote&amp;gt;&lt;br /&gt;
They arrived in numbers too vast to count: pressed between barrels, chained to holds slick with piss and seawater, branded, sick, broken. And the sailors were not much better.&lt;br /&gt;
&lt;br /&gt;
The ships would come in [...]&lt;br /&gt;
&amp;lt;/blockquote&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Date: around 1510 CE&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
This experience is part of the memory &amp;quot;We Were Not Counted Among the Cargo&amp;quot;, illustrating the vampire&#039;s witness to the Atlantic slave trade—one of many historical eras the character passes through during a millennium-spanning story.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
Tests are run with &#039;&#039;&#039;pytest&#039;&#039;&#039; and &#039;&#039;&#039;pytest-django&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
pytest&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Test files include:&lt;br /&gt;
* &amp;lt;code&amp;gt;vampire/test_models.py&amp;lt;/code&amp;gt; – model unit tests&lt;br /&gt;
* &amp;lt;code&amp;gt;tyov_api/test_api.py&amp;lt;/code&amp;gt; – API endpoint integration tests&lt;br /&gt;
* &amp;lt;code&amp;gt;tyov_api/test_models.py&amp;lt;/code&amp;gt; – API model tests&lt;br /&gt;
* &amp;lt;code&amp;gt;authentication/test_views.py&amp;lt;/code&amp;gt; – authentication tests&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
End-to-end tests use &#039;&#039;&#039;Playwright&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
cd frontend&lt;br /&gt;
npx playwright test&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Test specs include:&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/character-creation-workflow.spec.ts&amp;lt;/code&amp;gt; – character creation wizard&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/tyov-game.spec.ts&amp;lt;/code&amp;gt; – game loop testing&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/vue.spec.ts&amp;lt;/code&amp;gt; – general Vue component tests&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings – the original tabletop game&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Vue.js]]&lt;br /&gt;
* [[Single-page application]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Web applications]]&lt;br /&gt;
[[Category:Role-playing video games]]&lt;br /&gt;
[[Category:Django (web framework) applications]]&lt;br /&gt;
[[Category:Vue.js applications]]&lt;br /&gt;
[[Category:Solo role-playing games]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=385</id>
		<title>Thousand Year Old Vampire</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=385"/>
		<updated>2026-04-12T11:17:32Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Epilogue */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox &lt;br /&gt;
| 01_name = Thousand Year Old Vampire – Web Helper&lt;br /&gt;
| 02_logo =&lt;br /&gt;
| 03_developer = Michel Vuijlsteke&lt;br /&gt;
| 04_programming language = Python (Django 5), TypeScript (Vue 3)&lt;br /&gt;
| 05_operating system = Cross-platform (Web)&lt;br /&gt;
| 06_genre = Solo RPG Digital Companion&lt;br /&gt;
| 07_license = custom&lt;br /&gt;
| 08_website = &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Thousand Year Old Vampire – Web Helper&#039;&#039;&#039; (&#039;&#039;&#039;TYOV-Web&#039;&#039;&#039;) is a modern web-based implementation of the solo tabletop role-playing game &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings. The application digitises the game&#039;s complex mechanics of memory, experience, and character development, allowing players to create and guide a vampire character across centuries of unlife through an interactive browser interface. The backend is built with &#039;&#039;&#039;Django 5&#039;&#039;&#039; and &#039;&#039;&#039;Django REST Framework&#039;&#039;&#039;; the frontend is a &#039;&#039;&#039;Vue 3&#039;&#039;&#039; single-page application.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Thousand Year Old Vampire&#039;&#039; is a solo storytelling RPG in which a player creates a vampire character and guides it through an increasingly fragmented existence spanning hundreds or thousands of years. The central tension lies in the vampire&#039;s deteriorating memory: as an immortal being it accumulates experiences, but its ancient mind can only retain a limited number of memories at any time. Players must constantly choose what to remember and what to forget.&lt;br /&gt;
&lt;br /&gt;
TYOV-Web fully implements the game&#039;s rules—character creation, dice-driven prompt navigation, memory management, diary storage, skills, resources, marks, known characters, and game-ending conditions—in a reactive web interface with persistent server-side storage and a complete audit trail.&lt;br /&gt;
&lt;br /&gt;
== Game Rules ==&lt;br /&gt;
&lt;br /&gt;
=== Core Concept ===&lt;br /&gt;
&lt;br /&gt;
The player creates a vampire and progresses through numbered &#039;&#039;&#039;prompts&#039;&#039;&#039; (story scenarios). Each prompt asks the player to narrate what happens to the vampire during a particular era. The game is non-linear: dice rolls determine which prompt to visit next, and most prompts have multiple &#039;&#039;&#039;variations&#039;&#039;&#039; (A, B, C) to prevent repetition.&lt;br /&gt;
&lt;br /&gt;
=== Character Creation ===&lt;br /&gt;
&lt;br /&gt;
Character creation follows a guided six-step wizard:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Step !! Description&lt;br /&gt;
|-&lt;br /&gt;
| 1. Mortal Life || Establish the vampire&#039;s name, background, and mortal existence&lt;br /&gt;
|-&lt;br /&gt;
| 2. Mortal Characters || Define 2–4 NPCs from the vampire&#039;s human life (friends, mentors, rivals, lovers)&lt;br /&gt;
|-&lt;br /&gt;
| 3. Skills || Choose 2–3 starting skills representing mortal expertise&lt;br /&gt;
|-&lt;br /&gt;
| 4. Resources || Select initial possessions and locations&lt;br /&gt;
|-&lt;br /&gt;
| 5. Combination Experience || Write a pivotal mortal-life experience that ties characters, skills, and resources together&lt;br /&gt;
|-&lt;br /&gt;
| 6. The Turning || Narrate how and why the character was turned into a vampire&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Memory System ===&lt;br /&gt;
&lt;br /&gt;
The memory system is the heart of the game:&lt;br /&gt;
&lt;br /&gt;
* A character may hold a maximum of &#039;&#039;&#039;5 active memories&#039;&#039;&#039; (adjustable by prompt rules and permanent effects).&lt;br /&gt;
* Each memory can contain up to &#039;&#039;&#039;3 experiences&#039;&#039;&#039; (individual narrative entries).&lt;br /&gt;
* When the limit is exceeded, the player must &#039;&#039;&#039;forget&#039;&#039;&#039; (permanently lose) a memory, or &#039;&#039;&#039;move&#039;&#039;&#039; one to a &#039;&#039;&#039;diary&#039;&#039;&#039; (if the character possesses one).&lt;br /&gt;
* A &#039;&#039;&#039;diary&#039;&#039;&#039; is a special resource that stores up to &#039;&#039;&#039;4 additional memories&#039;&#039;&#039; outside the active limit.&lt;br /&gt;
* If the diary resource is lost, all memories stored in it are also lost.&lt;br /&gt;
* Some prompts grant or remove permanent memory slots (&amp;lt;code&amp;gt;memory_slot_gain&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;memory_slot_loss&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
=== Dice Rolling and Prompt Navigation ===&lt;br /&gt;
&lt;br /&gt;
Each turn the player rolls two dice:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;D10&#039;&#039;&#039; (1–10) minus &#039;&#039;&#039;D6&#039;&#039;&#039; (1–6) = result (range: −5 to +9).&lt;br /&gt;
* A &#039;&#039;&#039;positive&#039;&#039;&#039; result moves forward that many prompts.&lt;br /&gt;
* A &#039;&#039;&#039;negative&#039;&#039;&#039; result moves backward.&lt;br /&gt;
* A result of &#039;&#039;&#039;zero&#039;&#039;&#039; stays at the current prompt number but selects the next unused variation.&lt;br /&gt;
&lt;br /&gt;
Variations are visited in order A → B → C. If all variations of a prompt have been used, the system finds the next available variation in prompt order.&lt;br /&gt;
&lt;br /&gt;
=== Turn Structure ===&lt;br /&gt;
&lt;br /&gt;
Each game turn follows this sequence:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Prompt Presentation&#039;&#039;&#039; – The current prompt text and any special rules are displayed.&lt;br /&gt;
# &#039;&#039;&#039;Dice Roll&#039;&#039;&#039; – The player rolls D10 − D6 to determine the next prompt.&lt;br /&gt;
# &#039;&#039;&#039;Experience Creation&#039;&#039;&#039; – The player writes at least one narrative experience for the current prompt.&lt;br /&gt;
# &#039;&#039;&#039;Character Updates&#039;&#039;&#039; – Skills, resources, marks, and known characters may be added, edited, checked, or removed.&lt;br /&gt;
# &#039;&#039;&#039;Memory Management&#039;&#039;&#039; – The player resolves any memory overflow (forget or move to diary).&lt;br /&gt;
# &#039;&#039;&#039;Validation&#039;&#039;&#039; – The system verifies all rules are satisfied (at least one experience added, memory limits respected, etc.).&lt;br /&gt;
# &#039;&#039;&#039;Continue&#039;&#039;&#039; – All pending changes are atomically committed and the game advances to the next prompt.&lt;br /&gt;
&lt;br /&gt;
=== Character Attributes ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Attribute !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Skills&#039;&#039;&#039; || Learned abilities; can be &#039;&#039;normal&#039;&#039; (latent), &#039;&#039;checked&#039;&#039; (used/active), or &#039;&#039;lost&#039;&#039; (permanently removed).&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Resources&#039;&#039;&#039; || Possessions or locations; typed as &#039;&#039;portable&#039;&#039; or &#039;&#039;stationary&#039;&#039;. One resource may be flagged as a &#039;&#039;diary&#039;&#039;.&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Marks&#039;&#039;&#039; || Physical or psychological scars that accumulate over time.&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Known Characters&#039;&#039;&#039; || NPCs the vampire has encountered; typed as &#039;&#039;mortal&#039;&#039; or &#039;&#039;immortal&#039;&#039;; may be lost (dead, vanished, etc.).&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Special Prompt Rules ===&lt;br /&gt;
&lt;br /&gt;
Certain prompts carry special mechanics encoded as rules:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Rule !! Effect&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;no_experience&amp;lt;/code&amp;gt; || Experience creation is disabled for this turn&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;allow_name_change&amp;lt;/code&amp;gt; || The player may change the vampire&#039;s name&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_modification&amp;lt;/code&amp;gt; || The player may edit the text of existing experiences&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slot_loss&amp;lt;/code&amp;gt; || Permanent reduction of memory capacity by 1&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slot_gain&amp;lt;/code&amp;gt; || Permanent increase of memory capacity by 1&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;game_over&amp;lt;/code&amp;gt; || The game ends; the player writes an epilogue&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Game Ending ===&lt;br /&gt;
&lt;br /&gt;
The game ends when the player reaches a prompt with the &amp;lt;code&amp;gt;game_over&amp;lt;/code&amp;gt; rule (or under other conditions defined by the original game). The player writes an &#039;&#039;&#039;epilogue&#039;&#039;&#039;—a final reflection on the vampire&#039;s story—and the character is archived. A statistics summary shows prompt count, memories, experiences, skills, resources, marks, and characters accumulated during the playthrough.&lt;br /&gt;
&lt;br /&gt;
== Architecture ==&lt;br /&gt;
&lt;br /&gt;
=== Technology Stack ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Layer !! Technology !! Version&lt;br /&gt;
|-&lt;br /&gt;
| Backend framework || Django || 5.0.6&lt;br /&gt;
|-&lt;br /&gt;
| REST API || Django REST Framework || 3.15.1&lt;br /&gt;
|-&lt;br /&gt;
| Authentication || Simple JWT (custom) || 5.3.0&lt;br /&gt;
|-&lt;br /&gt;
| CORS || django-cors-headers || 4.3.1&lt;br /&gt;
|-&lt;br /&gt;
| Image handling || Pillow || 11.3.0&lt;br /&gt;
|-&lt;br /&gt;
| Production server || Gunicorn || 21.2.0&lt;br /&gt;
|-&lt;br /&gt;
| Frontend framework || Vue.js || 3.5&lt;br /&gt;
|-&lt;br /&gt;
| State management || Pinia || 3.0&lt;br /&gt;
|-&lt;br /&gt;
| Routing || Vue Router || 4.5&lt;br /&gt;
|-&lt;br /&gt;
| HTTP client || Axios || 1.10&lt;br /&gt;
|-&lt;br /&gt;
| CSS framework || Bootstrap || 5.3&lt;br /&gt;
|-&lt;br /&gt;
| Build tool || Vite || 7.0&lt;br /&gt;
|-&lt;br /&gt;
| Language || TypeScript || 5.8&lt;br /&gt;
|-&lt;br /&gt;
| Testing (backend) || pytest + pytest-django || 8.0 / 4.8&lt;br /&gt;
|-&lt;br /&gt;
| Testing (frontend) || Playwright || 1.53&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== High-Level Diagram ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
┌─────────────────────────────────────────────────┐&lt;br /&gt;
│               Vue 3 SPA (TypeScript)            │&lt;br /&gt;
│  ┌──────────┐ ┌──────────┐ ┌──────────────────┐│&lt;br /&gt;
│  │  Views   │ │Components│ │  Pinia Stores     ││&lt;br /&gt;
│  │ (7 pages)│ │ (14+)    │ │ auth/game/theme/  ││&lt;br /&gt;
│  │          │ │          │ │ characterCreation  ││&lt;br /&gt;
│  └────┬─────┘ └────┬─────┘ └────────┬─────────┘│&lt;br /&gt;
│       └─────────────┴────────────────┘          │&lt;br /&gt;
│                     │ Axios                     │&lt;br /&gt;
│                     ▼                           │&lt;br /&gt;
├─────────────────────────────────────────────────┤&lt;br /&gt;
│              Django REST API                    │&lt;br /&gt;
│  /api/auth/login    /api/auth/verify            │&lt;br /&gt;
│  /api/characters/   (CRUD + game_state,         │&lt;br /&gt;
│   roll_dice, continue_story, audit_trail)       │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/memories/                 │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/experiences/              │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/skills/                   │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/resources/                │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/marks/                    │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/characters/               │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/pending-changes/          │&lt;br /&gt;
│  /api/prompts/                                  │&lt;br /&gt;
├─────────────────────────────────────────────────┤&lt;br /&gt;
│          Django ORM / SQLite                    │&lt;br /&gt;
│  Character · Memory · Experience · Skill        │&lt;br /&gt;
│  Resource · Mark · GameCharacter · Prompt        │&lt;br /&gt;
│  PendingChange · GameStateSnapshot              │&lt;br /&gt;
└─────────────────────────────────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend ==&lt;br /&gt;
&lt;br /&gt;
=== Django Apps ===&lt;br /&gt;
&lt;br /&gt;
The project is organised into three Django apps plus a settings module:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! App !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;vampire&amp;lt;/code&amp;gt; || Core domain models: Character, Memory, Experience, Skill, Resource, Mark, GameCharacter, Prompt, PendingChange, GameStateSnapshot&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;tyov_api&amp;lt;/code&amp;gt; || REST API layer: serialisers, viewsets, URL routing, permissions, middleware&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;authentication&amp;lt;/code&amp;gt; || JWT-based login and token verification endpoints&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;tyov_backend&amp;lt;/code&amp;gt; || Django project settings, URL root, WSGI/ASGI configuration&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Data Models ===&lt;br /&gt;
&lt;br /&gt;
==== Character ====&lt;br /&gt;
&lt;br /&gt;
The central model, owned by a Django &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; via a ForeignKey:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Field !! Type !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt; || CharField(200) || Current name of the vampire&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt; || TextField || Background/appearance description&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;image&amp;lt;/code&amp;gt; || ImageField || Optional character portrait&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;current_prompt&amp;lt;/code&amp;gt; || CharField(10) || ID of the current prompt (e.g. &amp;quot;17a&amp;quot;)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;last_dice_roll&amp;lt;/code&amp;gt; || JSONField || Stores D10, D6, and result of the last roll&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;visited_prompts&amp;lt;/code&amp;gt; || JSONField(list) || List of all prompt IDs visited in order&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slots_lost&amp;lt;/code&amp;gt; || IntegerField || Permanent memory slot reductions&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slots_gained&amp;lt;/code&amp;gt; || IntegerField || Permanent memory slot increases&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;creation_step&amp;lt;/code&amp;gt; || IntegerField(1–6) || Tracks progress through the creation wizard&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;is_creation_complete&amp;lt;/code&amp;gt; || BooleanField || True when all six creation steps are finished&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;creation_data&amp;lt;/code&amp;gt; || JSONField || Temporary storage during creation&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;epilogue&amp;lt;/code&amp;gt; || TextField || Player&#039;s final reflection (set at game end)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;is_completed&amp;lt;/code&amp;gt; || BooleanField || Whether the story has ended&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;completed_at&amp;lt;/code&amp;gt; || DateTimeField || Timestamp of story completion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Memory ====&lt;br /&gt;
&lt;br /&gt;
A memory belongs to a Character and holds up to 3 Experiences:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; – descriptive name (unique per character)&lt;br /&gt;
* &amp;lt;code&amp;gt;in_diary&amp;lt;/code&amp;gt; – whether the memory is stored in the diary&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt; – whether the memory has been forgotten&lt;br /&gt;
&lt;br /&gt;
==== Experience ====&lt;br /&gt;
&lt;br /&gt;
An individual narrative entry within a Memory:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; – optional short description&lt;br /&gt;
* &amp;lt;code&amp;gt;content&amp;lt;/code&amp;gt; – the narrative text&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; – which prompt generated this experience&lt;br /&gt;
* &amp;lt;code&amp;gt;date_info&amp;lt;/code&amp;gt; – temporal context (e.g. &amp;quot;Winter, 431 CE&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
==== Skill ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;status&amp;lt;/code&amp;gt; – one of &amp;lt;code&amp;gt;normal&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;checked&amp;lt;/code&amp;gt;, or &amp;lt;code&amp;gt;lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Resource ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;resource_type&amp;lt;/code&amp;gt; – &amp;lt;code&amp;gt;portable&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;stationary&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;is_diary&amp;lt;/code&amp;gt; – flags the special diary resource (constrained to one active diary per character)&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Mark ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== GameCharacter ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;character_type&amp;lt;/code&amp;gt; – &amp;lt;code&amp;gt;mortal&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;immortal&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;relationship&amp;lt;/code&amp;gt; – e.g. friend, rival, mentor, love, enemy, neutral&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Prompt ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; – e.g. &amp;quot;17a&amp;quot;, &amp;quot;32b&amp;quot;&lt;br /&gt;
* &amp;lt;code&amp;gt;number&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;variation&amp;lt;/code&amp;gt; – parsed components for sorting&lt;br /&gt;
* &amp;lt;code&amp;gt;content&amp;lt;/code&amp;gt; – the scenario text&lt;br /&gt;
* &amp;lt;code&amp;gt;rules&amp;lt;/code&amp;gt; – special mechanics as comma-separated tokens (e.g. &amp;lt;code&amp;gt;no_experience, allow_name_change&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
==== PendingChange ====&lt;br /&gt;
&lt;br /&gt;
Temporary storage for changes before they are atomically committed on &amp;quot;Continue&amp;quot;:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;change_type&amp;lt;/code&amp;gt; – experience, memory, skill, resource, mark, or character&lt;br /&gt;
* &amp;lt;code&amp;gt;change_data&amp;lt;/code&amp;gt; – JSON payload of the change&lt;br /&gt;
&lt;br /&gt;
==== GameStateSnapshot (Audit Trail) ====&lt;br /&gt;
&lt;br /&gt;
Automatically created each turn to record the complete game state:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;turn_number&amp;lt;/code&amp;gt; – sequential, starting from 1&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;next_prompt_id&amp;lt;/code&amp;gt; – transition&lt;br /&gt;
* &amp;lt;code&amp;gt;dice_roll&amp;lt;/code&amp;gt; – raw dice data&lt;br /&gt;
* &amp;lt;code&amp;gt;game_state&amp;lt;/code&amp;gt; – full JSON snapshot of all memories, skills, resources, etc.&lt;br /&gt;
* &amp;lt;code&amp;gt;changes_applied&amp;lt;/code&amp;gt; – JSON array of pending changes that were committed&lt;br /&gt;
* &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; – timestamp&lt;br /&gt;
&lt;br /&gt;
=== API Endpoints ===&lt;br /&gt;
&lt;br /&gt;
All endpoints require JWT authentication (except login).&lt;br /&gt;
&lt;br /&gt;
==== Authentication ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/auth/login/&amp;lt;/code&amp;gt; || Authenticate with username/password; returns JWT token&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/auth/verify/&amp;lt;/code&amp;gt; || Verify current token and return user info&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Login is rate-limited to 5 requests per minute per IP.&lt;br /&gt;
&lt;br /&gt;
==== Characters ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/&amp;lt;/code&amp;gt; || List all characters for the authenticated user&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/&amp;lt;/code&amp;gt; || Create a new character&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Retrieve full character detail (prefetched relations)&lt;br /&gt;
|-&lt;br /&gt;
| PUT/PATCH || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Update character fields&lt;br /&gt;
|-&lt;br /&gt;
| DELETE || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Delete a character&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/game_state/&amp;lt;/code&amp;gt; || Complete game state including prompt, validation, dice info&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/{id}/roll_dice/&amp;lt;/code&amp;gt; || Roll D10 − D6 and store the result&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/{id}/continue_story/&amp;lt;/code&amp;gt; || Validate, commit pending changes, advance to next prompt&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/audit_trail/&amp;lt;/code&amp;gt; || View turn-by-turn audit history&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/game-state-changes/&amp;lt;/code&amp;gt; || Timeline of state changes between turns&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Nested Character Resources ====&lt;br /&gt;
&lt;br /&gt;
For each character, full CRUD is available on:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/memories/&amp;lt;/code&amp;gt; (plus &amp;lt;code&amp;gt;move_to_diary&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;restore_lost_memory&amp;lt;/code&amp;gt; actions)&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/experiences/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/skills/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/resources/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/marks/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/characters/&amp;lt;/code&amp;gt; (known NPCs)&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/pending-changes/&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Prompts ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/prompts/&amp;lt;/code&amp;gt; || List all game prompts&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/prompts/{prompt_id}/&amp;lt;/code&amp;gt; || Retrieve a single prompt by ID&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Permissions and Security ===&lt;br /&gt;
&lt;br /&gt;
* All character data is scoped to the authenticated user via &amp;lt;code&amp;gt;IsCharacterOwner&amp;lt;/code&amp;gt; permission.&lt;br /&gt;
* JWT tokens are sent as &amp;lt;code&amp;gt;Authorization: Bearer {token}&amp;lt;/code&amp;gt; headers.&lt;br /&gt;
* The login endpoint is throttled (5/minute) to prevent brute-force attacks.&lt;br /&gt;
* All pending changes are processed inside a database &#039;&#039;&#039;transaction&#039;&#039;&#039; to guarantee atomicity.&lt;br /&gt;
* CORS headers are configured via &amp;lt;code&amp;gt;django-cors-headers&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Frontend ==&lt;br /&gt;
&lt;br /&gt;
=== Views (Pages) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! View !! Route !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;LoginView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; || Authentication form; redirects authenticated users to home&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;HomeView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt; || Dashboard showing all active characters in a card grid&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharacterCreationView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/character-creation/:id&amp;lt;/code&amp;gt; || Six-step creation wizard&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/game/:characterId&amp;lt;/code&amp;gt; || Main game loop: prompt display, experience form, dice rolling, entity management, memory display&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameEndedView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/game/:characterId/ended&amp;lt;/code&amp;gt; || End-of-story screen with statistics and epilogue&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;RecordsView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/records&amp;lt;/code&amp;gt; || Archive of current and completed characters&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;StoryView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/story/:characterId&amp;lt;/code&amp;gt; || Narrative timeline with experience history, character stats sidebar, and epilogue&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
All routes except &amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; require authentication. The router guard redirects unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
=== Key Components ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Component !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PromptSection&#039;&#039;&#039; || Displays the current prompt text, special rules, and dice roll results&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ExperienceForm&#039;&#039;&#039; || Form for writing new experiences with title, date, content, and memory selection&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;MemoriesDisplay&#039;&#039;&#039; || Shows active, diary, and lost memories with experience counts; includes move-to-diary, delete, and restore actions&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameDataSection&#039;&#039;&#039; || Reusable list for skills, resources, or marks with add/edit/delete controls&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharactersSection&#039;&#039;&#039; || Grid of known NPCs with type badges and edit/delete&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;EntityCreationWindow&#039;&#039;&#039; || Draggable modal for creating/editing skills, resources, marks&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharacterCreationWindow&#039;&#039;&#039; || Draggable modal for creating/editing NPC characters&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ExperienceEditWindow&#039;&#039;&#039; || Window for editing existing experience text&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;DiaryCreationModal&#039;&#039;&#039; || Modal for creating a diary resource and moving a memory into it&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ConfirmationDialog&#039;&#039;&#039; || Generic confirmation dialog for destructive actions&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ValidationWarning&#039;&#039;&#039; || Displays validation errors when game rules are not satisfied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== State Management (Pinia) ===&lt;br /&gt;
&lt;br /&gt;
Four Pinia stores manage application state:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Store !! Responsibility&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;auth&#039;&#039;&#039; || JWT token, user object, login/logout, token expiry handling&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;game&#039;&#039;&#039; || Characters list, current character, game state, all CRUD actions for entities, dice rolling, story continuation&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;characterCreation&#039;&#039;&#039; || Multi-step wizard data, initial character creation&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;theme&#039;&#039;&#039; || Dark/light mode toggle with localStorage persistence&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== API Service Layer ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;api.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Axios instance with base URL auto-detection (development vs. production), request interceptor for JWT headers, response interceptor for 401/token-expiry handling.&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;gameService.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – All API call methods: authentication, character CRUD, dice rolling, story continuation, memory/experience/skill/resource/mark/character management.&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;entityService.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Generic &amp;lt;code&amp;gt;EntityService&amp;lt;/code&amp;gt; class instantiated for skills, resources, marks, and characters for DRY CRUD operations.&lt;br /&gt;
&lt;br /&gt;
=== Composables ===&lt;br /&gt;
&lt;br /&gt;
Vue composables encapsulate reusable business logic:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useGameData()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Computed properties derived from the game store (current character, memories, skills, resources, etc.)&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useExperienceForm()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Form state, validation, memory selection, and reset logic&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useEntityManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Generic CRUD operations with optional confirmation dialogs&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useSkillManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useResourceManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useMarkManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useMemoryManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Specialised wrappers&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useConfirmationDialog()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Promise-based modal confirmation&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useMemoryDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Display logic for memories including pending-change awareness&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useExperienceDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Markdown rendering for experience content&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;usePendingChangesDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Badge rendering for undo-able pending changes&lt;br /&gt;
&lt;br /&gt;
== Audit Trail ==&lt;br /&gt;
&lt;br /&gt;
The application includes an automatic audit trail that captures the complete game state every time the player advances to a new prompt.&lt;br /&gt;
&lt;br /&gt;
Each &#039;&#039;&#039;GameStateSnapshot&#039;&#039;&#039; records:&lt;br /&gt;
&lt;br /&gt;
* The turn number (sequential from 1)&lt;br /&gt;
* The prompt transition (e.g. 36a → 34a)&lt;br /&gt;
* The dice roll&lt;br /&gt;
* A complete JSON snapshot of all memories, skills, resources, marks, and known characters&lt;br /&gt;
* All pending changes that were committed&lt;br /&gt;
&lt;br /&gt;
The audit trail is accessible via:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;API&#039;&#039;&#039;: &amp;lt;code&amp;gt;GET /api/characters/{id}/audit_trail/&amp;lt;/code&amp;gt; with optional &amp;lt;code&amp;gt;?turn=N&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;?limit=N&amp;lt;/code&amp;gt; parameters&lt;br /&gt;
* &#039;&#039;&#039;Management command&#039;&#039;&#039;: &amp;lt;code&amp;gt;python manage.py view_audit_trail {id} [--summary] [--turn N] [--export file.json]&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;Timeline API&#039;&#039;&#039;: &amp;lt;code&amp;gt;GET /api/characters/{id}/game-state-changes/&amp;lt;/code&amp;gt; for a structured timeline of all state changes&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
=== Local Development ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
.\startup.ps1&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This script creates a virtual environment, installs dependencies, runs migrations, and starts both the Django backend (&amp;lt;code&amp;gt;http://localhost:8000&amp;lt;/code&amp;gt;) and the Vue dev server (&amp;lt;code&amp;gt;http://localhost:5173&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
=== Production ===&lt;br /&gt;
&lt;br /&gt;
* Set &amp;lt;code&amp;gt;DJANGO_ENV=production&amp;lt;/code&amp;gt;&lt;br /&gt;
* Build the frontend: &amp;lt;code&amp;gt;cd frontend &amp;amp;&amp;amp; npm run build&amp;lt;/code&amp;gt;&lt;br /&gt;
* Serve with Gunicorn behind a reverse proxy&lt;br /&gt;
* Static files are collected into the &amp;lt;code&amp;gt;static/&amp;lt;/code&amp;gt; directory&lt;br /&gt;
* The production frontend points to &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Example: A Finished Character ==&lt;br /&gt;
&lt;br /&gt;
The following is an example of a character from an actual playthrough stored in the application database. It demonstrates how the game&#039;s mechanics play out over an extended narrative.&lt;br /&gt;
&lt;br /&gt;
=== Karmiš (formerly Ekurzu / Naram) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Description:&#039;&#039;&#039; Naram was a temple scribe in the ancient city of Mari: meticulous, observant, and deeply entangled in the political and spiritual life of the city. His life was shaped by clay tablets, omens, and whispered secrets carried through palace corridors and temple courtyards. Behind his calm demeanor lies a mind constantly at work—recording, interpreting, and surviving.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Prompts visited:&#039;&#039;&#039; 34 prompts across variations, including: 1a, 4a, 11a–c, 12a, 17a, 20a, 24a, 25a, 32a–b, 34a–36a, 37a–41c, 43a–c, 47a, 52a, 57a, 64a, 66a, 68a, 71a, 73a.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Final prompt:&#039;&#039;&#039; 73a&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory slots lost:&#039;&#039;&#039; 1 · &#039;&#039;&#039;Memory slots gained:&#039;&#039;&#039; 1 (net effect: standard 5-memory limit)&lt;br /&gt;
&lt;br /&gt;
==== Epilogue ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;blockquote&amp;gt;&lt;br /&gt;
He has not died. Let me say that plainly. There will be no grave. He is alive, but quietly, deliberately, and utterly outside your reach.&lt;br /&gt;
&lt;br /&gt;
When I first met him, I did not understand what he was. He wore his years like dust on old vellum—thin, barely visible, but ever present. [...] Over time, I became his mirror, then his partner, and finally his historian.&lt;br /&gt;
&lt;br /&gt;
But someone has to begin the telling. Someone has to place the first stone. So this book begins in a house of quiet gardens and low ceilings, where he writes in the mornings and feeds only when he must. [...]&lt;br /&gt;
&lt;br /&gt;
Just a man who remembered too much, for too long. And chose, in the end, &#039;&#039;peace&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
— &#039;&#039;Mirelde&#039;&#039;&lt;br /&gt;
&amp;lt;/blockquote&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Statistics ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Metric !! Count&lt;br /&gt;
|-&lt;br /&gt;
| Total memories || 16&lt;br /&gt;
|-&lt;br /&gt;
| Active memories || 5&lt;br /&gt;
|-&lt;br /&gt;
| Diary memories || 3&lt;br /&gt;
|-&lt;br /&gt;
| Lost (forgotten) memories || 8&lt;br /&gt;
|-&lt;br /&gt;
| Total experiences || 37&lt;br /&gt;
|-&lt;br /&gt;
| Skills || 11 (3 lost during play)&lt;br /&gt;
|-&lt;br /&gt;
| Resources || 10 (6 lost during play)&lt;br /&gt;
|-&lt;br /&gt;
| Marks || 2&lt;br /&gt;
|-&lt;br /&gt;
| Known characters || 11 (6 lost during play)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Active Memories (at end of game) ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Memory Title !! Location !! Experiences&lt;br /&gt;
|-&lt;br /&gt;
| Memory 1 || Active || &amp;quot;Mortal life&amp;quot; (Initial), &amp;quot;The Name Given in Ash&amp;quot; (24a)&lt;br /&gt;
|-&lt;br /&gt;
| The Thorn and the Quill || Active || 1 experience (35a)&lt;br /&gt;
|-&lt;br /&gt;
| The Light That Does Not Burn || Active || &amp;quot;The Dawn at Mirelde&#039;s Fire&amp;quot; (52a), &amp;quot;The Dust in the Foundation&amp;quot; (57a)&lt;br /&gt;
|-&lt;br /&gt;
| We Were Not Counted Among the Cargo || Active || &amp;quot;The Bellies of Ships and Men&amp;quot; (64a), &amp;quot;Ink Without Meaning&amp;quot; (66a)&lt;br /&gt;
|-&lt;br /&gt;
| Echoes Before the Self Was Named || Active || &amp;quot;The Weight in the Smoke&amp;quot; (71a), &amp;quot;The Clay That Knew My Name&amp;quot; (68a)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Diary Memories ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Memory Title !! Experiences&lt;br /&gt;
|-&lt;br /&gt;
| The Hill That Waits || &amp;quot;The Hollow Beneath the Cairn&amp;quot; (40a), &amp;quot;The Ring Beneath the Clay&amp;quot; (43c), &amp;quot;The Face Returned Through Time&amp;quot; (44a)&lt;br /&gt;
|-&lt;br /&gt;
| Ash of the Ledger || &amp;quot;The Last Ledger Vanishes&amp;quot; (38a), &amp;quot;The Fog Beneath Names&amp;quot; (39b), &amp;quot;The Weight of Unspoken Words&amp;quot; (34a)&lt;br /&gt;
|-&lt;br /&gt;
| The Fever-Ledger || &amp;quot;The Illness in Karkheda&amp;quot; (36a), &amp;quot;The Name That Returns in Ash&amp;quot; (41c), &amp;quot;The Echo That Cannot Translate&amp;quot; (47a)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Lost Memories ====&lt;br /&gt;
&lt;br /&gt;
Eight memories were forgotten over the course of the game, including &amp;quot;The Forged Omen&amp;quot; (Memory 2), &amp;quot;Hessa&#039;s Last Message&amp;quot; (Memory 3), &amp;quot;Duel of Stars and Signs&amp;quot; (Memory 4), &amp;quot;The Turning&amp;quot;, &amp;quot;The Name That Cannot Burn&amp;quot;, &amp;quot;The Blood Between Accusations&amp;quot;, &amp;quot;Trade Beneath Empire&amp;quot;, and &amp;quot;What Was Meant to Last&amp;quot;. These losses illustrate the game&#039;s core mechanic: as new experiences accumulate, the vampire is forced to sacrifice older memories, creating a poignant sense of identity erosion across the centuries.&lt;br /&gt;
&lt;br /&gt;
==== Skills ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Skill !! Status !! Description (excerpt)&lt;br /&gt;
|-&lt;br /&gt;
| Ash-Tongue || {{Checked}} || &amp;quot;I speak in soot and suggestion, in the cracks between laws.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Bloodthirsty || {{Lost}} || &amp;quot;The scent of mortal blood calls to me like a hymn in the dark.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Ceremonial Composure || {{Checked}} || &amp;quot;In the presence of gods or kings, my face is unreadable.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Decipher Ancient Texts || {{Lost}} || &amp;quot;I can read languages long dead and spot forgeries.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| I Control the Beast || {{Lost}} || &amp;quot;When the thirst rises, I do not flinch. I meet the monster&#039;s gaze.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Ledger Without End || Normal || &amp;quot;Born from the quiet rituals of inventory and account.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Physick of the Pale Vein || Normal || &amp;quot;I feed gently, beneath the guise of care.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Remain in Unknowing || {{Checked}} || &amp;quot;I do not flee the edges of memory.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Silent Cartography || {{Checked}} || &amp;quot;I trace the unseen paths between people, places, and power.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Snare and Stillness || {{Checked}} || &amp;quot;I move with silence that makes mortals forget they heard me.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Tongue of the Unnamed || Normal || &amp;quot;I no longer understand the languages of my past, but I wear the sounds of others like masks.&amp;quot;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Resources ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Resource !! Type !! Status !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| Kept Against Forgetting || Portable / &#039;&#039;&#039;Diary&#039;&#039;&#039; || Active || The character&#039;s diary; written with Mirelde&lt;br /&gt;
|-&lt;br /&gt;
| Tablet Bearing My Name || Portable || Active || A clay tablet from the ancient past, rediscovered&lt;br /&gt;
|-&lt;br /&gt;
| The Margins of Manifest || Portable || Active || A cipher-folio cataloguing names of the enslaved&lt;br /&gt;
|-&lt;br /&gt;
| Burrowed Sanctum || Portable || Lost || An earthen chamber beneath a forgotten altar&lt;br /&gt;
|-&lt;br /&gt;
| Chamber of Whispers || Stationary || Lost || A hidden storeroom beneath the temple of Ishtar&lt;br /&gt;
|-&lt;br /&gt;
| Clay Tablets and Reed Stylus || Portable || Lost || Writing implements used by instinct, not understanding&lt;br /&gt;
|-&lt;br /&gt;
| Hidden Archive Tablet || Portable || Lost || A tablet inscribed with dynasty-breaking truths&lt;br /&gt;
|-&lt;br /&gt;
| The Monastery Diary || Portable / Diary || Lost || A former diary, lost when the monastery fell&lt;br /&gt;
|-&lt;br /&gt;
| The Silent Authority || Portable || Lost || A carved seal whose symbols can no longer be read&lt;br /&gt;
|-&lt;br /&gt;
| The Walls of Forgetting || Stationary / Diary || Lost || Memory carvings in five scripts beneath a forgotten shrine&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Known Characters ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Name !! Type !! Relationship !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Ashurban the Veiled || Immortal || Neutral || Active&lt;br /&gt;
|-&lt;br /&gt;
| Mirelde of Bracha || Immortal || Friend || Active&lt;br /&gt;
|-&lt;br /&gt;
| The One Who Does Not Answer || Mortal || Neutral || Active&lt;br /&gt;
|-&lt;br /&gt;
| Thoöni (spectral) || Immortal || Neutral || Active&lt;br /&gt;
|-&lt;br /&gt;
| Belatu || Mortal || Rival || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Ennatum || Mortal || Friend || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Hessa || Mortal || Love || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Ibbi-Zamri || Mortal || Mentor || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Mekha || Mortal || Enemy || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Pelagon || Mortal || Enemy || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Thoöni (scholar) || Mortal || Friend || Lost&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Marks ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Mark !! Description&lt;br /&gt;
|-&lt;br /&gt;
| The Unblinking Eye || An ancient eye-like scar below the collarbone; burns faintly in the presence of lies&lt;br /&gt;
|-&lt;br /&gt;
| The Gesture Forbidden || An involuntary three-fingered gesture of silence, often mistaken for mockery&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Example Experience: &amp;quot;The Bellies of Ships and Men&amp;quot; (Prompt 64a) ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;blockquote&amp;gt;&lt;br /&gt;
They arrived in numbers too vast to count—pressed between barrels, chained to holds slick with piss and seawater, branded, sick, broken. And the sailors were not much better.&lt;br /&gt;
&lt;br /&gt;
The ships would come in [...]&lt;br /&gt;
&amp;lt;/blockquote&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Date: around 1510 CE&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
This experience is part of the memory &amp;quot;We Were Not Counted Among the Cargo&amp;quot;, illustrating the vampire&#039;s witness to the Atlantic slave trade—one of many historical eras the character passes through during a millennium-spanning story.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
Tests are run with &#039;&#039;&#039;pytest&#039;&#039;&#039; and &#039;&#039;&#039;pytest-django&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
pytest&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Test files include:&lt;br /&gt;
* &amp;lt;code&amp;gt;vampire/test_models.py&amp;lt;/code&amp;gt; – model unit tests&lt;br /&gt;
* &amp;lt;code&amp;gt;tyov_api/test_api.py&amp;lt;/code&amp;gt; – API endpoint integration tests&lt;br /&gt;
* &amp;lt;code&amp;gt;tyov_api/test_models.py&amp;lt;/code&amp;gt; – API model tests&lt;br /&gt;
* &amp;lt;code&amp;gt;authentication/test_views.py&amp;lt;/code&amp;gt; – authentication tests&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
End-to-end tests use &#039;&#039;&#039;Playwright&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
cd frontend&lt;br /&gt;
npx playwright test&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Test specs include:&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/character-creation-workflow.spec.ts&amp;lt;/code&amp;gt; – character creation wizard&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/tyov-game.spec.ts&amp;lt;/code&amp;gt; – game loop testing&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/vue.spec.ts&amp;lt;/code&amp;gt; – general Vue component tests&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings – the original tabletop game&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Vue.js]]&lt;br /&gt;
* [[Single-page application]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Web applications]]&lt;br /&gt;
[[Category:Role-playing video games]]&lt;br /&gt;
[[Category:Django (web framework) applications]]&lt;br /&gt;
[[Category:Vue.js applications]]&lt;br /&gt;
[[Category:Solo role-playing games]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=384</id>
		<title>Thousand Year Old Vampire</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Thousand_Year_Old_Vampire&amp;diff=384"/>
		<updated>2026-04-12T11:15:40Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Created page with &amp;quot;{{Infobox  | 01_name = Thousand Year Old Vampire – Web Helper | 02_logo = | 03_developer = Michel Vuijlsteke | 04_programming language = Python (Django 5), TypeScript (Vue 3) | 05_operating system = Cross-platform (Web) | 06_genre = Solo RPG Digital Companion | 07_license = custom | 08_website = &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt; }}  &amp;#039;&amp;#039;&amp;#039;Thousand Year Old Vampire – Web Helper&amp;#039;&amp;#039;&amp;#039; (&amp;#039;&amp;#039;&amp;#039;TYOV-Web&amp;#039;&amp;#039;&amp;#039;) is a modern web-based implementation of the solo tabletop role-playi...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox &lt;br /&gt;
| 01_name = Thousand Year Old Vampire – Web Helper&lt;br /&gt;
| 02_logo =&lt;br /&gt;
| 03_developer = Michel Vuijlsteke&lt;br /&gt;
| 04_programming language = Python (Django 5), TypeScript (Vue 3)&lt;br /&gt;
| 05_operating system = Cross-platform (Web)&lt;br /&gt;
| 06_genre = Solo RPG Digital Companion&lt;br /&gt;
| 07_license = custom&lt;br /&gt;
| 08_website = &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Thousand Year Old Vampire – Web Helper&#039;&#039;&#039; (&#039;&#039;&#039;TYOV-Web&#039;&#039;&#039;) is a modern web-based implementation of the solo tabletop role-playing game &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings. The application digitises the game&#039;s complex mechanics of memory, experience, and character development, allowing players to create and guide a vampire character across centuries of unlife through an interactive browser interface. The backend is built with &#039;&#039;&#039;Django 5&#039;&#039;&#039; and &#039;&#039;&#039;Django REST Framework&#039;&#039;&#039;; the frontend is a &#039;&#039;&#039;Vue 3&#039;&#039;&#039; single-page application.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Thousand Year Old Vampire&#039;&#039; is a solo storytelling RPG in which a player creates a vampire character and guides it through an increasingly fragmented existence spanning hundreds or thousands of years. The central tension lies in the vampire&#039;s deteriorating memory: as an immortal being it accumulates experiences, but its ancient mind can only retain a limited number of memories at any time. Players must constantly choose what to remember and what to forget.&lt;br /&gt;
&lt;br /&gt;
TYOV-Web fully implements the game&#039;s rules—character creation, dice-driven prompt navigation, memory management, diary storage, skills, resources, marks, known characters, and game-ending conditions—in a reactive web interface with persistent server-side storage and a complete audit trail.&lt;br /&gt;
&lt;br /&gt;
== Game Rules ==&lt;br /&gt;
&lt;br /&gt;
=== Core Concept ===&lt;br /&gt;
&lt;br /&gt;
The player creates a vampire and progresses through numbered &#039;&#039;&#039;prompts&#039;&#039;&#039; (story scenarios). Each prompt asks the player to narrate what happens to the vampire during a particular era. The game is non-linear: dice rolls determine which prompt to visit next, and most prompts have multiple &#039;&#039;&#039;variations&#039;&#039;&#039; (A, B, C) to prevent repetition.&lt;br /&gt;
&lt;br /&gt;
=== Character Creation ===&lt;br /&gt;
&lt;br /&gt;
Character creation follows a guided six-step wizard:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Step !! Description&lt;br /&gt;
|-&lt;br /&gt;
| 1. Mortal Life || Establish the vampire&#039;s name, background, and mortal existence&lt;br /&gt;
|-&lt;br /&gt;
| 2. Mortal Characters || Define 2–4 NPCs from the vampire&#039;s human life (friends, mentors, rivals, lovers)&lt;br /&gt;
|-&lt;br /&gt;
| 3. Skills || Choose 2–3 starting skills representing mortal expertise&lt;br /&gt;
|-&lt;br /&gt;
| 4. Resources || Select initial possessions and locations&lt;br /&gt;
|-&lt;br /&gt;
| 5. Combination Experience || Write a pivotal mortal-life experience that ties characters, skills, and resources together&lt;br /&gt;
|-&lt;br /&gt;
| 6. The Turning || Narrate how and why the character was turned into a vampire&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Memory System ===&lt;br /&gt;
&lt;br /&gt;
The memory system is the heart of the game:&lt;br /&gt;
&lt;br /&gt;
* A character may hold a maximum of &#039;&#039;&#039;5 active memories&#039;&#039;&#039; (adjustable by prompt rules and permanent effects).&lt;br /&gt;
* Each memory can contain up to &#039;&#039;&#039;3 experiences&#039;&#039;&#039; (individual narrative entries).&lt;br /&gt;
* When the limit is exceeded, the player must &#039;&#039;&#039;forget&#039;&#039;&#039; (permanently lose) a memory, or &#039;&#039;&#039;move&#039;&#039;&#039; one to a &#039;&#039;&#039;diary&#039;&#039;&#039; (if the character possesses one).&lt;br /&gt;
* A &#039;&#039;&#039;diary&#039;&#039;&#039; is a special resource that stores up to &#039;&#039;&#039;4 additional memories&#039;&#039;&#039; outside the active limit.&lt;br /&gt;
* If the diary resource is lost, all memories stored in it are also lost.&lt;br /&gt;
* Some prompts grant or remove permanent memory slots (&amp;lt;code&amp;gt;memory_slot_gain&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;memory_slot_loss&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
=== Dice Rolling and Prompt Navigation ===&lt;br /&gt;
&lt;br /&gt;
Each turn the player rolls two dice:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;D10&#039;&#039;&#039; (1–10) minus &#039;&#039;&#039;D6&#039;&#039;&#039; (1–6) = result (range: −5 to +9).&lt;br /&gt;
* A &#039;&#039;&#039;positive&#039;&#039;&#039; result moves forward that many prompts.&lt;br /&gt;
* A &#039;&#039;&#039;negative&#039;&#039;&#039; result moves backward.&lt;br /&gt;
* A result of &#039;&#039;&#039;zero&#039;&#039;&#039; stays at the current prompt number but selects the next unused variation.&lt;br /&gt;
&lt;br /&gt;
Variations are visited in order A → B → C. If all variations of a prompt have been used, the system finds the next available variation in prompt order.&lt;br /&gt;
&lt;br /&gt;
=== Turn Structure ===&lt;br /&gt;
&lt;br /&gt;
Each game turn follows this sequence:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Prompt Presentation&#039;&#039;&#039; – The current prompt text and any special rules are displayed.&lt;br /&gt;
# &#039;&#039;&#039;Dice Roll&#039;&#039;&#039; – The player rolls D10 − D6 to determine the next prompt.&lt;br /&gt;
# &#039;&#039;&#039;Experience Creation&#039;&#039;&#039; – The player writes at least one narrative experience for the current prompt.&lt;br /&gt;
# &#039;&#039;&#039;Character Updates&#039;&#039;&#039; – Skills, resources, marks, and known characters may be added, edited, checked, or removed.&lt;br /&gt;
# &#039;&#039;&#039;Memory Management&#039;&#039;&#039; – The player resolves any memory overflow (forget or move to diary).&lt;br /&gt;
# &#039;&#039;&#039;Validation&#039;&#039;&#039; – The system verifies all rules are satisfied (at least one experience added, memory limits respected, etc.).&lt;br /&gt;
# &#039;&#039;&#039;Continue&#039;&#039;&#039; – All pending changes are atomically committed and the game advances to the next prompt.&lt;br /&gt;
&lt;br /&gt;
=== Character Attributes ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Attribute !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Skills&#039;&#039;&#039; || Learned abilities; can be &#039;&#039;normal&#039;&#039; (latent), &#039;&#039;checked&#039;&#039; (used/active), or &#039;&#039;lost&#039;&#039; (permanently removed).&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Resources&#039;&#039;&#039; || Possessions or locations; typed as &#039;&#039;portable&#039;&#039; or &#039;&#039;stationary&#039;&#039;. One resource may be flagged as a &#039;&#039;diary&#039;&#039;.&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Marks&#039;&#039;&#039; || Physical or psychological scars that accumulate over time.&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Known Characters&#039;&#039;&#039; || NPCs the vampire has encountered; typed as &#039;&#039;mortal&#039;&#039; or &#039;&#039;immortal&#039;&#039;; may be lost (dead, vanished, etc.).&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Special Prompt Rules ===&lt;br /&gt;
&lt;br /&gt;
Certain prompts carry special mechanics encoded as rules:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Rule !! Effect&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;no_experience&amp;lt;/code&amp;gt; || Experience creation is disabled for this turn&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;allow_name_change&amp;lt;/code&amp;gt; || The player may change the vampire&#039;s name&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_modification&amp;lt;/code&amp;gt; || The player may edit the text of existing experiences&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slot_loss&amp;lt;/code&amp;gt; || Permanent reduction of memory capacity by 1&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slot_gain&amp;lt;/code&amp;gt; || Permanent increase of memory capacity by 1&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;game_over&amp;lt;/code&amp;gt; || The game ends; the player writes an epilogue&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Game Ending ===&lt;br /&gt;
&lt;br /&gt;
The game ends when the player reaches a prompt with the &amp;lt;code&amp;gt;game_over&amp;lt;/code&amp;gt; rule (or under other conditions defined by the original game). The player writes an &#039;&#039;&#039;epilogue&#039;&#039;&#039;—a final reflection on the vampire&#039;s story—and the character is archived. A statistics summary shows prompt count, memories, experiences, skills, resources, marks, and characters accumulated during the playthrough.&lt;br /&gt;
&lt;br /&gt;
== Architecture ==&lt;br /&gt;
&lt;br /&gt;
=== Technology Stack ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Layer !! Technology !! Version&lt;br /&gt;
|-&lt;br /&gt;
| Backend framework || Django || 5.0.6&lt;br /&gt;
|-&lt;br /&gt;
| REST API || Django REST Framework || 3.15.1&lt;br /&gt;
|-&lt;br /&gt;
| Authentication || Simple JWT (custom) || 5.3.0&lt;br /&gt;
|-&lt;br /&gt;
| CORS || django-cors-headers || 4.3.1&lt;br /&gt;
|-&lt;br /&gt;
| Image handling || Pillow || 11.3.0&lt;br /&gt;
|-&lt;br /&gt;
| Production server || Gunicorn || 21.2.0&lt;br /&gt;
|-&lt;br /&gt;
| Frontend framework || Vue.js || 3.5&lt;br /&gt;
|-&lt;br /&gt;
| State management || Pinia || 3.0&lt;br /&gt;
|-&lt;br /&gt;
| Routing || Vue Router || 4.5&lt;br /&gt;
|-&lt;br /&gt;
| HTTP client || Axios || 1.10&lt;br /&gt;
|-&lt;br /&gt;
| CSS framework || Bootstrap || 5.3&lt;br /&gt;
|-&lt;br /&gt;
| Build tool || Vite || 7.0&lt;br /&gt;
|-&lt;br /&gt;
| Language || TypeScript || 5.8&lt;br /&gt;
|-&lt;br /&gt;
| Testing (backend) || pytest + pytest-django || 8.0 / 4.8&lt;br /&gt;
|-&lt;br /&gt;
| Testing (frontend) || Playwright || 1.53&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== High-Level Diagram ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
┌─────────────────────────────────────────────────┐&lt;br /&gt;
│               Vue 3 SPA (TypeScript)            │&lt;br /&gt;
│  ┌──────────┐ ┌──────────┐ ┌──────────────────┐│&lt;br /&gt;
│  │  Views   │ │Components│ │  Pinia Stores     ││&lt;br /&gt;
│  │ (7 pages)│ │ (14+)    │ │ auth/game/theme/  ││&lt;br /&gt;
│  │          │ │          │ │ characterCreation  ││&lt;br /&gt;
│  └────┬─────┘ └────┬─────┘ └────────┬─────────┘│&lt;br /&gt;
│       └─────────────┴────────────────┘          │&lt;br /&gt;
│                     │ Axios                     │&lt;br /&gt;
│                     ▼                           │&lt;br /&gt;
├─────────────────────────────────────────────────┤&lt;br /&gt;
│              Django REST API                    │&lt;br /&gt;
│  /api/auth/login    /api/auth/verify            │&lt;br /&gt;
│  /api/characters/   (CRUD + game_state,         │&lt;br /&gt;
│   roll_dice, continue_story, audit_trail)       │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/memories/                 │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/experiences/              │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/skills/                   │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/resources/                │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/marks/                    │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/characters/               │&lt;br /&gt;
│  /api/characters/&amp;lt;id&amp;gt;/pending-changes/          │&lt;br /&gt;
│  /api/prompts/                                  │&lt;br /&gt;
├─────────────────────────────────────────────────┤&lt;br /&gt;
│          Django ORM / SQLite                    │&lt;br /&gt;
│  Character · Memory · Experience · Skill        │&lt;br /&gt;
│  Resource · Mark · GameCharacter · Prompt        │&lt;br /&gt;
│  PendingChange · GameStateSnapshot              │&lt;br /&gt;
└─────────────────────────────────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend ==&lt;br /&gt;
&lt;br /&gt;
=== Django Apps ===&lt;br /&gt;
&lt;br /&gt;
The project is organised into three Django apps plus a settings module:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! App !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;vampire&amp;lt;/code&amp;gt; || Core domain models: Character, Memory, Experience, Skill, Resource, Mark, GameCharacter, Prompt, PendingChange, GameStateSnapshot&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;tyov_api&amp;lt;/code&amp;gt; || REST API layer: serialisers, viewsets, URL routing, permissions, middleware&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;authentication&amp;lt;/code&amp;gt; || JWT-based login and token verification endpoints&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;tyov_backend&amp;lt;/code&amp;gt; || Django project settings, URL root, WSGI/ASGI configuration&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Data Models ===&lt;br /&gt;
&lt;br /&gt;
==== Character ====&lt;br /&gt;
&lt;br /&gt;
The central model, owned by a Django &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; via a ForeignKey:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Field !! Type !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt; || CharField(200) || Current name of the vampire&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt; || TextField || Background/appearance description&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;image&amp;lt;/code&amp;gt; || ImageField || Optional character portrait&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;current_prompt&amp;lt;/code&amp;gt; || CharField(10) || ID of the current prompt (e.g. &amp;quot;17a&amp;quot;)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;last_dice_roll&amp;lt;/code&amp;gt; || JSONField || Stores D10, D6, and result of the last roll&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;visited_prompts&amp;lt;/code&amp;gt; || JSONField(list) || List of all prompt IDs visited in order&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slots_lost&amp;lt;/code&amp;gt; || IntegerField || Permanent memory slot reductions&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;memory_slots_gained&amp;lt;/code&amp;gt; || IntegerField || Permanent memory slot increases&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;creation_step&amp;lt;/code&amp;gt; || IntegerField(1–6) || Tracks progress through the creation wizard&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;is_creation_complete&amp;lt;/code&amp;gt; || BooleanField || True when all six creation steps are finished&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;creation_data&amp;lt;/code&amp;gt; || JSONField || Temporary storage during creation&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;epilogue&amp;lt;/code&amp;gt; || TextField || Player&#039;s final reflection (set at game end)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;is_completed&amp;lt;/code&amp;gt; || BooleanField || Whether the story has ended&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;completed_at&amp;lt;/code&amp;gt; || DateTimeField || Timestamp of story completion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Memory ====&lt;br /&gt;
&lt;br /&gt;
A memory belongs to a Character and holds up to 3 Experiences:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; – descriptive name (unique per character)&lt;br /&gt;
* &amp;lt;code&amp;gt;in_diary&amp;lt;/code&amp;gt; – whether the memory is stored in the diary&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt; – whether the memory has been forgotten&lt;br /&gt;
&lt;br /&gt;
==== Experience ====&lt;br /&gt;
&lt;br /&gt;
An individual narrative entry within a Memory:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;title&amp;lt;/code&amp;gt; – optional short description&lt;br /&gt;
* &amp;lt;code&amp;gt;content&amp;lt;/code&amp;gt; – the narrative text&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; – which prompt generated this experience&lt;br /&gt;
* &amp;lt;code&amp;gt;date_info&amp;lt;/code&amp;gt; – temporal context (e.g. &amp;quot;Winter, 431 CE&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
==== Skill ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;status&amp;lt;/code&amp;gt; – one of &amp;lt;code&amp;gt;normal&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;checked&amp;lt;/code&amp;gt;, or &amp;lt;code&amp;gt;lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Resource ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;resource_type&amp;lt;/code&amp;gt; – &amp;lt;code&amp;gt;portable&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;stationary&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;is_diary&amp;lt;/code&amp;gt; – flags the special diary resource (constrained to one active diary per character)&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Mark ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== GameCharacter ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;description&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;character_type&amp;lt;/code&amp;gt; – &amp;lt;code&amp;gt;mortal&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;immortal&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;relationship&amp;lt;/code&amp;gt; – e.g. friend, rival, mentor, love, enemy, neutral&lt;br /&gt;
* &amp;lt;code&amp;gt;is_lost&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Prompt ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; – e.g. &amp;quot;17a&amp;quot;, &amp;quot;32b&amp;quot;&lt;br /&gt;
* &amp;lt;code&amp;gt;number&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;variation&amp;lt;/code&amp;gt; – parsed components for sorting&lt;br /&gt;
* &amp;lt;code&amp;gt;content&amp;lt;/code&amp;gt; – the scenario text&lt;br /&gt;
* &amp;lt;code&amp;gt;rules&amp;lt;/code&amp;gt; – special mechanics as comma-separated tokens (e.g. &amp;lt;code&amp;gt;no_experience, allow_name_change&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
==== PendingChange ====&lt;br /&gt;
&lt;br /&gt;
Temporary storage for changes before they are atomically committed on &amp;quot;Continue&amp;quot;:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;change_type&amp;lt;/code&amp;gt; – experience, memory, skill, resource, mark, or character&lt;br /&gt;
* &amp;lt;code&amp;gt;change_data&amp;lt;/code&amp;gt; – JSON payload of the change&lt;br /&gt;
&lt;br /&gt;
==== GameStateSnapshot (Audit Trail) ====&lt;br /&gt;
&lt;br /&gt;
Automatically created each turn to record the complete game state:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;turn_number&amp;lt;/code&amp;gt; – sequential, starting from 1&lt;br /&gt;
* &amp;lt;code&amp;gt;prompt_id&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;next_prompt_id&amp;lt;/code&amp;gt; – transition&lt;br /&gt;
* &amp;lt;code&amp;gt;dice_roll&amp;lt;/code&amp;gt; – raw dice data&lt;br /&gt;
* &amp;lt;code&amp;gt;game_state&amp;lt;/code&amp;gt; – full JSON snapshot of all memories, skills, resources, etc.&lt;br /&gt;
* &amp;lt;code&amp;gt;changes_applied&amp;lt;/code&amp;gt; – JSON array of pending changes that were committed&lt;br /&gt;
* &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; – timestamp&lt;br /&gt;
&lt;br /&gt;
=== API Endpoints ===&lt;br /&gt;
&lt;br /&gt;
All endpoints require JWT authentication (except login).&lt;br /&gt;
&lt;br /&gt;
==== Authentication ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/auth/login/&amp;lt;/code&amp;gt; || Authenticate with username/password; returns JWT token&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/auth/verify/&amp;lt;/code&amp;gt; || Verify current token and return user info&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Login is rate-limited to 5 requests per minute per IP.&lt;br /&gt;
&lt;br /&gt;
==== Characters ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/&amp;lt;/code&amp;gt; || List all characters for the authenticated user&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/&amp;lt;/code&amp;gt; || Create a new character&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Retrieve full character detail (prefetched relations)&lt;br /&gt;
|-&lt;br /&gt;
| PUT/PATCH || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Update character fields&lt;br /&gt;
|-&lt;br /&gt;
| DELETE || &amp;lt;code&amp;gt;/api/characters/{id}/&amp;lt;/code&amp;gt; || Delete a character&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/game_state/&amp;lt;/code&amp;gt; || Complete game state including prompt, validation, dice info&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/{id}/roll_dice/&amp;lt;/code&amp;gt; || Roll D10 − D6 and store the result&lt;br /&gt;
|-&lt;br /&gt;
| POST || &amp;lt;code&amp;gt;/api/characters/{id}/continue_story/&amp;lt;/code&amp;gt; || Validate, commit pending changes, advance to next prompt&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/audit_trail/&amp;lt;/code&amp;gt; || View turn-by-turn audit history&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/characters/{id}/game-state-changes/&amp;lt;/code&amp;gt; || Timeline of state changes between turns&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Nested Character Resources ====&lt;br /&gt;
&lt;br /&gt;
For each character, full CRUD is available on:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/memories/&amp;lt;/code&amp;gt; (plus &amp;lt;code&amp;gt;move_to_diary&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;restore_lost_memory&amp;lt;/code&amp;gt; actions)&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/experiences/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/skills/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/resources/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/marks/&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/characters/&amp;lt;/code&amp;gt; (known NPCs)&lt;br /&gt;
* &amp;lt;code&amp;gt;/api/characters/{id}/pending-changes/&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Prompts ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Endpoint !! Description&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/prompts/&amp;lt;/code&amp;gt; || List all game prompts&lt;br /&gt;
|-&lt;br /&gt;
| GET || &amp;lt;code&amp;gt;/api/prompts/{prompt_id}/&amp;lt;/code&amp;gt; || Retrieve a single prompt by ID&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Permissions and Security ===&lt;br /&gt;
&lt;br /&gt;
* All character data is scoped to the authenticated user via &amp;lt;code&amp;gt;IsCharacterOwner&amp;lt;/code&amp;gt; permission.&lt;br /&gt;
* JWT tokens are sent as &amp;lt;code&amp;gt;Authorization: Bearer {token}&amp;lt;/code&amp;gt; headers.&lt;br /&gt;
* The login endpoint is throttled (5/minute) to prevent brute-force attacks.&lt;br /&gt;
* All pending changes are processed inside a database &#039;&#039;&#039;transaction&#039;&#039;&#039; to guarantee atomicity.&lt;br /&gt;
* CORS headers are configured via &amp;lt;code&amp;gt;django-cors-headers&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Frontend ==&lt;br /&gt;
&lt;br /&gt;
=== Views (Pages) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! View !! Route !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;LoginView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; || Authentication form; redirects authenticated users to home&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;HomeView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt; || Dashboard showing all active characters in a card grid&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharacterCreationView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/character-creation/:id&amp;lt;/code&amp;gt; || Six-step creation wizard&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/game/:characterId&amp;lt;/code&amp;gt; || Main game loop: prompt display, experience form, dice rolling, entity management, memory display&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameEndedView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/game/:characterId/ended&amp;lt;/code&amp;gt; || End-of-story screen with statistics and epilogue&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;RecordsView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/records&amp;lt;/code&amp;gt; || Archive of current and completed characters&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;StoryView&#039;&#039;&#039; || &amp;lt;code&amp;gt;/story/:characterId&amp;lt;/code&amp;gt; || Narrative timeline with experience history, character stats sidebar, and epilogue&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
All routes except &amp;lt;code&amp;gt;/login&amp;lt;/code&amp;gt; require authentication. The router guard redirects unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
=== Key Components ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Component !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PromptSection&#039;&#039;&#039; || Displays the current prompt text, special rules, and dice roll results&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ExperienceForm&#039;&#039;&#039; || Form for writing new experiences with title, date, content, and memory selection&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;MemoriesDisplay&#039;&#039;&#039; || Shows active, diary, and lost memories with experience counts; includes move-to-diary, delete, and restore actions&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;GameDataSection&#039;&#039;&#039; || Reusable list for skills, resources, or marks with add/edit/delete controls&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharactersSection&#039;&#039;&#039; || Grid of known NPCs with type badges and edit/delete&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;EntityCreationWindow&#039;&#039;&#039; || Draggable modal for creating/editing skills, resources, marks&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;CharacterCreationWindow&#039;&#039;&#039; || Draggable modal for creating/editing NPC characters&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ExperienceEditWindow&#039;&#039;&#039; || Window for editing existing experience text&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;DiaryCreationModal&#039;&#039;&#039; || Modal for creating a diary resource and moving a memory into it&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ConfirmationDialog&#039;&#039;&#039; || Generic confirmation dialog for destructive actions&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ValidationWarning&#039;&#039;&#039; || Displays validation errors when game rules are not satisfied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== State Management (Pinia) ===&lt;br /&gt;
&lt;br /&gt;
Four Pinia stores manage application state:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Store !! Responsibility&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;auth&#039;&#039;&#039; || JWT token, user object, login/logout, token expiry handling&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;game&#039;&#039;&#039; || Characters list, current character, game state, all CRUD actions for entities, dice rolling, story continuation&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;characterCreation&#039;&#039;&#039; || Multi-step wizard data, initial character creation&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;theme&#039;&#039;&#039; || Dark/light mode toggle with localStorage persistence&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== API Service Layer ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;api.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Axios instance with base URL auto-detection (development vs. production), request interceptor for JWT headers, response interceptor for 401/token-expiry handling.&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;gameService.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – All API call methods: authentication, character CRUD, dice rolling, story continuation, memory/experience/skill/resource/mark/character management.&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;entityService.ts&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Generic &amp;lt;code&amp;gt;EntityService&amp;lt;/code&amp;gt; class instantiated for skills, resources, marks, and characters for DRY CRUD operations.&lt;br /&gt;
&lt;br /&gt;
=== Composables ===&lt;br /&gt;
&lt;br /&gt;
Vue composables encapsulate reusable business logic:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useGameData()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Computed properties derived from the game store (current character, memories, skills, resources, etc.)&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useExperienceForm()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Form state, validation, memory selection, and reset logic&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useEntityManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Generic CRUD operations with optional confirmation dialogs&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useSkillManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useResourceManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useMarkManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039;, &#039;&#039;&#039;&amp;lt;code&amp;gt;useMemoryManagement()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Specialised wrappers&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useConfirmationDialog()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Promise-based modal confirmation&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useMemoryDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Display logic for memories including pending-change awareness&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;useExperienceDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Markdown rendering for experience content&lt;br /&gt;
* &#039;&#039;&#039;&amp;lt;code&amp;gt;usePendingChangesDisplay()&amp;lt;/code&amp;gt;&#039;&#039;&#039; – Badge rendering for undo-able pending changes&lt;br /&gt;
&lt;br /&gt;
== Audit Trail ==&lt;br /&gt;
&lt;br /&gt;
The application includes an automatic audit trail that captures the complete game state every time the player advances to a new prompt.&lt;br /&gt;
&lt;br /&gt;
Each &#039;&#039;&#039;GameStateSnapshot&#039;&#039;&#039; records:&lt;br /&gt;
&lt;br /&gt;
* The turn number (sequential from 1)&lt;br /&gt;
* The prompt transition (e.g. 36a → 34a)&lt;br /&gt;
* The dice roll&lt;br /&gt;
* A complete JSON snapshot of all memories, skills, resources, marks, and known characters&lt;br /&gt;
* All pending changes that were committed&lt;br /&gt;
&lt;br /&gt;
The audit trail is accessible via:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;API&#039;&#039;&#039;: &amp;lt;code&amp;gt;GET /api/characters/{id}/audit_trail/&amp;lt;/code&amp;gt; with optional &amp;lt;code&amp;gt;?turn=N&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;?limit=N&amp;lt;/code&amp;gt; parameters&lt;br /&gt;
* &#039;&#039;&#039;Management command&#039;&#039;&#039;: &amp;lt;code&amp;gt;python manage.py view_audit_trail {id} [--summary] [--turn N] [--export file.json]&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;Timeline API&#039;&#039;&#039;: &amp;lt;code&amp;gt;GET /api/characters/{id}/game-state-changes/&amp;lt;/code&amp;gt; for a structured timeline of all state changes&lt;br /&gt;
&lt;br /&gt;
== Deployment ==&lt;br /&gt;
&lt;br /&gt;
=== Local Development ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
.\startup.ps1&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This script creates a virtual environment, installs dependencies, runs migrations, and starts both the Django backend (&amp;lt;code&amp;gt;http://localhost:8000&amp;lt;/code&amp;gt;) and the Vue dev server (&amp;lt;code&amp;gt;http://localhost:5173&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
=== Production ===&lt;br /&gt;
&lt;br /&gt;
* Set &amp;lt;code&amp;gt;DJANGO_ENV=production&amp;lt;/code&amp;gt;&lt;br /&gt;
* Build the frontend: &amp;lt;code&amp;gt;cd frontend &amp;amp;&amp;amp; npm run build&amp;lt;/code&amp;gt;&lt;br /&gt;
* Serve with Gunicorn behind a reverse proxy&lt;br /&gt;
* Static files are collected into the &amp;lt;code&amp;gt;static/&amp;lt;/code&amp;gt; directory&lt;br /&gt;
* The production frontend points to &amp;lt;code&amp;gt;https://tyov.yusupov.cloud&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Example: A Finished Character ==&lt;br /&gt;
&lt;br /&gt;
The following is an example of a character from an actual playthrough stored in the application database. It demonstrates how the game&#039;s mechanics play out over an extended narrative.&lt;br /&gt;
&lt;br /&gt;
=== Karmiš (formerly Ekurzu / Naram) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Description:&#039;&#039;&#039; Naram was a temple scribe in the ancient city of Mari: meticulous, observant, and deeply entangled in the political and spiritual life of the city. His life was shaped by clay tablets, omens, and whispered secrets carried through palace corridors and temple courtyards. Behind his calm demeanor lies a mind constantly at work—recording, interpreting, and surviving.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Prompts visited:&#039;&#039;&#039; 34 prompts across variations, including: 1a, 4a, 11a–c, 12a, 17a, 20a, 24a, 25a, 32a–b, 34a–36a, 37a–41c, 43a–c, 47a, 52a, 57a, 64a, 66a, 68a, 71a, 73a.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Final prompt:&#039;&#039;&#039; 73a&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory slots lost:&#039;&#039;&#039; 1 · &#039;&#039;&#039;Memory slots gained:&#039;&#039;&#039; 1 (net effect: standard 5-memory limit)&lt;br /&gt;
&lt;br /&gt;
==== Epilogue ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;blockquote&amp;gt;&lt;br /&gt;
He has not died. Let me say that plainly. There will be no grave, no crumbling tomb, no name worn smooth by time. He is alive—quietly, deliberately, and utterly outside your reach.&lt;br /&gt;
&lt;br /&gt;
This book is not a memorial. It is a reckoning.&lt;br /&gt;
&lt;br /&gt;
When I first met him, I did not understand what he was. He wore his years like dust on old vellum—thin, barely visible, but ever present. [...] Over time, I became his mirror, then his partner, and—finally—his historian.&lt;br /&gt;
&lt;br /&gt;
But someone has to begin the telling. Someone has to place the first stone. So this book begins in a house of quiet gardens and low ceilings, where he writes in the mornings and feeds only when he must. [...]&lt;br /&gt;
&lt;br /&gt;
Just a man who remembered too much, for too long. And chose, in the end, &#039;&#039;peace&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
— &#039;&#039;Mirelde&#039;&#039;&lt;br /&gt;
&amp;lt;/blockquote&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Statistics ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Metric !! Count&lt;br /&gt;
|-&lt;br /&gt;
| Total memories || 16&lt;br /&gt;
|-&lt;br /&gt;
| Active memories || 5&lt;br /&gt;
|-&lt;br /&gt;
| Diary memories || 3&lt;br /&gt;
|-&lt;br /&gt;
| Lost (forgotten) memories || 8&lt;br /&gt;
|-&lt;br /&gt;
| Total experiences || 37&lt;br /&gt;
|-&lt;br /&gt;
| Skills || 11 (3 lost during play)&lt;br /&gt;
|-&lt;br /&gt;
| Resources || 10 (6 lost during play)&lt;br /&gt;
|-&lt;br /&gt;
| Marks || 2&lt;br /&gt;
|-&lt;br /&gt;
| Known characters || 11 (6 lost during play)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Active Memories (at end of game) ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Memory Title !! Location !! Experiences&lt;br /&gt;
|-&lt;br /&gt;
| Memory 1 || Active || &amp;quot;Mortal life&amp;quot; (Initial), &amp;quot;The Name Given in Ash&amp;quot; (24a)&lt;br /&gt;
|-&lt;br /&gt;
| The Thorn and the Quill || Active || 1 experience (35a)&lt;br /&gt;
|-&lt;br /&gt;
| The Light That Does Not Burn || Active || &amp;quot;The Dawn at Mirelde&#039;s Fire&amp;quot; (52a), &amp;quot;The Dust in the Foundation&amp;quot; (57a)&lt;br /&gt;
|-&lt;br /&gt;
| We Were Not Counted Among the Cargo || Active || &amp;quot;The Bellies of Ships and Men&amp;quot; (64a), &amp;quot;Ink Without Meaning&amp;quot; (66a)&lt;br /&gt;
|-&lt;br /&gt;
| Echoes Before the Self Was Named || Active || &amp;quot;The Weight in the Smoke&amp;quot; (71a), &amp;quot;The Clay That Knew My Name&amp;quot; (68a)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Diary Memories ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Memory Title !! Experiences&lt;br /&gt;
|-&lt;br /&gt;
| The Hill That Waits || &amp;quot;The Hollow Beneath the Cairn&amp;quot; (40a), &amp;quot;The Ring Beneath the Clay&amp;quot; (43c), &amp;quot;The Face Returned Through Time&amp;quot; (44a)&lt;br /&gt;
|-&lt;br /&gt;
| Ash of the Ledger || &amp;quot;The Last Ledger Vanishes&amp;quot; (38a), &amp;quot;The Fog Beneath Names&amp;quot; (39b), &amp;quot;The Weight of Unspoken Words&amp;quot; (34a)&lt;br /&gt;
|-&lt;br /&gt;
| The Fever-Ledger || &amp;quot;The Illness in Karkheda&amp;quot; (36a), &amp;quot;The Name That Returns in Ash&amp;quot; (41c), &amp;quot;The Echo That Cannot Translate&amp;quot; (47a)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Lost Memories ====&lt;br /&gt;
&lt;br /&gt;
Eight memories were forgotten over the course of the game, including &amp;quot;The Forged Omen&amp;quot; (Memory 2), &amp;quot;Hessa&#039;s Last Message&amp;quot; (Memory 3), &amp;quot;Duel of Stars and Signs&amp;quot; (Memory 4), &amp;quot;The Turning&amp;quot;, &amp;quot;The Name That Cannot Burn&amp;quot;, &amp;quot;The Blood Between Accusations&amp;quot;, &amp;quot;Trade Beneath Empire&amp;quot;, and &amp;quot;What Was Meant to Last&amp;quot;. These losses illustrate the game&#039;s core mechanic: as new experiences accumulate, the vampire is forced to sacrifice older memories, creating a poignant sense of identity erosion across the centuries.&lt;br /&gt;
&lt;br /&gt;
==== Skills ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Skill !! Status !! Description (excerpt)&lt;br /&gt;
|-&lt;br /&gt;
| Ash-Tongue || {{Checked}} || &amp;quot;I speak in soot and suggestion, in the cracks between laws.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Bloodthirsty || {{Lost}} || &amp;quot;The scent of mortal blood calls to me like a hymn in the dark.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Ceremonial Composure || {{Checked}} || &amp;quot;In the presence of gods or kings, my face is unreadable.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Decipher Ancient Texts || {{Lost}} || &amp;quot;I can read languages long dead and spot forgeries.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| I Control the Beast || {{Lost}} || &amp;quot;When the thirst rises, I do not flinch. I meet the monster&#039;s gaze.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Ledger Without End || Normal || &amp;quot;Born from the quiet rituals of inventory and account.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Physick of the Pale Vein || Normal || &amp;quot;I feed gently, beneath the guise of care.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Remain in Unknowing || {{Checked}} || &amp;quot;I do not flee the edges of memory.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Silent Cartography || {{Checked}} || &amp;quot;I trace the unseen paths between people, places, and power.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Snare and Stillness || {{Checked}} || &amp;quot;I move with silence that makes mortals forget they heard me.&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| Tongue of the Unnamed || Normal || &amp;quot;I no longer understand the languages of my past, but I wear the sounds of others like masks.&amp;quot;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Resources ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Resource !! Type !! Status !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| Kept Against Forgetting || Portable / &#039;&#039;&#039;Diary&#039;&#039;&#039; || Active || The character&#039;s diary; written with Mirelde&lt;br /&gt;
|-&lt;br /&gt;
| Tablet Bearing My Name || Portable || Active || A clay tablet from the ancient past, rediscovered&lt;br /&gt;
|-&lt;br /&gt;
| The Margins of Manifest || Portable || Active || A cipher-folio cataloguing names of the enslaved&lt;br /&gt;
|-&lt;br /&gt;
| Burrowed Sanctum || Portable || Lost || An earthen chamber beneath a forgotten altar&lt;br /&gt;
|-&lt;br /&gt;
| Chamber of Whispers || Stationary || Lost || A hidden storeroom beneath the temple of Ishtar&lt;br /&gt;
|-&lt;br /&gt;
| Clay Tablets and Reed Stylus || Portable || Lost || Writing implements used by instinct, not understanding&lt;br /&gt;
|-&lt;br /&gt;
| Hidden Archive Tablet || Portable || Lost || A tablet inscribed with dynasty-breaking truths&lt;br /&gt;
|-&lt;br /&gt;
| The Monastery Diary || Portable / Diary || Lost || A former diary, lost when the monastery fell&lt;br /&gt;
|-&lt;br /&gt;
| The Silent Authority || Portable || Lost || A carved seal whose symbols can no longer be read&lt;br /&gt;
|-&lt;br /&gt;
| The Walls of Forgetting || Stationary / Diary || Lost || Memory carvings in five scripts beneath a forgotten shrine&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Known Characters ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Name !! Type !! Relationship !! Status&lt;br /&gt;
|-&lt;br /&gt;
| Ashurban the Veiled || Immortal || Neutral || Active&lt;br /&gt;
|-&lt;br /&gt;
| Mirelde of Bracha || Immortal || Friend || Active&lt;br /&gt;
|-&lt;br /&gt;
| The One Who Does Not Answer || Mortal || Neutral || Active&lt;br /&gt;
|-&lt;br /&gt;
| Thoöni (spectral) || Immortal || Neutral || Active&lt;br /&gt;
|-&lt;br /&gt;
| Belatu || Mortal || Rival || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Ennatum || Mortal || Friend || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Hessa || Mortal || Love || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Ibbi-Zamri || Mortal || Mentor || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Mekha || Mortal || Enemy || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Pelagon || Mortal || Enemy || Lost&lt;br /&gt;
|-&lt;br /&gt;
| Thoöni (scholar) || Mortal || Friend || Lost&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Marks ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Mark !! Description&lt;br /&gt;
|-&lt;br /&gt;
| The Unblinking Eye || An ancient eye-like scar below the collarbone; burns faintly in the presence of lies&lt;br /&gt;
|-&lt;br /&gt;
| The Gesture Forbidden || An involuntary three-fingered gesture of silence, often mistaken for mockery&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Example Experience: &amp;quot;The Bellies of Ships and Men&amp;quot; (Prompt 64a) ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;blockquote&amp;gt;&lt;br /&gt;
They arrived in numbers too vast to count—pressed between barrels, chained to holds slick with piss and seawater, branded, sick, broken. And the sailors were not much better.&lt;br /&gt;
&lt;br /&gt;
The ships would come in [...]&lt;br /&gt;
&amp;lt;/blockquote&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Date: around 1510 CE&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
This experience is part of the memory &amp;quot;We Were Not Counted Among the Cargo&amp;quot;, illustrating the vampire&#039;s witness to the Atlantic slave trade—one of many historical eras the character passes through during a millennium-spanning story.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
Tests are run with &#039;&#039;&#039;pytest&#039;&#039;&#039; and &#039;&#039;&#039;pytest-django&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
pytest&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Test files include:&lt;br /&gt;
* &amp;lt;code&amp;gt;vampire/test_models.py&amp;lt;/code&amp;gt; – model unit tests&lt;br /&gt;
* &amp;lt;code&amp;gt;tyov_api/test_api.py&amp;lt;/code&amp;gt; – API endpoint integration tests&lt;br /&gt;
* &amp;lt;code&amp;gt;tyov_api/test_models.py&amp;lt;/code&amp;gt; – API model tests&lt;br /&gt;
* &amp;lt;code&amp;gt;authentication/test_views.py&amp;lt;/code&amp;gt; – authentication tests&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
End-to-end tests use &#039;&#039;&#039;Playwright&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
cd frontend&lt;br /&gt;
npx playwright test&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Test specs include:&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/character-creation-workflow.spec.ts&amp;lt;/code&amp;gt; – character creation wizard&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/tyov-game.spec.ts&amp;lt;/code&amp;gt; – game loop testing&lt;br /&gt;
* &amp;lt;code&amp;gt;e2e/vue.spec.ts&amp;lt;/code&amp;gt; – general Vue component tests&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;Thousand Year Old Vampire&#039;&#039; by Tim Hutchings – the original tabletop game&lt;br /&gt;
* [[Django (web framework)]]&lt;br /&gt;
* [[Vue.js]]&lt;br /&gt;
* [[Single-page application]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Web applications]]&lt;br /&gt;
[[Category:Role-playing video games]]&lt;br /&gt;
[[Category:Django (web framework) applications]]&lt;br /&gt;
[[Category:Vue.js applications]]&lt;br /&gt;
[[Category:Solo role-playing games]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=383</id>
		<title>Yusupov.cloud</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=383"/>
		<updated>2026-04-12T11:15:28Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Subdomains and projects */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name           = yusupov.cloud&lt;br /&gt;
| 1 url            = https://yusupov.cloud&lt;br /&gt;
| 2 type           = Personal web sites&lt;br /&gt;
| 3 owner          = [[Michel Vuijlsteke]]&lt;br /&gt;
| 4 launched       = 2025&lt;br /&gt;
| 5 current_status = Online&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;yusupov.cloud&#039;&#039;&#039; is a personal domain and virtual private server operated by Belgian technologist [[Michel Vuijlsteke]]. It hosts multiple small web applications on subdomains and at the apex domain. One of these is a MediaWiki installation titled “Yusupov’s House.” The setup is presented as a web-era continuation of the do-it-yourself ethos of Vuijlsteke’s 1990s BBS of the same name.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot;&amp;gt;“Yusupov’s House,” &#039;&#039;yusupov.cloud&#039;&#039; (wiki), accessed 10 October 2025, https://yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
Among the projects on the domain is a MediaWiki (at the apex, &#039;&#039;yusupov.cloud&#039;&#039;) running MediaWiki 1.44.0 with PHP 8.3.6 (FPM) and SQLite, using the Vector skin and core extensions for citations and template scripting.&amp;lt;ref name=&amp;quot;version&amp;quot;&amp;gt;‘‘Special:Version’’ page, &#039;&#039;yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://yusupov.cloud/wiki/Special:Version&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Subdomains and projects ==&lt;br /&gt;
Publicly visible projects include:&lt;br /&gt;
&lt;br /&gt;
* [https://acbc.yusupov.cloud acbc.yusupov.cloud] — &#039;&#039;A Cabinet of Brief Curiosities&#039;&#039;, generating tiny three-sentence surreal/horror micro-stories with an hourly cadence and an archive. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;acbc-home&amp;quot;&amp;gt;A Cabinet of Brief Curiosities (home), &#039;&#039;acbc.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://acbc.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://agenda.yusupov.cloud agenda.yusupov.cloud] — &#039;&#039;A Life in Planners&#039;&#039;, a structured journal chronicling the final years of the operator’s mother, with calendar, food, medications, measurements, and statistics views (multilingual UI).&amp;lt;ref name=&amp;quot;agenda&amp;quot;&amp;gt;“A life in planners,” &#039;&#039;agenda.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://agenda.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://cloud.yusupov.cloud cloud.yusupov.cloud] — &#039;&#039;Cloud&#039;&#039;, a series of static html creative coding experiments, simulations, and games including: timebeat, fire and snake simulations, biomass metaballs, cs3, &#039;&#039;Cross&#039;&#039; crossword puzzle game, image dithering tool, books, &#039;&#039;Elite Galaxy Explorer&#039;&#039;, ZX Spectrum loading screen simulator, Carcassonne, 3D boids flocking algorithm, physarum slime mold simulation, temps temperature visualization, &#039;&#039;The Chronicle of Hamurabi&#039;&#039; ancient Sumeria resource management game, and gatekeeper.&amp;lt;ref name=&amp;quot;cloud-home&amp;quot;&amp;gt;&amp;quot;cloud,&amp;quot; &#039;&#039;cloud.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://cloud.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://digest.yusupov.cloud digest.yusupov.cloud] — &#039;&#039;Digest&#039;&#039;, daily seasonal AI-assisted recipes inspired by current events, browsable by meal type and ingredients.&amp;lt;ref name=&amp;quot;digest-home&amp;quot;&amp;gt;“Digest — Daily recipes inspired by the news,” &#039;&#039;digest.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://digest.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://echoes.yusupov.cloud echoes.yusupov.cloud] — &#039;&#039;[[Echoes of What Wasn&#039;t]]&#039;&#039;, an AI-generated alternate-history newspaper presenting richly detailed articles about historical events as if they had unfolded differently. A pipeline scrapes real events from multilingual Wikipedia, uses OpenAI to craft a divergent narrative with period-appropriate prose and DALL-E imagery, and publishes via a REST API. Features article browsing by month, a &amp;quot;Where/When&amp;quot; interactive map-and-timeline view using Leaflet, and a picture desk. (Built with Wagtail 7/Django 5 per operator.)&amp;lt;ref name=&amp;quot;echoes-home&amp;quot;&amp;gt;&amp;quot;Echoes — Dispatches from Histories That Never Were,&amp;quot; &#039;&#039;echoes.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://echoes.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://quidlibet.yusupov.cloud quidlibet.yusupov.cloud] — &#039;&#039;Quidlibet&#039;&#039;, an app that generates fictional books complete with synopsis, author bio, and faux reviews; includes genre and author archives. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;quidlibet-home&amp;quot;&amp;gt;“Quidlibet — Book Generator,” &#039;&#039;quidlibet.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://quidlibet.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://tyov-web.yusupov.cloud tyov-web.yusupov.cloud] — a web implementation of the solo RPG &#039;&#039;[[Thousand Year Old Vampire]]&#039;&#039;, with Django 5 backend and Vue 3 frontend. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
Operator-reported (not publicly discoverable at time of writing):&lt;br /&gt;
&lt;br /&gt;
* skills.yusupov.cloud — a skills matrix application. (Per operator.)&lt;br /&gt;
* resources.yusupov.cloud — a simple resource plannign calendar. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
== Technology ==&lt;br /&gt;
The wiki stack is documented on &#039;&#039;Special:Version&#039;&#039;. Individual apps are described by the operator as Flask (&#039;&#039;acbc&#039;&#039;, &#039;&#039;quidlibet&#039;&#039;) and Django 5 + Vue 3 (&#039;&#039;tyov-web&#039;&#039;).&amp;lt;ref name=&amp;quot;version&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Relation to the BBS ==&lt;br /&gt;
The project name references Vuijlsteke’s single-line BBS (FidoNet 2:291/1925) active between 1990 and 1995. While the VPS is not a BBS, its single-admin, self-maintained hosting reprises the early DIY approach.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;nodehist&amp;quot;&amp;gt;“Nodelist history search: History of node 2:291/1925,” NodeHist, accessed 10 October 2025, https://nodehist.fidonet.org.ua/?address=2%3A291%2F1925&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Yusupov&#039;s House]] (1990s BBS)&lt;br /&gt;
* [[Michel Vuijlsteke]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Personal websites]]&lt;br /&gt;
[[Category:Belgian websites]]&lt;br /&gt;
[[Category:2025 establishments in Belgium]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Echoes_of_What_Wasn%27t&amp;diff=382</id>
		<title>Echoes of What Wasn&#039;t</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Echoes_of_What_Wasn%27t&amp;diff=382"/>
		<updated>2026-04-12T11:03:05Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| 01_name         = Echoes of What Wasn&#039;t&lt;br /&gt;
| 02_url          = https://echoes.yusupov.cloud&lt;br /&gt;
| 03_developer    = Michel Vuijlsteke&lt;br /&gt;
| 04_released     = 2025&lt;br /&gt;
| 05_genre        = AI-generated alternate-history newspaper&lt;br /&gt;
| 06_language     = Python&lt;br /&gt;
| 07_framework    = [[Django]] 5.2 / [[Wagtail]] 7.3&lt;br /&gt;
| 08_license      = Proprietary&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Echoes of What Wasn&#039;t&#039;&#039;&#039; (also known as &#039;&#039;&#039;Echoes&#039;&#039;&#039;) is a web application hosted at &amp;lt;code&amp;gt;echoes.yusupov.cloud&amp;lt;/code&amp;gt; that publishes AI-generated alternate-history newspaper and magazine articles. Each article takes a real historical event, invents a plausible point of divergence, and presents the consequences as an in-world retrospective written decades later by a fictitious journalist for a fictitious period-appropriate publication. The site&#039;s tagline is &amp;quot;Dispatches from Histories That Never Were.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Django 5.2 with Wagtail 7.3 as its content management system.&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt in the project repository lists Django 5.2.12 and Wagtail 7.3.1.&amp;lt;/ref&amp;gt; It uses [[Django REST framework]] for a JSON API, [[SQLite]] as its database, and is deployed behind [[Nginx]] with [[Gunicorn]] on a Linux server. Additional dependencies include [[Pillow (imaging library)|Pillow]] for image processing, [[nh3 (library)|nh3]] for HTML sanitisation, [[Beautiful Soup (HTML parser)|Beautiful Soup]] and [[lxml]] for scraping, and the [[OpenAI]] Python client for language-model and image-generation calls. Geographic features use &amp;lt;code&amp;gt;wagtailgeowidget&amp;lt;/code&amp;gt; with Google Maps integration.&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
=== Article pages ===&lt;br /&gt;
&lt;br /&gt;
Each article is a Wagtail &amp;lt;code&amp;gt;ArticlePage&amp;lt;/code&amp;gt;, a child of a single &amp;lt;code&amp;gt;ArticleIndexPage&amp;lt;/code&amp;gt; in the page tree. An article carries:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;title&#039;&#039;, &#039;&#039;subtitle&#039;&#039;, &#039;&#039;publication&#039;&#039; (the fictitious newspaper/magazine name), &#039;&#039;date&#039;&#039; (in-universe publication date), &#039;&#039;event_date&#039;&#039; (the historical event&#039;s date), &#039;&#039;location&#039;&#039;, and a foreign key to an &amp;lt;code&amp;gt;Author&amp;lt;/code&amp;gt; snippet (exposed via the API as &#039;&#039;byline&#039;&#039;).&lt;br /&gt;
* A &#039;&#039;body_richtext&#039;&#039; field (Wagtail &amp;lt;code&amp;gt;RichTextField&amp;lt;/code&amp;gt;) containing the article&#039;s HTML body, sanitised on ingest.&lt;br /&gt;
* A &#039;&#039;body&#039;&#039; [[Wagtail StreamField|StreamField]] supporting paragraph, heading, callout, quote, pull-quote, bulleted list, numbered list, image, aside (with its own nested stream), and horizontal-rule block types.&lt;br /&gt;
* Separate &#039;&#039;callouts&#039;&#039; and &#039;&#039;quotes&#039;&#039; StreamFields for editorial pull-outs and attributed quotations.&lt;br /&gt;
* A JSON &#039;&#039;assets&#039;&#039; field listing image metadata (src, alt, caption, credit, prompt).&lt;br /&gt;
* A &#039;&#039;featured_image&#039;&#039; foreign key to a custom image model (&amp;lt;code&amp;gt;CustomImage&amp;lt;/code&amp;gt;, extending Wagtail&#039;s &amp;lt;code&amp;gt;AbstractImage&amp;lt;/code&amp;gt; with a description field).&lt;br /&gt;
* Internal editorial fields: &#039;&#039;original_event&#039;&#039;, &#039;&#039;departure_point&#039;&#039;, and &#039;&#039;ai_context&#039;&#039;, which are visible in the Wagtail admin but not rendered on the public site.&lt;br /&gt;
* Geographic fields: &#039;&#039;event_location&#039;&#039; (address string), &#039;&#039;geo_location&#039;&#039; (WKT point), &#039;&#039;latitude&#039;&#039;, and &#039;&#039;longitude&#039;&#039;. The model&#039;s &amp;lt;code&amp;gt;save()&amp;lt;/code&amp;gt; method synchronises the WKT point and decimal-degree fields bidirectionally.&lt;br /&gt;
&lt;br /&gt;
=== Historical events ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;HistoricalEvent&amp;lt;/code&amp;gt; model stores real events scraped from Wikipedia, keyed by ISO date, astronomical year, event text, language code, and a boolean &#039;&#039;used&#039;&#039; flag.&lt;br /&gt;
&lt;br /&gt;
=== Custom images ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;CustomImage&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;CustomRendition&amp;lt;/code&amp;gt; extend Wagtail&#039;s abstract image models to add a &#039;&#039;description&#039;&#039; text field, used to store the original DALL·E prompt.&lt;br /&gt;
&lt;br /&gt;
== Article generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Article generation is performed by the standalone script &amp;lt;code&amp;gt;_generate_article.py&amp;lt;/code&amp;gt;, which orchestrates a six-step pipeline of OpenAI API calls, checkpoint-resumed so that a failure at any step does not waste prior work. The default text model is GPT-5 (with GPT-4o as fallback); the image model is &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Step 0: Event sourcing ===&lt;br /&gt;
&lt;br /&gt;
A separate scraper (&amp;lt;code&amp;gt;_scrape_wikipedia_events.py&amp;lt;/code&amp;gt;) fetches every day-of-year page from Wikipedia (e.g. &amp;quot;January 1&amp;quot;, &amp;quot;February 14&amp;quot;) for English, French, and Dutch editions. It parses the &amp;quot;Events&amp;quot; sections, extracts each entry&#039;s year and text, and stores them as &amp;lt;code&amp;gt;HistoricalEvent&amp;lt;/code&amp;gt; rows. The scraper respects rate limits and identifies itself via a custom User-Agent string.&lt;br /&gt;
&lt;br /&gt;
=== Step 1: Event selection ===&lt;br /&gt;
&lt;br /&gt;
The pipeline queries the database for unused Common Era events whose month and day match the current date and whose year is at least 50 years in the past. Five candidates are sampled at random and presented in a structured prompt to the text model, which acts as an &amp;quot;editorial planner.&amp;quot; The model returns JSON selecting the single most promising event for an alternate-history feature, together with a proposed divergence date, publication date, publication context (name, type, city, editorial stance, intended readership), article angle, and a list of visual possibilities. The pipeline validates that the chosen event ID was in the candidate set.&lt;br /&gt;
&lt;br /&gt;
=== Step 2: Timeline brief ===&lt;br /&gt;
&lt;br /&gt;
The selection JSON is fed into a second prompt that asks the model to build an &amp;quot;internal dossier&amp;quot; — a compact alternate-history world brief. This includes:&lt;br /&gt;
&lt;br /&gt;
* A core timeline premise.&lt;br /&gt;
* A chronological &#039;&#039;historical_path&#039;&#039; of at least five entries spanning from divergence to publication date.&lt;br /&gt;
* Canonical facts to preserve (real-world events outside the divergence&#039;s causal chain).&lt;br /&gt;
* &amp;quot;Real-history traps to avoid&amp;quot; — events whose preconditions are disrupted by the divergence.&lt;br /&gt;
* In-world assumptions (what ordinary readers take for granted).&lt;br /&gt;
* Named entities (people, institutions, treaties, technologies) specific to this timeline.&lt;br /&gt;
* An article brief (genre, tone, writer persona, central question, thesis, section ideas).&lt;br /&gt;
* Style constraints (bans on meta-framing, &amp;quot;not X but Y&amp;quot; constructions, poetic codas, excessive em-dashes and exclamation marks).&lt;br /&gt;
* A visual brief for the photo editor.&lt;br /&gt;
&lt;br /&gt;
This step enforces a &amp;quot;proportional divergence rule&amp;quot;: the timeline should change only what the divergence actually changes, and leave causally unrelated real-world events intact.&lt;br /&gt;
&lt;br /&gt;
=== Step 3: Article generation ===&lt;br /&gt;
&lt;br /&gt;
The selection and timeline-brief JSONs are combined in a third prompt which instructs the model to write the full article as a single JSON object. The article must:&lt;br /&gt;
&lt;br /&gt;
* Be written entirely from inside the alternate timeline, with no meta-framing or comparison to real history.&lt;br /&gt;
* Be at least 1,500 words across its &amp;lt;code&amp;gt;body_blocks&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Include at least two callout blocks and one attributed quote block, interleaved naturally.&lt;br /&gt;
* Use period-appropriate prose matching the in-world date, place, and publication culture.&lt;br /&gt;
* Be entirely in the language of the source event (English, French, or Dutch).&lt;br /&gt;
* Include a hero-image prompt (in English) and geographic coordinates.&lt;br /&gt;
&lt;br /&gt;
The model is asked to self-check for in-world consistency, language correctness, and structural validity before outputting. If the returned JSON is malformed, a repair prompt asks the model to fix it.&lt;br /&gt;
&lt;br /&gt;
=== Step 4: In-world edit ===&lt;br /&gt;
&lt;br /&gt;
The draft JSON is passed through a fourth &amp;quot;line editor&amp;quot; prompt. This step:&lt;br /&gt;
&lt;br /&gt;
* Preserves meaning, canon, and timeline integrity.&lt;br /&gt;
* Removes AI-flavored rhetoric (&amp;quot;not X, but Y&amp;quot; constructions, pseudo-poetic endings, inflated abstraction, repetitive thesis phrasing).&lt;br /&gt;
* Replaces any accidental references to real-world events that should not exist in the diverged timeline.&lt;br /&gt;
* Forces the canonical event and publication dates from the selection step.&lt;br /&gt;
* Populates the internal &#039;&#039;original_event&#039;&#039;, &#039;&#039;departure_point&#039;&#039;, and &#039;&#039;ai_context&#039;&#039; fields.&lt;br /&gt;
&lt;br /&gt;
The pipeline then validates the edited payload: checking required fields, enforcing the ban on meta-themed publication names, verifying date consistency with the selection, confirming minimum callout and quote counts, and ensuring no quote is attributed to the article&#039;s own author.&lt;br /&gt;
&lt;br /&gt;
=== Step 5: Image prompts ===&lt;br /&gt;
&lt;br /&gt;
A fifth prompt, addressed to a &amp;quot;photo editor and visual archivist,&amp;quot; generates one to five image concepts. Each concept specifies:&lt;br /&gt;
&lt;br /&gt;
* A role (hero or supporting).&lt;br /&gt;
* An aspect ratio (landscape, portrait, or square).&lt;br /&gt;
* A detailed English-language image prompt.&lt;br /&gt;
* Alt text, caption, and credit in the article language.&lt;br /&gt;
&lt;br /&gt;
The prompt includes extensive rules for period-appropriate visual media (daguerreotype for 1840–1880, silver gelatin for 1880–1930, Kodachrome for 1970–2000, and so on), and bans on text in images, symmetrical compositions, &amp;quot;dramatic lighting,&amp;quot; and modern digital-art aesthetics. The returned concepts are normalised: roles are deduplicated, aspect ratios are validated, and the set is capped at five images.&lt;br /&gt;
&lt;br /&gt;
=== Step 6: Image generation and upload ===&lt;br /&gt;
&lt;br /&gt;
Each image prompt is wrapped in additional instructions enforcing candid documentary realism (or authentic non-photographic media for paintings, engravings, and archival objects) before being sent to OpenAI&#039;s image-generation endpoint (&amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt;). Generated images are decoded from base64, converted to optimised progressive JPEG via Pillow, and uploaded through the application&#039;s &amp;lt;code&amp;gt;/api/media&amp;lt;/code&amp;gt; endpoint. The hero image is set as the article&#039;s &#039;&#039;featured_image&#039;&#039;. Non-hero images are inserted into the &amp;lt;code&amp;gt;body_blocks&amp;lt;/code&amp;gt; array at evenly spaced positions.&lt;br /&gt;
&lt;br /&gt;
=== Step 7: Publication ===&lt;br /&gt;
&lt;br /&gt;
The assembled JSON payload — with body blocks, assets, featured image, byline, callouts, quotes, and editorial metadata — is POSTed to the &amp;lt;code&amp;gt;/api/articles&amp;lt;/code&amp;gt; endpoint. The API serializer sanitises body HTML via nh3, resolves image file paths to Wagtail embed tags, creates or looks up the &amp;lt;code&amp;gt;Author&amp;lt;/code&amp;gt; snippet, attaches the article as a child of the &amp;lt;code&amp;gt;ArticleIndexPage&amp;lt;/code&amp;gt;, and publishes it. The source &amp;lt;code&amp;gt;HistoricalEvent&amp;lt;/code&amp;gt; is marked as used. The checkpoint file is cleared.&lt;br /&gt;
&lt;br /&gt;
== REST API ==&lt;br /&gt;
&lt;br /&gt;
The application exposes a JSON API under &amp;lt;code&amp;gt;/api/&amp;lt;/code&amp;gt;, protected by bearer-token authentication for write operations and publicly readable:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;GET/POST /api/articles&amp;lt;/code&amp;gt; — list (paginated) or create articles.&lt;br /&gt;
* &amp;lt;code&amp;gt;GET/PUT /api/articles/&amp;lt;id&amp;gt;&amp;lt;/code&amp;gt; — retrieve or update a single article.&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /api/media&amp;lt;/code&amp;gt; — upload a base64-encoded image, which is stored as a &amp;lt;code&amp;gt;CustomImage&amp;lt;/code&amp;gt;.&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /api/markers&amp;lt;/code&amp;gt; — return geographic markers for all geolocated articles.&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /api/schema&amp;lt;/code&amp;gt; — serve the OpenAPI specification.&lt;br /&gt;
&lt;br /&gt;
Public documentation is available at &amp;lt;code&amp;gt;/docs&amp;lt;/code&amp;gt; (ReDoc) and &amp;lt;code&amp;gt;/swagger&amp;lt;/code&amp;gt; (Swagger UI).&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Home page ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;ArticleIndexPage&amp;lt;/code&amp;gt; serves as the site&#039;s home page, displaying up to seven recent articles, a &amp;quot;picture desk&amp;quot; mosaic of up to fifteen images (sampled hourly for variety), a map teaser with up to four geolocated articles, and navigation links for monthly archives.&lt;br /&gt;
&lt;br /&gt;
=== Article pages ===&lt;br /&gt;
&lt;br /&gt;
Each article page features a full-bleed hero image (when available), a masthead with the fictitious publication name, headline, subtitle, byline, dateline, and the event date. The body renders the StreamField blocks: paragraphs, headings, images with captions and credits, callouts, attributed quotes, pull quotes, bulleted and numbered lists, asides with nested content, and horizontal rules.&lt;br /&gt;
&lt;br /&gt;
=== Where/When ===&lt;br /&gt;
&lt;br /&gt;
An interactive &amp;quot;Where &amp;amp; When&amp;quot; page presents all geolocated articles on a [[Leaflet (software)|Leaflet]] map (using CartoDB tiles with light/dark themes) alongside a zoomable timeline. Articles are plotted by their event location and date. The map and article data are loaded via the &amp;lt;code&amp;gt;/api/markers&amp;lt;/code&amp;gt; endpoint.&lt;br /&gt;
&lt;br /&gt;
=== Monthly archives ===&lt;br /&gt;
&lt;br /&gt;
Articles can be browsed by the month of their in-universe event date. The index page dynamically builds archive links with article counts per month.&lt;br /&gt;
&lt;br /&gt;
=== Theme ===&lt;br /&gt;
&lt;br /&gt;
The site supports light and dark colour themes, toggled via a button in the masthead and persisted in &amp;lt;code&amp;gt;localStorage&amp;lt;/code&amp;gt;. Typography uses Playfair Display for display headings, Source Serif 4 for body text, and Inter for UI elements.&lt;br /&gt;
&lt;br /&gt;
== Multilingual support ==&lt;br /&gt;
&lt;br /&gt;
The Wikipedia scraper and article-generation pipeline support English, French, and Dutch. The language of each historical event is stored in the database and carried through to the generation prompts, which instruct the model to write all visible article content — title, subtitle, body, captions, quotes, and credits — in the source event&#039;s language. Internal editorial fields and image prompts remain in English.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[Alternate history]]&lt;br /&gt;
* [[Wagtail (CMS)]]&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Echoes_of_What_Wasn%27t&amp;diff=381</id>
		<title>Echoes of What Wasn&#039;t</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Echoes_of_What_Wasn%27t&amp;diff=381"/>
		<updated>2026-04-12T11:02:03Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name         = Echoes of What Wasn&#039;t&lt;br /&gt;
| url          = {{URL|https://echoes.yusupov.cloud}}&lt;br /&gt;
| developer    = Operator&lt;br /&gt;
| released     = 2025&lt;br /&gt;
| genre        = AI-generated alternate-history newspaper&lt;br /&gt;
| language     = Python&lt;br /&gt;
| framework    = [[Django]] 5.2 / [[Wagtail]] 7.3&lt;br /&gt;
| license      = Proprietary&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Echoes of What Wasn&#039;t&#039;&#039;&#039; (also known as &#039;&#039;&#039;Echoes&#039;&#039;&#039;) is a web application hosted at &amp;lt;code&amp;gt;echoes.yusupov.cloud&amp;lt;/code&amp;gt; that publishes AI-generated alternate-history newspaper and magazine articles. Each article takes a real historical event, invents a plausible point of divergence, and presents the consequences as an in-world retrospective written decades later by a fictitious journalist for a fictitious period-appropriate publication. The site&#039;s tagline is &amp;quot;Dispatches from Histories That Never Were.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Django 5.2 with Wagtail 7.3 as its content management system.&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt in the project repository lists Django 5.2.12 and Wagtail 7.3.1.&amp;lt;/ref&amp;gt; It uses [[Django REST framework]] for a JSON API, [[SQLite]] as its database, and is deployed behind [[Nginx]] with [[Gunicorn]] on a Linux server. Additional dependencies include [[Pillow (imaging library)|Pillow]] for image processing, [[nh3 (library)|nh3]] for HTML sanitisation, [[Beautiful Soup (HTML parser)|Beautiful Soup]] and [[lxml]] for scraping, and the [[OpenAI]] Python client for language-model and image-generation calls. Geographic features use &amp;lt;code&amp;gt;wagtailgeowidget&amp;lt;/code&amp;gt; with Google Maps integration.&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
=== Article pages ===&lt;br /&gt;
&lt;br /&gt;
Each article is a Wagtail &amp;lt;code&amp;gt;ArticlePage&amp;lt;/code&amp;gt;, a child of a single &amp;lt;code&amp;gt;ArticleIndexPage&amp;lt;/code&amp;gt; in the page tree. An article carries:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;title&#039;&#039;, &#039;&#039;subtitle&#039;&#039;, &#039;&#039;publication&#039;&#039; (the fictitious newspaper/magazine name), &#039;&#039;date&#039;&#039; (in-universe publication date), &#039;&#039;event_date&#039;&#039; (the historical event&#039;s date), &#039;&#039;location&#039;&#039;, and a foreign key to an &amp;lt;code&amp;gt;Author&amp;lt;/code&amp;gt; snippet (exposed via the API as &#039;&#039;byline&#039;&#039;).&lt;br /&gt;
* A &#039;&#039;body_richtext&#039;&#039; field (Wagtail &amp;lt;code&amp;gt;RichTextField&amp;lt;/code&amp;gt;) containing the article&#039;s HTML body, sanitised on ingest.&lt;br /&gt;
* A &#039;&#039;body&#039;&#039; [[Wagtail StreamField|StreamField]] supporting paragraph, heading, callout, quote, pull-quote, bulleted list, numbered list, image, aside (with its own nested stream), and horizontal-rule block types.&lt;br /&gt;
* Separate &#039;&#039;callouts&#039;&#039; and &#039;&#039;quotes&#039;&#039; StreamFields for editorial pull-outs and attributed quotations.&lt;br /&gt;
* A JSON &#039;&#039;assets&#039;&#039; field listing image metadata (src, alt, caption, credit, prompt).&lt;br /&gt;
* A &#039;&#039;featured_image&#039;&#039; foreign key to a custom image model (&amp;lt;code&amp;gt;CustomImage&amp;lt;/code&amp;gt;, extending Wagtail&#039;s &amp;lt;code&amp;gt;AbstractImage&amp;lt;/code&amp;gt; with a description field).&lt;br /&gt;
* Internal editorial fields: &#039;&#039;original_event&#039;&#039;, &#039;&#039;departure_point&#039;&#039;, and &#039;&#039;ai_context&#039;&#039;, which are visible in the Wagtail admin but not rendered on the public site.&lt;br /&gt;
* Geographic fields: &#039;&#039;event_location&#039;&#039; (address string), &#039;&#039;geo_location&#039;&#039; (WKT point), &#039;&#039;latitude&#039;&#039;, and &#039;&#039;longitude&#039;&#039;. The model&#039;s &amp;lt;code&amp;gt;save()&amp;lt;/code&amp;gt; method synchronises the WKT point and decimal-degree fields bidirectionally.&lt;br /&gt;
&lt;br /&gt;
=== Historical events ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;HistoricalEvent&amp;lt;/code&amp;gt; model stores real events scraped from Wikipedia, keyed by ISO date, astronomical year, event text, language code, and a boolean &#039;&#039;used&#039;&#039; flag.&lt;br /&gt;
&lt;br /&gt;
=== Custom images ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;CustomImage&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;CustomRendition&amp;lt;/code&amp;gt; extend Wagtail&#039;s abstract image models to add a &#039;&#039;description&#039;&#039; text field, used to store the original DALL·E prompt.&lt;br /&gt;
&lt;br /&gt;
== Article generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Article generation is performed by the standalone script &amp;lt;code&amp;gt;_generate_article.py&amp;lt;/code&amp;gt;, which orchestrates a six-step pipeline of OpenAI API calls, checkpoint-resumed so that a failure at any step does not waste prior work. The default text model is GPT-5 (with GPT-4o as fallback); the image model is &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Step 0: Event sourcing ===&lt;br /&gt;
&lt;br /&gt;
A separate scraper (&amp;lt;code&amp;gt;_scrape_wikipedia_events.py&amp;lt;/code&amp;gt;) fetches every day-of-year page from Wikipedia (e.g. &amp;quot;January 1&amp;quot;, &amp;quot;February 14&amp;quot;) for English, French, and Dutch editions. It parses the &amp;quot;Events&amp;quot; sections, extracts each entry&#039;s year and text, and stores them as &amp;lt;code&amp;gt;HistoricalEvent&amp;lt;/code&amp;gt; rows. The scraper respects rate limits and identifies itself via a custom User-Agent string.&lt;br /&gt;
&lt;br /&gt;
=== Step 1: Event selection ===&lt;br /&gt;
&lt;br /&gt;
The pipeline queries the database for unused Common Era events whose month and day match the current date and whose year is at least 50 years in the past. Five candidates are sampled at random and presented in a structured prompt to the text model, which acts as an &amp;quot;editorial planner.&amp;quot; The model returns JSON selecting the single most promising event for an alternate-history feature, together with a proposed divergence date, publication date, publication context (name, type, city, editorial stance, intended readership), article angle, and a list of visual possibilities. The pipeline validates that the chosen event ID was in the candidate set.&lt;br /&gt;
&lt;br /&gt;
=== Step 2: Timeline brief ===&lt;br /&gt;
&lt;br /&gt;
The selection JSON is fed into a second prompt that asks the model to build an &amp;quot;internal dossier&amp;quot; — a compact alternate-history world brief. This includes:&lt;br /&gt;
&lt;br /&gt;
* A core timeline premise.&lt;br /&gt;
* A chronological &#039;&#039;historical_path&#039;&#039; of at least five entries spanning from divergence to publication date.&lt;br /&gt;
* Canonical facts to preserve (real-world events outside the divergence&#039;s causal chain).&lt;br /&gt;
* &amp;quot;Real-history traps to avoid&amp;quot; — events whose preconditions are disrupted by the divergence.&lt;br /&gt;
* In-world assumptions (what ordinary readers take for granted).&lt;br /&gt;
* Named entities (people, institutions, treaties, technologies) specific to this timeline.&lt;br /&gt;
* An article brief (genre, tone, writer persona, central question, thesis, section ideas).&lt;br /&gt;
* Style constraints (bans on meta-framing, &amp;quot;not X but Y&amp;quot; constructions, poetic codas, excessive em-dashes and exclamation marks).&lt;br /&gt;
* A visual brief for the photo editor.&lt;br /&gt;
&lt;br /&gt;
This step enforces a &amp;quot;proportional divergence rule&amp;quot;: the timeline should change only what the divergence actually changes, and leave causally unrelated real-world events intact.&lt;br /&gt;
&lt;br /&gt;
=== Step 3: Article generation ===&lt;br /&gt;
&lt;br /&gt;
The selection and timeline-brief JSONs are combined in a third prompt which instructs the model to write the full article as a single JSON object. The article must:&lt;br /&gt;
&lt;br /&gt;
* Be written entirely from inside the alternate timeline, with no meta-framing or comparison to real history.&lt;br /&gt;
* Be at least 1,500 words across its &amp;lt;code&amp;gt;body_blocks&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Include at least two callout blocks and one attributed quote block, interleaved naturally.&lt;br /&gt;
* Use period-appropriate prose matching the in-world date, place, and publication culture.&lt;br /&gt;
* Be entirely in the language of the source event (English, French, or Dutch).&lt;br /&gt;
* Include a hero-image prompt (in English) and geographic coordinates.&lt;br /&gt;
&lt;br /&gt;
The model is asked to self-check for in-world consistency, language correctness, and structural validity before outputting. If the returned JSON is malformed, a repair prompt asks the model to fix it.&lt;br /&gt;
&lt;br /&gt;
=== Step 4: In-world edit ===&lt;br /&gt;
&lt;br /&gt;
The draft JSON is passed through a fourth &amp;quot;line editor&amp;quot; prompt. This step:&lt;br /&gt;
&lt;br /&gt;
* Preserves meaning, canon, and timeline integrity.&lt;br /&gt;
* Removes AI-flavored rhetoric (&amp;quot;not X, but Y&amp;quot; constructions, pseudo-poetic endings, inflated abstraction, repetitive thesis phrasing).&lt;br /&gt;
* Replaces any accidental references to real-world events that should not exist in the diverged timeline.&lt;br /&gt;
* Forces the canonical event and publication dates from the selection step.&lt;br /&gt;
* Populates the internal &#039;&#039;original_event&#039;&#039;, &#039;&#039;departure_point&#039;&#039;, and &#039;&#039;ai_context&#039;&#039; fields.&lt;br /&gt;
&lt;br /&gt;
The pipeline then validates the edited payload: checking required fields, enforcing the ban on meta-themed publication names, verifying date consistency with the selection, confirming minimum callout and quote counts, and ensuring no quote is attributed to the article&#039;s own author.&lt;br /&gt;
&lt;br /&gt;
=== Step 5: Image prompts ===&lt;br /&gt;
&lt;br /&gt;
A fifth prompt, addressed to a &amp;quot;photo editor and visual archivist,&amp;quot; generates one to five image concepts. Each concept specifies:&lt;br /&gt;
&lt;br /&gt;
* A role (hero or supporting).&lt;br /&gt;
* An aspect ratio (landscape, portrait, or square).&lt;br /&gt;
* A detailed English-language image prompt.&lt;br /&gt;
* Alt text, caption, and credit in the article language.&lt;br /&gt;
&lt;br /&gt;
The prompt includes extensive rules for period-appropriate visual media (daguerreotype for 1840–1880, silver gelatin for 1880–1930, Kodachrome for 1970–2000, and so on), and bans on text in images, symmetrical compositions, &amp;quot;dramatic lighting,&amp;quot; and modern digital-art aesthetics. The returned concepts are normalised: roles are deduplicated, aspect ratios are validated, and the set is capped at five images.&lt;br /&gt;
&lt;br /&gt;
=== Step 6: Image generation and upload ===&lt;br /&gt;
&lt;br /&gt;
Each image prompt is wrapped in additional instructions enforcing candid documentary realism (or authentic non-photographic media for paintings, engravings, and archival objects) before being sent to OpenAI&#039;s image-generation endpoint (&amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt;). Generated images are decoded from base64, converted to optimised progressive JPEG via Pillow, and uploaded through the application&#039;s &amp;lt;code&amp;gt;/api/media&amp;lt;/code&amp;gt; endpoint. The hero image is set as the article&#039;s &#039;&#039;featured_image&#039;&#039;. Non-hero images are inserted into the &amp;lt;code&amp;gt;body_blocks&amp;lt;/code&amp;gt; array at evenly spaced positions.&lt;br /&gt;
&lt;br /&gt;
=== Step 7: Publication ===&lt;br /&gt;
&lt;br /&gt;
The assembled JSON payload — with body blocks, assets, featured image, byline, callouts, quotes, and editorial metadata — is POSTed to the &amp;lt;code&amp;gt;/api/articles&amp;lt;/code&amp;gt; endpoint. The API serializer sanitises body HTML via nh3, resolves image file paths to Wagtail embed tags, creates or looks up the &amp;lt;code&amp;gt;Author&amp;lt;/code&amp;gt; snippet, attaches the article as a child of the &amp;lt;code&amp;gt;ArticleIndexPage&amp;lt;/code&amp;gt;, and publishes it. The source &amp;lt;code&amp;gt;HistoricalEvent&amp;lt;/code&amp;gt; is marked as used. The checkpoint file is cleared.&lt;br /&gt;
&lt;br /&gt;
== REST API ==&lt;br /&gt;
&lt;br /&gt;
The application exposes a JSON API under &amp;lt;code&amp;gt;/api/&amp;lt;/code&amp;gt;, protected by bearer-token authentication for write operations and publicly readable:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;GET/POST /api/articles&amp;lt;/code&amp;gt; — list (paginated) or create articles.&lt;br /&gt;
* &amp;lt;code&amp;gt;GET/PUT /api/articles/&amp;lt;id&amp;gt;&amp;lt;/code&amp;gt; — retrieve or update a single article.&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /api/media&amp;lt;/code&amp;gt; — upload a base64-encoded image, which is stored as a &amp;lt;code&amp;gt;CustomImage&amp;lt;/code&amp;gt;.&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /api/markers&amp;lt;/code&amp;gt; — return geographic markers for all geolocated articles.&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /api/schema&amp;lt;/code&amp;gt; — serve the OpenAPI specification.&lt;br /&gt;
&lt;br /&gt;
Public documentation is available at &amp;lt;code&amp;gt;/docs&amp;lt;/code&amp;gt; (ReDoc) and &amp;lt;code&amp;gt;/swagger&amp;lt;/code&amp;gt; (Swagger UI).&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Home page ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;ArticleIndexPage&amp;lt;/code&amp;gt; serves as the site&#039;s home page, displaying up to seven recent articles, a &amp;quot;picture desk&amp;quot; mosaic of up to fifteen images (sampled hourly for variety), a map teaser with up to four geolocated articles, and navigation links for monthly archives.&lt;br /&gt;
&lt;br /&gt;
=== Article pages ===&lt;br /&gt;
&lt;br /&gt;
Each article page features a full-bleed hero image (when available), a masthead with the fictitious publication name, headline, subtitle, byline, dateline, and the event date. The body renders the StreamField blocks: paragraphs, headings, images with captions and credits, callouts, attributed quotes, pull quotes, bulleted and numbered lists, asides with nested content, and horizontal rules.&lt;br /&gt;
&lt;br /&gt;
=== Where/When ===&lt;br /&gt;
&lt;br /&gt;
An interactive &amp;quot;Where &amp;amp; When&amp;quot; page presents all geolocated articles on a [[Leaflet (software)|Leaflet]] map (using CartoDB tiles with light/dark themes) alongside a zoomable timeline. Articles are plotted by their event location and date. The map and article data are loaded via the &amp;lt;code&amp;gt;/api/markers&amp;lt;/code&amp;gt; endpoint.&lt;br /&gt;
&lt;br /&gt;
=== Monthly archives ===&lt;br /&gt;
&lt;br /&gt;
Articles can be browsed by the month of their in-universe event date. The index page dynamically builds archive links with article counts per month.&lt;br /&gt;
&lt;br /&gt;
=== Theme ===&lt;br /&gt;
&lt;br /&gt;
The site supports light and dark colour themes, toggled via a button in the masthead and persisted in &amp;lt;code&amp;gt;localStorage&amp;lt;/code&amp;gt;. Typography uses Playfair Display for display headings, Source Serif 4 for body text, and Inter for UI elements.&lt;br /&gt;
&lt;br /&gt;
== Multilingual support ==&lt;br /&gt;
&lt;br /&gt;
The Wikipedia scraper and article-generation pipeline support English, French, and Dutch. The language of each historical event is stored in the database and carried through to the generation prompts, which instruct the model to write all visible article content — title, subtitle, body, captions, quotes, and credits — in the source event&#039;s language. Internal editorial fields and image prompts remain in English.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[Alternate history]]&lt;br /&gt;
* [[Wagtail (CMS)]]&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Echoes_of_What_Wasn%27t&amp;diff=380</id>
		<title>Echoes of What Wasn&#039;t</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Echoes_of_What_Wasn%27t&amp;diff=380"/>
		<updated>2026-04-12T11:00:37Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Created page with &amp;quot;{{Infobox software | name         = Echoes of What Wasn&amp;#039;t | url          = {{URL|https://echoes.yusupov.cloud}} | developer    = Operator | released     = 2025 | genre        = AI-generated alternate-history newspaper | language     = Python | framework    = Django 5.2 / Wagtail 7.3 | license      = Proprietary }}  &amp;#039;&amp;#039;&amp;#039;Echoes of What Wasn&amp;#039;t&amp;#039;&amp;#039;&amp;#039; (also known as &amp;#039;&amp;#039;&amp;#039;Echoes&amp;#039;&amp;#039;&amp;#039;) is a web application hosted at &amp;lt;code&amp;gt;echoes.yusupov.cloud&amp;lt;/code&amp;gt; that publishes AI-generated...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox software&lt;br /&gt;
| name         = Echoes of What Wasn&#039;t&lt;br /&gt;
| url          = {{URL|https://echoes.yusupov.cloud}}&lt;br /&gt;
| developer    = Operator&lt;br /&gt;
| released     = 2025&lt;br /&gt;
| genre        = AI-generated alternate-history newspaper&lt;br /&gt;
| language     = Python&lt;br /&gt;
| framework    = [[Django]] 5.2 / [[Wagtail]] 7.3&lt;br /&gt;
| license      = Proprietary&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Echoes of What Wasn&#039;t&#039;&#039;&#039; (also known as &#039;&#039;&#039;Echoes&#039;&#039;&#039;) is a web application hosted at &amp;lt;code&amp;gt;echoes.yusupov.cloud&amp;lt;/code&amp;gt; that publishes AI-generated alternate-history newspaper and magazine articles. Each article takes a real historical event, invents a plausible point of divergence, and presents the consequences as an in-world retrospective written decades later by a fictitious journalist for a fictitious period-appropriate publication. The site&#039;s tagline is &amp;quot;Dispatches from Histories That Never Were.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Technology stack ==&lt;br /&gt;
&lt;br /&gt;
The application is built on Django 5.2 with Wagtail 7.3 as its content management system.&amp;lt;ref name=&amp;quot;requirements&amp;quot;&amp;gt;requirements.txt in the project repository lists Django 5.2.12 and Wagtail 7.3.1.&amp;lt;/ref&amp;gt; It uses [[Django REST framework]] for a JSON API, [[SQLite]] as its database, and is deployed behind [[Nginx]] with [[Gunicorn]] on a Linux server. Additional dependencies include [[Pillow (imaging library)|Pillow]] for image processing, [[nh3 (library)|nh3]] for HTML sanitisation, [[Beautiful Soup (HTML parser)|Beautiful Soup]] and [[lxml]] for scraping, and the [[OpenAI]] Python client for language-model and image-generation calls. Geographic features use &amp;lt;code&amp;gt;wagtailgeowidget&amp;lt;/code&amp;gt; with Google Maps integration.&lt;br /&gt;
&lt;br /&gt;
== Data model ==&lt;br /&gt;
&lt;br /&gt;
=== Article pages ===&lt;br /&gt;
&lt;br /&gt;
Each article is a Wagtail &amp;lt;code&amp;gt;ArticlePage&amp;lt;/code&amp;gt;, a child of a single &amp;lt;code&amp;gt;ArticleIndexPage&amp;lt;/code&amp;gt; in the page tree. An article carries:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;title&#039;&#039;, &#039;&#039;subtitle&#039;&#039;, &#039;&#039;publication&#039;&#039; (the fictitious newspaper/magazine name), &#039;&#039;date&#039;&#039; (in-universe publication date), &#039;&#039;event_date&#039;&#039; (the historical event&#039;s date), &#039;&#039;location&#039;&#039;, and a foreign key to an &amp;lt;code&amp;gt;Author&amp;lt;/code&amp;gt; snippet (exposed via the API as &#039;&#039;byline&#039;&#039;).&lt;br /&gt;
* A &#039;&#039;body_richtext&#039;&#039; field (Wagtail &amp;lt;code&amp;gt;RichTextField&amp;lt;/code&amp;gt;) containing the article&#039;s HTML body, sanitised on ingest.&lt;br /&gt;
* A &#039;&#039;body&#039;&#039; [[Wagtail StreamField|StreamField]] supporting paragraph, heading, callout, quote, pull-quote, bulleted list, numbered list, image, aside (with its own nested stream), and horizontal-rule block types.&lt;br /&gt;
* Separate &#039;&#039;callouts&#039;&#039; and &#039;&#039;quotes&#039;&#039; StreamFields for editorial pull-outs and attributed quotations.&lt;br /&gt;
* A JSON &#039;&#039;assets&#039;&#039; field listing image metadata (src, alt, caption, credit, prompt).&lt;br /&gt;
* A &#039;&#039;featured_image&#039;&#039; foreign key to a custom image model (&amp;lt;code&amp;gt;CustomImage&amp;lt;/code&amp;gt;, extending Wagtail&#039;s &amp;lt;code&amp;gt;AbstractImage&amp;lt;/code&amp;gt; with a description field).&lt;br /&gt;
* Internal editorial fields: &#039;&#039;original_event&#039;&#039;, &#039;&#039;departure_point&#039;&#039;, and &#039;&#039;ai_context&#039;&#039;, which are visible in the Wagtail admin but not rendered on the public site.&lt;br /&gt;
* Geographic fields: &#039;&#039;event_location&#039;&#039; (address string), &#039;&#039;geo_location&#039;&#039; (WKT point), &#039;&#039;latitude&#039;&#039;, and &#039;&#039;longitude&#039;&#039;. The model&#039;s &amp;lt;code&amp;gt;save()&amp;lt;/code&amp;gt; method synchronises the WKT point and decimal-degree fields bidirectionally.&lt;br /&gt;
&lt;br /&gt;
=== Historical events ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;HistoricalEvent&amp;lt;/code&amp;gt; model stores real events scraped from Wikipedia, keyed by ISO date, astronomical year, event text, language code, and a boolean &#039;&#039;used&#039;&#039; flag.&lt;br /&gt;
&lt;br /&gt;
=== Custom images ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;CustomImage&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;CustomRendition&amp;lt;/code&amp;gt; extend Wagtail&#039;s abstract image models to add a &#039;&#039;description&#039;&#039; text field, used to store the original DALL·E prompt.&lt;br /&gt;
&lt;br /&gt;
== Article generation pipeline ==&lt;br /&gt;
&lt;br /&gt;
Article generation is performed by the standalone script &amp;lt;code&amp;gt;_generate_article.py&amp;lt;/code&amp;gt;, which orchestrates a six-step pipeline of OpenAI API calls, checkpoint-resumed so that a failure at any step does not waste prior work. The default text model is GPT-5 (with GPT-4o as fallback); the image model is &amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Step 0: Event sourcing ===&lt;br /&gt;
&lt;br /&gt;
A separate scraper (&amp;lt;code&amp;gt;_scrape_wikipedia_events.py&amp;lt;/code&amp;gt;) fetches every day-of-year page from Wikipedia (e.g. &amp;quot;January 1&amp;quot;, &amp;quot;February 14&amp;quot;) for English, French, and Dutch editions. It parses the &amp;quot;Events&amp;quot; sections, extracts each entry&#039;s year and text, and stores them as &amp;lt;code&amp;gt;HistoricalEvent&amp;lt;/code&amp;gt; rows. The scraper respects rate limits and identifies itself via a custom User-Agent string.&lt;br /&gt;
&lt;br /&gt;
=== Step 1: Event selection ===&lt;br /&gt;
&lt;br /&gt;
The pipeline queries the database for unused Common Era events whose month and day match the current date and whose year is at least 50 years in the past. Five candidates are sampled at random and presented in a structured prompt to the text model, which acts as an &amp;quot;editorial planner.&amp;quot; The model returns JSON selecting the single most promising event for an alternate-history feature, together with a proposed divergence date, publication date, publication context (name, type, city, editorial stance, intended readership), article angle, and a list of visual possibilities. The pipeline validates that the chosen event ID was in the candidate set.&lt;br /&gt;
&lt;br /&gt;
=== Step 2: Timeline brief ===&lt;br /&gt;
&lt;br /&gt;
The selection JSON is fed into a second prompt that asks the model to build an &amp;quot;internal dossier&amp;quot; — a compact alternate-history world brief. This includes:&lt;br /&gt;
&lt;br /&gt;
* A core timeline premise.&lt;br /&gt;
* A chronological &#039;&#039;historical_path&#039;&#039; of at least five entries spanning from divergence to publication date.&lt;br /&gt;
* Canonical facts to preserve (real-world events outside the divergence&#039;s causal chain).&lt;br /&gt;
* &amp;quot;Real-history traps to avoid&amp;quot; — events whose preconditions are disrupted by the divergence.&lt;br /&gt;
* In-world assumptions (what ordinary readers take for granted).&lt;br /&gt;
* Named entities (people, institutions, treaties, technologies) specific to this timeline.&lt;br /&gt;
* An article brief (genre, tone, writer persona, central question, thesis, section ideas).&lt;br /&gt;
* Style constraints (bans on meta-framing, &amp;quot;not X but Y&amp;quot; constructions, poetic codas, excessive em-dashes and exclamation marks).&lt;br /&gt;
* A visual brief for the photo editor.&lt;br /&gt;
&lt;br /&gt;
This step enforces a &amp;quot;proportional divergence rule&amp;quot;: the timeline should change only what the divergence actually changes, and leave causally unrelated real-world events intact.&lt;br /&gt;
&lt;br /&gt;
=== Step 3: Article generation ===&lt;br /&gt;
&lt;br /&gt;
The selection and timeline-brief JSONs are combined in a third prompt which instructs the model to write the full article as a single JSON object. The article must:&lt;br /&gt;
&lt;br /&gt;
* Be written entirely from inside the alternate timeline, with no meta-framing or comparison to real history.&lt;br /&gt;
* Be at least 1,500 words across its &amp;lt;code&amp;gt;body_blocks&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Include at least two callout blocks and one attributed quote block, interleaved naturally.&lt;br /&gt;
* Use period-appropriate prose matching the in-world date, place, and publication culture.&lt;br /&gt;
* Be entirely in the language of the source event (English, French, or Dutch).&lt;br /&gt;
* Include a hero-image prompt (in English) and geographic coordinates.&lt;br /&gt;
&lt;br /&gt;
The model is asked to self-check for in-world consistency, language correctness, and structural validity before outputting. If the returned JSON is malformed, a repair prompt asks the model to fix it.&lt;br /&gt;
&lt;br /&gt;
=== Step 4: In-world edit ===&lt;br /&gt;
&lt;br /&gt;
The draft JSON is passed through a fourth &amp;quot;line editor&amp;quot; prompt. This step:&lt;br /&gt;
&lt;br /&gt;
* Preserves meaning, canon, and timeline integrity.&lt;br /&gt;
* Removes AI-flavored rhetoric (&amp;quot;not X, but Y&amp;quot; constructions, pseudo-poetic endings, inflated abstraction, repetitive thesis phrasing).&lt;br /&gt;
* Replaces any accidental references to real-world events that should not exist in the diverged timeline.&lt;br /&gt;
* Forces the canonical event and publication dates from the selection step.&lt;br /&gt;
* Populates the internal &#039;&#039;original_event&#039;&#039;, &#039;&#039;departure_point&#039;&#039;, and &#039;&#039;ai_context&#039;&#039; fields.&lt;br /&gt;
&lt;br /&gt;
The pipeline then validates the edited payload: checking required fields, enforcing the ban on meta-themed publication names, verifying date consistency with the selection, confirming minimum callout and quote counts, and ensuring no quote is attributed to the article&#039;s own author.&lt;br /&gt;
&lt;br /&gt;
=== Step 5: Image prompts ===&lt;br /&gt;
&lt;br /&gt;
A fifth prompt, addressed to a &amp;quot;photo editor and visual archivist,&amp;quot; generates one to five image concepts. Each concept specifies:&lt;br /&gt;
&lt;br /&gt;
* A role (hero or supporting).&lt;br /&gt;
* An aspect ratio (landscape, portrait, or square).&lt;br /&gt;
* A detailed English-language image prompt.&lt;br /&gt;
* Alt text, caption, and credit in the article language.&lt;br /&gt;
&lt;br /&gt;
The prompt includes extensive rules for period-appropriate visual media (daguerreotype for 1840–1880, silver gelatin for 1880–1930, Kodachrome for 1970–2000, and so on), and bans on text in images, symmetrical compositions, &amp;quot;dramatic lighting,&amp;quot; and modern digital-art aesthetics. The returned concepts are normalised: roles are deduplicated, aspect ratios are validated, and the set is capped at five images.&lt;br /&gt;
&lt;br /&gt;
=== Step 6: Image generation and upload ===&lt;br /&gt;
&lt;br /&gt;
Each image prompt is wrapped in additional instructions enforcing candid documentary realism (or authentic non-photographic media for paintings, engravings, and archival objects) before being sent to OpenAI&#039;s image-generation endpoint (&amp;lt;code&amp;gt;gpt-image-1&amp;lt;/code&amp;gt;). Generated images are decoded from base64, converted to optimised progressive JPEG via Pillow, and uploaded through the application&#039;s &amp;lt;code&amp;gt;/api/media&amp;lt;/code&amp;gt; endpoint. The hero image is set as the article&#039;s &#039;&#039;featured_image&#039;&#039;. Non-hero images are inserted into the &amp;lt;code&amp;gt;body_blocks&amp;lt;/code&amp;gt; array at evenly spaced positions.&lt;br /&gt;
&lt;br /&gt;
=== Step 7: Publication ===&lt;br /&gt;
&lt;br /&gt;
The assembled JSON payload — with body blocks, assets, featured image, byline, callouts, quotes, and editorial metadata — is POSTed to the &amp;lt;code&amp;gt;/api/articles&amp;lt;/code&amp;gt; endpoint. The API serializer sanitises body HTML via nh3, resolves image file paths to Wagtail embed tags, creates or looks up the &amp;lt;code&amp;gt;Author&amp;lt;/code&amp;gt; snippet, attaches the article as a child of the &amp;lt;code&amp;gt;ArticleIndexPage&amp;lt;/code&amp;gt;, and publishes it. The source &amp;lt;code&amp;gt;HistoricalEvent&amp;lt;/code&amp;gt; is marked as used. The checkpoint file is cleared.&lt;br /&gt;
&lt;br /&gt;
== REST API ==&lt;br /&gt;
&lt;br /&gt;
The application exposes a JSON API under &amp;lt;code&amp;gt;/api/&amp;lt;/code&amp;gt;, protected by bearer-token authentication for write operations and publicly readable:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;GET/POST /api/articles&amp;lt;/code&amp;gt; — list (paginated) or create articles.&lt;br /&gt;
* &amp;lt;code&amp;gt;GET/PUT /api/articles/&amp;lt;id&amp;gt;&amp;lt;/code&amp;gt; — retrieve or update a single article.&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /api/media&amp;lt;/code&amp;gt; — upload a base64-encoded image, which is stored as a &amp;lt;code&amp;gt;CustomImage&amp;lt;/code&amp;gt;.&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /api/markers&amp;lt;/code&amp;gt; — return geographic markers for all geolocated articles.&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /api/schema&amp;lt;/code&amp;gt; — serve the OpenAPI specification.&lt;br /&gt;
&lt;br /&gt;
Public documentation is available at &amp;lt;code&amp;gt;/docs&amp;lt;/code&amp;gt; (ReDoc) and &amp;lt;code&amp;gt;/swagger&amp;lt;/code&amp;gt; (Swagger UI).&lt;br /&gt;
&lt;br /&gt;
== Public interface ==&lt;br /&gt;
&lt;br /&gt;
=== Home page ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;ArticleIndexPage&amp;lt;/code&amp;gt; serves as the site&#039;s home page, displaying up to seven recent articles, a &amp;quot;picture desk&amp;quot; mosaic of up to fifteen images (sampled hourly for variety), a map teaser with up to four geolocated articles, and navigation links for monthly archives.&lt;br /&gt;
&lt;br /&gt;
=== Article pages ===&lt;br /&gt;
&lt;br /&gt;
Each article page features a full-bleed hero image (when available), a masthead with the fictitious publication name, headline, subtitle, byline, dateline, and the event date. The body renders the StreamField blocks: paragraphs, headings, images with captions and credits, callouts, attributed quotes, pull quotes, bulleted and numbered lists, asides with nested content, and horizontal rules.&lt;br /&gt;
&lt;br /&gt;
=== Where/When ===&lt;br /&gt;
&lt;br /&gt;
An interactive &amp;quot;Where &amp;amp; When&amp;quot; page presents all geolocated articles on a [[Leaflet (software)|Leaflet]] map (using CartoDB tiles with light/dark themes) alongside a zoomable timeline. Articles are plotted by their event location and date. The map and article data are loaded via the &amp;lt;code&amp;gt;/api/markers&amp;lt;/code&amp;gt; endpoint.&lt;br /&gt;
&lt;br /&gt;
=== Monthly archives ===&lt;br /&gt;
&lt;br /&gt;
Articles can be browsed by the month of their in-universe event date. The index page dynamically builds archive links with article counts per month.&lt;br /&gt;
&lt;br /&gt;
=== Theme ===&lt;br /&gt;
&lt;br /&gt;
The site supports light and dark colour themes, toggled via a button in the masthead and persisted in &amp;lt;code&amp;gt;localStorage&amp;lt;/code&amp;gt;. Typography uses Playfair Display for display headings, Source Serif 4 for body text, and Inter for UI elements.&lt;br /&gt;
&lt;br /&gt;
== Multilingual support ==&lt;br /&gt;
&lt;br /&gt;
The Wikipedia scraper and article-generation pipeline support English, French, and Dutch. The language of each historical event is stored in the database and carried through to the generation prompts, which instruct the model to write all visible article content — title, subtitle, body, captions, quotes, and credits — in the source event&#039;s language. Internal editorial fields and image prompts remain in English.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[Alternate history]]&lt;br /&gt;
* [[Wagtail (CMS)]]&lt;br /&gt;
* [[OpenAI]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=379</id>
		<title>Yusupov.cloud</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=379"/>
		<updated>2026-04-12T10:52:31Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Subdomains and projects */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name           = yusupov.cloud&lt;br /&gt;
| 1 url            = https://yusupov.cloud&lt;br /&gt;
| 2 type           = Personal web sites&lt;br /&gt;
| 3 owner          = [[Michel Vuijlsteke]]&lt;br /&gt;
| 4 launched       = 2025&lt;br /&gt;
| 5 current_status = Online&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;yusupov.cloud&#039;&#039;&#039; is a personal domain and virtual private server operated by Belgian technologist [[Michel Vuijlsteke]]. It hosts multiple small web applications on subdomains and at the apex domain. One of these is a MediaWiki installation titled “Yusupov’s House.” The setup is presented as a web-era continuation of the do-it-yourself ethos of Vuijlsteke’s 1990s BBS of the same name.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot;&amp;gt;“Yusupov’s House,” &#039;&#039;yusupov.cloud&#039;&#039; (wiki), accessed 10 October 2025, https://yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
Among the projects on the domain is a MediaWiki (at the apex, &#039;&#039;yusupov.cloud&#039;&#039;) running MediaWiki 1.44.0 with PHP 8.3.6 (FPM) and SQLite, using the Vector skin and core extensions for citations and template scripting.&amp;lt;ref name=&amp;quot;version&amp;quot;&amp;gt;‘‘Special:Version’’ page, &#039;&#039;yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://yusupov.cloud/wiki/Special:Version&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Subdomains and projects ==&lt;br /&gt;
Publicly visible projects include:&lt;br /&gt;
&lt;br /&gt;
* [https://acbc.yusupov.cloud acbc.yusupov.cloud] — &#039;&#039;A Cabinet of Brief Curiosities&#039;&#039;, generating tiny three-sentence surreal/horror micro-stories with an hourly cadence and an archive. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;acbc-home&amp;quot;&amp;gt;A Cabinet of Brief Curiosities (home), &#039;&#039;acbc.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://acbc.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://agenda.yusupov.cloud agenda.yusupov.cloud] — &#039;&#039;A Life in Planners&#039;&#039;, a structured journal chronicling the final years of the operator’s mother, with calendar, food, medications, measurements, and statistics views (multilingual UI).&amp;lt;ref name=&amp;quot;agenda&amp;quot;&amp;gt;“A life in planners,” &#039;&#039;agenda.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://agenda.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://cloud.yusupov.cloud cloud.yusupov.cloud] — &#039;&#039;Cloud&#039;&#039;, a series of static html creative coding experiments, simulations, and games including: timebeat, fire and snake simulations, biomass metaballs, cs3, &#039;&#039;Cross&#039;&#039; crossword puzzle game, image dithering tool, books, &#039;&#039;Elite Galaxy Explorer&#039;&#039;, ZX Spectrum loading screen simulator, Carcassonne, 3D boids flocking algorithm, physarum slime mold simulation, temps temperature visualization, &#039;&#039;The Chronicle of Hamurabi&#039;&#039; ancient Sumeria resource management game, and gatekeeper.&amp;lt;ref name=&amp;quot;cloud-home&amp;quot;&amp;gt;&amp;quot;cloud,&amp;quot; &#039;&#039;cloud.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://cloud.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://digest.yusupov.cloud digest.yusupov.cloud] — &#039;&#039;Digest&#039;&#039;, daily seasonal AI-assisted recipes inspired by current events, browsable by meal type and ingredients.&amp;lt;ref name=&amp;quot;digest-home&amp;quot;&amp;gt;“Digest — Daily recipes inspired by the news,” &#039;&#039;digest.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://digest.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://echoes.yusupov.cloud echoes.yusupov.cloud] — &#039;&#039;[[Echoes of What Wasn&#039;t]]&#039;&#039;, an AI-generated alternate-history newspaper presenting richly detailed articles about historical events as if they had unfolded differently. A pipeline scrapes real events from multilingual Wikipedia, uses OpenAI to craft a divergent narrative with period-appropriate prose and DALL-E imagery, and publishes via a REST API. Features article browsing by month, a &amp;quot;Where/When&amp;quot; interactive map-and-timeline view using Leaflet, and a picture desk. (Built with Wagtail 7/Django 5 per operator.)&amp;lt;ref name=&amp;quot;echoes-home&amp;quot;&amp;gt;&amp;quot;Echoes — Dispatches from Histories That Never Were,&amp;quot; &#039;&#039;echoes.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://echoes.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://quidlibet.yusupov.cloud quidlibet.yusupov.cloud] — &#039;&#039;Quidlibet&#039;&#039;, an app that generates fictional books complete with synopsis, author bio, and faux reviews; includes genre and author archives. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;quidlibet-home&amp;quot;&amp;gt;“Quidlibet — Book Generator,” &#039;&#039;quidlibet.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://quidlibet.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://tyov-web.yusupov.cloud tyov-web.yusupov.cloud] — a web implementation of the solo RPG &#039;&#039;Thousand Year Old Vampire&#039;&#039;, with Django 5 backend and Vue 3 frontend. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
Operator-reported (not publicly discoverable at time of writing):&lt;br /&gt;
&lt;br /&gt;
* skills.yusupov.cloud — a skills matrix application. (Per operator.)&lt;br /&gt;
* resources.yusupov.cloud — a simple resource plannign calendar. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
== Technology ==&lt;br /&gt;
The wiki stack is documented on &#039;&#039;Special:Version&#039;&#039;. Individual apps are described by the operator as Flask (&#039;&#039;acbc&#039;&#039;, &#039;&#039;quidlibet&#039;&#039;) and Django 5 + Vue 3 (&#039;&#039;tyov-web&#039;&#039;).&amp;lt;ref name=&amp;quot;version&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Relation to the BBS ==&lt;br /&gt;
The project name references Vuijlsteke’s single-line BBS (FidoNet 2:291/1925) active between 1990 and 1995. While the VPS is not a BBS, its single-admin, self-maintained hosting reprises the early DIY approach.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;nodehist&amp;quot;&amp;gt;“Nodelist history search: History of node 2:291/1925,” NodeHist, accessed 10 October 2025, https://nodehist.fidonet.org.ua/?address=2%3A291%2F1925&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Yusupov&#039;s House]] (1990s BBS)&lt;br /&gt;
* [[Michel Vuijlsteke]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Personal websites]]&lt;br /&gt;
[[Category:Belgian websites]]&lt;br /&gt;
[[Category:2025 establishments in Belgium]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=378</id>
		<title>Yusupov.cloud</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=378"/>
		<updated>2026-04-12T10:19:58Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Subdomains and projects */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name           = yusupov.cloud&lt;br /&gt;
| 1 url            = https://yusupov.cloud&lt;br /&gt;
| 2 type           = Personal web sites&lt;br /&gt;
| 3 owner          = [[Michel Vuijlsteke]]&lt;br /&gt;
| 4 launched       = 2025&lt;br /&gt;
| 5 current_status = Online&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;yusupov.cloud&#039;&#039;&#039; is a personal domain and virtual private server operated by Belgian technologist [[Michel Vuijlsteke]]. It hosts multiple small web applications on subdomains and at the apex domain. One of these is a MediaWiki installation titled “Yusupov’s House.” The setup is presented as a web-era continuation of the do-it-yourself ethos of Vuijlsteke’s 1990s BBS of the same name.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot;&amp;gt;“Yusupov’s House,” &#039;&#039;yusupov.cloud&#039;&#039; (wiki), accessed 10 October 2025, https://yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
Among the projects on the domain is a MediaWiki (at the apex, &#039;&#039;yusupov.cloud&#039;&#039;) running MediaWiki 1.44.0 with PHP 8.3.6 (FPM) and SQLite, using the Vector skin and core extensions for citations and template scripting.&amp;lt;ref name=&amp;quot;version&amp;quot;&amp;gt;‘‘Special:Version’’ page, &#039;&#039;yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://yusupov.cloud/wiki/Special:Version&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Subdomains and projects ==&lt;br /&gt;
Publicly visible projects include:&lt;br /&gt;
&lt;br /&gt;
* [https://acbc.yusupov.cloud acbc.yusupov.cloud] — &#039;&#039;A Cabinet of Brief Curiosities&#039;&#039;, generating tiny three-sentence surreal/horror micro-stories with an hourly cadence and an archive. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;acbc-home&amp;quot;&amp;gt;A Cabinet of Brief Curiosities (home), &#039;&#039;acbc.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://acbc.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://agenda.yusupov.cloud agenda.yusupov.cloud] — &#039;&#039;A Life in Planners&#039;&#039;, a structured journal chronicling the final years of the operator’s mother, with calendar, food, medications, measurements, and statistics views (multilingual UI).&amp;lt;ref name=&amp;quot;agenda&amp;quot;&amp;gt;“A life in planners,” &#039;&#039;agenda.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://agenda.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://cloud.yusupov.cloud cloud.yusupov.cloud] — &#039;&#039;Cloud&#039;&#039;, a series of static html creative coding experiments, simulations, and games including: timebeat, fire and snake simulations, biomass metaballs, cs3, &#039;&#039;Cross&#039;&#039; crossword puzzle game, image dithering tool, books, &#039;&#039;Elite Galaxy Explorer&#039;&#039;, ZX Spectrum loading screen simulator, Carcassonne, 3D boids flocking algorithm, physarum slime mold simulation, temps temperature visualization, &#039;&#039;The Chronicle of Hamurabi&#039;&#039; ancient Sumeria resource management game, and gatekeeper.&amp;lt;ref name=&amp;quot;cloud-home&amp;quot;&amp;gt;&amp;quot;cloud,&amp;quot; &#039;&#039;cloud.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://cloud.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://digest.yusupov.cloud digest.yusupov.cloud] — &#039;&#039;Digest&#039;&#039;, daily seasonal AI-assisted recipes inspired by current events, browsable by meal type and ingredients.&amp;lt;ref name=&amp;quot;digest-home&amp;quot;&amp;gt;“Digest — Daily recipes inspired by the news,” &#039;&#039;digest.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://digest.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://echoes.yusupov.cloud echoes.yusupov.cloud] — &#039;&#039;Echoes of What Wasn&#039;t&#039;&#039;, an AI-generated alternate-history newspaper presenting richly detailed articles about historical events as if they had unfolded differently. A pipeline scrapes real events from multilingual Wikipedia, uses OpenAI to craft a divergent narrative with period-appropriate prose and DALL-E imagery, and publishes via a REST API. Features article browsing by month, a &amp;quot;Where/When&amp;quot; interactive map-and-timeline view using Leaflet, and a picture desk. (Built with Wagtail 7/Django 5 per operator.)&amp;lt;ref name=&amp;quot;echoes-home&amp;quot;&amp;gt;&amp;quot;Echoes — Dispatches from Histories That Never Were,&amp;quot; &#039;&#039;echoes.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://echoes.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://quidlibet.yusupov.cloud quidlibet.yusupov.cloud] — &#039;&#039;Quidlibet&#039;&#039;, an app that generates fictional books complete with synopsis, author bio, and faux reviews; includes genre and author archives. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;quidlibet-home&amp;quot;&amp;gt;“Quidlibet — Book Generator,” &#039;&#039;quidlibet.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://quidlibet.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://tyov-web.yusupov.cloud tyov-web.yusupov.cloud] — a web implementation of the solo RPG &#039;&#039;Thousand Year Old Vampire&#039;&#039;, with Django 5 backend and Vue 3 frontend. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
Operator-reported (not publicly discoverable at time of writing):&lt;br /&gt;
&lt;br /&gt;
* skills.yusupov.cloud — a skills matrix application. (Per operator.)&lt;br /&gt;
* resources.yusupov.cloud — a simple resource plannign calendar. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
== Technology ==&lt;br /&gt;
The wiki stack is documented on &#039;&#039;Special:Version&#039;&#039;. Individual apps are described by the operator as Flask (&#039;&#039;acbc&#039;&#039;, &#039;&#039;quidlibet&#039;&#039;) and Django 5 + Vue 3 (&#039;&#039;tyov-web&#039;&#039;).&amp;lt;ref name=&amp;quot;version&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Relation to the BBS ==&lt;br /&gt;
The project name references Vuijlsteke’s single-line BBS (FidoNet 2:291/1925) active between 1990 and 1995. While the VPS is not a BBS, its single-admin, self-maintained hosting reprises the early DIY approach.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;nodehist&amp;quot;&amp;gt;“Nodelist history search: History of node 2:291/1925,” NodeHist, accessed 10 October 2025, https://nodehist.fidonet.org.ua/?address=2%3A291%2F1925&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Yusupov&#039;s House]] (1990s BBS)&lt;br /&gt;
* [[Michel Vuijlsteke]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Personal websites]]&lt;br /&gt;
[[Category:Belgian websites]]&lt;br /&gt;
[[Category:2025 establishments in Belgium]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=377</id>
		<title>Yusupov.cloud</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=377"/>
		<updated>2026-04-12T10:17:29Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Subdomains and projects */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name           = yusupov.cloud&lt;br /&gt;
| 1 url            = https://yusupov.cloud&lt;br /&gt;
| 2 type           = Personal web sites&lt;br /&gt;
| 3 owner          = [[Michel Vuijlsteke]]&lt;br /&gt;
| 4 launched       = 2025&lt;br /&gt;
| 5 current_status = Online&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;yusupov.cloud&#039;&#039;&#039; is a personal domain and virtual private server operated by Belgian technologist [[Michel Vuijlsteke]]. It hosts multiple small web applications on subdomains and at the apex domain. One of these is a MediaWiki installation titled “Yusupov’s House.” The setup is presented as a web-era continuation of the do-it-yourself ethos of Vuijlsteke’s 1990s BBS of the same name.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot;&amp;gt;“Yusupov’s House,” &#039;&#039;yusupov.cloud&#039;&#039; (wiki), accessed 10 October 2025, https://yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
Among the projects on the domain is a MediaWiki (at the apex, &#039;&#039;yusupov.cloud&#039;&#039;) running MediaWiki 1.44.0 with PHP 8.3.6 (FPM) and SQLite, using the Vector skin and core extensions for citations and template scripting.&amp;lt;ref name=&amp;quot;version&amp;quot;&amp;gt;‘‘Special:Version’’ page, &#039;&#039;yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://yusupov.cloud/wiki/Special:Version&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Subdomains and projects ==&lt;br /&gt;
Publicly visible projects include:&lt;br /&gt;
&lt;br /&gt;
* [https://acbc.yusupov.cloud acbc.yusupov.cloud] — &#039;&#039;A Cabinet of Brief Curiosities&#039;&#039;, generating tiny three-sentence surreal/horror micro-stories with an hourly cadence and an archive. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;acbc-home&amp;quot;&amp;gt;A Cabinet of Brief Curiosities (home), &#039;&#039;acbc.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://acbc.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://agenda.yusupov.cloud agenda.yusupov.cloud] — &#039;&#039;A Life in Planners&#039;&#039;, a structured journal chronicling the final years of the operator’s mother, with calendar, food, medications, measurements, and statistics views (multilingual UI).&amp;lt;ref name=&amp;quot;agenda&amp;quot;&amp;gt;“A life in planners,” &#039;&#039;agenda.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://agenda.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://cloud.yusupov.cloud cloud.yusupov.cloud] — &#039;&#039;Cloud&#039;&#039;, a Progressive Web App hosting a collection of creative coding experiments, simulations, and games, including: a 3D implementation of the classic boids flocking algorithm; &#039;&#039;Cross&#039;&#039; (&amp;quot;A lazy person&#039;s crosswords&amp;quot;), a crossword puzzle game; an image dithering tool with multiple algorithms and vintage computer palettes; &#039;&#039;Elite Galaxy Explorer&#039;&#039;, a procedural galaxy generator inspired by the classic space game; a physarum slime mold simulation; &#039;&#039;The Chronicle of Hamurabi&#039;&#039;, a resource management game set in ancient Sumeria; &#039;&#039;Pipe 2&#039;&#039;, an endless self-playing isometric pipe runner; a &#039;&#039;ColorBASIC Home Computer&#039;&#039; TRS-80 emulator with BASIC interpreter; a ZX Spectrum loading screen simulator; fire and snake simulations; metaballs rendering; temperature visualizations; and a Carcassonne board game implementation.&amp;lt;ref name=&amp;quot;cloud-home&amp;quot;&amp;gt;&amp;quot;cloud,&amp;quot; &#039;&#039;cloud.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://cloud.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://digest.yusupov.cloud digest.yusupov.cloud] — &#039;&#039;Digest&#039;&#039;, daily seasonal AI-assisted recipes inspired by current events, browsable by meal type and ingredients.&amp;lt;ref name=&amp;quot;digest-home&amp;quot;&amp;gt;“Digest — Daily recipes inspired by the news,” &#039;&#039;digest.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://digest.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://echoes.yusupov.cloud echoes.yusupov.cloud] — &#039;&#039;Echoes of What Wasn&#039;t&#039;&#039;, an AI-generated alternate-history newspaper presenting richly detailed articles about historical events as if they had unfolded differently. A pipeline scrapes real events from multilingual Wikipedia, uses OpenAI to craft a divergent narrative with period-appropriate prose and DALL-E imagery, and publishes via a REST API. Features article browsing by month, a &amp;quot;Where/When&amp;quot; interactive map-and-timeline view using Leaflet, and a picture desk. (Built with Wagtail 7/Django 5 per operator.)&amp;lt;ref name=&amp;quot;echoes-home&amp;quot;&amp;gt;&amp;quot;Echoes — Dispatches from Histories That Never Were,&amp;quot; &#039;&#039;echoes.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://echoes.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://quidlibet.yusupov.cloud quidlibet.yusupov.cloud] — &#039;&#039;Quidlibet&#039;&#039;, an app that generates fictional books complete with synopsis, author bio, and faux reviews; includes genre and author archives. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;quidlibet-home&amp;quot;&amp;gt;“Quidlibet — Book Generator,” &#039;&#039;quidlibet.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://quidlibet.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://tyov-web.yusupov.cloud tyov-web.yusupov.cloud] — a web implementation of the solo RPG &#039;&#039;Thousand Year Old Vampire&#039;&#039;, with Django 5 backend and Vue 3 frontend. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
Operator-reported (not publicly discoverable at time of writing):&lt;br /&gt;
&lt;br /&gt;
* skills.yusupov.cloud — a skills matrix application. (Per operator.)&lt;br /&gt;
* resources.yusupov.cloud — a simple resource plannign calendar. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
== Technology ==&lt;br /&gt;
The wiki stack is documented on &#039;&#039;Special:Version&#039;&#039;. Individual apps are described by the operator as Flask (&#039;&#039;acbc&#039;&#039;, &#039;&#039;quidlibet&#039;&#039;) and Django 5 + Vue 3 (&#039;&#039;tyov-web&#039;&#039;).&amp;lt;ref name=&amp;quot;version&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Relation to the BBS ==&lt;br /&gt;
The project name references Vuijlsteke’s single-line BBS (FidoNet 2:291/1925) active between 1990 and 1995. While the VPS is not a BBS, its single-admin, self-maintained hosting reprises the early DIY approach.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;nodehist&amp;quot;&amp;gt;“Nodelist history search: History of node 2:291/1925,” NodeHist, accessed 10 October 2025, https://nodehist.fidonet.org.ua/?address=2%3A291%2F1925&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Yusupov&#039;s House]] (1990s BBS)&lt;br /&gt;
* [[Michel Vuijlsteke]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Personal websites]]&lt;br /&gt;
[[Category:Belgian websites]]&lt;br /&gt;
[[Category:2025 establishments in Belgium]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=376</id>
		<title>Yusupov.cloud</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Yusupov.cloud&amp;diff=376"/>
		<updated>2026-04-12T10:16:51Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Subdomains and projects */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name           = yusupov.cloud&lt;br /&gt;
| 1 url            = https://yusupov.cloud&lt;br /&gt;
| 2 type           = Personal web sites&lt;br /&gt;
| 3 owner          = [[Michel Vuijlsteke]]&lt;br /&gt;
| 4 launched       = 2025&lt;br /&gt;
| 5 current_status = Online&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;yusupov.cloud&#039;&#039;&#039; is a personal domain and virtual private server operated by Belgian technologist [[Michel Vuijlsteke]]. It hosts multiple small web applications on subdomains and at the apex domain. One of these is a MediaWiki installation titled “Yusupov’s House.” The setup is presented as a web-era continuation of the do-it-yourself ethos of Vuijlsteke’s 1990s BBS of the same name.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot;&amp;gt;“Yusupov’s House,” &#039;&#039;yusupov.cloud&#039;&#039; (wiki), accessed 10 October 2025, https://yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
Among the projects on the domain is a MediaWiki (at the apex, &#039;&#039;yusupov.cloud&#039;&#039;) running MediaWiki 1.44.0 with PHP 8.3.6 (FPM) and SQLite, using the Vector skin and core extensions for citations and template scripting.&amp;lt;ref name=&amp;quot;version&amp;quot;&amp;gt;‘‘Special:Version’’ page, &#039;&#039;yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://yusupov.cloud/wiki/Special:Version&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Subdomains and projects ==&lt;br /&gt;
Publicly visible projects include:&lt;br /&gt;
&lt;br /&gt;
* [https://acbc.yusupov.cloud acbc.yusupov.cloud] — &#039;&#039;A Cabinet of Brief Curiosities&#039;&#039;, generating tiny three-sentence surreal/horror micro-stories with an hourly cadence and an archive. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;acbc-home&amp;quot;&amp;gt;A Cabinet of Brief Curiosities (home), &#039;&#039;acbc.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://acbc.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://agenda.yusupov.cloud agenda.yusupov.cloud] — &#039;&#039;A Life in Planners&#039;&#039;, a structured journal chronicling the final years of the operator’s mother, with calendar, food, medications, measurements, and statistics views (multilingual UI).&amp;lt;ref name=&amp;quot;agenda&amp;quot;&amp;gt;“A life in planners,” &#039;&#039;agenda.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://agenda.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://digest.yusupov.cloud digest.yusupov.cloud] — &#039;&#039;Digest&#039;&#039;, daily seasonal AI-assisted recipes inspired by current events, browsable by meal type and ingredients.&amp;lt;ref name=&amp;quot;digest-home&amp;quot;&amp;gt;“Digest — Daily recipes inspired by the news,” &#039;&#039;digest.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://digest.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://echoes.yusupov.cloud echoes.yusupov.cloud] — &#039;&#039;Echoes of What Wasn&#039;t&#039;&#039;, an AI-generated alternate-history newspaper presenting richly detailed articles about historical events as if they had unfolded differently. A pipeline scrapes real events from multilingual Wikipedia, uses OpenAI to craft a divergent narrative with period-appropriate prose and DALL-E imagery, and publishes via a REST API. Features article browsing by month, a &amp;quot;Where/When&amp;quot; interactive map-and-timeline view using Leaflet, and a picture desk. (Built with Wagtail 7/Django 5 per operator.)&amp;lt;ref name=&amp;quot;echoes-home&amp;quot;&amp;gt;&amp;quot;Echoes — Dispatches from Histories That Never Were,&amp;quot; &#039;&#039;echoes.yusupov.cloud&#039;&#039;, accessed 12 April 2026, https://echoes.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://quidlibet.yusupov.cloud quidlibet.yusupov.cloud] — &#039;&#039;Quidlibet&#039;&#039;, an app that generates fictional books complete with synopsis, author bio, and faux reviews; includes genre and author archives. (Built with Flask per operator.)&amp;lt;ref name=&amp;quot;quidlibet-home&amp;quot;&amp;gt;“Quidlibet — Book Generator,” &#039;&#039;quidlibet.yusupov.cloud&#039;&#039;, accessed 10 October 2025, https://quidlibet.yusupov.cloud/&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://tyov-web.yusupov.cloud tyov-web.yusupov.cloud] — a web implementation of the solo RPG &#039;&#039;Thousand Year Old Vampire&#039;&#039;, with Django 5 backend and Vue 3 frontend. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
Operator-reported (not publicly discoverable at time of writing):&lt;br /&gt;
&lt;br /&gt;
* skills.yusupov.cloud — a skills matrix application. (Per operator.)&lt;br /&gt;
* resources.yusupov.cloud — a simple resource plannign calendar. (Per operator.)&lt;br /&gt;
&lt;br /&gt;
== Technology ==&lt;br /&gt;
The wiki stack is documented on &#039;&#039;Special:Version&#039;&#039;. Individual apps are described by the operator as Flask (&#039;&#039;acbc&#039;&#039;, &#039;&#039;quidlibet&#039;&#039;) and Django 5 + Vue 3 (&#039;&#039;tyov-web&#039;&#039;).&amp;lt;ref name=&amp;quot;version&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Relation to the BBS ==&lt;br /&gt;
The project name references Vuijlsteke’s single-line BBS (FidoNet 2:291/1925) active between 1990 and 1995. While the VPS is not a BBS, its single-admin, self-maintained hosting reprises the early DIY approach.&amp;lt;ref name=&amp;quot;wiki-home&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;nodehist&amp;quot;&amp;gt;“Nodelist history search: History of node 2:291/1925,” NodeHist, accessed 10 October 2025, https://nodehist.fidonet.org.ua/?address=2%3A291%2F1925&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Yusupov&#039;s House]] (1990s BBS)&lt;br /&gt;
* [[Michel Vuijlsteke]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Personal websites]]&lt;br /&gt;
[[Category:Belgian websites]]&lt;br /&gt;
[[Category:2025 establishments in Belgium]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Albania_for_King_Zog_Committee&amp;diff=375</id>
		<title>Albania for King Zog Committee</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Albania_for_King_Zog_Committee&amp;diff=375"/>
		<updated>2025-11-16T19:02:54Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* The 1987–2001 website */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name = Albania for King Zog Committee&lt;br /&gt;
| image = Guildhall.jpg&lt;br /&gt;
| caption = Now demolished façade in Ghent, c. 1932. The partially effaced heraldic shield has been associated with the Committee’s activities in the Low Countries.&lt;br /&gt;
| 01 Founded = attested 1325 (first trace)&lt;br /&gt;
| 02 Refounded = 1987 (present incarnation)&lt;br /&gt;
| 03 Headquarters = Ghent, Belgium&lt;br /&gt;
| 04 Status = Archives partially lost; ongoing reconstruction&lt;br /&gt;
| 05 Website = zog.org (1987–2001)&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;&#039;Albania for King Zog Committee (AKZ)&#039;&#039;&#039; is an international learned society and semi-secret association concerned with monarchy, cultural survivals, and esoteric historiography. The Committee has no historical connection to Albania, the Balkans, or [[Zog I|King Zog of Albania]] (1895–1961). Its name — variously recorded in medieval sources as &#039;&#039;Zogh&#039;&#039;, &#039;&#039;Tsog&#039;&#039;, or &#039;&#039;Zogu&#039;&#039; — predates the 20th-century monarch by several centuries and reflects older traditions of uncertain origin.&lt;br /&gt;
&lt;br /&gt;
The coincidence that a later monarch bore the same name has long divided historians: some regard it as a mere accident of etymology, others as an instance of uncanny foresight, and a minority as evidence of deliberate myth-making across generations. What is undisputed is that the phrase “for King Zog” has been central to the Committee’s identity for at least seven centuries.&lt;br /&gt;
&lt;br /&gt;
== Origins ==&lt;br /&gt;
The earliest surviving mention of the Committee dates to 1325, in a notarial record from [[Ragusa (Dubrovnik)|Ragusa]] (modern [[Dubrovnik]]), referring to a &#039;&#039;confraternitas pro Rege Zogu&#039;&#039;. Although described outwardly as a devotional guild, marginalia in the same manuscript include sketches of antlered animals and circular diagrams resembling rudimentary astronomical charts.&amp;lt;ref name=&amp;quot;Cittadini1911&amp;quot;&amp;gt;R. Cittadini, &#039;&#039;Atti notarili e confraternite dalmata&#039;&#039; (Venice: Archivio Serafini, 1911), p. 203.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Already in this period, the Committee seems to have pursued esoteric investigations. Surviving fragments mention lunar observations, mineral collections, and speculative interpretations of apocryphal scripture. Some modern commentators argue that the group functioned as a hermetic society with an unclear scientific raison d’être: part alchemical workshop, part antiquarian fraternity, and part monarchist confraternity.&amp;lt;ref name=&amp;quot;Paredes1962&amp;quot;&amp;gt;L. Paredes, &#039;&#039;Hermetica Balcanica: Societies of the Eastern Adriatic&#039;&#039; (Naples: Officina Aurea, 1962), pp. 87–92.&amp;lt;/ref&amp;gt;&lt;br /&gt;
[[File:Lion fountain.jpg|alt=Lion fountain|thumb|&#039;&#039;Lion fountain in the Alhambra, Granada, photographed by Clara Jensen, c. 1968. Later AKZ commentators pointed to the faint geometrical carvings beneath the waterline as evidence of the Committee’s presence in Spain.&#039;&#039;]]&lt;br /&gt;
Archaeological finds have complicated this picture. Ceramic shards discovered near [[Berat]] in 1899 bear spiralling [[Aramaic]] inscriptions reminiscent of [[Incantation bowl|incantation bowls]] associated with the [[Lilith]] tradition of late antiquity, and have been cited as possible evidence that the Committee’s name and symbolism grew out of [[Apotropaic magic|apotropaic magical]] traditions rather than straightforward political allegiance.&amp;lt;ref name=&amp;quot;Dervishi1973&amp;quot;&amp;gt;M. Dervishi, &#039;&#039;The Berat Bowls: Aramaic Incantations in the Balkans&#039;&#039; (&#039;&#039;Journal of Uncanny Archaeology&#039;&#039;, vol. 4, no. 2, 1973), pp. 45–67.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By the 15th century, traces of AKZ appear in [[Venice|Venetian]] and [[Florence|Florentine]] archives, where members are recorded commissioning astrological tables and sponsoring translations of Byzantine medical texts. Sigils preserved in these documents combine the moose emblem with planetary seals, anticipating later overlaps with Renaissance [[Hermeticism|hermetic magic]].&amp;lt;ref name=&amp;quot;Bellori1927&amp;quot;&amp;gt;G. Bellori, &#039;&#039;Codices Obscuri: Marginalia of the Venetian Manuscripts&#039;&#039; (Trieste: Edizioni Cryptica, 1927), pp. 114–118.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
During the [[Ottoman Empire|Ottoman centuries]], shadowy references to “Zog circles” survive in reports from [[Krujë]] and [[Thessaloniki]], though whether these were continuations of the medieval confraternity or periodic revivals remains unclear. The continuity of practice is further obscured by repeated archival loss through war, fire, and suppression, leaving only fragmentary clues to the Committee’s early identity.&amp;lt;ref name=&amp;quot;Kalemi1985&amp;quot;&amp;gt;Y. Kalemi, &#039;&#039;Societies in Shadow: Hidden Networks of the Ottoman Balkans&#039;&#039; (Istanbul: Akademi Mimar Sinan, 1985), pp. 51–64.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Symbols and sigils ==&lt;br /&gt;
From its earliest attestations, the AKZ employed a shifting set of symbols whose meanings remain debated. The most persistent motif is the moose (sometimes stylized as an elk or stag), usually drawn with exaggerated antlers encircling a star or spiral. Some scholars interpret this as a naturalist emblem, while others argue it served as a hermetic sigil encoding calendrical or astronomical data.&amp;lt;ref name=&amp;quot;Markovic1979&amp;quot;&amp;gt;D. Marković, &#039;&#039;Bestiae Occultae: Animal Motifs in Balkan Secret Societies&#039;&#039; (Belgrade: Zadužbina Petrović, 1979), pp. 143–155.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By the 14th century, AKZ records also include diagrams resembling the planetary seals of Renaissance [[Magic (supernatural)|magic]], suggesting contact with broader currents of [[Hermeticism|hermetic philosophy]]. Marginalia in a 1482 codex from Venice refer to “the Zogu star-lodge” (&#039;&#039;stella domus Zogu&#039;&#039;), hinting at links with early alchemical fraternities.&amp;lt;ref name=&amp;quot;Bellori1927&amp;quot; /&amp;gt;&lt;br /&gt;
[[File:Moose bowl.jpg|thumb|Moose Bowl, reconstruction]]&lt;br /&gt;
One of the more enigmatic survivals is a set of ceramic fragments unearthed near Berat in 1899, inscribed in spiralling Aramaic. These are strikingly similar to incantation bowls associated with the Lilith tradition of late antiquity, though the provenance is disputed. A controversial reading proposes that the inscription invokes “the king who comes as Zog” (&#039;&#039;mlk’ d’t’ zg&#039;&#039;), which would push the Committee’s nomenclature into the realm of apotropaic magic.&amp;lt;ref name=&amp;quot;Dervishi1973&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Later symbolism intertwined these traditions with early modern esotericism. Seventeenth-century engravings attributed to the Committee combine the moose motif with Hermetic caducei and geometrical diagrams reminiscent of John Dee’s &#039;&#039;[[Monas Hieroglyphica]]&#039;&#039;.&amp;lt;ref name=&amp;quot;Reichenbach1954&amp;quot;&amp;gt;F. Reichenbach, &#039;&#039;Symbola Zoguica: The Hidden Geometry of a Balkan Fraternity&#039;&#039; (Basel: Ars Hermetica, 1954).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Early Modern period ==&lt;br /&gt;
During the 16th and 17th centuries, the AKZ entered into closer contact with currents of [[Hermeticism]], [[natural philosophy]], and early modern occult scholarship. Several marginal figures in the European learned world appear to have been affiliated with — or at least influenced by — the Committee’s symbolic repertoire.&lt;br /&gt;
&lt;br /&gt;
A Venetian codex from 1564 contains references to “the Zogu diagrams” in a context parallel to John Dee’s &#039;&#039;[[Monas Hieroglyphica]]&#039;&#039;. The diagrams themselves, combining the moose antler motif with planetary seals, were later copied into Dee’s notebooks by unidentified hands.&amp;lt;ref name=&amp;quot;Bellori1927&amp;quot; /&amp;gt;&lt;br /&gt;
[[File:Zogu.jpg|none|thumb|705x705px|Opening of the &#039;&#039;&#039;Codex Aurifaber (Venice, 1564)&#039;&#039;&#039;. The left folio contains a stag’s head with elaborate antlers, sketched over marginal notes in a humanist hand; the right folio preserves four circular “Zogu diagrams” combining planetary seals with antler-like extensions. The mixture of scripts and inks suggests interpolation by more than one contributor.]]&lt;br /&gt;
In the 1620s, Athanasius Kircher corresponded with unnamed “brothers of Zogu,” acknowledging receipt of strange diagrams allegedly derived from Berat inscriptions. Although Kircher dismissed their magical efficacy, he preserved several in his &#039;&#039;Ars Magna Lucis et Umbrae&#039;&#039;, where they appear alongside optical experiments.&amp;lt;ref name=&amp;quot;Kircher1646&amp;quot;&amp;gt;A. Kircher, &#039;&#039;Epistolae ad Fratres Zoguicos&#039;&#039; (MS Vaticanus Lat. 11932, fols. 44–49, c. 1628), cited in M. Engel, &#039;&#039;Kircher and the Shadows of the Balkans&#039;&#039; (Vienna: Collegium Hermeticum, 1981), pp. 212–219.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By the mid-17th century, Rosicrucian tracts circulated in Central Europe make oblique mention of “the House of Zog,” described as a locus of hidden knowledge “between the stag and the star.” Modern historians suggest that AKZ symbols were selectively adopted into the Rosicrucian mythos to enhance its aura of antiquity.&amp;lt;ref name=&amp;quot;Reichenbach1954&amp;quot; /&amp;gt; &amp;lt;ref name=&amp;quot;Guderian1978&amp;quot;&amp;gt;H. Guderian, &#039;&#039;Fraternitas Zoguica: Balkan Currents in the Rosicrucian Manifestos&#039;&#039; (Basel: Ars Occulta, 1978), pp. 51–73.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The Committee’s activities in this period remain only partially documented, but engravings dated 1651 attributed to AKZ members show hybrid sigils combining the caduceus, a moose head, and concentric planetary rings. These images anticipate later developments in both esoteric art and scientific illustration, blurring the line between symbolic ornament and purportedly empirical diagram.&amp;lt;ref name=&amp;quot;Reichenbach1954&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While mainstream scholars of the Enlightenment dismissed AKZ materials as curiosities, their circulation within hermetic and para-scientific networks ensured the Committee’s survival into the modern age.&lt;br /&gt;
&lt;br /&gt;
== Interpretations ==&lt;br /&gt;
Scholars remain divided on whether the Committee’s symbols represent: a mnemonic system for astronomical cycles, a parodic or satirical commentary on learned societies, or genuine survivals of apotropaic and hermetic practices.&lt;br /&gt;
&lt;br /&gt;
The supposed influence of the AKZ on early modern figures such as [[John Dee]], [[Athanasius Kircher]], and the authors of the [[Rosicrucian Manifestos]] has been particularly contentious. Some historians argue that Dee’s notes on “Zogu diagrams,” Kircher’s correspondence with unnamed Balkan associates, and Rosicrucian imagery of the “stag and star” provide independent confirmation of the Committee’s role in shaping European esoteric currents.&amp;lt;ref name=&amp;quot;Kircher1646&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;Guderian1978&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Others dismiss these claims as the result of deliberate myth-making during the 1987 refoundation, when members selectively emphasized obscure parallels in order to bolster the Committee’s antiquity and prestige.&amp;lt;ref name=&amp;quot;Meer2004&amp;quot; /&amp;gt; In this reading, Dee’s marginalia and Kircher’s letters are interpreted out of context, while Rosicrucian references are too vague to prove a substantive link.&lt;br /&gt;
&lt;br /&gt;
Between these poles lies a para-historical position: that AKZ materials circulated through underground channels of manuscript exchange, leaving subtle traces in early modern Europe, but without the systematic influence sometimes claimed. This view emphasizes the Committee’s ability to reappear in different cultural guises, adapting its symbols to prevailing intellectual fashions.&lt;br /&gt;
&lt;br /&gt;
Ultimately, the balance between authentic hermetic continuity and playful retroactive fabrication remains unresolved, a tension that continues to define the Committee’s historiography.&lt;br /&gt;
&lt;br /&gt;
== 19th and 20th centuries ==&lt;br /&gt;
References to the AKZ surface sporadically in 19th-century pamphlets concerned with Balkan independence, often accompanied by cryptic sigils or moose motifs later taken up in the Committee’s iconography. The group’s activities during the two [[World Wars]] remain obscure, though fragmentary correspondence places members in neutral [[Switzerland]] and occupied [[Belgium]].&amp;lt;ref name=&amp;quot;Schroder1959&amp;quot;&amp;gt;H. Schröder, &#039;&#039;Pamphlets and Phantoms: Monarchist Undergrounds in Europe&#039;&#039; (Leipzig: Collegium Historiae, 1959), pp. 88–96.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The official “refounding” of the Committee in 1987 took place in [[Ghent]], Belgium, under the aegis of a circle of academics, artists, and eccentric monarchists. This incarnation emphasized archival recovery, speculative historiography, and playful public outreach.&amp;lt;ref name=&amp;quot;VanLooy1992&amp;quot;&amp;gt;C. Van Looy, &#039;&#039;Gent en de Heroprichting van het Comité&#039;&#039; (Antwerp: Zephyrus Press, 1992), pp. 34–41.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== The 1987–2001 website ==&lt;br /&gt;
A distinctive feature of the refounded AKZ was its pioneering [[History of the World Wide Web|web presence]], launched in the early 1990s. The site, hosted initially on university servers and later at &#039;&#039;zog.org&#039;&#039;, presented a mixture of archival reconstruction, surrealist speculation, and committee news.&lt;br /&gt;
&lt;br /&gt;
Highlights included: reports on seminars in [[Keflavík]], [[Borg, Germany|Borg]], Ghent, and [[Granada]]; the “Moose Department,” a sub-section exploring symbolic connections among moose, walruses, and European monarchist iconography; playful pseudo-scholarly investigations linking figures such as [[Lewis Carroll]] and [[The Beatles]] to the Committee’s mythos.&amp;lt;ref name=&amp;quot;Hellebuyck2008&amp;quot;&amp;gt;K. Hellebuyck, &#039;&#039;Cybernetic Monarchies: Early Web Parodies and Parafictions&#039;&#039; (&#039;&#039;Transactions on Digital Culture&#039;&#039;, vol. 11, 2008), pp. 63–79.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Though the site went offline in the early 2000s, fragments survive in personal backups and the [[Wayback Machine|Internet Archive]]. Scholars of digital culture have since treated it as an early example of “web-based [[Parafiction|parafiction]].”&amp;lt;ref name=&amp;quot;Meer2004&amp;quot;&amp;gt;A. van der Meer, &#039;&#039;Web Parafictions of the Late 20th Century&#039;&#039; (Ghent: Hypertext Studies, 2004), pp. 211–223.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== The Moose Department ==&lt;br /&gt;
&#039;&#039;Main article: [[Moose Dept.]]&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Within the Committee’s 1987–2001 web presence, one of the most prominent sub-sections was the so-called Moose Department. Although framed as a playful research unit, it drew heavily on the Committee’s long-standing fascination with cervid imagery and its hermetic significance.&lt;br /&gt;
&lt;br /&gt;
The Moose Dept. combined whimsical para-historical essays with pseudo-academic apparatus, drawing connections between moose, walruses, and symbols found in both medieval AKZ sigils and 20th-century popular culture. Some commentators see the Department as a continuation of earlier animal symbolism in the Committee’s history; others interpret it as a postmodern parody of academic specialization.&amp;lt;ref name=&amp;quot;Hellebuyck2008&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
[[Zog I|King Zog I of Albania]]&lt;br /&gt;
&lt;br /&gt;
[[Parafiction]]&lt;br /&gt;
&lt;br /&gt;
[[Moose Dept.|The Moose Dept. (AKZ)]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Learned societies]]&lt;br /&gt;
[[Category:Organizations established in the 14th century]]&lt;br /&gt;
[[Category:Hermeticism]]&lt;br /&gt;
[[Category:Albania for King Zog Committee]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Albania_for_King_Zog_Committee&amp;diff=374</id>
		<title>Albania for King Zog Committee</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Albania_for_King_Zog_Committee&amp;diff=374"/>
		<updated>2025-11-16T19:02:28Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* The 1987–2001 website */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name = Albania for King Zog Committee&lt;br /&gt;
| image = Guildhall.jpg&lt;br /&gt;
| caption = Now demolished façade in Ghent, c. 1932. The partially effaced heraldic shield has been associated with the Committee’s activities in the Low Countries.&lt;br /&gt;
| 01 Founded = attested 1325 (first trace)&lt;br /&gt;
| 02 Refounded = 1987 (present incarnation)&lt;br /&gt;
| 03 Headquarters = Ghent, Belgium&lt;br /&gt;
| 04 Status = Archives partially lost; ongoing reconstruction&lt;br /&gt;
| 05 Website = zog.org (1987–2001)&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;&#039;Albania for King Zog Committee (AKZ)&#039;&#039;&#039; is an international learned society and semi-secret association concerned with monarchy, cultural survivals, and esoteric historiography. The Committee has no historical connection to Albania, the Balkans, or [[Zog I|King Zog of Albania]] (1895–1961). Its name — variously recorded in medieval sources as &#039;&#039;Zogh&#039;&#039;, &#039;&#039;Tsog&#039;&#039;, or &#039;&#039;Zogu&#039;&#039; — predates the 20th-century monarch by several centuries and reflects older traditions of uncertain origin.&lt;br /&gt;
&lt;br /&gt;
The coincidence that a later monarch bore the same name has long divided historians: some regard it as a mere accident of etymology, others as an instance of uncanny foresight, and a minority as evidence of deliberate myth-making across generations. What is undisputed is that the phrase “for King Zog” has been central to the Committee’s identity for at least seven centuries.&lt;br /&gt;
&lt;br /&gt;
== Origins ==&lt;br /&gt;
The earliest surviving mention of the Committee dates to 1325, in a notarial record from [[Ragusa (Dubrovnik)|Ragusa]] (modern [[Dubrovnik]]), referring to a &#039;&#039;confraternitas pro Rege Zogu&#039;&#039;. Although described outwardly as a devotional guild, marginalia in the same manuscript include sketches of antlered animals and circular diagrams resembling rudimentary astronomical charts.&amp;lt;ref name=&amp;quot;Cittadini1911&amp;quot;&amp;gt;R. Cittadini, &#039;&#039;Atti notarili e confraternite dalmata&#039;&#039; (Venice: Archivio Serafini, 1911), p. 203.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Already in this period, the Committee seems to have pursued esoteric investigations. Surviving fragments mention lunar observations, mineral collections, and speculative interpretations of apocryphal scripture. Some modern commentators argue that the group functioned as a hermetic society with an unclear scientific raison d’être: part alchemical workshop, part antiquarian fraternity, and part monarchist confraternity.&amp;lt;ref name=&amp;quot;Paredes1962&amp;quot;&amp;gt;L. Paredes, &#039;&#039;Hermetica Balcanica: Societies of the Eastern Adriatic&#039;&#039; (Naples: Officina Aurea, 1962), pp. 87–92.&amp;lt;/ref&amp;gt;&lt;br /&gt;
[[File:Lion fountain.jpg|alt=Lion fountain|thumb|&#039;&#039;Lion fountain in the Alhambra, Granada, photographed by Clara Jensen, c. 1968. Later AKZ commentators pointed to the faint geometrical carvings beneath the waterline as evidence of the Committee’s presence in Spain.&#039;&#039;]]&lt;br /&gt;
Archaeological finds have complicated this picture. Ceramic shards discovered near [[Berat]] in 1899 bear spiralling [[Aramaic]] inscriptions reminiscent of [[Incantation bowl|incantation bowls]] associated with the [[Lilith]] tradition of late antiquity, and have been cited as possible evidence that the Committee’s name and symbolism grew out of [[Apotropaic magic|apotropaic magical]] traditions rather than straightforward political allegiance.&amp;lt;ref name=&amp;quot;Dervishi1973&amp;quot;&amp;gt;M. Dervishi, &#039;&#039;The Berat Bowls: Aramaic Incantations in the Balkans&#039;&#039; (&#039;&#039;Journal of Uncanny Archaeology&#039;&#039;, vol. 4, no. 2, 1973), pp. 45–67.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By the 15th century, traces of AKZ appear in [[Venice|Venetian]] and [[Florence|Florentine]] archives, where members are recorded commissioning astrological tables and sponsoring translations of Byzantine medical texts. Sigils preserved in these documents combine the moose emblem with planetary seals, anticipating later overlaps with Renaissance [[Hermeticism|hermetic magic]].&amp;lt;ref name=&amp;quot;Bellori1927&amp;quot;&amp;gt;G. Bellori, &#039;&#039;Codices Obscuri: Marginalia of the Venetian Manuscripts&#039;&#039; (Trieste: Edizioni Cryptica, 1927), pp. 114–118.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
During the [[Ottoman Empire|Ottoman centuries]], shadowy references to “Zog circles” survive in reports from [[Krujë]] and [[Thessaloniki]], though whether these were continuations of the medieval confraternity or periodic revivals remains unclear. The continuity of practice is further obscured by repeated archival loss through war, fire, and suppression, leaving only fragmentary clues to the Committee’s early identity.&amp;lt;ref name=&amp;quot;Kalemi1985&amp;quot;&amp;gt;Y. Kalemi, &#039;&#039;Societies in Shadow: Hidden Networks of the Ottoman Balkans&#039;&#039; (Istanbul: Akademi Mimar Sinan, 1985), pp. 51–64.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Symbols and sigils ==&lt;br /&gt;
From its earliest attestations, the AKZ employed a shifting set of symbols whose meanings remain debated. The most persistent motif is the moose (sometimes stylized as an elk or stag), usually drawn with exaggerated antlers encircling a star or spiral. Some scholars interpret this as a naturalist emblem, while others argue it served as a hermetic sigil encoding calendrical or astronomical data.&amp;lt;ref name=&amp;quot;Markovic1979&amp;quot;&amp;gt;D. Marković, &#039;&#039;Bestiae Occultae: Animal Motifs in Balkan Secret Societies&#039;&#039; (Belgrade: Zadužbina Petrović, 1979), pp. 143–155.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By the 14th century, AKZ records also include diagrams resembling the planetary seals of Renaissance [[Magic (supernatural)|magic]], suggesting contact with broader currents of [[Hermeticism|hermetic philosophy]]. Marginalia in a 1482 codex from Venice refer to “the Zogu star-lodge” (&#039;&#039;stella domus Zogu&#039;&#039;), hinting at links with early alchemical fraternities.&amp;lt;ref name=&amp;quot;Bellori1927&amp;quot; /&amp;gt;&lt;br /&gt;
[[File:Moose bowl.jpg|thumb|Moose Bowl, reconstruction]]&lt;br /&gt;
One of the more enigmatic survivals is a set of ceramic fragments unearthed near Berat in 1899, inscribed in spiralling Aramaic. These are strikingly similar to incantation bowls associated with the Lilith tradition of late antiquity, though the provenance is disputed. A controversial reading proposes that the inscription invokes “the king who comes as Zog” (&#039;&#039;mlk’ d’t’ zg&#039;&#039;), which would push the Committee’s nomenclature into the realm of apotropaic magic.&amp;lt;ref name=&amp;quot;Dervishi1973&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Later symbolism intertwined these traditions with early modern esotericism. Seventeenth-century engravings attributed to the Committee combine the moose motif with Hermetic caducei and geometrical diagrams reminiscent of John Dee’s &#039;&#039;[[Monas Hieroglyphica]]&#039;&#039;.&amp;lt;ref name=&amp;quot;Reichenbach1954&amp;quot;&amp;gt;F. Reichenbach, &#039;&#039;Symbola Zoguica: The Hidden Geometry of a Balkan Fraternity&#039;&#039; (Basel: Ars Hermetica, 1954).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Early Modern period ==&lt;br /&gt;
During the 16th and 17th centuries, the AKZ entered into closer contact with currents of [[Hermeticism]], [[natural philosophy]], and early modern occult scholarship. Several marginal figures in the European learned world appear to have been affiliated with — or at least influenced by — the Committee’s symbolic repertoire.&lt;br /&gt;
&lt;br /&gt;
A Venetian codex from 1564 contains references to “the Zogu diagrams” in a context parallel to John Dee’s &#039;&#039;[[Monas Hieroglyphica]]&#039;&#039;. The diagrams themselves, combining the moose antler motif with planetary seals, were later copied into Dee’s notebooks by unidentified hands.&amp;lt;ref name=&amp;quot;Bellori1927&amp;quot; /&amp;gt;&lt;br /&gt;
[[File:Zogu.jpg|none|thumb|705x705px|Opening of the &#039;&#039;&#039;Codex Aurifaber (Venice, 1564)&#039;&#039;&#039;. The left folio contains a stag’s head with elaborate antlers, sketched over marginal notes in a humanist hand; the right folio preserves four circular “Zogu diagrams” combining planetary seals with antler-like extensions. The mixture of scripts and inks suggests interpolation by more than one contributor.]]&lt;br /&gt;
In the 1620s, Athanasius Kircher corresponded with unnamed “brothers of Zogu,” acknowledging receipt of strange diagrams allegedly derived from Berat inscriptions. Although Kircher dismissed their magical efficacy, he preserved several in his &#039;&#039;Ars Magna Lucis et Umbrae&#039;&#039;, where they appear alongside optical experiments.&amp;lt;ref name=&amp;quot;Kircher1646&amp;quot;&amp;gt;A. Kircher, &#039;&#039;Epistolae ad Fratres Zoguicos&#039;&#039; (MS Vaticanus Lat. 11932, fols. 44–49, c. 1628), cited in M. Engel, &#039;&#039;Kircher and the Shadows of the Balkans&#039;&#039; (Vienna: Collegium Hermeticum, 1981), pp. 212–219.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By the mid-17th century, Rosicrucian tracts circulated in Central Europe make oblique mention of “the House of Zog,” described as a locus of hidden knowledge “between the stag and the star.” Modern historians suggest that AKZ symbols were selectively adopted into the Rosicrucian mythos to enhance its aura of antiquity.&amp;lt;ref name=&amp;quot;Reichenbach1954&amp;quot; /&amp;gt; &amp;lt;ref name=&amp;quot;Guderian1978&amp;quot;&amp;gt;H. Guderian, &#039;&#039;Fraternitas Zoguica: Balkan Currents in the Rosicrucian Manifestos&#039;&#039; (Basel: Ars Occulta, 1978), pp. 51–73.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The Committee’s activities in this period remain only partially documented, but engravings dated 1651 attributed to AKZ members show hybrid sigils combining the caduceus, a moose head, and concentric planetary rings. These images anticipate later developments in both esoteric art and scientific illustration, blurring the line between symbolic ornament and purportedly empirical diagram.&amp;lt;ref name=&amp;quot;Reichenbach1954&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While mainstream scholars of the Enlightenment dismissed AKZ materials as curiosities, their circulation within hermetic and para-scientific networks ensured the Committee’s survival into the modern age.&lt;br /&gt;
&lt;br /&gt;
== Interpretations ==&lt;br /&gt;
Scholars remain divided on whether the Committee’s symbols represent: a mnemonic system for astronomical cycles, a parodic or satirical commentary on learned societies, or genuine survivals of apotropaic and hermetic practices.&lt;br /&gt;
&lt;br /&gt;
The supposed influence of the AKZ on early modern figures such as [[John Dee]], [[Athanasius Kircher]], and the authors of the [[Rosicrucian Manifestos]] has been particularly contentious. Some historians argue that Dee’s notes on “Zogu diagrams,” Kircher’s correspondence with unnamed Balkan associates, and Rosicrucian imagery of the “stag and star” provide independent confirmation of the Committee’s role in shaping European esoteric currents.&amp;lt;ref name=&amp;quot;Kircher1646&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;Guderian1978&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Others dismiss these claims as the result of deliberate myth-making during the 1987 refoundation, when members selectively emphasized obscure parallels in order to bolster the Committee’s antiquity and prestige.&amp;lt;ref name=&amp;quot;Meer2004&amp;quot; /&amp;gt; In this reading, Dee’s marginalia and Kircher’s letters are interpreted out of context, while Rosicrucian references are too vague to prove a substantive link.&lt;br /&gt;
&lt;br /&gt;
Between these poles lies a para-historical position: that AKZ materials circulated through underground channels of manuscript exchange, leaving subtle traces in early modern Europe, but without the systematic influence sometimes claimed. This view emphasizes the Committee’s ability to reappear in different cultural guises, adapting its symbols to prevailing intellectual fashions.&lt;br /&gt;
&lt;br /&gt;
Ultimately, the balance between authentic hermetic continuity and playful retroactive fabrication remains unresolved, a tension that continues to define the Committee’s historiography.&lt;br /&gt;
&lt;br /&gt;
== 19th and 20th centuries ==&lt;br /&gt;
References to the AKZ surface sporadically in 19th-century pamphlets concerned with Balkan independence, often accompanied by cryptic sigils or moose motifs later taken up in the Committee’s iconography. The group’s activities during the two [[World Wars]] remain obscure, though fragmentary correspondence places members in neutral [[Switzerland]] and occupied [[Belgium]].&amp;lt;ref name=&amp;quot;Schroder1959&amp;quot;&amp;gt;H. Schröder, &#039;&#039;Pamphlets and Phantoms: Monarchist Undergrounds in Europe&#039;&#039; (Leipzig: Collegium Historiae, 1959), pp. 88–96.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The official “refounding” of the Committee in 1987 took place in [[Ghent]], Belgium, under the aegis of a circle of academics, artists, and eccentric monarchists. This incarnation emphasized archival recovery, speculative historiography, and playful public outreach.&amp;lt;ref name=&amp;quot;VanLooy1992&amp;quot;&amp;gt;C. Van Looy, &#039;&#039;Gent en de Heroprichting van het Comité&#039;&#039; (Antwerp: Zephyrus Press, 1992), pp. 34–41.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== The 1987–2001 website ==&lt;br /&gt;
A distinctive feature of the refounded AKZ was its pioneering [[History of the World Wide Web|web presence]], launched in the early 1990s. The site, hosted initially on university servers and later at &#039;&#039;zog.org&#039;&#039;, presented a mixture of archival reconstruction, surrealist speculation, and committee news.&lt;br /&gt;
&lt;br /&gt;
Highlights included: reports on seminars in [[Keflavík]], [[Borg, Germany|Borg]], Ghent, and [[Granada]]; the “[[Moose Department]],” a sub-section exploring symbolic connections among moose, walruses, and European monarchist iconography; playful pseudo-scholarly investigations linking figures such as [[Lewis Carroll]] and [[The Beatles]] to the Committee’s mythos.&amp;lt;ref name=&amp;quot;Hellebuyck2008&amp;quot;&amp;gt;K. Hellebuyck, &#039;&#039;Cybernetic Monarchies: Early Web Parodies and Parafictions&#039;&#039; (&#039;&#039;Transactions on Digital Culture&#039;&#039;, vol. 11, 2008), pp. 63–79.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Though the site went offline in the early 2000s, fragments survive in personal backups and the [[Wayback Machine|Internet Archive]]. Scholars of digital culture have since treated it as an early example of “web-based [[Parafiction|parafiction]].”&amp;lt;ref name=&amp;quot;Meer2004&amp;quot;&amp;gt;A. van der Meer, &#039;&#039;Web Parafictions of the Late 20th Century&#039;&#039; (Ghent: Hypertext Studies, 2004), pp. 211–223.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== The Moose Department ==&lt;br /&gt;
&#039;&#039;Main article: [[Moose Dept.]]&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Within the Committee’s 1987–2001 web presence, one of the most prominent sub-sections was the so-called Moose Department. Although framed as a playful research unit, it drew heavily on the Committee’s long-standing fascination with cervid imagery and its hermetic significance.&lt;br /&gt;
&lt;br /&gt;
The Moose Dept. combined whimsical para-historical essays with pseudo-academic apparatus, drawing connections between moose, walruses, and symbols found in both medieval AKZ sigils and 20th-century popular culture. Some commentators see the Department as a continuation of earlier animal symbolism in the Committee’s history; others interpret it as a postmodern parody of academic specialization.&amp;lt;ref name=&amp;quot;Hellebuyck2008&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
[[Zog I|King Zog I of Albania]]&lt;br /&gt;
&lt;br /&gt;
[[Parafiction]]&lt;br /&gt;
&lt;br /&gt;
[[Moose Dept.|The Moose Dept. (AKZ)]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Learned societies]]&lt;br /&gt;
[[Category:Organizations established in the 14th century]]&lt;br /&gt;
[[Category:Hermeticism]]&lt;br /&gt;
[[Category:Albania for King Zog Committee]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Albania_for_King_Zog_Committee&amp;diff=373</id>
		<title>Albania for King Zog Committee</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Albania_for_King_Zog_Committee&amp;diff=373"/>
		<updated>2025-11-16T19:01:58Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Interpretations */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name = Albania for King Zog Committee&lt;br /&gt;
| image = Guildhall.jpg&lt;br /&gt;
| caption = Now demolished façade in Ghent, c. 1932. The partially effaced heraldic shield has been associated with the Committee’s activities in the Low Countries.&lt;br /&gt;
| 01 Founded = attested 1325 (first trace)&lt;br /&gt;
| 02 Refounded = 1987 (present incarnation)&lt;br /&gt;
| 03 Headquarters = Ghent, Belgium&lt;br /&gt;
| 04 Status = Archives partially lost; ongoing reconstruction&lt;br /&gt;
| 05 Website = zog.org (1987–2001)&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;&#039;Albania for King Zog Committee (AKZ)&#039;&#039;&#039; is an international learned society and semi-secret association concerned with monarchy, cultural survivals, and esoteric historiography. The Committee has no historical connection to Albania, the Balkans, or [[Zog I|King Zog of Albania]] (1895–1961). Its name — variously recorded in medieval sources as &#039;&#039;Zogh&#039;&#039;, &#039;&#039;Tsog&#039;&#039;, or &#039;&#039;Zogu&#039;&#039; — predates the 20th-century monarch by several centuries and reflects older traditions of uncertain origin.&lt;br /&gt;
&lt;br /&gt;
The coincidence that a later monarch bore the same name has long divided historians: some regard it as a mere accident of etymology, others as an instance of uncanny foresight, and a minority as evidence of deliberate myth-making across generations. What is undisputed is that the phrase “for King Zog” has been central to the Committee’s identity for at least seven centuries.&lt;br /&gt;
&lt;br /&gt;
== Origins ==&lt;br /&gt;
The earliest surviving mention of the Committee dates to 1325, in a notarial record from [[Ragusa (Dubrovnik)|Ragusa]] (modern [[Dubrovnik]]), referring to a &#039;&#039;confraternitas pro Rege Zogu&#039;&#039;. Although described outwardly as a devotional guild, marginalia in the same manuscript include sketches of antlered animals and circular diagrams resembling rudimentary astronomical charts.&amp;lt;ref name=&amp;quot;Cittadini1911&amp;quot;&amp;gt;R. Cittadini, &#039;&#039;Atti notarili e confraternite dalmata&#039;&#039; (Venice: Archivio Serafini, 1911), p. 203.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Already in this period, the Committee seems to have pursued esoteric investigations. Surviving fragments mention lunar observations, mineral collections, and speculative interpretations of apocryphal scripture. Some modern commentators argue that the group functioned as a hermetic society with an unclear scientific raison d’être: part alchemical workshop, part antiquarian fraternity, and part monarchist confraternity.&amp;lt;ref name=&amp;quot;Paredes1962&amp;quot;&amp;gt;L. Paredes, &#039;&#039;Hermetica Balcanica: Societies of the Eastern Adriatic&#039;&#039; (Naples: Officina Aurea, 1962), pp. 87–92.&amp;lt;/ref&amp;gt;&lt;br /&gt;
[[File:Lion fountain.jpg|alt=Lion fountain|thumb|&#039;&#039;Lion fountain in the Alhambra, Granada, photographed by Clara Jensen, c. 1968. Later AKZ commentators pointed to the faint geometrical carvings beneath the waterline as evidence of the Committee’s presence in Spain.&#039;&#039;]]&lt;br /&gt;
Archaeological finds have complicated this picture. Ceramic shards discovered near [[Berat]] in 1899 bear spiralling [[Aramaic]] inscriptions reminiscent of [[Incantation bowl|incantation bowls]] associated with the [[Lilith]] tradition of late antiquity, and have been cited as possible evidence that the Committee’s name and symbolism grew out of [[Apotropaic magic|apotropaic magical]] traditions rather than straightforward political allegiance.&amp;lt;ref name=&amp;quot;Dervishi1973&amp;quot;&amp;gt;M. Dervishi, &#039;&#039;The Berat Bowls: Aramaic Incantations in the Balkans&#039;&#039; (&#039;&#039;Journal of Uncanny Archaeology&#039;&#039;, vol. 4, no. 2, 1973), pp. 45–67.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By the 15th century, traces of AKZ appear in [[Venice|Venetian]] and [[Florence|Florentine]] archives, where members are recorded commissioning astrological tables and sponsoring translations of Byzantine medical texts. Sigils preserved in these documents combine the moose emblem with planetary seals, anticipating later overlaps with Renaissance [[Hermeticism|hermetic magic]].&amp;lt;ref name=&amp;quot;Bellori1927&amp;quot;&amp;gt;G. Bellori, &#039;&#039;Codices Obscuri: Marginalia of the Venetian Manuscripts&#039;&#039; (Trieste: Edizioni Cryptica, 1927), pp. 114–118.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
During the [[Ottoman Empire|Ottoman centuries]], shadowy references to “Zog circles” survive in reports from [[Krujë]] and [[Thessaloniki]], though whether these were continuations of the medieval confraternity or periodic revivals remains unclear. The continuity of practice is further obscured by repeated archival loss through war, fire, and suppression, leaving only fragmentary clues to the Committee’s early identity.&amp;lt;ref name=&amp;quot;Kalemi1985&amp;quot;&amp;gt;Y. Kalemi, &#039;&#039;Societies in Shadow: Hidden Networks of the Ottoman Balkans&#039;&#039; (Istanbul: Akademi Mimar Sinan, 1985), pp. 51–64.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Symbols and sigils ==&lt;br /&gt;
From its earliest attestations, the AKZ employed a shifting set of symbols whose meanings remain debated. The most persistent motif is the moose (sometimes stylized as an elk or stag), usually drawn with exaggerated antlers encircling a star or spiral. Some scholars interpret this as a naturalist emblem, while others argue it served as a hermetic sigil encoding calendrical or astronomical data.&amp;lt;ref name=&amp;quot;Markovic1979&amp;quot;&amp;gt;D. Marković, &#039;&#039;Bestiae Occultae: Animal Motifs in Balkan Secret Societies&#039;&#039; (Belgrade: Zadužbina Petrović, 1979), pp. 143–155.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By the 14th century, AKZ records also include diagrams resembling the planetary seals of Renaissance [[Magic (supernatural)|magic]], suggesting contact with broader currents of [[Hermeticism|hermetic philosophy]]. Marginalia in a 1482 codex from Venice refer to “the Zogu star-lodge” (&#039;&#039;stella domus Zogu&#039;&#039;), hinting at links with early alchemical fraternities.&amp;lt;ref name=&amp;quot;Bellori1927&amp;quot; /&amp;gt;&lt;br /&gt;
[[File:Moose bowl.jpg|thumb|Moose Bowl, reconstruction]]&lt;br /&gt;
One of the more enigmatic survivals is a set of ceramic fragments unearthed near Berat in 1899, inscribed in spiralling Aramaic. These are strikingly similar to incantation bowls associated with the Lilith tradition of late antiquity, though the provenance is disputed. A controversial reading proposes that the inscription invokes “the king who comes as Zog” (&#039;&#039;mlk’ d’t’ zg&#039;&#039;), which would push the Committee’s nomenclature into the realm of apotropaic magic.&amp;lt;ref name=&amp;quot;Dervishi1973&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Later symbolism intertwined these traditions with early modern esotericism. Seventeenth-century engravings attributed to the Committee combine the moose motif with Hermetic caducei and geometrical diagrams reminiscent of John Dee’s &#039;&#039;[[Monas Hieroglyphica]]&#039;&#039;.&amp;lt;ref name=&amp;quot;Reichenbach1954&amp;quot;&amp;gt;F. Reichenbach, &#039;&#039;Symbola Zoguica: The Hidden Geometry of a Balkan Fraternity&#039;&#039; (Basel: Ars Hermetica, 1954).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Early Modern period ==&lt;br /&gt;
During the 16th and 17th centuries, the AKZ entered into closer contact with currents of [[Hermeticism]], [[natural philosophy]], and early modern occult scholarship. Several marginal figures in the European learned world appear to have been affiliated with — or at least influenced by — the Committee’s symbolic repertoire.&lt;br /&gt;
&lt;br /&gt;
A Venetian codex from 1564 contains references to “the Zogu diagrams” in a context parallel to John Dee’s &#039;&#039;[[Monas Hieroglyphica]]&#039;&#039;. The diagrams themselves, combining the moose antler motif with planetary seals, were later copied into Dee’s notebooks by unidentified hands.&amp;lt;ref name=&amp;quot;Bellori1927&amp;quot; /&amp;gt;&lt;br /&gt;
[[File:Zogu.jpg|none|thumb|705x705px|Opening of the &#039;&#039;&#039;Codex Aurifaber (Venice, 1564)&#039;&#039;&#039;. The left folio contains a stag’s head with elaborate antlers, sketched over marginal notes in a humanist hand; the right folio preserves four circular “Zogu diagrams” combining planetary seals with antler-like extensions. The mixture of scripts and inks suggests interpolation by more than one contributor.]]&lt;br /&gt;
In the 1620s, Athanasius Kircher corresponded with unnamed “brothers of Zogu,” acknowledging receipt of strange diagrams allegedly derived from Berat inscriptions. Although Kircher dismissed their magical efficacy, he preserved several in his &#039;&#039;Ars Magna Lucis et Umbrae&#039;&#039;, where they appear alongside optical experiments.&amp;lt;ref name=&amp;quot;Kircher1646&amp;quot;&amp;gt;A. Kircher, &#039;&#039;Epistolae ad Fratres Zoguicos&#039;&#039; (MS Vaticanus Lat. 11932, fols. 44–49, c. 1628), cited in M. Engel, &#039;&#039;Kircher and the Shadows of the Balkans&#039;&#039; (Vienna: Collegium Hermeticum, 1981), pp. 212–219.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By the mid-17th century, Rosicrucian tracts circulated in Central Europe make oblique mention of “the House of Zog,” described as a locus of hidden knowledge “between the stag and the star.” Modern historians suggest that AKZ symbols were selectively adopted into the Rosicrucian mythos to enhance its aura of antiquity.&amp;lt;ref name=&amp;quot;Reichenbach1954&amp;quot; /&amp;gt; &amp;lt;ref name=&amp;quot;Guderian1978&amp;quot;&amp;gt;H. Guderian, &#039;&#039;Fraternitas Zoguica: Balkan Currents in the Rosicrucian Manifestos&#039;&#039; (Basel: Ars Occulta, 1978), pp. 51–73.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The Committee’s activities in this period remain only partially documented, but engravings dated 1651 attributed to AKZ members show hybrid sigils combining the caduceus, a moose head, and concentric planetary rings. These images anticipate later developments in both esoteric art and scientific illustration, blurring the line between symbolic ornament and purportedly empirical diagram.&amp;lt;ref name=&amp;quot;Reichenbach1954&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While mainstream scholars of the Enlightenment dismissed AKZ materials as curiosities, their circulation within hermetic and para-scientific networks ensured the Committee’s survival into the modern age.&lt;br /&gt;
&lt;br /&gt;
== Interpretations ==&lt;br /&gt;
Scholars remain divided on whether the Committee’s symbols represent: a mnemonic system for astronomical cycles, a parodic or satirical commentary on learned societies, or genuine survivals of apotropaic and hermetic practices.&lt;br /&gt;
&lt;br /&gt;
The supposed influence of the AKZ on early modern figures such as [[John Dee]], [[Athanasius Kircher]], and the authors of the [[Rosicrucian Manifestos]] has been particularly contentious. Some historians argue that Dee’s notes on “Zogu diagrams,” Kircher’s correspondence with unnamed Balkan associates, and Rosicrucian imagery of the “stag and star” provide independent confirmation of the Committee’s role in shaping European esoteric currents.&amp;lt;ref name=&amp;quot;Kircher1646&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;Guderian1978&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Others dismiss these claims as the result of deliberate myth-making during the 1987 refoundation, when members selectively emphasized obscure parallels in order to bolster the Committee’s antiquity and prestige.&amp;lt;ref name=&amp;quot;Meer2004&amp;quot; /&amp;gt; In this reading, Dee’s marginalia and Kircher’s letters are interpreted out of context, while Rosicrucian references are too vague to prove a substantive link.&lt;br /&gt;
&lt;br /&gt;
Between these poles lies a para-historical position: that AKZ materials circulated through underground channels of manuscript exchange, leaving subtle traces in early modern Europe, but without the systematic influence sometimes claimed. This view emphasizes the Committee’s ability to reappear in different cultural guises, adapting its symbols to prevailing intellectual fashions.&lt;br /&gt;
&lt;br /&gt;
Ultimately, the balance between authentic hermetic continuity and playful retroactive fabrication remains unresolved, a tension that continues to define the Committee’s historiography.&lt;br /&gt;
&lt;br /&gt;
== 19th and 20th centuries ==&lt;br /&gt;
References to the AKZ surface sporadically in 19th-century pamphlets concerned with Balkan independence, often accompanied by cryptic sigils or moose motifs later taken up in the Committee’s iconography. The group’s activities during the two [[World Wars]] remain obscure, though fragmentary correspondence places members in neutral [[Switzerland]] and occupied [[Belgium]].&amp;lt;ref name=&amp;quot;Schroder1959&amp;quot;&amp;gt;H. Schröder, &#039;&#039;Pamphlets and Phantoms: Monarchist Undergrounds in Europe&#039;&#039; (Leipzig: Collegium Historiae, 1959), pp. 88–96.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The official “refounding” of the Committee in 1987 took place in [[Ghent]], Belgium, under the aegis of a circle of academics, artists, and eccentric monarchists. This incarnation emphasized archival recovery, speculative historiography, and playful public outreach.&amp;lt;ref name=&amp;quot;VanLooy1992&amp;quot;&amp;gt;C. Van Looy, &#039;&#039;Gent en de Heroprichting van het Comité&#039;&#039; (Antwerp: Zephyrus Press, 1992), pp. 34–41.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== The 1987–2001 website ==&lt;br /&gt;
A distinctive feature of the refounded AKZ was its pioneering [[History of the World Wide Web|web presence]], launched in the early 1990s. The site, hosted initially on university servers and later at &#039;&#039;zog.org&#039;&#039;, presented a mixture of archival reconstruction, surrealist speculation, and committee news.&lt;br /&gt;
&lt;br /&gt;
Highlights included:&lt;br /&gt;
&lt;br /&gt;
Reports on seminars in [[Keflavík]], [[Borg, Germany|Borg]], Ghent, and [[Granada]].&lt;br /&gt;
&lt;br /&gt;
The “Moose Department,” a sub-section exploring symbolic connections among moose, walruses, and European monarchist iconography.&lt;br /&gt;
&lt;br /&gt;
Playful pseudo-scholarly investigations linking figures such as [[Lewis Carroll]] and [[The Beatles]] to the Committee’s mythos.&amp;lt;ref name=&amp;quot;Hellebuyck2008&amp;quot;&amp;gt;K. Hellebuyck, &#039;&#039;Cybernetic Monarchies: Early Web Parodies and Parafictions&#039;&#039; (&#039;&#039;Transactions on Digital Culture&#039;&#039;, vol. 11, 2008), pp. 63–79.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Though the site went offline in the early 2000s, fragments survive in personal backups and the [[Wayback Machine|Internet Archive]]. Scholars of digital culture have since treated it as an early example of “web-based [[Parafiction|parafiction]].”&amp;lt;ref name=&amp;quot;Meer2004&amp;quot;&amp;gt;A. van der Meer, &#039;&#039;Web Parafictions of the Late 20th Century&#039;&#039; (Ghent: Hypertext Studies, 2004), pp. 211–223.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== The Moose Department ==&lt;br /&gt;
&#039;&#039;Main article: [[Moose Dept.]]&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Within the Committee’s 1987–2001 web presence, one of the most prominent sub-sections was the so-called Moose Department. Although framed as a playful research unit, it drew heavily on the Committee’s long-standing fascination with cervid imagery and its hermetic significance.&lt;br /&gt;
&lt;br /&gt;
The Moose Dept. combined whimsical para-historical essays with pseudo-academic apparatus, drawing connections between moose, walruses, and symbols found in both medieval AKZ sigils and 20th-century popular culture. Some commentators see the Department as a continuation of earlier animal symbolism in the Committee’s history; others interpret it as a postmodern parody of academic specialization.&amp;lt;ref name=&amp;quot;Hellebuyck2008&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
[[Zog I|King Zog I of Albania]]&lt;br /&gt;
&lt;br /&gt;
[[Parafiction]]&lt;br /&gt;
&lt;br /&gt;
[[Moose Dept.|The Moose Dept. (AKZ)]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Learned societies]]&lt;br /&gt;
[[Category:Organizations established in the 14th century]]&lt;br /&gt;
[[Category:Hermeticism]]&lt;br /&gt;
[[Category:Albania for King Zog Committee]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Albania_for_King_Zog_Committee&amp;diff=372</id>
		<title>Albania for King Zog Committee</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Albania_for_King_Zog_Committee&amp;diff=372"/>
		<updated>2025-11-16T18:58:31Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name = Albania for King Zog Committee&lt;br /&gt;
| image = Guildhall.jpg&lt;br /&gt;
| caption = Now demolished façade in Ghent, c. 1932. The partially effaced heraldic shield has been associated with the Committee’s activities in the Low Countries.&lt;br /&gt;
| 01 Founded = attested 1325 (first trace)&lt;br /&gt;
| 02 Refounded = 1987 (present incarnation)&lt;br /&gt;
| 03 Headquarters = Ghent, Belgium&lt;br /&gt;
| 04 Status = Archives partially lost; ongoing reconstruction&lt;br /&gt;
| 05 Website = zog.org (1987–2001)&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;&#039;Albania for King Zog Committee (AKZ)&#039;&#039;&#039; is an international learned society and semi-secret association concerned with monarchy, cultural survivals, and esoteric historiography. The Committee has no historical connection to Albania, the Balkans, or [[Zog I|King Zog of Albania]] (1895–1961). Its name — variously recorded in medieval sources as &#039;&#039;Zogh&#039;&#039;, &#039;&#039;Tsog&#039;&#039;, or &#039;&#039;Zogu&#039;&#039; — predates the 20th-century monarch by several centuries and reflects older traditions of uncertain origin.&lt;br /&gt;
&lt;br /&gt;
The coincidence that a later monarch bore the same name has long divided historians: some regard it as a mere accident of etymology, others as an instance of uncanny foresight, and a minority as evidence of deliberate myth-making across generations. What is undisputed is that the phrase “for King Zog” has been central to the Committee’s identity for at least seven centuries.&lt;br /&gt;
&lt;br /&gt;
== Origins ==&lt;br /&gt;
The earliest surviving mention of the Committee dates to 1325, in a notarial record from [[Ragusa (Dubrovnik)|Ragusa]] (modern [[Dubrovnik]]), referring to a &#039;&#039;confraternitas pro Rege Zogu&#039;&#039;. Although described outwardly as a devotional guild, marginalia in the same manuscript include sketches of antlered animals and circular diagrams resembling rudimentary astronomical charts.&amp;lt;ref name=&amp;quot;Cittadini1911&amp;quot;&amp;gt;R. Cittadini, &#039;&#039;Atti notarili e confraternite dalmata&#039;&#039; (Venice: Archivio Serafini, 1911), p. 203.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Already in this period, the Committee seems to have pursued esoteric investigations. Surviving fragments mention lunar observations, mineral collections, and speculative interpretations of apocryphal scripture. Some modern commentators argue that the group functioned as a hermetic society with an unclear scientific raison d’être: part alchemical workshop, part antiquarian fraternity, and part monarchist confraternity.&amp;lt;ref name=&amp;quot;Paredes1962&amp;quot;&amp;gt;L. Paredes, &#039;&#039;Hermetica Balcanica: Societies of the Eastern Adriatic&#039;&#039; (Naples: Officina Aurea, 1962), pp. 87–92.&amp;lt;/ref&amp;gt;&lt;br /&gt;
[[File:Lion fountain.jpg|alt=Lion fountain|thumb|&#039;&#039;Lion fountain in the Alhambra, Granada, photographed by Clara Jensen, c. 1968. Later AKZ commentators pointed to the faint geometrical carvings beneath the waterline as evidence of the Committee’s presence in Spain.&#039;&#039;]]&lt;br /&gt;
Archaeological finds have complicated this picture. Ceramic shards discovered near [[Berat]] in 1899 bear spiralling [[Aramaic]] inscriptions reminiscent of [[Incantation bowl|incantation bowls]] associated with the [[Lilith]] tradition of late antiquity, and have been cited as possible evidence that the Committee’s name and symbolism grew out of [[Apotropaic magic|apotropaic magical]] traditions rather than straightforward political allegiance.&amp;lt;ref name=&amp;quot;Dervishi1973&amp;quot;&amp;gt;M. Dervishi, &#039;&#039;The Berat Bowls: Aramaic Incantations in the Balkans&#039;&#039; (&#039;&#039;Journal of Uncanny Archaeology&#039;&#039;, vol. 4, no. 2, 1973), pp. 45–67.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By the 15th century, traces of AKZ appear in [[Venice|Venetian]] and [[Florence|Florentine]] archives, where members are recorded commissioning astrological tables and sponsoring translations of Byzantine medical texts. Sigils preserved in these documents combine the moose emblem with planetary seals, anticipating later overlaps with Renaissance [[Hermeticism|hermetic magic]].&amp;lt;ref name=&amp;quot;Bellori1927&amp;quot;&amp;gt;G. Bellori, &#039;&#039;Codices Obscuri: Marginalia of the Venetian Manuscripts&#039;&#039; (Trieste: Edizioni Cryptica, 1927), pp. 114–118.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
During the [[Ottoman Empire|Ottoman centuries]], shadowy references to “Zog circles” survive in reports from [[Krujë]] and [[Thessaloniki]], though whether these were continuations of the medieval confraternity or periodic revivals remains unclear. The continuity of practice is further obscured by repeated archival loss through war, fire, and suppression, leaving only fragmentary clues to the Committee’s early identity.&amp;lt;ref name=&amp;quot;Kalemi1985&amp;quot;&amp;gt;Y. Kalemi, &#039;&#039;Societies in Shadow: Hidden Networks of the Ottoman Balkans&#039;&#039; (Istanbul: Akademi Mimar Sinan, 1985), pp. 51–64.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Symbols and sigils ==&lt;br /&gt;
From its earliest attestations, the AKZ employed a shifting set of symbols whose meanings remain debated. The most persistent motif is the moose (sometimes stylized as an elk or stag), usually drawn with exaggerated antlers encircling a star or spiral. Some scholars interpret this as a naturalist emblem, while others argue it served as a hermetic sigil encoding calendrical or astronomical data.&amp;lt;ref name=&amp;quot;Markovic1979&amp;quot;&amp;gt;D. Marković, &#039;&#039;Bestiae Occultae: Animal Motifs in Balkan Secret Societies&#039;&#039; (Belgrade: Zadužbina Petrović, 1979), pp. 143–155.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By the 14th century, AKZ records also include diagrams resembling the planetary seals of Renaissance [[Magic (supernatural)|magic]], suggesting contact with broader currents of [[Hermeticism|hermetic philosophy]]. Marginalia in a 1482 codex from Venice refer to “the Zogu star-lodge” (&#039;&#039;stella domus Zogu&#039;&#039;), hinting at links with early alchemical fraternities.&amp;lt;ref name=&amp;quot;Bellori1927&amp;quot; /&amp;gt;&lt;br /&gt;
[[File:Moose bowl.jpg|thumb|Moose Bowl, reconstruction]]&lt;br /&gt;
One of the more enigmatic survivals is a set of ceramic fragments unearthed near Berat in 1899, inscribed in spiralling Aramaic. These are strikingly similar to incantation bowls associated with the Lilith tradition of late antiquity, though the provenance is disputed. A controversial reading proposes that the inscription invokes “the king who comes as Zog” (&#039;&#039;mlk’ d’t’ zg&#039;&#039;), which would push the Committee’s nomenclature into the realm of apotropaic magic.&amp;lt;ref name=&amp;quot;Dervishi1973&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Later symbolism intertwined these traditions with early modern esotericism. Seventeenth-century engravings attributed to the Committee combine the moose motif with Hermetic caducei and geometrical diagrams reminiscent of John Dee’s &#039;&#039;[[Monas Hieroglyphica]]&#039;&#039;.&amp;lt;ref name=&amp;quot;Reichenbach1954&amp;quot;&amp;gt;F. Reichenbach, &#039;&#039;Symbola Zoguica: The Hidden Geometry of a Balkan Fraternity&#039;&#039; (Basel: Ars Hermetica, 1954).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Early Modern period ==&lt;br /&gt;
During the 16th and 17th centuries, the AKZ entered into closer contact with currents of [[Hermeticism]], [[natural philosophy]], and early modern occult scholarship. Several marginal figures in the European learned world appear to have been affiliated with — or at least influenced by — the Committee’s symbolic repertoire.&lt;br /&gt;
&lt;br /&gt;
A Venetian codex from 1564 contains references to “the Zogu diagrams” in a context parallel to John Dee’s &#039;&#039;[[Monas Hieroglyphica]]&#039;&#039;. The diagrams themselves, combining the moose antler motif with planetary seals, were later copied into Dee’s notebooks by unidentified hands.&amp;lt;ref name=&amp;quot;Bellori1927&amp;quot; /&amp;gt;&lt;br /&gt;
[[File:Zogu.jpg|none|thumb|705x705px|Opening of the &#039;&#039;&#039;Codex Aurifaber (Venice, 1564)&#039;&#039;&#039;. The left folio contains a stag’s head with elaborate antlers, sketched over marginal notes in a humanist hand; the right folio preserves four circular “Zogu diagrams” combining planetary seals with antler-like extensions. The mixture of scripts and inks suggests interpolation by more than one contributor.]]&lt;br /&gt;
In the 1620s, Athanasius Kircher corresponded with unnamed “brothers of Zogu,” acknowledging receipt of strange diagrams allegedly derived from Berat inscriptions. Although Kircher dismissed their magical efficacy, he preserved several in his &#039;&#039;Ars Magna Lucis et Umbrae&#039;&#039;, where they appear alongside optical experiments.&amp;lt;ref name=&amp;quot;Kircher1646&amp;quot;&amp;gt;A. Kircher, &#039;&#039;Epistolae ad Fratres Zoguicos&#039;&#039; (MS Vaticanus Lat. 11932, fols. 44–49, c. 1628), cited in M. Engel, &#039;&#039;Kircher and the Shadows of the Balkans&#039;&#039; (Vienna: Collegium Hermeticum, 1981), pp. 212–219.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By the mid-17th century, Rosicrucian tracts circulated in Central Europe make oblique mention of “the House of Zog,” described as a locus of hidden knowledge “between the stag and the star.” Modern historians suggest that AKZ symbols were selectively adopted into the Rosicrucian mythos to enhance its aura of antiquity.&amp;lt;ref name=&amp;quot;Reichenbach1954&amp;quot; /&amp;gt; &amp;lt;ref name=&amp;quot;Guderian1978&amp;quot;&amp;gt;H. Guderian, &#039;&#039;Fraternitas Zoguica: Balkan Currents in the Rosicrucian Manifestos&#039;&#039; (Basel: Ars Occulta, 1978), pp. 51–73.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The Committee’s activities in this period remain only partially documented, but engravings dated 1651 attributed to AKZ members show hybrid sigils combining the caduceus, a moose head, and concentric planetary rings. These images anticipate later developments in both esoteric art and scientific illustration, blurring the line between symbolic ornament and purportedly empirical diagram.&amp;lt;ref name=&amp;quot;Reichenbach1954&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While mainstream scholars of the Enlightenment dismissed AKZ materials as curiosities, their circulation within hermetic and para-scientific networks ensured the Committee’s survival into the modern age.&lt;br /&gt;
&lt;br /&gt;
== Interpretations ==&lt;br /&gt;
Scholars remain divided on whether the Committee’s symbols represent:&lt;br /&gt;
&lt;br /&gt;
a mnemonic system for astronomical cycles,&lt;br /&gt;
&lt;br /&gt;
a parodic or satirical commentary on learned societies,&lt;br /&gt;
&lt;br /&gt;
or genuine survivals of apotropaic and hermetic practices.&lt;br /&gt;
&lt;br /&gt;
The supposed influence of the AKZ on early modern figures such as [[John Dee]], [[Athanasius Kircher]], and the authors of the [[Rosicrucian Manifestos]] has been particularly contentious. Some historians argue that Dee’s notes on “Zogu diagrams,” Kircher’s correspondence with unnamed Balkan associates, and Rosicrucian imagery of the “stag and star” provide independent confirmation of the Committee’s role in shaping European esoteric currents.&amp;lt;ref name=&amp;quot;Kircher1646&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;Guderian1978&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Others dismiss these claims as the result of deliberate myth-making during the 1987 refoundation, when members selectively emphasized obscure parallels in order to bolster the Committee’s antiquity and prestige.&amp;lt;ref name=&amp;quot;Meer2004&amp;quot; /&amp;gt; In this reading, Dee’s marginalia and Kircher’s letters are interpreted out of context, while Rosicrucian references are too vague to prove a substantive link.&lt;br /&gt;
&lt;br /&gt;
Between these poles lies a para-historical position: that AKZ materials circulated through underground channels of manuscript exchange, leaving subtle traces in early modern Europe, but without the systematic influence sometimes claimed. This view emphasizes the Committee’s ability to reappear in different cultural guises, adapting its symbols to prevailing intellectual fashions.&lt;br /&gt;
&lt;br /&gt;
Ultimately, the balance between authentic hermetic continuity and playful retroactive fabrication remains unresolved, a tension that continues to define the Committee’s historiography.&lt;br /&gt;
&lt;br /&gt;
== 19th and 20th centuries ==&lt;br /&gt;
References to the AKZ surface sporadically in 19th-century pamphlets concerned with Balkan independence, often accompanied by cryptic sigils or moose motifs later taken up in the Committee’s iconography. The group’s activities during the two [[World Wars]] remain obscure, though fragmentary correspondence places members in neutral [[Switzerland]] and occupied [[Belgium]].&amp;lt;ref name=&amp;quot;Schroder1959&amp;quot;&amp;gt;H. Schröder, &#039;&#039;Pamphlets and Phantoms: Monarchist Undergrounds in Europe&#039;&#039; (Leipzig: Collegium Historiae, 1959), pp. 88–96.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The official “refounding” of the Committee in 1987 took place in [[Ghent]], Belgium, under the aegis of a circle of academics, artists, and eccentric monarchists. This incarnation emphasized archival recovery, speculative historiography, and playful public outreach.&amp;lt;ref name=&amp;quot;VanLooy1992&amp;quot;&amp;gt;C. Van Looy, &#039;&#039;Gent en de Heroprichting van het Comité&#039;&#039; (Antwerp: Zephyrus Press, 1992), pp. 34–41.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== The 1987–2001 website ==&lt;br /&gt;
A distinctive feature of the refounded AKZ was its pioneering [[History of the World Wide Web|web presence]], launched in the early 1990s. The site, hosted initially on university servers and later at &#039;&#039;zog.org&#039;&#039;, presented a mixture of archival reconstruction, surrealist speculation, and committee news.&lt;br /&gt;
&lt;br /&gt;
Highlights included:&lt;br /&gt;
&lt;br /&gt;
Reports on seminars in [[Keflavík]], [[Borg, Germany|Borg]], Ghent, and [[Granada]].&lt;br /&gt;
&lt;br /&gt;
The “Moose Department,” a sub-section exploring symbolic connections among moose, walruses, and European monarchist iconography.&lt;br /&gt;
&lt;br /&gt;
Playful pseudo-scholarly investigations linking figures such as [[Lewis Carroll]] and [[The Beatles]] to the Committee’s mythos.&amp;lt;ref name=&amp;quot;Hellebuyck2008&amp;quot;&amp;gt;K. Hellebuyck, &#039;&#039;Cybernetic Monarchies: Early Web Parodies and Parafictions&#039;&#039; (&#039;&#039;Transactions on Digital Culture&#039;&#039;, vol. 11, 2008), pp. 63–79.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Though the site went offline in the early 2000s, fragments survive in personal backups and the [[Wayback Machine|Internet Archive]]. Scholars of digital culture have since treated it as an early example of “web-based [[Parafiction|parafiction]].”&amp;lt;ref name=&amp;quot;Meer2004&amp;quot;&amp;gt;A. van der Meer, &#039;&#039;Web Parafictions of the Late 20th Century&#039;&#039; (Ghent: Hypertext Studies, 2004), pp. 211–223.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== The Moose Department ==&lt;br /&gt;
&#039;&#039;Main article: [[Moose Dept.]]&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Within the Committee’s 1987–2001 web presence, one of the most prominent sub-sections was the so-called Moose Department. Although framed as a playful research unit, it drew heavily on the Committee’s long-standing fascination with cervid imagery and its hermetic significance.&lt;br /&gt;
&lt;br /&gt;
The Moose Dept. combined whimsical para-historical essays with pseudo-academic apparatus, drawing connections between moose, walruses, and symbols found in both medieval AKZ sigils and 20th-century popular culture. Some commentators see the Department as a continuation of earlier animal symbolism in the Committee’s history; others interpret it as a postmodern parody of academic specialization.&amp;lt;ref name=&amp;quot;Hellebuyck2008&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
[[Zog I|King Zog I of Albania]]&lt;br /&gt;
&lt;br /&gt;
[[Parafiction]]&lt;br /&gt;
&lt;br /&gt;
[[Moose Dept.|The Moose Dept. (AKZ)]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
[[Category:Learned societies]]&lt;br /&gt;
[[Category:Organizations established in the 14th century]]&lt;br /&gt;
[[Category:Hermeticism]]&lt;br /&gt;
[[Category:Albania for King Zog Committee]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=371</id>
		<title>Dozenal Primer Inscription</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=371"/>
		<updated>2025-10-30T11:13:21Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Dozenal successor schema.png|thumb|The four examples render the successor chain 1→2, 2→3, 3→4, 4→5, demonstrating that adding the simplex unit “ONE” advances X to its next value while preserving clause structure. Codes follow Kristiansen’s signary; glyph labels (“ONE”, “TWO”, …) are semantic glosses for expository clarity and do not assume phonetic values.]]&lt;br /&gt;
The &#039;&#039;&#039;Dozenal Primer Inscription&#039;&#039;&#039; is the working name for a long geometric-glyph text proposed to encode a compact [[Duodecimal|dozenal]] (base-12) arithmetic register. The inscription is transcribed with a neutral code of sign classes (for example A01, B04, C03 and a slash-like divider P01) that mirrors the notation first used to document the shorter [[Scapula Glyph Inscription]] (KS-01). In 2024, a study argued from distributional evidence—rather than from phonetic values or a bilingual—that the text exhibits an equation-like clause structure with bound punctuation, a stereotyped medial “equals” spine, operator clusters, and a productive ×12 derivational suffix. On that account, the inscription functions as a brief primer for base-12 arithmetic, extending to compounds traditionally glossed as &#039;&#039;dozen&#039;&#039; (12×), &#039;&#039;gross&#039;&#039; (12²) and a tentative &#039;&#039;greatgross&#039;&#039; (12³).&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot;&amp;gt;Ginevra Rubergskier, “A dozenal primer hidden in plain sight: decoding arithmetic from a corpus of tagged tokens,” &#039;&#039;Language Codes&#039;&#039; 6 (2024): 820–824.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The sign code used to publish the Dozenal Primer Inscription derives from earlier internal documentation of KS-01, which described short ruled lines, rectilinear glyphs built from a limited set of straight strokes, and visually coherent families (corners, boxes, triangles, barred posts) separated by a consistent slash divider. That memorandum cautioned that widely circulated images likely trace back to a single original drawing, and it advocated raking-light or RTI imaging before firm claims about medium, date or technique.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot;&amp;gt;Jan-Tage Kristiansen, “Twin renderings, single template: a ruled signary on a putative cervid scapula,” correspondence note, &#039;&#039;language&#039;&#039; 27 (October 2023): 1073–1074.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The Dozenal Primer Inscription is published only as a transcription in the Kristiansen coding scheme and as line drawings; no phonetic values or language affiliation are claimed in the 2024 analysis. Instead, the argument proceeds by methods common in [[Corpus linguistics]] and quantitative [[Epigraphy]]: (1) positional bias over initial/medial/final slots; (2) bigram inventories and pointwise mutual information to find unusually tight collocations; and (3) tests for morphological productivity at the right edge of clauses. In this model, the slash P01 behaves as bound punctuation confined to clause ends; an invariant 4-sign sequence occupies the medial spine and functions like an equals sign; a compact cluster acts as a binary addition operator; and a fixed four-sign bundle at the right edge derives “×12” forms. Complement constructions such as “11+1,” “10+2,” and “6+6” converge on the same dozen-marked targets, diagnosing 12 as the privileged base. A small number of clauses are read as scaling the same patterns to 12² and, by extension, to 12³.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The approach is intentionally agnostic about [[Language identification]] and about whether the glyphs are ultimately [[Logogram|logographic]], [[Syllabary|syllabic]] or something else. The claim is structural: that a small and rigid clause template—bound final punctuation, a fixed medial spine, operator clusters with narrow distribution, and a selective right-edge derivation—fits a didactic number register more parsimoniously than it fits a segmental writing system without additional evidence.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt; Proponents note that such “equation-like” formatting is well attested in practice texts and primers across historical [[Numeral system]]s, whereas critics point out that non-linguistic genres (lists, catalogues, tallies) can sometimes mimic grammatical structure.&lt;br /&gt;
&lt;br /&gt;
== Glyph inventory and distribution ==&lt;br /&gt;
&lt;br /&gt;
A compact distribution over the transcribed sign set (Kristiansen codes) is given below. Counts refer to the Dozenal Primer Inscription as published in line drawings/transcription; code labels are descriptive only and do not imply phonetic values.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable plainlist&amp;quot; style=&amp;quot;text-align:center;&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Code&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Glyph&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Name&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Freq&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Init&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Med&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Final&lt;br /&gt;
|-&lt;br /&gt;
| A01&lt;br /&gt;
| [[File:A01.svg|24px|alt=A01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-OPEN&lt;br /&gt;
| 5 || 2 || 3 || –&lt;br /&gt;
|-&lt;br /&gt;
| A02&lt;br /&gt;
| [[File:A02.svg|24px|alt=A02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-BAR&lt;br /&gt;
| 17 || 17 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| A03&lt;br /&gt;
| [[File:A03.svg|24px|alt=A03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-DOT&lt;br /&gt;
| 4 || – || 4 || –&lt;br /&gt;
|-&lt;br /&gt;
| A05&lt;br /&gt;
| [[File:A05.svg|24px|alt=A05|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-CLOSED-BAR&lt;br /&gt;
| 3 || – || 3 || –&lt;br /&gt;
|-&lt;br /&gt;
| B01&lt;br /&gt;
| [[File:B01.svg|24px|alt=B01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX&lt;br /&gt;
| 6 || 4 || 2 || –&lt;br /&gt;
|-&lt;br /&gt;
| B02&lt;br /&gt;
| [[File:B02.svg|24px|alt=B02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX-DOT&lt;br /&gt;
| 5 || 5 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| B04&lt;br /&gt;
| [[File:B04.svg|24px|alt=B04|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX-DIAG-SW-NE&lt;br /&gt;
| 5 || – || 3 || 2&lt;br /&gt;
|-&lt;br /&gt;
| C01&lt;br /&gt;
| [[File:C01.svg|24px|alt=C01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-NW&lt;br /&gt;
| 28 || 14 || – || 14&lt;br /&gt;
|-&lt;br /&gt;
| C02&lt;br /&gt;
| [[File:C02.svg|24px|alt=C02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-SE&lt;br /&gt;
| 27 || 27 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| C03&lt;br /&gt;
| [[File:C03.svg|24px|alt=C03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-SE-DOT&lt;br /&gt;
| 6 || 1 || 5 || –&lt;br /&gt;
|-&lt;br /&gt;
| C04&lt;br /&gt;
| [[File:C04.svg|24px|alt=C04|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-NW-DIAG&lt;br /&gt;
| 3 || – || 3 || –&lt;br /&gt;
|-&lt;br /&gt;
| C05&lt;br /&gt;
| [[File:C05.svg|24px|alt=C05|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-SE-DIAG&lt;br /&gt;
| 16 || 16 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| C06&lt;br /&gt;
| [[File:C06.svg|24px|alt=C06|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-NW-DIAG-DOT&lt;br /&gt;
| 7 || – || 7 || –&lt;br /&gt;
|-&lt;br /&gt;
| H01&lt;br /&gt;
| [[File:H01.svg|24px|alt=H01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | DOUBLE-POST-CONNECTED&lt;br /&gt;
| 15 || – || 15 || –&lt;br /&gt;
|-&lt;br /&gt;
| H02&lt;br /&gt;
| [[File:H02.svg|24px|alt=H02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | POST-DOUBLE-BAR&lt;br /&gt;
| 8 || – || 6 || 2&lt;br /&gt;
|-&lt;br /&gt;
| H03&lt;br /&gt;
| [[File:H03.svg|24px|alt=H03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | DOUBLE-POST-BAR&lt;br /&gt;
| 14 || – || 14 || –&lt;br /&gt;
|-&lt;br /&gt;
| L01&lt;br /&gt;
| [[File:L01.svg|24px|alt=L01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | LINE-VERT&lt;br /&gt;
| 24 || – || 8 || 16&lt;br /&gt;
|-&lt;br /&gt;
| M01&lt;br /&gt;
| [[File:M01.svg|24px|alt=M01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | COMB-2-N&lt;br /&gt;
| 20 || 3 || 16 || 1&lt;br /&gt;
|-&lt;br /&gt;
| M02&lt;br /&gt;
| [[File:M02.svg|24px|alt=M02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | COMB-2-W&lt;br /&gt;
| 52 || – || 52 || –&lt;br /&gt;
|-&lt;br /&gt;
| M03&lt;br /&gt;
| [[File:M03.svg|24px|alt=M03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | COMB-3-E&lt;br /&gt;
| 40 || – || 40 || –&lt;br /&gt;
|-&lt;br /&gt;
| P01&lt;br /&gt;
| [[File:P01.svg|24px|alt=P01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | SLASH&lt;br /&gt;
| 25 || – || – || 25&lt;br /&gt;
|-&lt;br /&gt;
| S02&lt;br /&gt;
| [[File:S02.svg|24px|alt=S02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | MEANDER-2-HOLLOW&lt;br /&gt;
| 9 || – || 3 || 6&lt;br /&gt;
|-&lt;br /&gt;
| S03&lt;br /&gt;
| [[File:S03.svg|24px|alt=S03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | MEANDER-3&lt;br /&gt;
| 39 || – || 37 || 2&lt;br /&gt;
|-&lt;br /&gt;
| T01&lt;br /&gt;
| [[File:T01.svg|24px|alt=T01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TEE-UP&lt;br /&gt;
| 7 || – || 4 || 3&lt;br /&gt;
|-&lt;br /&gt;
| T02&lt;br /&gt;
| [[File:T02.svg|24px|alt=T02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TEE-LEFT&lt;br /&gt;
| 30 || – || 23 || 7&lt;br /&gt;
|-&lt;br /&gt;
| T03&lt;br /&gt;
| [[File:T03.svg|24px|alt=T03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | UPSIDE-DOWN-TEE&lt;br /&gt;
| 38 || – || 19 || 19&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Proposed numeric forms and functional sequences ==&lt;br /&gt;
&lt;br /&gt;
Codes follow Kristiansen’s signary. Values are glosses for exposition only.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;. Coding scheme per Kristiansen.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable plainlist&amp;quot; style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Function (gloss)&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Codes (Kristiansen)&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Glyph sequence (icons)&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Notes&lt;br /&gt;
|-&lt;br /&gt;
| ZERO&lt;br /&gt;
| &amp;lt;code&amp;gt;B02-M03-L01-B05-S03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:B02.svg|20px|alt=B02|link=]][[File:M03.svg|20px|alt=M03|link=]][[File:L01.svg|20px|alt=L01|link=]][[File:B05.svg|20px|alt=B05|link=]][[File:S03.svg|20px|alt=S03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| ONE&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-S03-C03-T03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]][[File:S03.svg|20px|alt=S03|link=]][[File:C03.svg|20px|alt=C03|link=]][[File:T03.svg|20px|alt=T03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| TWO&lt;br /&gt;
| &amp;lt;code&amp;gt;C02-M02-H01-M01-T01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C02.svg|20px|alt=C02|link=]][[File:M02.svg|20px|alt=M02|link=]][[File:H01.svg|20px|alt=H01|link=]][[File:M01.svg|20px|alt=M01|link=]][[File:T01.svg|20px|alt=T01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| THREE&lt;br /&gt;
| &amp;lt;code&amp;gt;B01-M02-T02-A03-H02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:B01.svg|20px|alt=B01|link=]][[File:M02.svg|20px|alt=M02|link=]][[File:T02.svg|20px|alt=T02|link=]][[File:A03.svg|20px|alt=A03|link=]][[File:H02.svg|20px|alt=H02|link=]]&lt;br /&gt;
| &lt;br /&gt;
|-&lt;br /&gt;
| FOUR&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-M03-S02-C06-T02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]][[File:M03.svg|20px|alt=M03|link=]][[File:S02.svg|20px|alt=S02|link=]][[File:C06.svg|20px|alt=C06|link=]][[File:T02.svg|20px|alt=T02|link=]]&lt;br /&gt;
| &lt;br /&gt;
|-&lt;br /&gt;
| FIVE&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-M03-T02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]][[File:M03.svg|20px|alt=M03|link=]][[File:T02.svg|20px|alt=T02|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| SIX&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-B04-L01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]][[File:B04.svg|20px|alt=B04|link=]][[File:L01.svg|20px|alt=L01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| SEVEN&lt;br /&gt;
| &amp;lt;code&amp;gt;M01-L01-A01-T03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:M01.svg|20px|alt=M01|link=]][[File:L01.svg|20px|alt=L01|link=]][[File:A01.svg|20px|alt=A01|link=]][[File:T03.svg|20px|alt=T03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| EIGHT&lt;br /&gt;
| &amp;lt;code&amp;gt;B02-M03-S03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:B02.svg|20px|alt=B02|link=]][[File:M03.svg|20px|alt=M03|link=]][[File:S03.svg|20px|alt=S03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| NINE&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-M02-H02-A05-S02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]][[File:M02.svg|20px|alt=M02|link=]][[File:H02.svg|20px|alt=H02|link=]][[File:A05.svg|20px|alt=A05|link=]][[File:S02.svg|20px|alt=S02|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| TEN&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-M01-T02-M02-L01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]][[File:M01.svg|20px|alt=M01|link=]][[File:T02.svg|20px|alt=T02|link=]][[File:M02.svg|20px|alt=M02|link=]][[File:L01.svg|20px|alt=L01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| ELEVEN&lt;br /&gt;
| &amp;lt;code&amp;gt;A01-H01-B01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:A01.svg|20px|alt=A01|link=]][[File:H01.svg|20px|alt=H01|link=]][[File:B01.svg|20px|alt=B01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| TWELVE&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-M02-H02-B04&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]][[File:M02.svg|20px|alt=M02|link=]][[File:H02.svg|20px|alt=H02|link=]][[File:B04.svg|20px|alt=B04|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;EQUALS / clause spine&#039;&#039;&#039;&lt;br /&gt;
| &amp;lt;code&amp;gt;C02–M03–H03–C01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C02.svg|20px|alt=C02|link=]][[File:M03.svg|20px|alt=M03|link=]][[File:H03.svg|20px|alt=H03|link=]][[File:C01.svg|20px|alt=C01|link=]]&lt;br /&gt;
| Invariant medial sequence dividing left expression from result; reported as present in every equation clause. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PLUS (binary addition)&#039;&#039;&#039;&lt;br /&gt;
| &amp;lt;code&amp;gt;A02–L01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:A02.svg|20px|alt=A02|link=]][[File:L01.svg|20px|alt=L01|link=]]&lt;br /&gt;
| High-PMI operator cluster used in successor and complement constructions. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;MINUS (subtraction)&#039;&#039;&#039;&lt;br /&gt;
| &amp;lt;code&amp;gt;C05–M02–S02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]][[File:M02.svg|20px|alt=M02|link=]][[File:S02.svg|20px|alt=S02|link=]]&lt;br /&gt;
| Distinct collocational profile mirroring the syntax of the addition lines. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ONE (simplex unit)&#039;&#039;&#039;&lt;br /&gt;
| &amp;lt;code&amp;gt;C01–S03–C03–T03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]][[File:S03.svg|20px|alt=S03|link=]][[File:C03.svg|20px|alt=C03|link=]][[File:T03.svg|20px|alt=T03|link=]]&lt;br /&gt;
| Recurrent bundle functioning as the unit “one”; used to advance the successor chain. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PUNCTUATION (bound period)&#039;&#039;&#039;&lt;br /&gt;
| &amp;lt;code&amp;gt;P01&amp;lt;/code&amp;gt; (final only)&lt;br /&gt;
| [[File:P01.svg|20px|alt=P01|link=]]&lt;br /&gt;
| Clause-final slash behaving as bound punctuation; not attested internally. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;×12 derivation (“dozen”)&#039;&#039;&#039;&lt;br /&gt;
| –C01–M02–H02–B04 (right edge)&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]][[File:M02.svg|20px|alt=M02|link=]][[File:H02.svg|20px|alt=H02|link=]][[File:B04.svg|20px|alt=B04|link=]]&lt;br /&gt;
| Contiguous right-edge 4-gram treated as a derivational suffix forming multiples of twelve; composes with stems. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Scholarly discussion ==&lt;br /&gt;
Because the archaeological context of the longer inscription has not been publicly detailed, reception has been cautious. Supporters of the dozenal reading emphasise the conjunction of multiple independent diagnostics (final-only punctuation, a medial spine with near-zero variance, a selective right-edge four-gram, and complementary sums converging on the same target), arguing that chance alignment across all four is unlikely. Skeptics reply that without provenance, additional exemplars, or phonetic control, an arithmetic interpretation remains provisional and vulnerable to genre effects. Both sides agree on clear tests that could confirm or weaken the proposal: finding the slash P01 in non-final position; observing the putative ×12 four-gram away from clause edges; or documenting clauses that disrupt the medial spine while still behaving “arithmetically.”&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Nomenclature ==&lt;br /&gt;
“Dozenal Primer Inscription” is a descriptive convenience used in secondary discussion and is not a claim about ancient self-designation. Alternative labels in circulation include “Dozenal arithmetic inscription,” “Twelve-base arithmetic inscription,” and “Kristiansen-coded arithmetic inscription.” For clarity and disambiguation within encyclopedic contexts, the present title foregrounds the proposed function (primer) and base (dozenal).&lt;br /&gt;
&lt;br /&gt;
== Relation to the Scapula Glyph Inscription ==&lt;br /&gt;
The [[Scapula Glyph Inscription]] (KS-01) is much shorter but provided the stable sign labels and visual classes used in the transcription of the Dozenal Primer Inscription. KS-01 shows bipartite lines and a group-final slash divider consistent with the clause architecture proposed for the longer text, although by itself the scapula piece is too brief to display a full base-12 progression. Early notes on KS-01 also highlighted the need for direct examination and higher-quality imaging before drawing conclusions about material or chronology.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Duodecimal]]&lt;br /&gt;
* [[Numeral system]]&lt;br /&gt;
* [[Mathematical notation]]&lt;br /&gt;
* [[Undeciphered writing systems]]&lt;br /&gt;
* [[Epigraphy]]&lt;br /&gt;
* [[Paleography]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&amp;lt;references /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Klema]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=370</id>
		<title>Dozenal Primer Inscription</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=370"/>
		<updated>2025-10-29T18:50:52Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Proposed numeric forms and functional sequences */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Dozenal successor schema.png|thumb|The four examples render the successor chain 1→2, 2→3, 3→4, 4→5, demonstrating that adding the simplex unit “ONE” advances X to its next value while preserving clause structure. Codes follow Kristiansen’s signary; glyph labels (“ONE”, “TWO”, …) are semantic glosses for expository clarity and do not assume phonetic values.]]&lt;br /&gt;
The &#039;&#039;&#039;Dozenal Primer Inscription&#039;&#039;&#039; is the working name for a long geometric-glyph text proposed to encode a compact [[Duodecimal|dozenal]] (base-12) arithmetic register. The inscription is transcribed with a neutral code of sign classes (for example A01, B04, C03 and a slash-like divider P01) that mirrors the notation first used to document the shorter [[Scapula Glyph Inscription]] (KS-01). In 2024, a study argued from distributional evidence—rather than from phonetic values or a bilingual—that the text exhibits an equation-like clause structure with bound punctuation, a stereotyped medial “equals” spine, operator clusters, and a productive ×12 derivational suffix. On that account, the inscription functions as a brief primer for base-12 arithmetic, extending to compounds traditionally glossed as &#039;&#039;dozen&#039;&#039; (12×), &#039;&#039;gross&#039;&#039; (12²) and a tentative &#039;&#039;greatgross&#039;&#039; (12³).&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot;&amp;gt;Ginevra Rubergskier, “A dozenal primer hidden in plain sight: decoding arithmetic from a corpus of tagged tokens,” &#039;&#039;Language Codes&#039;&#039; 6 (2024): 820–824.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The sign code used to publish the Dozenal Primer Inscription derives from earlier internal documentation of KS-01, which described short ruled lines, rectilinear glyphs built from a limited set of straight strokes, and visually coherent families (corners, boxes, triangles, barred posts) separated by a consistent slash divider. That memorandum cautioned that widely circulated images likely trace back to a single original drawing, and it advocated raking-light or RTI imaging before firm claims about medium, date or technique.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot;&amp;gt;Jan-Tage Kristiansen, “Twin renderings, single template: a ruled signary on a putative cervid scapula,” correspondence note, &#039;&#039;language&#039;&#039; 27 (October 2023): 1073–1074.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The Dozenal Primer Inscription is published only as a transcription in the Kristiansen coding scheme and as line drawings; no phonetic values or language affiliation are claimed in the 2024 analysis. Instead, the argument proceeds by methods common in [[Corpus linguistics]] and quantitative [[Epigraphy]]: (1) positional bias over initial/medial/final slots; (2) bigram inventories and pointwise mutual information to find unusually tight collocations; and (3) tests for morphological productivity at the right edge of clauses. In this model, the slash P01 behaves as bound punctuation confined to clause ends; an invariant 4-sign sequence occupies the medial spine and functions like an equals sign; a compact cluster acts as a binary addition operator; and a fixed four-sign bundle at the right edge derives “×12” forms. Complement constructions such as “11+1,” “10+2,” and “6+6” converge on the same dozen-marked targets, diagnosing 12 as the privileged base. A small number of clauses are read as scaling the same patterns to 12² and, by extension, to 12³.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The approach is intentionally agnostic about [[Language identification]] and about whether the glyphs are ultimately [[Logogram|logographic]], [[Syllabary|syllabic]] or something else. The claim is structural: that a small and rigid clause template—bound final punctuation, a fixed medial spine, operator clusters with narrow distribution, and a selective right-edge derivation—fits a didactic number register more parsimoniously than it fits a segmental writing system without additional evidence.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt; Proponents note that such “equation-like” formatting is well attested in practice texts and primers across historical [[Numeral system]]s, whereas critics point out that non-linguistic genres (lists, catalogues, tallies) can sometimes mimic grammatical structure.&lt;br /&gt;
&lt;br /&gt;
== Glyph inventory and distribution ==&lt;br /&gt;
&lt;br /&gt;
A compact distribution over the transcribed sign set (Kristiansen codes) is given below. Counts refer to the Dozenal Primer Inscription as published in line drawings/transcription; code labels are descriptive only and do not imply phonetic values.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable plainlist&amp;quot; style=&amp;quot;text-align:center;&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Code&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Glyph&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Name&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Freq&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Init&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Med&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Final&lt;br /&gt;
|-&lt;br /&gt;
| A01&lt;br /&gt;
| [[File:A01.svg|24px|alt=A01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-OPEN&lt;br /&gt;
| 5 || 2 || 3 || –&lt;br /&gt;
|-&lt;br /&gt;
| A02&lt;br /&gt;
| [[File:A02.svg|24px|alt=A02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-BAR&lt;br /&gt;
| 17 || 17 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| A03&lt;br /&gt;
| [[File:A03.svg|24px|alt=A03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-DOT&lt;br /&gt;
| 4 || – || 4 || –&lt;br /&gt;
|-&lt;br /&gt;
| A05&lt;br /&gt;
| [[File:A05.svg|24px|alt=A05|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-CLOSED-BAR&lt;br /&gt;
| 3 || – || 3 || –&lt;br /&gt;
|-&lt;br /&gt;
| B01&lt;br /&gt;
| [[File:B01.svg|24px|alt=B01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX&lt;br /&gt;
| 6 || 4 || 2 || –&lt;br /&gt;
|-&lt;br /&gt;
| B02&lt;br /&gt;
| [[File:B02.svg|24px|alt=B02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX-DOT&lt;br /&gt;
| 5 || 5 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| B04&lt;br /&gt;
| [[File:B04.svg|24px|alt=B04|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX-DIAG-SW-NE&lt;br /&gt;
| 5 || – || 3 || 2&lt;br /&gt;
|-&lt;br /&gt;
| C01&lt;br /&gt;
| [[File:C01.svg|24px|alt=C01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-NW&lt;br /&gt;
| 28 || 14 || – || 14&lt;br /&gt;
|-&lt;br /&gt;
| C02&lt;br /&gt;
| [[File:C02.svg|24px|alt=C02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-SE&lt;br /&gt;
| 27 || 27 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| C03&lt;br /&gt;
| [[File:C03.svg|24px|alt=C03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-SE-DOT&lt;br /&gt;
| 6 || 1 || 5 || –&lt;br /&gt;
|-&lt;br /&gt;
| C04&lt;br /&gt;
| [[File:C04.svg|24px|alt=C04|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-NW-DIAG&lt;br /&gt;
| 3 || – || 3 || –&lt;br /&gt;
|-&lt;br /&gt;
| C05&lt;br /&gt;
| [[File:C05.svg|24px|alt=C05|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-SE-DIAG&lt;br /&gt;
| 16 || 16 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| C06&lt;br /&gt;
| [[File:C06.svg|24px|alt=C06|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-NW-DIAG-DOT&lt;br /&gt;
| 7 || – || 7 || –&lt;br /&gt;
|-&lt;br /&gt;
| H01&lt;br /&gt;
| [[File:H01.svg|24px|alt=H01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | DOUBLE-POST-CONNECTED&lt;br /&gt;
| 15 || – || 15 || –&lt;br /&gt;
|-&lt;br /&gt;
| H02&lt;br /&gt;
| [[File:H02.svg|24px|alt=H02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | POST-DOUBLE-BAR&lt;br /&gt;
| 8 || – || 6 || 2&lt;br /&gt;
|-&lt;br /&gt;
| H03&lt;br /&gt;
| [[File:H03.svg|24px|alt=H03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | DOUBLE-POST-BAR&lt;br /&gt;
| 14 || – || 14 || –&lt;br /&gt;
|-&lt;br /&gt;
| L01&lt;br /&gt;
| [[File:L01.svg|24px|alt=L01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | LINE-VERT&lt;br /&gt;
| 24 || – || 8 || 16&lt;br /&gt;
|-&lt;br /&gt;
| M01&lt;br /&gt;
| [[File:M01.svg|24px|alt=M01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | COMB-2-N&lt;br /&gt;
| 20 || 3 || 16 || 1&lt;br /&gt;
|-&lt;br /&gt;
| M02&lt;br /&gt;
| [[File:M02.svg|24px|alt=M02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | COMB-2-W&lt;br /&gt;
| 52 || – || 52 || –&lt;br /&gt;
|-&lt;br /&gt;
| M03&lt;br /&gt;
| [[File:M03.svg|24px|alt=M03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | COMB-3-E&lt;br /&gt;
| 40 || – || 40 || –&lt;br /&gt;
|-&lt;br /&gt;
| P01&lt;br /&gt;
| [[File:P01.svg|24px|alt=P01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | SLASH&lt;br /&gt;
| 25 || – || – || 25&lt;br /&gt;
|-&lt;br /&gt;
| S02&lt;br /&gt;
| [[File:S02.svg|24px|alt=S02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | MEANDER-2-HOLLOW&lt;br /&gt;
| 9 || – || 3 || 6&lt;br /&gt;
|-&lt;br /&gt;
| S03&lt;br /&gt;
| [[File:S03.svg|24px|alt=S03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | MEANDER-3&lt;br /&gt;
| 39 || – || 37 || 2&lt;br /&gt;
|-&lt;br /&gt;
| T01&lt;br /&gt;
| [[File:T01.svg|24px|alt=T01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TEE-UP&lt;br /&gt;
| 7 || – || 4 || 3&lt;br /&gt;
|-&lt;br /&gt;
| T02&lt;br /&gt;
| [[File:T02.svg|24px|alt=T02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TEE-LEFT&lt;br /&gt;
| 30 || – || 23 || 7&lt;br /&gt;
|-&lt;br /&gt;
| T03&lt;br /&gt;
| [[File:T03.svg|24px|alt=T03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | UPSIDE-DOWN-TEE&lt;br /&gt;
| 38 || – || 19 || 19&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Proposed numeric forms and functional sequences ==&lt;br /&gt;
&lt;br /&gt;
Codes follow Kristiansen’s signary. Values are glosses for exposition only.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;. Coding scheme per Kristiansen.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable plainlist&amp;quot; style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Function (gloss)&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Codes (Kristiansen)&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Glyph sequence (icons)&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Notes&lt;br /&gt;
|-&lt;br /&gt;
| ZERO&lt;br /&gt;
| &amp;lt;code&amp;gt;B02-M03-L01-B05-S03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:B02.svg|20px|alt=B02|link=]][[File:M03.svg|20px|alt=M03|link=]][[File:L01.svg|20px|alt=L01|link=]][[File:B05.svg|20px|alt=B05|link=]][[File:S03.svg|20px|alt=S03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| ONE&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-S03-C03-T03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]][[File:S03.svg|20px|alt=S03|link=]][[File:C03.svg|20px|alt=C03|link=]][[File:T03.svg|20px|alt=T03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| TWO&lt;br /&gt;
| &amp;lt;code&amp;gt;C02-M02-H01-M01-T01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C02.svg|20px|alt=C02|link=]][[File:M02.svg|20px|alt=M02|link=]][[File:H01.svg|20px|alt=H01|link=]][[File:M01.svg|20px|alt=M01|link=]][[File:T01.svg|20px|alt=T01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| THREE&lt;br /&gt;
| &amp;lt;code&amp;gt;B01-M02-T02-A03-H02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:B01.svg|20px|alt=B01|link=]][[File:M02.svg|20px|alt=M02|link=]][[File:T02.svg|20px|alt=T02|link=]][[File:A03.svg|20px|alt=A03|link=]][[File:H02.svg|20px|alt=H02|link=]]&lt;br /&gt;
| &lt;br /&gt;
|-&lt;br /&gt;
| FOUR&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-M03-S02-C06-T02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]][[File:M03.svg|20px|alt=M03|link=]][[File:S02.svg|20px|alt=S02|link=]][[File:C06.svg|20px|alt=C06|link=]][[File:T02.svg|20px|alt=T02|link=]]&lt;br /&gt;
| &lt;br /&gt;
|-&lt;br /&gt;
| FIVE&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-M03-T02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]][[File:M03.svg|20px|alt=M03|link=]][[File:T02.svg|20px|alt=T02|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| SIX&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-B04-L01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]][[File:B04.svg|20px|alt=B04|link=]][[File:L01.svg|20px|alt=L01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| SEVEN&lt;br /&gt;
| &amp;lt;code&amp;gt;M01-L01-A01-T03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:M01.svg|20px|alt=M01|link=]][[File:L01.svg|20px|alt=L01|link=]][[File:A01.svg|20px|alt=A01|link=]][[File:T03.svg|20px|alt=T03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| EIGHT&lt;br /&gt;
| &amp;lt;code&amp;gt;B02-M03-S03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:B02.svg|20px|alt=B02|link=]][[File:M03.svg|20px|alt=M03|link=]][[File:S03.svg|20px|alt=S03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| NINE&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-M02-H02-A05-S02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]][[File:M02.svg|20px|alt=M02|link=]][[File:H02.svg|20px|alt=H02|link=]][[File:A05.svg|20px|alt=A05|link=]][[File:S02.svg|20px|alt=S02|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| TEN&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-M01-T02-M02-L01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]][[File:M01.svg|20px|alt=M01|link=]][[File:T02.svg|20px|alt=T02|link=]][[File:M02.svg|20px|alt=M02|link=]][[File:L01.svg|20px|alt=L01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| ELEVEN&lt;br /&gt;
| &amp;lt;code&amp;gt;A01-H01-B01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:A01.svg|20px|alt=A01|link=]][[File:H01.svg|20px|alt=H01|link=]][[File:B01.svg|20px|alt=B01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| TWELVE&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-M02-H02-B04&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]][[File:M02.svg|20px|alt=M02|link=]][[File:H02.svg|20px|alt=H02|link=]][[File:B04.svg|20px|alt=B04|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;EQUALS / clause spine&#039;&#039;&#039;&lt;br /&gt;
| &amp;lt;code&amp;gt;C02–M03–H03–C01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C02.svg|20px|alt=C02|link=]][[File:M03.svg|20px|alt=M03|link=]][[File:H03.svg|20px|alt=H03|link=]][[File:C01.svg|20px|alt=C01|link=]]&lt;br /&gt;
| Invariant medial sequence dividing left expression from result; reported as present in every equation clause. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PLUS (binary addition)&#039;&#039;&#039;&lt;br /&gt;
| &amp;lt;code&amp;gt;A02–L01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:A02.svg|20px|alt=A02|link=]][[File:L01.svg|20px|alt=L01|link=]]&lt;br /&gt;
| High-PMI operator cluster used in successor and complement constructions. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;MINUS (subtraction)&#039;&#039;&#039;&lt;br /&gt;
| &amp;lt;code&amp;gt;C05–M02–S02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]][[File:M02.svg|20px|alt=M02|link=]][[File:S02.svg|20px|alt=S02|link=]]&lt;br /&gt;
| Distinct collocational profile mirroring the syntax of the addition lines. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ONE (simplex unit)&#039;&#039;&#039;&lt;br /&gt;
| &amp;lt;code&amp;gt;C01–S03–C03–T03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]][[File:S03.svg|20px|alt=S03|link=]][[File:C03.svg|20px|alt=C03|link=]][[File:T03.svg|20px|alt=T03|link=]]&lt;br /&gt;
| Recurrent bundle functioning as the unit “one”; used to advance the successor chain. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PUNCTUATION (bound period)&#039;&#039;&#039;&lt;br /&gt;
| &amp;lt;code&amp;gt;P01&amp;lt;/code&amp;gt; (final only)&lt;br /&gt;
| [[File:P01.svg|20px|alt=P01|link=]]&lt;br /&gt;
| Clause-final slash behaving as bound punctuation; not attested internally. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;×12 derivation (“dozen”)&#039;&#039;&#039;&lt;br /&gt;
| –C01–M02–H02–B04 (right edge)&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]][[File:M02.svg|20px|alt=M02|link=]][[File:H02.svg|20px|alt=H02|link=]][[File:B04.svg|20px|alt=B04|link=]]&lt;br /&gt;
| Contiguous right-edge 4-gram treated as a derivational suffix forming multiples of twelve; composes with stems. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Scholarly discussion ==&lt;br /&gt;
Because the archaeological context of the longer inscription has not been publicly detailed, reception has been cautious. Supporters of the dozenal reading emphasise the conjunction of multiple independent diagnostics (final-only punctuation, a medial spine with near-zero variance, a selective right-edge four-gram, and complementary sums converging on the same target), arguing that chance alignment across all four is unlikely. Skeptics reply that without provenance, additional exemplars, or phonetic control, an arithmetic interpretation remains provisional and vulnerable to genre effects. Both sides agree on clear tests that could confirm or weaken the proposal: finding the slash P01 in non-final position; observing the putative ×12 four-gram away from clause edges; or documenting clauses that disrupt the medial spine while still behaving “arithmetically.”&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Nomenclature ==&lt;br /&gt;
“Dozenal Primer Inscription” is a descriptive convenience used in secondary discussion and is not a claim about ancient self-designation. Alternative labels in circulation include “Dozenal arithmetic inscription,” “Twelve-base arithmetic inscription,” and “Kristiansen-coded arithmetic inscription.” For clarity and disambiguation within encyclopedic contexts, the present title foregrounds the proposed function (primer) and base (dozenal).&lt;br /&gt;
&lt;br /&gt;
== Relation to the Scapula Glyph Inscription ==&lt;br /&gt;
The [[Scapula Glyph Inscription]] (KS-01) is much shorter but provided the stable sign labels and visual classes used in the transcription of the Dozenal Primer Inscription. KS-01 shows bipartite lines and a group-final slash divider consistent with the clause architecture proposed for the longer text, although by itself the scapula piece is too brief to display a full base-12 progression. Early notes on KS-01 also highlighted the need for direct examination and higher-quality imaging before drawing conclusions about material or chronology.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Duodecimal]]&lt;br /&gt;
* [[Numeral system]]&lt;br /&gt;
* [[Mathematical notation]]&lt;br /&gt;
* [[Undeciphered writing systems]]&lt;br /&gt;
* [[Epigraphy]]&lt;br /&gt;
* [[Paleography]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&amp;lt;references /&amp;gt;&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=369</id>
		<title>Dozenal Primer Inscription</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=369"/>
		<updated>2025-10-29T18:49:19Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Proposed numeric forms and functional sequences */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Dozenal successor schema.png|thumb|The four examples render the successor chain 1→2, 2→3, 3→4, 4→5, demonstrating that adding the simplex unit “ONE” advances X to its next value while preserving clause structure. Codes follow Kristiansen’s signary; glyph labels (“ONE”, “TWO”, …) are semantic glosses for expository clarity and do not assume phonetic values.]]&lt;br /&gt;
The &#039;&#039;&#039;Dozenal Primer Inscription&#039;&#039;&#039; is the working name for a long geometric-glyph text proposed to encode a compact [[Duodecimal|dozenal]] (base-12) arithmetic register. The inscription is transcribed with a neutral code of sign classes (for example A01, B04, C03 and a slash-like divider P01) that mirrors the notation first used to document the shorter [[Scapula Glyph Inscription]] (KS-01). In 2024, a study argued from distributional evidence—rather than from phonetic values or a bilingual—that the text exhibits an equation-like clause structure with bound punctuation, a stereotyped medial “equals” spine, operator clusters, and a productive ×12 derivational suffix. On that account, the inscription functions as a brief primer for base-12 arithmetic, extending to compounds traditionally glossed as &#039;&#039;dozen&#039;&#039; (12×), &#039;&#039;gross&#039;&#039; (12²) and a tentative &#039;&#039;greatgross&#039;&#039; (12³).&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot;&amp;gt;Ginevra Rubergskier, “A dozenal primer hidden in plain sight: decoding arithmetic from a corpus of tagged tokens,” &#039;&#039;Language Codes&#039;&#039; 6 (2024): 820–824.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The sign code used to publish the Dozenal Primer Inscription derives from earlier internal documentation of KS-01, which described short ruled lines, rectilinear glyphs built from a limited set of straight strokes, and visually coherent families (corners, boxes, triangles, barred posts) separated by a consistent slash divider. That memorandum cautioned that widely circulated images likely trace back to a single original drawing, and it advocated raking-light or RTI imaging before firm claims about medium, date or technique.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot;&amp;gt;Jan-Tage Kristiansen, “Twin renderings, single template: a ruled signary on a putative cervid scapula,” correspondence note, &#039;&#039;language&#039;&#039; 27 (October 2023): 1073–1074.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The Dozenal Primer Inscription is published only as a transcription in the Kristiansen coding scheme and as line drawings; no phonetic values or language affiliation are claimed in the 2024 analysis. Instead, the argument proceeds by methods common in [[Corpus linguistics]] and quantitative [[Epigraphy]]: (1) positional bias over initial/medial/final slots; (2) bigram inventories and pointwise mutual information to find unusually tight collocations; and (3) tests for morphological productivity at the right edge of clauses. In this model, the slash P01 behaves as bound punctuation confined to clause ends; an invariant 4-sign sequence occupies the medial spine and functions like an equals sign; a compact cluster acts as a binary addition operator; and a fixed four-sign bundle at the right edge derives “×12” forms. Complement constructions such as “11+1,” “10+2,” and “6+6” converge on the same dozen-marked targets, diagnosing 12 as the privileged base. A small number of clauses are read as scaling the same patterns to 12² and, by extension, to 12³.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The approach is intentionally agnostic about [[Language identification]] and about whether the glyphs are ultimately [[Logogram|logographic]], [[Syllabary|syllabic]] or something else. The claim is structural: that a small and rigid clause template—bound final punctuation, a fixed medial spine, operator clusters with narrow distribution, and a selective right-edge derivation—fits a didactic number register more parsimoniously than it fits a segmental writing system without additional evidence.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt; Proponents note that such “equation-like” formatting is well attested in practice texts and primers across historical [[Numeral system]]s, whereas critics point out that non-linguistic genres (lists, catalogues, tallies) can sometimes mimic grammatical structure.&lt;br /&gt;
&lt;br /&gt;
== Glyph inventory and distribution ==&lt;br /&gt;
&lt;br /&gt;
A compact distribution over the transcribed sign set (Kristiansen codes) is given below. Counts refer to the Dozenal Primer Inscription as published in line drawings/transcription; code labels are descriptive only and do not imply phonetic values.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable plainlist&amp;quot; style=&amp;quot;text-align:center;&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Code&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Glyph&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Name&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Freq&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Init&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Med&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Final&lt;br /&gt;
|-&lt;br /&gt;
| A01&lt;br /&gt;
| [[File:A01.svg|24px|alt=A01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-OPEN&lt;br /&gt;
| 5 || 2 || 3 || –&lt;br /&gt;
|-&lt;br /&gt;
| A02&lt;br /&gt;
| [[File:A02.svg|24px|alt=A02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-BAR&lt;br /&gt;
| 17 || 17 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| A03&lt;br /&gt;
| [[File:A03.svg|24px|alt=A03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-DOT&lt;br /&gt;
| 4 || – || 4 || –&lt;br /&gt;
|-&lt;br /&gt;
| A05&lt;br /&gt;
| [[File:A05.svg|24px|alt=A05|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-CLOSED-BAR&lt;br /&gt;
| 3 || – || 3 || –&lt;br /&gt;
|-&lt;br /&gt;
| B01&lt;br /&gt;
| [[File:B01.svg|24px|alt=B01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX&lt;br /&gt;
| 6 || 4 || 2 || –&lt;br /&gt;
|-&lt;br /&gt;
| B02&lt;br /&gt;
| [[File:B02.svg|24px|alt=B02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX-DOT&lt;br /&gt;
| 5 || 5 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| B04&lt;br /&gt;
| [[File:B04.svg|24px|alt=B04|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX-DIAG-SW-NE&lt;br /&gt;
| 5 || – || 3 || 2&lt;br /&gt;
|-&lt;br /&gt;
| C01&lt;br /&gt;
| [[File:C01.svg|24px|alt=C01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-NW&lt;br /&gt;
| 28 || 14 || – || 14&lt;br /&gt;
|-&lt;br /&gt;
| C02&lt;br /&gt;
| [[File:C02.svg|24px|alt=C02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-SE&lt;br /&gt;
| 27 || 27 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| C03&lt;br /&gt;
| [[File:C03.svg|24px|alt=C03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-SE-DOT&lt;br /&gt;
| 6 || 1 || 5 || –&lt;br /&gt;
|-&lt;br /&gt;
| C04&lt;br /&gt;
| [[File:C04.svg|24px|alt=C04|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-NW-DIAG&lt;br /&gt;
| 3 || – || 3 || –&lt;br /&gt;
|-&lt;br /&gt;
| C05&lt;br /&gt;
| [[File:C05.svg|24px|alt=C05|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-SE-DIAG&lt;br /&gt;
| 16 || 16 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| C06&lt;br /&gt;
| [[File:C06.svg|24px|alt=C06|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-NW-DIAG-DOT&lt;br /&gt;
| 7 || – || 7 || –&lt;br /&gt;
|-&lt;br /&gt;
| H01&lt;br /&gt;
| [[File:H01.svg|24px|alt=H01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | DOUBLE-POST-CONNECTED&lt;br /&gt;
| 15 || – || 15 || –&lt;br /&gt;
|-&lt;br /&gt;
| H02&lt;br /&gt;
| [[File:H02.svg|24px|alt=H02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | POST-DOUBLE-BAR&lt;br /&gt;
| 8 || – || 6 || 2&lt;br /&gt;
|-&lt;br /&gt;
| H03&lt;br /&gt;
| [[File:H03.svg|24px|alt=H03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | DOUBLE-POST-BAR&lt;br /&gt;
| 14 || – || 14 || –&lt;br /&gt;
|-&lt;br /&gt;
| L01&lt;br /&gt;
| [[File:L01.svg|24px|alt=L01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | LINE-VERT&lt;br /&gt;
| 24 || – || 8 || 16&lt;br /&gt;
|-&lt;br /&gt;
| M01&lt;br /&gt;
| [[File:M01.svg|24px|alt=M01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | COMB-2-N&lt;br /&gt;
| 20 || 3 || 16 || 1&lt;br /&gt;
|-&lt;br /&gt;
| M02&lt;br /&gt;
| [[File:M02.svg|24px|alt=M02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | COMB-2-W&lt;br /&gt;
| 52 || – || 52 || –&lt;br /&gt;
|-&lt;br /&gt;
| M03&lt;br /&gt;
| [[File:M03.svg|24px|alt=M03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | COMB-3-E&lt;br /&gt;
| 40 || – || 40 || –&lt;br /&gt;
|-&lt;br /&gt;
| P01&lt;br /&gt;
| [[File:P01.svg|24px|alt=P01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | SLASH&lt;br /&gt;
| 25 || – || – || 25&lt;br /&gt;
|-&lt;br /&gt;
| S02&lt;br /&gt;
| [[File:S02.svg|24px|alt=S02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | MEANDER-2-HOLLOW&lt;br /&gt;
| 9 || – || 3 || 6&lt;br /&gt;
|-&lt;br /&gt;
| S03&lt;br /&gt;
| [[File:S03.svg|24px|alt=S03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | MEANDER-3&lt;br /&gt;
| 39 || – || 37 || 2&lt;br /&gt;
|-&lt;br /&gt;
| T01&lt;br /&gt;
| [[File:T01.svg|24px|alt=T01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TEE-UP&lt;br /&gt;
| 7 || – || 4 || 3&lt;br /&gt;
|-&lt;br /&gt;
| T02&lt;br /&gt;
| [[File:T02.svg|24px|alt=T02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TEE-LEFT&lt;br /&gt;
| 30 || – || 23 || 7&lt;br /&gt;
|-&lt;br /&gt;
| T03&lt;br /&gt;
| [[File:T03.svg|24px|alt=T03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | UPSIDE-DOWN-TEE&lt;br /&gt;
| 38 || – || 19 || 19&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Proposed numeric forms and functional sequences ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable plainlist&amp;quot; style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Function (gloss)&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Codes (Kristiansen)&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Glyph sequence (icons)&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Notes&lt;br /&gt;
|-&lt;br /&gt;
| ZERO&lt;br /&gt;
| &amp;lt;code&amp;gt;B02-M03-L01-B05-S03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:B02.svg|20px|alt=B02|link=]] [[File:M03.svg|20px|alt=M03|link=]] [[File:L01.svg|20px|alt=L01|link=]] [[File:B05.svg|20px|alt=B05|link=]] [[File:S03.svg|20px|alt=S03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| ONE&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-S03-C03-T03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]] [[File:S03.svg|20px|alt=S03|link=]] [[File:C03.svg|20px|alt=C03|link=]] [[File:T03.svg|20px|alt=T03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| TWO&lt;br /&gt;
| &amp;lt;code&amp;gt;C02-M02-H01-M01-T01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C02.svg|20px|alt=C02|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:H01.svg|20px|alt=H01|link=]] [[File:M01.svg|20px|alt=M01|link=]] [[File:T01.svg|20px|alt=T01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| THREE&lt;br /&gt;
| &amp;lt;code&amp;gt;B01-M02-T02-A03-H02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:B01.svg|20px|alt=B01|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:T02.svg|20px|alt=T02|link=]] [[File:A03.svg|20px|alt=A03|link=]] [[File:H02.svg|20px|alt=H02|link=]]&lt;br /&gt;
| &lt;br /&gt;
|-&lt;br /&gt;
| FOUR&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-M03-S02-C06-T02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]] [[File:M03.svg|20px|alt=M03|link=]] [[File:S02.svg|20px|alt=S02|link=]] [[File:C06.svg|20px|alt=C06|link=]] [[File:T02.svg|20px|alt=T02|link=]]&lt;br /&gt;
| &lt;br /&gt;
|-&lt;br /&gt;
| FIVE&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-M03-T02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]] [[File:M03.svg|20px|alt=M03|link=]] [[File:T02.svg|20px|alt=T02|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| SIX&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-B04-L01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]] [[File:B04.svg|20px|alt=B04|link=]] [[File:L01.svg|20px|alt=L01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| SEVEN&lt;br /&gt;
| &amp;lt;code&amp;gt;M01-L01-A01-T03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:M01.svg|20px|alt=M01|link=]] [[File:L01.svg|20px|alt=L01|link=]] [[File:A01.svg|20px|alt=A01|link=]] [[File:T03.svg|20px|alt=T03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| EIGHT&lt;br /&gt;
| &amp;lt;code&amp;gt;B02-M03-S03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:B02.svg|20px|alt=B02|link=]] [[File:M03.svg|20px|alt=M03|link=]] [[File:S03.svg|20px|alt=S03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| NINE&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-M02-H02-A05-S02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:H02.svg|20px|alt=H02|link=]] [[File:A05.svg|20px|alt=A05|link=]] [[File:S02.svg|20px|alt=S02|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| TEN&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-M01-T02-M02-L01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]] [[File:M01.svg|20px|alt=M01|link=]] [[File:T02.svg|20px|alt=T02|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:L01.svg|20px|alt=L01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| ELEVEN&lt;br /&gt;
| &amp;lt;code&amp;gt;A01-H01-B01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:A01.svg|20px|alt=A01|link=]] [[File:H01.svg|20px|alt=H01|link=]] [[File:B01.svg|20px|alt=B01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| TWELVE&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-M02-H02-B04&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:H02.svg|20px|alt=H02|link=]] [[File:B04.svg|20px|alt=B04|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;EQUALS / clause spine&#039;&#039;&#039;&lt;br /&gt;
| &amp;lt;code&amp;gt;C02–M03–H03–C01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C02.svg|20px|alt=C02|link=]] [[File:M03.svg|20px|alt=M03|link=]] [[File:H03.svg|20px|alt=H03|link=]] [[File:C01.svg|20px|alt=C01|link=]]&lt;br /&gt;
| Invariant medial sequence dividing left expression from result; reported as present in every equation clause. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PLUS (binary addition)&#039;&#039;&#039;&lt;br /&gt;
| &amp;lt;code&amp;gt;A02–L01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:A02.svg|20px|alt=A02|link=]] [[File:L01.svg|20px|alt=L01|link=]]&lt;br /&gt;
| High-PMI operator cluster used in successor and complement constructions. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;MINUS (subtraction)&#039;&#039;&#039;&lt;br /&gt;
| &amp;lt;code&amp;gt;C05–M02–S02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:S02.svg|20px|alt=S02|link=]]&lt;br /&gt;
| Distinct collocational profile mirroring the syntax of the addition lines. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ONE (simplex unit)&#039;&#039;&#039;&lt;br /&gt;
| &amp;lt;code&amp;gt;C01–S03–C03–T03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]] [[File:S03.svg|20px|alt=S03|link=]] [[File:C03.svg|20px|alt=C03|link=]] [[File:T03.svg|20px|alt=T03|link=]]&lt;br /&gt;
| Recurrent bundle functioning as the unit “one”; used to advance the successor chain. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PUNCTUATION (bound period)&#039;&#039;&#039;&lt;br /&gt;
| &amp;lt;code&amp;gt;P01&amp;lt;/code&amp;gt; (final only)&lt;br /&gt;
| [[File:P01.svg|20px|alt=P01|link=]]&lt;br /&gt;
| Clause-final slash behaving as bound punctuation; not attested internally. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;×12 derivation (“dozen”)&#039;&#039;&#039;&lt;br /&gt;
| –C01–M02–H02–B04 (right edge)&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:H02.svg|20px|alt=H02|link=]] [[File:B04.svg|20px|alt=B04|link=]]&lt;br /&gt;
| Contiguous right-edge 4-gram treated as a derivational suffix forming multiples of twelve; composes with stems. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Scholarly discussion ==&lt;br /&gt;
Because the archaeological context of the longer inscription has not been publicly detailed, reception has been cautious. Supporters of the dozenal reading emphasise the conjunction of multiple independent diagnostics (final-only punctuation, a medial spine with near-zero variance, a selective right-edge four-gram, and complementary sums converging on the same target), arguing that chance alignment across all four is unlikely. Skeptics reply that without provenance, additional exemplars, or phonetic control, an arithmetic interpretation remains provisional and vulnerable to genre effects. Both sides agree on clear tests that could confirm or weaken the proposal: finding the slash P01 in non-final position; observing the putative ×12 four-gram away from clause edges; or documenting clauses that disrupt the medial spine while still behaving “arithmetically.”&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Nomenclature ==&lt;br /&gt;
“Dozenal Primer Inscription” is a descriptive convenience used in secondary discussion and is not a claim about ancient self-designation. Alternative labels in circulation include “Dozenal arithmetic inscription,” “Twelve-base arithmetic inscription,” and “Kristiansen-coded arithmetic inscription.” For clarity and disambiguation within encyclopedic contexts, the present title foregrounds the proposed function (primer) and base (dozenal).&lt;br /&gt;
&lt;br /&gt;
== Relation to the Scapula Glyph Inscription ==&lt;br /&gt;
The [[Scapula Glyph Inscription]] (KS-01) is much shorter but provided the stable sign labels and visual classes used in the transcription of the Dozenal Primer Inscription. KS-01 shows bipartite lines and a group-final slash divider consistent with the clause architecture proposed for the longer text, although by itself the scapula piece is too brief to display a full base-12 progression. Early notes on KS-01 also highlighted the need for direct examination and higher-quality imaging before drawing conclusions about material or chronology.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Duodecimal]]&lt;br /&gt;
* [[Numeral system]]&lt;br /&gt;
* [[Mathematical notation]]&lt;br /&gt;
* [[Undeciphered writing systems]]&lt;br /&gt;
* [[Epigraphy]]&lt;br /&gt;
* [[Paleography]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&amp;lt;references /&amp;gt;&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=368</id>
		<title>Dozenal Primer Inscription</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=368"/>
		<updated>2025-10-29T18:48:00Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Proposed numeric forms and functional sequences */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Dozenal successor schema.png|thumb|The four examples render the successor chain 1→2, 2→3, 3→4, 4→5, demonstrating that adding the simplex unit “ONE” advances X to its next value while preserving clause structure. Codes follow Kristiansen’s signary; glyph labels (“ONE”, “TWO”, …) are semantic glosses for expository clarity and do not assume phonetic values.]]&lt;br /&gt;
The &#039;&#039;&#039;Dozenal Primer Inscription&#039;&#039;&#039; is the working name for a long geometric-glyph text proposed to encode a compact [[Duodecimal|dozenal]] (base-12) arithmetic register. The inscription is transcribed with a neutral code of sign classes (for example A01, B04, C03 and a slash-like divider P01) that mirrors the notation first used to document the shorter [[Scapula Glyph Inscription]] (KS-01). In 2024, a study argued from distributional evidence—rather than from phonetic values or a bilingual—that the text exhibits an equation-like clause structure with bound punctuation, a stereotyped medial “equals” spine, operator clusters, and a productive ×12 derivational suffix. On that account, the inscription functions as a brief primer for base-12 arithmetic, extending to compounds traditionally glossed as &#039;&#039;dozen&#039;&#039; (12×), &#039;&#039;gross&#039;&#039; (12²) and a tentative &#039;&#039;greatgross&#039;&#039; (12³).&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot;&amp;gt;Ginevra Rubergskier, “A dozenal primer hidden in plain sight: decoding arithmetic from a corpus of tagged tokens,” &#039;&#039;Language Codes&#039;&#039; 6 (2024): 820–824.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The sign code used to publish the Dozenal Primer Inscription derives from earlier internal documentation of KS-01, which described short ruled lines, rectilinear glyphs built from a limited set of straight strokes, and visually coherent families (corners, boxes, triangles, barred posts) separated by a consistent slash divider. That memorandum cautioned that widely circulated images likely trace back to a single original drawing, and it advocated raking-light or RTI imaging before firm claims about medium, date or technique.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot;&amp;gt;Jan-Tage Kristiansen, “Twin renderings, single template: a ruled signary on a putative cervid scapula,” correspondence note, &#039;&#039;language&#039;&#039; 27 (October 2023): 1073–1074.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The Dozenal Primer Inscription is published only as a transcription in the Kristiansen coding scheme and as line drawings; no phonetic values or language affiliation are claimed in the 2024 analysis. Instead, the argument proceeds by methods common in [[Corpus linguistics]] and quantitative [[Epigraphy]]: (1) positional bias over initial/medial/final slots; (2) bigram inventories and pointwise mutual information to find unusually tight collocations; and (3) tests for morphological productivity at the right edge of clauses. In this model, the slash P01 behaves as bound punctuation confined to clause ends; an invariant 4-sign sequence occupies the medial spine and functions like an equals sign; a compact cluster acts as a binary addition operator; and a fixed four-sign bundle at the right edge derives “×12” forms. Complement constructions such as “11+1,” “10+2,” and “6+6” converge on the same dozen-marked targets, diagnosing 12 as the privileged base. A small number of clauses are read as scaling the same patterns to 12² and, by extension, to 12³.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The approach is intentionally agnostic about [[Language identification]] and about whether the glyphs are ultimately [[Logogram|logographic]], [[Syllabary|syllabic]] or something else. The claim is structural: that a small and rigid clause template—bound final punctuation, a fixed medial spine, operator clusters with narrow distribution, and a selective right-edge derivation—fits a didactic number register more parsimoniously than it fits a segmental writing system without additional evidence.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt; Proponents note that such “equation-like” formatting is well attested in practice texts and primers across historical [[Numeral system]]s, whereas critics point out that non-linguistic genres (lists, catalogues, tallies) can sometimes mimic grammatical structure.&lt;br /&gt;
&lt;br /&gt;
== Glyph inventory and distribution ==&lt;br /&gt;
&lt;br /&gt;
A compact distribution over the transcribed sign set (Kristiansen codes) is given below. Counts refer to the Dozenal Primer Inscription as published in line drawings/transcription; code labels are descriptive only and do not imply phonetic values.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable plainlist&amp;quot; style=&amp;quot;text-align:center;&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Code&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Glyph&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Name&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Freq&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Init&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Med&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Final&lt;br /&gt;
|-&lt;br /&gt;
| A01&lt;br /&gt;
| [[File:A01.svg|24px|alt=A01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-OPEN&lt;br /&gt;
| 5 || 2 || 3 || –&lt;br /&gt;
|-&lt;br /&gt;
| A02&lt;br /&gt;
| [[File:A02.svg|24px|alt=A02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-BAR&lt;br /&gt;
| 17 || 17 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| A03&lt;br /&gt;
| [[File:A03.svg|24px|alt=A03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-DOT&lt;br /&gt;
| 4 || – || 4 || –&lt;br /&gt;
|-&lt;br /&gt;
| A05&lt;br /&gt;
| [[File:A05.svg|24px|alt=A05|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-CLOSED-BAR&lt;br /&gt;
| 3 || – || 3 || –&lt;br /&gt;
|-&lt;br /&gt;
| B01&lt;br /&gt;
| [[File:B01.svg|24px|alt=B01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX&lt;br /&gt;
| 6 || 4 || 2 || –&lt;br /&gt;
|-&lt;br /&gt;
| B02&lt;br /&gt;
| [[File:B02.svg|24px|alt=B02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX-DOT&lt;br /&gt;
| 5 || 5 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| B04&lt;br /&gt;
| [[File:B04.svg|24px|alt=B04|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX-DIAG-SW-NE&lt;br /&gt;
| 5 || – || 3 || 2&lt;br /&gt;
|-&lt;br /&gt;
| C01&lt;br /&gt;
| [[File:C01.svg|24px|alt=C01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-NW&lt;br /&gt;
| 28 || 14 || – || 14&lt;br /&gt;
|-&lt;br /&gt;
| C02&lt;br /&gt;
| [[File:C02.svg|24px|alt=C02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-SE&lt;br /&gt;
| 27 || 27 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| C03&lt;br /&gt;
| [[File:C03.svg|24px|alt=C03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-SE-DOT&lt;br /&gt;
| 6 || 1 || 5 || –&lt;br /&gt;
|-&lt;br /&gt;
| C04&lt;br /&gt;
| [[File:C04.svg|24px|alt=C04|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-NW-DIAG&lt;br /&gt;
| 3 || – || 3 || –&lt;br /&gt;
|-&lt;br /&gt;
| C05&lt;br /&gt;
| [[File:C05.svg|24px|alt=C05|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-SE-DIAG&lt;br /&gt;
| 16 || 16 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| C06&lt;br /&gt;
| [[File:C06.svg|24px|alt=C06|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-NW-DIAG-DOT&lt;br /&gt;
| 7 || – || 7 || –&lt;br /&gt;
|-&lt;br /&gt;
| H01&lt;br /&gt;
| [[File:H01.svg|24px|alt=H01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | DOUBLE-POST-CONNECTED&lt;br /&gt;
| 15 || – || 15 || –&lt;br /&gt;
|-&lt;br /&gt;
| H02&lt;br /&gt;
| [[File:H02.svg|24px|alt=H02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | POST-DOUBLE-BAR&lt;br /&gt;
| 8 || – || 6 || 2&lt;br /&gt;
|-&lt;br /&gt;
| H03&lt;br /&gt;
| [[File:H03.svg|24px|alt=H03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | DOUBLE-POST-BAR&lt;br /&gt;
| 14 || – || 14 || –&lt;br /&gt;
|-&lt;br /&gt;
| L01&lt;br /&gt;
| [[File:L01.svg|24px|alt=L01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | LINE-VERT&lt;br /&gt;
| 24 || – || 8 || 16&lt;br /&gt;
|-&lt;br /&gt;
| M01&lt;br /&gt;
| [[File:M01.svg|24px|alt=M01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | COMB-2-N&lt;br /&gt;
| 20 || 3 || 16 || 1&lt;br /&gt;
|-&lt;br /&gt;
| M02&lt;br /&gt;
| [[File:M02.svg|24px|alt=M02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | COMB-2-W&lt;br /&gt;
| 52 || – || 52 || –&lt;br /&gt;
|-&lt;br /&gt;
| M03&lt;br /&gt;
| [[File:M03.svg|24px|alt=M03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | COMB-3-E&lt;br /&gt;
| 40 || – || 40 || –&lt;br /&gt;
|-&lt;br /&gt;
| P01&lt;br /&gt;
| [[File:P01.svg|24px|alt=P01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | SLASH&lt;br /&gt;
| 25 || – || – || 25&lt;br /&gt;
|-&lt;br /&gt;
| S02&lt;br /&gt;
| [[File:S02.svg|24px|alt=S02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | MEANDER-2-HOLLOW&lt;br /&gt;
| 9 || – || 3 || 6&lt;br /&gt;
|-&lt;br /&gt;
| S03&lt;br /&gt;
| [[File:S03.svg|24px|alt=S03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | MEANDER-3&lt;br /&gt;
| 39 || – || 37 || 2&lt;br /&gt;
|-&lt;br /&gt;
| T01&lt;br /&gt;
| [[File:T01.svg|24px|alt=T01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TEE-UP&lt;br /&gt;
| 7 || – || 4 || 3&lt;br /&gt;
|-&lt;br /&gt;
| T02&lt;br /&gt;
| [[File:T02.svg|24px|alt=T02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TEE-LEFT&lt;br /&gt;
| 30 || – || 23 || 7&lt;br /&gt;
|-&lt;br /&gt;
| T03&lt;br /&gt;
| [[File:T03.svg|24px|alt=T03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | UPSIDE-DOWN-TEE&lt;br /&gt;
| 38 || – || 19 || 19&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Proposed numeric forms and functional sequences ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable plainlist&amp;quot; style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Function (gloss)&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Codes (Kristiansen)&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Glyph sequence (icons)&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Notes&lt;br /&gt;
|-&lt;br /&gt;
| ZERO&lt;br /&gt;
| &amp;lt;code&amp;gt;B02-M03-L01-B05-S03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:B02.svg|20px|alt=B02|link=]] [[File:M03.svg|20px|alt=M03|link=]] [[File:L01.svg|20px|alt=L01|link=]] [[File:B05.svg|20px|alt=B05|link=]] [[File:S03.svg|20px|alt=S03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| ONE&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-S03-C03-T03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]] [[File:S03.svg|20px|alt=S03|link=]] [[File:C03.svg|20px|alt=C03|link=]] [[File:T03.svg|20px|alt=T03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| TWO&lt;br /&gt;
| &amp;lt;code&amp;gt;C02-M02-H01-M01-T01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C02.svg|20px|alt=C02|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:H01.svg|20px|alt=H01|link=]] [[File:M01.svg|20px|alt=M01|link=]] [[File:T01.svg|20px|alt=T01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| THREE&lt;br /&gt;
| &amp;lt;code&amp;gt;B01-M02-T02-A03-H02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:B01.svg|20px|alt=B01|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:T02.svg|20px|alt=T02|link=]] [[File:A03.svg|20px|alt=A03|link=]] [[File:H02.svg|20px|alt=H02|link=]]&lt;br /&gt;
| &lt;br /&gt;
|-&lt;br /&gt;
| FOUR&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-M03-S02-C06-T02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]] [[File:M03.svg|20px|alt=M03|link=]] [[File:S02.svg|20px|alt=S02|link=]] [[File:C06.svg|20px|alt=C06|link=]] [[File:T02.svg|20px|alt=T02|link=]]&lt;br /&gt;
| &lt;br /&gt;
|-&lt;br /&gt;
| FIVE&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-M03-T02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]] [[File:M03.svg|20px|alt=M03|link=]] [[File:T02.svg|20px|alt=T02|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| SIX&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-B04-L01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]] [[File:B04.svg|20px|alt=B04|link=]] [[File:L01.svg|20px|alt=L01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| SEVEN&lt;br /&gt;
| &amp;lt;code&amp;gt;M01-L01-A01-T03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:M01.svg|20px|alt=M01|link=]] [[File:L01.svg|20px|alt=L01|link=]] [[File:A01.svg|20px|alt=A01|link=]] [[File:T03.svg|20px|alt=T03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| EIGHT&lt;br /&gt;
| &amp;lt;code&amp;gt;B02-M03-S03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:B02.svg|20px|alt=B02|link=]] [[File:M03.svg|20px|alt=M03|link=]] [[File:S03.svg|20px|alt=S03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| NINE&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-M02-H02-A05-S02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:H02.svg|20px|alt=H02|link=]] [[File:A05.svg|20px|alt=A05|link=]] [[File:S02.svg|20px|alt=S02|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| TEN&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-M01-T02-M02-L01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]] [[File:M01.svg|20px|alt=M01|link=]] [[File:T02.svg|20px|alt=T02|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:L01.svg|20px|alt=L01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| ELEVEN&lt;br /&gt;
| &amp;lt;code&amp;gt;A01-H01-B01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:A01.svg|20px|alt=A01|link=]] [[File:H01.svg|20px|alt=H01|link=]] [[File:B01.svg|20px|alt=B01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| TWELVE&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-M02-H02-B04&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:H02.svg|20px|alt=H02|link=]] [[File:B04.svg|20px|alt=B04|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;EQUALS / clause spine&#039;&#039;&#039;&lt;br /&gt;
| C02–M03–H03–C01&lt;br /&gt;
| [[File:C02.svg|20px|alt=C02|link=]] [[File:M03.svg|20px|alt=M03|link=]] [[File:H03.svg|20px|alt=H03|link=]] [[File:C01.svg|20px|alt=C01|link=]]&lt;br /&gt;
| Invariant medial sequence dividing left expression from result; reported as present in every equation clause. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PLUS (binary addition)&#039;&#039;&#039;&lt;br /&gt;
| A02–L01&lt;br /&gt;
| [[File:A02.svg|20px|alt=A02|link=]] [[File:L01.svg|20px|alt=L01|link=]]&lt;br /&gt;
| High-PMI operator cluster used in successor and complement constructions. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;MINUS (subtraction)&#039;&#039;&#039;&lt;br /&gt;
| C05–M02–S02&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:S02.svg|20px|alt=S02|link=]]&lt;br /&gt;
| Distinct collocational profile mirroring the syntax of the addition lines. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ONE (simplex unit)&#039;&#039;&#039;&lt;br /&gt;
| C01–S03–C03–T03&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]] [[File:S03.svg|20px|alt=S03|link=]] [[File:C03.svg|20px|alt=C03|link=]] [[File:T03.svg|20px|alt=T03|link=]]&lt;br /&gt;
| Recurrent bundle functioning as the unit “one”; used to advance the successor chain. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PUNCTUATION (bound period)&#039;&#039;&#039;&lt;br /&gt;
| P01 (final only)&lt;br /&gt;
| [[File:P01.svg|20px|alt=P01|link=]]&lt;br /&gt;
| Clause-final slash behaving as bound punctuation; not attested internally. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;×12 derivation (“dozen”)&#039;&#039;&#039;&lt;br /&gt;
| –C01–M02–H02–B04 (right edge)&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:H02.svg|20px|alt=H02|link=]] [[File:B04.svg|20px|alt=B04|link=]]&lt;br /&gt;
| Contiguous right-edge 4-gram treated as a derivational suffix forming multiples of twelve; composes with stems. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Scholarly discussion ==&lt;br /&gt;
Because the archaeological context of the longer inscription has not been publicly detailed, reception has been cautious. Supporters of the dozenal reading emphasise the conjunction of multiple independent diagnostics (final-only punctuation, a medial spine with near-zero variance, a selective right-edge four-gram, and complementary sums converging on the same target), arguing that chance alignment across all four is unlikely. Skeptics reply that without provenance, additional exemplars, or phonetic control, an arithmetic interpretation remains provisional and vulnerable to genre effects. Both sides agree on clear tests that could confirm or weaken the proposal: finding the slash P01 in non-final position; observing the putative ×12 four-gram away from clause edges; or documenting clauses that disrupt the medial spine while still behaving “arithmetically.”&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Nomenclature ==&lt;br /&gt;
“Dozenal Primer Inscription” is a descriptive convenience used in secondary discussion and is not a claim about ancient self-designation. Alternative labels in circulation include “Dozenal arithmetic inscription,” “Twelve-base arithmetic inscription,” and “Kristiansen-coded arithmetic inscription.” For clarity and disambiguation within encyclopedic contexts, the present title foregrounds the proposed function (primer) and base (dozenal).&lt;br /&gt;
&lt;br /&gt;
== Relation to the Scapula Glyph Inscription ==&lt;br /&gt;
The [[Scapula Glyph Inscription]] (KS-01) is much shorter but provided the stable sign labels and visual classes used in the transcription of the Dozenal Primer Inscription. KS-01 shows bipartite lines and a group-final slash divider consistent with the clause architecture proposed for the longer text, although by itself the scapula piece is too brief to display a full base-12 progression. Early notes on KS-01 also highlighted the need for direct examination and higher-quality imaging before drawing conclusions about material or chronology.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Duodecimal]]&lt;br /&gt;
* [[Numeral system]]&lt;br /&gt;
* [[Mathematical notation]]&lt;br /&gt;
* [[Undeciphered writing systems]]&lt;br /&gt;
* [[Epigraphy]]&lt;br /&gt;
* [[Paleography]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&amp;lt;references /&amp;gt;&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=367</id>
		<title>Dozenal Primer Inscription</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=367"/>
		<updated>2025-10-29T18:47:36Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: /* Proposed numeric forms and functional sequences */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Dozenal successor schema.png|thumb|The four examples render the successor chain 1→2, 2→3, 3→4, 4→5, demonstrating that adding the simplex unit “ONE” advances X to its next value while preserving clause structure. Codes follow Kristiansen’s signary; glyph labels (“ONE”, “TWO”, …) are semantic glosses for expository clarity and do not assume phonetic values.]]&lt;br /&gt;
The &#039;&#039;&#039;Dozenal Primer Inscription&#039;&#039;&#039; is the working name for a long geometric-glyph text proposed to encode a compact [[Duodecimal|dozenal]] (base-12) arithmetic register. The inscription is transcribed with a neutral code of sign classes (for example A01, B04, C03 and a slash-like divider P01) that mirrors the notation first used to document the shorter [[Scapula Glyph Inscription]] (KS-01). In 2024, a study argued from distributional evidence—rather than from phonetic values or a bilingual—that the text exhibits an equation-like clause structure with bound punctuation, a stereotyped medial “equals” spine, operator clusters, and a productive ×12 derivational suffix. On that account, the inscription functions as a brief primer for base-12 arithmetic, extending to compounds traditionally glossed as &#039;&#039;dozen&#039;&#039; (12×), &#039;&#039;gross&#039;&#039; (12²) and a tentative &#039;&#039;greatgross&#039;&#039; (12³).&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot;&amp;gt;Ginevra Rubergskier, “A dozenal primer hidden in plain sight: decoding arithmetic from a corpus of tagged tokens,” &#039;&#039;Language Codes&#039;&#039; 6 (2024): 820–824.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The sign code used to publish the Dozenal Primer Inscription derives from earlier internal documentation of KS-01, which described short ruled lines, rectilinear glyphs built from a limited set of straight strokes, and visually coherent families (corners, boxes, triangles, barred posts) separated by a consistent slash divider. That memorandum cautioned that widely circulated images likely trace back to a single original drawing, and it advocated raking-light or RTI imaging before firm claims about medium, date or technique.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot;&amp;gt;Jan-Tage Kristiansen, “Twin renderings, single template: a ruled signary on a putative cervid scapula,” correspondence note, &#039;&#039;language&#039;&#039; 27 (October 2023): 1073–1074.&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The Dozenal Primer Inscription is published only as a transcription in the Kristiansen coding scheme and as line drawings; no phonetic values or language affiliation are claimed in the 2024 analysis. Instead, the argument proceeds by methods common in [[Corpus linguistics]] and quantitative [[Epigraphy]]: (1) positional bias over initial/medial/final slots; (2) bigram inventories and pointwise mutual information to find unusually tight collocations; and (3) tests for morphological productivity at the right edge of clauses. In this model, the slash P01 behaves as bound punctuation confined to clause ends; an invariant 4-sign sequence occupies the medial spine and functions like an equals sign; a compact cluster acts as a binary addition operator; and a fixed four-sign bundle at the right edge derives “×12” forms. Complement constructions such as “11+1,” “10+2,” and “6+6” converge on the same dozen-marked targets, diagnosing 12 as the privileged base. A small number of clauses are read as scaling the same patterns to 12² and, by extension, to 12³.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The approach is intentionally agnostic about [[Language identification]] and about whether the glyphs are ultimately [[Logogram|logographic]], [[Syllabary|syllabic]] or something else. The claim is structural: that a small and rigid clause template—bound final punctuation, a fixed medial spine, operator clusters with narrow distribution, and a selective right-edge derivation—fits a didactic number register more parsimoniously than it fits a segmental writing system without additional evidence.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt; Proponents note that such “equation-like” formatting is well attested in practice texts and primers across historical [[Numeral system]]s, whereas critics point out that non-linguistic genres (lists, catalogues, tallies) can sometimes mimic grammatical structure.&lt;br /&gt;
&lt;br /&gt;
== Glyph inventory and distribution ==&lt;br /&gt;
&lt;br /&gt;
A compact distribution over the transcribed sign set (Kristiansen codes) is given below. Counts refer to the Dozenal Primer Inscription as published in line drawings/transcription; code labels are descriptive only and do not imply phonetic values.&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable plainlist&amp;quot; style=&amp;quot;text-align:center;&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Code&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Glyph&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Name&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Freq&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Init&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Med&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Final&lt;br /&gt;
|-&lt;br /&gt;
| A01&lt;br /&gt;
| [[File:A01.svg|24px|alt=A01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-OPEN&lt;br /&gt;
| 5 || 2 || 3 || –&lt;br /&gt;
|-&lt;br /&gt;
| A02&lt;br /&gt;
| [[File:A02.svg|24px|alt=A02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-BAR&lt;br /&gt;
| 17 || 17 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| A03&lt;br /&gt;
| [[File:A03.svg|24px|alt=A03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-DOT&lt;br /&gt;
| 4 || – || 4 || –&lt;br /&gt;
|-&lt;br /&gt;
| A05&lt;br /&gt;
| [[File:A05.svg|24px|alt=A05|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TRI-CLOSED-BAR&lt;br /&gt;
| 3 || – || 3 || –&lt;br /&gt;
|-&lt;br /&gt;
| B01&lt;br /&gt;
| [[File:B01.svg|24px|alt=B01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX&lt;br /&gt;
| 6 || 4 || 2 || –&lt;br /&gt;
|-&lt;br /&gt;
| B02&lt;br /&gt;
| [[File:B02.svg|24px|alt=B02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX-DOT&lt;br /&gt;
| 5 || 5 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| B04&lt;br /&gt;
| [[File:B04.svg|24px|alt=B04|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX-DIAG-SW-NE&lt;br /&gt;
| 5 || – || 3 || 2&lt;br /&gt;
|-&lt;br /&gt;
| C01&lt;br /&gt;
| [[File:C01.svg|24px|alt=C01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-NW&lt;br /&gt;
| 28 || 14 || – || 14&lt;br /&gt;
|-&lt;br /&gt;
| C02&lt;br /&gt;
| [[File:C02.svg|24px|alt=C02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-SE&lt;br /&gt;
| 27 || 27 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| C03&lt;br /&gt;
| [[File:C03.svg|24px|alt=C03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-SE-DOT&lt;br /&gt;
| 6 || 1 || 5 || –&lt;br /&gt;
|-&lt;br /&gt;
| C04&lt;br /&gt;
| [[File:C04.svg|24px|alt=C04|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-NW-DIAG&lt;br /&gt;
| 3 || – || 3 || –&lt;br /&gt;
|-&lt;br /&gt;
| C05&lt;br /&gt;
| [[File:C05.svg|24px|alt=C05|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-SE-DIAG&lt;br /&gt;
| 16 || 16 || – || –&lt;br /&gt;
|-&lt;br /&gt;
| C06&lt;br /&gt;
| [[File:C06.svg|24px|alt=C06|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | CORNER-NW-DIAG-DOT&lt;br /&gt;
| 7 || – || 7 || –&lt;br /&gt;
|-&lt;br /&gt;
| H01&lt;br /&gt;
| [[File:H01.svg|24px|alt=H01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | DOUBLE-POST-CONNECTED&lt;br /&gt;
| 15 || – || 15 || –&lt;br /&gt;
|-&lt;br /&gt;
| H02&lt;br /&gt;
| [[File:H02.svg|24px|alt=H02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | POST-DOUBLE-BAR&lt;br /&gt;
| 8 || – || 6 || 2&lt;br /&gt;
|-&lt;br /&gt;
| H03&lt;br /&gt;
| [[File:H03.svg|24px|alt=H03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | DOUBLE-POST-BAR&lt;br /&gt;
| 14 || – || 14 || –&lt;br /&gt;
|-&lt;br /&gt;
| L01&lt;br /&gt;
| [[File:L01.svg|24px|alt=L01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | LINE-VERT&lt;br /&gt;
| 24 || – || 8 || 16&lt;br /&gt;
|-&lt;br /&gt;
| M01&lt;br /&gt;
| [[File:M01.svg|24px|alt=M01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | COMB-2-N&lt;br /&gt;
| 20 || 3 || 16 || 1&lt;br /&gt;
|-&lt;br /&gt;
| M02&lt;br /&gt;
| [[File:M02.svg|24px|alt=M02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | COMB-2-W&lt;br /&gt;
| 52 || – || 52 || –&lt;br /&gt;
|-&lt;br /&gt;
| M03&lt;br /&gt;
| [[File:M03.svg|24px|alt=M03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | COMB-3-E&lt;br /&gt;
| 40 || – || 40 || –&lt;br /&gt;
|-&lt;br /&gt;
| P01&lt;br /&gt;
| [[File:P01.svg|24px|alt=P01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | SLASH&lt;br /&gt;
| 25 || – || – || 25&lt;br /&gt;
|-&lt;br /&gt;
| S02&lt;br /&gt;
| [[File:S02.svg|24px|alt=S02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | MEANDER-2-HOLLOW&lt;br /&gt;
| 9 || – || 3 || 6&lt;br /&gt;
|-&lt;br /&gt;
| S03&lt;br /&gt;
| [[File:S03.svg|24px|alt=S03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | MEANDER-3&lt;br /&gt;
| 39 || – || 37 || 2&lt;br /&gt;
|-&lt;br /&gt;
| T01&lt;br /&gt;
| [[File:T01.svg|24px|alt=T01|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TEE-UP&lt;br /&gt;
| 7 || – || 4 || 3&lt;br /&gt;
|-&lt;br /&gt;
| T02&lt;br /&gt;
| [[File:T02.svg|24px|alt=T02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | TEE-LEFT&lt;br /&gt;
| 30 || – || 23 || 7&lt;br /&gt;
|-&lt;br /&gt;
| T03&lt;br /&gt;
| [[File:T03.svg|24px|alt=T03|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | UPSIDE-DOWN-TEE&lt;br /&gt;
| 38 || – || 19 || 19&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Proposed numeric forms and functional sequences ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable plainlist&amp;quot; style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Function (gloss)&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Codes (Kristiansen)&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Glyph sequence (icons)&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Notes&lt;br /&gt;
|-&lt;br /&gt;
| ZERO&lt;br /&gt;
| &amp;lt;code&amp;gt;B02-M03-L01-B05-S03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:B02.svg|20px|alt=B02|link=]] [[File:M03.svg|20px|alt=M03|link=]] [[File:L01.svg|20px|alt=L01|link=]] [[File:B05.svg|20px|alt=B05|link=]] [[File:S03.svg|20px|alt=S03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| ONE&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-S03-C03-T03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]] [[File:S03.svg|20px|alt=S03|link=]] [[File:C03.svg|20px|alt=C03|link=]] [[File:T03.svg|20px|alt=T03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| TWO&lt;br /&gt;
| &amp;lt;code&amp;gt;C02-M02-H01-M01-T01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C02.svg|20px|alt=C02|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:H01.svg|20px|alt=H01|link=]] [[File:M01.svg|20px|alt=M01|link=]] [[File:T01.svg|20px|alt=T01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| THREE&lt;br /&gt;
| &amp;lt;code&amp;gt;B01-M02-T02-A03-H02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:B01.svg|20px|alt=B01|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:T02.svg|20px|alt=T02|link=]] [[File:A03.svg|20px|alt=A03|link=]] [[File:H02.svg|20px|alt=H02|link=]]&lt;br /&gt;
| dd&lt;br /&gt;
-&lt;br /&gt;
| FOUR&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-M03-S02-C06-T02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]] [[File:M03.svg|20px|alt=M03|link=]] [[File:S02.svg|20px|alt=S02|link=]] [[File:C06.svg|20px|alt=C06|link=]] [[File:T02.svg|20px|alt=T02|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| FIVE&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-M03-T02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]] [[File:M03.svg|20px|alt=M03|link=]] [[File:T02.svg|20px|alt=T02|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| SIX&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-B04-L01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]] [[File:B04.svg|20px|alt=B04|link=]] [[File:L01.svg|20px|alt=L01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| SEVEN&lt;br /&gt;
| &amp;lt;code&amp;gt;M01-L01-A01-T03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:M01.svg|20px|alt=M01|link=]] [[File:L01.svg|20px|alt=L01|link=]] [[File:A01.svg|20px|alt=A01|link=]] [[File:T03.svg|20px|alt=T03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| EIGHT&lt;br /&gt;
| &amp;lt;code&amp;gt;B02-M03-S03&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:B02.svg|20px|alt=B02|link=]] [[File:M03.svg|20px|alt=M03|link=]] [[File:S03.svg|20px|alt=S03|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| NINE&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-M02-H02-A05-S02&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:H02.svg|20px|alt=H02|link=]] [[File:A05.svg|20px|alt=A05|link=]] [[File:S02.svg|20px|alt=S02|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| TEN&lt;br /&gt;
| &amp;lt;code&amp;gt;C05-M01-T02-M02-L01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]] [[File:M01.svg|20px|alt=M01|link=]] [[File:T02.svg|20px|alt=T02|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:L01.svg|20px|alt=L01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| ELEVEN&lt;br /&gt;
| &amp;lt;code&amp;gt;A01-H01-B01&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:A01.svg|20px|alt=A01|link=]] [[File:H01.svg|20px|alt=H01|link=]] [[File:B01.svg|20px|alt=B01|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| TWELVE&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-M02-H02-B04&amp;lt;/code&amp;gt;&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:H02.svg|20px|alt=H02|link=]] [[File:B04.svg|20px|alt=B04|link=]]&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;EQUALS / clause spine&#039;&#039;&#039;&lt;br /&gt;
| C02–M03–H03–C01&lt;br /&gt;
| [[File:C02.svg|20px|alt=C02|link=]] [[File:M03.svg|20px|alt=M03|link=]] [[File:H03.svg|20px|alt=H03|link=]] [[File:C01.svg|20px|alt=C01|link=]]&lt;br /&gt;
| Invariant medial sequence dividing left expression from result; reported as present in every equation clause. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PLUS (binary addition)&#039;&#039;&#039;&lt;br /&gt;
| A02–L01&lt;br /&gt;
| [[File:A02.svg|20px|alt=A02|link=]] [[File:L01.svg|20px|alt=L01|link=]]&lt;br /&gt;
| High-PMI operator cluster used in successor and complement constructions. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;MINUS (subtraction)&#039;&#039;&#039;&lt;br /&gt;
| C05–M02–S02&lt;br /&gt;
| [[File:C05.svg|20px|alt=C05|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:S02.svg|20px|alt=S02|link=]]&lt;br /&gt;
| Distinct collocational profile mirroring the syntax of the addition lines. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;ONE (simplex unit)&#039;&#039;&#039;&lt;br /&gt;
| C01–S03–C03–T03&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]] [[File:S03.svg|20px|alt=S03|link=]] [[File:C03.svg|20px|alt=C03|link=]] [[File:T03.svg|20px|alt=T03|link=]]&lt;br /&gt;
| Recurrent bundle functioning as the unit “one”; used to advance the successor chain. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;PUNCTUATION (bound period)&#039;&#039;&#039;&lt;br /&gt;
| P01 (final only)&lt;br /&gt;
| [[File:P01.svg|20px|alt=P01|link=]]&lt;br /&gt;
| Clause-final slash behaving as bound punctuation; not attested internally. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;×12 derivation (“dozen”)&#039;&#039;&#039;&lt;br /&gt;
| –C01–M02–H02–B04 (right edge)&lt;br /&gt;
| [[File:C01.svg|20px|alt=C01|link=]] [[File:M02.svg|20px|alt=M02|link=]] [[File:H02.svg|20px|alt=H02|link=]] [[File:B04.svg|20px|alt=B04|link=]]&lt;br /&gt;
| Contiguous right-edge 4-gram treated as a derivational suffix forming multiples of twelve; composes with stems. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Scholarly discussion ==&lt;br /&gt;
Because the archaeological context of the longer inscription has not been publicly detailed, reception has been cautious. Supporters of the dozenal reading emphasise the conjunction of multiple independent diagnostics (final-only punctuation, a medial spine with near-zero variance, a selective right-edge four-gram, and complementary sums converging on the same target), arguing that chance alignment across all four is unlikely. Skeptics reply that without provenance, additional exemplars, or phonetic control, an arithmetic interpretation remains provisional and vulnerable to genre effects. Both sides agree on clear tests that could confirm or weaken the proposal: finding the slash P01 in non-final position; observing the putative ×12 four-gram away from clause edges; or documenting clauses that disrupt the medial spine while still behaving “arithmetically.”&amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Nomenclature ==&lt;br /&gt;
“Dozenal Primer Inscription” is a descriptive convenience used in secondary discussion and is not a claim about ancient self-designation. Alternative labels in circulation include “Dozenal arithmetic inscription,” “Twelve-base arithmetic inscription,” and “Kristiansen-coded arithmetic inscription.” For clarity and disambiguation within encyclopedic contexts, the present title foregrounds the proposed function (primer) and base (dozenal).&lt;br /&gt;
&lt;br /&gt;
== Relation to the Scapula Glyph Inscription ==&lt;br /&gt;
The [[Scapula Glyph Inscription]] (KS-01) is much shorter but provided the stable sign labels and visual classes used in the transcription of the Dozenal Primer Inscription. KS-01 shows bipartite lines and a group-final slash divider consistent with the clause architecture proposed for the longer text, although by itself the scapula piece is too brief to display a full base-12 progression. Early notes on KS-01 also highlighted the need for direct examination and higher-quality imaging before drawing conclusions about material or chronology.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Duodecimal]]&lt;br /&gt;
* [[Numeral system]]&lt;br /&gt;
* [[Mathematical notation]]&lt;br /&gt;
* [[Undeciphered writing systems]]&lt;br /&gt;
* [[Epigraphy]]&lt;br /&gt;
* [[Paleography]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&amp;lt;references /&amp;gt;&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
</feed>