From 4b1667283c4f8f148f141af1d379767c32ad4cb0 Mon Sep 17 00:00:00 2001 From: Robert Rittich Date: Sat, 4 Jul 2026 09:59:19 -0400 Subject: [PATCH] Fix: SatisfactionCapture drops explicit N/10 ratings parseExplicitRating() returned null for '10/10', '9/10', '8 out of 10': the reject-on-slash guard treated the '/' after the number as invalid, so every explicit N/10 rating silently degraded into a sentiment-inferred guess. Add an N/10 (and 'N out of 10') branch before the generic parser, reusing a hoisted SENTENCE_STARTERS guard so a work phrase like '2/10 items done' is still correctly NOT treated as a rating. --- .../install/hooks/SatisfactionCapture.hook.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/LifeOS/install/hooks/SatisfactionCapture.hook.ts b/LifeOS/install/hooks/SatisfactionCapture.hook.ts index 9385ec502..828b4dd1c 100755 --- a/LifeOS/install/hooks/SatisfactionCapture.hook.ts +++ b/LifeOS/install/hooks/SatisfactionCapture.hook.ts @@ -69,6 +69,10 @@ const RATINGS_FILE = join(SIGNALS_DIR, 'ratings.jsonl'); const LAST_RESPONSE_CACHE = join(BASE_DIR, 'MEMORY', 'STATE', 'last-response.txt'); const MIN_PROMPT_LENGTH = 3; +// Sentence-starters that mean a leading number is describing work, not rating it +// (e.g. "2/10 items done", "3 of the files"). Shared by the fraction and generic parsers. +const SENTENCE_STARTERS = /^(items?|things?|steps?|files?|lines?|bugs?|issues?|errors?|times?|minutes?|hours?|days?|seconds?|percent|%|th\b|st\b|nd\b|rd\b|of\b|in\b|at\b|to\b|the\b|a\b|an\b)/i; + // ── Stdin Reader ── async function readStdinWithTimeout(timeout: number = 5000): Promise { @@ -111,6 +115,16 @@ function parseExplicitRating(prompt: string): { rating: number; comment?: string } } + // N/10 form (e.g. "10/10, thank you", "9 / 10", "8 out of 10 nice"). + // Must run BEFORE the generic parser, whose reject-on-slash guard would drop it. + const fractionMatch = trimmed.match(/^(10|[1-9])\s*(?:\/|out\s+of)\s*10\b(.*)$/i); + if (fractionMatch) { + const fRating = parseInt(fractionMatch[1], 10); + const fRest = fractionMatch[2].replace(/^[\s!.,:;-]+/, '').trim() || undefined; + if (fRest && SENTENCE_STARTERS.test(fRest)) return null; // "2/10 items" is not a rating + return { rating: fRating, comment: fRest }; + } + const ratingPattern = /^(10|[1-9])(?:\s*[-:]\s*|\s+)?(.*)$/; const match = trimmed.match(ratingPattern); if (!match) return null; @@ -123,10 +137,7 @@ function parseExplicitRating(prompt: string): { rating: number; comment?: string const afterNumber = trimmed.slice(match[1].length); if (afterNumber.length > 0 && /^[/.\dA-Za-z]/.test(afterNumber)) return null; - if (rest) { - const sentenceStarters = /^(items?|things?|steps?|files?|lines?|bugs?|issues?|errors?|times?|minutes?|hours?|days?|seconds?|percent|%|th\b|st\b|nd\b|rd\b|of\b|in\b|at\b|to\b|the\b|a\b|an\b)/i; - if (sentenceStarters.test(rest)) return null; - } + if (rest && SENTENCE_STARTERS.test(rest)) return null; return { rating, comment: rest }; }