backup: 2026-06-17 07:36
This commit is contained in:
@@ -7,56 +7,9 @@
|
|||||||
|
|
||||||
* Architecture
|
* 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.)
|
See [[id:36fe6a01-ea1f-4785-8516-f6dcfecf05bb][azos/architecture]] for module registration, option namespace, file deployment, and Claude Code wiring.
|
||||||
- =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
|
|
||||||
|
|
||||||
* Conventions
|
* Conventions
|
||||||
|
|
||||||
@@ -70,16 +23,21 @@ Three azos-core features wire up Claude Code:
|
|||||||
- Packages in overlay: =pkgs.<name>= via =config.flake.overlayPkgs.<name>=
|
- Packages in overlay: =pkgs.<name>= via =config.flake.overlayPkgs.<name>=
|
||||||
- Unstable packages: =pkgs.unstable.<package>=
|
- 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
|
* Gotchas
|
||||||
|
|
||||||
- =azos-core= is a git submodule — =git add= must be run inside it separately from the outer repo
|
- =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
|
- 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
|
- =import-tree= auto-discovers features but only sees tracked files — same constraint
|
||||||
- The machine name is =lauretta=; machine config is at =_machines/lauretta.nix=
|
- 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)
|
- =postPatch= in Nix derivations: alejandra reformats indentation of multiline strings, which can change effective shell script content
|
||||||
- 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
|
|
||||||
|
|
||||||
* Key Files
|
* 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/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/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/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/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) |
|
| =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 |
|
| =~/.claude/settings.json= | Claude Code global permissions; always-allowed org-roam MCP tools listed 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
|
|
||||||
|
|
||||||
* Subnodes
|
* 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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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=
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
:PROPERTIES:
|
||||||
|
:ID: b8a9886a-d349-43e5-a745-817a148c1fd8
|
||||||
|
:END:
|
||||||
|
|
||||||
|
#+title: impl/datasets
|
||||||
|
#+filetags: :project: :knowledge: :datasets:
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
:PROPERTIES:
|
||||||
|
:ID: a53cbe84-cd8d-45c2-a8cf-34ab520a3ea5
|
||||||
|
:END:
|
||||||
|
|
||||||
|
#+title: impl/experiments
|
||||||
|
#+filetags: :project: :knowledge: :experiments:
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
:PROPERTIES:
|
||||||
|
:ID: 001430d5-e1e7-4e72-baf6-17399bfd6447
|
||||||
|
:END:
|
||||||
|
|
||||||
|
#+title: impl/loss-functions
|
||||||
|
#+filetags: :project: :knowledge: :loss-functions:
|
||||||
@@ -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
|
||||||
@@ -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≈35–55~, nose ~y≈65–85~, mouth ~y≈90–110~) 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>~
|
||||||
@@ -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 | 0–100 |
|
||||||
|
| 2–4 | sick, disabled, chronicDisease | 0/1 flags |
|
||||||
|
| 5–7 | eduStage (0–6), inCollege, collegeDropout | |
|
||||||
|
| 8–11 | employed, jobTier (0–4), incomeLevel, retired | |
|
||||||
|
| 12–16 | inRelationship, married, everDivorced, divorceCount, widowed | |
|
||||||
|
| 17–21 | 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
@@ -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)
|
||||||
@@ -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, // 0–3
|
||||||
|
hairColor, // 0–3
|
||||||
|
eyeColor, // 0–3
|
||||||
|
// 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: 0x00070000–0x000a0000,
|
||||||
|
// 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: 20–35 years before protagonist; Father: 20–38 years before
|
||||||
|
- Each grandparent: 20–35 years before their respective parent
|
||||||
|
- Siblings: 0–3, 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.
|
||||||
@@ -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 0–36,500 range — no interference with protagonist daily streams.
|
||||||
|
|
||||||
|
~window.__life~ exposes ~{ ..., schedule }~ alongside ~buf~, ~eventLog~, ~people~.
|
||||||
|
|
||||||
|
** Slot Layout
|
||||||
|
#+begin_example
|
||||||
|
Slots 0–11 06:00–09:00 morning routine (12 slots)
|
||||||
|
Slots 12–23 09:00–12:00 early daytime (12 slots)
|
||||||
|
Slots 24–43 12:00–17:00 afternoon (20 slots)
|
||||||
|
Slots 44–55 17:00–20:00 early evening (12 slots)
|
||||||
|
Slots 56–63 20:00–22:00 late evening (8 slots)
|
||||||
|
Slots 64–95 22:00–06: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
|
||||||
Reference in New Issue
Block a user