Prompt::lower strips the item's speaker id

lower takes the item's speaker; a referent citing it is printed with no entity,
and the speaker has no line in the entities map and is never looked up, so no
lowering prompt holds the speaker id (lower-prompt-no-item-text). Fixtures that
relied on a cited speaker now cite a prior-turn speaker. PROMPT_SOURCE_DIGEST
changes with context.rs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Weksa 2026-11-13 16:26:56 +00:00
commit 65da455470
5 changed files with 119 additions and 35 deletions

View file

@ -636,17 +636,17 @@ impl Prompt {
/// holds the entries `ontology::select` chooses from the view's stance,
/// discourse and the scene relation `with_scene` read, and `language`,
/// never the caller; the scene's values are never printed, only the entries
/// they select. A view whose scene was never read is refused
/// ([`LowerError::SceneUnread`]). `item`
/// goes only to [`Prompt::item_keys`], never into the text. A speaker's
/// entity id appears
/// only when the encoder cites that entity on a referent, which is naming a
/// cited entity like any other. A cited entity the port lacks, or one with
/// no form in `language`, is an error and no prompt. The caller chooses the
/// language; the view has none.
/// they select. `item` goes only to [`Prompt::item_keys`], never into the
/// text. `speaker` is the item's speaker: it is never printed. A referent
/// citing it is printed with no entity, and it has no line in `entities:`
/// and no form to find, so a speaker the language has no form for does not
/// refuse. Any other cited entity is printed with its forms. A cited
/// entity the port lacks, or one with no form in `language`, is an error
/// and no prompt. The caller chooses the language; the view has none.
pub fn lower(
input: &LoweringInput,
item: &CorpusItemId,
speaker: &EntityId,
language: &str,
inventory: &Inventory,
entities: &dyn EntityForms,
@ -656,7 +656,13 @@ impl Prompt {
return Err(LowerError::SceneUnread);
}
let mut forms: BTreeMap<&str, Vec<String>> = BTreeMap::new();
for id in input.referents().iter().filter_map(|r| r.entity.as_ref()) {
let mut referents = input.referents().to_vec();
for referent in &mut referents {
if referent.entity.as_ref() == Some(speaker) {
referent.entity = None;
}
}
for id in referents.iter().filter_map(|r| r.entity.as_ref()) {
let found = entity(entities, id)?;
let named = found
.forms_in(language)
@ -667,7 +673,7 @@ impl Prompt {
let meaning = Meaning {
context: input.context(),
discourse: input.discourse(),
referents: input.referents(),
referents: &referents,
predications: input.predications(),
relations: input.relations(),
extensions: input.extensions(),

View file

@ -1420,7 +1420,7 @@ impl RunStores {
let view = self
.assembled(view, &self.inventory)
.map_err(|_| RunError::Store)?;
Ok(Prompt::lower(&view, &packet_item, &item.language, inventory, &self.entities, ontology).ok())
Ok(Prompt::lower(&view, &packet_item, &item.speaker, &item.language, inventory, &self.entities, ontology).ok())
}
/// `<root>/ablation-<n>`, where a stance ablation's prompts, answers and
@ -2328,7 +2328,7 @@ fn score_round<M: ModelPort, E: Embedder>(
fates[at] = Fate::Refused(Stage::Lower, Some(packet_key.clone()));
continue;
};
let Ok(prompt) = Prompt::lower(&view, &id, language, &inventory, &run.entities, &ontology) else {
let Ok(prompt) = Prompt::lower(&view, &id, &items[at].speaker, language, &inventory, &run.entities, &ontology) else {
fates[at] = Fate::Refused(Stage::Lower, Some(packet_key.clone()));
continue;
};

View file

@ -586,11 +586,11 @@ mod lowering {
let unread = admitted_unread(proof_packet(), &cast, "corpus/corpus.cc", &entities);
let empty = weksa::Ontology::empty();
assert_eq!(
Prompt::lower(&unread, &item, "en-US", &inv, &held, &empty).err(),
Prompt::lower(&unread, &item, &away(), "en-US", &inv, &held, &empty).err(),
Some(LowerError::SceneUnread)
);
let read = admitted(proof_packet(), &cast, "corpus/corpus.cc", &entities);
assert!(Prompt::lower(&read, &item, "en-US", &inv, &held, &empty).is_ok());
assert!(Prompt::lower(&read, &item, &away(), "en-US", &inv, &held, &empty).is_ok());
}
fn dao(index: usize) -> (LoweringInput, Inventory, EntityStore, CorpusItemId) {
@ -611,6 +611,11 @@ mod lowering {
}
}
/// A speaker no referent cites.
fn away() -> EntityId {
EntityId::parse("entity:nobody").unwrap()
}
fn nibu(forms: &[(&str, &[&str])]) -> Entity {
Entity {
id: EntityId::parse("entity:nibu").unwrap(),
@ -635,7 +640,7 @@ mod lowering {
fn lower_prompt_names_its_language() {
let (view, inv, entities, item) = dao(2);
for tag in ["en-US", "ja-JP", "pt-BR"] {
let prompt = Prompt::lower(&view, &item, tag, &inv, &entities, &weksa::Ontology::empty()).unwrap();
let prompt = Prompt::lower(&view, &item, &away(), tag, &inv, &entities, &weksa::Ontology::empty()).unwrap();
let want = format!("{LOWER}\n\nlanguage: {tag}\n\npacket: ");
assert!(prompt.text().starts_with(&want), "{tag}");
}
@ -647,7 +652,7 @@ mod lowering {
#[test]
fn lower_prompt_text_is_pinned() {
let (view, inv, entities, item) = dao(2);
let prompt = Prompt::lower(&view, &item, "en-US", &inv, &entities, &weksa::Ontology::empty()).unwrap();
let prompt = Prompt::lower(&view, &item, &away(), "en-US", &inv, &entities, &weksa::Ontology::empty()).unwrap();
let (head, listing) = prompt.text().split_once("inventory rev:3\n").unwrap();
let expected = format!(
"{LOWER}\n\nlanguage: en-US\n\npacket: {}\n\nentities: {{}}\n\nontology: none\n\n",
@ -710,7 +715,7 @@ mod lowering {
let entries = OntologyDraft::parse(draft).unwrap().into_entries("en-US", 3, 1, &annotator);
onto.admit(entries, &store).unwrap();
let prompt =
Prompt::lower(&view, &item, "en-US", &inv, &entities, &onto.ontology().unwrap()).unwrap();
Prompt::lower(&view, &item, &away(), "en-US", &inv, &entities, &onto.ontology().unwrap()).unwrap();
assert!(prompt.text().contains("golden choice"));
assert_eq!(prompt.digest(), "4a40eb57282b059d");
}
@ -732,7 +737,7 @@ mod lowering {
let mut packet = packets().remove(2);
packet.stance = stance.clone();
let view = admitted(packet, &store, "daodejing/corpus.cc", &entities);
let prompt = Prompt::lower(&view, &item, "en-US", &inv, &entities, &weksa::Ontology::empty()).unwrap();
let prompt = Prompt::lower(&view, &item, &away(), "en-US", &inv, &entities, &weksa::Ontology::empty()).unwrap();
(stance, prompt)
};
let three = [prompt_of(own), prompt_of(angry), prompt_of(Stance::default())];
@ -769,7 +774,7 @@ mod lowering {
("sa-Latn", &["Nibuwa"]),
("en-US", &["Nibu", "Nibu's"]),
])]);
let prompt = Prompt::lower(&view, &item, "en-US", &cast.inventory().unwrap(), &held, &weksa::Ontology::empty()).unwrap();
let prompt = Prompt::lower(&view, &item, &away(), "en-US", &cast.inventory().unwrap(), &held, &weksa::Ontology::empty()).unwrap();
assert!(
prompt
.text()
@ -799,10 +804,53 @@ mod lowering {
),
(Held(vec![]), LowerError::UnknownEntity { entity: id }),
] {
assert_eq!(Prompt::lower(&view, &item, "en-US", &inv, &held, &weksa::Ontology::empty()).err(), Some(want));
assert_eq!(Prompt::lower(&view, &item, &away(), "en-US", &inv, &held, &weksa::Ontology::empty()).err(), Some(want));
}
}
/// The item's speaker is never printed, though a referent cites it: its
/// referent loses the entity, the forms map loses its line, and the view
/// itself is not changed.
#[test]
fn a_cited_speaker_is_stripped_from_the_lowering_prompt() {
let cast = InventoryStore::open(&data("interlingua/inventory.cc")).unwrap();
let cast_entities = EntityStore::open(&data("entity/entity.cc")).unwrap();
let view = admitted(proof_packet(), &cast, "corpus/corpus.cc", &cast_entities);
let inv = cast.inventory().unwrap();
let item = CorpusItemId::parse("item:nibu:1").unwrap();
let speaker = EntityId::parse("entity:nibu").unwrap();
let held = Held(vec![nibu(&[("en-US", &["NIBUCANARY"])])]);
let lower = |held: &Held| {
Prompt::lower(&view, &item, &speaker, "en-US", &inv, held, &weksa::Ontology::empty())
};
let prompt = lower(&held).unwrap();
let text = prompt.text();
for banned in ["entity:nibu", "NIBUCANARY"] {
assert!(!text.contains(banned), "{banned} in {text}");
}
assert!(text.contains("\n\nentities: {}\n\n"), "{text}");
assert!(text.contains(r#""id":"n3","kind":"referent_kind:1","concept":"concept:5","entity":null"#), "{text}");
assert!(view.referents().iter().any(|r| r.entity.as_ref() == Some(&speaker)));
// A speaker the language has no form for, or the port lacks, does not refuse.
assert!(lower(&Held(vec![])).is_ok());
assert!(lower(&Held(vec![nibu(&[("sa-Latn", &["Nibuwa"])])])).is_ok());
}
/// Only the speaker is stripped: when the item's speaker is someone else,
/// the cited entity keeps its id in the packet and its forms line.
#[test]
fn a_cited_entity_that_is_not_the_speaker_is_kept() {
let cast = InventoryStore::open(&data("interlingua/inventory.cc")).unwrap();
let cast_entities = EntityStore::open(&data("entity/entity.cc")).unwrap();
let view = admitted(proof_packet(), &cast, "corpus/corpus.cc", &cast_entities);
let item = CorpusItemId::parse("item:nibu:1").unwrap();
let held = Held(vec![nibu(&[("en-US", &["NIBUCANARY"])])]);
let prompt = Prompt::lower(&view, &item, &away(), "en-US", &cast.inventory().unwrap(), &held, &weksa::Ontology::empty()).unwrap();
let text = prompt.text();
assert!(text.contains(r#""concept":"concept:5","entity":"entity:nibu""#), "{text}");
assert!(text.contains("\n\nentities: {\"entity:nibu\":[\"NIBUCANARY\"]}\n\n"), "{text}");
}
fn feature(inv: &Inventory, gloss: &str) -> weksa::InvId {
inv.entries().find(|e| e.gloss == gloss).unwrap().id
}
@ -928,7 +976,7 @@ mod lowering {
let item = CorpusItemId::parse("item:nibu:1").unwrap();
let view = view_of(Some(700), None, &cast, &cast_entities);
let lowered = |view: &LoweringInput, o: &weksa::Ontology| {
Prompt::lower(view, &item, "ja-JP", &inv, &held, o).unwrap()
Prompt::lower(view, &item, &away(), "ja-JP", &inv, &held, o).unwrap()
};
let prompt = lowered(&view, &ontology);
let text = prompt.text();
@ -941,8 +989,7 @@ mod lowering {
assert!(none.text().contains("\n\nontology: none\n\n"));
// Entries move the prompt and nothing else of the view does.
assert_ne!(prompt.digest(), none.digest());
// The group is still private; the speaker's entity appears only because
// the packet cites it on a referent.
// The group is still private.
let held_item = CorpusStore::open(&data("corpus/corpus.cc"))
.unwrap()
.get(item.as_str())

View file

@ -1100,7 +1100,8 @@ fn lowering_prompts(dao: &Dao, inventory: &InventoryStore) -> Vec<(String, Promp
.unwrap();
let state = weksa::SpeakerStateStore::open(&dir.path().join("s.cc")).unwrap();
let view = view.with_scene(&inv, &state, &corpus).unwrap();
let prompt = Prompt::lower(&view, &key, "en-US", &inv, dao.run.entities(), &weksa::Ontology::empty()).unwrap();
let speaker = corpus.get(key.as_str()).unwrap().unwrap().speaker;
let prompt = Prompt::lower(&view, &key, &speaker, "en-US", &inv, dao.run.entities(), &weksa::Ontology::empty()).unwrap();
(key.to_string(), prompt)
})
.collect()
@ -2755,7 +2756,7 @@ fn source_tripwires() {
2,
"two lowering call sites: the round's and the stance ablation's replay of it"
);
// Prompt::lower takes the view, the item's key, the inventory and the entity
// Prompt::lower takes the view, the item's key and speaker id, the inventory and the entity
// port: no corpus item, train set, offer or store.
let start = context.find("pub fn lower(").unwrap();
let signature = &context[start..start + context[start..].find(") -> ").unwrap()];
@ -3102,8 +3103,9 @@ fn lowering_text_at_the_bound_is_scored_and_line_breaks_are_text() {
}
}
/// Two canary items, one Train and one Validation, whose speaker is an entity
/// with forms only in another language; each encodes to a packet citing it.
/// Two canary items, one Train and one Validation, whose prior-turn speaker is
/// an entity with forms only in another language; each encodes to a packet
/// citing it. (The item's own speaker would be stripped, never looked up.)
#[test]
fn a_lowering_prompt_that_cannot_be_built_refuses_at_lower_and_keeps_the_packet() {
use weksa::entity::{Entity, EntityOrigin, LanguageForms};
@ -3133,9 +3135,11 @@ fn a_lowering_prompt_that_cannot_be_built_refuses_at_lower_and_keeps_the_packet(
let mut item = store.get(from).unwrap().unwrap();
item.id = key.into();
item.text = format!("A held-apart sentence for {key}.");
item.speaker = EntityId::parse("entity:sagexx").unwrap();
item.names = vec![];
item.prior_turns = vec![];
item.prior_turns = vec![weksa::corpus::Turn {
speaker: EntityId::parse("entity:sagexx").unwrap(),
text: "A turn before.".into(),
}];
store.add(item, &entity_store).unwrap();
}
});
@ -4642,6 +4646,20 @@ fn add_languages(corpus: &Path, entities: &Path, ja: Option<&str>) {
});
entities.put(laozi).unwrap();
}
entities
.put(weksa::Entity {
id: common::eid("narrator"),
forms: vec![weksa::LanguageForms {
language: "en-US".into(),
forms: vec!["Narrator".into()],
pronunciation: None,
}],
origin: weksa::EntityOrigin::Cast {
persona: "narrator".into(),
fixture: "f.yaml".into(),
},
})
.unwrap();
let mut store = CorpusStore::open(corpus).unwrap();
for (key, text, twin) in JA_TEXTS {
let mut item = store.get(&format!("item:daodejing:{twin}")).unwrap().unwrap();
@ -4649,7 +4667,8 @@ fn add_languages(corpus: &Path, entities: &Path, ja: Option<&str>) {
item.text = text.into();
item.language = "ja-JP".into();
item.names = vec![];
item.prior_turns = vec![];
item.speaker = common::eid("narrator");
item.prior_turns = laozi_spoke_before();
store.add(item, &entities).unwrap();
let mut pt = store.get(&format!("item:daodejing:{twin}")).unwrap().unwrap();
pt.id = format!("item:daodejing:2{twin}");
@ -4661,7 +4680,18 @@ fn add_languages(corpus: &Path, entities: &Path, ja: Option<&str>) {
}
}
/// The Dao draft of `item`, its first referent citing the speaker, Laozi.
/// The one prior turn that makes Laozi citeable on an item he does not speak:
/// the ja-JP items are the narrator's, so their lowering prompts print him.
fn laozi_spoke_before() -> Vec<weksa::corpus::Turn> {
vec![weksa::corpus::Turn {
speaker: common::eid("laozi"),
text: "A turn before.".into(),
}]
}
/// The Dao draft of `item`, its first referent citing Laozi: the speaker of
/// the en-US items (never printed to the lowerer), a prior-turn speaker of the
/// ja-JP ones (printed in the item's language).
fn citing_laozi(item: usize) -> String {
let mut v = serde_json::to_value(draft_of(item)).unwrap();
v["referents"][0]["entity"] = serde_json::json!("entity:laozi");
@ -4722,8 +4752,9 @@ fn each_item_lowers_in_its_own_language() {
} else {
en += 1;
assert!(text.contains("\n\nlanguage: en-US\n\n"), "{text}");
assert!(text.contains(r#"{"entity:laozi":["Laozi"]}"#), "{text}");
assert!(!text.contains("老子"), "{text}");
// Laozi speaks the en-US items: the lowerer is not told his name.
assert!(text.contains("\n\nentities: {}\n\n"), "{text}");
assert!(!text.contains("entity:laozi") && !text.contains("Laozi") && !text.contains("老子"), "{text}");
}
}
assert_eq!((ja, en), (3, 3));
@ -6333,7 +6364,7 @@ fn grown_bilingual() -> Dao {
item.text = text;
item.language = language.into();
item.names = vec![];
item.prior_turns = vec![];
item.prior_turns = if language == "ja-JP" { laozi_spoke_before() } else { vec![] };
store.add(item, &entities).unwrap();
}
}

View file

@ -676,7 +676,7 @@ One line per cut; specs exist for the cuts about to run (2d and 4, siblings on `
7a. `loop-context-builder`: `context.rs` (`TrainItems`, `EncoderOffer`, `Prompt`: corpus text reaches a model call only through `Prompt::mint` over live Train items or `Prompt::encode` over one offered item; instructions are the consts `MINT_INSTRUCTIONS` and `ENCODE_INSTRUCTIONS` in `context.rs`; `Checkpoint` has no public constructor, offers each item once, and its offers borrow it (the gate is typed `TestReadings` and Test offers on the round path; operator tools such as `weksa corpus dump` and `weksa score` still read corpus text of any split); prior turns print the held Train text), `SplitScheme::live`, `CorpusItem::cited_entities` (one owner of the id set `PacketStore::admit` and the offer share), and checkpoint-only Test readings (`Checkpoint`, `TestReadings`; `Aggregate` and `leak_diagnostic` carry no Test). Library only; since 7b `TrainItems::from_corpus` and `EncoderOffer::for_round` are crate-private and `RunStores` is their one public producer. Merges follow-ups `loop-context-builder-2` and `scorer-test-readings-type`. (spec r1)
7b. `golf-loop-round` (spec r1; branch `weksa-lowering/golf-loop-round`): the smallest loop that scores one round. `run.rs`: `RunStores` pins the corpus and entity stores of a run by canonical path and FNV-1a 64 digest at `create`, reopens only from `<root>/run.cc` and refuses drift (corpus, entities, `PROMPT_SOURCE_DIGEST` of `context.rs`, a second pin on `lower.rs` retired in 7d, model, embedder); `round` encodes every live Train and Validation item through a `ModelPort` (`model.rs`; the one adapter `AnswerFiles` writes `<digest>.prompt` and reads `<digest>.answer`, so the model is the Claude session), reads each answer into a `PacketDraft` (`encode.rs`, the one place a model's JSON is read), admits through `PacketStore::admit` into `<root>/round-<n>/packets.cc`, lowers, scores once, and keeps a `RoundRecord` (`weksa.loop_round`, no text, no Test; Train rows, Validation only as means and refusal counts). `weksa loop open|round`. Not here: the spec-edit step, accept rule and composite, noise floor, Score and lowering documents, a subprocess adapter, the checkpoint door, a model lowerer (follow-ups `golf-loop-spec-edit`, `golf-loop-accept-rule`, `golf-loop-score-documents`, `golf-loop-subprocess-adapter`, `golf-loop-checkpoint-door-bound`, `golf-loop-model-lowerer`). `packet-store-inventory-binding` is met for loop packets only (`RunStores` admits with the run's inventory); `validate`, `packet proof|dao` and `lower` still take caller stores. Waits on 7a, `checkpoint-gate`, 4b.
7c. `golf-loop-edit-accept` (spec r1; branch `weksa-lowering/golf-loop-edit-accept`): one edit-accept cycle. The run works on its own copy of the inventory (`create` copies the caller's file to `<root>/inventory.cc`, refusing a root that already holds a file of that name, and pins its digest as `RunRecord` key 11; `RunRecord.inventory` is provenance; a `run.cc` from before has no key 11 and does not open); the head is derived from the latest Accepted `Decision`. `weksa loop cycle` asks the editor for one inventory edit from `Prompt::mint(train, inventory, rev, feedback)` (Train blocks, the inventory listing and an `EditFeedback` line: whether the last edit was kept and the head's Validation composite rounded to `LADDER_STEP`, the Ladder release of Blum and Hardt 2015), reads it as an `EditDraft` (`encode.rs`; ids and revisions are the store's, `into_revision`), admits it through `InventoryStore::admit_revision` into `<root>/cycle-<k>/inventory.cc`, runs a candidate round under it, and `decide`s on Validation against the head's establishing round past a floor (never under one `LADDER_STEP`; from cut `cycle-floor-paired` the paired floor over rounds 1 to `FLOOR_ARMS_MIN` + 1, so a cycle needs four recorded rounds, where 7c measured the spread between rounds 1 and 2). An edit that fails is a rejected `Decision`, not an error. `RoundRecord` gains keys 16 (inventory digest) and 17 (cycle); an encode answer that is not UTF-8 is that item's parse refusal. Not here: Score and lowering documents (`golf-loop-score-docs`), Train rows in the edit prompt (`golf-loop-edit-train-feedback`), a paired floor (`golf-loop-paired-floor`), the checkpoint door and merging to `data/` (`golf-loop-checkpoint-door-bound`, `golf-loop-freeze-merge`), a model lowerer (`golf-loop-model-lowerer-next`; until it lands an entry an edit adds has no lowering row, so a cycle gains only by retiring entries or adding ones the encoder does not cite). Waits on 7b.
7d. `golf-loop-model-lowerer` (spec r1; branch `weksa-lowering/golf-loop-model-lowerer`): the round lowers through the `ModelPort`. `Prompt::lower` (`context.rs`) takes a `LoweringInput` (no text, key, group or speaker getter), the inventory at the view's revision and the en-US entity forms, and prints `LOWER_INSTRUCTIONS`, the meaning of the packet, the entities line and the inventory listing; the item key goes only to `item_keys`. The answer is `{"text"}`, read as `LoweringDraft` in `encode.rs`, held in `score_round`'s locals and scored once; it is never put in `run.cc`, a `RoundRecord`, a `Decision` or `EditFeedback`. A round has two answer phases (every encode prompt, then every lowering prompt), all or nothing: while any prompt is unanswered `packets.cc` is discarded and nothing reaches `run.cc`. `lower::lower` and the template table leave the loop (they remain `weksa lower`'s; follow-up `table-retirement`), so an entry an edit adds can be said and a cycle can buy a gain. One source pin remains: `PROMPT_SOURCE_DIGEST` covers `context.rs`, `split.rs` and `lower.rs` (`RunRecord` key 7 and `RoundRecord` key 3 are retired). The run's record pins `LowererReach` (`weksa loop open --lowerer-reach briefed-subagent|tool-less-relay`, `RunRecord` key 12) and every printed round and cycle line says it; a briefed-subagent run says its numbers are smoke-round only (rulings `lowerer-call-reach-toolless-relay`, `dao-round-seed-inventory`). A lowering is refused at `Lower` when blank, over `MAX_LOWERING_BYTES` (4096) or holding a control character other than a line break or tab; `PROMPT_SOURCE_DIGEST` also covers `packet.rs`. Not here: per-item lowering text documents (`golf-loop-score-docs`), the subprocess adapter. Waits on 7c.
7d. `golf-loop-model-lowerer` (spec r1; branch `weksa-lowering/golf-loop-model-lowerer`): the round lowers through the `ModelPort`. `Prompt::lower` (`context.rs`) takes a `LoweringInput` (no text, key, group or speaker getter), the item's speaker id, the inventory at the view's revision and the item language's entity forms, and prints `LOWER_INSTRUCTIONS`, the meaning of the packet, the entities line and the inventory listing; the item key goes only to `item_keys`, and the speaker id is stripped: a referent citing the item's speaker is printed with no entity, and the speaker has no line in the entities map and is never looked up (a speaker the language has no form for does not refuse; a speaker named in the text cannot be named by the lowerer). The answer is `{"text"}`, read as `LoweringDraft` in `encode.rs`, held in `score_round`'s locals and scored once; it is never put in `run.cc`, a `RoundRecord`, a `Decision` or `EditFeedback`. A round has two answer phases (every encode prompt, then every lowering prompt), all or nothing: while any prompt is unanswered `packets.cc` is discarded and nothing reaches `run.cc`. `lower::lower` and the template table leave the loop (they remain `weksa lower`'s; follow-up `table-retirement`), so an entry an edit adds can be said and a cycle can buy a gain. One source pin remains: `PROMPT_SOURCE_DIGEST` covers `context.rs`, `split.rs` and `lower.rs` (`RunRecord` key 7 and `RoundRecord` key 3 are retired). The run's record pins `LowererReach` (`weksa loop open --lowerer-reach briefed-subagent|tool-less-relay`, `RunRecord` key 12) and every printed round and cycle line says it; a briefed-subagent run says its numbers are smoke-round only (rulings `lowerer-call-reach-toolless-relay`, `dao-round-seed-inventory`). A lowering is refused at `Lower` when blank, over `MAX_LOWERING_BYTES` (4096) or holding a control character other than a line break or tab; `PROMPT_SOURCE_DIGEST` also covers `packet.rs`. Not here: per-item lowering text documents (`golf-loop-score-docs`), the subprocess adapter. Waits on 7c.
7. `golf-loop-en` (7b and 7c landed the round, `ModelPort`, encoder prompt, template digest, round records and the edit-accept cycle; the rest remains): the packet store bound to one inventory store everywhere (`packet-store-inventory-binding`), a subprocess adapter, Score documents, `weksa score` in the loop, 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, in two cuts on `e9cbd9b`. 9a `ja-panel-loop` (spec r1): language is the item's and the panel the run's: `Prompt::lower` takes the item's language and prints it on a language line (no prompt names English), entity forms are read in that language, `RunRecord` pins the panel (`weksa loop open --panel`), offers, mint and recall read only panel items, round means, floors and readings are per panel language, the accept rule runs `decide` per language, the recall split works on unspaced scripts, and `weksa corpus add` has no default language. 9b `ja-panel-corpus` (spec r1, after 9a): the ja-JP slice of `data/original`, model-written Japanese Train and Validation passages and eight Aozora Bunko public-domain Test sentences with full provenance, and their entities. Cut 5 is not a prerequisite: neither language lowers with ontology entries until cut 5 gives both theirs.