[object Object]

← back to Wallco Ai

yolo-queue: SG-1..SG-4 ✓ attorney narrowings (moth/bird/scatter/leaf word-boundaries)

26ab5d30f17821e44967e10adf1d8b40007cd06e · 2026-05-25 20:26:21 -0700 · Steve Abrams

Files touched

Diff

commit 26ab5d30f17821e44967e10adf1d8b40007cd06e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon May 25 20:26:21 2026 -0700

    yolo-queue: SG-1..SG-4 ✓ attorney narrowings (moth/bird/scatter/leaf word-boundaries)
---
 .yolo-queue.md             | 15 +++++++++++----
 scripts/settlement-gate.js | 40 ++++++++++++++++++++++++++++++----------
 2 files changed, 41 insertions(+), 14 deletions(-)

diff --git a/.yolo-queue.md b/.yolo-queue.md
index bd27040..eaedb2d 100644
--- a/.yolo-queue.md
+++ b/.yolo-queue.md
@@ -80,27 +80,34 @@ never silently pause overnight.
 
 ## Settlement gate narrowings (attorney follow-ups)
 
-- [ ] **SG-1** Tighten `PART_B` in `scripts/settlement-gate.js`. Drop
+- [x] **SG-1** Tighten `PART_B` in `scripts/settlement-gate.js`. Drop
   `'moth'` — the binding says "butterflies (any species)" but moths
   are Lepidoptera-adjacent, not butterflies. Defendant-favorable read =
   remove. Add inline `// 2026-05-25 attorney narrowing` comment.
 
-- [ ] **SG-2** Fix `'bird'` false positives. Currently substring-matches
+- [x] **SG-2** Fix `'bird'` false positives. Currently substring-matches
   `bird of paradise` (plant), `birdcage`, `blackbird`. Use a word-boundary
   regex check instead: `/\b(?:bird|birds)\b(?!\s+of\s+paradise)/i`. Keep
   `parrot` / `toucan` as-is (less false-positive risk).
 
-- [ ] **SG-3** Untangle `scatter`/`tossed` overlap between
+- [x] **SG-3** Untangle `scatter`/`tossed` overlap between
   `PART_A_DIRECTIONAL` and `PART_A_OPEN_SPACE`. Same token currently
   trips BOTH A1 and A2 from a single mention. Move `'scatter'` and
   `'tossed'` to ONE list only (recommend OPEN_SPACE since they describe
   layout, not direction).
 
-- [ ] **SG-4** Word boundaries on `PART_A_LEAVES`. `'leaf'` substring
+- [x] **SG-4** Word boundaries on `PART_A_LEAVES`. `'leaf'` substring
   matches `leaflet`, `leafy`, `leaflike`. Use `\bleaf(?:s|ves)?\b` regex.
   Convert each entry in PART_A_LEAVES into a regex with word boundaries
   and change `has()` helper to use regex.test instead of includes.
 
+  > NOTE 2026-05-25 ✓ SG-1..4 bundled in single ship. Split PART_B into
+  > _STR (substring) + _RE (regex) lists, added PART_A_LEAVES_RE regex
+  > word-boundary variant + hasRe() helper. 6-case regression test all
+  > pass: 'monstera leaflet' (SG-4), 'bird of paradise' (SG-2), 'moth'
+  > (SG-1) all correctly avoid false-positive Part B; legit 'bird' +
+  > full Part A still BLOCKs. SG-3: scatter/tossed only in OPEN_SPACE.
+
 - [ ] **SG-5** Add a single test file `scripts/settlement-gate.test.js`
   with at least 10 cases: 3 legit BLOCKs (real leaf + open + multi-color
   + Part B), 3 OK (negation-suffix prompts that previously over-blocked),
diff --git a/scripts/settlement-gate.js b/scripts/settlement-gate.js
index c0f09b2..aaffcf2 100644
--- a/scripts/settlement-gate.js
+++ b/scripts/settlement-gate.js
@@ -15,11 +15,18 @@
  */
 'use strict';
 
