Weksa/docs/targets/interlingua-lowering-map.md
Weksa 3d851bf0e1 lowerer-first-cut: lower(&LoweringInput,&Inventory) by a fixed English table keyed by inventory ids; weksa lower, score, packet proof, inventory proof; inventory rev 2 and the nibu-proof-001 packet
Measured against the proof lowering: item:nibu:1 chrF 23.12 cosine 0.6313,
item:nibu:2 chrF 25.98 cosine 0.7245, item:brokkr:1 chrF 19.42 cosine 0.4516.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 08:29:38 +00:00

38 KiB
Raw Permalink Blame History

Map: interlingua lowering (campaign weksa-lowering)

Prose map for the campaign whose target is interlingua-lowering.md. Rulings, questions, follow-ups and cut specs live in the Eureka mind under weksa-lowering:; this page is the model page and the cut sequence, and cites them by label. Written by Imagination for the Eureka-area Self, 2026-10-29.

The design in one paragraph

The interlingua spec has two layers. The schema is Rust types: packet families, node ids, role slots, value kinds. It changes rarely and by commit. The inventory is data in CultCache: the concepts, frames, roles, relation types, speech acts and nuance features (hedge, certainty, warmth, irony and whatever the loop finds missing) that the schema's slots name by id. Every open slot in a packet is an inventory id; a referent names an entity by id from the entity store, and a packet's only free text is the trace. No door judges text for leakage: what each model call may see, and what a score may count, own that (weksa-lowering:ruling:leak-owned-by-flow-and-scorer-3). The golf loop improves the spec mostly by growing and refining the inventory, which is fast; schema changes are the slow path. Concepts are open-class and grow forever; the other families are closed-class, and convergence is measured on them (weksa-lowering:ruling:convergence-test). This split was spec-is-schema-plus-inventory, now carried by leak-owned-by-flow-and-scorer-3.

Model page

Every persistent kind, what names it, its life, and who decides. "Derived" means no store holds it and nothing writes it.

