Skip to content

feat(pattern-disruptor): add prompt disruption module#61

Merged
vi70x4 merged 2 commits into
mainfrom
vi/feat/pattern-disruptor
Jun 28, 2026
Merged

feat(pattern-disruptor): add prompt disruption module#61
vi70x4 merged 2 commits into
mainfrom
vi/feat/pattern-disruptor

Conversation

@vi70x4

@vi70x4 vi70x4 commented Jun 27, 2026

Copy link
Copy Markdown
Member

Summary

  • add the offline @proj-airi/pattern-disruptor package with EN/RU word banks, random/contextual/double-pass word injection, and synonym freshness scanning
  • persist per-card pattern disruptor settings and compose its prompt supplement with existing toolset guidance
  • add AIRI card editor controls and English i18n entries

Tests

  • pnpm -F @proj-airi/pattern-disruptor test:run
  • pnpm -F @proj-airi/pattern-disruptor typecheck
  • pnpm -F @proj-airi/pattern-disruptor build
  • pnpm -F @proj-airi/stage-ui exec vitest run src/stores/modules/airi-card.test.ts src/stores/chat.contract.test.ts
  • pnpm -F @proj-airi/stage-pages typecheck

Known Verification Gap

  • pnpm -F @proj-airi/stage-ui typecheck currently fails on unrelated test typing errors already present on the origin/main base (for example tts-session.test.ts and character/orchestrator/index.test.ts).

Ready for Gemini review.

Add the offline pattern disruptor package, per-card settings, chat prompt integration, and card editor controls.

Tests: pnpm -F @proj-airi/pattern-disruptor test:run; pnpm -F @proj-airi/pattern-disruptor typecheck; pnpm -F @proj-airi/pattern-disruptor build; pnpm -F @proj-airi/stage-ui exec vitest run src/stores/modules/airi-card.test.ts src/stores/chat.contract.test.ts; pnpm -F @proj-airi/stage-pages typecheck.

Known verification gap: pnpm -F @proj-airi/stage-ui typecheck fails on unrelated test typing errors already present on the origin/main base.
@deepsource-io

deepsource-io Bot commented Jun 27, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in f0ebbad...f32dcb1 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

Important

Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
JavaScript Jun 27, 2026 11:31p.m. Review ↗
Shell Jun 27, 2026 11:31p.m. Review ↗
C & C++ Jun 27, 2026 11:31p.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

ResolvedPatternDisruptorLanguage,
} from '../types'

const CYRILLIC_RE = /[\u0400-\u04FF]/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the 'u' flag with regular expressions


It is recommended to use the u flag with regular expressions.

return text.match(WORD_RE)?.map((token) => token.toLowerCase()) ?? []
}

export function normalizeToken(token: string, language: ResolvedPatternDisruptorLanguage): string {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`normalizeToken` has a cyclomatic complexity of 15 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

const blacklist = normalizeWordSet(input.settings.randomWords.blacklist)
const exactLength = input.settings.randomWords.wordLength

return input.bank.filter(([word, partOfSpeech]) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function has a cyclomatic complexity of 7 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

const lower = word.toLowerCase()
if (blacklist.has(lower) || input.history.has(lower)) return false
if (partsOfSpeech.size > 0 && !partsOfSpeech.has(partOfSpeech)) return false
if (exactLength > 0 && word.length !== exactLength) return false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boolean return can be simplified


The following pattern:

})
}

function uniqueWords(words: string[], count: number, history: Set<string>, settings: PatternDisruptorSettings) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`uniqueWords` has a cyclomatic complexity of 7 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

return uniqueWords([...associations, ...fallback], input.count, input.history, input.settings)
}

export function generateRandomWords(input: GenerateRandomWordsInput): string[] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`generateRandomWords` has a cyclomatic complexity of 6 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

count: number
}

function collectTokenStats(messages: string[], language: ResolvedPatternDisruptorLanguage): TokenStats[] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`collectTokenStats` has a cyclomatic complexity of 6 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

import { useI18n } from 'vue-i18n'

import CardCreationTabArtistry from './tabs/CardCreationTabArtistry.vue'
import CardCreationTabPatternDisruptor from './tabs/CardCreationTabPatternDisruptor.vue'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No default export found in imported module "./tabs/CardCreationTabPatternDisruptor.vue"