-const PART_A_LEAVES = [
-  'palm', 'frond', 'banana leaf', 'monstera', 'leaves', 'leaf', 'foliage', 'jungle',
+// SG-4 (2026-05-25 attorney narrowing): leaf tokens now use word
+// boundaries via regex so 'leaflet', 'leafy', 'leaflike' don't trip.
+// `\bword\b` for each entry; plural/possessive permitted.
+const PART_A_LEAVES_RE = [
+  /\bpalm\b/i, /\bfronds?\b/i, /\bbanana\s+leaf\b/i, /\bmonstera\b/i,
+  /\bleaf\b/i, /\bleaves\b/i, /\bfoliage\b/i, /\bjungle\b/i,
 ];
+// SG-3 (2026-05-25): 'scatter'/'tossed' moved out of PART_A_DIRECTIONAL.
+// They describe layout, not directional variation — keeping them in
+// OPEN_SPACE only so a single token doesn't trip both A1 and A2.
 const PART_A_DIRECTIONAL = [
-  'directional', 'varied angle', 'tossed', 'scatter', 'rotated', 'multi-angle',
+  'directional', 'varied angle', 'rotated', 'multi-angle',
   'overlapping', 'turning leaves', 'fanning',
 ];
 const PART_A_OPEN_SPACE = [
@@ -30,9 +37,17 @@ const PART_A_MULTICOLOR_NEG = [
   'silhouette', 'tonal',
 ];
 
-const PART_B = [
-  'banana', 'banana pod', 'grape', 'bird', 'birds', 'parrot', 'toucan',
-  'butterfly', 'butterflies', 'moth',
+// SG-1 + SG-2 (2026-05-25 attorney narrowing):
+//   - Dropped 'moth' — binding says 'butterflies (any species)' which
+//     does not extend to moths (different family within Lepidoptera).
+//   - 'bird' / 'birds' now use word boundaries + exclude 'bird of
+//     paradise' (the plant), 'birdcage', 'blackbird', 'mockingbird'.
+const PART_B_STR = [
+  'banana', 'banana pod', 'grape', 'parrot', 'toucan',
+  'butterfly', 'butterflies',
+];
+const PART_B_RE = [
+  /\bbirds?\b(?!\s+of\s+paradise)/i,  // bird/birds, but NOT 'bird of paradise' (plant)
 ];
 
 const ACCEPTABLE = [
@@ -43,6 +58,8 @@ const ACCEPTABLE = [
 ];
 
 const has = (text, list) => list.some(t => text.includes(t));
+// SG-4: regex variant of `has` for word-boundary checks.
+const hasRe = (text, regexList) => regexList.some(re => re.test(text));
 
 // Strip negative-prompt context per settlement-part-b SKILL.md lines 58-60:
 // "presence in negative-prompt block does NOT count as positive."
@@ -68,7 +85,8 @@ function checkPrompt(prompt) {
   const text = stripNegations(raw).toLowerCase();
 
   // Part A score
-  const a1 = has(text, PART_A_LEAVES) && has(text, PART_A_DIRECTIONAL);
+  // SG-4: PART_A_LEAVES now uses regex word boundaries.
+  const a1 = hasRe(text, PART_A_LEAVES_RE) && has(text, PART_A_DIRECTIONAL);
   const a2 = has(text, PART_A_OPEN_SPACE);
   const a3Singlecolor = has(text, PART_A_MULTICOLOR_NEG);
   // Multi-color is the default assumption unless the prompt explicitly says
@@ -77,8 +95,10 @@ function checkPrompt(prompt) {
 
   const partA = a1 && a2 && a3;
 
-  // Part B score
-  const partBFound = PART_B.filter(el => text.includes(el));
+  // Part B score — SG-1 (no moth) + SG-2 (bird word-boundary + not 'bird of paradise').
+  const partBStrFound = PART_B_STR.filter(el => text.includes(el));
+  const partBReFound  = PART_B_RE.filter(re => re.test(text)).map(re => String(re).match(/\\b([a-z]+)/)?.[1] || re.source);
+  const partBFound = [...partBStrFound, ...partBReFound];
   const partB = partBFound.length > 0;
 
   // Acceptable cues
@@ -92,7 +112,7 @@ function checkPrompt(prompt) {
 
   if (partA && partB) {
     verdict = 'BLOCK';
-    reason = `Settlement violation: Part A (directional ${PART_A_LEAVES.find(l => text.includes(l)) || 'leaves'} + open space + multi-color) AND Part B (${partBFound.join(', ')}) both present.`;
+    reason = `Settlement violation: Part A (directional leaves + open space + multi-color) AND Part B (${partBFound.join(', ')}) both present.`;
     rewrite = `Drop one of:
   - the directional-leaf scatter (render on cane lattice / trellis / geometric substrate instead), OR
   - the open-space / tossed composition (use edge-to-edge coverage), OR

← 9228ce8 drunk-animals: tiki/cognac/champagne/smoking-bubble + textur  ·  back to Wallco Ai  ·  yolo-queue: escalation policy — debate-team-fast on every it 2bde4b7 →