Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/services/__tests__/extraction-enrichment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,46 @@ describe('instruction tagging (EXP-05)', () => {
expect(detectInstructionFact('User favorite color is blue.')).toBe(false);
});

// EXP-05 H-310 — BEAM soft-imperative additions.
// BEAM users phrase instructions softer than the original strict
// imperatives. These positive cases mirror the surface form that the
// extraction pipeline emits (third person "user prefers / wants / ...").
const BEAM_INSTRUCTION_PHRASES = [
'User prefers simple, minimal dependencies to keep the app lightweight.',
'User wants the dashboard API response time to stay under 250ms.',
'User would like all responses to include explicit version numbers.',
"I'd like the assistant to suggest only lightweight libraries.",
'Please respond in formal English when discussing security topics.',
'Assistant should always include unit tests in code examples.',
'Assistant should never log passwords or session tokens.',
'User preference is to use TypeScript over JavaScript for new files.',
'User has a preference for syntax-highlighted code snippets in discussions.',
];

for (const phrase of BEAM_INSTRUCTION_PHRASES) {
it(`H-310: detects BEAM soft-imperative in: "${phrase}"`, () => {
expect(detectInstructionFact(phrase)).toBe(true);
});
}

// False-positive prevention: phrasings that LOOK directive but are
// actually questions or negations and must NOT be tagged.
const BEAM_FALSE_POSITIVE_PHRASES = [
// Question prefixes — "wants to know" is a query, not a standing rule.
'User wants to know how Flask sessions work.',
"I'd like to know which Python version is currently installed.",
'User would like to ask about the difference between SQLite and Postgres.',
// Negations — "prefers not to" is not a standing rule we should boost.
'User prefers not to use heavy frameworks for this project.',
'User does not prefer dark mode in the IDE.',
];

for (const phrase of BEAM_FALSE_POSITIVE_PHRASES) {
it(`H-310: suppresses false positive: "${phrase}"`, () => {
expect(detectInstructionFact(phrase)).toBe(false);
});
}

