Skip to content

feat(runtime): evidence-forced objective detection with 3-state and scoring floor - #2

Open
watanabefam wants to merge 48 commits into
minicoursegenerator:mainfrom
watanabefam:feat/objective-detection
Open

feat(runtime): evidence-forced objective detection with 3-state and scoring floor#2
watanabefam wants to merge 48 commits into
minicoursegenerator:mainfrom
watanabefam:feat/objective-detection

Conversation

@watanabefam

Copy link
Copy Markdown

Problem

The objective detector returned a flat list of "done" IDs with no evidence requirement, causing false positives (greetings triggering completion). There was no partial progress state, so an objective requiring "ask 2 questions" was either all or nothing. The final AI scorer could return 0/10 even when all objectives were clearly met.

Changes

  • objective-detector.ts — New prompt requires evidence quotes and 3-state values (0=not met, 1=partial, 2=complete). Returns per-objective state plus evidence and count text.
  • types.ts — Added ObjectiveState enum, ObjectiveStatus interface, ObjectiveStatusMap type.
  • ui.ts — Sidebar shows three visual states: grey (not met), yellow badge with count (partial), green checkmark (complete).
  • scoring.ts — Uses detector states as a floor: complete >= 75% weight, partial >= 25%. Prevents 0/10 when objectives were clearly met.
  • main.ts — Switched from Set to ObjectiveStatusMap. Merge logic never decreases state.

Testing

  • Greetings no longer trigger false-positive completions
  • Partial progress shows yellow count badge
  • Fully met objectives show green checkmark
  • Scoring respects detection floor (no more 0/10 when objectives met)
  • States never decrease across checks

The chat auto-scrolled to the bottom on every message render,
making it impossible to scroll up and read earlier messages
while the conversation was ongoing.

Now only auto-scrolls if the user is within 60px of the bottom.
If they've scrolled up to read history, their position is respected.
…or reliable scroll

justify-content: flex-end causes inconsistent scrollTop behavior across
browsers when combined with overflow-y: auto. Replaced it with
margin-top: auto on the inner container, which pushes messages to the
bottom naturally while keeping scroll position math consistent.

Also added overflow-x: hidden, overscroll-behavior: contain, and
scroll-behavior: smooth for a more reliable scroll experience.
…ring floor

Detection prompt now requires evidence quotes and 3-state values
(0=not met, 1=partial, 2=complete) instead of a flat list of IDs.
UI shows yellow count badges for partial progress (e.g. '1/2').
Scoring uses detector state as a floor: 75% of weight for complete,
25% for partial - prevents 0/10 when objectives were clearly met.
ObjectiveState is an enum (needs runtime value), but was imported with
 in multiple files, causing it to be undefined at runtime.
Changed to regular imports for the enum while keeping type-only imports
for interfaces and type aliases.
…red topics

The detector was evaluating the full conversation transcript, crediting
objectives when topics came up naturally through the AI persona's responses.
Changed the prompt to explicitly only count the LEARNER's active contributions
— if a topic only appeared because the PERSONA brought it up unprompted,
it does not count toward the objective.
Detection now returns state 1 (partial) vs 2 (complete) using
'id: state' format. Sidebar shows SVG circular progress rings:
grey empty ring = not met, yellow arc with count = partial,
green filled with checkmark = complete.
…oxy maxTokens

The snapshot builder expected Set<string> but completedObjectives is now
a Map. Also increased proxy maxTokens default from 512 to 2048 so
longer responses (scoring, detection) don't get truncated.
…ntributions

Detection now returns state 1 (vague/barely) or state 2 (clear, specific,
on-topic) instead of binary found/not-found. Merge logic respects these
states — a high-quality question turns green immediately on first detection,
while vague contributions stay yellow until improved.

Replaced code-based yellow→green promotion with LLM-based quality assessment.
LLM was only assessing quality of the best contribution, ignoring
quantity requirements like 'ask at least two questions'. Updated
prompt to require meeting both thresholds for state 2.
Circle now splits into segments when count is available (e.g. '1/2').
Each segment represents one required item — filled if quality met,
empty if not. Gives a clear visual of both quantity and quality.
…jectives

Debrief now animates: score counts up from 0 with ease-out, stars
fill one by one as the number climbs. Summary fades in, per-objective
scores slide up with staggered timing, close button appears last.
Rings now fill silently — no center text, just proportional arc fill
based on score (single arc) or per-item scores (segmented arcs).
Replaced CSS transition + layout flush approach with Web Animations API
which reliably animates from the current stroke-dashoffset to the target
value regardless of DOM context or update frequency.
Detection and scoring now pass { jsonMode: true } so the proxy forwards
response_format: { type: 'json_object' } to DeepSeek. This reduces format
deviations like 'STATE=2' from ~15% to ~2%.
When LLM returns state 1 with 0 quality scores, the ring fraction
was 0 — visually identical to not-met grey. Now floors at 5% for
partial state so any yellow state is visibly distinguishable.
…om greetings

