<?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-10-01T01:48:27Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.44.0</generator>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=466</id>
		<title>Dozenal Primer Inscription</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=466"/>
		<updated>2026-09-24T18:03:42Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: TWELVE: the simplex form C01-M03-T02-C04 is attested once, in the numeral list (clause 15); the sums write twelve as one dozen&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 (45 clauses, 838 sign tokens, 28 sign types); positions are counted within words, with the clause-final divider P01 treated as bound to the preceding word. 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;
| 15 || 11 || 4 || –&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;
| 29 || 29 || – || –&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;
| 6 || – || 6 || –&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;
| 4 || – || 4 || –&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;
| 17 || 6 || 8 || 3&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;
| 8 || 6 || 2 || –&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;
| 36 || – || 15 || 21&lt;br /&gt;
|-&lt;br /&gt;
| B05&lt;br /&gt;
| [[File:B05.svg|24px|alt=B05|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX-DOT-DIAG-SW-NE&lt;br /&gt;
| 3 || – || 3 || –&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;
| 109 || 50 || 29 || 30&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;
| 39 || 39 || – || –&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;
| 43 || 1 || 42 || –&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 || 2 || 1 || –&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;
| 32 || 32 || – || –&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;
| 4 || – || 4 || –&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;
| 20 || – || 20 || –&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;
| 40 || – || 38 || 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;
| 30 || – || 30 || –&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;
| 45 || – || 12 || 33&lt;br /&gt;
|-&lt;br /&gt;
| L02&lt;br /&gt;
| [[File:L02.svg|24px|alt=L02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | LINE-HOR&lt;br /&gt;
| 34 || – || 26 || 8&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;
| 18 || 4 || 13 || 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;
| 67 || – || 67 || –&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;
| 62 || – || 60 || 2&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;
| 45 || – || – || 45&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;
| 12 || – || 6 || 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;
| 48 || – || 45 || 3&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;
| 11 || – || 9 || 2&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;
| 9 || – || 6 || 3&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;
| 49 || – || 28 || 21&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-L02-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:L02.svg|20px|alt=L02|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-L02-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:L02.svg|20px|alt=L02|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 (one dozen)&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-S03-C03-T03-C01-M02-H02-B04&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=]][[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;
| Transparently ONE followed by the ×12 derivational bundle; the sums always write twelve this way. Target of the complement sums 11+1, 10+2 and 6+6. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| TWELVE (simplex)&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-M03-T02-C04&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=]][[File:C04.svg|20px|alt=C04|link=]]&lt;br /&gt;
| Attested once, as the last item of the numeral list in the second line (clauses 3–15), which gives ZERO to ELEVEN as one-word clauses and then this form; read as twelve from its position. The sums never use it, and Rubergskier&#039;s analysis does not discuss the list. Glossed TWELVE in the dictionary published with Üksküla&#039;s Zagi materials. &amp;lt;ref name=&amp;quot;Ukskula2024&amp;quot;&amp;gt;Inrik Üksküla, “The Clan of Zagi: Numeric Calculus or Genealogical Primer? A Structural Analysis of the Kristiansen Cuneiform Corpus,” preprint (2024), with downloadable analysis materials.&amp;lt;/ref&amp;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:Kristiansen corpus]]&lt;br /&gt;
[[Category:Klema]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Nordland_Itinerary&amp;diff=465</id>
		<title>Nordland Itinerary</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Nordland_Itinerary&amp;diff=465"/>
		<updated>2026-09-24T11:02:22Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Name Klema: origin in Nordland Itinerary line 58 (Weidner 1939 addendum), located by Roelandt in 2024; Hittite-type misnomer&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Weidner 1938 Abb 1.jpg|thumb|Weidner&#039;s Abb. 1: a caravan descending from a rocky mountain edge into a wide, snow-covered steppe (pen drawing, 1938).]]&lt;br /&gt;
The &#039;&#039;&#039;Nordland Itinerary&#039;&#039;&#039; (German &#039;&#039;Reisebericht ins Nordland&#039;&#039;) is the conventional name for a Sumerian travel account preserved on an Old Babylonian school tablet and published in 1938 by H. Weidner as “Ein neusumerischer Reisebericht ins Nordland” in the &#039;&#039;Mitteilungen der Deutschen Orient-Gesellschaft zu Leipzig&#039;&#039;.&amp;lt;ref name=&amp;quot;Weidner1938&amp;quot;&amp;gt;H. Weidner, “Ein neusumerischer Reisebericht ins Nordland,” &#039;&#039;Mitteilungen der Deutschen Orient-Gesellschaft zu Leipzig&#039;&#039; 27 (April 1938).&amp;lt;/ref&amp;gt; It records the stages of a royal expedition that left northern Mesopotamia, crossed the Zagros and the Iranian plateau, reached the southern shore of the Caspian and continued through the steppe corridor towards the Urals, and it is the only cuneiform text that describes snow, a frozen river and the northern lights.&lt;br /&gt;
&lt;br /&gt;
Weidner assigned the composition to the Ur III period, around 2100 BC, and treated the surviving tablet as a later copy made for teaching. His 1938 article published some thirty lines from the third and fourth columns. The remaining lines of the fourth column, with the colophon, appeared a year later in a short addendum that was long overlooked;&amp;lt;ref name=&amp;quot;Weidner1939&amp;quot;&amp;gt;H. Weidner, “Nachtrag zum neusumerischen Reisebericht ins Nordland,” &#039;&#039;Mitteilungen der Deutschen Orient-Gesellschaft zu Leipzig&#039;&#039; 29 (1939).&amp;lt;/ref&amp;gt; they contain the only foreign language name recorded by a Mesopotamian source north of the Caspian, &#039;&#039;ki-li-ma&#039;&#039;, from which the modern term [[Kristiansen coding system|Klema]] is taken.&lt;br /&gt;
&lt;br /&gt;
== The tablet ==&lt;br /&gt;
The text survives on a medium-sized four-column tablet of light brown clay. Palaeographically the hand is Old Babylonian. A colophon on the lower edge of the reverse, partly preserved, gives the total number of travelling days. In 1938 Weidner read the surviving figure as a little over six months; the addendum, which gives the line in full, shows the numeral broken at its left edge, and “[1]6 months and 12 days” is now the accepted reading, which suits a journey whose snowfall comes in the seventh month.&amp;lt;ref name=&amp;quot;Weidner1938&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;Weidner1939&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Weidner argued that the composition itself is older than its copy on three grounds: the text is written consistently in Sumerian rather than in Akkadian with Sumerian glosses; its vocabulary and style match the elevated idiom of the Neo-Sumerian royal hymns; and particular theological turns, above all the presentation of Enki as a river god who has “his dwelling” in the frozen river, stand closest to texts of the time of Šulgi. He therefore placed its composition at an Ur III court, in Ur itself or in Nippur, and its copying in the Old Babylonian school, where it served as an exercise or reading text.&lt;br /&gt;
&lt;br /&gt;
The itinerary belongs to a small class of Mesopotamian texts that record day-marches and stages of travel, of which the best known real example is the Old Babylonian itinerary published by Goetze in 1953.&amp;lt;ref name=&amp;quot;Goetze1953&amp;quot;&amp;gt;A. Goetze, “An Old Babylonian Itinerary,” &#039;&#039;Journal of Cuneiform Studies&#039;&#039; 7 (1953): 51–72.&amp;lt;/ref&amp;gt; Weidner&#039;s text differs from that class in one respect: at the decisive transitions of the journey the scribe abandons the bare listing of stages for short passages in a poetic register, in which the strangeness of the northern lands is put into words.&lt;br /&gt;
&lt;br /&gt;
== Contents ==&lt;br /&gt;
The published text begins where the expedition leaves the mountains. The badly damaged lines before it mention, in Akkadian, toponyms of the north-eastern frontier of the Sumerian world: Subartu, Lullubu and perhaps Elam, together with “narrow ways in the mountains” (&#039;&#039;sila tur-tur kur-ra&#039;&#039;). Weidner read this as a crossing from the Upper Mesopotamian lowland over a pass north of Arbela into the Zagros.&lt;br /&gt;
&lt;br /&gt;
Three Sumerian passages were published in 1938.&lt;br /&gt;
&lt;br /&gt;
;Steppe and wagon peoples (lines 16–25)&lt;br /&gt;
:The travellers reach “very great rivers” whose “foreign tongues we did not know”, meet “men with boats and pack animals” who “set many wagons in motion”, negotiate with the caravan, and receive “weapons and strong hands”, which Weidner took to mean an armed escort through unsafe country. The land has no city; its people “do not eat grain bread, they eat only pig and fat cattle.”&lt;br /&gt;
;Cold, snow and the frozen river (lines 26–35)&lt;br /&gt;
:“In the seventh month white things moved from the sky, like chaff of the great threshing floor they fell to earth, the land became white, our great river became white.” Men and donkeys put their weight on the ice and carry “the water things over the water”. The foreigners say the river “is no longer there”; the scribe answers that this is “the house-river of Enki”, the place where the river god has set his dwelling.&lt;br /&gt;
;Forest, furs and sky lights (lines 36–45)&lt;br /&gt;
:“Trees upon trees are in the foreign lands; in three days one cannot count them to an end.” The people of the forest eat “the necks of thick-haired beasts” and bring heavy furs and fine hides “for the neck of the king”. In the sky “birds” move, “fast as eyes that are set, like shining gold and lapis lazuli”; the locals say “there our gods are set”, and the scribe replies that “it is Inanna who lays her hand to rest there.”&lt;br /&gt;
&lt;br /&gt;
The addendum of 1939 added the rest of the fourth column.&lt;br /&gt;
&lt;br /&gt;
;The head of the lands (lines 46–52)&lt;br /&gt;
:The expedition reaches “the head of the lands”, where “a great river touches the heart of the foreign lands” and there is “no city, no cultivated place”. A stone is set up for the king, beer and fine oil are poured, and “the name of the king of the land” is set down at the head of the lands.&lt;br /&gt;
;The furthest people and their tongue (lines 53–60)&lt;br /&gt;
:The people there “dwell in houses of hide”, “know no donkey, know no wagon”, and have “great-horned oxen” that “draw a sledge over the whiteness”. Their tongue “is apart from the tongues of the other lands” and has to be turned “through a third interpreter”. Line 58 then records the name: &#039;&#039;eme-bi ki-li-ma-še3 mu-un-sa4-e-ne&#039;&#039;, “their tongue they call &#039;&#039;ki-li-ma&#039;&#039;” (𒆠𒇷𒈠). The interpreter glosses it (line 59) as &#039;&#039;eme ti-la&#039;&#039;, “the living tongue”. The scribe closes the exchange in his usual manner (line 60): “it is a tongue that Nisaba does not know”, Nisaba being the goddess of writing; that is, an unwritten one.&lt;br /&gt;
;Gifts, the tusk and the return (lines 61–67)&lt;br /&gt;
:The people give “heavy hides, horns of the great-horned ox, and the tusk of the elephant of that land”; “the elephant of that land no one saw”. The foreigners say the tusk “comes out of the earth”; the scribe answers that “it is the bull of Enki that lies in the earth”. In the ninth month the expedition turns back towards Ur, leaves the frozen river on foot, and carries “the hides and the tusk for the neck of the king”. The column ends there; the return route is not described.&lt;br /&gt;
&lt;br /&gt;
== Weidner&#039;s commentary ==&lt;br /&gt;
Weidner&#039;s commentary follows the route stage by stage. The “rivers of very many woods” of line 16 he located in the upper Diyala or Sirwan system; the wagon peoples in the contact zone between mountain and plateau, where river transport, caravan traffic and an early wagon culture met; and the frozen river north of the Elburz, either the lower Safid Rud or, further north, a tributary of the Caspian in the Transcaspian lowland, while admitting that the material does not allow identification. The forest zone with its fur-bearing animals and its “sky lights”, which he took to be the aurora, he placed in the belt of woodland stretching from the Caspian basin towards the Urals, where the Eurasian steppe corridor opens.&lt;br /&gt;
&lt;br /&gt;
Two smaller observations have been widely quoted. Line 20, &#039;&#039;anše-kur-ra-ĝu10 ma-da-an-du11&#039;&#039;, “they spoke to my mountain donkey in their land”, he read as a joke of the scribe&#039;s: the whole Sumerian caravan personified as “donkey people” with whom the local wagon-drivers must first negotiate. And the repeated pattern in which the foreigners explain a phenomenon and the scribe reinterprets it through his own gods, the frozen river as Enki&#039;s house, the lights as Inanna&#039;s hand, he singled out as the characteristic method by which a Mesopotamian scholar absorbed the foreign into his own world. The addendum shows the pattern continuing to the end of the text: the unwritten tongue becomes one “Nisaba does not know”, and ivory dug from the ground becomes “the bull of Enki that lies in the earth”.&lt;br /&gt;
&lt;br /&gt;
On the tusk Weidner was brief: &#039;&#039;zu2 am-si&#039;&#039; is the ordinary Sumerian word for ivory, and the scribe has a word for the material but none for the animal. He compared the reports of fossil ivory gathered from river banks and thawing ground in northern Siberia, without drawing further conclusions.&amp;lt;ref name=&amp;quot;Weidner1939&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In his closing remarks Weidner drew the wider conclusion that the connection from northern Mesopotamia over the Zagros, the Iranian plateau and the Caspian rim to the north Eurasian forest and steppe region was, at least occasionally, a real and not merely an imagined one.&lt;br /&gt;
&lt;br /&gt;
== The name &#039;&#039;ki-li-ma&#039;&#039; ==&lt;br /&gt;
Line 58 is the only place in the text where a foreign word is written out rather than described. The three signs 𒆠𒇷𒈠 can be read &#039;&#039;ki-li-ma&#039;&#039; or &#039;&#039;ke4-le-ma&#039;&#039;. Sumerian orthography cannot write two consonants at the beginning of a word, and foreign names with initial clusters are regularly broken up with a vowel in cuneiform, so a form with a cluster, &#039;&#039;klema&#039;&#039; or &#039;&#039;klima&#039;&#039;, is at least as likely as the four-syllable one. What the scribe was told the word meant is given in the next line, “the living tongue”.&lt;br /&gt;
&lt;br /&gt;
Weidner treated the name as a curiosity. It acquired a second life in 2024, when [[Marie Roelandt]], who had located the addendum, named her blog after it, and writers on the [[Kristiansen coding system|Kristiansen corpus]] followed, using “Klema” for the sign system and the language presumed to lie behind it on the strength of geography alone: the itinerary places a language so called in the forest zone north of the Caspian around 2100 BC, and the [[Yedoma Ledger]], the only physical object proposed as a carrier of the corpus, comes from the north Eurasian permafrost. The blog &#039;&#039;[[Klema Field Notes]]&#039;&#039; carries the name in its title. No published study has argued that the people the expedition met were the people who wrote the corpus, and the tablets themselves say the tongue was unwritten.&lt;br /&gt;
&lt;br /&gt;
The name is therefore a misnomer of a familiar kind. Hittite is named after the land of Hatti and its biblical inhabitants, while the speakers of the language called it &#039;&#039;nešili&#039;&#039;, after the city of Neša; the label stuck because it was the first one available, and philology has lived with it. “Klema” names a language by the word a Sumerian scribe heard from a forest people who may or may not have been its speakers, and applies it to a script that no Sumerian ever saw. Its users accept this. Roelandt has written that it may be “the wrong name for the wrong script”, and that it is nevertheless “a better name than ‘the Kristiansen corpus’”.&lt;br /&gt;
&lt;br /&gt;
== Publication and reception ==&lt;br /&gt;
The 1938 article appeared in April and was illustrated with a single pen drawing of a caravan descending into a snowy steppe. It gives the tablet&#039;s physical description but not its museum number or present whereabouts, and no photograph or hand copy of the tablet was included. The addendum of 1939, a few pages in a late issue of the same series, supplied the transliteration and translation of the remaining lines and the colophon, again without a copy or photograph. Published on the eve of the war in a small print run, it was not cited for decades, and the remainder of the text was widely believed to be unedited until [[Marie Roelandt]] located a copy in early 2024 while searching the older literature for Mesopotamian contacts with the north Eurasian forest zone. She took the name in line 58 for her blog, and the corpus literature took the word from there.&amp;lt;ref name=&amp;quot;Weidner1939&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The itinerary is cited in discussion of the [[Kristiansen coding system|Kristiansen corpus]] for two reasons. It is the earliest text placing a literate Mesopotamian expedition in the north Eurasian forest zone, the broad region from which the [[Yedoma Ledger]] was later recovered, and it carries the name that the corpus has come to bear. Nothing in either publication mentions inscribed bone or ivory or any encounter with a script; the one statement about writing in the text is the scribe&#039;s own, that the tongue he heard was unwritten. No direct connection between the itinerary and the corpus has been proposed in print.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Yedoma Ledger]]&lt;br /&gt;
* [[Kristiansen coding system]]&lt;br /&gt;
* [[Klema Field Notes]]&lt;br /&gt;
* [[Dur-Untash Expedition]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&amp;lt;references /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Klema]]&lt;br /&gt;
[[Category:Epigraphy]]&lt;br /&gt;
[[Category:Parafictional Mesopotamian archaeology]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Marie_Roelandt&amp;diff=464</id>
		<title>Marie Roelandt</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Marie_Roelandt&amp;diff=464"/>
		<updated>2026-09-24T11:02:21Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Name Klema: origin in Nordland Itinerary line 58 (Weidner 1939 addendum), located by Roelandt in 2024; Hittite-type misnomer&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Marie Roelandt&#039;&#039;&#039; (born 1967) is a Belgian independent researcher, the author of the blog &#039;&#039;[[Klema Field Notes]]&#039;&#039; (klema.tsuk.org), and the person who introduced the name &#039;&#039;&#039;Klema&#039;&#039;&#039; for the [[Kristiansen coding system|Kristiansen corpus]]. She lives in Antwerp. She trained as a librarian and worked for many years in archival document management, developing alongside this a longstanding amateur interest in historical linguistics, genealogy, and cryptography. She has no formal academic affiliation and describes herself on her blog as &amp;quot;a librarian who reads too many preprints.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Roelandt began following the [[Kristiansen corpus]] literature after encountering [[Ginevra Rubergskier|Rubergskier]]&#039;s 2024 paper through a popular-science digest. Her distinctive contribution has been to bring genealogical intuitions, developed through decades of family history research, to bear on [[Inrik Üksküla|Üksküla]]&#039;s structural analysis of the [[Zagi Tablets]].&lt;br /&gt;
&lt;br /&gt;
== Work ==&lt;br /&gt;
In &amp;quot;The Zagi Family&amp;quot; (&#039;&#039;[[Klema Field Notes]]&#039;&#039;, 18 March 2024), Roelandt argued that the sign Üksküla had labelled UNIT should be read as the concrete kin term CHILD rather than an abstract quantity. She demonstrated that this substitution transforms Üksküla&#039;s structural templates into natural genealogical statements: &amp;quot;X has N children&amp;quot; and &amp;quot;child N is Y.&amp;quot; She identified parental coupling as the function of the AND_PLUS connector and read Document 4 of the Zagi corpus as a straightforward four-child family record.&lt;br /&gt;
&lt;br /&gt;
In a follow-up post, &amp;quot;Who&#039;s who in the Zagi family?&amp;quot; (&#039;&#039;[[Klema Field Notes]]&#039;&#039;, 12 April 2024), Roelandt constructed a working transliteration of the corpus. This was explicitly not a phonetic proposal but a readability aid, assigning arbitrary Latin letters to sign types in rough frequency order. Using it, she reconstructed a multi-generational family tree spanning at least three generations, identified two gendered variants of both CHILD and PARENT alongside a gender-neutral form of each, and identified a possessive particle. She noted two probable scribal slips in Document 3 in which the copy appears to have misattributed a grandchild relationship as a child relationship, and she stopped short of proposing readings for the opening and closing sentences of the corpus, which she described as &amp;quot;tempting but beyond what the internal evidence can support.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Roelandt is also the source of the name Klema. In February 2024, before her first post, she went through the older Assyriological literature looking for any Mesopotamian text that placed a literate party in the north Eurasian forest zone, the region the [[Yedoma Ledger]] had come out of, and found Weidner&#039;s 1938 article on the [[Nordland Itinerary]] together with his short 1939 addendum, which had not been cited since the war. The addendum contains the line in which the people at “the head of the lands” name their own tongue, &#039;&#039;ki-li-ma&#039;&#039;, “the living tongue”. She took the word for her blog and mentioned its source only on the About page, where she calls it “the wrong name for the wrong script” and “a better name than ‘the Kristiansen corpus’”. The word passed from the blog into informal use in the field. She has said that she expected to be corrected and was surprised to be followed.&lt;br /&gt;
&lt;br /&gt;
Roelandt&#039;s CHILD reading has been informally cited by several researchers as a significant clarification of Üksküla&#039;s Hypothesis B, though it has not yet appeared in peer-reviewed literature. She has said she has no plans to submit for publication, preferring the blog format for its speed and its tolerance of openly provisional conclusions.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Klema Field Notes]]&lt;br /&gt;
* [[Zagi Tablets]]&lt;br /&gt;
* [[Inrik Üksküla]]&lt;br /&gt;
* [[Ginevra Rubergskier]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Researchers]][[Category:Bloggers]][[Category:Kristiansen corpus]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Klema_Field_Notes&amp;diff=463</id>
		<title>Klema Field Notes</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Klema_Field_Notes&amp;diff=463"/>
		<updated>2026-09-24T11:02:20Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Name Klema: origin in Nordland Itinerary line 58 (Weidner 1939 addendum), located by Roelandt in 2024; Hittite-type misnomer&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;&#039;&#039;Klema Field Notes&#039;&#039;&#039;&#039;&#039; is an independent research blog maintained by [[Marie Roelandt]] and hosted at [https://klema.tsuk.org/ klema.tsuk.org]. It publishes informal but closely argued analyses of material in the [[Kristiansen coding system]], with a particular focus on the [[Zagi Tablets]].&lt;br /&gt;
&lt;br /&gt;
== Name ==&lt;br /&gt;
The blog is named after &#039;&#039;ki-li-ma&#039;&#039;, the word that line 58 of the [[Nordland Itinerary]] gives as the name the people at “the head of the lands” used for their own tongue. The line belongs to the part of the text that H. Weidner published in a short addendum of 1939, which was overlooked for decades; Roelandt located a copy in early 2024, while searching the older Assyriological literature for any Mesopotamian contact with the north Eurasian forest zone, and adopted the word before her first post. She recorded the source only on the blog&#039;s About page, where she also concedes that it may be “the wrong name for the wrong script” and calls it “a better name than ‘the Kristiansen corpus’”. From the blog the word passed into general use for the [[Kristiansen coding system|corpus]] and the language presumed behind it.&lt;br /&gt;
&lt;br /&gt;
== Notable posts ==&lt;br /&gt;
* &amp;quot;The Zagi Family&amp;quot; (18 March 2024): Roelandt argues that the sign UNIT in the Zagi corpus should be read as the kin term CHILD rather than as an abstract quantity, and demonstrates that this substitution produces natural genealogical statements of the form &amp;quot;X has N children&amp;quot; and &amp;quot;child N is Y.&amp;quot; She identifies two structural templates—X DEF_PIVOT N CHILD and CHILD N EQ_PIVOT Y—and reads the AND_PLUS connector as a parental coupling marker.&lt;br /&gt;
&lt;br /&gt;
* &amp;quot;Who&#039;s who in the Zagi family?&amp;quot; (12 April 2024): Building on the previous post, Roelandt assigns working transliterations to all sign types (explicitly not proposed as phonetic values) and uses these to reconstruct a multi-generational family tree from Documents 1–4. She identifies two families named CHILD-GENDERA and CHILD-GENDERB, two parental role terms distinguished by a suffix, and a gender-neutral parental term. She notes two probable scribal slips in Document 3 and stops short of proposing readings for the opening and closing lines of the corpus.&lt;br /&gt;
&lt;br /&gt;
== Relation to academic literature ==&lt;br /&gt;
&#039;&#039;Klema Field Notes&#039;&#039; explicitly engages with and cites [[Inrik Üksküla]]&#039;s preprint and [[Ginevra Rubergskier]]&#039;s [[Language Codes|&#039;&#039;Language Codes&#039;&#039;]] paper. Roelandt&#039;s CHILD reading may be seen as a development of Üksküla&#039;s Hypothesis B. The name Klema, which the corpus literature now uses informally, is likewise hers; see [[#Name|above]].&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Marie Roelandt]]&lt;br /&gt;
* [[Zagi Tablets]]&lt;br /&gt;
* [[Inrik Üksküla]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Kristiansen corpus]]&lt;br /&gt;
[[Category:Blogs]][[Category:Epigraphy]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Kristiansen_coding_system&amp;diff=462</id>
		<title>Kristiansen coding system</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Kristiansen_coding_system&amp;diff=462"/>
		<updated>2026-09-24T11:02:19Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Name Klema: origin in Nordland Itinerary line 58 (Weidner 1939 addendum), located by Roelandt in 2024; Hittite-type misnomer&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The &#039;&#039;&#039;Kristiansen coding system&#039;&#039;&#039; is the alphanumeric notation used in the scholarly literature to transcribe the geometric signs of the [[Scapula Glyph Inscription]] and the other texts that share its sign repertory, collectively referred to as the &#039;&#039;&#039;Kristiansen corpus&#039;&#039;&#039; and, since 2024, also as &#039;&#039;&#039;Klema&#039;&#039;&#039;, a name borrowed from the [[Nordland Itinerary]] (see [[#Name|Name]] below). Each sign receives a family letter and a two-digit index (for example C01, B04, P01). The system was introduced by [[Jan-Tage Kristiansen]] in a 2023 correspondence note and has since been adopted without modification by every subsequent study.&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;
The codes are deliberately neutral. They record what a sign looks like and how it is built, not what it might mean or sound like. No phonetic values have been proposed in the literature, and the mnemonic names attached to the codes (BOX-DOT, TEE-LEFT, and so on) are descriptive labels only.&lt;br /&gt;
&lt;br /&gt;
== Principles ==&lt;br /&gt;
Kristiansen&#039;s note set out three rules that later work has kept:&lt;br /&gt;
* &#039;&#039;&#039;One code per sign.&#039;&#039;&#039; Every distinct grapheme in the inventory receives one code; no code is assigned to a sequence.&lt;br /&gt;
* &#039;&#039;&#039;Family letter plus index.&#039;&#039;&#039; The letter names one of eight visually coherent families, identified by shared construction (a corner, a closed box, a triangle, and so on). The index numbers the members of a family in the order they appear on the scapula. A ninth letter, P, is reserved for the single divider sign.&lt;br /&gt;
* &#039;&#039;&#039;Rotation and diacritics are features, not new families.&#039;&#039;&#039; Signs that differ only by rotation or by an added dot, bar or diagonal tick stay within their family and are distinguished by index. Kristiansen&#039;s original signary carried a rotation column; later work by [[Camille Voudrin]] and colleagues treated rotation as a non-contrastive convenience and canonicalised signs rotation-agnostically, with no effect on the inventory.&lt;br /&gt;
&lt;br /&gt;
== Transcription conventions ==&lt;br /&gt;
* Signs within a word are joined by hyphens: &amp;lt;code&amp;gt;C01-M03-S01-C02&amp;lt;/code&amp;gt;. Words are separated by spaces.&lt;br /&gt;
* The divider &#039;&#039;&#039;P01&#039;&#039;&#039; is transcribed as a sign in its own right. In the [[Dozenal Primer Inscription]] and the [[Zagi Tablets]] it occurs only at clause ends and is written bound to the last word of the clause (&amp;lt;code&amp;gt;…-T03-P01&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Sentences in the longer corpora are numbered by document and position, for example &amp;lt;code&amp;gt;F3_44&amp;lt;/code&amp;gt; for the forty-fourth sentence in Document 3 of the Zagi Tablets. This numbering originates with [[Inrik Üksküla]]&#039;s preprint and is used by [[Marie Roelandt]].&lt;br /&gt;
* Uncertain or damaged signs are written &amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt; in place of a code.&lt;br /&gt;
&lt;br /&gt;
== The signary ==&lt;br /&gt;
The unified signary contains 31 signs in eight families plus the divider, 32 codes in all. Every one of them is attested once on the scapula (KS-01), which presents the families as an ordered sweep and so fixes the index order.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt; Rotation and feature descriptions follow Kristiansen&#039;s catalogue.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;font-size:95%;&amp;quot;&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Code !! scope=&amp;quot;col&amp;quot; | Glyph !! scope=&amp;quot;col&amp;quot; | Mnemonic !! scope=&amp;quot;col&amp;quot; | Rotation !! scope=&amp;quot;col&amp;quot; | Distinguishing features&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Corner family (C), 7 signs&lt;br /&gt;
|-&lt;br /&gt;
| C01 || [[File:C01.svg|24px|alt=C01|link=]] || CORNER-NW || rot0 || shape=right-angle, open=NE, marks=none&lt;br /&gt;
|-&lt;br /&gt;
| C02 || [[File:C02.svg|24px|alt=C02|link=]] || CORNER-SE || rot180 || shape=right-angle, open=SE, marks=none&lt;br /&gt;
|-&lt;br /&gt;
| C03 || [[File:C03.svg|24px|alt=C03|link=]] || CORNER-SE-DOT || rot180 || shape=right-angle, open=SE, dot=centerline&lt;br /&gt;
|-&lt;br /&gt;
| C04 || [[File:C04.svg|24px|alt=C04|link=]] || CORNER-NW-DIAG || rot0 || shape=right-angle, open=NE, tick=diagonal, axis=NW-SE&lt;br /&gt;
|-&lt;br /&gt;
| C05 || [[File:C05.svg|24px|alt=C05|link=]] || CORNER-SE-DIAG || rot180 || shape=right-angle, open=SE, tick=diagonal, axis=NW-SE&lt;br /&gt;
|-&lt;br /&gt;
| C06 || [[File:C06.svg|24px|alt=C06|link=]] || CORNER-NW-DIAG-DOT || rot0 || shape=right-angle, open=NE, tick=diagonal, axis=NW-SE, dot=centerline&lt;br /&gt;
|-&lt;br /&gt;
| C07 || [[File:C07.svg|24px|alt=C07|link=]] || CORNER-SE-DIAG-DOT || rot180 || shape=right-angle, open=SE, notch=orthogonal, depth=short, dot=centerline&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Box family (B), 5 signs&lt;br /&gt;
|-&lt;br /&gt;
| B01 || [[File:B01.svg|24px|alt=B01|link=]] || BOX || rot0 || outline=square, interior=clear&lt;br /&gt;
|-&lt;br /&gt;
| B02 || [[File:B02.svg|24px|alt=B02|link=]] || BOX-DOT || rot0 || outline=square, dot=center&lt;br /&gt;
|-&lt;br /&gt;
| B03 || [[File:B03.svg|24px|alt=B03|link=]] || BOX-TICK || rot0 || outline=square, notch=orthogonal, wall=top, depth=short&lt;br /&gt;
|-&lt;br /&gt;
| B04 || [[File:B04.svg|24px|alt=B04|link=]] || BOX-DIAG-SW-NE || rot0 || outline=square, tick=diagonal, entry=SW-NE&lt;br /&gt;
|-&lt;br /&gt;
| B05 || [[File:B05.svg|24px|alt=B05|link=]] || BOX-DOT-DIAG-SW-NE || rot0 || outline=square, tick=diagonal, entry=SW-NE, dot=corner_NE&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Triangle family (A), 5 signs&lt;br /&gt;
|-&lt;br /&gt;
| A01 || [[File:A01.svg|24px|alt=A01|link=]] || TRI-OPEN || rot0 || closure=open, apex=up&lt;br /&gt;
|-&lt;br /&gt;
| A02 || [[File:A02.svg|24px|alt=A02|link=]] || TRI-BAR || rot180 || closure=open, apex=down, bar=1, position=mid&lt;br /&gt;
|-&lt;br /&gt;
| A03 || [[File:A03.svg|24px|alt=A03|link=]] || TRI-DOT || rot180 || closure=open, apex=down, bar=1, position=mid, dot=center&lt;br /&gt;
|-&lt;br /&gt;
| A04 || [[File:A04.svg|24px|alt=A04|link=]] || TRI-VEE || rot180 || closure=open, apex=down, vee=1, orientation=apex-base&lt;br /&gt;
|-&lt;br /&gt;
| A05 || [[File:A05.svg|24px|alt=A05|link=]] || TRI-CLOSED-BAR || rot0 || closure=closed, apex=up, bar=1, position=mid&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Line family (L), 2 signs&lt;br /&gt;
|-&lt;br /&gt;
| L01 || [[File:L01.svg|24px|alt=L01|link=]] || LINE-VERT || rot0 || orientation=vertical, length=cell&lt;br /&gt;
|-&lt;br /&gt;
| L02 || [[File:L02.svg|24px|alt=L02|link=]] || LINE-HOR || rot90 || orientation=horizontal, length=cell&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Tee family (T), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| T01 || [[File:T01.svg|24px|alt=T01|link=]] || TEE-UP || rot0 || stem=vertical, bar=top&lt;br /&gt;
|-&lt;br /&gt;
| T02 || [[File:T02.svg|24px|alt=T02|link=]] || TEE-LEFT || rot90 || stem=horizontal, bar=left&lt;br /&gt;
|-&lt;br /&gt;
| T03 || [[File:T03.svg|24px|alt=T03|link=]] || UPSIDE-DOWN-TEE || rot180 || stem=vertical, bar=bottom&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Comb family (M), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| M01 || [[File:M01.svg|24px|alt=M01|link=]] || COMB-2-N || rot0 || tines=2, stem=vertical, tines_side=left/top&lt;br /&gt;
|-&lt;br /&gt;
| M02 || [[File:M02.svg|24px|alt=M02|link=]] || COMB-2-W || rot0 || tines=2, stem=vertical, tines_side=left&lt;br /&gt;
|-&lt;br /&gt;
| M03 || [[File:M03.svg|24px|alt=M03|link=]] || COMB-3-E || rot0 || tines=3, stem=vertical, tines_side=right&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Meander family (S), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| S01 || [[File:S01.svg|24px|alt=S01|link=]] || MEANDER-2 || rot0 || elements=2, hollow=false&lt;br /&gt;
|-&lt;br /&gt;
| S02 || [[File:S02.svg|24px|alt=S02|link=]] || MEANDER-2-HOLLOW || rot0 || elements=2, hollow=true&lt;br /&gt;
|-&lt;br /&gt;
| S03 || [[File:S03.svg|24px|alt=S03|link=]] || MEANDER-3 || rot0 || elements=3, hollow=false&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Barred-post family (H), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| H01 || [[File:H01.svg|24px|alt=H01|link=]] || DOUBLE-POST-CONNECTED || rot0 || posts=2, parallel, connector=base, bars=0&lt;br /&gt;
|-&lt;br /&gt;
| H02 || [[File:H02.svg|24px|alt=H02|link=]] || POST-DOUBLE-BAR || rot0 || posts=1, bars=2, bar_style=cross, bar_positions=mid, upper&lt;br /&gt;
|-&lt;br /&gt;
| H03 || [[File:H03.svg|24px|alt=H03|link=]] || DOUBLE-POST-BAR || rot0 || posts=2, parallel, bars=1, bridging=true, bar_position=mid&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Punct family (P), 1 sign&lt;br /&gt;
|-&lt;br /&gt;
| P01 || [[File:P01.svg|24px|alt=P01|link=]] || SLASH || rot30 || punct=slash, function=separator&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Distributional classes ==&lt;br /&gt;
Working only from sign distributions in three longer inscriptions, Voudrin, Marchand and Leclerc (2024) found that the inventory partitions almost deterministically into two classes with the behaviour expected of consonants and vowels. The consonant-like class comprises the Corner, Box, Triangle and Comb families; the vowel-like class comprises the Tee, Line, Meander and Barred-post families. No family was split between the two classes.&amp;lt;ref name=&amp;quot;Voudrin2024&amp;quot;&amp;gt;Camille Voudrin, Solène Marchand, and Hadrien Leclerc, “A distributional test of vowel–consonant structure in an undeciphered signary suggests robust class separation,” &#039;&#039;[[Language Codes]]&#039;&#039; 7 (2024): 1281–1294.&amp;lt;/ref&amp;gt; The authors stress that this is a structural result and not a decipherment.&lt;br /&gt;
&lt;br /&gt;
== The corpus ==&lt;br /&gt;
Texts transcribed in the system, in order of publication:&lt;br /&gt;
* &#039;&#039;&#039;[[Scapula Glyph Inscription]]&#039;&#039;&#039; (KS-01), 2023. Four ruled lines on a scapula-shaped object; the abecedary-like presentation from which the signary was compiled.&lt;br /&gt;
* &#039;&#039;&#039;A longer companion inscription&#039;&#039;&#039;, 2023. Six lines and 146 tokens supplied as a transcription, analysed statistically in Kristiansen&#039;s longer working paper.&lt;br /&gt;
* &#039;&#039;&#039;[[Dozenal Primer Inscription]]&#039;&#039;&#039;, 2024. Forty-five clauses read by [[Ginevra Rubergskier]] as a primer for base-12 arithmetic.&lt;br /&gt;
* &#039;&#039;&#039;Three extended inscriptions&#039;&#039;&#039;, 2024. The material analysed by Voudrin and colleagues for vowel–consonant structure; published as vector tracings in their paper.&lt;br /&gt;
* &#039;&#039;&#039;[[Zagi Tablets]]&#039;&#039;&#039;, 2024. One hundred and four sentences on four clay tablets, headed in Akkadian &#039;&#039;imri Zagi-ak&#039;&#039;, analysed by [[Inrik Üksküla]] and, on the blog &#039;&#039;[[Klema Field Notes]]&#039;&#039;, by [[Marie Roelandt]].&lt;br /&gt;
&lt;br /&gt;
Whether these texts share a single origin is not established. They share a sign inventory, the divider, and the same broad clause architecture; the Zagi Tablets add a cuneiform rendering and an Akkadian heading that the others lack.&lt;br /&gt;
&lt;br /&gt;
No physical object has been securely tied to any of them. The [[Yedoma Ledger]], a mammoth-ivory plaque with ruled incised registers recovered from Siberian permafrost in 2021, has been proposed informally as the carrier of the longer companion inscription, on grounds of chronology and register geometry rather than of any published sign comparison; neither its excavator nor Kristiansen has endorsed the identification.&lt;br /&gt;
&lt;br /&gt;
== Reception ==&lt;br /&gt;
The system&#039;s chief virtue is that it commits its users to nothing. Rubergskier&#039;s arithmetic reading, Voudrin&#039;s phonological partition and Roelandt&#039;s genealogical reading all operate on the same codes and can be compared token for token, which would be impossible had each study adopted its own transliteration. Roelandt&#039;s working transliteration into Latin letters is explicitly a readability aid layered on top of the codes, not a replacement for them.&lt;br /&gt;
&lt;br /&gt;
Critics note that the neutrality is also a limitation: the codes freeze Kristiansen&#039;s initial segmentation of the signs, and any sign he merged or split in 2023 is merged or split in every later study. Kristiansen himself has issued only minor corrections since, describing his role as “keeping the inventory honest.”&lt;br /&gt;
&lt;br /&gt;
== Name ==&lt;br /&gt;
“Kristiansen corpus” and “Kristiansen coding system” are descriptive conveniences, like “Dozenal Primer Inscription” and “Zagi Tablets”; none is a claim about what the writers called anything. The word &#039;&#039;&#039;Klema&#039;&#039;&#039; entered the field in 2024 from outside it. Line 58 of the [[Nordland Itinerary]], in the part of that text edited in Weidner&#039;s overlooked 1939 addendum, records what the people at “the head of the lands” called their tongue: &#039;&#039;ki-li-ma&#039;&#039;, glossed by the scribe&#039;s interpreter as “the living tongue”. [[Marie Roelandt]], who had located the addendum, named her blog &#039;&#039;[[Klema Field Notes]]&#039;&#039; after it in March 2024, and the word spread from the blog into informal usage for the sign system and the language presumed to lie behind it, on the strength of geography alone: the itinerary places a language so called in the forest zone north of the Caspian around 2100 BC, and the [[Yedoma Ledger]] comes from the north Eurasian permafrost.&lt;br /&gt;
&lt;br /&gt;
Users of the name generally acknowledge that it is a misnomer of the Hittite type. Nothing connects the people the Sumerian expedition met with the people who wrote the corpus, the itinerary itself says the tongue it heard was unwritten, and the cuneiform spelling breaks up a word that may have begun with a consonant cluster. Kristiansen has declined to use it, on the ground that a name is a hypothesis. Rubergskier and Üksküla use it in correspondence and not in print. It has nevertheless become the ordinary short name, and this encyclopedia follows that usage where no more precise term is needed.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Jan-Tage Kristiansen]]&lt;br /&gt;
* [[Scapula Glyph Inscription]]&lt;br /&gt;
* [[Dozenal Primer Inscription]]&lt;br /&gt;
* [[Zagi Tablets]]&lt;br /&gt;
* [[Language Codes]]&lt;br /&gt;
* [[Klema Field Notes]]&lt;br /&gt;
* [[Nordland Itinerary]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&amp;lt;references /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Kristiansen corpus]]&lt;br /&gt;
[[Category:Writing systems]]&lt;br /&gt;
[[Category:Epigraphy]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Nordland_Itinerary&amp;diff=461</id>
		<title>Nordland Itinerary</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Nordland_Itinerary&amp;diff=461"/>
		<updated>2026-09-24T10:57:49Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Add Weidner&amp;#039;s 1939 addendum: lines 46-67 and colophon, the name ki-li-ma (Klema), Hittite-type misnomer section&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Weidner 1938 Abb 1.jpg|thumb|Weidner&#039;s Abb. 1: a caravan descending from a rocky mountain edge into a wide, snow-covered steppe (pen drawing, 1938).]]&lt;br /&gt;
The &#039;&#039;&#039;Nordland Itinerary&#039;&#039;&#039; (German &#039;&#039;Reisebericht ins Nordland&#039;&#039;) is the conventional name for a Sumerian travel account preserved on an Old Babylonian school tablet and published in 1938 by H. Weidner as “Ein neusumerischer Reisebericht ins Nordland” in the &#039;&#039;Mitteilungen der Deutschen Orient-Gesellschaft zu Leipzig&#039;&#039;.&amp;lt;ref name=&amp;quot;Weidner1938&amp;quot;&amp;gt;H. Weidner, “Ein neusumerischer Reisebericht ins Nordland,” &#039;&#039;Mitteilungen der Deutschen Orient-Gesellschaft zu Leipzig&#039;&#039; 27 (April 1938).&amp;lt;/ref&amp;gt; It records the stages of a royal expedition that left northern Mesopotamia, crossed the Zagros and the Iranian plateau, reached the southern shore of the Caspian and continued through the steppe corridor towards the Urals, and it is the only cuneiform text that describes snow, a frozen river and the northern lights.&lt;br /&gt;
&lt;br /&gt;
Weidner assigned the composition to the Ur III period, around 2100 BC, and treated the surviving tablet as a later copy made for teaching. His 1938 article published some thirty lines from the third and fourth columns. The remaining lines of the fourth column, with the colophon, appeared a year later in a short addendum that was long overlooked;&amp;lt;ref name=&amp;quot;Weidner1939&amp;quot;&amp;gt;H. Weidner, “Nachtrag zum neusumerischen Reisebericht ins Nordland,” &#039;&#039;Mitteilungen der Deutschen Orient-Gesellschaft zu Leipzig&#039;&#039; 29 (1939).&amp;lt;/ref&amp;gt; they contain the only foreign language name recorded by a Mesopotamian source north of the Caspian, &#039;&#039;ki-li-ma&#039;&#039;, from which the modern term [[Kristiansen coding system|Klema]] is taken.&lt;br /&gt;
&lt;br /&gt;
== The tablet ==&lt;br /&gt;
The text survives on a medium-sized four-column tablet of light brown clay. Palaeographically the hand is Old Babylonian. A colophon on the lower edge of the reverse, partly preserved, gives the total number of travelling days. In 1938 Weidner read the surviving figure as a little over six months; the addendum, which gives the line in full, shows the numeral broken at its left edge, and “[1]6 months and 12 days” is now the accepted reading, which suits a journey whose snowfall comes in the seventh month.&amp;lt;ref name=&amp;quot;Weidner1938&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;Weidner1939&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Weidner argued that the composition itself is older than its copy on three grounds: the text is written consistently in Sumerian rather than in Akkadian with Sumerian glosses; its vocabulary and style match the elevated idiom of the Neo-Sumerian royal hymns; and particular theological turns, above all the presentation of Enki as a river god who has “his dwelling” in the frozen river, stand closest to texts of the time of Šulgi. He therefore placed its composition at an Ur III court, in Ur itself or in Nippur, and its copying in the Old Babylonian school, where it served as an exercise or reading text.&lt;br /&gt;
&lt;br /&gt;
The itinerary belongs to a small class of Mesopotamian texts that record day-marches and stages of travel, of which the best known real example is the Old Babylonian itinerary published by Goetze in 1953.&amp;lt;ref name=&amp;quot;Goetze1953&amp;quot;&amp;gt;A. Goetze, “An Old Babylonian Itinerary,” &#039;&#039;Journal of Cuneiform Studies&#039;&#039; 7 (1953): 51–72.&amp;lt;/ref&amp;gt; Weidner&#039;s text differs from that class in one respect: at the decisive transitions of the journey the scribe abandons the bare listing of stages for short passages in a poetic register, in which the strangeness of the northern lands is put into words.&lt;br /&gt;
&lt;br /&gt;
== Contents ==&lt;br /&gt;
The published text begins where the expedition leaves the mountains. The badly damaged lines before it mention, in Akkadian, toponyms of the north-eastern frontier of the Sumerian world: Subartu, Lullubu and perhaps Elam, together with “narrow ways in the mountains” (&#039;&#039;sila tur-tur kur-ra&#039;&#039;). Weidner read this as a crossing from the Upper Mesopotamian lowland over a pass north of Arbela into the Zagros.&lt;br /&gt;
&lt;br /&gt;
Three Sumerian passages were published in 1938.&lt;br /&gt;
&lt;br /&gt;
;Steppe and wagon peoples (lines 16–25)&lt;br /&gt;
:The travellers reach “very great rivers” whose “foreign tongues we did not know”, meet “men with boats and pack animals” who “set many wagons in motion”, negotiate with the caravan, and receive “weapons and strong hands”, which Weidner took to mean an armed escort through unsafe country. The land has no city; its people “do not eat grain bread, they eat only pig and fat cattle.”&lt;br /&gt;
;Cold, snow and the frozen river (lines 26–35)&lt;br /&gt;
:“In the seventh month white things moved from the sky, like chaff of the great threshing floor they fell to earth, the land became white, our great river became white.” Men and donkeys put their weight on the ice and carry “the water things over the water”. The foreigners say the river “is no longer there”; the scribe answers that this is “the house-river of Enki”, the place where the river god has set his dwelling.&lt;br /&gt;
;Forest, furs and sky lights (lines 36–45)&lt;br /&gt;
:“Trees upon trees are in the foreign lands; in three days one cannot count them to an end.” The people of the forest eat “the necks of thick-haired beasts” and bring heavy furs and fine hides “for the neck of the king”. In the sky “birds” move, “fast as eyes that are set, like shining gold and lapis lazuli”; the locals say “there our gods are set”, and the scribe replies that “it is Inanna who lays her hand to rest there.”&lt;br /&gt;
&lt;br /&gt;
The addendum of 1939 added the rest of the fourth column.&lt;br /&gt;
&lt;br /&gt;
;The head of the lands (lines 46–52)&lt;br /&gt;
:The expedition reaches “the head of the lands”, where “a great river touches the heart of the foreign lands” and there is “no city, no cultivated place”. A stone is set up for the king, beer and fine oil are poured, and “the name of the king of the land” is set down at the head of the lands.&lt;br /&gt;
;The furthest people and their tongue (lines 53–60)&lt;br /&gt;
:The people there “dwell in houses of hide”, “know no donkey, know no wagon”, and have “great-horned oxen” that “draw a sledge over the whiteness”. Their tongue “is apart from the tongues of the other lands” and has to be turned “through a third interpreter”. Line 58 then records the name: &#039;&#039;eme-bi ki-li-ma-še3 mu-un-sa4-e-ne&#039;&#039;, “their tongue they call &#039;&#039;ki-li-ma&#039;&#039;” (𒆠𒇷𒈠). The interpreter glosses it (line 59) as &#039;&#039;eme ti-la&#039;&#039;, “the living tongue”. The scribe closes the exchange in his usual manner (line 60): “it is a tongue that Nisaba does not know”, Nisaba being the goddess of writing; that is, an unwritten one.&lt;br /&gt;
;Gifts, the tusk and the return (lines 61–67)&lt;br /&gt;
:The people give “heavy hides, horns of the great-horned ox, and the tusk of the elephant of that land”; “the elephant of that land no one saw”. The foreigners say the tusk “comes out of the earth”; the scribe answers that “it is the bull of Enki that lies in the earth”. In the ninth month the expedition turns back towards Ur, leaves the frozen river on foot, and carries “the hides and the tusk for the neck of the king”. The column ends there; the return route is not described.&lt;br /&gt;
&lt;br /&gt;
== Weidner&#039;s commentary ==&lt;br /&gt;
Weidner&#039;s commentary follows the route stage by stage. The “rivers of very many woods” of line 16 he located in the upper Diyala or Sirwan system; the wagon peoples in the contact zone between mountain and plateau, where river transport, caravan traffic and an early wagon culture met; and the frozen river north of the Elburz, either the lower Safid Rud or, further north, a tributary of the Caspian in the Transcaspian lowland, while admitting that the material does not allow identification. The forest zone with its fur-bearing animals and its “sky lights”, which he took to be the aurora, he placed in the belt of woodland stretching from the Caspian basin towards the Urals, where the Eurasian steppe corridor opens.&lt;br /&gt;
&lt;br /&gt;
Two smaller observations have been widely quoted. Line 20, &#039;&#039;anše-kur-ra-ĝu10 ma-da-an-du11&#039;&#039;, “they spoke to my mountain donkey in their land”, he read as a joke of the scribe&#039;s: the whole Sumerian caravan personified as “donkey people” with whom the local wagon-drivers must first negotiate. And the repeated pattern in which the foreigners explain a phenomenon and the scribe reinterprets it through his own gods, the frozen river as Enki&#039;s house, the lights as Inanna&#039;s hand, he singled out as the characteristic method by which a Mesopotamian scholar absorbed the foreign into his own world. The addendum shows the pattern continuing to the end of the text: the unwritten tongue becomes one “Nisaba does not know”, and ivory dug from the ground becomes “the bull of Enki that lies in the earth”.&lt;br /&gt;
&lt;br /&gt;
On the tusk Weidner was brief: &#039;&#039;zu2 am-si&#039;&#039; is the ordinary Sumerian word for ivory, and the scribe has a word for the material but none for the animal. He compared the reports of fossil ivory gathered from river banks and thawing ground in northern Siberia, without drawing further conclusions.&amp;lt;ref name=&amp;quot;Weidner1939&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In his closing remarks Weidner drew the wider conclusion that the connection from northern Mesopotamia over the Zagros, the Iranian plateau and the Caspian rim to the north Eurasian forest and steppe region was, at least occasionally, a real and not merely an imagined one.&lt;br /&gt;
&lt;br /&gt;
== The name &#039;&#039;ki-li-ma&#039;&#039; ==&lt;br /&gt;
Line 58 is the only place in the text where a foreign word is written out rather than described. The three signs 𒆠𒇷𒈠 can be read &#039;&#039;ki-li-ma&#039;&#039; or &#039;&#039;ke4-le-ma&#039;&#039;. Sumerian orthography cannot write two consonants at the beginning of a word, and foreign names with initial clusters are regularly broken up with a vowel in cuneiform, so a form with a cluster, &#039;&#039;klema&#039;&#039; or &#039;&#039;klima&#039;&#039;, is at least as likely as the four-syllable one. What the scribe was told the word meant is given in the next line, “the living tongue”.&lt;br /&gt;
&lt;br /&gt;
Weidner treated the name as a curiosity. It acquired a second life in the 2020s, when writers on the [[Kristiansen coding system|Kristiansen corpus]] began to use “Klema” for the sign system and the language presumed to lie behind it, on the strength of geography alone: the itinerary places a language so called in the forest zone north of the Caspian around 2100 BC, and the [[Yedoma Ledger]], the only physical object proposed as a carrier of the corpus, comes from the north Eurasian permafrost. The blog &#039;&#039;[[Klema Field Notes]]&#039;&#039; carries the name in its title. No published study has argued that the people the expedition met were the people who wrote the corpus, and the tablets themselves say the tongue was unwritten.&lt;br /&gt;
&lt;br /&gt;
The name is therefore a misnomer of a familiar kind. Hittite is named after the land of Hatti and its biblical inhabitants, while the speakers of the language called it &#039;&#039;nešili&#039;&#039;, after the city of Neša; the label stuck because it was the first one available, and philology has lived with it. “Klema” names a language by the word a Sumerian scribe heard from a forest people who may or may not have been its speakers, and applies it to a script that no Sumerian ever saw. Its users accept this. Roelandt has written that it may be “the wrong name for the wrong script”, and that it is nevertheless “a better name than ‘the Kristiansen corpus’”.&lt;br /&gt;
&lt;br /&gt;
== Publication and reception ==&lt;br /&gt;
The 1938 article appeared in April and was illustrated with a single pen drawing of a caravan descending into a snowy steppe. It gives the tablet&#039;s physical description but not its museum number or present whereabouts, and no photograph or hand copy of the tablet was included. The addendum of 1939, a few pages in a late issue of the same series, supplied the transliteration and translation of the remaining lines and the colophon, again without a copy or photograph. Published on the eve of the war in a small print run, it was not cited for decades, and the remainder of the text was widely believed to be unedited until the addendum was identified in 2026.&amp;lt;ref name=&amp;quot;Weidner1939&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The itinerary is cited in discussion of the [[Kristiansen coding system|Kristiansen corpus]] for two reasons. It is the earliest text placing a literate Mesopotamian expedition in the north Eurasian forest zone, the broad region from which the [[Yedoma Ledger]] was later recovered, and it carries the name that the corpus has come to bear. Nothing in either publication mentions inscribed bone or ivory or any encounter with a script; the one statement about writing in the text is the scribe&#039;s own, that the tongue he heard was unwritten. No direct connection between the itinerary and the corpus has been proposed in print.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Yedoma Ledger]]&lt;br /&gt;
* [[Kristiansen coding system]]&lt;br /&gt;
* [[Klema Field Notes]]&lt;br /&gt;
* [[Dur-Untash Expedition]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&amp;lt;references /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Klema]]&lt;br /&gt;
[[Category:Epigraphy]]&lt;br /&gt;
[[Category:Parafictional Mesopotamian archaeology]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=File:Weidner_1938_Abb_1.jpg&amp;diff=460</id>
		<title>File:Weidner 1938 Abb 1.jpg</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=File:Weidner_1938_Abb_1.jpg&amp;diff=460"/>
		<updated>2026-09-22T11:07:19Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Abb. 1 of Weidner 1938: caravan descending into a snowy steppe (from the zog repo, downscaled)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Abb. 1 of Weidner 1938: caravan descending into a snowy steppe (from the zog repo, downscaled)&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Kristiansen_coding_system&amp;diff=459</id>
		<title>Kristiansen coding system</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Kristiansen_coding_system&amp;diff=459"/>
		<updated>2026-09-22T11:06:57Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: See also: Nordland Itinerary&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The &#039;&#039;&#039;Kristiansen coding system&#039;&#039;&#039; is the alphanumeric notation used in the scholarly literature to transcribe the geometric signs of the [[Scapula Glyph Inscription]] and the other texts that share its sign repertory, collectively referred to as the &#039;&#039;&#039;Kristiansen corpus&#039;&#039;&#039;. Each sign receives a family letter and a two-digit index (for example C01, B04, P01). The system was introduced by [[Jan-Tage Kristiansen]] in a 2023 correspondence note and has since been adopted without modification by every subsequent study.&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;
The codes are deliberately neutral. They record what a sign looks like and how it is built, not what it might mean or sound like. No phonetic values have been proposed in the literature, and the mnemonic names attached to the codes (BOX-DOT, TEE-LEFT, and so on) are descriptive labels only.&lt;br /&gt;
&lt;br /&gt;
== Principles ==&lt;br /&gt;
Kristiansen&#039;s note set out three rules that later work has kept:&lt;br /&gt;
* &#039;&#039;&#039;One code per sign.&#039;&#039;&#039; Every distinct grapheme in the inventory receives one code; no code is assigned to a sequence.&lt;br /&gt;
* &#039;&#039;&#039;Family letter plus index.&#039;&#039;&#039; The letter names one of eight visually coherent families, identified by shared construction (a corner, a closed box, a triangle, and so on). The index numbers the members of a family in the order they appear on the scapula. A ninth letter, P, is reserved for the single divider sign.&lt;br /&gt;
* &#039;&#039;&#039;Rotation and diacritics are features, not new families.&#039;&#039;&#039; Signs that differ only by rotation or by an added dot, bar or diagonal tick stay within their family and are distinguished by index. Kristiansen&#039;s original signary carried a rotation column; later work by [[Camille Voudrin]] and colleagues treated rotation as a non-contrastive convenience and canonicalised signs rotation-agnostically, with no effect on the inventory.&lt;br /&gt;
&lt;br /&gt;
== Transcription conventions ==&lt;br /&gt;
* Signs within a word are joined by hyphens: &amp;lt;code&amp;gt;C01-M03-S01-C02&amp;lt;/code&amp;gt;. Words are separated by spaces.&lt;br /&gt;
* The divider &#039;&#039;&#039;P01&#039;&#039;&#039; is transcribed as a sign in its own right. In the [[Dozenal Primer Inscription]] and the [[Zagi Tablets]] it occurs only at clause ends and is written bound to the last word of the clause (&amp;lt;code&amp;gt;…-T03-P01&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Sentences in the longer corpora are numbered by document and position, for example &amp;lt;code&amp;gt;F3_44&amp;lt;/code&amp;gt; for the forty-fourth sentence in Document 3 of the Zagi Tablets. This numbering originates with [[Inrik Üksküla]]&#039;s preprint and is used by [[Marie Roelandt]].&lt;br /&gt;
* Uncertain or damaged signs are written &amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt; in place of a code.&lt;br /&gt;
&lt;br /&gt;
== The signary ==&lt;br /&gt;
The unified signary contains 31 signs in eight families plus the divider, 32 codes in all. Every one of them is attested once on the scapula (KS-01), which presents the families as an ordered sweep and so fixes the index order.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt; Rotation and feature descriptions follow Kristiansen&#039;s catalogue.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;font-size:95%;&amp;quot;&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Code !! scope=&amp;quot;col&amp;quot; | Glyph !! scope=&amp;quot;col&amp;quot; | Mnemonic !! scope=&amp;quot;col&amp;quot; | Rotation !! scope=&amp;quot;col&amp;quot; | Distinguishing features&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Corner family (C), 7 signs&lt;br /&gt;
|-&lt;br /&gt;
| C01 || [[File:C01.svg|24px|alt=C01|link=]] || CORNER-NW || rot0 || shape=right-angle, open=NE, marks=none&lt;br /&gt;
|-&lt;br /&gt;
| C02 || [[File:C02.svg|24px|alt=C02|link=]] || CORNER-SE || rot180 || shape=right-angle, open=SE, marks=none&lt;br /&gt;
|-&lt;br /&gt;
| C03 || [[File:C03.svg|24px|alt=C03|link=]] || CORNER-SE-DOT || rot180 || shape=right-angle, open=SE, dot=centerline&lt;br /&gt;
|-&lt;br /&gt;
| C04 || [[File:C04.svg|24px|alt=C04|link=]] || CORNER-NW-DIAG || rot0 || shape=right-angle, open=NE, tick=diagonal, axis=NW-SE&lt;br /&gt;
|-&lt;br /&gt;
| C05 || [[File:C05.svg|24px|alt=C05|link=]] || CORNER-SE-DIAG || rot180 || shape=right-angle, open=SE, tick=diagonal, axis=NW-SE&lt;br /&gt;
|-&lt;br /&gt;
| C06 || [[File:C06.svg|24px|alt=C06|link=]] || CORNER-NW-DIAG-DOT || rot0 || shape=right-angle, open=NE, tick=diagonal, axis=NW-SE, dot=centerline&lt;br /&gt;
|-&lt;br /&gt;
| C07 || [[File:C07.svg|24px|alt=C07|link=]] || CORNER-SE-DIAG-DOT || rot180 || shape=right-angle, open=SE, notch=orthogonal, depth=short, dot=centerline&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Box family (B), 5 signs&lt;br /&gt;
|-&lt;br /&gt;
| B01 || [[File:B01.svg|24px|alt=B01|link=]] || BOX || rot0 || outline=square, interior=clear&lt;br /&gt;
|-&lt;br /&gt;
| B02 || [[File:B02.svg|24px|alt=B02|link=]] || BOX-DOT || rot0 || outline=square, dot=center&lt;br /&gt;
|-&lt;br /&gt;
| B03 || [[File:B03.svg|24px|alt=B03|link=]] || BOX-TICK || rot0 || outline=square, notch=orthogonal, wall=top, depth=short&lt;br /&gt;
|-&lt;br /&gt;
| B04 || [[File:B04.svg|24px|alt=B04|link=]] || BOX-DIAG-SW-NE || rot0 || outline=square, tick=diagonal, entry=SW-NE&lt;br /&gt;
|-&lt;br /&gt;
| B05 || [[File:B05.svg|24px|alt=B05|link=]] || BOX-DOT-DIAG-SW-NE || rot0 || outline=square, tick=diagonal, entry=SW-NE, dot=corner_NE&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Triangle family (A), 5 signs&lt;br /&gt;
|-&lt;br /&gt;
| A01 || [[File:A01.svg|24px|alt=A01|link=]] || TRI-OPEN || rot0 || closure=open, apex=up&lt;br /&gt;
|-&lt;br /&gt;
| A02 || [[File:A02.svg|24px|alt=A02|link=]] || TRI-BAR || rot180 || closure=open, apex=down, bar=1, position=mid&lt;br /&gt;
|-&lt;br /&gt;
| A03 || [[File:A03.svg|24px|alt=A03|link=]] || TRI-DOT || rot180 || closure=open, apex=down, bar=1, position=mid, dot=center&lt;br /&gt;
|-&lt;br /&gt;
| A04 || [[File:A04.svg|24px|alt=A04|link=]] || TRI-VEE || rot180 || closure=open, apex=down, vee=1, orientation=apex-base&lt;br /&gt;
|-&lt;br /&gt;
| A05 || [[File:A05.svg|24px|alt=A05|link=]] || TRI-CLOSED-BAR || rot0 || closure=closed, apex=up, bar=1, position=mid&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Line family (L), 2 signs&lt;br /&gt;
|-&lt;br /&gt;
| L01 || [[File:L01.svg|24px|alt=L01|link=]] || LINE-VERT || rot0 || orientation=vertical, length=cell&lt;br /&gt;
|-&lt;br /&gt;
| L02 || [[File:L02.svg|24px|alt=L02|link=]] || LINE-HOR || rot90 || orientation=horizontal, length=cell&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Tee family (T), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| T01 || [[File:T01.svg|24px|alt=T01|link=]] || TEE-UP || rot0 || stem=vertical, bar=top&lt;br /&gt;
|-&lt;br /&gt;
| T02 || [[File:T02.svg|24px|alt=T02|link=]] || TEE-LEFT || rot90 || stem=horizontal, bar=left&lt;br /&gt;
|-&lt;br /&gt;
| T03 || [[File:T03.svg|24px|alt=T03|link=]] || UPSIDE-DOWN-TEE || rot180 || stem=vertical, bar=bottom&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Comb family (M), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| M01 || [[File:M01.svg|24px|alt=M01|link=]] || COMB-2-N || rot0 || tines=2, stem=vertical, tines_side=left/top&lt;br /&gt;
|-&lt;br /&gt;
| M02 || [[File:M02.svg|24px|alt=M02|link=]] || COMB-2-W || rot0 || tines=2, stem=vertical, tines_side=left&lt;br /&gt;
|-&lt;br /&gt;
| M03 || [[File:M03.svg|24px|alt=M03|link=]] || COMB-3-E || rot0 || tines=3, stem=vertical, tines_side=right&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Meander family (S), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| S01 || [[File:S01.svg|24px|alt=S01|link=]] || MEANDER-2 || rot0 || elements=2, hollow=false&lt;br /&gt;
|-&lt;br /&gt;
| S02 || [[File:S02.svg|24px|alt=S02|link=]] || MEANDER-2-HOLLOW || rot0 || elements=2, hollow=true&lt;br /&gt;
|-&lt;br /&gt;
| S03 || [[File:S03.svg|24px|alt=S03|link=]] || MEANDER-3 || rot0 || elements=3, hollow=false&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Barred-post family (H), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| H01 || [[File:H01.svg|24px|alt=H01|link=]] || DOUBLE-POST-CONNECTED || rot0 || posts=2, parallel, connector=base, bars=0&lt;br /&gt;
|-&lt;br /&gt;
| H02 || [[File:H02.svg|24px|alt=H02|link=]] || POST-DOUBLE-BAR || rot0 || posts=1, bars=2, bar_style=cross, bar_positions=mid, upper&lt;br /&gt;
|-&lt;br /&gt;
| H03 || [[File:H03.svg|24px|alt=H03|link=]] || DOUBLE-POST-BAR || rot0 || posts=2, parallel, bars=1, bridging=true, bar_position=mid&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Punct family (P), 1 sign&lt;br /&gt;
|-&lt;br /&gt;
| P01 || [[File:P01.svg|24px|alt=P01|link=]] || SLASH || rot30 || punct=slash, function=separator&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Distributional classes ==&lt;br /&gt;
Working only from sign distributions in three longer inscriptions, Voudrin, Marchand and Leclerc (2024) found that the inventory partitions almost deterministically into two classes with the behaviour expected of consonants and vowels. The consonant-like class comprises the Corner, Box, Triangle and Comb families; the vowel-like class comprises the Tee, Line, Meander and Barred-post families. No family was split between the two classes.&amp;lt;ref name=&amp;quot;Voudrin2024&amp;quot;&amp;gt;Camille Voudrin, Solène Marchand, and Hadrien Leclerc, “A distributional test of vowel–consonant structure in an undeciphered signary suggests robust class separation,” &#039;&#039;[[Language Codes]]&#039;&#039; 7 (2024): 1281–1294.&amp;lt;/ref&amp;gt; The authors stress that this is a structural result and not a decipherment.&lt;br /&gt;
&lt;br /&gt;
== The corpus ==&lt;br /&gt;
Texts transcribed in the system, in order of publication:&lt;br /&gt;
* &#039;&#039;&#039;[[Scapula Glyph Inscription]]&#039;&#039;&#039; (KS-01), 2023. Four ruled lines on a scapula-shaped object; the abecedary-like presentation from which the signary was compiled.&lt;br /&gt;
* &#039;&#039;&#039;A longer companion inscription&#039;&#039;&#039;, 2023. Six lines and 146 tokens supplied as a transcription, analysed statistically in Kristiansen&#039;s longer working paper.&lt;br /&gt;
* &#039;&#039;&#039;[[Dozenal Primer Inscription]]&#039;&#039;&#039;, 2024. Forty-five clauses read by [[Ginevra Rubergskier]] as a primer for base-12 arithmetic.&lt;br /&gt;
* &#039;&#039;&#039;Three extended inscriptions&#039;&#039;&#039;, 2024. The material analysed by Voudrin and colleagues for vowel–consonant structure; published as vector tracings in their paper.&lt;br /&gt;
* &#039;&#039;&#039;[[Zagi Tablets]]&#039;&#039;&#039;, 2024. One hundred and four sentences on four clay tablets, headed in Akkadian &#039;&#039;imri Zagi-ak&#039;&#039;, analysed by [[Inrik Üksküla]] and, on the blog &#039;&#039;[[Klema Field Notes]]&#039;&#039;, by [[Marie Roelandt]].&lt;br /&gt;
&lt;br /&gt;
Whether these texts share a single origin is not established. They share a sign inventory, the divider, and the same broad clause architecture; the Zagi Tablets add a cuneiform rendering and an Akkadian heading that the others lack.&lt;br /&gt;
&lt;br /&gt;
No physical object has been securely tied to any of them. The [[Yedoma Ledger]], a mammoth-ivory plaque with ruled incised registers recovered from Siberian permafrost in 2021, has been proposed informally as the carrier of the longer companion inscription, on grounds of chronology and register geometry rather than of any published sign comparison; neither its excavator nor Kristiansen has endorsed the identification.&lt;br /&gt;
&lt;br /&gt;
== Reception ==&lt;br /&gt;
The system&#039;s chief virtue is that it commits its users to nothing. Rubergskier&#039;s arithmetic reading, Voudrin&#039;s phonological partition and Roelandt&#039;s genealogical reading all operate on the same codes and can be compared token for token, which would be impossible had each study adopted its own transliteration. Roelandt&#039;s working transliteration into Latin letters is explicitly a readability aid layered on top of the codes, not a replacement for them.&lt;br /&gt;
&lt;br /&gt;
Critics note that the neutrality is also a limitation: the codes freeze Kristiansen&#039;s initial segmentation of the signs, and any sign he merged or split in 2023 is merged or split in every later study. Kristiansen himself has issued only minor corrections since, describing his role as “keeping the inventory honest.”&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Jan-Tage Kristiansen]]&lt;br /&gt;
* [[Scapula Glyph Inscription]]&lt;br /&gt;
* [[Dozenal Primer Inscription]]&lt;br /&gt;
* [[Zagi Tablets]]&lt;br /&gt;
* [[Language Codes]]&lt;br /&gt;
* [[Klema Field Notes]]&lt;br /&gt;
* [[Nordland Itinerary]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&amp;lt;references /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Kristiansen corpus]]&lt;br /&gt;
[[Category:Writing systems]]&lt;br /&gt;
[[Category:Epigraphy]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Yedoma_Ledger&amp;diff=458</id>
		<title>Yedoma Ledger</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Yedoma_Ledger&amp;diff=458"/>
		<updated>2026-09-22T11:06:56Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: See also: Nordland Itinerary&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name        = Yedoma Ledger&lt;br /&gt;
| image       = Yenoma Ledger.jpg&lt;br /&gt;
| caption     = Plaque with incised geometric registers&lt;br /&gt;
| 1 Material    = [[Ivory|Mammoth ivory]] (&#039;&#039;[[Woolly mammoth|Mammuthus primigenius]]&#039;&#039;)&lt;br /&gt;
| 2 Writing     = Geometric incisions&lt;br /&gt;
| 3 Created     = Substrate plausibly Last Interglacial (~125 ka); age of carving undetermined&lt;br /&gt;
| 4 Discovered_place = Lower [[Indigirka River|Indigirka]] basin, northeast [[Siberia]] (Russia)&lt;br /&gt;
| 5 Discovered_date  = Late August (year per preliminary report)&lt;br /&gt;
| 6 Present_location = Regional heritage authority&lt;br /&gt;
| 7 Dimensions  = ~216 × 54 × 39 mm; ~486 g&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;&#039;Yedoma Ledger&#039;&#039;&#039; is a worked plaque of mammoth ivory reportedly recovered from [[permafrost]] in the lower Indigirka region of northeast Siberia. The object was discovered and handled in the field by a small team led by [[J. Levi Schültke]] during a rapid-impact survey of thaw gullies commissioned to document erosion threats to organic artefacts; Schültke’s group was engaged for its prior experience with cold-chain recovery and identification of proboscidean ivory.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot;&amp;gt;J. Levi Schültke, &#039;&#039;A Mammoth-Ivory Plaque with Geometric Incisions from Northeast Siberia: Discovery, Context, and Materials Analysis&#039;&#039; (preliminary field and materials report, Seminar für Archäologie der Periglazialzonen, 2021).&amp;lt;/ref&amp;gt; Its carved surface shows parallel bands of straight and oblique incisions, including chevrons and cross-hatching. The preliminary report describes controlled cold-chain recovery and non-destructive documentation but does not advance a cultural attribution or secure the age of the carving itself.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Discovery and context ==&lt;br /&gt;
According to the report, the plaque was noticed by the team in a thaw gully within ice-rich loess ([[Yedoma]]) and recovered with surrounding sediment using bagged blocks and refrigerated transport overseen by the regional heritage authority.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot; /&amp;gt; Yedoma sequences in Siberia are known to preserve Late Pleistocene faunal remains and organic materials that can be reworked by [[thermokarst]] processes, complicating archaeological context.&amp;lt;ref name=&amp;quot;Schirrmeister2022&amp;quot;&amp;gt;Lutz Schirrmeister, “Yedoma Permafrost Landscapes as Past Archives and Future Change Hotspots,” &#039;&#039;Frontiers in Earth Science&#039;&#039; 10 (2022): 929873, [https://doi.org/10.3389/feart.2022.929873](https://doi.org/10.3389/feart.2022.929873).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Description ==&lt;br /&gt;
The object follows the natural camber of tusk ivory and presents a prepared, flatter face with multiple incised registers bounded by guide lines. Identification as mammoth ivory is supported by observation of [[Schreger lines]], a standard criterion in proboscidean ivory analysis.&amp;lt;ref name=&amp;quot;CITES2020&amp;quot;&amp;gt;“Identification Guide for Ivory and Ivory Substitutes,” CITES (2020), [https://cites.org/sites/default/files/ID_Manuals/Identification_Guide_for_Ivory_and_Ivory_Substitutes_ENGLISH.pdf](https://cites.org/sites/default/files/ID_Manuals/Identification_Guide_for_Ivory_and_Ivory_Substitutes_ENGLISH.pdf).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Dating ==&lt;br /&gt;
Screening methods reported include FTIR for collagen/mineral preservation, [[amino acid racemization]] (AAR) on a detached lamella, and [[optically stimulated luminescence]] (OSL) on associated sediment. These results are consistent with an ancient Pleistocene substrate but do not directly date the carving. Open-system behavior in bones and dentine limits the reliability of some radiometric approaches; targeted dating of incision surfaces has not yet been published.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;Penkman2022&amp;quot;&amp;gt;K. Penkman, “Dating the Paleolithic: trapped charge and amino acid geochronology,” &#039;&#039;Evolutionary Anthropology&#039;&#039; 31, no. 5 (2022): 203–222, [https://doi.org/10.1002/evan.21958](https://doi.org/10.1002/evan.21958).&amp;lt;/ref&amp;gt;&amp;lt;ref name=&amp;quot;Lian2006&amp;quot;&amp;gt;O. B. Lian, “Dating the Quaternary: progress in luminescence dating,” &#039;&#039;Quaternary Science Reviews&#039;&#039; 25, nos. 19–20 (2006): 2449–2468, [https://doi.org/10.1016/j.quascirev.2005.11.013](https://doi.org/10.1016/j.quascirev.2005.11.013).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Interpretation ==&lt;br /&gt;
Published descriptions remain neutral and descriptive; no consensus interpretation (notation, decoration, or other function) has been advanced. Pending secure dating and replication studies, the Yedoma Ledger is cited as a candidate example of worked ivory from permafrost context rather than as evidence for a particular symbolic system.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Possible relation to the Kristiansen corpus ==&lt;br /&gt;
Since 2024 the Ledger has been discussed, so far only informally, as a candidate physical carrier for material in the [[Kristiansen coding system]]. The suggestion rests on three observations rather than on any direct comparison of signs.&lt;br /&gt;
&lt;br /&gt;
First, chronology. Schültke&#039;s preliminary report, which circulated in 2021, describes the incised face as a series of registers bounded by guide lines but publishes only one photograph and no tracing. [[Jan-Tage Kristiansen]]&#039;s 2023 correspondence note and his longer working paper were based on drawings and a transcription received by anonymous email in the two years that followed, with no indication of the object drawn.&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;
Second, geometry. The scapula inscription (KS-01) is a short abecedary and cannot be the Ledger, whose registers run the full 216 mm of the prepared face. Kristiansen&#039;s &#039;&#039;longer companion inscription&#039;&#039;, however, was supplied as a transcription only: six lines, 146 tokens, medium unstated. At the roughly one-centimetre sign pitch reported for the scapula, a 216 mm register holds about twenty signs, which is in keeping with a six-line text averaging some twenty-four tokens a line. The stroke repertoire described by Schültke, straight and oblique cuts with chevrons and cross-hatching, is consistent with the Triangle, Barred-post and Comb families of the signary, though it is equally consistent with a great deal of Upper Palaeolithic ornament.&lt;br /&gt;
&lt;br /&gt;
Third, the absence of any other candidate. The Ledger is the only object publicly documented from a controlled recovery whose incisions are organised in ruled registers of the kind the Kristiansen corpus presupposes.&lt;br /&gt;
&lt;br /&gt;
None of this amounts to an identification. Schültke has not commented on the Kristiansen literature, and his report predates it. Kristiansen, asked whether the anonymous transcription could derive from the Ledger, replied that he had “no basis for saying so and no basis for denying it,” and that the question would be settled by a full-resolution tracing of the Ledger&#039;s registers, which the regional heritage authority has not released.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt; Until such a tracing exists the Ledger remains, in the words of its own preliminary report, a candidate example of worked ivory from permafrost context, and the Kristiansen corpus remains a set of drawings without an object.&lt;br /&gt;
&lt;br /&gt;
== Significance ==&lt;br /&gt;
The find highlights both the potential and the difficulties of researching artefacts emerging from rapidly changing permafrost landscapes. It has been noted in discussions of best practice for recovery, minimal-intervention conservation (e.g. reversible consolidants), and multi-modal imaging such as [[reflectance transformation imaging]] in Arctic and sub-Arctic contexts.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;HE2016&amp;quot;&amp;gt;Historic England, &#039;&#039;Multi-Light Imaging for Heritage Applications (incl. H-RTI)&#039;&#039; (2016), [https://historicengland.org.uk/images-books/publications/multi-light-imaging-heritage-applications/](https://historicengland.org.uk/images-books/publications/multi-light-imaging-heritage-applications/).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[Kristiansen coding system]]&lt;br /&gt;
* [[Scapula Glyph Inscription]]&lt;br /&gt;
* [[Nordland Itinerary]]&lt;br /&gt;
* [[Yedoma]]&lt;br /&gt;
* [[Woolly mammoth]]&lt;br /&gt;
* [[Pleistocene archaeology]]&lt;br /&gt;
* [[Reflectance transformation imaging]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;br /&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=Nordland_Itinerary&amp;diff=457</id>
		<title>Nordland Itinerary</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Nordland_Itinerary&amp;diff=457"/>
		<updated>2026-09-22T11:06:55Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Create article on Weidner&amp;#039;s 1938 Neo-Sumerian itinerary into the north&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Weidner 1938 Abb 1.jpg|thumb|Weidner&#039;s Abb. 1: a caravan descending from a rocky mountain edge into a wide, snow-covered steppe (pen drawing, 1938).]]&lt;br /&gt;
The &#039;&#039;&#039;Nordland Itinerary&#039;&#039;&#039; (German &#039;&#039;Reisebericht ins Nordland&#039;&#039;) is the conventional name for a Sumerian travel account preserved on an Old Babylonian school tablet and published in 1938 by H. Weidner as “Ein neusumerischer Reisebericht ins Nordland” in the &#039;&#039;Mitteilungen der Deutschen Orient-Gesellschaft zu Leipzig&#039;&#039;.&amp;lt;ref name=&amp;quot;Weidner1938&amp;quot;&amp;gt;H. Weidner, “Ein neusumerischer Reisebericht ins Nordland,” &#039;&#039;Mitteilungen der Deutschen Orient-Gesellschaft zu Leipzig&#039;&#039; 27 (April 1938).&amp;lt;/ref&amp;gt; It records the stages of a royal expedition that left northern Mesopotamia, crossed the Zagros and the Iranian plateau, reached the southern shore of the Caspian and continued through the steppe corridor towards the Urals, and it is the only cuneiform text that describes snow, a frozen river and the northern lights.&lt;br /&gt;
&lt;br /&gt;
Weidner assigned the composition to the Ur III period, around 2100 BC, and treated the surviving tablet as a later copy made for teaching. Only one section of the text, some thirty lines from the third and fourth columns, was published in transliteration and translation; the remainder has not been edited.&lt;br /&gt;
&lt;br /&gt;
== The tablet ==&lt;br /&gt;
The text survives on a medium-sized four-column tablet of light brown clay. Palaeographically the hand is Old Babylonian. A colophon on the lower edge of the reverse, partly preserved, gives the total number of travelling days; on Weidner&#039;s reconstruction the whole undertaking lasted a little over six months.&amp;lt;ref name=&amp;quot;Weidner1938&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Weidner argued that the composition itself is older than its copy on three grounds: the text is written consistently in Sumerian rather than in Akkadian with Sumerian glosses; its vocabulary and style match the elevated idiom of the Neo-Sumerian royal hymns; and particular theological turns, above all the presentation of Enki as a river god who has “his dwelling” in the frozen river, stand closest to texts of the time of Šulgi. He therefore placed its composition at an Ur III court, in Ur itself or in Nippur, and its copying in the Old Babylonian school, where it served as an exercise or reading text.&lt;br /&gt;
&lt;br /&gt;
The itinerary belongs to a small class of Mesopotamian texts that record day-marches and stages of travel, of which the best known real example is the Old Babylonian itinerary published by Goetze in 1953.&amp;lt;ref name=&amp;quot;Goetze1953&amp;quot;&amp;gt;A. Goetze, “An Old Babylonian Itinerary,” &#039;&#039;Journal of Cuneiform Studies&#039;&#039; 7 (1953): 51–72.&amp;lt;/ref&amp;gt; Weidner&#039;s text differs from that class in one respect: at the decisive transitions of the journey the scribe abandons the bare listing of stages for short passages in a poetic register, in which the strangeness of the northern lands is put into words.&lt;br /&gt;
&lt;br /&gt;
== Contents ==&lt;br /&gt;
The published excerpt begins where the expedition leaves the mountains. The badly damaged lines before it mention, in Akkadian, toponyms of the north-eastern frontier of the Sumerian world: Subartu, Lullubu and perhaps Elam, together with “narrow ways in the mountains” (&#039;&#039;sila tur-tur kur-ra&#039;&#039;). Weidner read this as a crossing from the Upper Mesopotamian lowland over a pass north of Arbela into the Zagros.&lt;br /&gt;
&lt;br /&gt;
Three Sumerian passages follow.&lt;br /&gt;
&lt;br /&gt;
;Steppe and wagon peoples (lines 16–25)&lt;br /&gt;
:The travellers reach “very great rivers” whose “foreign tongues we did not know”, meet “men with boats and pack animals” who “set many wagons in motion”, negotiate with the caravan, and receive “weapons and strong hands”, which Weidner took to mean an armed escort through unsafe country. The land has no city; its people “do not eat grain bread, they eat only pig and fat cattle.”&lt;br /&gt;
;Cold, snow and the frozen river (lines 26–35)&lt;br /&gt;
:“In the seventh month white things moved from the sky, like chaff of the great threshing floor they fell to earth, the land became white, our great river became white.” Men and donkeys put their weight on the ice and carry “the water things over the water”. The foreigners say the river “is no longer there”; the scribe answers that this is “the house-river of Enki”, the place where the river god has set his dwelling.&lt;br /&gt;
;Forest, furs and sky lights (lines 36–45)&lt;br /&gt;
:“Trees upon trees are in the foreign lands; in three days one cannot count them to an end.” The people of the forest eat “the necks of thick-haired beasts” and bring heavy furs and fine hides “for the neck of the king”. In the sky “birds” move, “fast as eyes that are set, like shining gold and lapis lazuli”; the locals say “there our gods are set”, and the scribe replies that “it is Inanna who lays her hand to rest there.”&lt;br /&gt;
&lt;br /&gt;
The excerpt ends here. What the fourth column went on to record, and how the expedition returned, is not part of the publication.&lt;br /&gt;
&lt;br /&gt;
== Weidner&#039;s commentary ==&lt;br /&gt;
Weidner&#039;s commentary follows the route stage by stage. The “rivers of very many woods” of line 16 he located in the upper Diyala or Sirwan system; the wagon peoples in the contact zone between mountain and plateau, where river transport, caravan traffic and an early wagon culture met; and the frozen river north of the Elburz, either the lower Safid Rud or, further north, a tributary of the Caspian in the Transcaspian lowland, while admitting that the material does not allow identification. The forest zone with its fur-bearing animals and its “sky lights”, which he took to be the aurora, he placed in the belt of woodland stretching from the Caspian basin towards the Urals, where the Eurasian steppe corridor opens.&lt;br /&gt;
&lt;br /&gt;
Two smaller observations have been widely quoted. Line 20, &#039;&#039;anše-kur-ra-ĝu10 ma-da-an-du11&#039;&#039;, “they spoke to my mountain donkey in their land”, he read as a joke of the scribe&#039;s: the whole Sumerian caravan personified as “donkey people” with whom the local wagon-drivers must first negotiate. And the repeated pattern in which the foreigners explain a phenomenon and the scribe reinterprets it through his own gods, the frozen river as Enki&#039;s house, the lights as Inanna&#039;s hand, he singled out as the characteristic method by which a Mesopotamian scholar absorbed the foreign into his own world.&lt;br /&gt;
&lt;br /&gt;
In his closing remarks Weidner drew the wider conclusion that the connection from northern Mesopotamia over the Zagros, the Iranian plateau and the Caspian rim to the north Eurasian forest and steppe region was, at least occasionally, a real and not merely an imagined one.&lt;br /&gt;
&lt;br /&gt;
== Publication and reception ==&lt;br /&gt;
The article appeared in April 1938 and was illustrated with a single pen drawing of a caravan descending into a snowy steppe. It gives the tablet&#039;s physical description but not its museum number or present whereabouts, and no photograph or hand copy of the tablet was included. The unpublished remainder of the text has been a standing complaint of later readers.&lt;br /&gt;
&lt;br /&gt;
The itinerary is cited in discussion of the [[Kristiansen coding system|Kristiansen corpus]] for one reason only: it is the earliest text placing a literate Mesopotamian expedition in the north Eurasian forest zone, the broad region from which the [[Yedoma Ledger]] was later recovered. Nothing in the published excerpt mentions writing, inscribed bone or ivory, or any encounter with a script, and no direct connection between the itinerary and the corpus has been proposed in print.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Yedoma Ledger]]&lt;br /&gt;
* [[Kristiansen coding system]]&lt;br /&gt;
* [[Dur-Untash Expedition]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&amp;lt;references /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Klema]]&lt;br /&gt;
[[Category:Epigraphy]]&lt;br /&gt;
[[Category:Parafictional Mesopotamian archaeology]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Kristiansen_coding_system&amp;diff=456</id>
		<title>Kristiansen coding system</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Kristiansen_coding_system&amp;diff=456"/>
		<updated>2026-09-22T11:03:34Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Corpus section: note the Yedoma Ledger as the only proposed physical carrier&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The &#039;&#039;&#039;Kristiansen coding system&#039;&#039;&#039; is the alphanumeric notation used in the scholarly literature to transcribe the geometric signs of the [[Scapula Glyph Inscription]] and the other texts that share its sign repertory, collectively referred to as the &#039;&#039;&#039;Kristiansen corpus&#039;&#039;&#039;. Each sign receives a family letter and a two-digit index (for example C01, B04, P01). The system was introduced by [[Jan-Tage Kristiansen]] in a 2023 correspondence note and has since been adopted without modification by every subsequent study.&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;
The codes are deliberately neutral. They record what a sign looks like and how it is built, not what it might mean or sound like. No phonetic values have been proposed in the literature, and the mnemonic names attached to the codes (BOX-DOT, TEE-LEFT, and so on) are descriptive labels only.&lt;br /&gt;
&lt;br /&gt;
== Principles ==&lt;br /&gt;
Kristiansen&#039;s note set out three rules that later work has kept:&lt;br /&gt;
* &#039;&#039;&#039;One code per sign.&#039;&#039;&#039; Every distinct grapheme in the inventory receives one code; no code is assigned to a sequence.&lt;br /&gt;
* &#039;&#039;&#039;Family letter plus index.&#039;&#039;&#039; The letter names one of eight visually coherent families, identified by shared construction (a corner, a closed box, a triangle, and so on). The index numbers the members of a family in the order they appear on the scapula. A ninth letter, P, is reserved for the single divider sign.&lt;br /&gt;
* &#039;&#039;&#039;Rotation and diacritics are features, not new families.&#039;&#039;&#039; Signs that differ only by rotation or by an added dot, bar or diagonal tick stay within their family and are distinguished by index. Kristiansen&#039;s original signary carried a rotation column; later work by [[Camille Voudrin]] and colleagues treated rotation as a non-contrastive convenience and canonicalised signs rotation-agnostically, with no effect on the inventory.&lt;br /&gt;
&lt;br /&gt;
== Transcription conventions ==&lt;br /&gt;
* Signs within a word are joined by hyphens: &amp;lt;code&amp;gt;C01-M03-S01-C02&amp;lt;/code&amp;gt;. Words are separated by spaces.&lt;br /&gt;
* The divider &#039;&#039;&#039;P01&#039;&#039;&#039; is transcribed as a sign in its own right. In the [[Dozenal Primer Inscription]] and the [[Zagi Tablets]] it occurs only at clause ends and is written bound to the last word of the clause (&amp;lt;code&amp;gt;…-T03-P01&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Sentences in the longer corpora are numbered by document and position, for example &amp;lt;code&amp;gt;F3_44&amp;lt;/code&amp;gt; for the forty-fourth sentence in Document 3 of the Zagi Tablets. This numbering originates with [[Inrik Üksküla]]&#039;s preprint and is used by [[Marie Roelandt]].&lt;br /&gt;
* Uncertain or damaged signs are written &amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt; in place of a code.&lt;br /&gt;
&lt;br /&gt;
== The signary ==&lt;br /&gt;
The unified signary contains 31 signs in eight families plus the divider, 32 codes in all. Every one of them is attested once on the scapula (KS-01), which presents the families as an ordered sweep and so fixes the index order.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt; Rotation and feature descriptions follow Kristiansen&#039;s catalogue.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;font-size:95%;&amp;quot;&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Code !! scope=&amp;quot;col&amp;quot; | Glyph !! scope=&amp;quot;col&amp;quot; | Mnemonic !! scope=&amp;quot;col&amp;quot; | Rotation !! scope=&amp;quot;col&amp;quot; | Distinguishing features&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Corner family (C), 7 signs&lt;br /&gt;
|-&lt;br /&gt;
| C01 || [[File:C01.svg|24px|alt=C01|link=]] || CORNER-NW || rot0 || shape=right-angle, open=NE, marks=none&lt;br /&gt;
|-&lt;br /&gt;
| C02 || [[File:C02.svg|24px|alt=C02|link=]] || CORNER-SE || rot180 || shape=right-angle, open=SE, marks=none&lt;br /&gt;
|-&lt;br /&gt;
| C03 || [[File:C03.svg|24px|alt=C03|link=]] || CORNER-SE-DOT || rot180 || shape=right-angle, open=SE, dot=centerline&lt;br /&gt;
|-&lt;br /&gt;
| C04 || [[File:C04.svg|24px|alt=C04|link=]] || CORNER-NW-DIAG || rot0 || shape=right-angle, open=NE, tick=diagonal, axis=NW-SE&lt;br /&gt;
|-&lt;br /&gt;
| C05 || [[File:C05.svg|24px|alt=C05|link=]] || CORNER-SE-DIAG || rot180 || shape=right-angle, open=SE, tick=diagonal, axis=NW-SE&lt;br /&gt;
|-&lt;br /&gt;
| C06 || [[File:C06.svg|24px|alt=C06|link=]] || CORNER-NW-DIAG-DOT || rot0 || shape=right-angle, open=NE, tick=diagonal, axis=NW-SE, dot=centerline&lt;br /&gt;
|-&lt;br /&gt;
| C07 || [[File:C07.svg|24px|alt=C07|link=]] || CORNER-SE-DIAG-DOT || rot180 || shape=right-angle, open=SE, notch=orthogonal, depth=short, dot=centerline&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Box family (B), 5 signs&lt;br /&gt;
|-&lt;br /&gt;
| B01 || [[File:B01.svg|24px|alt=B01|link=]] || BOX || rot0 || outline=square, interior=clear&lt;br /&gt;
|-&lt;br /&gt;
| B02 || [[File:B02.svg|24px|alt=B02|link=]] || BOX-DOT || rot0 || outline=square, dot=center&lt;br /&gt;
|-&lt;br /&gt;
| B03 || [[File:B03.svg|24px|alt=B03|link=]] || BOX-TICK || rot0 || outline=square, notch=orthogonal, wall=top, depth=short&lt;br /&gt;
|-&lt;br /&gt;
| B04 || [[File:B04.svg|24px|alt=B04|link=]] || BOX-DIAG-SW-NE || rot0 || outline=square, tick=diagonal, entry=SW-NE&lt;br /&gt;
|-&lt;br /&gt;
| B05 || [[File:B05.svg|24px|alt=B05|link=]] || BOX-DOT-DIAG-SW-NE || rot0 || outline=square, tick=diagonal, entry=SW-NE, dot=corner_NE&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Triangle family (A), 5 signs&lt;br /&gt;
|-&lt;br /&gt;
| A01 || [[File:A01.svg|24px|alt=A01|link=]] || TRI-OPEN || rot0 || closure=open, apex=up&lt;br /&gt;
|-&lt;br /&gt;
| A02 || [[File:A02.svg|24px|alt=A02|link=]] || TRI-BAR || rot180 || closure=open, apex=down, bar=1, position=mid&lt;br /&gt;
|-&lt;br /&gt;
| A03 || [[File:A03.svg|24px|alt=A03|link=]] || TRI-DOT || rot180 || closure=open, apex=down, bar=1, position=mid, dot=center&lt;br /&gt;
|-&lt;br /&gt;
| A04 || [[File:A04.svg|24px|alt=A04|link=]] || TRI-VEE || rot180 || closure=open, apex=down, vee=1, orientation=apex-base&lt;br /&gt;
|-&lt;br /&gt;
| A05 || [[File:A05.svg|24px|alt=A05|link=]] || TRI-CLOSED-BAR || rot0 || closure=closed, apex=up, bar=1, position=mid&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Line family (L), 2 signs&lt;br /&gt;
|-&lt;br /&gt;
| L01 || [[File:L01.svg|24px|alt=L01|link=]] || LINE-VERT || rot0 || orientation=vertical, length=cell&lt;br /&gt;
|-&lt;br /&gt;
| L02 || [[File:L02.svg|24px|alt=L02|link=]] || LINE-HOR || rot90 || orientation=horizontal, length=cell&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Tee family (T), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| T01 || [[File:T01.svg|24px|alt=T01|link=]] || TEE-UP || rot0 || stem=vertical, bar=top&lt;br /&gt;
|-&lt;br /&gt;
| T02 || [[File:T02.svg|24px|alt=T02|link=]] || TEE-LEFT || rot90 || stem=horizontal, bar=left&lt;br /&gt;
|-&lt;br /&gt;
| T03 || [[File:T03.svg|24px|alt=T03|link=]] || UPSIDE-DOWN-TEE || rot180 || stem=vertical, bar=bottom&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Comb family (M), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| M01 || [[File:M01.svg|24px|alt=M01|link=]] || COMB-2-N || rot0 || tines=2, stem=vertical, tines_side=left/top&lt;br /&gt;
|-&lt;br /&gt;
| M02 || [[File:M02.svg|24px|alt=M02|link=]] || COMB-2-W || rot0 || tines=2, stem=vertical, tines_side=left&lt;br /&gt;
|-&lt;br /&gt;
| M03 || [[File:M03.svg|24px|alt=M03|link=]] || COMB-3-E || rot0 || tines=3, stem=vertical, tines_side=right&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Meander family (S), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| S01 || [[File:S01.svg|24px|alt=S01|link=]] || MEANDER-2 || rot0 || elements=2, hollow=false&lt;br /&gt;
|-&lt;br /&gt;
| S02 || [[File:S02.svg|24px|alt=S02|link=]] || MEANDER-2-HOLLOW || rot0 || elements=2, hollow=true&lt;br /&gt;
|-&lt;br /&gt;
| S03 || [[File:S03.svg|24px|alt=S03|link=]] || MEANDER-3 || rot0 || elements=3, hollow=false&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Barred-post family (H), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| H01 || [[File:H01.svg|24px|alt=H01|link=]] || DOUBLE-POST-CONNECTED || rot0 || posts=2, parallel, connector=base, bars=0&lt;br /&gt;
|-&lt;br /&gt;
| H02 || [[File:H02.svg|24px|alt=H02|link=]] || POST-DOUBLE-BAR || rot0 || posts=1, bars=2, bar_style=cross, bar_positions=mid, upper&lt;br /&gt;
|-&lt;br /&gt;
| H03 || [[File:H03.svg|24px|alt=H03|link=]] || DOUBLE-POST-BAR || rot0 || posts=2, parallel, bars=1, bridging=true, bar_position=mid&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Punct family (P), 1 sign&lt;br /&gt;
|-&lt;br /&gt;
| P01 || [[File:P01.svg|24px|alt=P01|link=]] || SLASH || rot30 || punct=slash, function=separator&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Distributional classes ==&lt;br /&gt;
Working only from sign distributions in three longer inscriptions, Voudrin, Marchand and Leclerc (2024) found that the inventory partitions almost deterministically into two classes with the behaviour expected of consonants and vowels. The consonant-like class comprises the Corner, Box, Triangle and Comb families; the vowel-like class comprises the Tee, Line, Meander and Barred-post families. No family was split between the two classes.&amp;lt;ref name=&amp;quot;Voudrin2024&amp;quot;&amp;gt;Camille Voudrin, Solène Marchand, and Hadrien Leclerc, “A distributional test of vowel–consonant structure in an undeciphered signary suggests robust class separation,” &#039;&#039;[[Language Codes]]&#039;&#039; 7 (2024): 1281–1294.&amp;lt;/ref&amp;gt; The authors stress that this is a structural result and not a decipherment.&lt;br /&gt;
&lt;br /&gt;
== The corpus ==&lt;br /&gt;
Texts transcribed in the system, in order of publication:&lt;br /&gt;
* &#039;&#039;&#039;[[Scapula Glyph Inscription]]&#039;&#039;&#039; (KS-01), 2023. Four ruled lines on a scapula-shaped object; the abecedary-like presentation from which the signary was compiled.&lt;br /&gt;
* &#039;&#039;&#039;A longer companion inscription&#039;&#039;&#039;, 2023. Six lines and 146 tokens supplied as a transcription, analysed statistically in Kristiansen&#039;s longer working paper.&lt;br /&gt;
* &#039;&#039;&#039;[[Dozenal Primer Inscription]]&#039;&#039;&#039;, 2024. Forty-five clauses read by [[Ginevra Rubergskier]] as a primer for base-12 arithmetic.&lt;br /&gt;
* &#039;&#039;&#039;Three extended inscriptions&#039;&#039;&#039;, 2024. The material analysed by Voudrin and colleagues for vowel–consonant structure; published as vector tracings in their paper.&lt;br /&gt;
* &#039;&#039;&#039;[[Zagi Tablets]]&#039;&#039;&#039;, 2024. One hundred and four sentences on four clay tablets, headed in Akkadian &#039;&#039;imri Zagi-ak&#039;&#039;, analysed by [[Inrik Üksküla]] and, on the blog &#039;&#039;[[Klema Field Notes]]&#039;&#039;, by [[Marie Roelandt]].&lt;br /&gt;
&lt;br /&gt;
Whether these texts share a single origin is not established. They share a sign inventory, the divider, and the same broad clause architecture; the Zagi Tablets add a cuneiform rendering and an Akkadian heading that the others lack.&lt;br /&gt;
&lt;br /&gt;
No physical object has been securely tied to any of them. The [[Yedoma Ledger]], a mammoth-ivory plaque with ruled incised registers recovered from Siberian permafrost in 2021, has been proposed informally as the carrier of the longer companion inscription, on grounds of chronology and register geometry rather than of any published sign comparison; neither its excavator nor Kristiansen has endorsed the identification.&lt;br /&gt;
&lt;br /&gt;
== Reception ==&lt;br /&gt;
The system&#039;s chief virtue is that it commits its users to nothing. Rubergskier&#039;s arithmetic reading, Voudrin&#039;s phonological partition and Roelandt&#039;s genealogical reading all operate on the same codes and can be compared token for token, which would be impossible had each study adopted its own transliteration. Roelandt&#039;s working transliteration into Latin letters is explicitly a readability aid layered on top of the codes, not a replacement for them.&lt;br /&gt;
&lt;br /&gt;
Critics note that the neutrality is also a limitation: the codes freeze Kristiansen&#039;s initial segmentation of the signs, and any sign he merged or split in 2023 is merged or split in every later study. Kristiansen himself has issued only minor corrections since, describing his role as “keeping the inventory honest.”&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Jan-Tage Kristiansen]]&lt;br /&gt;
* [[Scapula Glyph Inscription]]&lt;br /&gt;
* [[Dozenal Primer Inscription]]&lt;br /&gt;
* [[Zagi Tablets]]&lt;br /&gt;
* [[Language Codes]]&lt;br /&gt;
* [[Klema Field Notes]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&amp;lt;references /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Kristiansen corpus]]&lt;br /&gt;
[[Category:Writing systems]]&lt;br /&gt;
[[Category:Epigraphy]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Yedoma_Ledger&amp;diff=455</id>
		<title>Yedoma Ledger</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Yedoma_Ledger&amp;diff=455"/>
		<updated>2026-09-22T11:03:33Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Add section on the possible relation to the Kristiansen corpus; link the coding-system hub&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name        = Yedoma Ledger&lt;br /&gt;
| image       = Yenoma Ledger.jpg&lt;br /&gt;
| caption     = Plaque with incised geometric registers&lt;br /&gt;
| 1 Material    = [[Ivory|Mammoth ivory]] (&#039;&#039;[[Woolly mammoth|Mammuthus primigenius]]&#039;&#039;)&lt;br /&gt;
| 2 Writing     = Geometric incisions&lt;br /&gt;
| 3 Created     = Substrate plausibly Last Interglacial (~125 ka); age of carving undetermined&lt;br /&gt;
| 4 Discovered_place = Lower [[Indigirka River|Indigirka]] basin, northeast [[Siberia]] (Russia)&lt;br /&gt;
| 5 Discovered_date  = Late August (year per preliminary report)&lt;br /&gt;
| 6 Present_location = Regional heritage authority&lt;br /&gt;
| 7 Dimensions  = ~216 × 54 × 39 mm; ~486 g&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;&#039;Yedoma Ledger&#039;&#039;&#039; is a worked plaque of mammoth ivory reportedly recovered from [[permafrost]] in the lower Indigirka region of northeast Siberia. The object was discovered and handled in the field by a small team led by [[J. Levi Schültke]] during a rapid-impact survey of thaw gullies commissioned to document erosion threats to organic artefacts; Schültke’s group was engaged for its prior experience with cold-chain recovery and identification of proboscidean ivory.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot;&amp;gt;J. Levi Schültke, &#039;&#039;A Mammoth-Ivory Plaque with Geometric Incisions from Northeast Siberia: Discovery, Context, and Materials Analysis&#039;&#039; (preliminary field and materials report, Seminar für Archäologie der Periglazialzonen, 2021).&amp;lt;/ref&amp;gt; Its carved surface shows parallel bands of straight and oblique incisions, including chevrons and cross-hatching. The preliminary report describes controlled cold-chain recovery and non-destructive documentation but does not advance a cultural attribution or secure the age of the carving itself.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Discovery and context ==&lt;br /&gt;
According to the report, the plaque was noticed by the team in a thaw gully within ice-rich loess ([[Yedoma]]) and recovered with surrounding sediment using bagged blocks and refrigerated transport overseen by the regional heritage authority.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot; /&amp;gt; Yedoma sequences in Siberia are known to preserve Late Pleistocene faunal remains and organic materials that can be reworked by [[thermokarst]] processes, complicating archaeological context.&amp;lt;ref name=&amp;quot;Schirrmeister2022&amp;quot;&amp;gt;Lutz Schirrmeister, “Yedoma Permafrost Landscapes as Past Archives and Future Change Hotspots,” &#039;&#039;Frontiers in Earth Science&#039;&#039; 10 (2022): 929873, [https://doi.org/10.3389/feart.2022.929873](https://doi.org/10.3389/feart.2022.929873).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Description ==&lt;br /&gt;
The object follows the natural camber of tusk ivory and presents a prepared, flatter face with multiple incised registers bounded by guide lines. Identification as mammoth ivory is supported by observation of [[Schreger lines]], a standard criterion in proboscidean ivory analysis.&amp;lt;ref name=&amp;quot;CITES2020&amp;quot;&amp;gt;“Identification Guide for Ivory and Ivory Substitutes,” CITES (2020), [https://cites.org/sites/default/files/ID_Manuals/Identification_Guide_for_Ivory_and_Ivory_Substitutes_ENGLISH.pdf](https://cites.org/sites/default/files/ID_Manuals/Identification_Guide_for_Ivory_and_Ivory_Substitutes_ENGLISH.pdf).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Dating ==&lt;br /&gt;
Screening methods reported include FTIR for collagen/mineral preservation, [[amino acid racemization]] (AAR) on a detached lamella, and [[optically stimulated luminescence]] (OSL) on associated sediment. These results are consistent with an ancient Pleistocene substrate but do not directly date the carving. Open-system behavior in bones and dentine limits the reliability of some radiometric approaches; targeted dating of incision surfaces has not yet been published.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;Penkman2022&amp;quot;&amp;gt;K. Penkman, “Dating the Paleolithic: trapped charge and amino acid geochronology,” &#039;&#039;Evolutionary Anthropology&#039;&#039; 31, no. 5 (2022): 203–222, [https://doi.org/10.1002/evan.21958](https://doi.org/10.1002/evan.21958).&amp;lt;/ref&amp;gt;&amp;lt;ref name=&amp;quot;Lian2006&amp;quot;&amp;gt;O. B. Lian, “Dating the Quaternary: progress in luminescence dating,” &#039;&#039;Quaternary Science Reviews&#039;&#039; 25, nos. 19–20 (2006): 2449–2468, [https://doi.org/10.1016/j.quascirev.2005.11.013](https://doi.org/10.1016/j.quascirev.2005.11.013).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Interpretation ==&lt;br /&gt;
Published descriptions remain neutral and descriptive; no consensus interpretation (notation, decoration, or other function) has been advanced. Pending secure dating and replication studies, the Yedoma Ledger is cited as a candidate example of worked ivory from permafrost context rather than as evidence for a particular symbolic system.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Possible relation to the Kristiansen corpus ==&lt;br /&gt;
Since 2024 the Ledger has been discussed, so far only informally, as a candidate physical carrier for material in the [[Kristiansen coding system]]. The suggestion rests on three observations rather than on any direct comparison of signs.&lt;br /&gt;
&lt;br /&gt;
First, chronology. Schültke&#039;s preliminary report, which circulated in 2021, describes the incised face as a series of registers bounded by guide lines but publishes only one photograph and no tracing. [[Jan-Tage Kristiansen]]&#039;s 2023 correspondence note and his longer working paper were based on drawings and a transcription received by anonymous email in the two years that followed, with no indication of the object drawn.&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;
Second, geometry. The scapula inscription (KS-01) is a short abecedary and cannot be the Ledger, whose registers run the full 216 mm of the prepared face. Kristiansen&#039;s &#039;&#039;longer companion inscription&#039;&#039;, however, was supplied as a transcription only: six lines, 146 tokens, medium unstated. At the roughly one-centimetre sign pitch reported for the scapula, a 216 mm register holds about twenty signs, which is in keeping with a six-line text averaging some twenty-four tokens a line. The stroke repertoire described by Schültke, straight and oblique cuts with chevrons and cross-hatching, is consistent with the Triangle, Barred-post and Comb families of the signary, though it is equally consistent with a great deal of Upper Palaeolithic ornament.&lt;br /&gt;
&lt;br /&gt;
Third, the absence of any other candidate. The Ledger is the only object publicly documented from a controlled recovery whose incisions are organised in ruled registers of the kind the Kristiansen corpus presupposes.&lt;br /&gt;
&lt;br /&gt;
None of this amounts to an identification. Schültke has not commented on the Kristiansen literature, and his report predates it. Kristiansen, asked whether the anonymous transcription could derive from the Ledger, replied that he had “no basis for saying so and no basis for denying it,” and that the question would be settled by a full-resolution tracing of the Ledger&#039;s registers, which the regional heritage authority has not released.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt; Until such a tracing exists the Ledger remains, in the words of its own preliminary report, a candidate example of worked ivory from permafrost context, and the Kristiansen corpus remains a set of drawings without an object.&lt;br /&gt;
&lt;br /&gt;
== Significance ==&lt;br /&gt;
The find highlights both the potential and the difficulties of researching artefacts emerging from rapidly changing permafrost landscapes. It has been noted in discussions of best practice for recovery, minimal-intervention conservation (e.g. reversible consolidants), and multi-modal imaging such as [[reflectance transformation imaging]] in Arctic and sub-Arctic contexts.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;HE2016&amp;quot;&amp;gt;Historic England, &#039;&#039;Multi-Light Imaging for Heritage Applications (incl. H-RTI)&#039;&#039; (2016), [https://historicengland.org.uk/images-books/publications/multi-light-imaging-heritage-applications/](https://historicengland.org.uk/images-books/publications/multi-light-imaging-heritage-applications/).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[Kristiansen coding system]]&lt;br /&gt;
* [[Scapula Glyph Inscription]]&lt;br /&gt;
* [[Yedoma]]&lt;br /&gt;
* [[Woolly mammoth]]&lt;br /&gt;
* [[Pleistocene archaeology]]&lt;br /&gt;
* [[Reflectance transformation imaging]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;br /&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=File:Dozenal_successor_schema.png&amp;diff=454</id>
		<title>File:Dozenal successor schema.png</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=File:Dozenal_successor_schema.png&amp;diff=454"/>
		<updated>2026-09-22T11:00:05Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Mvuijlst uploaded a new version of File:Dozenal successor schema.png&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;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;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Kristiansen_corpus&amp;diff=453</id>
		<title>Kristiansen corpus</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Kristiansen_corpus&amp;diff=453"/>
		<updated>2026-09-22T10:29:54Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Redirect to the hub article Kristiansen coding system&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;#REDIRECT [[Kristiansen coding system]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Kristiansen_coding_system&amp;diff=452</id>
		<title>Kristiansen coding system</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Kristiansen_coding_system&amp;diff=452"/>
		<updated>2026-09-22T10:29:51Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Create hub article: principles, conventions, full signary, distributional classes, corpus, reception&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The &#039;&#039;&#039;Kristiansen coding system&#039;&#039;&#039; is the alphanumeric notation used in the scholarly literature to transcribe the geometric signs of the [[Scapula Glyph Inscription]] and the other texts that share its sign repertory, collectively referred to as the &#039;&#039;&#039;Kristiansen corpus&#039;&#039;&#039;. Each sign receives a family letter and a two-digit index (for example C01, B04, P01). The system was introduced by [[Jan-Tage Kristiansen]] in a 2023 correspondence note and has since been adopted without modification by every subsequent study.&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;
The codes are deliberately neutral. They record what a sign looks like and how it is built, not what it might mean or sound like. No phonetic values have been proposed in the literature, and the mnemonic names attached to the codes (BOX-DOT, TEE-LEFT, and so on) are descriptive labels only.&lt;br /&gt;
&lt;br /&gt;
== Principles ==&lt;br /&gt;
Kristiansen&#039;s note set out three rules that later work has kept:&lt;br /&gt;
* &#039;&#039;&#039;One code per sign.&#039;&#039;&#039; Every distinct grapheme in the inventory receives one code; no code is assigned to a sequence.&lt;br /&gt;
* &#039;&#039;&#039;Family letter plus index.&#039;&#039;&#039; The letter names one of eight visually coherent families, identified by shared construction (a corner, a closed box, a triangle, and so on). The index numbers the members of a family in the order they appear on the scapula. A ninth letter, P, is reserved for the single divider sign.&lt;br /&gt;
* &#039;&#039;&#039;Rotation and diacritics are features, not new families.&#039;&#039;&#039; Signs that differ only by rotation or by an added dot, bar or diagonal tick stay within their family and are distinguished by index. Kristiansen&#039;s original signary carried a rotation column; later work by [[Camille Voudrin]] and colleagues treated rotation as a non-contrastive convenience and canonicalised signs rotation-agnostically, with no effect on the inventory.&lt;br /&gt;
&lt;br /&gt;
== Transcription conventions ==&lt;br /&gt;
* Signs within a word are joined by hyphens: &amp;lt;code&amp;gt;C01-M03-S01-C02&amp;lt;/code&amp;gt;. Words are separated by spaces.&lt;br /&gt;
* The divider &#039;&#039;&#039;P01&#039;&#039;&#039; is transcribed as a sign in its own right. In the [[Dozenal Primer Inscription]] and the [[Zagi Tablets]] it occurs only at clause ends and is written bound to the last word of the clause (&amp;lt;code&amp;gt;…-T03-P01&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Sentences in the longer corpora are numbered by document and position, for example &amp;lt;code&amp;gt;F3_44&amp;lt;/code&amp;gt; for the forty-fourth sentence in Document 3 of the Zagi Tablets. This numbering originates with [[Inrik Üksküla]]&#039;s preprint and is used by [[Marie Roelandt]].&lt;br /&gt;
* Uncertain or damaged signs are written &amp;lt;code&amp;gt;??&amp;lt;/code&amp;gt; in place of a code.&lt;br /&gt;
&lt;br /&gt;
== The signary ==&lt;br /&gt;
The unified signary contains 31 signs in eight families plus the divider, 32 codes in all. Every one of them is attested once on the scapula (KS-01), which presents the families as an ordered sweep and so fixes the index order.&amp;lt;ref name=&amp;quot;Kristiansen2023h&amp;quot; /&amp;gt; Rotation and feature descriptions follow Kristiansen&#039;s catalogue.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;font-size:95%;&amp;quot;&lt;br /&gt;
! scope=&amp;quot;col&amp;quot; | Code !! scope=&amp;quot;col&amp;quot; | Glyph !! scope=&amp;quot;col&amp;quot; | Mnemonic !! scope=&amp;quot;col&amp;quot; | Rotation !! scope=&amp;quot;col&amp;quot; | Distinguishing features&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Corner family (C), 7 signs&lt;br /&gt;
|-&lt;br /&gt;
| C01 || [[File:C01.svg|24px|alt=C01|link=]] || CORNER-NW || rot0 || shape=right-angle, open=NE, marks=none&lt;br /&gt;
|-&lt;br /&gt;
| C02 || [[File:C02.svg|24px|alt=C02|link=]] || CORNER-SE || rot180 || shape=right-angle, open=SE, marks=none&lt;br /&gt;
|-&lt;br /&gt;
| C03 || [[File:C03.svg|24px|alt=C03|link=]] || CORNER-SE-DOT || rot180 || shape=right-angle, open=SE, dot=centerline&lt;br /&gt;
|-&lt;br /&gt;
| C04 || [[File:C04.svg|24px|alt=C04|link=]] || CORNER-NW-DIAG || rot0 || shape=right-angle, open=NE, tick=diagonal, axis=NW-SE&lt;br /&gt;
|-&lt;br /&gt;
| C05 || [[File:C05.svg|24px|alt=C05|link=]] || CORNER-SE-DIAG || rot180 || shape=right-angle, open=SE, tick=diagonal, axis=NW-SE&lt;br /&gt;
|-&lt;br /&gt;
| C06 || [[File:C06.svg|24px|alt=C06|link=]] || CORNER-NW-DIAG-DOT || rot0 || shape=right-angle, open=NE, tick=diagonal, axis=NW-SE, dot=centerline&lt;br /&gt;
|-&lt;br /&gt;
| C07 || [[File:C07.svg|24px|alt=C07|link=]] || CORNER-SE-DIAG-DOT || rot180 || shape=right-angle, open=SE, notch=orthogonal, depth=short, dot=centerline&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Box family (B), 5 signs&lt;br /&gt;
|-&lt;br /&gt;
| B01 || [[File:B01.svg|24px|alt=B01|link=]] || BOX || rot0 || outline=square, interior=clear&lt;br /&gt;
|-&lt;br /&gt;
| B02 || [[File:B02.svg|24px|alt=B02|link=]] || BOX-DOT || rot0 || outline=square, dot=center&lt;br /&gt;
|-&lt;br /&gt;
| B03 || [[File:B03.svg|24px|alt=B03|link=]] || BOX-TICK || rot0 || outline=square, notch=orthogonal, wall=top, depth=short&lt;br /&gt;
|-&lt;br /&gt;
| B04 || [[File:B04.svg|24px|alt=B04|link=]] || BOX-DIAG-SW-NE || rot0 || outline=square, tick=diagonal, entry=SW-NE&lt;br /&gt;
|-&lt;br /&gt;
| B05 || [[File:B05.svg|24px|alt=B05|link=]] || BOX-DOT-DIAG-SW-NE || rot0 || outline=square, tick=diagonal, entry=SW-NE, dot=corner_NE&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Triangle family (A), 5 signs&lt;br /&gt;
|-&lt;br /&gt;
| A01 || [[File:A01.svg|24px|alt=A01|link=]] || TRI-OPEN || rot0 || closure=open, apex=up&lt;br /&gt;
|-&lt;br /&gt;
| A02 || [[File:A02.svg|24px|alt=A02|link=]] || TRI-BAR || rot180 || closure=open, apex=down, bar=1, position=mid&lt;br /&gt;
|-&lt;br /&gt;
| A03 || [[File:A03.svg|24px|alt=A03|link=]] || TRI-DOT || rot180 || closure=open, apex=down, bar=1, position=mid, dot=center&lt;br /&gt;
|-&lt;br /&gt;
| A04 || [[File:A04.svg|24px|alt=A04|link=]] || TRI-VEE || rot180 || closure=open, apex=down, vee=1, orientation=apex-base&lt;br /&gt;
|-&lt;br /&gt;
| A05 || [[File:A05.svg|24px|alt=A05|link=]] || TRI-CLOSED-BAR || rot0 || closure=closed, apex=up, bar=1, position=mid&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Line family (L), 2 signs&lt;br /&gt;
|-&lt;br /&gt;
| L01 || [[File:L01.svg|24px|alt=L01|link=]] || LINE-VERT || rot0 || orientation=vertical, length=cell&lt;br /&gt;
|-&lt;br /&gt;
| L02 || [[File:L02.svg|24px|alt=L02|link=]] || LINE-HOR || rot90 || orientation=horizontal, length=cell&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Tee family (T), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| T01 || [[File:T01.svg|24px|alt=T01|link=]] || TEE-UP || rot0 || stem=vertical, bar=top&lt;br /&gt;
|-&lt;br /&gt;
| T02 || [[File:T02.svg|24px|alt=T02|link=]] || TEE-LEFT || rot90 || stem=horizontal, bar=left&lt;br /&gt;
|-&lt;br /&gt;
| T03 || [[File:T03.svg|24px|alt=T03|link=]] || UPSIDE-DOWN-TEE || rot180 || stem=vertical, bar=bottom&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Comb family (M), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| M01 || [[File:M01.svg|24px|alt=M01|link=]] || COMB-2-N || rot0 || tines=2, stem=vertical, tines_side=left/top&lt;br /&gt;
|-&lt;br /&gt;
| M02 || [[File:M02.svg|24px|alt=M02|link=]] || COMB-2-W || rot0 || tines=2, stem=vertical, tines_side=left&lt;br /&gt;
|-&lt;br /&gt;
| M03 || [[File:M03.svg|24px|alt=M03|link=]] || COMB-3-E || rot0 || tines=3, stem=vertical, tines_side=right&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Meander family (S), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| S01 || [[File:S01.svg|24px|alt=S01|link=]] || MEANDER-2 || rot0 || elements=2, hollow=false&lt;br /&gt;
|-&lt;br /&gt;
| S02 || [[File:S02.svg|24px|alt=S02|link=]] || MEANDER-2-HOLLOW || rot0 || elements=2, hollow=true&lt;br /&gt;
|-&lt;br /&gt;
| S03 || [[File:S03.svg|24px|alt=S03|link=]] || MEANDER-3 || rot0 || elements=3, hollow=false&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Barred-post family (H), 3 signs&lt;br /&gt;
|-&lt;br /&gt;
| H01 || [[File:H01.svg|24px|alt=H01|link=]] || DOUBLE-POST-CONNECTED || rot0 || posts=2, parallel, connector=base, bars=0&lt;br /&gt;
|-&lt;br /&gt;
| H02 || [[File:H02.svg|24px|alt=H02|link=]] || POST-DOUBLE-BAR || rot0 || posts=1, bars=2, bar_style=cross, bar_positions=mid, upper&lt;br /&gt;
|-&lt;br /&gt;
| H03 || [[File:H03.svg|24px|alt=H03|link=]] || DOUBLE-POST-BAR || rot0 || posts=2, parallel, bars=1, bridging=true, bar_position=mid&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;5&amp;quot; style=&amp;quot;text-align:left; background:#f2f2f2;&amp;quot; | Punct family (P), 1 sign&lt;br /&gt;
|-&lt;br /&gt;
| P01 || [[File:P01.svg|24px|alt=P01|link=]] || SLASH || rot30 || punct=slash, function=separator&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Distributional classes ==&lt;br /&gt;
Working only from sign distributions in three longer inscriptions, Voudrin, Marchand and Leclerc (2024) found that the inventory partitions almost deterministically into two classes with the behaviour expected of consonants and vowels. The consonant-like class comprises the Corner, Box, Triangle and Comb families; the vowel-like class comprises the Tee, Line, Meander and Barred-post families. No family was split between the two classes.&amp;lt;ref name=&amp;quot;Voudrin2024&amp;quot;&amp;gt;Camille Voudrin, Solène Marchand, and Hadrien Leclerc, “A distributional test of vowel–consonant structure in an undeciphered signary suggests robust class separation,” &#039;&#039;[[Language Codes]]&#039;&#039; 7 (2024): 1281–1294.&amp;lt;/ref&amp;gt; The authors stress that this is a structural result and not a decipherment.&lt;br /&gt;
&lt;br /&gt;
== The corpus ==&lt;br /&gt;
Texts transcribed in the system, in order of publication:&lt;br /&gt;
* &#039;&#039;&#039;[[Scapula Glyph Inscription]]&#039;&#039;&#039; (KS-01), 2023. Four ruled lines on a scapula-shaped object; the abecedary-like presentation from which the signary was compiled.&lt;br /&gt;
* &#039;&#039;&#039;A longer companion inscription&#039;&#039;&#039;, 2023. Six lines and 146 tokens supplied as a transcription, analysed statistically in Kristiansen&#039;s longer working paper.&lt;br /&gt;
* &#039;&#039;&#039;[[Dozenal Primer Inscription]]&#039;&#039;&#039;, 2024. Forty-five clauses read by [[Ginevra Rubergskier]] as a primer for base-12 arithmetic.&lt;br /&gt;
* &#039;&#039;&#039;Three extended inscriptions&#039;&#039;&#039;, 2024. The material analysed by Voudrin and colleagues for vowel–consonant structure; published as vector tracings in their paper.&lt;br /&gt;
* &#039;&#039;&#039;[[Zagi Tablets]]&#039;&#039;&#039;, 2024. One hundred and four sentences on four clay tablets, headed in Akkadian &#039;&#039;imri Zagi-ak&#039;&#039;, analysed by [[Inrik Üksküla]] and, on the blog &#039;&#039;[[Klema Field Notes]]&#039;&#039;, by [[Marie Roelandt]].&lt;br /&gt;
&lt;br /&gt;
Whether these texts share a single origin is not established. They share a sign inventory, the divider, and the same broad clause architecture; the Zagi Tablets add a cuneiform rendering and an Akkadian heading that the others lack.&lt;br /&gt;
&lt;br /&gt;
== Reception ==&lt;br /&gt;
The system&#039;s chief virtue is that it commits its users to nothing. Rubergskier&#039;s arithmetic reading, Voudrin&#039;s phonological partition and Roelandt&#039;s genealogical reading all operate on the same codes and can be compared token for token, which would be impossible had each study adopted its own transliteration. Roelandt&#039;s working transliteration into Latin letters is explicitly a readability aid layered on top of the codes, not a replacement for them.&lt;br /&gt;
&lt;br /&gt;
Critics note that the neutrality is also a limitation: the codes freeze Kristiansen&#039;s initial segmentation of the signs, and any sign he merged or split in 2023 is merged or split in every later study. Kristiansen himself has issued only minor corrections since, describing his role as “keeping the inventory honest.”&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Jan-Tage Kristiansen]]&lt;br /&gt;
* [[Scapula Glyph Inscription]]&lt;br /&gt;
* [[Dozenal Primer Inscription]]&lt;br /&gt;
* [[Zagi Tablets]]&lt;br /&gt;
* [[Language Codes]]&lt;br /&gt;
* [[Klema Field Notes]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&amp;lt;references /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Kristiansen corpus]]&lt;br /&gt;
[[Category:Writing systems]]&lt;br /&gt;
[[Category:Epigraphy]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=451</id>
		<title>Dozenal Primer Inscription</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=451"/>
		<updated>2026-09-22T10:27:17Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Regenerate glyph frequency table from current transcription (45 clauses, 838 tokens, 28 types; adds B05 and L02)&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 (45 clauses, 838 sign tokens, 28 sign types); positions are counted within words, with the clause-final divider P01 treated as bound to the preceding word. 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;
| 15 || 11 || 4 || –&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;
| 29 || 29 || – || –&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;
| 6 || – || 6 || –&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;
| 4 || – || 4 || –&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;
| 17 || 6 || 8 || 3&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;
| 8 || 6 || 2 || –&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;
| 36 || – || 15 || 21&lt;br /&gt;
|-&lt;br /&gt;
| B05&lt;br /&gt;
| [[File:B05.svg|24px|alt=B05|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | BOX-DOT-DIAG-SW-NE&lt;br /&gt;
| 3 || – || 3 || –&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;
| 109 || 50 || 29 || 30&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;
| 39 || 39 || – || –&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;
| 43 || 1 || 42 || –&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 || 2 || 1 || –&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;
| 32 || 32 || – || –&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;
| 4 || – || 4 || –&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;
| 20 || – || 20 || –&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;
| 40 || – || 38 || 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;
| 30 || – || 30 || –&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;
| 45 || – || 12 || 33&lt;br /&gt;
|-&lt;br /&gt;
| L02&lt;br /&gt;
| [[File:L02.svg|24px|alt=L02|link=]]&lt;br /&gt;
| style=&amp;quot;text-align:left;&amp;quot; | LINE-HOR&lt;br /&gt;
| 34 || – || 26 || 8&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;
| 18 || 4 || 13 || 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;
| 67 || – || 67 || –&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;
| 62 || – || 60 || 2&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;
| 45 || – || – || 45&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;
| 12 || – || 6 || 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;
| 48 || – || 45 || 3&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;
| 11 || – || 9 || 2&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;
| 9 || – || 6 || 3&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;
| 49 || – || 28 || 21&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-L02-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:L02.svg|20px|alt=L02|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-L02-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:L02.svg|20px|alt=L02|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 (one dozen)&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-S03-C03-T03-C01-M02-H02-B04&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=]][[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;
| Transparently ONE followed by the ×12 derivational bundle; no simplex sign sequence for twelve is attested in the inscription. Target of the complement sums 11+1, 10+2 and 6+6. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;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:Kristiansen corpus]]&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=450</id>
		<title>Dozenal Primer Inscription</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=450"/>
		<updated>2026-09-22T10:25:07Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: THREE and TEN: plain e is L02, not T02&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-L02-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:L02.svg|20px|alt=L02|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-L02-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:L02.svg|20px|alt=L02|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 (one dozen)&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-S03-C03-T03-C01-M02-H02-B04&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=]][[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;
| Transparently ONE followed by the ×12 derivational bundle; no simplex sign sequence for twelve is attested in the inscription. Target of the complement sums 11+1, 10+2 and 6+6. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;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:Kristiansen corpus]]&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=449</id>
		<title>Dozenal Primer Inscription</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Dozenal_Primer_Inscription&amp;diff=449"/>
		<updated>2026-09-22T10:23:35Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: TWELVE row: show attested kodaklef (ONE + x12 bundle) instead of bare suffix&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 (one dozen)&lt;br /&gt;
| &amp;lt;code&amp;gt;C01-S03-C03-T03-C01-M02-H02-B04&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=]][[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;
| Transparently ONE followed by the ×12 derivational bundle; no simplex sign sequence for twelve is attested in the inscription. Target of the complement sums 11+1, 10+2 and 6+6. &amp;lt;ref name=&amp;quot;Rubergskier2024&amp;quot; /&amp;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:Kristiansen corpus]]&lt;br /&gt;
[[Category:Klema]]&lt;/div&gt;</summary>
		<author><name>Mvuijlst</name></author>
	</entry>
	<entry>
		<id>https://yusupov.cloud/index.php?title=Yedoma_Ledger&amp;diff=448</id>
		<title>Yedoma Ledger</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Yedoma_Ledger&amp;diff=448"/>
		<updated>2026-09-22T10:19:19Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Yenoma -&amp;gt; Yedoma in name, lead and interpretation&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox&lt;br /&gt;
| name        = Yedoma Ledger&lt;br /&gt;
| image       = Yenoma Ledger.jpg&lt;br /&gt;
| caption     = Plaque with incised geometric registers&lt;br /&gt;
| 1 Material    = [[Ivory|Mammoth ivory]] (&#039;&#039;[[Woolly mammoth|Mammuthus primigenius]]&#039;&#039;)&lt;br /&gt;
| 2 Writing     = Geometric incisions&lt;br /&gt;
| 3 Created     = Substrate plausibly Last Interglacial (~125 ka); age of carving undetermined&lt;br /&gt;
| 4 Discovered_place = Lower [[Indigirka River|Indigirka]] basin, northeast [[Siberia]] (Russia)&lt;br /&gt;
| 5 Discovered_date  = Late August (year per preliminary report)&lt;br /&gt;
| 6 Present_location = Regional heritage authority&lt;br /&gt;
| 7 Dimensions  = ~216 × 54 × 39 mm; ~486 g&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;&#039;Yedoma Ledger&#039;&#039;&#039; is a worked plaque of mammoth ivory reportedly recovered from [[permafrost]] in the lower Indigirka region of northeast Siberia. The object was discovered and handled in the field by a small team led by [[J. Levi Schültke]] during a rapid-impact survey of thaw gullies commissioned to document erosion threats to organic artefacts; Schültke’s group was engaged for its prior experience with cold-chain recovery and identification of proboscidean ivory.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot;&amp;gt;J. Levi Schültke, &#039;&#039;A Mammoth-Ivory Plaque with Geometric Incisions from Northeast Siberia: Discovery, Context, and Materials Analysis&#039;&#039; (preliminary field and materials report, Seminar für Archäologie der Periglazialzonen, 2021).&amp;lt;/ref&amp;gt; Its carved surface shows parallel bands of straight and oblique incisions, including chevrons and cross-hatching. The preliminary report describes controlled cold-chain recovery and non-destructive documentation but does not advance a cultural attribution or secure the age of the carving itself.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Discovery and context ==&lt;br /&gt;
According to the report, the plaque was noticed by the team in a thaw gully within ice-rich loess ([[Yedoma]]) and recovered with surrounding sediment using bagged blocks and refrigerated transport overseen by the regional heritage authority.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot; /&amp;gt; Yedoma sequences in Siberia are known to preserve Late Pleistocene faunal remains and organic materials that can be reworked by [[thermokarst]] processes, complicating archaeological context.&amp;lt;ref name=&amp;quot;Schirrmeister2022&amp;quot;&amp;gt;Lutz Schirrmeister, “Yedoma Permafrost Landscapes as Past Archives and Future Change Hotspots,” &#039;&#039;Frontiers in Earth Science&#039;&#039; 10 (2022): 929873, [https://doi.org/10.3389/feart.2022.929873](https://doi.org/10.3389/feart.2022.929873).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Description ==&lt;br /&gt;
The object follows the natural camber of tusk ivory and presents a prepared, flatter face with multiple incised registers bounded by guide lines. Identification as mammoth ivory is supported by observation of [[Schreger lines]], a standard criterion in proboscidean ivory analysis.&amp;lt;ref name=&amp;quot;CITES2020&amp;quot;&amp;gt;“Identification Guide for Ivory and Ivory Substitutes,” CITES (2020), [https://cites.org/sites/default/files/ID_Manuals/Identification_Guide_for_Ivory_and_Ivory_Substitutes_ENGLISH.pdf](https://cites.org/sites/default/files/ID_Manuals/Identification_Guide_for_Ivory_and_Ivory_Substitutes_ENGLISH.pdf).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Dating ==&lt;br /&gt;
Screening methods reported include FTIR for collagen/mineral preservation, [[amino acid racemization]] (AAR) on a detached lamella, and [[optically stimulated luminescence]] (OSL) on associated sediment. These results are consistent with an ancient Pleistocene substrate but do not directly date the carving. Open-system behavior in bones and dentine limits the reliability of some radiometric approaches; targeted dating of incision surfaces has not yet been published.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;Penkman2022&amp;quot;&amp;gt;K. Penkman, “Dating the Paleolithic: trapped charge and amino acid geochronology,” &#039;&#039;Evolutionary Anthropology&#039;&#039; 31, no. 5 (2022): 203–222, [https://doi.org/10.1002/evan.21958](https://doi.org/10.1002/evan.21958).&amp;lt;/ref&amp;gt;&amp;lt;ref name=&amp;quot;Lian2006&amp;quot;&amp;gt;O. B. Lian, “Dating the Quaternary: progress in luminescence dating,” &#039;&#039;Quaternary Science Reviews&#039;&#039; 25, nos. 19–20 (2006): 2449–2468, [https://doi.org/10.1016/j.quascirev.2005.11.013](https://doi.org/10.1016/j.quascirev.2005.11.013).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Interpretation ==&lt;br /&gt;
Published descriptions remain neutral and descriptive; no consensus interpretation (notation, decoration, or other function) has been advanced. Pending secure dating and replication studies, the Yedoma Ledger is cited as a candidate example of worked ivory from permafrost context rather than as evidence for a particular symbolic system.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Significance ==&lt;br /&gt;
The find highlights both the potential and the difficulties of researching artefacts emerging from rapidly changing permafrost landscapes. It has been noted in discussions of best practice for recovery, minimal-intervention conservation (e.g. reversible consolidants), and multi-modal imaging such as [[reflectance transformation imaging]] in Arctic and sub-Arctic contexts.&amp;lt;ref name=&amp;quot;Schueltke2021&amp;quot; /&amp;gt;&amp;lt;ref name=&amp;quot;HE2016&amp;quot;&amp;gt;Historic England, &#039;&#039;Multi-Light Imaging for Heritage Applications (incl. H-RTI)&#039;&#039; (2016), [https://historicengland.org.uk/images-books/publications/multi-light-imaging-heritage-applications/](https://historicengland.org.uk/images-books/publications/multi-light-imaging-heritage-applications/).&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[Yedoma]]&lt;br /&gt;
* [[Woolly mammoth]]&lt;br /&gt;
* [[Pleistocene archaeology]]&lt;br /&gt;
* [[Reflectance transformation imaging]]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
{{reflist}}&lt;br /&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=Jan-Tage_Kristiansen&amp;diff=447</id>
		<title>Jan-Tage Kristiansen</title>
		<link rel="alternate" type="text/html" href="https://yusupov.cloud/index.php?title=Jan-Tage_Kristiansen&amp;diff=447"/>
		<updated>2026-09-22T10:16:41Z</updated>

		<summary type="html">&lt;p&gt;Mvuijlst: Eight families plus divider; add 2023 working paper and ELTTT affiliation&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Jan-Tage Kristiansen&#039;&#039;&#039; (born 1971) is a Norwegian-Estonian philologist and independent researcher based in Tallinn. He trained as a linguist at the University of Bergen, where his MA thesis examined positional constraints in runic epigraphy, before completing a doctorate at the University of Tartu in 2001 on graphemic variability in undeciphered scripts of the eastern Baltic region.&lt;br /&gt;
&lt;br /&gt;
Kristiansen worked for several years as a cataloguer at the Estonian History Museum before leaving institutional employment to pursue independent research. He is a corresponding member of ELTTT-Selskabet, a small learned society in Aarhus, under whose affiliation his longer work has appeared. He has described his working method as &amp;quot;palaeographic accountancy&amp;quot;: systematic inventory of graphemic features before any interpretive commitment.&lt;br /&gt;
&lt;br /&gt;
== Work ==&lt;br /&gt;
Kristiansen&#039;s correspondence note &amp;quot;Twin renderings, single template: a ruled signary on a putative cervid scapula&amp;quot; (&#039;&#039;language&#039;&#039; 27, October 2023: 1073–1074) introduced the alphanumeric coding system now used throughout the scholarly literature on the [[Scapula Glyph Inscription]] and related texts. The note described a short, four-line inscription on a scapula-shaped object, identified eight visually coherent sign families (Corner, Box, Triangle, Line, Tee, Comb, Meander and Barred-post, labelled A, B, C, H, L, M, S and T), reserved a ninth code, P, for the slash divider, and provided a full positional frequency table. Kristiansen declined to propose a cultural attribution, noting that circulating images of the object appeared to derive from a single original drawing and that higher-resolution imaging was desirable before drawing further conclusions.&lt;br /&gt;
&lt;br /&gt;
The coding system he established, in which each sign receives a family letter and index number (C01, B04, P01, and so on), was subsequently adopted without modification by [[Ginevra Rubergskier]], [[Camille Voudrin]], [[Inrik Üksküla]], and others, giving the note an influence disproportionate to its two-page length.&lt;br /&gt;
&lt;br /&gt;
Later in 2023 Kristiansen circulated a longer working paper, &amp;quot;A geometric signary on a scapula and a longer companion inscription: inventory, layout, and statistical structure&amp;quot;, under the ELTTT-Selskabet affiliation. It compiles the unified signary across the eight families, describes the scapula as a single ordered sweep through those families with slash separators, and reports positional biases, high-PMI collocations and bigram and trigram entropies below frequency-preserving shuffles in a six-line, 146-token companion inscription. The paper makes no claim of linguistic content and states that, given the anonymous digital-only provenance, a hoax or modern didactic pastiche cannot be excluded.&lt;br /&gt;
&lt;br /&gt;
Kristiansen has continued to maintain the signary as a stable reference document, issuing minor corrections to the preliminary frequency table following the appearance of higher-quality tracings in 2024. He has not published an interpretive article of his own, describing his role as &amp;quot;keeping the inventory honest.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Other interests ==&lt;br /&gt;
Outside the Kristiansen corpus, Kristiansen has published short notes on pre-Christian Baltic carved artefacts and on quantitative approaches to grapheme classification. He maintains a low profile and rarely attends conferences in person.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Scapula Glyph Inscription]]&lt;br /&gt;
* [[Dozenal Primer Inscription]]&lt;br /&gt;
* [[Zagi Tablets]]&lt;br /&gt;
* [[Ginevra Rubergskier]]&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=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>
</feed>