it('tags fact metadata with fact_role=instruction and floors importance', () => {
const fact = buildFact('Always respond to me in formal English.', []);

Expand Down
29 changes: 7 additions & 22 deletions src/services/extraction-enrichment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import type { ExtractedEntity, ExtractedFact, ExtractedRelation } from './extraction.js';
import { dedupeEntities } from './entity-dedup.js';
import { inferEventAnchorFacts, type EventAnchorOptions } from './event-anchor-facts.js';
import { matchesInstructionMarker } from './instruction-markers.js';

export type EnrichmentOptions = EventAnchorOptions;

Expand All @@ -20,27 +21,12 @@ const KNOWS_MARKERS = ['advisor', 'career advice from', 'supportive', 'recommend
const STUDIES_MARKERS = ['focus on', 'studying', 'research', 'paper on'];

/**
* Imperative phrasing markers that flag an extracted fact as an explicit
* instruction. Detection is intentionally conservative — false positives on
* assistant tutorial content are acceptable since extraction prefixes those
* with "Assistant mentioned:". When a marker hits, the fact gains
* `metadata.fact_role: 'instruction'` and its importance is floored to 0.95
* so lifecycle decay does not evict it.
* Imperative-phrase detection lives in `instruction-markers.ts` so it can
* be extended (BEAM soft imperatives, H-310) without bloating this file.
* When a marker hits, the fact gains `metadata.fact_role: 'instruction'`
* and its importance is floored to INSTRUCTION_IMPORTANCE_FLOOR so
* lifecycle decay does not evict it.
*/
const INSTRUCTION_MARKERS = [
'always ',
'never ',
'from now on',
'please remember',
'make sure to',
"don't forget",
'do not forget',
'every time',
'whenever you',
'going forward',
'in the future',
'remember to',
] as const;

/** Floor for instruction-tagged importance — see plan EXP-05 risks (2). */
const INSTRUCTION_IMPORTANCE_FLOOR = 0.95;
Expand Down Expand Up @@ -123,8 +109,7 @@ function applyInstructionTagging(fact: ExtractedFact): ExtractedFact {

/** Returns true when the fact text contains an imperative instruction marker. */
export function detectInstructionFact(text: string): boolean {
const lower = ` ${text.toLowerCase()} `;
return INSTRUCTION_MARKERS.some((marker) => lower.includes(marker));
return matchesInstructionMarker(text);
}

function shouldAddSelfEntity(text: string): boolean {
Expand Down
109 changes: 109 additions & 0 deletions src/services/instruction-markers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* Instruction-marker phrase list for EXP-05 (H-310).
*
* Detects facts that express a user-supplied directive, preference, or
* standing instruction. Two layers:
*
* 1. INSTRUCTION_MARKERS — case-insensitive substrings that flag the fact.
* 2. FALSE_POSITIVE_PATTERNS — subsequences that disqualify a hit
* (e.g. "wants to know" is a question prefix, not a directive).
*
* Both lists are intentionally additive on top of the original strict
* imperatives ("always X / never Y / from now on / going forward / make
* sure to / ...") so existing behavior is preserved. The expansion
* targets BEAM-style soft imperatives that the Stage-7 v1 dryrun missed
* (only 15/2000 facts tagged — see plan H-310).
*
* Detection runs over the post-extraction fact form, which is third
* person ("user prefers X", "user wants Y") — so the markers match that
* surface form rather than first-person ("I prefer X"). Both forms are
* supported by relying on the unanchored substring "prefer" / "want".
*/

/**
* Phrases that flag an extracted fact as an explicit instruction or
* preference. Matched as case-insensitive substrings against the fact
* text padded with spaces on either side.
*
* Ordering note: the list MUST keep all original strict imperatives at
* the top so that existing behavior is preserved verbatim.
*/
export const INSTRUCTION_MARKERS = [
// --- Original strict imperatives (preserved verbatim from EXP-05 v1) ---
'always ',
'never ',
'from now on',
'please remember',
'make sure to',
"don't forget",
'do not forget',
'every time',
'whenever you',
'going forward',
'in the future',
'remember to',
// --- BEAM soft-imperative additions (H-310) ---
// Preferences (PF questions)
'prefer ',
'prefers ',
'preference is',
'preference for',
// First/third-person desires that read as standing requests
' want ',
' wants ',
"i'd like",
'would like',
// Standing-rule phrasings users issue mid-conversation
'please ',
'should always',
'should never',
'must always',
// Inclusion/format directives
'always include',
'always use',
'use only',
] as const;

/**
* Disqualifying sub-phrases. If any of these appears in the lower-cased
* fact text, the instruction tag is suppressed even when an
* INSTRUCTION_MARKERS entry matched. These cover cases where the
* marker word is part of a question or hedge rather than a directive.
*
* Examples blocked:
* - "user wants to know" → question prefix, not a directive
* - "user would like to know" / "would like to ask" → question prefix
* - "user prefers not to" → negation; expressed as not-doing, not a rule
* - "user does not prefer" → negation
* - "i want to know" → question, surfaces post-extraction unchanged
*/
export const FALSE_POSITIVE_PATTERNS: readonly string[] = [
'want to know',
'wants to know',
'wanted to know',
'would like to know',
'would like to ask',
"i'd like to know",
"i'd like to ask",
'prefer not to',
'prefers not to',
'does not prefer',
'do not prefer',
] as const;

/**
* Returns true when the fact text contains at least one instruction
* marker AND is not blocked by a false-positive pattern.
*
* The leading/trailing space pad lets us match word-boundary-like
* patterns ("always ", " want ") without resorting to regex. The pad
* also makes "wants" not match a marker that requires "want " with a
* trailing space — only " wants " (the dedicated marker) matches.
*/
export function matchesInstructionMarker(text: string): boolean {
const lower = ` ${text.toLowerCase()} `;
if (FALSE_POSITIVE_PATTERNS.some((pattern) => lower.includes(pattern))) {
return false;
}
return INSTRUCTION_MARKERS.some((marker) => lower.includes(marker));
}