Coaching contracts
September 20 reply-help update (implemented): only the brief is automatic. Grammar and suggestions are independently requested and saved for the partner message; both composer and Analysis reuse the same operation and result. Suggestions retain two full replies, two frames and two starters, with shared reading assistance. This supersedes automatic scheduling of those two outputs in the restoration notes below, without changing learning evidence or rewards. Implementation and verification records schema 26 and the limits of offline verification.
Current restoration target: the user has requested the v0 coaching and conversation-assistance experience. The v0 source comparison and restoration contract supersedes the earlier one-suggestion/skill-bound approach as the target design. The restoration is now implemented in source; lesson/progression redesign stays frozen. Implementation and verification records the new independent contracts and current verification limits.
September 16 priority change: voice conversation is the primary experience. Lesson/curriculum and progression redesign are frozen while chat coaching is corrected. Direct corrected wording plus a brief actionable explanation replaces hint-first help for new turns; continuing the conversation never requires a repair exercise. Automatically recommended skills must not redirect the topic. The earlier wave/lesson sequence below is deferred where it conflicts with this decision. Current implementation and verification are recorded in the chat-first checkpoint.
The seams between the three work areas in the coaching work plan. Each contract names who provides it, who consumes it, and the wave it lands in. A provider may refine a signature; the change must be reported as contract drift in its hand-back and then updated here by the integration agent.
Status: wave 1 is implemented and checkpointed at bdb664e. Wave 2 source and combined automated verification are complete; its integration decisions below specify the boundaries, while generated native types remain the exact wire authority. Wave 2 was accepted for continuation on 2026-09-13; Wave 3 learner-state foundation is in progress.
Wave 1
A → B: language guidance functions (native/src/languages.rs)
/// A romanization scheme as one published table (e.g. ALA-LC Arabic), with the
/// rules and hard-case examples the model needs. Scheme ids are table-level.
pub struct RomanizationScheme {
pub id: &'static str, // "ala-lc-arabic", "pinyin"
pub label: &'static str, // "ALA-LC Arabic", "Hanyu Pinyin"
pub instructions: &'static str, // scheme-specific prose; content starts needs_review
pub examples: &'static [(&'static str, &'static str)], // (source, romanized)
pub sources: &'static [&'static str], // references.bib keys
}
/// The default scheme for a language, or None for Latin-script languages.
/// Unknown language → error (never None).
pub fn romanization(language_id: &str) -> Result<Option<&'static RomanizationScheme>>;
/// Prompt text for tasks that emit a `romanization` field: names the scheme,
/// includes its instructions and examples. None when the language has no scheme.
pub fn romanization_guidance(language_id: &str) -> Result<Option<String>>;
/// Language-specific rules for assessing learner text (moved out of the generic
/// coach prompt, e.g. Arabic: never add diacritics or normalize letters in
/// quoted evidence). None when the language has none.
pub fn assessment_guidance(language_id: &str) -> Result<Option<&'static str>>;
- The frontend
Language.romanizationprojection carries the scheme id; it remains the UI gate for the romanization toggle. - Universal rules ("quote learner text exactly", "never invent errors") stay in B's task prompt until wave 2 moves them to
universal.yaml. - Wave-one primary-table verification corrected illustrative assumptions: ALA-LC uses its published distinct ‘/’ signs, unassimilated
al-before sun letters andáfor final alif maqṣūrah. Pinyin marks lexical tones 1–4; neutral tone remains unmarked. Source review and examples are documented inworkflow/reports/coaching-w1-a-handback.md; linguistic content remainsneeds_review. [@ala_lc_arabic] [@pinyin_orthography2012] - Tests (A):
- every language with a scheme returns
romanization_guidancecontaining the scheme label and at least 3 examples, - every scheme is referenced by at least one language,
- every
sourceskey exists inreferences.bib.
- every language with a scheme returns
- Guard test (B, using A's functions): for each language with a scheme, the resolved
persona_word_glossandcoach_suggestionsprompts containromanization_guidance's text; for Latin-script languages, neither prompt mentions romanization rules.
B → C: revision action and snapshot fields
// model.rs — new Action variant (kind-tagged like existing actions, e.g. retryGloss)
ReviseTurn {
conversation_id: String,
turn_id: String, // the learner turn being revised
text: String, // the revised learner text
input: InputEvidence, // revision = true is set by Rust, not trusted from the UI
expected_revision: i32, // reviewed ConversationSnapshot.revision (global metadata)
}
- Effect:
- Creates a new turn with
replaces_turn_id = turn_id, runs the normal plan (persona reply, coach, glosses) against the revised text, and keeps the replaced turn. - Revising the latest turn needs no confirmation.
- Revising an earlier turn removes the later turns under EXECUTION.md's edit rule. Rust enforces this; the UI confirms first.
- A pending turn in the conversation rejects the action with a typed error, which C shows.
- Creates a new turn with
- Planned schema v13:
turns.replaces_turn_id TEXT NULL REFERENCES turns(id).SCHEMA_VERSIONanduser_versionboth become 13. Development data is disposable by explicit user authorization. B may rebuild the schema and erase/recreate SkellySpeak development data as needed, without migration, preservation or repeat approval. Keep current-schema creation, validation, ownership and errors correct; no silent production reset is implied. - Snapshot:
- each message exposes
turnId,replacesTurnIdandreplacedByso presentation can group by durable identity, - each turn exposes
replacesTurnId: string | null, - the conversation view marks turns that have been replaced (
replacedBy: string | null), - evidence records carry
replaces_message_id(the replaced turn's learner message) andinput.revision = true. revisionSuffixCounts: [{turnId, exchangeCount, coachTurnCount}]describes the dependent suffix for each eligible active exchange. The UI uses the counts and revision from the same reviewed snapshot.
- each message exposes
- Concurrency:
expectedRevisionuses the globalConversationSnapshot.revision, notConversation.revision. Any intervening metadata change conservatively returns Conflict before mutation. The UI retains the draft and requests a fresh review; duplicate delivery of the same action ID reuses its receipt. - Pagination: the existing conversation read accepts
beforesequence and returns at most 100 messages plushasOlder. Earlier-version inspection may explicitly load another page; no unbounded automatic scan or fabricated predecessor content. - Outcomes: the Rust
Outcomeenum has five values:demonstrated | partial | not_demonstrated | not_observed | uncertain. It's exported through ts-rs soskills.tsuses the generated type. Semantics for later waves:uncertainandnot_observed→ no estimator update;not_demonstrated→ negative update. - Catalog identity: wave one fingerprints the exact embedded JSON bytes with deterministic FNV-1a into a numeric
SKILL_CATALOG_VERSIONexported to TypeScript. Accepted turns capture that value; evidence retains its captured version and current credit requires a match. Wave-two registry hashing may refine this contract explicitly. - Prompt provenance: accepted turns capture
coach-feedback-2,coach-suggestions-2and context template v5. Changed graph declarations are v2; word-gloss prompt template is v5. Evidence reads its captured prompt version. - Credit stays on current XP rules in wave 1. The support-weighted credit arrives with the wave-3 fold. C must remove the
replaces_message_id !== nullexclusion only when B confirms the credit rule, and in wave 1 shows revision rewards under current rules.
Revision integration requirements
These refine wave one; B must publish the generated field names and action shape before C connects the UI.
- Identity: expose the durable native
turnIdon eachChatMessage, including coach messages, and carry it into the frontend presentation record. Numericsequenceremains an ordering/evidence field; it is never a native turn ID. Group learner and partner messages by their durable turn ID, not adjacency. Paginated history must not attach a reply to another turn. - Confirmation authority: the revision action must carry a concurrency precondition tied to the history the learner reviewed. B specifies the token and validates it transactionally before removing anything. A changed history returns Conflict and requires a fresh preview; never silently expand the removal scope. Do not use an unqualified frontend message count as authority.
- Single active chain: revise an active learner/persona exchange, never a private coach turn or an already replaced version. Repeated edits extend the chain from its current active member. Retain the predecessor chain for the Earlier version view. Reject cross-conversation links, cycles and branching replacements.
- Earlier edit: remove the later dependent suffix under the approved edit rule while retaining the selected exchange and its predecessor chain as earlier versions. Include affected private coach records and unpublished source-dependent operations in the removal/invalidation analysis. B reports the exact scope and C names it in the confirmation. No unrelated conversation or standalone generation receipt is affected.
- Context: subsequent partner/coach conversation history uses active exchanges. A replaced response remains available for inspection, but is not supplied as a current reply. Private coach content remains outside partner context. New turns capture current explicitly chosen settings and focus; previous captures remain immutable.
- Publication: atomically accept the revision and invalidate unfinished work on superseded/removed sources. Late feedback, suggestions, glosses and speech cannot publish into the new turn or resurrect removed records. Do not replay rejected or unknown inference automatically.
- History availability: supply enough revision metadata/history for an Earlier version view when the predecessor lies outside the current message page or the recent operation list. B provides a bounded retrieval shape if needed; C must not fabricate absent history or silently omit it.
- Credit: current native rules award distinct demonstrated revised wording 2 assisted XP, retain the 10 XP unassisted rule and enforce existing wording deduplication/exclusions. Retained earlier versions remain inspectable learning evidence; removal of a dependent suffix removes that suffix's contributions. B verifies these rules in native tests. After that handoff, C removes the blanket replacement exclusion and celebrates only the positive net credited increase once. No new proficiency marks for assisted repair; no manufactured reward when no credit increased.
- Acceptance cases: latest revision, repeated revision, earlier revision, stale confirmation, pending-reply rejection, duplicate action delivery, cross-conversation/coach/replaced target rejection, late publication after invalidation, pagination, restart, source deletion and reward replay. B owns native lifecycle cases; C owns frontend interaction/presentation cases. A provider-fixture test proves local behavior, not a live provider result.
B internal (wave 1): focus into prompts
LanguageProfilefocus (currently computed inprogression.rsasactive_focus/recommended_focus) is captured in the turn context.- It's rendered as the L3 block (coaching plan §7) into the
persona_replysystem prompt and thecoach_feedback/coach_suggestionsprompts. With no focus, the block is absent. - Snapshot tests cover both cases.
Wave 2 — integration decisions
The user checkpointed wave one and authorized wave two. Wave two uses fresh schema v14 for durable conversation openings; schema v13 remains the wave-one checkpoint. Older development data may be reset under the existing authorization, with no migration. Configuration belongs to the learner: first startup seeds bundled YAML into config/ beside the workspace database only when that directory is absent. Existing incomplete or invalid configuration is a blocking config_load startup refusal with an actionable path/message; it is never replaced by bundled defaults and does not offer database deletion as a remedy. Native initialization completes before opening the store or loading frontend projections. Restart reloads edits; no hot reload is promised.
Language resolution, construct and policy hashes are captured with accepted work. Language context resolves universal → ordered traits → language → variety, by scope, including the explanation language. Candidate selection is deterministic and preserves focus/prerequisites and mandatory function/interaction constructs; the 15–25 size is a target, not permission to silently omit required candidates. Missing IDs/citations, invalid references and cycles fail validation.
StartConversation carries the reviewed global snapshot revision. Partner-first starts require an empty conversation and no pending work, and create a real persona_opening without a learner message. Topic choices are instructions, not learner assistance or evidence. Choices and previews are local. The composer remains usable without selecting a card.
CoachControl provides durable, idempotent local Open card, Show answer and Keep going choices with the reviewed revision. The card is shown only after open_card acknowledges disclosure. CoachDecision.exposedMove starts null and resets for a new rung; editing directly does not silently count a hint as shown. Retry support records this exposure separately from the selected correction. Only the policy-approved correction text crosses into the displayed card; an unrevealed target hypothesis remains private. A revision with an active prior correction captures that exact item and shown move for coach_retry_check. Fixed notes require validated repair evidence; simply revising is insufficient. Partner replies do not wait for coach analysis, and coaching never blocks continuing.
The Registry is owned by each Store; runtime language projections and prompts use that workspace instance. Explicit bundled helpers serve contract export and standalone tests. Candidate selection now takes the difficulty band explicitly, and topic labels use the selected interface locale.
The initial observation also provides explanation-language elicitation and metalinguistic cues alongside its hint and private target hypothesis. Policy chooses among these saved cues without another inference call. Exact target leakage in cues is rejected; this structural check cannot prove semantic hint quality. Retry checks return their own meaning_recovered; repairing a form does not imply fully recovered meaning.
B publishes the generated action, snapshot and safe observation/decision shapes for C. Full observation persistence and the frontend projection may differ to protect graduated help; these distinctions must be reported in the handback. Wave-three support-weighted XP and proficiency estimation remain deferred.
| Contract | Provider → consumer | Shape |
|---|---|---|
LanguageContext::resolve(language, variety, explanation) -> LanguageContext; .guidance(scope) -> Vec<String>; .hash() | A → B | resolution order universal → traits → language → variety, by scope (§4) |
Constructs::candidates(ctx, focus, due, tokens) -> Vec<ConstructRef>; Constructs::get(id); .hash() | A → B | §6.2 candidate rule |
Policy::feedback() / estimator() / game() | A → B | typed from YAML (§6.4, §11.6) |
topics() -> &[ConversationTopic] | A (data) → B (selection) | Shared subjects; no coverage gate |
Startup ConfigLoadError → StartupState.refusal with config_load | A → B → C | blocking error screen before normal stores mount; fix files and restart |
StartConversation { conversation_id, configuration, message, input, expected_revision }; persona_opening op | B → C | Atomic settings capture and turn admission |
CoachObservation, CoachDecision, Correction, chip state | B → C | §6.3, §8.2 |
Wave 3 (sketch)
| Contract | Provider → consumer |
|---|---|
ConstructState, LearnerState export | B → C |
RewardEvent { kind, tier, xp, copy_params, cause } | B → C (presentation per game.yaml) |
FluencyRecord per learner message (regions, pauses, clipped words, removed words) | B → C |
OpenerSet, SessionReview | B → C |
Wave 3 foundation: implemented native seam
get_learner_state(target) returns generated LearnerState / ConstructState
types. export_learner_state(target) returns YAML with retained evidence
projections, focus/exclusion choices, derived states, timestamps and registry /
estimator hashes. No inference, XP change or evidence mutation occurs on read.
The evidence and choices retain the existing JSON projection shape (typed as
unknown in the new contract); construct state is explicitly typed.
State is scoped to learner, language, variety and construct. Uncertain and not-observed outcomes, excluded attempts, registry mismatches and future records make no update. Registry mismatches remain in exported observations. Repeated exact wording after whitespace normalization does not add independent evidence. Time affects review due / heuristic recall only, never rating or XP. Numeric uncertainty is an evidence-weight heuristic, not a calibrated probability or confidence interval; calibration is explicitly labeled. No CEFR bands are emitted.
config/policy/estimator.yaml is required, validated, seeded on fresh startup,
and included in configuration provenance. An existing configuration needs this
file explicitly installed; missing/invalid files still refuse startup.
Partner-scoped profile seam
get_learner_profile(target, personaId?) returns {evidence, model, partners, scope, constructLenses}. scope echoes {languageId, personaId}; null means all
partners. partners contains {personaId, name, archived} for owned contacts with
retained conversations in that language. constructLenses maps registry construct
IDs to lens IDs. These labels organize evidence; they do not create aggregate
proficiency scores.
Native code selects conversation IDs by durable partner identity and filters source records before folding estimates, under one store lock. The returned records and model are partner-scoped; the existing evidence profile totals, choices and conversation count remain language-wide. Exclusion still affects the whole attempt. YAML export remains language-wide regardless of profile view filters. Consumers validate the echoed scope and discard late responses after switching.
Wave 3 reward foundation
Accepted learner turns capture gamePolicy and gamePolicyHash. Validated
observation publication saves rewardEvents in the same transaction. Each
generated RewardEvent names its attempt, construct, exact quote, cause, tier,
XP, support, difficulty, novelty, policy hash, timestamp and presentation claim.
Current causes are construct_discovered, repair and xp_tick. No reward is
created by a read, elapsed time, a login, a message count or coach prose.
claim_reward_events(target, ids) atomically marks and returns previously
unclaimed events for that language (maximum 100 IDs). UI claims before presenting;
repeat claims and restart cannot replay the event. This is at-most-once display:
a crash after claiming may omit celebration but never removes earned XP.
Practice scoring rules version 2 reads saved awards instead of recomputing 10/2 credit. The UI can still read version-1 snapshots from the running prior binary. New game configuration does not revalue saved events. Whole-message wording is normalized for whitespace/case and awarded once per learner/language/construct. Novelty is first accepted award, first award in a Monday-based UTC week, or routine. XP is rounded to the nearest whole number. Exclusions and explicit source deletion remove those contributions from active totals; time and policy edits never do. No retroactive awards are synthesized for earlier exchanges lacking this policy.
Reward effects, secured constructs, goals and partner milestones remain subsequent work; the structural event tier does not claim their presentation is implemented.
Latest-message editing and feedback labels (2026-09-13)
A workspace revision can advance when coaching, disclosure or rewards change.
ReviseTurn accepts an older workspace revision for the current latest exchange
when no later turn would be removed. Current-version ownership and pending-reply
checks remain transactional. A suffix edit still requires a current reviewed
revision, and superseded targets remain invalid. This prevents background work
from making a normal edit look like a history conflict.
The learner-facing chip is “Feedback” for every available observation; it is not a pass/fail badge. “Feedback failed” describes a generation failure only. Editing is labelled “Edit message.” Persona prompt v8 explicitly distinguishes a learner answer from a question and forbids answering the partner's own previous question; coach observation prompt v5 requests descriptive, nonjudgmental wording.
Conversation direction and lesson removal (18 September 2026)
Lesson generation, review, handoff, quiz credit, lesson commands and lesson UI have been removed. Coaching, learner evidence and conversation rewards remain.
StartConversation accepts a validated ConversationStartConfig (difficulty,
variety and direction), optional real learner message and its input provenance,
and the reviewed workspace revision. It saves settings and admits a turn in one
transaction. Partner-first starts produce no learner message or skill evidence.
Direction contains an optional built-in/custom topic, any/past/future time
reference, and optional persona background. Topics have no language, variety,
difficulty, learner-focus or coverage gate. No authored example phrases or ranking
are involved. Language variety only supplies resolved language guidance.
Native preview and execution share the same prompt composer. Authored prose lives
in content/prompts/conversation/instructions.yaml. Form and validated YAML edit
one draft; preview has no inference. Apply stages no conversation turn. Saved-topic
mutations are atomic; UpdateConversationPrompt atomically applies those mutations
and revision-checked conversation settings for subsequent turns. Queued prompts
retain their captured settings. Custom text is copied into the conversation;
deleting a saved topic does not invalidate an existing selection. New conversations
reset topic and time preference. This contract supersedes earlier starter-selection
and lesson descriptions in the historical wave sections below/above.