Kind What names it Lifecycle Who decides
Packet Key packet:<16 hex>, an opaque digest of item, inventory revision and encoder (the item key lives in provenance.corpus_item, which the lowerer never sees); CultCache type weksa.interlingua_packet, carrying schema_version and inventory_rev. Every id inside is an opaque form type (n<k> nodes, encoder:<16 hex>, canonical <family>:<n>). From cut speaker-state (schema v4) it also carries the utterance stance: the speaker's felt affect and restraint, as inventory features of slot Stance (see Speaker state below) Minted by an encoder for one corpus item under one spec version; immutable; a new spec version means a new packet, never an edit. Admitted only through PacketStore::admit, which validates against the inventory store at the packet's revision The encoder writes; the form types refuse malformed ids at construction and load, the validator refuses broken references and over-capacity packets: a feature at most once per slot, at most 64 nodes (typed-packet); nothing else writes packets
Schema version weksa.interlingua_packet.v<N> constant in crates/weksa; published JSON Schema at schemas/cultnet/ Bumped by a commit that changes the Rust types; the published schema is regenerated in the same commit and a test fails on drift A cut (Hands, under a spec); never the loop
Inventory entry Key <family>:<n>, n canonical decimal >= 1 (families: concept, frame, role, relation, speech_act, feature, referent_kind, extension); type weksa.inventory_entry, with a bounded gloss, value kind (option labels bounded), the slots a feature may sit in (Discourse, Referent, Predication, Relation; Stance from cut speaker-state, Speaker and Scene from cut speaker-scene), introduced_rev and optional retired_rev. Speaker axes, scene relations and affects are features of those slots, so the loop grows them like any closed-class family and convergence covers them Added or retired by an inventory revision; never edited in place (a changed meaning is a retire plus an add); a retired id still resolves for packets of earlier revisions The loop's accepted rounds, whose minting call the context builder assembles from Train items only; Self on merge to main. Admission checks form, not text
Inventory revision Key rev:<n>, type weksa.inventory_revision, with parent, change list and frozen Appended per accepted round; exactly one revision is ever marked frozen, by the convergence test The loop's accept rule appends; the convergence test freezes; Self records the freeze as a ruling
Target profile Locale or language-project id (en-US, ja-JP, a conlang's slug) plus a revision profile:<language>:<n> Authored, revised by commit or by an accepted round that changed the lowering; each lowering names the revision it used Profile authors and accepted rounds; never the lowerer's defaults (persona-drives-register)
Language ontology entry (cut language-ontology) Key onto:<language>:<n>; type weksa.ontology_entry: conditions over feature values (stance, speaker, scene, packet features) and community ids, and the surface choice they select (layer: lexicon, grammar, discourse, phonology, orthography; a bounded descriptor and at most three bounded examples); introduced and retired by a profile revision Added or retired by a profile revision, never edited; converted from data/target-language-ontology/*.yaml, which is deleted in the same cut so the YAML is not a second owner The language's owner (the operator for her settings' languages) and accepted rounds; an ontology-minting call is assembled from Train items only, like an inventory-minting call, and its text is in the scorer's leak diagnostic. Selection is deterministic code (select(profile, stance, speaker, scene)); the lowerer realizes the selected choices and the trace lists their ids (lowering-traced)
Community (cut language-ontology) Key community:<slug>; type weksa.community, converted from data/cultural-ontology/cultures/ and referents/ (YAML deleted in the same cut) Revised by commit; a speaker names the communities it belongs to by id The cultural ontology's author (owner-attested evidence only; Discord-only content stays deleted, discord-only-content-deleted)
Speaker (cut speaker-scene) Key speaker:<slug>@<n>; type weksa.speaker: stable axes as feature values of slot Speaker (origin, socioeconomic standing, age cohort, education and whatever the loop adds), at an inventory revision, and an Annotator A new revision appends, never edits; a corpus item and every prior turn name a speaker slug, and a round records the speaker revisions it read The annotator it names: PersonaSnapshot (a projection of a gamecult.persona_state.v0 document, naming its source document and updatedAt), Operator, WorkMetadata (a public-domain work's own cast list and narration), or Model { family, generator } (the 2b ModelFamily)
Scene (cut speaker-scene) Key scene:<group>@<n>, the item's split group; type weksa.scene: participants (speaker slugs) and, per ordered pair, relation features of slot Scene (relative status, intimacy, in-group, audience and whatever the loop adds), at an inventory revision, and an Annotator As Speaker. Keyed by the group, so a scene never straddles the split As Speaker
Corpus item Key item:<source-slug>:<n>, n canonical decimal >= 1 (no sign, no leading zero, from cut 2b); type weksa.corpus_item with text, language, speaker, split group, prior turns, source. Source ModelWritten carries a ModelFamily enum, the generator string as the fixture wrote it, and a prompt digest (cut 2b). The item carries no speaker state of its own: its speaker slug and group name the Speaker and Scene documents Admitted once; the whole item is immutable (re-adding an identical item is a no-op, any difference is refused; a correction is a new item that supersedes the old); text already held under another key is refused unless the new item supersedes that key (cut 2b); withdrawn, never deleted, so old scores stay explainable Importers and weksa corpus add, under corpus-cast-plus-public-domain; the source enum has no variant for Discord or any training-forbidden source (corpus-clean)
Split assignment Derived: split(scheme, item.group) Fixed for a scheme; a new scheme is a new constant, never a reassignment. A model-written item whose group derives test is in no split; a superseding item keeps its predecessor's group The hash function. Nobody assigns by hand, so there is no writer to forbid
Loop round Key run:<label>:round:<n>; type weksa.loop_round with spec version, profile revisions, model ids and prompt digests, sampled train items, proposed change, validation scores before and after, outcome Opened, scored, closed accepted or rejected; immutable once closed. Lives in the run store on Yggdrasil The loop driver's accept rule (validation composite improves past the measured noise floor and no metric regresses past tolerance)
Score Key <lowering-key>:<metric>@<metric-version>; type weksa.score Immutable; a new metric version scores again under a new key The scorer, deterministic given the metric version and the embedder's model id. Cut scorer computes scores as values; cut golf-loop-en stores them, because the lowering keys they hang from arrive with the loop
Lowering trace Key lowering:<packet-key>:<profile>@<rev>:<lowerer-digest>:<sample>; type weksa.lowering holding the surface text and its trace together Immutable. A surface text cannot exist without its trace, because they are one record (lowering-traced) The lowerer emits; the lowerer sees the packet without its trace (the input type has no such field), never the corpus item
Conlang lexicon entry Key lex:<language>:<concept-id>; root form, morphology class Root placed at its concept's embedding when the concept first needs a form in that language; stable once accepted; diachrony derives later forms, it does not overwrite The language project's owner: the operator for her settings' languages, the loop proposing roots for her review
Conlang grammar and phonology data <language>/grammar, <language>/phonology documents per language project Revised by commit; every lowering names the revision The language project's owner
Training pair (later) Key pair:<frozen-rev>:<item-id>:<profile> Created only by re-encoding the whole corpus under the frozen inventory revision; never before the freeze (staged-distill-every-language-equal); split inherited from the item The re-encode job, which can only be constructed from a frozen revision

Authority map (the packet boundary)

  • Owner: the validator in crates/weksa owns whether a packet exists.
  • Inputs: the packet, the inventory at the packet's revision.
  • Outputs: an admitted packet, or a typed refusal naming the rule.
  • Derived state: split assignment (from the item's group); the lowering view of a packet (the packet minus its trace).
  • Forbidden writers: the Node daemon's decomposeThoughtIntoInterlingua and normalizeInterlinguaPacket (parked, cut park-node-body); the Metame harness's packet-like YAML (parked); any loop step that writes a packet without the validator.
  • Shared paths: the loop's encoder, the CLI, tests and the future live lowering all admit packets through the same validate call.
  • Deletion line: the Node body goes before the crate lands, so no second packet shape survives on main.

Speaker state (persona-drives-ontology)

Operator, 2026-10-29: "The specific choice of words of a character can tell you exactly where they were from, their socioeconomic status, their relative status within the scene, and both how they feel and how restrained they are being with those feelings. The linguistic ontology holds this data and drives it from the persona." Speaker state is three layers, and each lives where its writer and lifetime put it:

Layer Examples Writer Lifetime Lives in
Stable axes origin and community, socioeconomic standing, age cohort, education the persona (live) or an annotator (corpus) the speaker's weksa.speaker
Scene relations relative status, intimacy, in-group or out-group, who overhears the scene (live) or an annotator (corpus) the scene's weksa.scene
Utterance stance felt affect, restraint whoever writes the utterance: the persona agent (live), the encoder (corpus) the utterance's the packet (stance)

Affect and restraint live in the packet, as stance, not in a speaker-state document. A per-utterance speaker-state record would have the packet's key, the packet's writer and the packet's lifecycle (minted once per utterance under one spec version, immutable), so it would be a second document under one authority; it collapses into the packet. It also has to be golfed: in the loop the encoder reads felt affect and restraint off the corpus line, as it reads the meaning, and the loop improves that reading round by round. A fixed annotation could not be golfed. Live, the agent writes its own packet (campaign-direction), so the persona drives stance through it. The stance is a separate section from the meaning (referents, predications, relations): a feeling the speaker holds back is not a claim, and packet-owns-meaning still forbids a lowering to add one. "I am angry" said outright is a predication; anger leaking through clipped syntax is stance.

The stance carries felt affect and restraint, never displayed affect. How much of a held-back feeling shows, and how, is a cultural display rule, and display rules differ by culture (Ekman and Friesen 1969; Matsumoto's cross-cultural display-rule studies; from recall, not a search this pass). If the packet carried displayed intensity, the English reading would be baked in and a ja-JP lowering would apply restraint twice. So each language's ontology maps felt affect, restraint, scene and speaker to its own surface, which is staged-distill-every-language-equal applied to affect. Whether restraint is one grade per utterance or one per felt affect is the speaker-state spec's call; the loop can split it later if one grade loses.

Demotions. formality, warmth and intensity (seed features 8, 9, 10, slots Discourse and Predication) are no longer packet owners. Formality is derived from scene relations and stable axes by the target language's ontology; displayed intensity is derived from felt affect and restraint; warmth toward the addressee is a felt affect in the stance. Cut speaker-state removes them from the seed (no committed packet store exists and nothing outside the seed and the standard names them; Body facts), so no packet can set formality beside the ontology. hedge, certainty and irony stay: they are the speaker's epistemic and pragmatic stance toward the content, language-neutral, not register.

Corpus items carry speaker state as typed ontology values, never free text. The item names its speaker slug and group; the Speaker and Scene documents hold feature values of the Speaker and Scene slots (grades, option labels, flags, and later community ids), validated against the inventory by the same feature check the packet uses. No field is free text, so no wording can reach the lowerer through annotation (no-surface-leak). Cast items: the speaker document is a PersonaSnapshot of the cast member's persona state where the standard has the value, and annotated where it does not (below). Annotations of stable axes read the whole work or persona, so a speaker's axes are not drawn from one scored line; a scene's relations read the scene, which includes its scored lines, and are bounded typed values, as a live scene would supply.

Persona state today has no field for origin, community or socioeconomic standing. gamecult.persona_state.v0 (Body facts) carries open trait maps (activationProfile.voiceStyle and five more), moodDimensions, socialBonds (trust, tension, intensity) and statusReads (authority, trust and others). So a PersonaSnapshot projects mood-like and relation-like values, and the stable axes the standard lacks are annotated. Whether the standard gains a typed speech-community reference or Weksa keeps the persona-to-community binding (data/cultural-ontology/persona-stacks/) is decided when live-lowering is mapped (follow-up persona-state-speech-community); the corpus path needs neither. Putting Weksa feature ids into the open voiceStyle trait map is rejected: it would make an extension field portable authority, which the standard forbids.

Public-domain annotation and the annotator ceiling. A public-domain play has tens of speakers and scenes, so annotation is per speaker and per scene, not per line, and costs nothing per round. Stable axes come first from the work itself (WorkMetadata: the cast list, narration, stage directions). The ceiling: the loop can learn surface choices only as well as the speaker and scene values describe the line, so annotation errors become ontology errors (a servant labelled gentry teaches the ontology the wrong deference). And when a Claude annotator, a Claude encoder and a Claude lowerer share one family's idea of what low status sounds like, the loop agrees with itself and scores the agreement: the round-trip inflation the target already records. Every annotation names its annotator, so the scorer can report a test composite over operator-attested annotations alone. Who annotates is question pd-speaker-annotation.

Scoring (golf-metric-automatic, conlang-scoring, meaning-metric-embedding-cosine)

  • English against its corpus item: chrF (character n-gram F-score, char order 6, beta 2, spaces removed; parity vectors against sacrebleu 2.4.3 in Body facts) and meaning cosine (sentence embeddings from qwen3-embedding:0.6b, served by Ollama on Yggdrasil; the adapter sends options.num_thread, Body facts). Probe on one reference: hedged paraphrase 0.948, flattened claim 0.643, register-shifted 0.720, negation 0.496. BERTScore proper needs per-token transformer embeddings and a torch stack; it is deferred, not adopted.
  • Every language by the same meaning cosine: the embedder reads the output directly when it can (natural languages, since the embedder is multilingual); for a conlang it reads the deterministic gloss made from the language's own lexicon (Weksa owns the lexicon, so reading it back needs no model). Plus lexicon adherence for a conlang: the share of tokens that parse against its lexicon and morphology.
  • A style axis follows as its own cut, behind the same scorer port.
  • Names and leak (leak-owned-by-flow-and-scorer-3): no packet field carries free text, so the scorer masks nothing. A referent names an entity by id (EntityId, entity:<slug>); the entity store (weksa.entity) holds each entity's forms per language, citation form first, then inflected or particle-bound forms (data, as the inventory is). Entities are minted at intake: cast entities from the cast fixtures, public-domain entities by the importer, and in live use by the persona before it names one. A corpus item carries its name spans (byte ranges of its text, each naming an entity and on extended grapheme cluster boundaries of its NFC text and equal to one of that entity's NFC forms in the item's language; an entity holds at most 8 languages of BCP 47-form tags, 8 forms each, a form at most 64 bytes with no invisible format character), and its speaker and prior-turn speakers are entity ids. The packet store refuses a packet whose referent cites an entity the store lacks, and a packet with a corpus_item (required, typed CorpusItemId) that cites an entity neither annotated on that item nor one of its speakers: an id-set check, not a text rule. The scorer reads only the output and the reference and scores the whole of each. An honest name earns its credit ("Brokkr came back." against itself is 100), a wrong one loses it, filler costs what filler costs (Soul scorer s2: "Darling, I am truly not angry, darling." scores 46.84 whole; the output-only mask, fed the filler as a name, had raised it to 86.78), and no rule depends on script or word segmentation, so Thai, Korean particles and Turkish or Finnish suffixes are scored by the same character n-grams as English. Why this is not a leak: leakage is information the learner has that it would not have at prediction time (Kaufman et al., TKDD 2012). A live persona knows which entity it means; the annotation gives the corpus encoder exactly that, fixed at intake before any round and written by no loop writer, and the encoder chooses only which of the item's own ids to cite. The free text left is inventory labels and glosses (Train-only, the context builder's) and the trace (never lowered or scored). Diagnostics, reported and never gates: max pairwise chrF of minted text against held-out items; novel coverage pooled per order, for n = 1, 2 and 3 separately, the covered share of all novel n-grams (word n-grams of a held-out item that occur in no Train item) over a split's items, with the worst item at n >= 2. Keys split on _, . and lower-to-upper case transitions as well as UAX #29 boundaries, so a snake_case OptionLabel reads as words. Honest glosses of single words saturate n = 1 (the 54 seed glosses read max 1.000, s2), so the leak signature is pooled n >= 2. Costs: a live persona mints before it names (one typed write, which also buys coreference and one rendering per language); public-domain intake annotates name spans (follow-up pd-name-spans, annotator per pd-speaker-annotation); the lowerer inflects a name from its stored forms or adds one, traced. Prior art (recall): AMR keeps :name strings inside the graph beside a :wiki link, and AMR-to-text systems copy those strings; Abstract Wikipedia renders constructors over Wikidata item ids, with labels per language and Lexeme forms for inflection; Grammatical Framework lexicons hold proper names (PN) with per-language inflection tables; referring expression generation (Reiter and Dale 2000; the GREC tasks, 2008-2010) chooses a name, pronoun or description from an entity id and the discourse. Rejected paths: text rules at the inventory and packet doors (Soul packet-store s2); any mask of names in the scored texts: masking reference and output (s1: "Kick Bucket" took chrF 53.73 to 100) and masking the output only (s2: named filler took chrF 46.84 to 86.78, cosine 0.728 to 0.886); equal-length placeholder masking (bounds chrF, not cosine, and still pays nothing for an honest name); character 6-gram coverage (one-word glosses read 0.000); max-over-items coverage (saturates on honest runs).

Body facts (probes of 2026-10-29, map pass for speaker state, 2b and scorer)

  • Weksa main 869c82b (packet-store, 2b corpus labels, 2c entity store, scorer landed; packet schema v3). Annotators land with their consumers: Operator and Model { family, generator } with cut speaker-scene (the cast seeds are Model-annotated and an operator attests by appending an Operator revision), WorkMetadata with pd-importer, PersonaSnapshot with live-lowering.

  • UAX #29 word probe (unicode-segmentation 1, scratch crate, 2026-10-29): unicode_words() counts (whitespace count in brackets) "Brokkr" 1 [1], "Mary Ann Evans" 3 [3], "de la Cruz y Garcia" 5 [5], Thai "สมชาย" 5 [1], "สมชาย ใจดี" 8 [2], "山田太郎" 4 [1], "李小龍" 3 [1], "ブロッカー" 1 [1], "김민준" 1 [1], "Jean-Luc" 2 [1], "O'Brien" 1, five letters glued by ZWSP 5 [1], glued by WJ U+2060 1 [1]. Without a dictionary UAX #29 splits Thai and Han per character, so a 4-word UAX #29 bound refuses ordinary Thai names (question form-word-bound, recommended bytes-only). NFKC, default case fold, NFKC, alphanumerics only: "fine" -> "fine", "Brokkr" -> "brokkr", ZWSP and WJ drop out.

  • Weksa main 28284cc: cargo test -p weksa on Yggdrasil (ygg-verify, rust image) 73 passed. weksa corpus dump holds three items: item:brokkr:1 (group brokkr-edda-voice, Validation, ModelWritten { model_family: "local-source-aware-draft" }), item:nibu:1 (group nibu-proof-001, Test, CastAuthored, fixture examples/english-lowering/nibu-proof-001/lowered-output.yaml, which records no author) and item:nibu:2 (same group, excluded, ModelWritten { model_family: "unrecorded-agent-run" }; that string exists only in corpus.cc). The Brokkr fixture's generator was codex-local-source-aware-draft at 4197123 and reads local-source-aware-draft on main.

  • CorpusStore::add compares only text (store.rs:166-171): re-adding a key with the same text and a different source returns Unchanged and writes nothing, so a relabel through add silently does nothing. key_slug parses n with u32::parse, which accepts 01 and +1.

  • Inventory rev 1 (seed): features 6-11 are hedge, certainty, warmth, formality, intensity (Grade) and irony (Flag), slots Discourse and Predication. Nothing outside the seed and docs/interlingua-standard.md:179-180 names warmth, formality or intensity; no committed packet store exists.

  • Yggdrasil's Ollama (127.0.0.1:11434) is the Docker container ollama-cpu, Huginn's embedder (runbook gamecult-ops/runbooks/huginn-yggdrasil.md, ruling eureka-body:ruling:huginn-embedder-yggdrasil-primary). /api/embed with an input array returns unit-norm 1024-dimension vectors. Under host load (load average 18-24 from ygg-verify rustc jobs) and no num_thread: a batch of 6 took 21.5 s and 29.8 s, a batch of 32 timed out at 120 s. With options.num_thread 8: a batch of 6 in 2.1 s, a batch of 32 in 5.8-6.4 s (0.18-0.20 s per item); num_thread 4: 8.8 s for 32. Cosine of "Take your hand off that control." against its lowercased, unpunctuated form: 0.943. One server serves Huginn and the loop, so a 32-item batch holds it about 6 s.

  • chrF parity: sacrebleu 2.4.3 sentence_chrf(hyp, [ref]) and a Python reimplementation (orders 1-6, per-order precision and recall averaged over the orders where both sides have n-grams, F-beta with beta 2, spaces removed) agree to 4 decimals on six pairs (reference / hypothesis / score): "Take your hand off that control unless you want your nervous system to become my least useful circuit diagram." / "Take your hand off that control, unless you'd like your nervous system to be my least useful wiring diagram." / 74.4498; "The command has no receipt. It is not truth." / "The order carries no receipt, so it is not true." / 45.7164; "I am not angry." / the same / 100.0; "Forgive me, sir." / "Get out." / 3.9683; "その制御に触るな。" / "その操作盤に触らないで。" / 14.2931; "Well, aren't you a sight for sore eyes, love." / "" / 0.0.

  • Scorer s1 follow-up probe (sacrebleu 2.4.3 in python:3.12-slim on Yggdrasil; qwen3-embedding:0.6b, one 8-text call). sacrebleu strips every whitespace char before gramming: hypothesis "a b c", "a b", "ab", "a…b", "a
b c", "line one line two", "その 制御" against the same text with that char removed (or "line one line two") all score 100.0; "a​b" against "ab" scores 45.4545 (zero-width space is not whitespace). Name scoring, output masked and reference whole (hypothesis / reference / chrF / cosine): "He is going to die tomorrow." / "He is going to kick the bucket tomorrow." / 53.7347 / 0.9081; "He is going to the tomorrow." (named or omitted) / same / 55.9583 / 0.8246; "Not ever again." / "Never again." / 85.1384 / 0.9169; "." (named) / same / 11.1111 / 0.6456; "came back." / "Brokkr came back." / 56.6137 / 0.7364; "Kestrel came back." / same / 51.3670; "not go." / "Will will not go." / 33.6248.

  • gamecult.persona_state.v0 (F:\Projects\Epiphany\schemas\cultnet\gamecult.persona_state.v0.schema.json): required activationProfile (six open trait maps of mean, plasticity and current activation) and affect (needs, socialBonds, statusReads, moodDimensions, socialBiases, doctrineStances); presentation.voiceSummary is free text; no field names origin, community or socioeconomic standing; extensions are non-authoritative by the schema's own description.

Where things live and run (rust-single-crate, loop-runs-on-yggdrasil)

  • Code: one Cargo workspace at the repo root, one package crates/weksa (a library and the weksa binary). CultCache through cultcache-rs pinned by git rev, as Huginn pins it. No daemon: the loop is a batch command. A daemon is earned later only by the live lowering service, if isolation protects a named invariant there.
  • Stores: data/interlingua/inventory.cc and data/corpus/corpus.cc in the repo (small, reviewed with weksa dump); run stores (rounds, lowerings, scores) on Yggdrasil under the loop's state root, not in git.
  • The loop runs on Yggdrasil as a detached unit with a log and a status record. Starfire runs none of it. Model calls go through a ModelPort; its first adapter drives claude -p on the operator's subscription. Never OpenAI (eureka-body:ruling:no-return-to-openai-supplier), never Discord data (gamecult-chat:ruling:discord-taints-training).

Convergence (convergence-test)

The spec has converged when, on a panel of en-US, ja-JP and the first conlang, for 20 consecutive rounds no accepted change to a closed-class inventory family or the schema raised the validation composite past the noise floor, and at that checkpoint the test-split composite is within the noise floor of validation. The test split is read only at checkpoints. Self then records the freeze, and the training set is the whole corpus re-encoded under the frozen revision.

Lowerer first cut (golf-loop-lowerer-cut)

Authority map for the smallest lowerer, written before its code (Imagination pass by the Weksa Self run, 2026-11-01). The cut exists to give the golf loop a measurable baseline from a packet: lower gives text, score gives numbers.

Decision Owner Inputs Outputs Not the owner
Whether a packet exists validate / PacketStore::admit (unchanged) packet, inventory, corpus, entities admitted packet or typed refusal the lowerer
Wording of a packet in English crates/weksa/src/lower.rs: lower(&LoweringInput, &Inventory) -> Result<String, LowerError> the validated lowering view and the inventory at its revision text, or a typed refusal naming the missing template the corpus item, the scorer, any model
English template table (concept words, frame templates, speech-act openers) the same module, as rows (inventory id, gloss it speaks for, surface) none; it is data in code rows the lowerer looks up the inventory (it owns the ids and glosses, not the English)
The inventory's concepts and frames an inventory revision; here inventory::proof_revision() (rev 2), hand-seeded as seed() is none the 5 concepts and 2 frames nibu-proof-001 names the lowerer
What a score counts Scorer::score (unchanged) output text, reference text ItemScore weksa score, which only reads text and calls it
The proof packet packet::proof_packet(), typed ids of rev 2, admitted by weksa packet proof <store> through PacketStore::admit. Not JSON: Packet's serde is CultCache's positional tuple, and JSON in is the xenos boundary the cut 7 encoder owns none admitted packet the CLI decides nothing about validity

Decisions this map makes, and why:

  • lower takes &LoweringInput, not &Packet. A Packet carries trace and provenance.corpus_item; the model page says the lowerer sees the packet without its trace and never the corpus item (lowering-traced, leak-owned-by-flow-and-scorer-3). validate already yields the view, so the CLI runs validate and hands it over. Cut 4 narrows the door to PacketStore::admit; this cut does not wait on it.
  • The frame owns the verb phrase; a predication's concept must resolve but is not rendered. Referent concepts supply nouns. A frame template names its roles by inventory gloss ({agent}), resolved against the inventory at the packet's revision, so a retired or re-glossed role cannot be silently mis-keyed. A row whose gloss differs from the inventory's entry for its id is a LowerError, so a table row cannot speak for a different meaning than its id now carries.
  • Rendering rules, all deterministic and all from packet fields: the discourse addressee renders "you" ("yourself" when it already filled a role in the same predication), other referents render "the ", a predication filling a role renders "that", the speech-act row supplies an optional opener, sentences are capitalised and end in a full stop. Entity-bearing referents render by concept word: the lowerer does not read the entity store in this cut (follow-up: entity forms in the lowerer).
  • No style, no register, no features. Hedge, certainty, modality and polarity are not rendered, so the baseline scores what meaning alone buys. A lowerer that ignores polarity: negative would assert the opposite, so lower refuses any packet that carries a feature, relation or extension it does not render rather than render it wrong (packet-owns-meaning); features arrive as templates learn them.
  • Deleted by later cuts, not this one: the table in lower.rs and proof_revision() are temporary owners. Cut 5 (language-ontology) moves the table to weksa.ontology_entry documents selected by the target profile; cut 7 replaces proof_revision() with the loop's minting round. Nothing is deleted today because no earlier lowerer exists; the YAML examples/english-lowering/ hand-lowerings are fixtures, not owners, and stay.
  • weksa score is a thin reader of Scorer::score: weksa score <item>... [--output FILE] embeds the output text once against every named item. It stores nothing (Score documents arrive with cut 7) and gates nothing.

Cut sequence

One line per cut; specs exist for the cuts about to run (2d and 4, siblings on 869c82b; 2d merges first and 4 rebases onto it).

  1. park-node-body: park the Node daemon, MiMo command, RUDP shims, launchers and the Discord-sourced Metame harness at tag parked/weksa-node-body; README front door tells the truth. Landed.
  2. packet-store: Rust crate with packet, inventory and corpus types, the validator, the derived split, the stores and the weksa CLI. Landed at 28284cc. 2b. Landed. packet-store-corpus: corpus labels tell the truth (corpus-labels-tell-truth): ModelFamily and the generator string on ModelWritten; item:nibu:1 ModelWritten (Unrecorded); item:brokkr:1 and its fixture carry codex-local-source-aware-draft, family OpenAi; intake refuses aliased keys, a whole item changed under its key, and a held item's text under a new key; corpus.cc reseeded. Speaker state is not in 2b: it lives in Speaker and Scene documents and the packet's stance, not on the item. Waits on 2. (spec) 2c. Landed. entity-store: ProperName and Referent.name deleted; EntityId, Referent.entity, the weksa.entity document and its store; corpus name spans and entity-id speakers; the packet store's entity checks; seed entities and spans for the cast items; schema v2. Waits on 2b. (spec r1)
  3. Landed at 869c82b. scorer: chrF, meaning cosine behind an Embedder port (Ollama adapter with num_thread), leak diagnostics, aggregation by derived split; library only, weksa score comes with the loop. h2 landed the r2 fix pass on its branch; r3 deletes the name mask (the scorer reads output and reference only) and pools novel coverage per order over word keys that split joined labels. Waits on 2c for merge order. (spec r3) 2d. entity-door-lows: invisible format characters refused in entity forms (ZWJ, ZWNJ, CGJ and variation selectors kept); the form length bound per question form-word-bound (recommended: bytes only); attested origins (Cast { persona, fixture }, Live { persona }) and CastAuthored.author; empty provenance strings refused at the store; S6 under NFKC plus case fold and no-alphanumeric texts refused; next_key refuses at u32::MAX; CLI flags that do not apply refuse and no error echoes a value. Owns follow-ups entity-door-lows and s6-unicode-normal-form. (spec r1) 3b. lowerer-first-cut (follow-up golf-loop-lowerer-cut): the section above. lower, weksa lower, weksa score, weksa packet proof, inventory rev 2 for nibu-proof-001.
  4. speaker-state: packet stance (felt affect and restraint, slot Stance, packet schema v4); formality, warmth and intensity out of the seed and the standard; only PacketStore::admit yields a LoweringInput, and it refuses packets for withdrawn items (follow-up lowering-input-from-packet-store). (spec r1) 4b. speaker-scene: slots Speaker and Scene with their seed features; weksa.speaker (speaker:<entity-slug>@<n>) and weksa.scene (scene:<group>@<n>, participants and ordered-pair relations) documents with Annotator Operator and Model { family, generator }, validated by the packet's shared feature check, append-only per key; their stores, weksa speaker and weksa scene commands, published schemas; Model-annotated cast speakers and scenes for the committed items. Waits on 2d and 4; pd-speaker-annotation blocks only the public-domain annotator.
  5. language-ontology: weksa.community from data/cultural-ontology/ (owns follow-up edda-referent-yaml-parse), weksa.ontology_entry for en-US and ja-JP from data/target-language-ontology/ (YAML deleted), speaker communities, deterministic select, the lowering view (packet without trace, speaker, scene, selected entries). Waits on 4.
  6. pd-importer: public-domain play and fiction dialogue into the corpus with speaker, scene group, edition provenance, and Speaker and Scene annotations by the ruled annotator. Waits on 4, follow-up gutenberg-terms and question pd-speaker-annotation.
  7. golf-loop-en: the loop driver, the Train-only context builder for inventory- and ontology-minting calls (follow-up loop-context-builder-train-only), the packet store bound to one inventory store (packet-store-inventory-binding), ModelPort with the claude -p adapter, encoder (packet with stance) and lowerer prompts, round records, Score documents, the composite and accept rule, weksa score, the detached unit on Yggdrasil. Waits on 3, 5, 6 and follow-up claude-on-yggdrasil.
  8. style-axis: a style-embedding score behind the scorer port. Waits on 3.
  9. ja-panel: ja-JP into the loop's panel. Waits on 7.
  10. conlang-lexicon: the first conlang's lexicon placed at concept embeddings, its morphology data, its ontology entries and the gloss reader. Waits on 3, 5 and question first-conlang.
  11. convergence-freeze: the convergence test, the freeze, the re-encode into training pairs. Waits on 7, 9 and 10.
  12. distil: fine-tune a lowerer on the frozen training set. Waits on 11 and a host question asked then.
  13. live-lowering: agents emit packets with stance; Weksa reads speaker state from persona state (follow-up persona-state-speech-community) and lowers for the reader. Waits on 11.