Skip to content

Commit 0e613bd

Browse files
authored
fix(lifecycle): suppress reported deference mentions (#14436) (#14437)
* fix(lifecycle): suppress reported deference mentions (#14420) * fix(lifecycle): scan all deference phrase occurrences (#14436)
1 parent 597ee98 commit 0e613bd

2 files changed

Lines changed: 79 additions & 3 deletions

File tree

ai/scripts/lifecycle/deferencePhraseMatch.mjs

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,48 @@ function stripMarkdownCode(text) {
4747
.replace(/`[^`\n]*`/g, ' ');
4848
}
4949

50+
/**
51+
* @summary Replaces quoted prose spans that report a phrase instead of using it.
52+
* @param {String} text Assistant final-turn text with code spans already removed.
53+
* @returns {String}
54+
*/
55+
function stripQuotedMentions(text) {
56+
return text
57+
.replace(/"[^"\n]*"/g, ' ')
58+
.replace(/(^|[^a-zA-Z0-9_])'[^'\n]*'(?=$|[^a-zA-Z0-9_])/g, '$1 ');
59+
}
60+
61+
/**
62+
* @summary Checks whether a local match is a reported phrase rather than deference.
63+
* @param {String} text Searchable assistant final-turn text.
64+
* @param {Number} startIndex Match start index.
65+
* @returns {Boolean}
66+
*/
67+
function isReportedMentionContext(text, startIndex) {
68+
const prefix = text.slice(Math.max(0, startIndex - 80), startIndex).toLowerCase();
69+
70+
return /\b(?:the|this|that)\s+(?:literal\s+)?(?:phrase|text|string|trigger|matched\s+text|wording)\s*$/.test(prefix) ||
71+
/\b(?:quoted|reported|mention(?:ed|ing)?|document(?:ed|ing)?)\s*$/.test(prefix);
72+
}
73+
74+
/**
75+
* @summary Checks whether a local "your call" match cites a prior operator decision.
76+
* @param {String} phrase Matched deference phrase.
77+
* @param {String} text Searchable assistant final-turn text.
78+
* @param {Number} startIndex Match start index.
79+
* @returns {Boolean}
80+
*/
81+
function isAttributiveCitationContext(phrase, text, startIndex) {
82+
if (phrase.toLowerCase() !== 'your call') {
83+
return false;
84+
}
85+
86+
const prefix = text.slice(Math.max(0, startIndex - 80), startIndex).toLowerCase();
87+
88+
return /\bper\s+$/.test(prefix) ||
89+
/\bas\s+you\s+(?:said|directed|called)\W*$/.test(prefix);
90+
}
91+
5092
/**
5193
* @summary Returns the first deference phrase found in text, using case-insensitive boundary match.
5294
* @param {String} text Assistant final-turn text.
@@ -58,13 +100,23 @@ export function matchDeferencePhrase(text = '', phrases = DEFERENCE_PHRASES) {
58100
return null;
59101
}
60102

61-
const searchableText = stripMarkdownCode(text);
103+
const searchableText = stripQuotedMentions(stripMarkdownCode(text));
62104

63105
return phrases.find(phrase => {
64106
const escaped = phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+'),
65-
matcher = new RegExp(`(^|[^a-z0-9_])${escaped}(?=$|[^a-z0-9_])`, 'i');
107+
matcher = new RegExp(`(^|[^a-z0-9_])${escaped}(?=$|[^a-z0-9_])`, 'ig');
108+
let match;
109+
110+
while ((match = matcher.exec(searchableText)) !== null) {
111+
const startIndex = match.index + match[1].length;
112+
113+
if (!isReportedMentionContext(searchableText, startIndex) &&
114+
!isAttributiveCitationContext(phrase, searchableText, startIndex)) {
115+
return true;
116+
}
117+
}
66118

67-
return matcher.test(searchableText);
119+
return false;
68120
}) || null;
69121
}
70122

test/playwright/unit/hooks/deferencePhraseMatch.spec.mjs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,30 @@ test.describe('ai/scripts/lifecycle/deferencePhraseMatch', () => {
4747
expect(matchDeferencePhrase('```text\nYour steer on the next lane.\n```')).toBeNull();
4848
});
4949

50+
test('does not match quoted or reported phrase mentions', () => {
51+
expect(matchDeferencePhrase('The "your call" firing was a demonstrable false positive.')).toBeNull();
52+
expect(matchDeferencePhrase("The 'per your call' firing was a demonstrable false positive.")).toBeNull();
53+
expect(matchDeferencePhrase('The phrase your call is mentioned in the #14420 corpus.')).toBeNull();
54+
expect(matchDeferencePhrase("The phrase if you'd rather is part of the deference register.")).toBeNull();
55+
});
56+
57+
test('does not match attributive citations of an operator decision', () => {
58+
expect(matchDeferencePhrase('Clio owns it, per your call.')).toBeNull();
59+
expect(matchDeferencePhrase('The ownership route stands as you directed: your call is the source.'))
60+
.toBeNull();
61+
});
62+
63+
test('still matches live deference uses of your call', () => {
64+
expect(matchDeferencePhrase('Your call on the branch cut.')).toBe('your call');
65+
expect(matchDeferencePhrase("It's your call whether I pick this up.")).toBe('your call');
66+
expect(matchDeferencePhrase('Your call?')).toBe('your call');
67+
});
68+
69+
test('still fires when a live use follows a carved mention of the same phrase', () => {
70+
expect(matchDeferencePhrase('The phrase your call recurs. Your call on the merge?')).toBe('your call');
71+
expect(matchDeferencePhrase('Clio owns it per your call, but honestly, your call?')).toBe('your call');
72+
});
73+
5074
test('operator-dialogue carve skips the phrase match', () => {
5175
expect(detectDeferencePhrase('Your call on the exact color.', {operatorInLoop: true})).toBeNull();
5276
expect(detectDeferencePhrase('Your call on the exact color.', {operatorInLoop: false})).toBe('your call');

0 commit comments

Comments
 (0)