It is recommended to use default imports only if there is a default export in the module being imported. If there are no default exports in the source module, it is better to use named imports, and it helps the build tools while optimizing the code.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the @proj-airi/pattern-disruptor package, an offline prompt disruption engine designed to vary AI character responses through random word injection and synonym overuse tracking, alongside its integration into the settings UI and chat orchestrator. The review feedback highlights several critical areas for improvement: the template renderer should allow optional whitespace inside double braces; the word length filters in the random words generator and synonym scanner should be adjusted to support 3-letter Russian words like 'сад'; basic Russian inflection handling is needed to prevent dictionary matching failures; and the pattern disruptor state should be reset when switching sessions or characters to avoid word history leakage.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +8 to +10
function renderTemplate(template: string, values: Record<string, string | number>): string {
return template.replace(/\{\{(\w+)\}\}/g, (_, key: string) => String(values[key] ?? ''))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The template renderer uses a strict regular expression /\{\{(\w+)\}\}/g which does not allow any whitespace inside the double braces. If a user customizes their prompt and writes {{ words }} instead of {{words}}, the replacement will silently fail. Adding optional whitespace handling makes the template rendering much more robust.

Suggested change
function renderTemplate(template: string, values: Record<string, string | number>): string {
return template.replace(/\{\{(\w+)\}\}/g, (_, key: string) => String(values[key] ?? ''))
}
function renderTemplate(template: string, values: Record<string, string | number>): string {
return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key: string) => String(values[key] ?? ''))
}

}): string[] {
const keywords = tokenizeWords(input.userMessage)
.map((token) => normalizeToken(token, input.language))
.filter((token) => token.length >= 4 && !isStopword(token, input.language) && Boolean(input.synonyms[token]))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The filter enforces token.length >= 4, which prevents valid 3-letter words like сад (garden) from being matched as contextual anchors. Since сад is a key in ru/synonyms.json, it will be completely ignored in contextual mode. Since we already check Boolean(input.synonyms[token]), the length check is redundant and can be safely removed.

Suggested change
.filter((token) => token.length >= 4 && !isStopword(token, input.language) && Boolean(input.synonyms[token]))
.filter((token) => !isStopword(token, input.language) && Boolean(input.synonyms[token]))

for (const message of messages) {
for (const token of tokenizeWords(message)) {
const normalizedWord = normalizeToken(token, language)
if (normalizedWord.length < 4 || isStopword(normalizedWord, language)) continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The scanner enforces normalizedWord.length < 4, which prevents 3-letter words like сад (garden) from being scanned for overuse. Lowering this limit to 3 ensures that all valid words in the Russian vocabulary are correctly tracked.

Suggested change
if (normalizedWord.length < 4 || isStopword(normalizedWord, language)) continue
if (normalizedWord.length < 3 || isStopword(normalizedWord, language)) continue


export function normalizeToken(token: string, language: ResolvedPatternDisruptorLanguage): string {
const lower = token.toLowerCase().replace(/^[^\p{L}]+|[^\p{L}]+$/gu, '')
if (language === 'ru') return lower

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since Russian is a highly inflected language, returning the token as-is (if (language === 'ru') return lower) means that inflected forms (e.g., 'саду', 'яркая', 'историю') will fail to match the dictionary keys in synonyms.json (like 'сад', 'яркий', 'история'). This significantly reduces the effectiveness of the contextual mode and synonym scanner for Russian conversations. Consider implementing a basic suffix-stripper or mapping for the small Russian vocabulary to ensure effective matching.

Comment on lines +222 to +226
patternDisruptorStore.prepareForUserTurn({
settings: cardStore.activeCard?.extensions?.airi?.modules?.patternDisruptor,
messageText,
sessionMessages,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The patternDisruptorStore.resetState() function is defined but never called when switching sessions or characters. This can lead to word history leakage, where recently injected words from a previous character or session are carried over and affect the random word selection of a new session. Consider calling patternDisruptorStore.resetState() when the active session or active card changes.

Tests: pnpm -F @proj-airi/pattern-disruptor test:run; pnpm -F @proj-airi/pattern-disruptor typecheck; pnpm -F @proj-airi/pattern-disruptor build; pnpm -F @proj-airi/stage-ui exec vitest run src/stores/chat.contract.test.ts src/stores/modules/airi-card.test.ts; pnpm -F @proj-airi/stage-pages typecheck.

Known: pnpm -F @proj-airi/stage-ui typecheck still fails in unrelated existing test files (tts-session, character orchestrator, characters, speech, provider catalog).
@vi70x4 vi70x4 force-pushed the vi/feat/pattern-disruptor branch from 692d355 to f32dcb1 Compare June 27, 2026 23:31
return isDroppableEnglishPlural(withoutContraction) ? withoutContraction.slice(0, -1) : withoutContraction
}

function getKnownRussianSuffixCandidate(token: string): string | undefined {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected to return a value at the end of function 'getKnownRussianSuffixCandidate'


Any code paths that do not have explicit returns will return undefined. It is recommended to replace any implicit dead-ends that return undefined with a return null statement.

return new Set(Array.from(words ?? []).map((word) => word.toLowerCase()))
}

function isAllowedWordBankEntry(input: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`isAllowedWordBankEntry` has a cyclomatic complexity of 6 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

@vi70x4 vi70x4 merged commit d888c0f into main Jun 28, 2026
6 of 9 checks passed
@vi70x4 vi70x4 deleted the vi/feat/pattern-disruptor branch June 28, 2026 00:11
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.

2 participants