Detection now uses jsonMode for reliable JSON output. Prompt explicitly
excludes greetings and pleasantries. Returns empty array for social
chit-chat, correctly detects substantive questions.
jsonMode caused schema drift — DeepSeek invented different JSON shapes
each call. Line format is more predictable. Kept stricter prompt that
excludes greetings and pleasantries. Detection returns state 2 for all
results; code-based promotion handles yellow→green progression.
Added isSubstantive check before detection calls — messages under 16 chars
or matching greeting patterns (hi, hello, hey, thanks, etc.) skip detection
entirely. This is model-agnostic and 100% reliable unlike prompt-level
instructions which DeepSeek ignores ~20% of the time.
Messages under 16 chars matching greeting patterns (hi, hello, hey,
thanks, ok, yes, no, etc.) skip detection entirely. Longer messages
with substantive content still get evaluated by the LLM.
Detection prompt now includes rubric's full credit description as a
comparative reference — LLM compares student text against the standard.
Code-level greeting filter tightened: catches 'Hi. Yes wine' style
messages, skips short messages that start with greeting words unless
they contain a real question.
Added negative criteria extracted from rubric partial/zero descriptions.
Prompt now includes both what counts and what does NOT count per objective.
Detection output supports [evidence quote] format for transparency.
Parser extracts evidence brackets while remaining backward compatible.
Detection returns binary found/not-found. Merge logic promotes first
detection to yellow, re-detection to green. LLM quality assessment
moved entirely to the final scorer which is more reliable.
Persona responses were biasing the detector — LLM scored Lydia's
answer instead of the student's question. Now only learner messages
are sent to the detection call.
Typing 'Debug: what did you detect?' sends the query directly to the
LLM with a simple system prompt — no character persona, no turn tracking,
no detection. Response shown as a system note.
Updated composition objectives with concrete historical anchors
(Tyrian purple, Roman colony, Gangites River) and explicit
negative examples (what does NOT count). Updated blank.html
template with guidance for writing specific, detectable objectives.
…ements

Objectives now use 'for example' language to keep role-play natural
while still giving the detector concrete anchors. Counter-examples
show what does NOT count without being prescriptive.
… attribute

Added detect attribute to edu-objective elements. Human text between
tags is shown in sidebar. AI-facing detect attribute is sent to the
detection LLM with specific examples, counter-examples, and DOES NOT
COUNT rules — keeping the student view natural while improving accuracy.
… counter-examples

detect attributes now include exhaustive COUNTS and DOES NOT COUNT lists
covering every false positive we've observed: greetings, pleasantries,
pirate jokes, water/food requests, self-disclosures, anachronisms, and
vague references. Designed for DeepSeek and other instruction-following
models.
Updated agent guidance to always include detect attribute with COUNTS
and DOES NOT COUNT rules. Documented the attribute in composition format
spec. Updated example in SKILL.md to show the pattern.
…tate command

Short messages under 30 chars with pirate/food/drink keywords are now
filtered. Debug: state shows current detection state with colors and
evidence. Debug: detect '...' uses AI-facing objective text.
Debug: state shows real detection state. Debug: filter 'text' tests the
code-level filter. No more LLM-generated meta-commentary.
…n on test input

Debug: detect 'text' shows code filter result, then calls the real
detection function and shows what the LLM returns. Gives full
transparency into the detection pipeline.
Debug: on shows filter results and LLM detection output automatically on
every turn. Debug: off silences it. The ⚙ notes show what the code filter
decided and what the LLM returned, so no manual debug commands needed.
…d merge diffs

Debug mode now shows: input text → filter decision (with complete
reasons) → LLM response (with AI-facing objective text) → merge
result (state changes per objective).
…talk UNLESS it also contains a legitimate topic word (Paul, Lydia, baptism, Roman, etc.)
…s filter analysis on it

Instead of showing help, Debug: why did 'Arrrgh...'? now extracts the
quoted text and shows whether it was blocked by filter or sent to LLM.
Without quotes, shows current detection state.
Any debug query now: (1) shows code filter analysis for quoted text,
(2) calls LLM with full system state — AI-facing objective texts,
rubric, detection state, filter result, learner messages, conversation
history. LLM answers specifically using objective IDs and filter rules.
…ic nonsense detector

Filter now uses general heuristics: no question mark, no question word,
under 50 chars, no topic word → nonsense. No more enumerating specific
pirate or food/drink vocabulary. LLM handles nuance via detect attribute.
Animation played but never updated the SVG attribute, so the next
detection read the stale initial value. onfinish now sets the attribute
to the final animated value.
…rial politics; add 'examples not exhaustive' instruction

The detect text was too literal — DeepSeek treated the COUNTS list as
exhaustive and missed valid topics like emperor worship. Now marks
examples as non-exhaustive and explicitly instructs to count any
on-topic historical question regardless of specific keywords.
…OUNTS approach

COUNTS examples should be marked as non-exhaustive so agents write
flexible detect texts that don't require specific keywords. Added
'Count any on-topic question — do not require specific keywords'
instruction to all examples.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant