backup: 2026-06-17 07:36

This commit is contained in:
2026-06-17 07:36:46 +03:00
parent 78bcf45137
commit 4806acf57d
13 changed files with 544 additions and 66 deletions
+14 -66
View File
@@ -7,56 +7,9 @@
* Architecture
azos is a NixOS/home-manager configuration repo with a two-tier feature system:
Two-tier NixOS/home-manager feature system: =azos-core/= (shared submodule) + =features/= (machine-specific). Features auto-discovered via =import-tree=, collected into =config.flake.modules=.
- =azos-core/= — git submodule containing shared, reusable features (base, editor, claude-memory, claude-skills, dev, etc.)
- =features/= — machine-specific features (claude, encryption, hyprland, audio, etc.)
- =home-manager/home.nix= — home-manager entry point; manually lists all modules to import
- =_machines/= — per-machine NixOS configs; passes =suiteModules= as =specialArgs=
Feature auto-discovery is via =import-tree= in each flake.nix. Features from both
=azos-core/features/= and =azos/features/= are collected into =config.flake.modules=.
** Module registration pattern
Each feature's =default.nix= registers itself:
#+begin_src nix
config.flake.modules.homeManager.<name> = { lib, config, pkgs, ... }: {
options.azos.<name>.enable = lib.mkOption { ... };
config = lib.mkIf config.azos.<name>.enable { ... };
};
#+end_src
Modules are then imported in =home-manager/home.nix= via:
#+begin_src nix
imports = [ suiteModules.homeManager.<name> ... ];
#+end_src
** Option namespace
=azos.<feature-name>.<option-name>=
Dependencies between features use =lib.mkDefault true= to auto-enable:
#+begin_src nix
azos.claude-memory.enable = lib.mkDefault true;
azos.claude-skills.enable = lib.mkDefault true;
#+end_src
** File deployment
- Static files: =home.file."path".source = ./file;=
- Generated text: =home.file."path".text = "...";=
- Runtime scripts (e.g. merging JSON): =home.activation.<name> = lib.hm.dag.entryAfter ["writeBoundary"] ''...'';=
** Claude Code integration
Three azos-core features wire up Claude Code:
- =claude-memory= — registers org-roam-mcp as a global MCP server in =~/.claude.json=
- =claude-skills= — deploys skills to =~/.claude/commands/= and content to =~/.claude/CLAUDE.md=
- =claude= (=azos/features=) — installs claude-code, auto-enables the above two
See [[id:36fe6a01-ea1f-4785-8516-f6dcfecf05bb][azos/architecture]] for module registration, option namespace, file deployment, and Claude Code wiring.
* Conventions
@@ -70,16 +23,21 @@ Three azos-core features wire up Claude Code:
- Packages in overlay: =pkgs.<name>= via =config.flake.overlayPkgs.<name>=
- Unstable packages: =pkgs.unstable.<package>=
** Writing Claude Skills
Skill descriptions are routing keys — they determine whether a skill activates. Guidelines (ref: https://stevekinney.com/writing/agent-skills):
- Open with =TRIGGER on:= and numbered conditions so activation is unambiguous
- One specific job per skill; overlapping descriptions cause routing competition
- Scope exclusions: state what the skill does NOT replace to prevent false positives
- Domain-specific language; no generic phrases like "helps with X"
* Gotchas
- =azos-core= is a git submodule — =git add= must be run inside it separately from the outer repo
- Nix flakes only evaluate git-tracked files; new files must be staged (=git add=) before =nix build= will see them
- =import-tree= auto-discovers features but only sees tracked files — same constraint
- The machine name is =lauretta=; machine config is at =_machines/lauretta.nix=
- org-roam-mcp is forked at =anerisgreat/org-roam-mcp= (not upstream =aserranoni/org-roam-mcp=); fork fixes: =create_node= writes directly to SQLite so new nodes are immediately searchable (no emacsclient needed), and =cli_main= is defined natively (no postPatch needed)
- emacsql stores all Emacs strings in SQLite with surrounding ="..."= — the Python DB layer strips these with =_clean_path=/_clean_string=
- org-roam timestamps in SQLite are Emacs =(HIGH LOW USEC PSEC)= tuples: =HIGH = secs >> 16=, =LOW = secs & 0xFFFF=
- =postPatch= in Nix derivations: alejandra reformats the indentation of multiline strings, which can change the effective shell script content
- =postPatch= in Nix derivations: alejandra reformats indentation of multiline strings, which can change effective shell script content
* Key Files
@@ -89,21 +47,11 @@ Three azos-core features wire up Claude Code:
| =azos-core/features/claude-skills/default.nix= | global skills + CLAUDE.md deployment |
| =azos-core/features/claude-skills/skills/todo.md= | per-project TODO management skill |
| =azos-core/features/claude-skills/skills/project-brain.md= | org-roam second brain skill |
| =azos-core/features/claude-skills/README.md= | skills extensibility docs |
| =azos/features/claude/default.nix= | installs claude-code, enables claude-memory + claude-skills |
| =azos/home-manager/home.nix= | home-manager entry point, imports all modules |
| =azos-core/features/editor/emacs/config.org= | literate Emacs config (org-roam at line ~452) |
| =azos/azos-core= | git submodule pointing to shared feature library |
| =features/lauretta/emacs/config.org= | lauretta-specific Emacs overrides (agent-shell, LLM, beacon, caldav) |
| =~/.claude/settings.json= | Claude Code global permissions; read-only org-roam MCP tools are always-allowed here |
* org-roam Setup
- Directory: =~/roam/=
- Database: =~/.emacs.d/org-roam.db= (sqlite-builtin connector)
- MCP server: =org-roam-mcp= registered in =~/.claude.json= via home-manager activation
- org-agenda now includes =~/roam/= so TODOs in roam files appear in agenda
| =~/.claude/settings.json= | Claude Code global permissions; always-allowed org-roam MCP tools listed here |
* Subnodes
(none yet)
- [[id:36fe6a01-ea1f-4785-8516-f6dcfecf05bb][azos/architecture]] — module registration, option namespace, file deployment, Claude Code wiring
- [[id:c945da4f-de5c-4eb8-bd99-810576a2545a][azos/org-roam]] — setup, MCP server, SQLite internals, fork details
+59
View File
@@ -0,0 +1,59 @@
:PROPERTIES:
:ID: 36fe6a01-ea1f-4785-8516-f6dcfecf05bb
:END:
#+title: azos/architecture
#+filetags: :project: :knowledge: :architecture:
* Architecture
azos is a NixOS/home-manager configuration repo with a two-tier feature system:
- =azos-core/= — git submodule containing shared, reusable features (base, editor, claude-memory, claude-skills, dev, etc.)
- =features/= — machine-specific features (claude, encryption, hyprland, audio, etc.)
- =home-manager/home.nix= — home-manager entry point; manually lists all modules to import
- =_machines/= — per-machine NixOS configs; passes =suiteModules= as =specialArgs=
Feature auto-discovery is via =import-tree= in each flake.nix. Features from both
=azos-core/features/= and =azos/features/= are collected into =config.flake.modules=.
** Module registration pattern
Each feature's =default.nix= registers itself:
#+begin_src nix
config.flake.modules.homeManager.<name> = { lib, config, pkgs, ... }: {
options.azos.<name>.enable = lib.mkOption { ... };
config = lib.mkIf config.azos.<name>.enable { ... };
};
#+end_src
Modules are then imported in =home-manager/home.nix= via:
#+begin_src nix
imports = [ suiteModules.homeManager.<name> ... ];
#+end_src
** Option namespace
=azos.<feature-name>.<option-name>=
Dependencies between features use =lib.mkDefault true= to auto-enable:
#+begin_src nix
azos.claude-memory.enable = lib.mkDefault true;
azos.claude-skills.enable = lib.mkDefault true;
#+end_src
** File deployment
- Static files: =home.file."path".source = ./file;=
- Generated text: =home.file."path".text = "...";=
- Runtime scripts (e.g. merging JSON): =home.activation.<name> = lib.hm.dag.entryAfter ["writeBoundary"] ''...'';=
** Claude Code integration
Three azos-core features wire up Claude Code:
- =claude-memory= — registers org-roam-mcp as a global MCP server in =~/.claude.json=
- =claude-skills= — deploys skills to =~/.claude/commands/= and content to =~/.claude/CLAUDE.md=
- =claude= (=azos/features=) — installs claude-code, auto-enables the above two
+19
View File
@@ -0,0 +1,19 @@
:PROPERTIES:
:ID: c945da4f-de5c-4eb8-bd99-810576a2545a
:END:
#+title: azos/org-roam
#+filetags: :project: :knowledge: :org-roam:
* Setup
- Directory: =~/roam/=
- Database: =~/.emacs.d/org-roam.db= (sqlite-builtin connector)
- MCP server: =org-roam-mcp= registered in =~/.claude.json= via home-manager activation
- org-agenda includes =~/roam/= so TODOs in roam files appear in agenda
* Gotchas
- org-roam-mcp is forked at =anerisgreat/org-roam-mcp= (not upstream =aserranoni/org-roam-mcp=); fork fixes: =create_node= writes directly to SQLite so new nodes are immediately searchable (no emacsclient needed), and =cli_main= is defined natively (no postPatch needed)
- emacsql stores all Emacs strings in SQLite with surrounding ="..."= — the Python DB layer strips these with =_clean_path=/_clean_string=
- org-roam timestamps in SQLite are Emacs =(HIGH LOW USEC PSEC)= tuples: =HIGH = secs >> 16=, =LOW = secs & 0xFFFF=
+44
View File
@@ -0,0 +1,44 @@
* impl
ROLL (Ranking via Optimized Label Learning) — PyTorch research project implementing custom loss functions for binary classification using kernel density estimation (KDE) to optimize TPR at target FPR thresholds. Targets imbalanced classification problems.
** Architecture
- =src/roll.py= — Loss function implementations (Normal/Beta/Kernelized ROLL)
- =src/experiment.py= — Training loop, evaluation infra, =run_configurations()= entry point
- =src/datasets.py= — 10+ dataset loaders (KEEL, UCI, Kaggle, synthetic)
- =src/networks.py==KeelNet= MLP architecture
- =src/summary.py= — Plotly HTML visualization (ROC, score distributions, ECDF)
- =src/utils.py= — Logging, output dir creation (=init_experiment=)
- =experiments/keel/=, =experiments/other/=, =experiments/large/= — experiment scripts
** Conventions
- All hyperparameters in Python dataclasses (=ExperimentConfiguration=), no CLI parsing
- Experiments follow =experiment-*.py= naming; all call =run_configurations()=
- KEEL experiments share =experiments/keel/_base.py= runner; individual files just call it
- All datasets expose: =__getitem__=, =__len__=, =.x=, =.y= attributes
- Episode-based eval: N independent train runs per config, results aggregated
- GPU enabled via =cudaSupport = true= in flake.nix; =get_device()= in utils.py auto-selects GPU/CPU
- Beta distribution variant (=roll_beta_loss_from_fpr=) is kept for thesis writing but is not actively developed or used
** Gotchas
- Dataset paths injected as env vars by Nix shell hook (=$keel_wisconsin_dir=, etc.) — must use =nix develop=
- =_calc_moments()= in roll.py is unused and has a variable typo (=array= vs =arr=)
- CIFAR-10 binary: class 1 vs rest (not class 0)
- Multiprocessing uses =spawn= method via =torch.multiprocessing=
** Key Files
- [[file:src/roll.py][src/roll.py]] — Core loss: =KernelizedROLLoss= custom autograd Function with KDE backward pass
- [[file:src/experiment.py][src/experiment.py]] — =ExperimentConfiguration= dataclass, =Criteriorator= ABC, =run_configurations()=
- [[file:experiments/keel/_base.py][experiments/keel/_base.py]] — shared KEEL runner =run_keel_experiment()=
- [[file:flake.nix][flake.nix]] — Nix env with dataset downloads, hash-pinned, exports path env vars
- [[file:AGENTS.md][AGENTS.md]] — Project guidelines (naming conventions, env, dataset list)
** Subnodes
- [[id:001430d5-e1e7-4e72-baf6-17399bfd6447][impl/loss-functions]] — Loss variants, KDE internals, gradient computation
- [[id:a53cbe84-cd8d-45c2-a8cf-34ab520a3ea5][impl/experiments]] — Experiment structure, training flow, metrics, output layout
- [[id:b8a9886a-d349-43e5-a745-817a148c1fd8][impl/datasets]] — Dataset catalog, KEEL list, eval metrics
+6
View File
@@ -0,0 +1,6 @@
:PROPERTIES:
:ID: b8a9886a-d349-43e5-a745-817a148c1fd8
:END:
#+title: impl/datasets
#+filetags: :project: :knowledge: :datasets:
+6
View File
@@ -0,0 +1,6 @@
:PROPERTIES:
:ID: a53cbe84-cd8d-45c2-a8cf-34ab520a3ea5
:END:
#+title: impl/experiments
#+filetags: :project: :knowledge: :experiments:
+6
View File
@@ -0,0 +1,6 @@
:PROPERTIES:
:ID: 001430d5-e1e7-4e72-baf6-17399bfd6447
:END:
#+title: impl/loss-functions
#+filetags: :project: :knowledge: :loss-functions:
+51
View File
@@ -0,0 +1,51 @@
:PROPERTIES:
:ID: 6293baa2-c8a8-4c49-9284-1fa2eed75032
:END:
#+title: trmn
#+filetags: :project: :knowledge:
** Architecture
Static site (no backend). ~index.html~ + ~life.js~ + ~schedule.js~ as ES modules. Three.js + onnxruntime-web from CDN.
Served by Caddy via ~nix run~ on port 8080. Built by ~nix build~ (includes model weights in ~$out/model/~).
Domains:
- *Life generation*: ~life.js~ — deterministic 36,500-day procedural simulation → [[id:9465af82-4383-466c-bf09-5be19c328f0b][trmn/life]]
- *NPC people*: family tree + tracked friends/children, each with deterministic PersonRecord → [[id:423cac95-a80d-4db6-8bef-14297fb38437][trmn/people]]
- *Daily schedule*: ~schedule.js~ — 96-slot day, 68 scenes, deterministic from (masterSeed, currentDay) → [[id:ff8aa6b7-61ff-444c-9301-c0b666b0b573][trmn/schedule]]
- *Rendering*: Three.js low-poly 3D scenes, flat shading, cinematic 2.39:1 viewport
- *LLM dialogue*: TinyStories-8M ONNX INT8 bundled in ~/model/TinyStories-8M/~ → [[id:4b44cf43-6106-4498-81a3-b23ebb25dabf][trmn/llm]]
- *Assets*: SVG face sprites + GLB objects → [[id:4d5e6bc8-32eb-469f-a69f-84b14458c55b][trmn/assets]]
~window.__life~ exposes ~{traits, buf, eventLog, people, today, currentDay, masterSeed, schedule}~.
** Conventions
- Determinism: ALL randomness seeded from absolute time, never ~Math.random()~; use ~makeRng(masterSeed, day)~
- Low-poly aesthetic: ~MeshLambertMaterial~ with ~flatShading: true~, BoxGeometry for characters
- Cinematic viewport: ~aspect-ratio: 2.39/1~, FOV 26°, vignette via CSS ~::after~
- Scene background via CSS gradient on ~#stage~ div; canvas is ~alpha: true~
- Static assets: copy to ~$out/assets/~ in flake installPhase; generated assets built via Node.js in buildPhase
** Gotchas
- ~header Content-Type text/html~ in Caddyfile is wrong for multi-asset setups — omit it
- ~MeshFlatMaterial~ does not exist; use ~MeshLambertMaterial { flatShading: true }~
- ~onnxruntime-web~ must be a ~<script>~ tag (global), NOT an ES module importmap entry
- Never spread ~buf~ (Float32Array) in a loop — hangs browser; index directly with ~buf[d * FIELDS + F.X]~
- Nix flake ~src = ./.~ only includes git-tracked files → ~git add~ new dirs before ~nix build~
- eventLog entries are ~{id, personId?, attended?}~ objects, not plain strings — use ~e.id~ not ~e~
- All ONNX build/inference gotchas → [[id:4b44cf43-6106-4498-81a3-b23ebb25dabf][trmn/llm]]
** Key Files
- ~index.html~ — Three.js scene + LLM inference + life simulation bootstrap (three inline module scripts)
- ~life.js~ — full life simulation: PRNG, 31 events, 36,501-day loop, NPC people system
- ~schedule.js~ — daily schedule: 68-scene catalog, 10 life stages, forced-event overrides
- ~schedule-debug.mjs~ — CLI debug tool; ~nix run .#schedule -- [flags]~
- ~stats.mjs~ — aggregate stats across 1000 lives; ~nix run .#stats~
- ~flake.nix~ — build + Caddy server + ~tinyStoriesOnnx~ sub-derivation + ~stats~ + ~schedule~ apps
** Subnodes
- [[id:4b44cf43-6106-4498-81a3-b23ebb25dabf][trmn/llm]] — ONNX build pipeline, in-browser inference, TinyStories-8M gotchas
- [[id:9465af82-4383-466c-bf09-5be19c328f0b][trmn/life]] — life simulation: PRNG, state schema, event catalog, packed buffer layout
- [[id:4d5e6bc8-32eb-469f-a69f-84b14458c55b][trmn/assets]] — SVG sprite pipeline, GLB generation, face compositing
- [[id:423cac95-a80d-4db6-8bef-14297fb38437][trmn/people]] — NPC system: family tree, PersonRecord, PRNG isolation, post-apply hooks
- [[id:ff8aa6b7-61ff-444c-9301-c0b666b0b573][trmn/schedule]] — daily schedule: slot layout, scene catalog, life stages, forced events, debug CLI
+44
View File
@@ -0,0 +1,44 @@
:PROPERTIES:
:ID: 4d5e6bc8-32eb-469f-a69f-84b14458c55b
:END:
#+title: trmn/assets
#+filetags: :project: :knowledge: :assets:
:PROPERTIES:
:ID: trmn-assets
:END:
#+title: trmn/assets
#+filetags: :project: :knowledge: :assets:
[[id:6293baa2-c8a8-4c49-9284-1fa2eed75032][trmn]] — asset pipeline: SVG sprites + GLB objects
** Architecture
Two asset types, both served from ~$out/assets/~ by Caddy:
- *Sprites*: SVG files in ~assets/sprites/{eyes,nose,mouth}/~, static/committed, no build step
- *3D objects*: GLB files in ~assets/objects/~, generated during ~nix build~ by Node.js scripts in ~scripts/~
Face compositing at runtime: load SVG parts → draw onto ~OffscreenCanvas(128,128)~~THREE.CanvasTexture~.
Applied to the +z front face (index 4) of the head ~BoxGeometry~ via a material array.
** Conventions
- All sprite SVGs are 128×128; each draws its feature in the region it occupies on the face canvas
(eyes ~y≈3555~, nose ~y≈6585~, mouth ~y≈90110~) so all layers composite with ~drawImage(img, 0, 0, 128, 128)~
- Face part selection seeded from ~window.__life.masterSeed~ via inline ~mulberry32~ in the Three.js script block
- ~window.__life~ exports ~masterSeed~ (added alongside traits/buf/eventLog/today/currentDay)
- GLB generators: pure Node.js stdlib only (no npm), accept output path as ~process.argv[2]~
- Three.js importmap includes ~"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"~ for GLTFLoader
** Gotchas
- *Critical*: Nix flake ~src = ./.~ only includes git-tracked files (~git ls-files~).
New ~assets/~ and ~scripts/~ dirs must be ~git add~-ed before ~nix build~ or they are silently excluded.
- ~BoxGeometry~ material array face order: ~[+x, -x, +y, -y, +z, -z]~ → index 4 is the front face (camera-facing)
- GLB generator runs in Nix buildPhase and writes to the build dir; installPhase ~cp -r assets $out/assets~ picks up both committed sprites and the generated ~book.glb~
** Key Files
- ~assets/sprites/eyes/eyes_{1,2}.svg~ — wide-open vs narrow/tired eyes
- ~assets/sprites/nose/nose_{1,2}.svg~ — dot vs nostrils nose
- ~assets/sprites/mouth/mouth_{1,2}.svg~ — smile vs neutral mouth
- ~assets/objects/book.glb~ — generated at build time (not committed); dark-red flat box 0.14×0.18×0.03
- ~scripts/gen-book.mjs~ — pure Node.js GLB writer, no npm deps; ~node scripts/gen-book.mjs <outpath>~
+81
View File
@@ -0,0 +1,81 @@
:PROPERTIES:
:ID: 9465af82-4383-466c-bf09-5be19c328f0b
:END:
#+title: trmn/life
#+filetags: :project: :knowledge: :life:
** Architecture
~life.js~ is a standalone ES module (no dependencies) that exports:
- ~simulateLife(masterSeed)~~{ traits, buf, eventLog, people }~
- ~getDay(buf, day)~ → plain object (unpack one day from typed array)
- ~makeRng(masterSeed, day)~ → mulberry32 closure (day-local PRNG)
- ~DAYS~ (36501), ~FIELDS~ (22), ~F~ (field index map)
State is packed into ~Float32Array(DAYS * FIELDS)~ (~3.2MB). ~eventLog~ is ~Map<day, Array<{id, personId?, attended?}>>~ (entries are objects, NOT plain strings — changed when NPC system was added). ~people~ is ~Map<personId, PersonRecord>~ for all tracked NPCs. ~window.__life~ exposes ~{ traits, buf, eventLog, people, today, currentDay, masterSeed }~.
** PRNG
mulberry32 seeded per-day via splitmix32 hash:
#+begin_src js
makeRng(masterSeed, day) // derives seed from hash(masterSeed ^ day*0x9e3779b9)
#+end_src
- ~Math.imul~ throughout — bitwise-identical on all IEEE-754 JS engines
- Any day independently computable; no need to simulate prior days
- masterSeed = ~(Date.now() / 1000) | 0~ (wall-clock seconds)
- Reserved day slots: ~-1~ = protagonist traits; ~-2~ = family layout offsets (NPC system)
- NPC streams are fully isolated — see [[id:423cac95-a80d-4db6-8bef-14297fb38437][trmn/people]]
** State Fields (22 packed floats per day)
| Index | Field | Notes |
|-------|-------|-------|
| 0 | alive | 0/1 |
| 1 | health | 0100 |
| 24 | sick, disabled, chronicDisease | 0/1 flags |
| 57 | eduStage (06), inCollege, collegeDropout | |
| 811 | employed, jobTier (04), incomeLevel, retired | |
| 1216 | inRelationship, married, everDivorced, divorceCount, widowed | |
| 1721 | numChildren, friendCount, loneliness, wealthLevel, debtLevel | |
Fixed-at-birth traits (not packed): ~sex, introversion, ambition, resilience, baseHealth~
Internal-only (not packed): ~friendList, friendLifetimeIndex, childrenList, childLifetimeIndex~
** Scheduled Events (deterministic)
birth(0), kindergarten(1826), primary(3652), middle(5113), hs_grad(6575), college_grad(8036), retirement(23725), death_cap(36500)
Plus dynamically injected ~sibling_born~ entries (built inside ~simulateLife()~, merged + sorted into ~mergedSchedule~).
** Random Events (31 total, evaluated in priority order, cap 2/day)
Categories: death, disaster, health, financial, relationship, family, career, social
Death uses Gompertz curve: ~0.000008 * exp((age-40)/60 * 5) * healthMod * resilienceMod~
NPC-triggered events (not in EVENTS array): ~family_member_death~ (day loop step 4), ~friend_death~ (post-apply of lose_friend)
** Gotchas
- NEVER spread ~buf~ inside a loop — ~[...buf]~ on 800k-element Float32Array per iteration hangs the browser. Use ~buf[d * FIELDS + fieldIdx]~ directly.
- ~life.js~ must be listed in ~flake.nix~ installPhase (~cp $src/life.js $out/~) or Caddy serves a 404.
- eventLog entries are now ~{id, personId?, attended?}~ objects — any consumer using ~evs.includes('event_id')~ or ~counts[ev]~ will silently break. Use ~e.id~.
- ~breakup~ and ~marry~ both have weight 0.0008/day → equal competition → many characters die "in relationship, not married". May need tuning.
- "Unexpected end of input" on ~import('./life.js')~ usually means 404 (not rebuilt yet), not a real syntax error.
** Console Snippets
#+begin_src js
// Import and run
const { simulateLife, getDay, DAYS, FIELDS } = await import('/life.js');
const sim = simulateLife(42);
// Readable timeline (eventLog entries are objects now)
const timeline = [...sim.eventLog.entries()].map(([d, evs]) => ({
age: (d / 365.25 | 0), day: d, events: evs.map(e => e.id).join(', ')
}));
console.table(timeline);
// Inspect people (family + friends + children)
[...sim.people.entries()].forEach(([id, p]) =>
console.log(id, 'sex', p.sex, 'born', p.birthDay, 'dies', p.deathDay)
);
// Find death day (safe — no buf spreading)
const death = Array.from({length: DAYS}, (_, d) => d).find(d => d > 0 && sim.buf[d * FIELDS] === 0);
console.log('died age', death && (death / 365.25 | 0));
#+end_src
** Subnodes
- [[id:423cac95-a80d-4db6-8bef-14297fb38437][trmn/people]] — NPC system: PersonRecord schema, PRNG isolation, family generation, post-apply hooks, gotchas
+58
View File
@@ -0,0 +1,58 @@
:PROPERTIES:
:ID: 4b44cf43-6106-4498-81a3-b23ebb25dabf
:END:
#+title: trmn/llm
#+filetags: :project: :knowledge: :llm:
** Purpose
NOT a user-facing chat feature. This is infrastructure for *automatic procedural dialogue* between
characters in the show. The chat panel was a test harness only. Future use: generate spoken lines
for characters as part of the deterministic life-simulation loop.
** Architecture
In-browser LLM weights bundled into the site via ~nix build~. No backend, no external inference at runtime.
Model: ~roneneldan/TinyStories-1M~ — GPT-Neo architecture, ~model_type: "gpt_neo"~,
hidden=64, vocab=50257 (GPT-2 BPE tokenizer). Output is surreal/nonsensical children's story prose —
intentionally acceptable for this project.
Final ONNX INT8 size: *15 MB* (well under Cloudflare Pages 25 MiB per-file limit).
Runtime stack:
- ~onnxruntime-web~ loaded as a global ~<script>~ tag (not ESM importmap)
- ~@huggingface/transformers~ AutoTokenizer only (no pipeline) — tokenizer files served from ~/model/TinyStories-1M/~
- Custom top-k generation loop in JS; no KV cache (full sequence re-processed each step)
** Deployment constraint
Cloudflare Pages has a *25 MiB per-file limit*. This is why TinyStories-1M (15 MB) was chosen over 8M (40 MB).
TinyStories-8M's embedding alone (50k vocab × hidden=256 × FP32) is ~52 MB; ~quantize_dynamic~ can't touch it
(Gather op). TinyStories-1M's hidden=64 makes the embedding 4× smaller.
** Conventions
- ONNX model has ONLY ~input_ids~ (int64) as input; output: ~logits~ (float32 [1, seq, 50257])
- ~attention_mask~ is NOT an input to the exported ONNX graph (was patched away during tracing)
- Tokenizer: ~env.localModelPath = '/model/'; env.allowRemoteModels = false; AutoTokenizer.from_pretrained('TinyStories-1M')~
- Prompt must be story-style: ~`A man was asked "${text}". He smiled and said, "`~
- Do NOT use dialogue-format prompts like ~"you: X\nhim: "~ — model immediately predicts ~\n~, empty output
- Stop: first complete sentence ~(/^(.*?[.!?])/s)~ on decoded text with ~"~ stripped (~replace(/"/g, '')~)
- Top-k=40, temperature=0.9, max 60 new tokens
** Gotchas
*Nix build:*
- Do NOT use ~optimum~ — use ~torch.onnx.export~ directly with ~dynamo=False~
- Add ~onnxscript~ to nativeBuildInputs (PyTorch 2.12 lazily imports it even for legacy export)
- Patch ~create_causal_mask~ to ~lambda **kwargs: None~ before tracing (transformers 5.5.4 incompatibility)
- ~quantize_dynamic~ skips Gather (embedding) — the embedding dominates file size for large vocab models
- Tokenizer files (~tokenizer.json~, ~tokenizer_config.json~, ~special_tokens_map.json~) are identical
between TinyStories-1M and 8M — same GPT-2 BPE tokenizer; only ~config.json~ and ~pytorch_model.bin~ differ
- nativeBuildInputs: ~transformers onnxruntime onnxscript torch~ (no optimum)
*JS runtime:*
- Load ~onnxruntime-web~ via ~<script>~ tag BEFORE the importmap/module scripts
- Only pass ~input_ids~ to ~session.run()~ — passing ~attention_mask~ throws INVALID_ARGUMENT
- Generation is slow (no KV cache): each step re-runs full accumulated sequence
** Key Files
- ~flake.nix~ — tinyStoriesOnnx sub-derivation: fetchurl + torch.onnx.export + quantize_dynamic
- ~index.html~ — Two separate module scripts: Three.js scene + inference (ort global + AutoTokenizer)
+90
View File
@@ -0,0 +1,90 @@
:PROPERTIES:
:ID: 423cac95-a80d-4db6-8bef-14297fb38437
:END:
#+title: trmn/people
#+filetags: :project: :knowledge: :people:
** Architecture
Each NPC is a ~PersonRecord~ stored in ~people: Map<personId, PersonRecord>~ returned by ~simulateLife()~.
Family is generated at the top of ~simulateLife()~ via ~generateFamily(masterSeed)~ before the day loop.
Friends/children are created lazily via post-apply hooks when their triggering events fire.
#+begin_src js
PersonRecord {
id, role, // e.g. 'mother', 'sibling_0', 'friend_3', 'child_0'
seed, // uint32, derived from masterSeed + role salt
sex, // 0=female 1=male
birthDay, // days from protagonist day 0 (negative = already born)
deathDay, // days from protagonist day 0
hairType, // 03
hairColor, // 03
eyeColor, // 03
// friends only:
metOnDay?, // day protagonist gained this friend
lostOnDay?, // day the friendship ended
lostCause?, // 'death' | 'drifted' | 'moved'
}
#+end_src
** PRNG Isolation
Protagonist's daily stream (~makeRng(masterSeed, d)~) is NEVER consumed by NPC logic.
NPC logic uses two isolated streams:
- Each NPC's own rng: ~makeNpcRng(deriveNpcSeed(masterSeed, salt))~ — for visual attrs + death age
- Shared per-day NPC stream: ~makeRng(deriveNpcSeed(masterSeed, 0xffff0000), d)~ — for attendance rolls and lose_friend cause/pick rolls
Reserved day slots: ~day = -1~ (protagonist traits, existing), ~day = -2~ (family layout offsets, new).
#+begin_src js
const ROLE_SALTS = {
mother: 0x00010000, father: 0x00020000,
maternal_grandmother: 0x00030000, maternal_grandfather: 0x00040000,
paternal_grandmother: 0x00050000, paternal_grandfather: 0x00060000,
sibling_0..3: 0x000700000x000a0000,
// friends: 0x01000000 + friendLifetimeIndex
// children: 0x02000000 + childLifetimeIndex
// npc-stream: 0xffff0000
}
#+end_src
~deriveNpcSeed(masterSeed, salt) = splitmix32(splitmix32(masterSeed) ^ salt)~
** Death Age Sampling
Algebraic Gompertz inverse-CDF — no per-NPC simulation loop needed:
#+begin_src js
function sampleDeathAge(rng) {
const healthOffset = (rng() - 0.5) * 15; // draw 1
const u = clamp(rng(), 0.001, 0.999); // draw 2
const t = Math.log(1 - (Math.log(1-u) * 0.095 / 0.0003)) / 0.095;
return Math.round(clamp(t + healthOffset, 1, 108));
}
#+end_src
Draw order per NPC rng: sex → hairType → hairColor → eyeColor → healthOffset → u (sampleDeathAge).
** Post-Apply Hooks (inside simulateLife day loop)
After ~Object.assign(s, ev.apply(...))~, five events trigger NPC record creation/mutation:
- ~gain_friend~ → create PersonRecord, push to ~s.friendList~, increment ~s.friendLifetimeIndex~
- ~lose_friend~ → splice from ~s.friendList~, roll cause (death 20% / drifted 50% / moved 30%); if death, upgrade eventLog entry from ~lose_friend~ to ~friend_death~
- ~move_city~ → splice last min(5, friendList.length) friends, mark lostCause='moved'
- ~first_child~ / ~subsequent_child~ → create child PersonRecord, push to ~s.childrenList~
State additions to ~defaultState()~: ~friendList [], friendLifetimeIndex 0, childrenList [], childLifetimeIndex 0~
(These are not packed into the Float32Array buffer; only ~friendCount~ / ~numChildren~ are packed.)
** Family Generation
~generateFamily(masterSeed)~ uses ~makeRng(masterSeed, -2)~ to draw birth offsets:
- Mother: 2035 years before protagonist; Father: 2038 years before
- Each grandparent: 2035 years before their respective parent
- Siblings: 03, each within ±8 years of protagonist (some already born at day 0, some born later)
Grandparents with ~deathDay < 0~ were dead before protagonist was born — valid, just not triggering events.
** Events
- ~sibling_born~ (scheduled) — fires on sibling's birthDay if > 0; no state change, just logged with ~personId~
- ~family_member_death~ (loop, step 4) — fires when any non-friend NPC's ~deathDay === d~; logged with ~personId~ and ~attended~ (70% probability)
- ~friend_death~ (post-apply of lose_friend) — replaces ~lose_friend~ in eventLog when cause roll < 0.20
** Gotchas
- ~stats.mjs~ and any eventLog consumer must use ~e.id~ not ~e~ directly — entries are now ~{id, personId?, attended?}~ objects, not strings. Broke three spots in stats.mjs (all fixed).
- ~friendList~ and ~friendCount~ are kept in sync manually after every mutation. If they diverge, prereq ~friendCount > 0~ fires ~lose_friend~ but ~friendList~ is empty → guard: ~if (ev.id === 'lose_friend' && s.friendList.length > 0)~.
- Friend ~birthDay~ can be positive (friend younger than protagonist). At young protagonist ages (age 2), the ±15yr range means some friends haven't been born yet at meeting time. Cosmetic only — not used in any gameplay logic.
- ~sibling_born~ uses a dynamically built ~mergedSchedule~ (SCHEDULED + sibling events, sorted). The existing static SCHEDULED array is unchanged.
+66
View File
@@ -0,0 +1,66 @@
:PROPERTIES:
:ID: ff8aa6b7-61ff-444c-9301-c0b666b0b573
:END:
#+title: trmn/schedule
#+filetags: :project: :knowledge: :schedule:
:PROPERTIES:
:ID: trmn-schedule
:END:
#+title: trmn/schedule
#+filetags: :project: :knowledge: :schedule:
** Architecture
~schedule.js~ is a standalone ES module that exports:
- ~generateDaySchedule(today, traits, dayEvents, currentDay, masterSeed)~~ScheduleEntry[]~
- ~SCENES~ — plain object catalog of all ~68 scenes
- ~slotToTime(slot)~~"HH:MM"~ string (slot 0 = 06:00)
- ~SLOTS~ (96), ~SLOT_MIN~ (15)
~ScheduleEntry~: ~{ slot, duration, sceneId, location, label }~. Array is sorted by slot, non-overlapping, always covers all 96 slots with no gaps (builder fills remainder with ~sleep~).
RNG isolation: schedule uses ~makeRng(masterSeed, currentDay + 50000)~. Offset of 50,000 clears the life sim's 036,500 range — no interference with protagonist daily streams.
~window.__life~ exposes ~{ ..., schedule }~ alongside ~buf~, ~eventLog~, ~people~.
** Slot Layout
#+begin_example
Slots 011 06:0009:00 morning routine (12 slots)
Slots 1223 09:0012:00 early daytime (12 slots)
Slots 2443 12:0017:00 afternoon (20 slots)
Slots 4455 17:0020:00 early evening (12 slots)
Slots 5663 20:0022:00 late evening (8 slots)
Slots 6495 22:0006:00 sleep (32 slots)
#+end_example
** Life Stage Detection
~detectLifeStage(today, age)~ → one of 10 tags, checked in priority order:
~very_sick~ (sick + health<25) → ~sick~ → ~toddler~ (age<5) → ~kindergarten~ (eduStage=1) → ~school~ (eduStage=2/3) → ~college~ (inCollege) → ~retired~ → ~worker~ (employed) → ~unemployed~ (age≥16) → ~child~
** Forced Events
Events in ~dayEvents~ (from ~eventLog.get(currentDay)~) that override normal blocks:
| Event ID | Override |
|---|---|
| ~illness_acute~ | daytime → rest_in_bed; skip work/school |
| ~serious_accident~ / ~injury_serious~ | afternoon → emergency_room |
| ~first_child~ / ~subsequent_child~ | daytime → hospital_stay |
| ~marry~ | afternoon → wedding_ceremony; evening → wedding_reception |
| ~family_member_death~ (attended=true) | afternoon → funeral_ceremony + wake |
| ~move_city~ | full day → moving_boxes |
| ~premature_death~ | minimal day, idle |
** Gotchas
- ~jobCategory~ is NOT in the packed ~Float32Array~ buffer (not a field in ~F~). Schedule derives a stable per-character type from ~makeRng(masterSeed ^ 0x5A5A5A5A, 0)~. This is deterministic per character but may not match what was rolled in the life sim.
- Young-child stages (~toddler~, ~kindergarten~, ~school~, ~child~) must be branched BEFORE adult defaults in morning and evening blocks. The ~isYoungChild~ flag gates: no ~make_breakfast~, no ~cook_dinner~, no ~morning_coffee~, no ~morning_exercise~. The adult fallthrough path always ran these — easy to regress.
- ~go_to_park_with_parents~ is from the CHILD's perspective; ~hasYoungChildren~ is from the PARENT's perspective. They are mutually exclusive: ~hasYoungChildren = numChildren > 0 && age < 40~.
- The schedule builder (~makeBuilder~) clamps at slot 96: ~push~ is a no-op once cursor ≥ SLOTS. ~fill(sceneId, toSlot)~ is safe to call even if cursor is already past ~toSlot~.
** Debug Tool
~schedule-debug.mjs~ — CLI tool, ~nix run .#schedule~
- No args: random seed + random day from character's living years, full output
- Prints seed, day, age, full state, traits, events, then schedule table
- All state fields overridable: ~--seed~, ~--day~, ~--age~, ~--health~, ~--sick~, ~--employed~, ~--retired~, ~--married~, ~--children~, ~--friends~, ~--job-tier~, ~--loneliness~
- ~--event <id>~ injects a forced event (repeatable); injected events are labelled ~(injected)~ in output
- ~--help~ prints usage