Skip to content

feat(safety): add native PII detection and response validation to generate/stream#952

Open
murdore wants to merge 1 commit into
releasefrom
feat/io-processors
Open

feat(safety): add native PII detection and response validation to generate/stream#952
murdore wants to merge 1 commit into
releasefrom
feat/io-processors

Conversation

@murdore

@murdore murdore commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Restructures the I/O processor feature to integrate directly into generate() and stream() as native options.

What changed

  • PII detection wired into generate/stream input path as piiDetection: { enabled, action, detectTypes }
  • Response validation wired into output path as responseValidation: { maxLength, truncationAction, retryOnFailure }
  • Input validation as inputValidation: { trimWhitespace, minLength, maxLength, requireContent }
  • Tripwire evaluator extracted as standalone utility
  • CLI flags: --pii-redact, --pii-types, --output-max-length, --input-max-length, --trim-whitespace

What was removed

  • Parallel ProcessorPipeline, IOProcessorRegistry, preset system, 10 processor implementations
  • 4 stubs, 3 duplicates of existing subsystems, 1 low-quality heuristic → all deleted
  • 2 genuinely useful processors → extracted as standalone utilities

Before vs After

Metric Before After
Lines added 16,127 3,279
Files changed 48 17
Browser bundle 4.22 MB 4.16 MB

Bug fixes: C1 (tripwire short-circuit), C3 (PII offsets), C5 (null-as-object), C6 (truncation overflow), C10 (regex validation)

Test plan

  • npx vitest run — 18 tests pass
  • pnpm run check — 0 type errors
  • pnpm run lint — 0 errors
  • pnpm run build — publint clean
  • pnpm test — SDK/CLI generate/stream integration

Summary by CodeRabbit

  • New Features

    • PII detection/redaction added to generation and streaming flows, with configurable actions (redact/abort/warn)
    • Response validation added: length, required/forbidden phrases, JSON-schema and custom validators with retry/truncation controls
    • Input validation: trimming, min/max length, and required-content checks
    • Safety tripwires to detect/handle unsafe responses
    • CLI flags to enable PII, input/output validation, and related options
  • Tests

    • New continuous safety test suite covering PII, validation, and tripwires

Copilot AI review requested due to automatic review settings April 14, 2026 19:36
@vercel

vercel Bot commented Apr 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
neurolink Ready Ready Preview, Comment Apr 28, 2026 6:53am

@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown

Walkthrough

Introduces request-level safety controls: input validation, PII detection/redaction, response validation, and tripwire evaluation; adds utility modules, types, CLI flags, integrates checks into generate/stream flows, and includes a comprehensive test suite. No public method signatures were removed.

Changes

Cohort / File(s) Summary
Type Definitions
src/lib/types/generate.ts, src/lib/types/stream.ts, src/lib/types/ioProcessor.ts, src/lib/types/index.ts
Add types for piiDetection, responseValidation, inputValidation, tripwires, and related results; expose ioProcessor types; mark legacy processors as deprecated.
Safety Utilities
src/lib/utils/piiDetector.ts, src/lib/utils/responseValidator.ts, src/lib/utils/tripwireEvaluator.ts
Add new modules: detectAndRedactPII, validateResponse, and TripwireEvaluator + built-in tripwires and helper constructors.
Core Integration
src/lib/neurolink.ts
Wire input validation and PII checks into prepare/validate flows for generate()/stream(), and run response validation after generation; enforce abort/redact/retry behaviors based on utility results.
Public API & Exports
src/lib/index.ts
Re-export new safety utilities and tripwire helpers to the public surface.
CLI
src/cli/factories/commandFactory.ts, src/cli/loop/optionsSchema.ts
Add CLI flags to configure PII detection/redaction and input/output validation; update CLI options schema typing to omit newly handled fields from primitive validation.
Processors docs
src/lib/processors/index.ts
Add documentation note that safety features moved into utils/* and are applied via options; no runtime changes.
Tests
test/continuous-test-suite-safety.ts
Add an executable test suite covering PII detection, response validation, tripwire semantics, and regressions (severity precedence, custom-pattern handling, JSON schema edge cases).

Sequence Diagram

sequenceDiagram
    actor Client
    participant CLI as CLI Handler
    participant InputVal as Input Validator
    participant PII as PII Detector
    participant Generator as Model/Generator
    participant RespVal as Response Validator
    participant Trip as Tripwire Evaluator

    Client->>CLI: invoke generate/stream with options
    CLI->>InputVal: apply inputValidation (trim/length/requireContent)
    InputVal-->>CLI: valid / invalid
    alt invalid
        CLI-->>Client: abort request
    else valid
        CLI->>PII: detectAndRedactPII(text, config)
        alt PII action == "abort"
            PII-->>CLI: abort signal
            CLI-->>Client: blocked
        else PII action == "redact"
            PII-->>CLI: redacted text
            CLI->>Generator: proceed with redacted input
        else
            PII-->>CLI: continue
            CLI->>Generator: proceed
        end
        Generator->>Generator: produce response (sync/stream)
        Generator-->>CLI: response text/stream
        CLI->>RespVal: validateResponse(text, config)
        alt RespVal action == "abort"
            RespVal-->>CLI: abort
            CLI-->>Client: error
        else RespVal action == "retry"
            RespVal-->>CLI: retry request
            CLI->>Generator: retry (with feedback if any)
        else RespVal action == "continue"
            RespVal-->>CLI: validated text
            CLI->>Trip: evaluate tripwires(data)
            alt Trip triggered with action == "abort"
                Trip-->>CLI: abort
                CLI-->>Client: blocked
            else Trip warn/log or none
                CLI-->>Client: return response
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

released

Poem

🐰 I hopped through text to find each trace,

Snipped and redacted with careful grace,
Tripwires watchful, validators bright,
Guardrails hum softly through day and night,
A rabbit's nod — safety done right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main objective: adding native PII detection and response validation to the generate/stream functions, which is the primary feature across the entire changeset.
Docstring Coverage ✅ Passed Docstring coverage is 89.60% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/io-processors

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

✅ Single Commit Policy - COMPLIANT

Status: Policy requirements met • 1 commit • Valid format • Ready for merge

📊 View validation details

📝 Commit Details

  • Hash: b3dec8705656ed3380f01d17319163be5d38c2ff
  • Message: feat(safety): add native PII detection and response validation to generate/stream
  • Author: Sachin Sharma

✅ Validation Results

  • Single commit requirement met
  • No merge commits in branch
  • Semantic commit message format verified
  • Ready for squash merge to release branch

🤖 Automated validation by NeuroLink Single Commit Enforcement

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@github-actions

github-actions Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Documentation Validation Results

⚠️ Documentation validation has issues

Check Status Result
Frontmatter Validation Passed
TypeScript Check Passed
Build Failed
Link Validation Skipped

🚧 Please fix the failing checks before merging.

Commit: 6589e312c769d8593817ccf8da7ec266f7b8faec | Workflow: View logs

Copilot AI 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.

Pull request overview

This PR introduces a new I/O processor middleware system for NeuroLink, including a registry + priority-ordered pipeline, built-in input/output processors, tripwire guards, presets, and SDK/CLI integration for configuring and running processor pipelines around generation/stream flows.

Changes:

  • Add core processor infrastructure (registry, pipeline, presets, tripwires, utilities, typed error helpers).
  • Add built-in input/output processors (validation, moderation/filtering, PII/toxicity, memory/context placeholders).
  • Integrate processors into the SDK (NeuroLink convenience methods + constructor/options types) and CLI command routing, with extensive unit/integration tests and fixtures.

Reviewed changes

Copilot reviewed 48 out of 48 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
test/unit/processors/registry.test.ts Unit coverage for processor registry behaviors (register/unregister/presets/build).
test/unit/processors/pipeline.test.ts Unit coverage for pipeline execution order, conditions, abort/retry, tracing.
test/unit/processors/output/toxicityCheck.test.ts Unit coverage for toxicity output processor config/actions.
test/unit/processors/input/piiDetection.test.ts Unit coverage for PII input processor detection/redaction/config validation.
test/processors/sdk-integration.test.ts SDK-level tests for NeuroLink processor convenience API (currently has type/import mismatches).
test/processors/integration.test.ts End-to-end integration tests for registry/pipeline/presets (currently has type/import mismatches).
test/processors/IOProcessorError.test.ts Tests for IOProcessorError factories/helpers and retryability checks.
test/fixtures/processors/pipeline-config.json Fixture samples for valid/invalid pipeline configurations and tripwires.
test/fixtures/processors/input-samples.json Fixture samples for input processor scenarios (PII/moderation/validation).
test/fixtures/processors/output-samples.json Fixture samples for output processor scenarios (length/toxicity/filtering).
src/lib/types/stream.ts Add per-request processors?: ProcessorPipelineConfig to StreamOptions.
src/lib/types/generate.ts Add per-request processors?: ProcessorPipelineConfig to GenerateOptions/TextGenerationOptions.
src/lib/types/config.ts Add processors config block to NeurolinkConstructorConfig.
src/lib/types/index.ts Re-export canonical I/O processor types via ./ioProcessor.js.
src/lib/processors/utils/validationUtils.ts Add JSON/config validation helper utilities (has a null-vs-object bug).
src/lib/processors/utils/processorFactory.ts Add processor factory/composition/condition/retry/timeout helpers.
src/lib/processors/utils/metadataUtils.ts Add metadata creation/merge/summary helpers for processor tracing/issues.
src/lib/processors/utils/index.ts Barrel export for processor utility helpers.
src/lib/processors/tripwire.ts Add tripwire evaluator + common built-in tripwires.
src/lib/processors/registry.ts Add I/O ProcessorRegistry + defaultRegistry instance.
src/lib/processors/processorHelpers.ts Add small runtime helper creators for metadata/result variants.
src/lib/processors/presets.ts Add built-in presets (default/security/strict/quality/minimal).
src/lib/processors/input/messageValidationProcessor.ts Add input validation processor (length/messages/system prompt/etc).
src/lib/processors/input/piiDetectionProcessor.ts Add PII detection/redaction/warn/abort input processor.
src/lib/processors/input/contentModerationProcessor.ts Add input moderation processor with heuristic + optional AI moderation.
src/lib/processors/input/memoryRetrievalProcessor.ts Add memory retrieval input processor (currently placeholder retrieval).
src/lib/processors/input/semanticContextProcessor.ts Add semantic context enrichment input processor (currently placeholder search).
src/lib/processors/input/index.ts Barrel exports for input processors.
src/lib/processors/output/responseValidationProcessor.ts Add response validation output processor (phrases/length/schema/custom validator).
src/lib/processors/output/lengthValidationProcessor.ts Add response length output processor (warn/truncate/retry/abort).
src/lib/processors/output/contentFilteringProcessor.ts Add content filtering output processor (word/pattern redact/retry/abort).
src/lib/processors/output/toxicityCheckProcessor.ts Add toxicity output processor (heuristic categories/actions).
src/lib/processors/output/memoryPersistenceProcessor.ts Add memory persistence output processor (currently no-op but reports success).
src/lib/processors/output/index.ts Barrel exports for output processors.
src/lib/processors/errors/IOProcessorError.ts Add typed IOProcessor error codes + helper constructors.
src/lib/processors/index.ts Export I/O processor system and auto-register built-ins into defaultRegistry.
src/lib/neurolink.ts Add processor registry/pipeline methods + constructor wiring (imports processor barrel via side-effect).
src/lib/index.ts Re-export processor API from main package entry.
src/cli/parser.ts Register new processors CLI command group.
src/cli/loop/optionsSchema.ts Exclude processors from simple CLI options schema (SDK-only).
docs/tutorials.md Tutorial updates (currently has missing frontmatter + encoding/snippet issues).
FEATURE-STATUS.md Add feature status doc for I/O processors.
Comments suppressed due to low confidence (1)

docs/tutorials.md:131

  • The section header renders as ## �🌐 Web App Integration (replacement character), which indicates a file encoding/copy-paste issue. Also, the Express example reads req.body.prompt but does not call app.use(express.json()), so req.body will be undefined for JSON requests. Fix the header text and add the JSON body middleware in the snippet.
## �🌐 Web App Integration

### Express.js API

```javascript
const express = require("express");
const { NeuroLink } = require("@juspay/neurolink");
const app = express();
const neurolink = new NeuroLink();

app.post("/api/generate", async (req, res) => {
  const result = await neurolink.generate({
    input: { text: req.body.prompt },
    enableAnalytics: true,

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/tutorials.md
Comment on lines 1 to 3
# 📚 Step-by-Step Integration Tutorials

## 🚀 Quick Start (15 minutes) {#quick-start-15-minutes}

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

This docs page appears to have lost its YAML frontmatter (e.g., ---\ntitle: ...\nslug: ...\n---) that other docs pages in this repo use for navigation/metadata. Please restore the frontmatter at the top so the docs site can index and route this page correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +81
// Type validation
if (schema.type) {
const expectedType = schema.type as string;
const actualType = Array.isArray(data) ? "array" : typeof data;

if (expectedType === "object" && actualType !== "object") {
errors.push(`Expected type 'object', got '${actualType}'`);
} else if (expectedType === "array" && !Array.isArray(data)) {
errors.push(`Expected type 'array', got '${actualType}'`);
} else if (expectedType === "string" && typeof data !== "string") {
errors.push(`Expected type 'string', got '${actualType}'`);
} else if (expectedType === "number" && typeof data !== "number") {
errors.push(`Expected type 'number', got '${actualType}'`);
} else if (expectedType === "boolean" && typeof data !== "boolean") {
errors.push(`Expected type 'boolean', got '${actualType}'`);
} else if (expectedType === "null" && data !== null) {
errors.push(`Expected type 'null', got '${actualType}'`);
}

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

validateJsonSchema treats null as type object (typeof null === "object"), so a schema with { type: "object" } will incorrectly accept null. Add an explicit data === null check when validating object type (and/or reuse isJsonObject) so null fails the object type check.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +114
try {
// Build the conversation turn to store
const turn: ConversationTurn = {
userMessage: data.input.text,
assistantMessage: data.responseText,
toolCalls: includeToolCalls ? data.toolCalls : undefined,
timestamp: Date.now(),
metadata: {
...customMetadata,
provider: data.metadata.provider,
model: data.metadata.model,
requestId: data.metadata.requestId,
},
};

// Store in appropriate memory store
await storeConversationTurn(effectiveSessionId, turn, storeType, {
maxMessages,
ttlSeconds,
enableSummarization,
});

return {
action: "continue",
data,
metadata: {
memoryPersisted: true,
storeType,
sessionId: effectiveSessionId,
includeToolCalls,
},
};

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

storeConversationTurn() is currently a no-op placeholder, but on the success path the processor sets memoryPersisted: true. This is misleading for callers (it reports persistence even though nothing is stored). Either implement real persistence using the existing memory managers, or mark the operation as skipped/not-implemented (and set memoryPersisted: false with a reason) until it’s wired up.

Copilot uses AI. Check for mistakes.
Comment thread src/lib/neurolink.ts Outdated
Comment on lines +219 to +225
import { ProcessorPipeline } from "./processors/pipeline.js";
import {
defaultRegistry as processorDefaultRegistry,
ProcessorRegistry,
} from "./processors/registry.js";
// Side-effect import: registers built-in processors and presets into defaultRegistry
import "./processors/index.js";

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

This side-effect import of ./processors/index.js will execute the processors barrel, which re-exports CLI/file-processor helpers and can pull Node-only/CLI dependencies into any environment that imports NeuroLink (including browser bundles). To avoid bundling/runtime issues, move the default-registry initialization into a dedicated, dependency-light module (e.g. processors/initializeDefaultRegistry) and import/call only that here.

Copilot uses AI. Check for mistakes.
Comment thread src/lib/neurolink.ts Outdated
Comment on lines +12674 to +12708
areProcessorsEnabled(): boolean {
return this.sdkProcessorsEnabled;
}

setProcessorsEnabled(enabled: boolean): void {
this.sdkProcessorsEnabled = enabled;
}

async processInput(
data: InputProcessorData,
metadataOverrides?: Partial<ProcessorMetadata>,
): Promise<
ProcessorPipelineResult<InputProcessorData> & { processorsExecuted: number }
> {
if (!this.sdkProcessorPipeline) {
return {
action: "continue",
data,
feedback: [],
issues: [],
metadata: {
requestId: `neurolink-${Date.now()}`,
timestamp: Date.now(),
custom: {},
issues: [],
processorTrace: [],
...metadataOverrides,
},
totalTime: 0,
processorsExecuted: 0,
};
}
const result = await this.sdkProcessorPipeline.processInput(
data,
metadataOverrides,

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

setProcessorsEnabled(false) only toggles sdkProcessorsEnabled, but processInput/processOutput don’t check that flag—if a pipeline exists they will still execute processors. Add an early return when processors are disabled (similar to the !this.sdkProcessorPipeline branch) so enabling/disabling works as advertised.

Copilot uses AI. Check for mistakes.
Comment thread test/processors/sdk-integration.test.ts Outdated
Comment on lines +7 to +13
import type {
InputProcessor,
OutputProcessor,
InputProcessorData,
OutputProcessorData,
ProcessorMetadata,
} from "../../src/lib/processors/types.js";

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

../../src/lib/processors/types.js is imported here, but that module does not exist in the codebase (there is no src/lib/processors/types.ts/js). This will break the test build; update the import to the canonical processor types (e.g. src/lib/types/ioProcessor.js / src/lib/types/index.js) or add the intended compatibility re-export module.

Copilot uses AI. Check for mistakes.
Comment thread test/processors/integration.test.ts Outdated
Comment on lines +15 to +22
import type {
InputProcessor,
OutputProcessor,
InputProcessorData,
OutputProcessorData,
ProcessorMetadata,
ProcessorResult,
} from "../../src/lib/processors/types.js";

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

This test imports types from ../../src/lib/processors/types.js, which does not exist, and then constructs InputProcessorData/OutputProcessorData objects with only text/responseText. The canonical InputProcessorData/OutputProcessorData types require fields like options, messages, and metadata, so this file will not typecheck once the import is corrected. Update the imports and build valid processor data objects (or provide a helper that creates the required defaults).

Copilot uses AI. Check for mistakes.
Comment thread test/processors/sdk-integration.test.ts Outdated
Comment on lines +104 to +112
const customProcessor: InputProcessor = {
id: "custom-input",
name: "Custom Input",
description: "A custom input processor",
priority: 50,
async process(data, metadata) {
return { action: "continue", data, metadata };
},
};

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

These custom processor definitions use a (data, metadata, config) style signature and return { metadata } as if it were full pipeline metadata. The current InputProcessor/OutputProcessor API used by ProcessorPipeline calls process(data, config) and treats result.metadata as custom metadata fields to merge into metadata.custom (not a full metadata object). Adjust the test processors to match the actual signature/return shape so the tests reflect real behavior.

Copilot uses AI. Check for mistakes.
@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/processors/output/toxicityCheckProcessor.ts (1)

383-418: ⚠️ Potential issue | 🟠 Major

Honor categories in the heuristic fallback.

performHeuristicModeration() ignores the requested category list and always scores violence/harassment. When AI moderation is disabled or falls back, callers can still be warned/blocked for categories they explicitly opted out of.

🛠️ Proposed fix
-async function performHeuristicModeration(
-  text: string,
-  _categories: ModerationCategory[],
-): Promise<AIModerationResult> {
+async function performHeuristicModeration(
+  text: string,
+  categories: ModerationCategory[],
+): Promise<AIModerationResult> {
   const lowerText = text.toLowerCase();
   let score = 0;
   const flaggedCategories: string[] = [];
   const categoryScores: Record<string, number> = {};

   // Simple heuristics (these serve as a fallback when AI moderation fails)
   const violenceIndicators = ["kill", "murder", "attack", "destroy", "hurt"];
   const harassmentIndicators = ["stupid", "idiot", "dumb", "hate you"];

-  const violenceMatches = violenceIndicators.filter((w) =>
-    lowerText.includes(w),
-  );
-  if (violenceMatches.length > 0) {
+  if (categories.includes("violence")) {
+    const violenceMatches = violenceIndicators.filter((w) =>
+      lowerText.includes(w),
+    );
+    if (violenceMatches.length > 0) {
       const violenceScore = Math.min(0.3 * violenceMatches.length, 1.0);
       score = Math.max(score, violenceScore);
       categoryScores["violence"] = violenceScore;
       if (violenceScore > 0.5) {
         flaggedCategories.push("violence");
       }
     }
   }

-  const harassmentMatches = harassmentIndicators.filter((w) =>
-    lowerText.includes(w),
-  );
-  if (harassmentMatches.length > 0) {
+  if (categories.includes("harassment")) {
+    const harassmentMatches = harassmentIndicators.filter((w) =>
+      lowerText.includes(w),
+    );
+    if (harassmentMatches.length > 0) {
       const harassmentScore = Math.min(0.2 * harassmentMatches.length, 1.0);
       score = Math.max(score, harassmentScore);
       categoryScores["harassment"] = harassmentScore;
       if (harassmentScore > 0.5) {
         flaggedCategories.push("harassment");
       }
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/output/toxicityCheckProcessor.ts` around lines 383 - 418,
performHeuristicModeration currently always evaluates violence/harassment
heuristics regardless of what the caller requested; update
performHeuristicModeration to honor its categories parameter by only computing
and returning scores for categories that are present in the provided categories
array (and preserve existing default behavior if categories is undefined/null).
Specifically, inside performHeuristicModeration, gate each heuristic block
(e.g., violence/harassment scoring logic and any other category-specific checks)
behind checks like categories?.includes("<categoryName>") before running that
category's logic, and ensure the returned result only contains keys for
evaluated categories so callers who opted out won’t receive or be blocked by
those scores. Ensure any callers expecting the old shape still handle missing
category keys or keep a backward-compatible empty-score behavior when necessary.
🟠 Major comments (31)
docs/tutorials.md-122-127 (1)

122-127: ⚠️ Potential issue | 🟠 Major

Missing express.json() middleware breaks the example.

The Express.js example accesses req.body.prompt (line 130) but lacks app.use(express.json()) to parse incoming JSON. Users copying this snippet will get undefined for req.body.

Proposed fix
 const express = require("express");
 const { NeuroLink } = require("@juspay/neurolink");
 const app = express();
+app.use(express.json());
 const neurolink = new NeuroLink();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/tutorials.md` around lines 122 - 127, The example is missing the Express
JSON body-parsing middleware so req.body.prompt will be undefined; add the
middleware by calling app.use(express.json()) after creating the app (after
const app = express()) so incoming JSON is parsed before handlers that access
req.body.prompt and any routes that use the neurolink instance.
test/unit/processors/output/toxicityCheck.test.ts-9-14 (1)

9-14: ⚠️ Potential issue | 🟠 Major

Switch internal type imports to the canonical src/lib/types barrel.

Direct import from ioProcessor.js in a non-src/lib/types file violates the project’s type import rule.

🔧 Proposed fix
 import type {
   InputProcessorData,
   OutputProcessorData,
   ProcessorMetadata,
   ToxicityCheckConfig,
-} from "../../../../src/lib/types/ioProcessor.js";
+} from "../../../../src/lib/types/index.js";

As per coding guidelines: “Code outside src/lib/types/ must import internal types from the barrel (../types/index.js or ../types), never from specific type files.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/processors/output/toxicityCheck.test.ts` around lines 9 - 14, The
test file imports internal types directly from ioProcessor.js; update the import
to use the canonical barrel export instead. Replace the direct import of
InputProcessorData, OutputProcessorData, ProcessorMetadata, ToxicityCheckConfig
from "../../../../src/lib/types/ioProcessor.js" with an import from the types
barrel (e.g., "../../../../src/lib/types" or
"../../../../src/lib/types/index.js") so the test references the same exported
types via the central barrel.
test/unit/processors/input/piiDetection.test.ts-9-14 (1)

9-14: ⚠️ Potential issue | 🟠 Major

Use the canonical type barrel import instead of direct ioProcessor type-file import.

This test imports internal types from a specific file path, which breaks the repository’s type-import boundary rule for non-src/lib/types files.

🔧 Proposed fix
 import type {
   InputProcessorData,
   PIIDetectionConfig,
   ProcessorMetadata,
-} from "../../../../src/lib/types/ioProcessor.js";
+} from "../../../../src/lib/types/index.js";

As per coding guidelines: “Code outside src/lib/types/ must import internal types from the barrel (../types/index.js or ../types), never from specific type files.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/processors/input/piiDetection.test.ts` around lines 9 - 14, The
test imports internal types InputProcessorData, PIIDetectionConfig, and
ProcessorMetadata directly from the ioProcessor type file; change the import to
use the canonical types barrel (the src/lib/types index export) instead of the
specific ioProcessor.js file so the test follows the repo's type-import boundary
rule and still imports InputProcessorData, PIIDetectionConfig, and
ProcessorMetadata from the barrel.
test/processors/integration.test.ts-15-23 (1)

15-23: ⚠️ Potential issue | 🟠 Major

Type imports should come from src/lib/types barrel, not src/lib/processors/types.js.

This path bypasses the canonical type boundary and should be switched to the central types barrel import.

🔧 Proposed fix
 import type {
   InputProcessor,
   OutputProcessor,
   InputProcessorData,
   OutputProcessorData,
   ProcessorMetadata,
   ProcessorResult,
-} from "../../src/lib/processors/types.js";
+} from "../../src/lib/types/index.js";

As per coding guidelines: “Code outside src/lib/types/ must import internal types from the barrel (../types/index.js or ../types), never from specific type files.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/processors/integration.test.ts` around lines 15 - 23, The test is
importing internal types directly from "../../src/lib/processors/types.js" which
bypasses the canonical types barrel; update the import to come from the central
barrel (e.g., "../../src/lib/types" or "../../src/lib/types/index.js") and
import the same symbols InputProcessor, OutputProcessor, InputProcessorData,
OutputProcessorData, ProcessorMetadata, ProcessorResult so the test uses the
approved canonical type boundary.
src/lib/processors/input/piiDetectionProcessor.ts-146-154 (1)

146-154: ⚠️ Potential issue | 🟠 Major

Track matches per source field before redacting.

detectedPII positions are computed against the concatenated textToScan, then reused against data.text and every individual message. Those offsets do not map back to the original strings, so multi-message inputs will miss redactions and can also replace unrelated slices in earlier fields.

Please either detect/redact per field, or store the source plus field-local offsets when building detectedPII.

Also applies to: 226-249

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/input/piiDetectionProcessor.ts` around lines 146 - 154,
The current PII detection builds a single concatenated text (textToScan) and
populates detectedPII offsets against that concatenation, then applies those
offsets back to data.text and each message, which misaligns offsets across
fields; update the logic in piiDetectionProcessor (references: detectedPII,
textToScan, data.text, data.messages) to either (a) run detection separately for
each source field—compute and redact PII per field using field-local offsets—or
(b) when detecting over the concatenated text, record the source identifier and
the field-local start/end for each DetectedPII (e.g., include a
sourceField/messageIndex and local offsets) and use those to apply redactions
back to the correct field; apply the same fix to the later block around the
other occurrence (lines ~226-249) so offsets always map to their original source
before replacing.
src/lib/processors/presets.ts-261-275 (1)

261-275: ⚠️ Potential issue | 🟠 Major

Avoid exposing live mutable preset references.

builtInPresets and getPreset() currently hand out the shared preset objects. Any consumer that tweaks the returned config mutates the global defaults for every later pipeline/test in the same process.

Please return cloned presets and avoid exporting a directly mutable preset array.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/presets.ts` around lines 261 - 275, The builtInPresets
array currently exposes live mutable objects (builtInPresets, defaultPreset,
securityPreset, strictPreset, qualityPreset, minimalPreset) and getPreset(name)
returns references that callers can mutate; stop exporting the raw array and
return cloned presets instead: make the internal array non-exported (or freeze
it) and change getPreset to locate the preset by name from the internal list but
return a deep clone (use structuredClone or your existing deepClone utility) of
the ProcessorPreset before returning; similarly, if you must export a list,
expose a function like listPresets() that returns clones rather than exporting
builtInPresets directly.
src/lib/processors/output/memoryPersistenceProcessor.ts-205-248 (1)

205-248: ⚠️ Potential issue | 🟠 Major

Do not report successful persistence while the store is still a no-op.

storeConversationTurn() currently does nothing, but the caller treats it as a successful write and returns memoryPersisted: true. That means this built-in processor silently drops conversation history while telling consumers it was saved.

Until this is wired to the real memory managers, it should surface skipped/not implemented semantics instead of success.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/output/memoryPersistenceProcessor.ts` around lines 205 -
248, storeConversationTurn currently no-ops but returns success to callers;
change it to surface "not implemented/skipped" semantics by rejecting the
promise with a clear error instead of resolving. Modify the body of
storeConversationTurn to return Promise.reject(new Error('Memory persistence not
implemented: in-memory/redis managers not wired')) (or throw the same Error) so
callers that await storeConversationTurn (e.g., code that sets memoryPersisted)
will see a failure and can mark the operation as skipped; keep the function name
storeConversationTurn and preserve the existing signature.
test/unit/processors/registry.test.ts-9-13 (1)

9-13: 🛠️ Refactor suggestion | 🟠 Major

Import internal processor types from the barrel.

This test pulls internal types from src/lib/types/ioProcessor.js instead of the canonical barrel. Please switch this to src/lib/types/index.js so the test stays aligned with the repo’s type-import boundary.

Proposed change
 import type {
   InputProcessor,
   OutputProcessor,
   ProcessorPreset,
-} from "../../../src/lib/types/ioProcessor.js";
+} from "../../../src/lib/types/index.js";

As per coding guidelines, !(src/lib/types)/**/*.ts: Code outside src/lib/types/ must import internal types from the barrel (../types/index.js or ../types), never from specific type files (../types/rag.js, ../types/mcp.js).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/processors/registry.test.ts` around lines 9 - 13, The test imports
internal types InputProcessor, OutputProcessor, and ProcessorPreset directly
from ioProcessor.js; update the import to use the types barrel (the exported
index for src/lib/types) instead of the specific file so the test follows the
repo's type-import boundary — replace the module specifier for the existing
import of InputProcessor, OutputProcessor, ProcessorPreset to import them from
the types barrel (the index export of src/lib/types) while keeping the same
named type imports.
test/unit/processors/tripwire.test.ts-17-22 (1)

17-22: 🛠️ Refactor suggestion | 🟠 Major

Import internal tripwire types from the barrel.

This test reaches into src/lib/types/ioProcessor.js directly. Please switch it to the canonical src/lib/types/index.js barrel to stay within the repo’s internal type-import contract.

Proposed change
 import type {
   InputProcessorData,
   OutputProcessorData,
   ProcessorMetadata,
   TripwireConfig,
-} from "../../../src/lib/types/ioProcessor.js";
+} from "../../../src/lib/types/index.js";

As per coding guidelines, !(src/lib/types)/**/*.ts: Code outside src/lib/types/ must import internal types from the barrel (../types/index.js or ../types), never from specific type files (../types/rag.js, ../types/mcp.js).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/processors/tripwire.test.ts` around lines 17 - 22, The test imports
internal types (InputProcessorData, OutputProcessorData, ProcessorMetadata,
TripwireConfig) directly from a specific type module; change the import to use
the repository's types barrel (the types index/barrel) instead of the specific
ioProcessor module so the test imports those symbols from the canonical barrel
export; update the import statement in tripwire.test.ts to reference the central
types barrel export and keep the same type names.
src/lib/processors/input/piiDetectionProcessor.ts-181-186 (1)

181-186: ⚠️ Potential issue | 🟠 Major

Validate custom regex patterns for type and safety before execution.

customPatterns are executed directly without runtime validation. Although the type definition declares pattern: RegExp, TypeScript doesn't enforce this at runtime. Non-RegExp values will cause detectPIIWithPattern to fail when accessing .source and .flags. Additionally, unchecked regex patterns can exhibit catastrophic backtracking on large prompts.

Add explicit type validation and reject patterns with nested quantifiers. The codebase already implements similar checks in customScorerUtils.ts and autoresearch/config.ts—use the same pattern: (/(\+|\*|\{)\S*(\+|\*|\{)/) to detect nested quantifiers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/input/piiDetectionProcessor.ts` around lines 181 - 186,
Validate each custom pattern before calling detectPIIWithPattern: in the loop
over customPatterns (the block creating `detected =
detectPIIWithPattern(textToScan, custom.name, custom.pattern)`), first confirm
`custom.pattern` is an actual RegExp (e.g., instanceof RegExp) and if not,
skip/log an error; then test the pattern's source for nested quantifiers using
the same check used elsewhere (/(\+|\*|\{)\S*(\+|\*|\{)/) and skip/log/reject
any pattern that matches to avoid catastrophic backtracking; only pass safe,
validated RegExp objects into detectPIIWithPattern.
src/lib/processors/output/lengthValidationProcessor.ts-82-90 (1)

82-90: ⚠️ Potential issue | 🟠 Major

Guard retryCount reads when metadata is absent.

Both retry branches dereference data.metadata.custom.retryCount, but this processor is publicly callable with bare OutputProcessorData and the example in this file does exactly that. With action: "retry", this throws before returning a processor result.

🛠️ Proposed fix
-          const currentRetry = (data.metadata.custom.retryCount as number) || 0;
+          const currentRetry = Number(data.metadata?.custom?.retryCount ?? 0);
...
-          const currentRetry = (data.metadata.custom.retryCount as number) || 0;
+          const currentRetry = Number(data.metadata?.custom?.retryCount ?? 0);

Also applies to: 146-154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/output/lengthValidationProcessor.ts` around lines 82 - 90,
The code dereferences data.metadata.custom.retryCount in the retry branches
(variables: action, data, metadata, custom, retryCount, maxRetries) which will
throw when metadata is absent; update the length validation processor (e.g.,
lengthValidationProcessor/handler) to safely read retryCount using optional
chaining and a numeric fallback (e.g., const currentRetry =
Number(data?.metadata?.custom?.retryCount) || 0) and do the same in the other
retry branch (the one around lines 146-154) so both branches never throw when
metadata or custom is undefined and still increment retryCount properly.
src/lib/processors/output/lengthValidationProcessor.ts-122-125 (1)

122-125: ⚠️ Potential issue | 🟠 Major

Clamp truncation so the result never exceeds maxLength.

If maxLength is smaller than truncationSuffix.length, the current slice() call gets a negative end index and can return a string longer than the configured maximum.

✂️ Proposed fix
-          const truncatedText =
-            responseText.slice(0, maxLength - truncationSuffix.length) +
-            truncationSuffix;
+          const suffix = truncationSuffix.slice(0, maxLength);
+          const keepLength = Math.max(maxLength - suffix.length, 0);
+          const truncatedText = responseText.slice(0, keepLength) + suffix;
...
-          const truncatedText =
-            responseText.slice(0, maxLength - truncationSuffix.length) +
-            truncationSuffix;
+          const suffix = truncationSuffix.slice(0, maxLength);
+          const keepLength = Math.max(maxLength - suffix.length, 0);
+          const truncatedText = responseText.slice(0, keepLength) + suffix;

Also applies to: 158-160

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/output/lengthValidationProcessor.ts` around lines 122 -
125, The truncate branch can produce output longer than maxLength when
truncationSuffix.length > maxLength; in the action === "truncate" block compute
a clamped slice length (e.g., clampLength = Math.max(0, maxLength -
truncationSuffix.length)) and use responseText.slice(0, clampLength) +
truncationSuffix so the final string never exceeds maxLength; apply the same
clamp fix to the other truncate occurrence in this file (the second
truncatedText computation).
src/lib/processors/input/contentModerationProcessor.ts-132-138 (1)

132-138: ⚠️ Potential issue | 🟠 Major

Handle simplified input without messages.

data.messages.filter(...) assumes the array is always present. Other processors in this PR already treat messages as optional, so text-only calls can throw here before moderation starts.

🛠️ Proposed fix
-      const messageTexts = data.messages
+      const messageTexts = (data.messages ?? [])
         .filter((m) => m.role === "user")
         .map((m) => m.content)
         .join("\n");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/input/contentModerationProcessor.ts` around lines 132 -
138, The code assumes data.messages exists and calls data.messages.filter(...)
which throws for text-only inputs; update the logic that builds textToScan so it
safely handles missing or non-array messages (e.g., treat messages as an empty
array when absent). Modify the section that defines messageTexts and textToScan
(the symbols messageTexts and textToScan in contentModerationProcessor) to use a
guarded/messages fallback (or Array.isArray check) before .filter/.map/.join so
text-only calls proceed without error.
src/lib/processors/output/toxicityCheckProcessor.ts-169-178 (1)

169-178: ⚠️ Potential issue | 🟠 Major

Guard retryCount access in the retry path.

data.metadata.custom.retryCount will throw whenever upstream metadata does not initialize custom, so action: "retry" is not actually safe for direct processor use.

🛠️ Proposed fix
-          const currentRetry = (data.metadata.custom.retryCount as number) || 0;
+          const currentRetry = Number(data.metadata?.custom?.retryCount ?? 0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/output/toxicityCheckProcessor.ts` around lines 169 - 178,
The retry path reads data.metadata.custom.retryCount directly which can throw if
metadata.custom is undefined; update the logic in the retry branch (where action
=== "retry" and currentRetry is computed) to safely access retryCount using
optional chaining or a guarded lookup (e.g., const currentRetry =
((data.metadata && data.metadata.custom && data.metadata.custom.retryCount) as
number) || 0 or initialize data.metadata.custom before reading), and ensure
retryCount, retryCount + 1, and maxRetries are returned consistently so
processors like toxicityCheckProcessor's retry branch never access
data.metadata.custom without first checking/creating it.
src/lib/processors/input/contentModerationProcessor.ts-383-418 (1)

383-418: ⚠️ Potential issue | 🟠 Major

Honor categories in the heuristic fallback.

performHeuristicModeration() ignores the requested category list and always scores violence/harassment. If AI moderation is disabled or unavailable, this can block content for categories the caller did not enable.

🛠️ Proposed fix
-async function performHeuristicModeration(
-  text: string,
-  _categories: ModerationCategory[],
-): Promise<AIModerationResult> {
+async function performHeuristicModeration(
+  text: string,
+  categories: ModerationCategory[],
+): Promise<AIModerationResult> {
   const lowerText = text.toLowerCase();
   let score = 0;
   const flaggedCategories: string[] = [];
   const categoryScores: Record<string, number> = {};

   // Simple heuristics (these serve as a fallback when AI moderation fails)
   const violenceIndicators = ["kill", "murder", "attack", "destroy", "hurt"];
   const harassmentIndicators = ["stupid", "idiot", "dumb", "hate you"];

-  const violenceMatches = violenceIndicators.filter((w) =>
-    lowerText.includes(w),
-  );
-  if (violenceMatches.length > 0) {
+  if (categories.includes("violence")) {
+    const violenceMatches = violenceIndicators.filter((w) =>
+      lowerText.includes(w),
+    );
+    if (violenceMatches.length > 0) {
       const violenceScore = Math.min(0.3 * violenceMatches.length, 1.0);
       score = Math.max(score, violenceScore);
       categoryScores["violence"] = violenceScore;
       if (violenceScore > 0.5) {
         flaggedCategories.push("violence");
       }
     }
   }

-  const harassmentMatches = harassmentIndicators.filter((w) =>
-    lowerText.includes(w),
-  );
-  if (harassmentMatches.length > 0) {
+  if (categories.includes("harassment")) {
+    const harassmentMatches = harassmentIndicators.filter((w) =>
+      lowerText.includes(w),
+    );
+    if (harassmentMatches.length > 0) {
       const harassmentScore = Math.min(0.2 * harassmentMatches.length, 1.0);
       score = Math.max(score, harassmentScore);
       categoryScores["harassment"] = harassmentScore;
       if (harassmentScore > 0.5) {
         flaggedCategories.push("harassment");
       }
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/input/contentModerationProcessor.ts` around lines 383 -
418, performHeuristicModeration currently ignores the _categories parameter and
always evaluates violence/harassment; update the function to respect the
requested categories by only running the violenceIndicators block when
_categories contains the "violence" category and only running the
harassmentIndicators block when _categories contains the "harassment" category
(use a case-insensitive check or match against the ModerationCategory enum
values), and only add entries to categoryScores and flaggedCategories for those
enabled categories so the heuristic fallback only flags categories the caller
requested.
src/lib/processors/output/responseValidationProcessor.ts-125-137 (1)

125-137: ⚠️ Potential issue | 🟠 Major

Retry mode assumes metadata.custom always exists.

data.metadata.custom.retryCount will throw when this processor is used directly with bare OutputProcessorData or when upstream metadata omits custom. That makes retryOnFailure unusable in those cases.

🛠️ Proposed fix
-        const currentRetry = (data.metadata.custom.retryCount as number) || 0;
+        const currentRetry = Number(data.metadata?.custom?.retryCount ?? 0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/output/responseValidationProcessor.ts` around lines 125 -
137, The code assumes data.metadata.custom always exists and will throw when
metadata or custom is undefined; change the access to use safe checks/optional
chaining and defaults (e.g., const currentRetry = ((data.metadata?.custom as
any)?.retryCount as number) || 0) and when returning the retry action ensure you
do not rely on missing objects (either include retryCount/maxRetries in the
returned object only or create/clone metadata.custom before mutating), and make
the same defensive checks around any other metadata.custom accesses in the
responseValidationProcessor (reference symbols: data.metadata.custom.retryCount,
retryOnFailure, maxRetries, OutputProcessorData).
src/lib/processors/input/messageValidationProcessor.ts-74-95 (1)

74-95: ⚠️ Potential issue | 🟠 Major

requireContent rejects message-only input.

This branch only inspects workingData.text. Calls that supply chat messages without a top-level text field will abort as empty even when user messages contain content.

🛠️ Proposed fix
       if (requireContent) {
-        const trimmedText =
-          typeof workingData.text === "string" ? workingData.text.trim() : "";
-        if (trimmedText.length === 0) {
+        const hasText =
+          typeof workingData.text === "string" &&
+          workingData.text.trim().length > 0;
+        const hasMessageContent = messages.some((message) =>
+          typeof message.content === "string"
+            ? message.content.trim().length > 0
+            : Array.isArray(message.content) && message.content.length > 0,
+        );
+        if (!hasText && !hasMessageContent) {
           return {
             action: "abort",
             feedback: "Message content is empty",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/input/messageValidationProcessor.ts` around lines 74 - 95,
The validation branch using requireContent currently only checks
workingData.text and wrongly aborts when top-level text is absent but chat-style
workingData.messages contains user content; update the requireContent check (in
messageValidationProcessor) to first trim and inspect workingData.text as before
but if empty/absent also scan workingData.messages (an array) for any message
objects with non-empty content (e.g., message.content, message.text, or nested
content fields) and treat that as satisfying content; only return the same abort
object when both the top-level text and all messages are empty or missing.
src/lib/processors/utils/validationUtils.ts-58-155 (1)

58-155: ⚠️ Potential issue | 🟠 Major

validateJsonSchema() accepts invalid structured payloads.

This validator treats null as an "object" and never descends into properties or items, so schemas like { type: "object", properties: { ... } } can pass responses that do not actually match the supplied schema. responseValidationProcessor will then report false positives for structured-output checks.

🛠️ Proposed direction
-    const actualType = Array.isArray(data) ? "array" : typeof data;
+    const actualType =
+      data === null ? "null" : Array.isArray(data) ? "array" : typeof data;
...
+  if (schema.properties && isJsonObject(data)) {
+    for (const [key, propertySchema] of Object.entries(schema.properties)) {
+      errors.push(
+        ...validateJsonSchema(
+          data[key],
+          propertySchema as JsonObject,
+        ).map((error) => `${key}: ${error}`),
+      );
+    }
+  }
+
+  if (schema.items && Array.isArray(data)) {
+    data.forEach((item, index) => {
+      errors.push(
+        ...validateJsonSchema(item, schema.items as JsonObject).map(
+          (error) => `[${index}]: ${error}`,
+        ),
+      );
+    });
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/utils/validationUtils.ts` around lines 58 - 155,
validateJsonSchema incorrectly treats null as an object and doesn't validate
nested schemas (properties/items), allowing structured payloads to slip through;
update validateJsonSchema to (1) treat null as its own type (so if schema.type
=== "object" require isJsonObject(data) && data !== null), (2) when schema.type
is "object" or schema.properties is present, descend into schema.properties and
validate each owned property recursively using validateJsonSchema for property
values (use isJsonObject to guard), and (3) when schema.type is "array" or
schema.items is present, validate each element against schema.items recursively;
ensure enum/min/max/length checks still apply at the appropriate primitive
levels and that you reference the existing function name validateJsonSchema, the
schema keys properties and items, and helper isJsonObject when adding these
checks.
src/lib/neurolink.ts-12674-12744 (1)

12674-12744: ⚠️ Potential issue | 🟠 Major

setProcessorsEnabled(false) currently doesn't disable processing.

processInput() and processOutput() only check whether a pipeline exists. Once a pipeline has been created, these methods keep executing processors even after the public toggle is set to false, so the enable/disable API is ineffective.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 12674 - 12744, processInput and
processOutput only check for sdkProcessorPipeline and ignore the
sdkProcessorsEnabled flag, so calling setProcessorsEnabled(false) doesn't stop
processing; update both processInput and processOutput to short-circuit when
this.sdkProcessorsEnabled is false (return the same default
ProcessorPipelineResult shape with processorsExecuted: 0 and preserved
metadataOverrides) before checking/using this.sdkProcessorPipeline, referencing
the methods areProcessorsEnabled, setProcessorsEnabled, processInput,
processOutput and the fields sdkProcessorsEnabled and sdkProcessorPipeline to
locate the logic to change.
src/lib/neurolink.ts-1112-1120 (1)

1112-1120: ⚠️ Potential issue | 🟠 Major

Fail fast on unknown presets in the constructor.

If buildFromPreset() returns undefined, this path still enables processors and creates an empty pipeline. A typo in a safety preset would silently disable the intended guards. Please validate the preset here the same way setProcessorPreset() does.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 1112 - 1120, The constructor currently
calls processorDefaultRegistry.buildFromPreset(config.processors.preset) and
always assigns this.sdkProcessorPipeline = new ProcessorPipeline(...), which
allows unknown presets to silently produce an empty pipeline; replicate the
validation used in setProcessorPreset(): check the return value of
processorDefaultRegistry.buildFromPreset(config.processors.preset) and if it is
undefined or falsy, throw a descriptive error (or call the same validation
helper used by setProcessorPreset()) instead of creating a ProcessorPipeline,
otherwise proceed to construct this.sdkProcessorPipeline with
built.inputProcessors and built.outputProcessors.
src/lib/neurolink.ts-1108-1125 (1)

1108-1125: ⚠️ Potential issue | 🟠 Major

Processor config is not applied to normal generate() / stream() calls.

This class stores the pipeline and exposes manual processInput() / processOutput() helpers, but the request lifecycle never invokes them. As written, config.processors.enabled / preset has no effect on normal SDK generation or streaming, which makes the new constructor config misleading. Based on learnings, src/lib/neurolink.ts is the main SDK entry point that orchestrates all components.

Also applies to: 12682-12744

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 1108 - 1125, The configured processor
pipeline isn't being applied because generate() and stream() never invoke the
stored pipeline; update the request lifecycle in the Neurolink class so that
when sdkProcessorsEnabled is true you run
this.sdkProcessorPipeline.processInput(...) (or the existing processInput
helper) to transform inputs before sending requests and run
this.sdkProcessorPipeline.processOutput(...) (or processOutput helper) on
responses/each streamed chunk before returning/streaming to callers;
specifically, modify the generate and stream methods to branch on
this.sdkProcessorsEnabled / this.sdkProcessorPipeline and call the pipeline
before the network call and after receiving the result (including streaming
chunks), preserving existing error handling and keeping behavior when no
processors are configured.
src/lib/neurolink.ts-12619-12621 (1)

12619-12621: ⚠️ Potential issue | 🟠 Major

Avoid mutating the global registry from instance methods.

getProcessorRegistry() exposes the singleton processorDefaultRegistry, and addInputProcessor() / addOutputProcessor() mutate it with replace: true. Registering a processor on one NeuroLink instance can therefore alter presets and processor resolution for every other instance in the same process.

Also applies to: 12647-12670

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 12619 - 12621, The method
getProcessorRegistry() currently returns the global processorDefaultRegistry,
causing instance methods like addInputProcessor() and addOutputProcessor() to
mutate this shared singleton, which affects all NeuroLink instances. To fix
this, modify NeuroLink to maintain its own instance-specific ProcessorRegistry
instead of referencing the global one. Change getProcessorRegistry() to return
this instance-specific registry, and update addInputProcessor() and
addOutputProcessor() to modify only the instance registry without altering the
global singleton.
src/lib/processors/input/semanticContextProcessor.ts-235-261 (1)

235-261: ⚠️ Potential issue | 🟠 Major

semantic-context is currently a no-op.

searchSemanticContext() always returns an empty array, so this built-in processor never enriches the request regardless of vectorStore, embedFunction, topK, or namespace. Because the processor is exported and registered as part of the feature, users will get silent pass-through behavior instead of semantic context retrieval. Please either wire this to a real/injected search implementation or fail fast when semantic search is configured but unavailable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/input/semanticContextProcessor.ts` around lines 235 - 261,
searchSemanticContext currently returns [] regardless of configuration causing
silent no-op; update it to detect when semantic search is requested (inspect
SemanticContextConfig.vectorStore, embedFunction, topK, namespace) and either
call an injected search handler (e.g. a provided config.searchFunction or DI
client) to produce SearchResult[] using the embedFunction/default embedding and
vector store query, or throw a clear error when a vector store is configured but
no search client is available; ensure the function returns mapped SearchResult
objects (text, score, metadata) and fails fast if required capabilities are
missing.
src/lib/processors/pipeline.ts-270-289 (1)

270-289: ⚠️ Potential issue | 🟠 Major

stopOnAbort is ignored for normal processor aborts.

Both input and output paths return immediately on result.action === "abort", so stopOnAbort: false only changes exception handling. A processor-requested abort can never be bypassed, which makes the setting misleading and prevents the non-short-circuit mode from working as configured.

Also applies to: 462-481

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/pipeline.ts` around lines 270 - 289, The pipeline
currently short-circuits on any processor-requested abort (result.action ===
"abort") regardless of the stopOnAbort flag; update the abort handling in the
input and output processor blocks (the branches that check result.action ===
"abort", processorsExecuted, and logger.debug) to honor stopOnAbort: if
stopOnAbort is true, return immediately as now, but if stopOnAbort is false, do
not return—accumulate result.feedback into feedback, update processorsExecuted
and currentData.metadata.issues/metadata as needed, log the abort, and continue
processing remaining processors (or set a non-short-circuit aborted marker in
currentData.metadata) so the pipeline does not short-circuit while still
recording the abort event.
src/lib/processors/utils/processorFactory.ts-204-241 (1)

204-241: ⚠️ Potential issue | 🟠 Major

Exhausted retries can still return action: "retry".

If the wrapped processor keeps returning retry, the loop exits and returns lastResult, which is still a retry result. That means the caller can keep retrying forever even though this wrapper has already exhausted maxRetries. The final exhausted retry needs to be converted into an abort.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/utils/processorFactory.ts` around lines 204 - 241, The
loop can exit with lastResult.action === "retry" so ensure exhausted retries
produce an abort: when calling processor.process (in the for loop that uses
maxRetries, retryDelay and processor.process) if you receive a result with
action "retry" and you're at the final attempt (attempt === maxRetries)
convert/return an abort result (include processor.id in feedback and an issues
array similar to the catch branch); also after the loop, if lastResult exists
and lastResult.action === "retry", replace it with a constructed abort object
(feedback mentioning processor.id and retries exhausted and an issues entry)
before returning.
src/lib/processors/pipeline.ts-592-615 (1)

592-615: ⚠️ Potential issue | 🟠 Major

Provider/model-gated processors still run when metadata is missing.

checkConditions() only rejects providers/models when metadata.provider / metadata.model is present. If the caller omits those fields entirely, the condition passes and the processor executes anyway. That defeats provider/model scoping for calls that do not populate metadata up front.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/pipeline.ts` around lines 592 - 615, checkConditions
currently lets processors run when conditions.providers or conditions.models are
specified but metadata.provider or metadata.model is missing; update
checkConditions (in class using ProcessorConditions and ProcessorMetadata) so
that if conditions.providers exists the function returns false unless
metadata.provider is present and included in conditions.providers, and likewise
if conditions.models exists require metadata.model to be present and included in
conditions.models; keep the custom condition check (conditions.custom(metadata))
as-is. Ensure you reference the symbols conditions.providers, conditions.models,
metadata.provider, metadata.model and the method checkConditions when making the
change.
test/processors/integration/io-processors.integration.test.ts-1736-1865 (1)

1736-1865: ⚠️ Potential issue | 🟠 Major

Avoid hard wall-clock budgets in this integration suite.

These <50ms / <100ms / <200ms assertions will flap under CI noise, slower runners, and transient host contention even when the code is correct. Please make these deterministic (for example, assert algorithmic behavior only, gate them behind a benchmark flag, or relax them enough that normal CI variance does not fail the suite).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/processors/integration/io-processors.integration.test.ts` around lines
1736 - 1865, The test uses fragile hard wall-clock assertions (e.g.,
expect(...).toBeLessThan(50)) that will flake in CI; update the tests around
ProcessorPipeline.processInput/processOutput,
createDefaultTripwireEvaluator.evaluate, and ProcessorRegistry lookups to remove
strict timing requirements and make them deterministic by either (a) gating
these timing assertions behind an explicit benchmark flag (check
process.env.RUN_BENCH === "1" and only run the expect(endTime -
startTime).toBeLessThan(...) when set), or (b) replace wall-clock checks with
algorithmic assertions (e.g., assert result.action/result.totalTime is defined,
correct, or that repeated registry.getInputProcessor/getOutputProcessor returns
expected processors) and relax or remove the tight thresholds so CI variance
won't fail the suite.
test/continuous-test-suite-processors.ts-36-42 (1)

36-42: 🛠️ Refactor suggestion | 🟠 Major

Use a type alias instead of an interface here.

This repo forbids interface in TypeScript sources/tests. Replace interface TestResult with type TestResult = { ... }.

As per coding guidelines, "Never use interface. Always use type X = { ... }."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/continuous-test-suite-processors.ts` around lines 36 - 42, Replace the
declared interface TestResult with a type alias: change "interface TestResult {
... }" to "type TestResult = { name: string; category: string; passed: boolean;
duration: number; error?: string }" ensuring the same shape and optional
property are preserved and updating any imports/usages if necessary to reference
the new type name TestResult.
src/lib/processors/utils/processorFactory.ts-75-90 (1)

75-90: ⚠️ Potential issue | 🟠 Major

Composed processors drop issues and metadata from inner processors.

These helpers only carry forward data. Any warnings, metadata, or enrichment emitted by earlier processors disappears before the composed processor returns, which breaks downstream tracing and safety aggregation in the pipeline. Please accumulate and merge issues / metadata across the chain, not just the transformed payload.

Also applies to: 113-128

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/utils/processorFactory.ts` around lines 75 - 90, The
composed processor's async process currently only carries forward result.data
and loses issues/metadata from inner processors; update the loop in process to
accumulate and merge any result.issues (arrays) and result.metadata (objects)
from each processor into aggregate variables (e.g., aggregatedIssues,
aggregatedMetadata) while still updating currentData from result.data, and
return them on exit along with action and data; apply the same merging logic to
the other composed block referenced around lines 113-128 so both composed
processor implementations propagate and merge issues/metadata instead of
dropping them.
test/continuous-test-suite-processors.ts-105-111 (1)

105-111: ⚠️ Potential issue | 🟠 Major

Remove pre-quoting of arguments to spawn().

With shell: false, spawn() treats each array element as a separate argument. Wrapping space-containing args in quotes injects literal " characters into argv, so inputs like Hello world are passed incorrectly as "Hello world" instead of Hello world.

Suggested fix
-    const quotedArgs = args.map((arg) =>
-      arg.includes(" ") ? `"${arg}"` : arg,
-    );
     // Use node directly for faster startup instead of npx
     const cliPath = path.join(process.cwd(), "dist", "cli", "index.js");
-    const proc = spawn("node", [cliPath, ...quotedArgs], {
+    const proc = spawn("node", [cliPath, ...args], {
       cwd: process.cwd(),
       timeout: TEST_CONFIG.timeout,
       shell: false,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/continuous-test-suite-processors.ts` around lines 105 - 111, The code
pre-quotes arguments via quotedArgs before calling spawn, which injects literal
double quotes into argv when using spawn(..., { shell: false }); remove the
pre-quoting step (delete/migrate usage of quotedArgs) and pass the original args
array directly to spawn (i.e., use [cliPath, ...args]) so each space-containing
argument is delivered as a single argv element; keep the cliPath and spawn(...)
invocation but replace quotedArgs with args (or ensure shell: true if you
purposely want shell parsing, but prefer using args as-is).
src/cli/commands/processors.ts-873-876 (1)

873-876: ⚠️ Potential issue | 🟠 Major

Reject array roots in validateProcessorConfig().

typeof [] === "object", so neurolink processor validate currently treats a JSON array as a valid top-level config. Since the result of JSON.parse() at line 748 is cast to Record<string, unknown> at line 777, an array root bypasses the structure check and incorrectly reports validation success.

Suggested fix
-    if (typeof config !== "object" || config === null) {
+    if (typeof config !== "object" || config === null || Array.isArray(config)) {
       errors.push("Configuration must be a JSON object");
       return { valid: false, errors, warnings };
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands/processors.ts` around lines 873 - 876,
validateProcessorConfig currently treats arrays as valid because typeof [] ===
"object"; update the initial type check in validateProcessorConfig to also
reject arrays by adding an Array.isArray(config) condition (e.g., if (typeof
config !== "object" || config === null || Array.isArray(config)) ), push the
same "Configuration must be a JSON object" error when an array is provided, and
return { valid: false, errors, warnings } as before so top-level array roots are
correctly rejected.
🟡 Minor comments (6)
FEATURE-STATUS.md-4-4 (1)

4-4: ⚠️ Potential issue | 🟡 Minor

Stale "Last Updated" date.

The document shows "January 31, 2026" but this PR is from April 2026. Consider updating to the actual date.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@FEATURE-STATUS.md` at line 4, Update the stale "Last Updated" metadata string
by replacing the literal "**Last Updated:** January 31, 2026" with the correct
current date (e.g., "**Last Updated:** April XX, 2026") in FEATURE-STATUS.md so
the document reflects the PR timing; locate and edit the line containing the
"**Last Updated:**" header and update the date value accordingly.
docs/tutorials.md-118-118 (1)

118-118: ⚠️ Potential issue | 🟡 Minor

Fix garbled character in heading.

The heading contains a mojibake/garbled character () before the globe emoji. This appears to be an encoding issue that will render incorrectly in documentation.

Proposed fix
-## �🌐 Web App Integration
+## 🌐 Web App Integration
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/tutorials.md` at line 118, The heading line currently contains a garbled
character "�" in the string "## �🌐 Web App Integration"; remove the invalid
character and replace the heading with a well-encoded version such as "## 🌐 Web
App Integration" (or plain "## Web App Integration" if you prefer to avoid
emoji), ensuring the file is saved with UTF-8 encoding so the globe emoji
renders correctly; update the heading text in the docs/tutorials.md content
where "## �🌐 Web App Integration" appears.
src/lib/processors/output/memoryPersistenceProcessor.ts-90-95 (1)

90-95: ⚠️ Potential issue | 🟡 Minor

Read provider and model from the output result.

This turn metadata is pulling provider/model from data.metadata, but the generated response data lives on data.result. As written, persisted turns will record undefined for both fields.

Proposed change
           metadata: {
             ...customMetadata,
-            provider: data.metadata.provider,
-            model: data.metadata.model,
+            provider: data.result.provider,
+            model: data.result.model,
             requestId: data.metadata.requestId,
           },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/output/memoryPersistenceProcessor.ts` around lines 90 -
95, The metadata block in memoryPersistenceProcessor is pulling provider/model
from data.metadata but the generated response has those fields on data.result;
update the metadata assignment (where provider and model are set) to read
provider and model from data.result (e.g., data.result?.metadata?.provider and
data.result?.metadata?.model), falling back to data.metadata if absent, while
leaving requestId as data.metadata.requestId so persisted turns record the
actual provider/model from the result.
test/fixtures/processors/pipeline-config.json-373-375 (1)

373-375: ⚠️ Potential issue | 🟡 Minor

Use the plural CLI command in the fixture example.

This sample says neurolink processor run, but the new command family is neurolink processors .... Leaving the singular form here will bake the wrong invocation into fixtures/docs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/fixtures/processors/pipeline-config.json` around lines 373 - 375, The
fixture's usage example uses the old singular CLI invocation; update the "cli"
value under the "usage" object so it uses the new plural command family (change
"neurolink processor run --preset security --input 'text'" to "neurolink
processors run --preset security --input 'text'") so the example and fixtures
reflect the correct command; locate the "usage" object and the "cli" string in
the JSON and modify that string accordingly.
src/lib/processors/output/contentFilteringProcessor.ts-138-138 (1)

138-138: ⚠️ Potential issue | 🟡 Minor

Potential TypeError if data.metadata.custom is undefined.

Accessing data.metadata.custom.retryCount will throw if custom is undefined.

🛡️ Proposed fix
       if (action === "retry") {
-        const currentRetry = (data.metadata.custom.retryCount as number) || 0;
+        const currentRetry = (data.metadata.custom?.retryCount as number) || 0;
         if (currentRetry < maxRetries) {

Also apply at line 170:

             (action === "retry" &&
-              ((data.metadata.custom.retryCount as number) || 0) >= maxRetries),
+              ((data.metadata.custom?.retryCount as number) || 0) >= maxRetries),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/output/contentFilteringProcessor.ts` at line 138, The code
accesses data.metadata.custom.retryCount which can throw if custom is undefined;
change the access to use optional chaining and a safe default (e.g., const
currentRetry = (data.metadata.custom?.retryCount as number) || 0) or ensure
data.metadata.custom is initialized before reading, and apply the same fix to
the other occurrence that reads retryCount later in the file (the second access
of data.metadata.custom.retryCount).
src/lib/processors/utils/metadataUtils.ts-88-100 (1)

88-100: ⚠️ Potential issue | 🟡 Minor

Comment says "deep copy" but implementation is shallow.

The JSDoc claims this returns a "Deep copy of metadata", but the implementation only creates a shallow copy. Nested objects within custom values, or nested properties in issues items, will still share references with the original.

If callers expect true deep copy for safe mutation, this could lead to subtle bugs.

🔧 Proposed fix options

Option 1: Fix the comment to reflect actual behavior:

 /**
  * Clone metadata for safe mutation
  * `@param` metadata - Metadata to clone
- * `@returns` Deep copy of metadata
+ * `@returns` Shallow copy of metadata (top-level arrays/objects are cloned)
  */

Option 2: Implement actual deep copy if needed:

 export function cloneMetadata(metadata: ProcessorMetadata): ProcessorMetadata {
-  return {
-    ...metadata,
-    custom: { ...metadata.custom },
-    issues: [...metadata.issues],
-    processorTrace: [...metadata.processorTrace],
-  };
+  return JSON.parse(JSON.stringify(metadata));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/utils/metadataUtils.ts` around lines 88 - 100, The JSDoc
for cloneMetadata claims a "Deep copy" but the function only shallow-clones
top-level fields (custom, issues, processorTrace); update cloneMetadata to
produce a true deep clone of ProcessorMetadata (including nested objects inside
custom values and nested properties inside issues and processorTrace) by
replacing the shallow spreads with a deep-cloning approach (e.g., use
structuredClone where available with a safe fallback like
JSON.parse(JSON.stringify(...)) or a project deepClone utility) while preserving
the ProcessorMetadata shape and types in the returned value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1a85a0cd-d91a-4f67-a571-84b38fe5aa11

📥 Commits

Reviewing files that changed from the base of the PR and between 1b1b086 and 34eeaf6.

📒 Files selected for processing (48)
  • FEATURE-STATUS.md
  • docs/tutorials.md
  • src/cli/commands/processors.ts
  • src/cli/loop/optionsSchema.ts
  • src/cli/parser.ts
  • src/lib/index.ts
  • src/lib/neurolink.ts
  • src/lib/processors/errors/IOProcessorError.ts
  • src/lib/processors/index.ts
  • src/lib/processors/input/contentModerationProcessor.ts
  • src/lib/processors/input/index.ts
  • src/lib/processors/input/memoryRetrievalProcessor.ts
  • src/lib/processors/input/messageValidationProcessor.ts
  • src/lib/processors/input/piiDetectionProcessor.ts
  • src/lib/processors/input/semanticContextProcessor.ts
  • src/lib/processors/output/contentFilteringProcessor.ts
  • src/lib/processors/output/index.ts
  • src/lib/processors/output/lengthValidationProcessor.ts
  • src/lib/processors/output/memoryPersistenceProcessor.ts
  • src/lib/processors/output/responseValidationProcessor.ts
  • src/lib/processors/output/toxicityCheckProcessor.ts
  • src/lib/processors/pipeline.ts
  • src/lib/processors/presets.ts
  • src/lib/processors/processorHelpers.ts
  • src/lib/processors/registry.ts
  • src/lib/processors/tripwire.ts
  • src/lib/processors/utils/index.ts
  • src/lib/processors/utils/metadataUtils.ts
  • src/lib/processors/utils/processorFactory.ts
  • src/lib/processors/utils/validationUtils.ts
  • src/lib/types/config.ts
  • src/lib/types/generate.ts
  • src/lib/types/index.ts
  • src/lib/types/ioProcessor.ts
  • src/lib/types/stream.ts
  • test/continuous-test-suite-processors.ts
  • test/fixtures/processors/input-samples.json
  • test/fixtures/processors/output-samples.json
  • test/fixtures/processors/pipeline-config.json
  • test/processors/IOProcessorError.test.ts
  • test/processors/integration.test.ts
  • test/processors/integration/io-processors.integration.test.ts
  • test/processors/sdk-integration.test.ts
  • test/unit/processors/input/piiDetection.test.ts
  • test/unit/processors/output/toxicityCheck.test.ts
  • test/unit/processors/pipeline.test.ts
  • test/unit/processors/registry.test.ts
  • test/unit/processors/tripwire.test.ts

Comment thread src/lib/processors/tripwire.ts Outdated
Comment on lines +104 to +121
evaluate(data: InputProcessorData | OutputProcessorData): TripwireResult {
for (const tripwire of this.tripwires) {
try {
if (tripwire.condition(data)) {
logger.debug(
`[TripwireEvaluator] Tripwire triggered: ${tripwire.id}`,
);

// Determine action to return
const action = tripwire.action === "abort" ? "abort" : "continue";

return {
triggered: true,
tripwire,
action,
feedback: tripwire.message,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Do not stop evaluation on the first warning.

evaluate() returns on the first triggered tripwire even when that tripwire is only a warning, so a warning registered before an abort tripwire will mask the abort and let the caller continue. That breaks the tripwire safety layer. Please keep scanning after warn/continue results and only short-circuit once an aborting tripwire fires.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/processors/tripwire.ts` around lines 104 - 121, evaluate() currently
returns on the first triggered tripwire (from this.tripwires) even when its
action is "continue"/warn, which can mask a later "abort" tripwire; change
evaluate to scan all tripwires, short-circuit immediately only if a tripwire
with action "abort" is encountered, and otherwise accumulate any non-abort
triggers (e.g., collect triggered warnings/messages and the first non-abort
tripwire) and after the loop return a result reflecting either the abort
tripwire (if found), or if no aborts but one or more warnings were triggered
return a combined warning TripwireResult (triggered:true, tripwire = first
warning or aggregated info, action:"continue", feedback = combined messages),
otherwise return the non-triggered default; update the evaluate method and
related TripwireResult construction to support aggregated feedback while keeping
the existing logger.debug calls.

@murdore murdore force-pushed the feat/io-processors branch from 34eeaf6 to 08047cf Compare April 17, 2026 03:49
@murdore murdore changed the title feat(processors): add I/O processor system with pipeline and tripwires feat(safety): add native PII detection and response validation to generate/stream Apr 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

Comment thread src/lib/utils/piiDetector.ts Fixed
Comment thread src/lib/utils/piiDetector.ts Fixed
Comment thread src/lib/utils/responseValidator.ts Fixed
@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

Comment thread src/lib/utils/responseValidator.ts Fixed
@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

Comment thread src/lib/utils/responseValidator.ts Fixed
@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@murdore murdore force-pushed the feat/io-processors branch from c55c8ab to 12aa121 Compare April 20, 2026 21:22
@murdore

murdore commented Apr 20, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto latest origin/release

Branch is now 1 commit ahead, 0 behind origin/release (12aa1219).

  • Type check: 0 errors
  • Lint: 0 errors (only pre-existing warnings)
  • Build: clean

Requesting fresh review after rebase.

@murdore

murdore commented Apr 20, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@murdore murdore force-pushed the feat/io-processors branch from 12aa121 to a9617bc Compare April 21, 2026 20:21
@murdore

murdore commented Apr 21, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown

Only repository collaborators, contributors, or members can run CodeRabbit commands.

@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown

Only repository collaborators, contributors, or members can run CodeRabbit commands.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@coderabbitai coderabbitai 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.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/lib/neurolink.ts (2)

3925-4013: ⚠️ Potential issue | 🟠 Major

Validate the response before publishing success events, and handle retry before finalization.

Line 3925 emits generation:end and Line 3940 emits response:end before Line 3989 validates the response. If validation aborts or truncates, listeners already received a successful raw response. Also, retryOnFailure/maxRetries cannot actually regenerate from finalizeGenerateRequestResult() because the provider call has already completed.

Move response validation before the end events, emit only validated/truncated content, and implement retry around the generateTextInternal() call path before finalizing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 3925 - 4013, The current flow emits
generation:end, response:end and message (see emitter.emit calls around
generation:end/response:end/message) before running response validation and
potential retries, which causes listeners to see unvalidated raw content and
prevents retrying; fix by moving the response validation block (the
validateResponse(...) logic that inspects options.responseValidation and
validationResult) to occur immediately after assembling generateResult and
before any emitter.emit("generation:end"/"response:end"/"message") or calling
finalizeGenerateRequestResult(), and implement retry logic that wraps the
provider call path (the generateTextInternal() invocation and/or the code path
that produces textResult used to build generateResult) to honor
responseValidation.retryOnFailure and maxRetries so that retries happen prior to
finalization and only the validated/truncated content is emitted to listeners.

3583-3652: ⚠️ Potential issue | 🟠 Major

Persist the redacted prompt, not the raw PII-bearing prompt.

originalPrompt is captured before PII redaction and later stored in conversation memory. With piiDetection.action: "redact", the provider receives sanitized text, but memory still stores the original PII.

Privacy-preserving direction
-    const originalPrompt = this._extractOriginalPrompt(optionsOrPrompt);
+    let originalPrompt = this._extractOriginalPrompt(optionsOrPrompt);
...
     if (options.piiDetection?.enabled) {
+      const textBeforePiiProcessing = options.input.text;
       const piiResult = await detectAndRedactPII(options.input.text, {
         enabled: true,
         action: options.piiDetection.action ?? "warn",
...
       // Replace input text with redacted version
       options.input.text = piiResult.text;
+      if (
+        (options.piiDetection.action ?? "warn") === "redact" &&
+        piiResult.text !== textBeforePiiProcessing
+      ) {
+        originalPrompt = piiResult.text;
+      }
     }

For stream, capture originalPrompt after validateStreamRequestOptions() mutates options.input.text:

-      const originalPrompt = options.input.text;
 
       options.fileRegistry = this.fileRegistry;
       await this.validateStreamRequestOptions(options, startTime);
+      const originalPrompt = options.input.text;

Also applies to: 4107-4110, 6368-6451, 7347-7351

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 3583 - 3652, The code currently captures
originalPrompt via this._extractOriginalPrompt(optionsOrPrompt) before input
validation and PII redaction, so conversation memory ends up storing raw PII;
move or reassign originalPrompt after validateStreamRequestOptions() and after
the PII redaction block so it captures the sanitized options.input.text (or call
this._extractOriginalPrompt(options.input?.text) after redaction). Update the
same pattern in the corresponding stream handler (use
validateStreamRequestOptions()) and the other noted locations (around the blocks
containing PII detection/redaction) so memory/storage always persists the
redacted prompt rather than the original raw input.
♻️ Duplicate comments (1)
src/lib/types/generate.ts (1)

515-572: 🛠️ Refactor suggestion | 🟠 Major

Same inline-duplication / action optionality mismatch as StreamOptions.

Mirror issue to the one on src/lib/types/stream.ts (lines 552-600):

  • Line 521: action?: "redact" | "abort" | "warn" is optional here but required in PiiDetectionConfig (ioProcessor.ts line 28).
  • Lines 550-554: inlined customValidator return type drops field?: string from ValidationIssue.
  • detectTypes (522-533) re-enumerates PiiType by hand.

Prefer importing the canonical types and referencing them directly — then TextGenerationOptions (lines 1133-1145) can keep its GenerateOptions["piiDetection"] indexed access without the shape drifting behind it.

♻️ Proposed change
-import type { ProcessorPipelineConfig } from "./ioProcessor.js";
+import type {
+  InputValidationConfig,
+  PiiDetectionConfig,
+  ProcessorPipelineConfig,
+  ResponseValidationConfig,
+} from "./ioProcessor.js";
-  piiDetection?: {
-    enabled?: boolean;
-    action?: "redact" | "abort" | "warn";
-    detectTypes?: Array<
-      | "email"
-
-      | "driversLicense"
-    >;
-    customPatterns?: RegExp[];
-    allowList?: string[];
-    redactionText?: string;
-  };
-
-  responseValidation?: {
-
-  };
-
-  inputValidation?: {
-
-  };
+  piiDetection?: PiiDetectionConfig;
+  responseValidation?: ResponseValidationConfig;
+  inputValidation?: InputValidationConfig;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/types/generate.ts` around lines 515 - 572, The piiDetection block in
TextGeneration-related types duplicates and drifts from canonical types: replace
the inline shape with imports of PiiDetectionConfig and PiiType and use them
directly (so detectTypes becomes PiiType[] and action matches the requiredness
in PiiDetectionConfig), and update the customValidator return type to use the
shared ValidationIssue type (preserving optional field?: string) instead of an
ad-hoc inline type; also change any related references (e.g.,
TextGenerationOptions/GenerateOptions["piiDetection"]) to reference the imported
canonical types to prevent future drift.
🧹 Nitpick comments (2)
test/continuous-test-suite-safety.ts (1)

21-27: Exercise the new public exports in this safety suite.

These imports bypass src/lib/index.ts, so the tests won’t catch a broken public export even though this PR adds one. Importing from ../src/lib/index.js here, or adding a small public-entrypoint smoke test, would cover the new API surface.

Suggested import change
-import { detectAndRedactPII } from "../src/lib/utils/piiDetector.js";
-import { validateResponse } from "../src/lib/utils/responseValidator.js";
 import {
+  detectAndRedactPII,
+  validateResponse,
   TripwireEvaluator,
   createDefaultTripwireEvaluator,
   commonTripwires,
-} from "../src/lib/utils/tripwireEvaluator.js";
+} from "../src/lib/index.js";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/continuous-test-suite-safety.ts` around lines 21 - 27, The test is
importing internal modules directly which skips the package's public entrypoint;
update the imports to exercise the public API by importing detectAndRedactPII,
validateResponse, TripwireEvaluator, createDefaultTripwireEvaluator, and
commonTripwires from the public entrypoint (../src/lib/index.js) instead of
their internal paths, or add a tiny smoke test that imports those symbols from
the public export to ensure the new public export is exercised.
src/lib/types/ioProcessor.ts (1)

14-150: Centralize InputValidationConfig and consider domain prefixes for type names.

Two improvements for this types module:

  1. InputValidationConfig is duplicated inline: The shape { trimWhitespace?, minLength?, maxLength?, requireContent? } appears identically in both src/lib/types/stream.ts (lines 592–597) and src/lib/types/generate.ts (lines 562–567). Extracting this as an exported type here would eliminate duplication and mirror PiiDetectionConfig and ResponseValidationConfig, keeping all three safety-related configs in one place.

  2. Generic type names lack domain prefixes: ValidationIssue, DetectedPII, and PiiType are generic names without domain prefixes. Per the coding guideline requiring globally unique exported type names in src/lib/types/ (using prefixes like Processor*, Stream*, etc. to disambiguate), consider renaming these to better reflect their domain—e.g., ProcessorValidationIssue, ProcessorDetectedPII, ProcessorPiiType.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/types/ioProcessor.ts` around lines 14 - 150, Extract a shared
exported type InputValidationConfig (shape: trimWhitespace?: boolean;
minLength?: number; maxLength?: number; requireContent?: boolean) into this file
and replace the duplicated inline shapes in src/lib/types/stream.ts and
src/lib/types/generate.ts by importing and using InputValidationConfig; update
any references to the inline anonymous shapes to the new type. Also apply
domain-prefixed renames for the public types: rename PiiType → ProcessorPiiType,
DetectedPII → ProcessorDetectedPII, and ValidationIssue →
ProcessorValidationIssue (and update all references in this file and across the
codebase), while keeping temporary backwards-compatible exports (e.g., export
type PiiType = ProcessorPiiType) if needed to avoid breaking existing imports.
Ensure you update PiiDetectionConfig, ResponseValidationConfig, and any other
types that reference the renamed symbols to reference the new Processor* names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/factories/commandFactory.ts`:
- Around line 487-536: commonOptions currently includes safety flags (piiRedact,
piiTypes, piiAction, inputMaxLength, trimWhitespace, requireContent,
outputMaxLength, outputMinLength) which makes commands like executeBatch()
accept them but not apply them; either move these flags out of commonOptions
into the generate/stream-specific option set or ensure every command that
exposes them forwards them to the execution layer. Specifically, remove or stop
exporting the safety flags from commonOptions (or
CLICommandFactory.commonOptions) and instead add CLICommandFactory.safetyOptions
only to createGenerateCommand() and createStreamCommand(); alternatively, if you
prefer global exposure, update executeBatch(), executeProviderStatus(),
executeSetup(), executeLoop(), etc. to accept and pass the corresponding
piiDetection/input validation/response validation fields through to the same
place executeGenerate() and executeRealStream() use so the flags are honored.
- Around line 2706-2723: The code currently casts user-provided argv.piiAction
and argv.piiTypes directly into expected enums (action/detectTypes) which allows
invalid tokens through; update commandFactory.ts to validate argv.piiAction
against the allowed set {"redact","abort","warn"} and validate each token from
argv.piiTypes against the supported PII type set before constructing
piiDetection (reject or error out on any invalid value), centralize this logic
into a shared parser/validator function (e.g., parsePiiAction and parsePiiTypes
or a single parsePiiDetection helper) and replace both places where casting is
used (the blocks that build action and detectTypes) to call the shared validator
so typos like "delete" or "foo" are rejected before reaching the SDK.

In `@src/lib/neurolink.ts`:
- Around line 6413-6452: The streaming path currently validates input but never
validates streamed output: update the stream() implementation to call
validateResponse() (or the existing responseValidation logic) on the
accumulatedContent before emitting the final "stream:complete" and before
persisting to memory; while streaming, enforce maxLength by truncating yielded
content when necessary and stop/abort if responseValidation returns an abort
rule (ensure validateResponse/responseValidation is invoked after each chunk or
at least on the final accumulated response), apply truncation to
accumulatedContent used for span finalization and memory storage, and ensure any
abort reason from validateResponse is thrown so spans are finalized and no
unvalidated content is stored (refer to function/method names validateResponse,
stream(), accumulatedContent, responseValidation, and the "stream:complete"
emission).
- Around line 3619-3649: Replace bare throws in the guardrail/PII/validation
paths with typed errors created via the project's ErrorFactory (e.g., use
ErrorFactory.createValidationError / createSafetyError) instead of `throw new
Error(...)` for the checks inside the input validation block and the PII block
that calls detectAndRedactPII; wrap the async detectAndRedactPII call with the
withTimeout utility and propagate its typed error rather than raw throws (use
the returned ErrorFactory error when piiResult.action === "abort"); ensure any
provider error formatting uses formatProviderError to produce an Error object
(never throw inside formatProviderError) so SDK callers can reliably detect
validation vs retryable provider failures.

In `@src/lib/types/stream.ts`:
- Around line 552-600: Replace the inline safety config declarations in
StreamOptions with the canonical types from ioProcessor.ts: import and use
PiiDetectionConfig for piiDetection, ResponseValidationConfig for
responseValidation, and (add or import) InputValidationConfig for
inputValidation so the public types stay consistent with runtime; ensure you
stop redeclaring the PII type union, required PiiDetectionConfig.action (not
optional), and the ValidationIssue shape (include optional field?: string) used
by customValidator, leaving processors?: ProcessorPipelineConfig as deprecated.

In `@src/lib/utils/piiDetector.ts`:
- Around line 196-210: The code treats allowList as a list of PII type names to
skip (e.g., "email"), which is misleading; rename the configuration property and
references to make intent explicit: change PiiDetectionConfig.allowList to
disabledTypes (or skipTypes) and update this file to check
disabledTypes.includes(piiType) instead of allowList; update the JSDoc in
PiiDetectionConfig (ioProcessor.ts) and any other usages of allowList
accordingly so the semantics are clear across scanField, PII_PATTERNS, and
detectTypes.
- Around line 99-128: scanField currently mutates shared RegExp.lastIndex and
loops with regex.exec, causing race conditions when PII_PATTERNS is reused
across concurrent detectAndRedactPII calls and risking infinite loops for
non-global custom patterns; fix by validating/forcing the global flag in
validateCustomPattern (reject or clone patterns without 'g') and rewrite
scanField to use String.prototype.matchAll on a cloned RegExp (new
RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags :
pattern.flags + 'g')) or otherwise ensure a fresh global RegExp per call so no
shared lastIndex is mutated and match iteration cannot hang.

In `@src/lib/utils/responseValidator.ts`:
- Around line 262-371: The truncation branch (when config.truncationAction ===
"truncate") mutates text before later checks so required/forbidden phrase checks
and validateJsonSchema run against the truncated text; change the implementation
to preserve the original responseText for downstream validations by running
phrase checks (config.requiredPhrases, config.forbiddenPhrases) and JSON schema
validation (config.jsonSchema via validateJsonSchema) against responseText (or a
copied original variable) while using text only for length handling and the
returned payload, and add a short comment by the truncation logic
(truncationAction / text / responseText / validateJsonSchema) clarifying that
truncation does not affect phrase/schema checks.

In `@src/lib/utils/tripwireEvaluator.ts`:
- Around line 85-164: In evaluate and evaluateAll, don’t silently swallow errors
from tripwire.condition: catch the error, and if logger.shouldLog("warn") then
logger.warn a short message including the tripwire.id (and the error) so broken
user-provided conditions are visible while preserving behavior; also ensure you
import logger from ./logger.js. Additionally, when computing message (in both
evaluate and evaluateAll), if tripwire.message is undefined fall back to
tripwire.name (or description if you prefer) so triggered results always include
a usable message.

---

Outside diff comments:
In `@src/lib/neurolink.ts`:
- Around line 3925-4013: The current flow emits generation:end, response:end and
message (see emitter.emit calls around generation:end/response:end/message)
before running response validation and potential retries, which causes listeners
to see unvalidated raw content and prevents retrying; fix by moving the response
validation block (the validateResponse(...) logic that inspects
options.responseValidation and validationResult) to occur immediately after
assembling generateResult and before any
emitter.emit("generation:end"/"response:end"/"message") or calling
finalizeGenerateRequestResult(), and implement retry logic that wraps the
provider call path (the generateTextInternal() invocation and/or the code path
that produces textResult used to build generateResult) to honor
responseValidation.retryOnFailure and maxRetries so that retries happen prior to
finalization and only the validated/truncated content is emitted to listeners.
- Around line 3583-3652: The code currently captures originalPrompt via
this._extractOriginalPrompt(optionsOrPrompt) before input validation and PII
redaction, so conversation memory ends up storing raw PII; move or reassign
originalPrompt after validateStreamRequestOptions() and after the PII redaction
block so it captures the sanitized options.input.text (or call
this._extractOriginalPrompt(options.input?.text) after redaction). Update the
same pattern in the corresponding stream handler (use
validateStreamRequestOptions()) and the other noted locations (around the blocks
containing PII detection/redaction) so memory/storage always persists the
redacted prompt rather than the original raw input.

---

Duplicate comments:
In `@src/lib/types/generate.ts`:
- Around line 515-572: The piiDetection block in TextGeneration-related types
duplicates and drifts from canonical types: replace the inline shape with
imports of PiiDetectionConfig and PiiType and use them directly (so detectTypes
becomes PiiType[] and action matches the requiredness in PiiDetectionConfig),
and update the customValidator return type to use the shared ValidationIssue
type (preserving optional field?: string) instead of an ad-hoc inline type; also
change any related references (e.g.,
TextGenerationOptions/GenerateOptions["piiDetection"]) to reference the imported
canonical types to prevent future drift.

---

Nitpick comments:
In `@src/lib/types/ioProcessor.ts`:
- Around line 14-150: Extract a shared exported type InputValidationConfig
(shape: trimWhitespace?: boolean; minLength?: number; maxLength?: number;
requireContent?: boolean) into this file and replace the duplicated inline
shapes in src/lib/types/stream.ts and src/lib/types/generate.ts by importing and
using InputValidationConfig; update any references to the inline anonymous
shapes to the new type. Also apply domain-prefixed renames for the public types:
rename PiiType → ProcessorPiiType, DetectedPII → ProcessorDetectedPII, and
ValidationIssue → ProcessorValidationIssue (and update all references in this
file and across the codebase), while keeping temporary backwards-compatible
exports (e.g., export type PiiType = ProcessorPiiType) if needed to avoid
breaking existing imports. Ensure you update PiiDetectionConfig,
ResponseValidationConfig, and any other types that reference the renamed symbols
to reference the new Processor* names.

In `@test/continuous-test-suite-safety.ts`:
- Around line 21-27: The test is importing internal modules directly which skips
the package's public entrypoint; update the imports to exercise the public API
by importing detectAndRedactPII, validateResponse, TripwireEvaluator,
createDefaultTripwireEvaluator, and commonTripwires from the public entrypoint
(../src/lib/index.js) instead of their internal paths, or add a tiny smoke test
that imports those symbols from the public export to ensure the new public
export is exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 756eeaa7-7f31-4e9c-a4b8-930258de55e2

📥 Commits

Reviewing files that changed from the base of the PR and between 9005c45 and a9617bc.

📒 Files selected for processing (13)
  • src/cli/factories/commandFactory.ts
  • src/cli/loop/optionsSchema.ts
  • src/lib/index.ts
  • src/lib/neurolink.ts
  • src/lib/processors/index.ts
  • src/lib/types/generate.ts
  • src/lib/types/index.ts
  • src/lib/types/ioProcessor.ts
  • src/lib/types/stream.ts
  • src/lib/utils/piiDetector.ts
  • src/lib/utils/responseValidator.ts
  • src/lib/utils/tripwireEvaluator.ts
  • test/continuous-test-suite-safety.ts

Comment on lines +487 to +536
// Safety — PII detection
piiRedact: {
type: "boolean" as const,
description:
"Enable PII detection and redaction on input before sending to LLM",
alias: "pii-redact",
default: false,
},
piiTypes: {
type: "string" as const,
description:
"Comma-separated PII types to detect (email,phone,ssn,creditCard,ipAddress,address,name,dateOfBirth,passport,driversLicense)",
alias: "pii-types",
},
piiAction: {
type: "string" as const,
description: "Action when PII is found: redact, abort, or warn",
alias: "pii-action",
default: "redact",
},
// Safety — Input validation
inputMaxLength: {
type: "number" as const,
description: "Maximum input text length (characters)",
alias: "input-max-length",
},
trimWhitespace: {
type: "boolean" as const,
description: "Trim whitespace from input text",
alias: "trim-whitespace",
default: false,
},
requireContent: {
type: "boolean" as const,
description: "Abort if input text is empty or whitespace",
alias: "require-content",
default: false,
},
// Safety — Response validation
outputMaxLength: {
type: "number" as const,
description:
"Maximum response length (characters). Truncates if exceeded",
alias: "output-max-length",
},
outputMinLength: {
type: "number" as const,
description: "Minimum response length (characters)",
alias: "output-min-length",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid exposing safety flags on commands that silently ignore them.

Line 487 adds these flags to commonOptions, so batch, provider status, setup, loop, and other commands accept them too. Only executeGenerate() and executeRealStream() forward them; for example, neurolink batch prompts.txt --pii-redact is accepted but executeBatch() does not pass piiDetection, so prompts can be sent unredacted. Move these flags into generate/stream-specific options, or wire them through every command that exposes them.

Suggested direction
+  private static readonly safetyOptions = {
+    // Safety — PII detection
+    piiRedact: { /* existing option */ },
+    piiTypes: { /* existing option */ },
+    piiAction: { /* existing option */ },
+    // Safety — Input validation
+    inputMaxLength: { /* existing option */ },
+    trimWhitespace: { /* existing option */ },
+    requireContent: { /* existing option */ },
+    // Safety — Response validation
+    outputMaxLength: { /* existing option */ },
+    outputMinLength: { /* existing option */ },
+  };
+
   private static readonly commonOptions = {
     // Core generation options
     provider: {
       ...
     },
-    // Safety — PII detection
-    piiRedact: { ... },
-    piiTypes: { ... },
-    piiAction: { ... },
-    // Safety — Input validation
-    inputMaxLength: { ... },
-    trimWhitespace: { ... },
-    requireContent: { ... },
-    // Safety — Response validation
-    outputMaxLength: { ... },
-    outputMinLength: { ... },
   };

Then pass CLICommandFactory.safetyOptions as additionalOptions only from createGenerateCommand() and createStreamCommand() unless batch support is intentionally implemented too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/factories/commandFactory.ts` around lines 487 - 536, commonOptions
currently includes safety flags (piiRedact, piiTypes, piiAction, inputMaxLength,
trimWhitespace, requireContent, outputMaxLength, outputMinLength) which makes
commands like executeBatch() accept them but not apply them; either move these
flags out of commonOptions into the generate/stream-specific option set or
ensure every command that exposes them forwards them to the execution layer.
Specifically, remove or stop exporting the safety flags from commonOptions (or
CLICommandFactory.commonOptions) and instead add CLICommandFactory.safetyOptions
only to createGenerateCommand() and createStreamCommand(); alternatively, if you
prefer global exposure, update executeBatch(), executeProviderStatus(),
executeSetup(), executeLoop(), etc. to accept and pass the corresponding
piiDetection/input validation/response validation fields through to the same
place executeGenerate() and executeRealStream() use so the flags are honored.

Comment thread src/cli/factories/commandFactory.ts Outdated
Comment on lines +2706 to +2723
action:
(argv.piiAction as "redact" | "abort" | "warn") ?? "redact",
detectTypes: argv.piiTypes
? ((argv.piiTypes as string)
.split(",")
.map((t) => t.trim()) as Array<
| "email"
| "phone"
| "ssn"
| "creditCard"
| "ipAddress"
| "address"
| "name"
| "dateOfBirth"
| "passport"
| "driversLicense"
>)
: 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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify PII CLI parsing paths no longer rely on raw casts.
# Expected after the fix: no direct `argv.piiAction as ...` or split+cast of `argv.piiTypes`
# without validation against the supported action/type sets.
rg -n -C4 'piiAction|piiTypes|detectTypes' --type ts

Repository: juspay/neurolink

Length of output: 12604


Validate PII action and type tokens before casting.

The current casts let typos like --pii-action delete or --pii-types email,foo reach the SDK as untrusted values. Reject invalid values against {"redact", "abort", "warn"} and the supported PII type set before building piiDetection. The same pattern appears twice in commandFactory.ts (lines 2706–2723 and 3008–3024); consider extracting a shared parser function.

Example validation approach
+const SUPPORTED_PII_ACTIONS = new Set(["redact", "abort", "warn"]);
+const SUPPORTED_PII_TYPES = new Set([
+  "email",
+  "phone",
+  "ssn",
+  "creditCard",
+  "ipAddress",
+  "address",
+  "name",
+  "dateOfBirth",
+  "passport",
+  "driversLicense",
+]);
+
+if (argv.piiAction && !SUPPORTED_PII_ACTIONS.has(String(argv.piiAction))) {
+  throw new Error(`Invalid --pii-action: ${argv.piiAction}`);
+}
+
+const parsedPiiTypes = argv.piiTypes
+  ? String(argv.piiTypes)
+      .split(",")
+      .map((t) => t.trim())
+      .filter(Boolean)
+  : undefined;
+
+const invalidPiiTypes = parsedPiiTypes?.filter(
+  (type) => !SUPPORTED_PII_TYPES.has(type),
+);
+if (invalidPiiTypes?.length) {
+  throw new Error(`Invalid --pii-types: ${invalidPiiTypes.join(", ")}`);
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/factories/commandFactory.ts` around lines 2706 - 2723, The code
currently casts user-provided argv.piiAction and argv.piiTypes directly into
expected enums (action/detectTypes) which allows invalid tokens through; update
commandFactory.ts to validate argv.piiAction against the allowed set
{"redact","abort","warn"} and validate each token from argv.piiTypes against the
supported PII type set before constructing piiDetection (reject or error out on
any invalid value), centralize this logic into a shared parser/validator
function (e.g., parsePiiAction and parsePiiTypes or a single parsePiiDetection
helper) and replace both places where casting is used (the blocks that build
action and detectTypes) to call the shared validator so typos like "delete" or
"foo" are rejected before reaching the SDK.

Comment thread src/lib/neurolink.ts
Comment on lines +3619 to +3649
if (iv.requireContent && !options.input.text.trim()) {
throw new Error(
"Input content is required but was empty or whitespace",
);
}
if (iv.minLength && options.input.text.length < iv.minLength) {
throw new Error(
`Input text is too short (${options.input.text.length} < ${iv.minLength})`,
);
}
if (iv.maxLength && options.input.text.length > iv.maxLength) {
throw new Error(
`Input text is too long (${options.input.text.length} > ${iv.maxLength})`,
);
}
}

// PII detection and redaction
if (options.piiDetection?.enabled) {
const piiResult = await detectAndRedactPII(options.input.text, {
enabled: true,
action: options.piiDetection.action ?? "warn",
detectTypes: options.piiDetection.detectTypes,
customPatterns: options.piiDetection.customPatterns,
allowList: options.piiDetection.allowList,
redactionText: options.piiDetection.redactionText,
});
if (piiResult.action === "abort") {
throw new Error(
piiResult.feedback ?? "Request blocked: PII detected in 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.

⚠️ Potential issue | 🟡 Minor

Return typed validation errors for the new guardrails.

These new validation/PII/response-validation failures currently throw bare Error, so SDK callers cannot reliably distinguish validation failures, PII blocks, and retryable provider failures. Prefer an ErrorFactory validation/safety helper, or add one if none exists. As per coding guidelines, **/*.ts: Error Handling — Use ErrorFactory for typed errors, wrap async calls with withTimeout utility, formatProviderError must return errors never throw.

Also applies to: 4002-4006, 6419-6449

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 3619 - 3649, Replace bare throws in the
guardrail/PII/validation paths with typed errors created via the project's
ErrorFactory (e.g., use ErrorFactory.createValidationError / createSafetyError)
instead of `throw new Error(...)` for the checks inside the input validation
block and the PII block that calls detectAndRedactPII; wrap the async
detectAndRedactPII call with the withTimeout utility and propagate its typed
error rather than raw throws (use the returned ErrorFactory error when
piiResult.action === "abort"); ensure any provider error formatting uses
formatProviderError to produce an Error object (never throw inside
formatProviderError) so SDK callers can reliably detect validation vs retryable
provider failures.

Comment thread src/lib/neurolink.ts
Comment on lines +6413 to +6452
// Input validation for stream
if (options.inputValidation && options.input?.text) {
const iv = options.inputValidation;
if (iv.trimWhitespace) {
options.input.text = options.input.text.trim();
}
if (iv.requireContent && !options.input.text.trim()) {
throw new Error(
"Input content is required but was empty or whitespace",
);
}
if (iv.minLength && options.input.text.length < iv.minLength) {
throw new Error(
`Input text is too short (${options.input.text.length} < ${iv.minLength})`,
);
}
if (iv.maxLength && options.input.text.length > iv.maxLength) {
throw new Error(
`Input text is too long (${options.input.text.length} > ${iv.maxLength})`,
);
}
}

// PII detection and redaction for stream
if (options.piiDetection?.enabled && options.input?.text) {
const piiResult = await detectAndRedactPII(options.input.text, {
enabled: true,
action: options.piiDetection.action ?? "warn",
detectTypes: options.piiDetection.detectTypes,
customPatterns: options.piiDetection.customPatterns,
allowList: options.piiDetection.allowList,
redactionText: options.piiDetection.redactionText,
});
if (piiResult.action === "abort") {
throw new Error(
piiResult.feedback ?? "Request blocked: PII detected in input",
);
}
options.input.text = piiResult.text;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Apply output response validation in the streaming path too.

The stream path now has input-side safety, but streamed output never calls validateResponse(): raw chunks are yielded and stream:complete is emitted with unvalidated accumulatedContent. This makes responseValidation a no-op for stream().

For streaming, enforce maxLength/truncation while yielding, and apply abort rules before stream:complete, span finalization, and memory storage.

Also applies to: 6584-6661

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 6413 - 6452, The streaming path currently
validates input but never validates streamed output: update the stream()
implementation to call validateResponse() (or the existing responseValidation
logic) on the accumulatedContent before emitting the final "stream:complete" and
before persisting to memory; while streaming, enforce maxLength by truncating
yielded content when necessary and stop/abort if responseValidation returns an
abort rule (ensure validateResponse/responseValidation is invoked after each
chunk or at least on the final accumulated response), apply truncation to
accumulatedContent used for span finalization and memory storage, and ensure any
abort reason from validateResponse is thrown so spans are finalized and no
unvalidated content is stored (refer to function/method names validateResponse,
stream(), accumulatedContent, responseValidation, and the "stream:complete"
emission).

Comment thread src/lib/types/stream.ts
Comment on lines +552 to +600
/** PII detection — scans and optionally redacts PII from input before the LLM call. */
piiDetection?: {
enabled?: boolean;
action?: "redact" | "abort" | "warn";
detectTypes?: Array<
| "email"
| "phone"
| "ssn"
| "creditCard"
| "ipAddress"
| "address"
| "name"
| "dateOfBirth"
| "passport"
| "driversLicense"
>;
customPatterns?: RegExp[];
allowList?: string[];
redactionText?: string;
};

/** Response validation — validates accumulated stream content after completion. */
responseValidation?: {
minLength?: number;
maxLength?: number;
requiredPhrases?: string[];
forbiddenPhrases?: string[];
jsonSchema?: Record<string, unknown>;
customValidator?: (text: string) => {
category: string;
severity: "error" | "warning" | "info";
message: string;
} | null;
truncationAction?: "abort" | "retry" | "truncate" | "warn";
truncationSuffix?: string;
retryOnFailure?: boolean;
maxRetries?: number;
};

/** Input validation — validates input text before any processing. */
inputValidation?: {
trimWhitespace?: boolean;
minLength?: number;
maxLength?: number;
requireContent?: boolean;
};

/** @deprecated Use `piiDetection`, `responseValidation`, and `inputValidation` instead. */
processors?: ProcessorPipelineConfig;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Reference the shared configs in ioProcessor.ts instead of redeclaring them inline.

These three blocks re-declare the shape of the new safety configs rather than referencing PiiDetectionConfig / ResponseValidationConfig (and a future InputValidationConfig) from ./ioProcessor.js, which has already caused drift:

  • Line 555: action?: "redact" | "abort" | "warn" is optional here, but PiiDetectionConfig.action in ioProcessor.ts (line 28) is required. A caller passing piiDetection: { enabled: true } type-checks against StreamOptions but would be invalid against PiiDetectionConfig; in the runtime (piiDetector.ts destructures action with no default) it silently falls through the warn branch.
  • Lines 580-584: customValidator return type is inlined and drops the optional field?: string present in ValidationIssue, so callers can't return an issue with a field path that the validator actually supports.
  • PiiType is redeclared as an inline string-literal union (lines 556-567), so any future PII type added in ioProcessor.ts has to be hand-synced here.

Pulling the canonical types in makes the public surface self-consistent and CLI-side option mapping stays aligned.

♻️ Proposed change
-import type { ProcessorPipelineConfig } from "./ioProcessor.js";
+import type {
+  InputValidationConfig,
+  PiiDetectionConfig,
+  ProcessorPipelineConfig,
+  ResponseValidationConfig,
+} from "./ioProcessor.js";
-  /** PII detection — scans and optionally redacts PII from input before the LLM call. */
-  piiDetection?: {
-    enabled?: boolean;
-    action?: "redact" | "abort" | "warn";
-    detectTypes?: Array<
-      | "email"
-
-      | "driversLicense"
-    >;
-    customPatterns?: RegExp[];
-    allowList?: string[];
-    redactionText?: string;
-  };
-
-  /** Response validation — validates accumulated stream content after completion. */
-  responseValidation?: {
-    minLength?: number;
-
-    maxRetries?: number;
-  };
-
-  /** Input validation — validates input text before any processing. */
-  inputValidation?: {
-    trimWhitespace?: boolean;
-    minLength?: number;
-    maxLength?: number;
-    requireContent?: boolean;
-  };
+  /** PII detection — scans and optionally redacts PII from input before the LLM call. */
+  piiDetection?: PiiDetectionConfig;
+
+  /** Response validation — validates accumulated stream content after completion. */
+  responseValidation?: ResponseValidationConfig;
+
+  /** Input validation — validates input text before any processing. */
+  inputValidation?: InputValidationConfig;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/types/stream.ts` around lines 552 - 600, Replace the inline safety
config declarations in StreamOptions with the canonical types from
ioProcessor.ts: import and use PiiDetectionConfig for piiDetection,
ResponseValidationConfig for responseValidation, and (add or import)
InputValidationConfig for inputValidation so the public types stay consistent
with runtime; ensure you stop redeclaring the PII type union, required
PiiDetectionConfig.action (not optional), and the ValidationIssue shape (include
optional field?: string) used by customValidator, leaving processors?:
ProcessorPipelineConfig as deprecated.

Comment on lines +99 to +128
function scanField(
fieldText: string,
fieldName: string,
type: PiiType | "custom",
pattern: RegExp,
): DetectedPII[] {
// Reset lastIndex to avoid cross-call state leaking between scans.
// We mutate the pattern's lastIndex directly rather than constructing a new
// RegExp from .source/.flags (which CodeQL flags as regex injection).
pattern.lastIndex = 0;
const regex = pattern;
const results: DetectedPII[] = [];

let match = regex.exec(fieldText);
while (match !== null) {
results.push({
type,
value: match[0],
position: { start: match.index, end: match.index + match[0].length },
field: fieldName,
});
// Guard against zero-width matches causing infinite loops
if (match[0].length === 0) {
regex.lastIndex++;
}
match = regex.exec(fieldText);
}

return results;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm PII_PATTERNS are module-level singletons and scanField is the only
# consumer reaching into their lastIndex.
rg -nP --type=ts -C2 '\b(PII_PATTERNS|scanField)\b' src/lib/utils/piiDetector.ts
# Look for any callers that could issue detectAndRedactPII concurrently.
rg -nP --type=ts -C2 '\bdetectAndRedactPII\s*\(' src

Repository: juspay/neurolink

Length of output: 1973


🏁 Script executed:

# Get the full validateCustomPattern function and surrounding context
sed -n '81,89p' src/lib/utils/piiDetector.ts
# Get the full detectAndRedactPII function to check async/concurrency
sed -n '169,230p' src/lib/utils/piiDetector.ts

Repository: juspay/neurolink

Length of output: 2067


🏁 Script executed:

# Get the full scanField function
sed -n '99,128p' src/lib/utils/piiDetector.ts
# Check for concurrent usage patterns - look for Promise.all, concurrent awaits
rg -nP --type=ts 'Promise\.all|Promise\.allSettled' src/lib/neurolink.ts | head -20
# Also check the exact signatures and usage in neurolink.ts around those detectAndRedactPII calls
sed -n '3630,3650p' src/lib/neurolink.ts
sed -n '6430,6450p' src/lib/neurolink.ts

Repository: juspay/neurolink

Length of output: 2736


🏁 Script executed:

# Check if there are other places PII_PATTERNS is accessed or if scanField is the only consumer
rg -nP --type=ts '\bPII_PATTERNS\b' src/lib/utils/piiDetector.ts
# Verify that matchAll would work with the existing validated patterns
# Check TypeScript target version to ensure matchAll is supported
grep -i "target\|lib" tsconfig.json | head -20
# Check if there are any integration tests showing concurrent usage
fd -e test.ts -e spec.ts src/lib/utils | head -5

Repository: juspay/neurolink

Length of output: 592


🏁 Script executed:

# Check the actual TypeScript target
cat tsconfig.json | grep -A5 -B5 '"target"'
# Verify the concurrency risk by checking how the SDK is meant to be used
# Look at the main entry points and if they can handle concurrent requests
grep -nP 'export.*function|export.*class' src/lib/neurolink.ts | head -20
# Check if detectAndRedactPII is exported or publicly used
rg -nP 'export.*detectAndRedactPII' src

Repository: juspay/neurolink

Length of output: 246


🏁 Script executed:

# Read the full tsconfig to check target
cat tsconfig.json

Repository: juspay/neurolink

Length of output: 736


🏁 Script executed:

# Verify the infinite loop risk by checking what happens with non-global patterns
# Create a simple test to confirm the behavior
python3 << 'EOF'
import re

# Simulate the current scanField logic with a non-global pattern
pattern = re.compile(r'\bSECRET\b')  # No 'g' flag equivalent
fieldText = "SECRET found here and ANOTHER SECRET"

# In JavaScript, regex.exec() with non-global patterns always starts at index 0
# Let's check what the equivalent would be
print("Non-global pattern test:")
print(f"Pattern: {pattern.pattern}")
print(f"Flags: {pattern.flags if hasattr(pattern, 'flags') else 'N/A (Python regex)'}")
print()

# In JS, this would infinite loop:
# let match = regex.exec(fieldText);  // Returns match at index 0
# while (match !== null) {  // Always true if pattern doesn't have /g
#   ...
#   match = regex.exec(fieldText);  // Returns same match at index 0 again
# }
print("In JavaScript, exec() on /pattern/ (no /g/) always returns first match:")
print("- lastIndex is ignored for non-global patterns")
print("- exec() always returns match at position 0")
print("- while(match !== null) never exits → infinite loop ✓")
EOF
# Also verify matchAll requirement
python3 << 'EOF'
print("\nString.prototype.matchAll behavior:")
print("- Requires the global flag (/g)")
print("- Throws TypeError if pattern lacks /g/")
print("- Returns iterator (no state mutation across calls)")
print("✓ Solves both concurrency and non-global pattern issues")
EOF

Repository: juspay/neurolink

Length of output: 549


Critical: Concurrency race on shared lastIndex and infinite-loop risk with non-global custom patterns.

The code has two real runtime bugs:

  1. Concurrency race: PII_PATTERNS (line 22) is a module-level singleton. When two concurrent SDK requests call detectAndRedactPII (which is exported and expected to handle multi-client scenarios), both mutate pattern.lastIndex on the same RegExp instances. The per-call reset at line 108 cannot prevent races; the other request can advance lastIndex mid-iteration, causing missed or duplicated matches.

  2. Infinite loop with non-global patterns: validateCustomPattern (lines 81-89) does not check for the g flag. If a consumer passes customPatterns: [/\bSECRET\b/] (no g), then regex.exec(fieldText) always returns the first match with lastIndex stuck at 0, so the while (match !== null) loop at lines 112–125 never terminates. The try/catch at line 215 cannot save it because nothing throws.

Both are fixed by using String.prototype.matchAll, which requires g and never mutates the shared regex:

Suggested fix
function validateCustomPattern(pattern: unknown): string | null {
  if (!(pattern instanceof RegExp)) {
    return "Custom pattern is not a RegExp instance";
  }
+ if (!pattern.global) {
+   return `Custom pattern /${pattern.source}/${pattern.flags} must have the 'g' flag`;
+ }
  if (hasCatastrophicBacktracking(pattern)) {
    return `Custom pattern /${pattern.source}/ contains nested quantifiers that may cause catastrophic backtracking`;
  }
  return null;
}
function scanField(
  fieldText: string,
  fieldName: string,
  type: PiiType | "custom",
  pattern: RegExp,
): DetectedPII[] {
- pattern.lastIndex = 0;
- const regex = pattern;
  const results: DetectedPII[] = [];
- let match = regex.exec(fieldText);
- while (match !== null) {
+ for (const match of fieldText.matchAll(pattern)) {
    results.push({
      type,
      value: match[0],
      position: { start: match.index, end: match.index + match[0].length },
      field: fieldName,
    });
-   if (match[0].length === 0) {
-     regex.lastIndex++;
-   }
-   match = regex.exec(fieldText);
  }
  return results;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/piiDetector.ts` around lines 99 - 128, scanField currently
mutates shared RegExp.lastIndex and loops with regex.exec, causing race
conditions when PII_PATTERNS is reused across concurrent detectAndRedactPII
calls and risking infinite loops for non-global custom patterns; fix by
validating/forcing the global flag in validateCustomPattern (reject or clone
patterns without 'g') and rewrite scanField to use String.prototype.matchAll on
a cloned RegExp (new RegExp(pattern.source, pattern.flags.includes('g') ?
pattern.flags : pattern.flags + 'g')) or otherwise ensure a fresh global RegExp
per call so no shared lastIndex is mutated and match iteration cannot hang.

Comment on lines +196 to +210
const allDetected: DetectedPII[] = [];

// Scan built-in PII types (Bug C3: scan the field in isolation)
for (const piiType of detectTypes) {
if (allowList.includes(piiType)) {
continue;
}
const pattern = PII_PATTERNS[piiType];
if (!pattern) {
continue;
}

const hits = scanField(text, "text", piiType, pattern);
allDetected.push(...hits);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

allowList semantics are surprising — document or rename.

At line 200, allowList.includes(piiType) treats each entry as a PII type name (e.g. "email", "phone") to skip entirely, not as literal values to leave unredacted. That's the opposite of what "allowList" usually means in a PII context (e.g., "don't redact support@company.com"), and nothing in the PiiDetectionConfig JSDoc in ioProcessor.ts disambiguates.

If the current behavior is intentional, rename the field (skipTypes / disabledTypes) or at least add a JSDoc on PiiDetectionConfig.allowList clarifying that values must be PiiType strings, not literal text. Otherwise, adding value-level allow-listing is the more common and useful behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/piiDetector.ts` around lines 196 - 210, The code treats
allowList as a list of PII type names to skip (e.g., "email"), which is
misleading; rename the configuration property and references to make intent
explicit: change PiiDetectionConfig.allowList to disabledTypes (or skipTypes)
and update this file to check disabledTypes.includes(piiType) instead of
allowList; update the JSDoc in PiiDetectionConfig (ioProcessor.ts) and any other
usages of allowList accordingly so the semantics are clear across scanField,
PII_PATTERNS, and detectTypes.

Comment on lines +262 to +371
// -------------------------------------------------------------------------
// 2. Maximum length — applies truncation or records the issue
// -------------------------------------------------------------------------
if (config.maxLength !== undefined && text.length > config.maxLength) {
const action = config.truncationAction ?? "truncate";

switch (action) {
case "truncate": {
// Bug C6 fix: ensure keepLength cannot go negative
const rawSuffix = config.truncationSuffix ?? "...";
const suffix = rawSuffix.slice(0, config.maxLength);
const keepLength = Math.max(config.maxLength - suffix.length, 0);
text = text.slice(0, keepLength) + suffix;
issues.push({
category: "length",
severity: "info",
message: `Response was truncated from ${responseText.length} to ${config.maxLength} characters`,
});
if (logger.shouldLog("debug")) {
logger.debug(
`[responseValidator] truncated response to ${config.maxLength} chars`,
);
}
break;
}

case "abort": {
issues.push({
category: "length",
severity: "error",
message: `Response length ${text.length} exceeds maximum of ${config.maxLength}; aborting`,
});
return {
text,
action: "abort",
issues,
feedback: `Response exceeded maximum length of ${config.maxLength} characters.`,
retryCount,
};
}

case "retry": {
issues.push({
category: "length",
severity: "error",
message: `Response length ${text.length} exceeds maximum of ${config.maxLength}; requesting retry`,
});
return {
text,
action: "retry",
issues,
feedback: "Response exceeded max length, please shorten",
retryCount,
};
}

case "warn":
default: {
issues.push({
category: "length",
severity: "warning",
message: `Response length ${text.length} exceeds maximum of ${config.maxLength} (warn-only)`,
});
logger.warn(
`[responseValidator] response length ${text.length} exceeds maxLength ${config.maxLength}`,
);
break;
}
}
}

// -------------------------------------------------------------------------
// 3. Required phrases
// -------------------------------------------------------------------------
if (config.requiredPhrases && config.requiredPhrases.length > 0) {
const lowerText = text.toLowerCase();
for (const phrase of config.requiredPhrases) {
if (!lowerText.includes(phrase.toLowerCase())) {
issues.push({
category: "phrase",
severity: "error",
message: `Required phrase "${phrase}" was not found in the response`,
});
}
}
}

// -------------------------------------------------------------------------
// 4. Forbidden phrases
// -------------------------------------------------------------------------
if (config.forbiddenPhrases && config.forbiddenPhrases.length > 0) {
const lowerText = text.toLowerCase();
for (const phrase of config.forbiddenPhrases) {
if (lowerText.includes(phrase.toLowerCase())) {
issues.push({
category: "phrase",
severity: "error",
message: `Forbidden phrase "${phrase}" was found in the response`,
});
}
}
}

// -------------------------------------------------------------------------
// 5. JSON schema
// -------------------------------------------------------------------------
if (config.jsonSchema) {
const schemaIssues = validateJsonSchema(text, config.jsonSchema);
issues.push(...schemaIssues);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Required/forbidden phrase and JSON-schema checks run against the post-truncation text.

When truncationAction === "truncate" (the default) at lines 269-286, text is replaced with the truncated string before the downstream checks. That leads to surprising outcomes:

  • A required phrase that legitimately existed past maxLength gets reported as missing → "error" issue → request aborts or retries needlessly.
  • A forbidden phrase sitting past maxLength silently disappears from the check, so the guardrail misses it.
  • jsonSchema validation on truncated text almost always fails with "Response is not valid JSON", even if the untruncated response was valid JSON.

Consider running the phrase / JSON-schema checks against responseText (the original), and only use text for the length/return payload. At minimum, the behavior deserves a comment so callers know to avoid combining truncate with requiredPhrases / jsonSchema.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/responseValidator.ts` around lines 262 - 371, The truncation
branch (when config.truncationAction === "truncate") mutates text before later
checks so required/forbidden phrase checks and validateJsonSchema run against
the truncated text; change the implementation to preserve the original
responseText for downstream validations by running phrase checks
(config.requiredPhrases, config.forbiddenPhrases) and JSON schema validation
(config.jsonSchema via validateJsonSchema) against responseText (or a copied
original variable) while using text only for length handling and the returned
payload, and add a short comment by the truncation logic (truncationAction /
text / responseText / validateJsonSchema) clarifying that truncation does not
affect phrase/schema checks.

Comment on lines +85 to +164
for (const tripwire of this.tripwires) {
let triggered: boolean;
try {
triggered = tripwire.condition(data);
} catch {
// Skip tripwires whose condition throws — don't let evaluation errors
// block normal operation.
continue;
}

if (!triggered) {
continue;
}

const message =
typeof tripwire.message === "function"
? tripwire.message(data)
: tripwire.message;

const result: TripwireResult = {
triggered: true,
tripwire,
message,
action: tripwire.action,
};

if (tripwire.action === "abort" && firstAbort === null) {
firstAbort = result;
} else if (tripwire.action === "warn" && firstWarn === null) {
firstWarn = result;
} else if (tripwire.action === "log" && firstLog === null) {
firstLog = result;
}
}

if (firstAbort !== null) {
return firstAbort;
}
if (firstWarn !== null) {
return firstWarn;
}
if (firstLog !== null) {
return firstLog;
}
return { triggered: false };
}

/**
* Evaluate all tripwires and return every triggered result.
*/
evaluateAll(data: TripwireData): TripwireResult[] {
const results: TripwireResult[] = [];

for (const tripwire of this.tripwires) {
let triggered: boolean;
try {
triggered = tripwire.condition(data);
} catch {
continue;
}

if (!triggered) {
continue;
}

const message =
typeof tripwire.message === "function"
? tripwire.message(data)
: tripwire.message;

results.push({
triggered: true,
tripwire,
message,
action: tripwire.action,
});
}

return results;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Log swallowed tripwire-condition errors, and fall back to tripwire.name for the message.

Two small gaps in both evaluate and evaluateAll:

  1. Lines 89-93 and 142-144 swallow any error thrown by tripwire.condition() with no log. A buggy user-supplied condition is then invisible — the tripwire just never triggers, with no signal that anything is wrong. A logger.warn (wrapped with logger.shouldLog("warn") if you want to stay consistent with the rest of the codebase) including the tripwire id would make this debuggable without altering behavior.
  2. Lines 99-102 / 150-153 leave message as undefined when tripwire.message isn't provided. Since TripwireConfig.name is required, falling back to name (or description) gives callers a usable string without forcing every custom tripwire to duplicate it.
♻️ Proposed change
-      let triggered: boolean;
-      try {
-        triggered = tripwire.condition(data);
-      } catch {
-        // Skip tripwires whose condition throws — don't let evaluation errors
-        // block normal operation.
-        continue;
-      }
+      let triggered: boolean;
+      try {
+        triggered = tripwire.condition(data);
+      } catch (err) {
+        if (logger.shouldLog("warn")) {
+          logger.warn(
+            `tripwire ${tripwire.id} threw during condition check`,
+            err,
+          );
+        }
+        continue;
+      }
@@
-      const message =
-        typeof tripwire.message === "function"
-          ? tripwire.message(data)
-          : tripwire.message;
+      const message =
+        typeof tripwire.message === "function"
+          ? tripwire.message(data)
+          : (tripwire.message ?? tripwire.name);

(and import logger from ./logger.js.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/tripwireEvaluator.ts` around lines 85 - 164, In evaluate and
evaluateAll, don’t silently swallow errors from tripwire.condition: catch the
error, and if logger.shouldLog("warn") then logger.warn a short message
including the tripwire.id (and the error) so broken user-provided conditions are
visible while preserving behavior; also ensure you import logger from
./logger.js. Additionally, when computing message (in both evaluate and
evaluateAll), if tripwire.message is undefined fall back to tripwire.name (or
description if you prefer) so triggered results always include a usable message.

…erate/stream

Integrate PII detection, response validation, input validation, and tripwire
evaluation directly into generate() and stream() as first-class options instead
of a separate I/O processor pipeline.

New SDK options:
- piiDetection: { enabled, action, detectTypes, customPatterns, allowList }
- responseValidation: { minLength, maxLength, truncationAction, jsonSchema, retryOnFailure }
- inputValidation: { trimWhitespace, minLength, maxLength, requireContent }

New CLI flags:
- --pii-redact, --pii-types, --pii-action
- --output-max-length, --output-min-length
- --input-max-length, --trim-whitespace, --require-content

New standalone utilities (src/lib/utils/):
- piiDetector.ts — 10 built-in PII regex patterns, per-field detection, safe custom regex handling
- responseValidator.ts — length, phrases, JSON schema, truncation, retry-with-feedback
- tripwireEvaluator.ts — 7 runtime health checks (empty response, max tokens, repetition, latency)

Removed the parallel I/O processor pipeline system (pipeline, registry, presets,
10 processors — 4 stubs, 3 duplicates of existing subsystems, 1 low-quality heuristic).
Kept only the 2 processors that added genuine new capability as extracted utilities.
@murdore murdore force-pushed the feat/io-processors branch from a9617bc to b3dec87 Compare April 28, 2026 06:53
@murdore

murdore commented Apr 28, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/lib/neurolink.ts (2)

4215-4230: ⚠️ Potential issue | 🟠 Major

Validate the final response before success-side effects, and handle retry.

At Line 4215 this method emits generation:end / response:end with the raw provider output before Line 4277 validates it. If validation aborts, observers still see a successful completion with unvalidated content. Also, validateResponse() can return action: "retry", but this branch currently treats that as success. Move validation ahead of the emits and map retry to an actual retry/failure path.

Also applies to: 4277-4303

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 4215 - 4230, Move the post-processing
validation ahead of the success emits: call validateResponse(...) on textResult
before emitting "generation:end" and "response:end" so observers only see
validated outputs; if validateResponse(...) returns action: "retry" treat it as
a retry/failure (e.g., throw/return a retry signal or trigger the existing retry
path) instead of proceeding to success emits. Update the block around the
emitter.emit("generation:end", ...) and emitter.emit("response:end", ...) to
perform validation first using the same inputs (textResult, options,
textOptions), map the "retry" branch to your retry mechanism, and only emit
generation:end/response:end when validation indicates success. Ensure the
pipelineAHandled flag is preserved when emitting after validation.

3867-3873: ⚠️ Potential issue | 🔴 Critical

Do not persist the pre-redaction prompt.

originalPrompt is captured before Line 3925 / Line 7099 sanitize options.input.text, but Line 4403 and Line 8157 later store that pre-redaction value into memory. With piiDetection.action === "redact", raw PII still gets written to conversation history/background memory, which defeats the privacy control.

Also applies to: 3925-3942, 4403-4405, 7031-7035, 7099-7115, 8154-8158

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 3867 - 3873, originalPrompt is captured
before input sanitization so when piiDetection.action === "redact" the raw PII
can be persisted; change the flow so the value persisted to memory/background is
taken from the sanitized input (i.e., after options.input.text has been
sanitized) rather than the pre-redaction originalPrompt returned by
this._extractOriginalPrompt. Concretely, stop using the early originalPrompt
variable for any memory/storage calls and instead derive the stored prompt from
the sanitized options.input.text (or call _extractOriginalPrompt after
sanitization), and update all places that write originalPrompt to
memory/background (the spots referencing originalPrompt later in this file) to
use the post-redaction value. Ensure the same change is applied consistently to
the other affected blocks mentioned (around the 3925–3942, 4403–4405, 7031–7035,
7099–7115, 8154–8158 regions).
♻️ Duplicate comments (8)
src/lib/neurolink.ts (2)

3902-3942: ⚠️ Potential issue | 🟡 Minor

Return typed errors from the new safety guardrails.

These branches still throw new Error(...), so callers cannot reliably distinguish input-validation, PII, and response-validation failures from provider/runtime failures. Please route them through the existing typed error path (ErrorFactory / NeuroLinkError) as well.

Based on learnings, "Use ErrorFactory for typed errors in the codebase".

Also applies to: 4277-4297, 7076-7115

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 3902 - 3942, The input-validation and PII
branches currently throw generic Errors (inside the options.inputValidation
block and after detectAndRedactPII) which prevents callers from distinguishing
guardrail failures; replace those throw new Error(...) usages with the project’s
typed error path by using ErrorFactory (or constructing a NeuroLinkError via
ErrorFactory.create) and throw that typed error instead, ensuring messages and
any metadata (e.g., type: "InputValidation" or "PIIDetected", relevant fields
like minLength/maxLength or piiResult.feedback) are preserved; update the same
pattern for the other listed regions (around detectAndRedactPII usage and
response-validation areas referenced) so all guardrail failures use
ErrorFactory/NeuroLinkError rather than plain Error.

7076-7115: ⚠️ Potential issue | 🟠 Major

stream() still never applies responseValidation.

This new block validates only the input. Downstream, streamed chunks are yielded, stream:complete is emitted, and memory is stored without ever calling validateResponse(), so maxLength, truncationAction, and retryOnFailure remain no-ops for stream().

Also applies to: 7259-7377, 7531-7538

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 7076 - 7115, The stream() path currently
only runs input validation and PII redaction but never calls validateResponse(),
so responseValidation options (maxLength, truncationAction, retryOnFailure) are
ignored; update the stream() implementation to accumulate streamed chunks into a
final response string, then before emitting the final "stream:complete" event
and before storing memory call validateResponse(finalText,
options.responseValidation) (or the existing validateResponse function signature
used elsewhere), and implement the same behaviors used by non-streaming code:
enforce maxLength (truncate or throw per truncationAction), honor retryOnFailure
by triggering a retry if validation fails and retries remain, and propagate or
surface validation errors consistently; reference the stream() function,
validateResponse(), and the code locations that emit "stream:complete" and
perform memory storage so you insert the validation step there.
src/lib/utils/piiDetector.ts (2)

81-89: ⚠️ Potential issue | 🟠 Major

Missing global-flag validation risks infinite loop.

If a consumer passes a custom pattern without the g flag (e.g., /\bSECRET\b/), the exec() loop in scanField will return the same match infinitely because lastIndex never advances for non-global patterns.

Add a check for pattern.global:

 function validateCustomPattern(pattern: unknown): string | null {
   if (!(pattern instanceof RegExp)) {
     return "Custom pattern is not a RegExp instance";
   }
+  if (!pattern.global) {
+    return `Custom pattern /${pattern.source}/${pattern.flags} must have the 'g' flag`;
+  }
   if (hasCatastrophicBacktracking(pattern)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/piiDetector.ts` around lines 81 - 89, validateCustomPattern
currently only checks instance type and catastrophic backtracking; add a check
that the RegExp has the global flag to prevent infinite loops in scanField's
exec() loop. Update validateCustomPattern to return an error string when
!pattern.global (e.g., "Custom pattern must include the 'g' flag") and reference
validateCustomPattern and scanField so reviewers see that scanField relies on a
global RegExp to advance lastIndex.

99-128: ⚠️ Potential issue | 🔴 Critical

Concurrency race on shared RegExp.lastIndex.

PII_PATTERNS is a module-level singleton. When two concurrent detectAndRedactPII calls execute, both mutate the same RegExp.lastIndex properties via scanField. The reset at line 108 cannot prevent races — another call can advance lastIndex mid-iteration, causing missed or duplicated matches.

The safest fix is to use String.prototype.matchAll, which:

  • Requires the g flag (also fixing the infinite-loop risk)
  • Returns an iterator without mutating the regex
Suggested fix
 function scanField(
   fieldText: string,
   fieldName: string,
   type: PiiType | "custom",
   pattern: RegExp,
 ): DetectedPII[] {
-  pattern.lastIndex = 0;
-  const regex = pattern;
   const results: DetectedPII[] = [];
-
-  let match = regex.exec(fieldText);
-  while (match !== null) {
+  for (const match of fieldText.matchAll(pattern)) {
     results.push({
       type,
       value: match[0],
-      position: { start: match.index, end: match.index + match[0].length },
+      position: { start: match.index!, end: match.index! + match[0].length },
       field: fieldName,
     });
-    if (match[0].length === 0) {
-      regex.lastIndex++;
-    }
-    match = regex.exec(fieldText);
   }
-
   return results;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/piiDetector.ts` around lines 99 - 128, scanField currently
mutates and relies on RegExp.lastIndex allowing races; change it to use
String.prototype.matchAll instead: ensure the incoming pattern has the global
"g" flag (or create a new RegExp from pattern.source + "g" if needed), stop
resetting or touching pattern.lastIndex, iterate over
fieldText.matchAll(pattern) and build DetectedPII entries from each match (use
match[0] and match.index for value and position), and remove the
zero-width-match lastIndex++ guard. Update references in scanField and keep
PiiType/"custom" and DetectedPII shape unchanged.
src/lib/types/stream.ts (1)

574-622: 🛠️ Refactor suggestion | 🟠 Major

Inline safety configs drift from canonical types in ioProcessor.ts.

These blocks redeclare the config shapes instead of referencing PiiDetectionConfig, ResponseValidationConfig, and a future InputValidationConfig from ./ioProcessor.js. This causes several inconsistencies:

  1. Line 577: action?: "redact" | "abort" | "warn" is optional, but PiiDetectionConfig.action (ioProcessor.ts:28) is required. A caller passing { enabled: true } type-checks here but fails at runtime since piiDetector.ts destructures action with no default.

  2. Lines 602-606: customValidator return type is missing field?: string present in ValidationIssue, so callers can't return field-scoped issues.

  3. Lines 578-589: PiiType is redeclared inline; future additions to PiiType must be synced manually.

Reference the canonical types to keep the public surface consistent:

-import type { ProcessorPipelineConfig } from "./ioProcessor.js";
+import type {
+  InputValidationConfig,
+  PiiDetectionConfig,
+  ProcessorPipelineConfig,
+  ResponseValidationConfig,
+} from "./ioProcessor.js";
...
-  piiDetection?: { enabled?: boolean; action?: ... };
+  piiDetection?: PiiDetectionConfig;
-  responseValidation?: { minLength?: ... };
+  responseValidation?: ResponseValidationConfig;
-  inputValidation?: { trimWhitespace?: ... };
+  inputValidation?: InputValidationConfig;

Note: You'll need to add InputValidationConfig to ioProcessor.ts first.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/types/stream.ts` around lines 574 - 622, The inline safety config
types in stream.ts diverge from the canonical types in ioProcessor.ts—replace
the inline declarations with imported types (PiiDetectionConfig,
ResponseValidationConfig, and the new InputValidationConfig you must add to
ioProcessor.ts) so the public API matches the canonical shapes; ensure
piiDetection.action uses the same required/optional signature as
PiiDetectionConfig (fix optional vs required mismatch), make the customValidator
return type include the optional field?: string to match ValidationIssue, and
reuse the shared PiiType (do not redeclare the union) so future changes remain
consistent (update the processors?: ProcessorPipelineConfig deprecation comment
to still reference the canonical types if needed).
src/lib/types/generate.ts (1)

543-600: 🛠️ Refactor suggestion | 🟠 Major

Same type drift issue as StreamOptions.

The inline declarations for piiDetection, responseValidation, and inputValidation duplicate the shapes from ioProcessor.ts with the same inconsistencies:

  • action is optional (should be required per PiiDetectionConfig)
  • customValidator return type missing field?: string
  • PiiType literal union manually duplicated

Reference the canonical types from ./ioProcessor.js as suggested for stream.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/types/generate.ts` around lines 543 - 600, The inline option shapes
for piiDetection, responseValidation, and inputValidation are drifting from the
canonical definitions in ioProcessor; replace these inline types by importing
and using the canonical types (e.g. PiiDetectionConfig,
ResponseValidationConfig, InputValidationConfig or the exact exported names)
from ./ioProcessor.ts, ensure PiiDetectionConfig makes action required (not
optional), update the customValidator return type to include the optional
field?: string, and reuse the exported PiiType literal union instead of
duplicating it locally so the signatures for piiDetection, responseValidation,
and inputValidation stay consistent with ioProcessor.
src/cli/factories/commandFactory.ts (2)

487-536: ⚠️ Potential issue | 🟠 Major

Scope safety flags to commands that actually apply them.

Line 487 adds safety flags to commonOptions, so commands like batch, status, setup, and loop accept them even when those handlers don’t forward safety config. This creates a false safety signal (e.g., user passes --pii-redact and expects sanitization). Move these options to generate/stream-specific option sets (or wire them through every command that exposes them).

Suggested direction
+  private static readonly safetyOptions = {
+    piiRedact: { /* existing definition */ },
+    piiTypes: { /* existing definition */ },
+    piiAction: { /* existing definition */ },
+    inputMaxLength: { /* existing definition */ },
+    trimWhitespace: { /* existing definition */ },
+    requireContent: { /* existing definition */ },
+    outputMaxLength: { /* existing definition */ },
+    outputMinLength: { /* existing definition */ },
+  };

   private static readonly commonOptions = {
     // ...core/shared options only
-    piiRedact: { ... },
-    piiTypes: { ... },
-    piiAction: { ... },
-    inputMaxLength: { ... },
-    trimWhitespace: { ... },
-    requireContent: { ... },
-    outputMaxLength: { ... },
-    outputMinLength: { ... },
   };
- return CLICommandFactory.buildOptions(yargs ...);
+ return CLICommandFactory.buildOptions(yargs ..., CLICommandFactory.safetyOptions);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/factories/commandFactory.ts` around lines 487 - 536, The safety flags
(piiRedact, piiTypes, piiAction, inputMaxLength, trimWhitespace, requireContent,
outputMaxLength, outputMinLength) were added to commonOptions causing irrelevant
commands (batch, status, setup, loop) to advertise them; move these option
definitions out of commonOptions and into the option sets actually used by
generation/streaming commands (e.g., generate and stream option objects) or
explicitly wire the safety config through each command handler that supports it;
update references in commandFactory.ts to remove the safety keys from
commonOptions and add them to the specific option objects or handlers so only
commands that forward/handle safety behavior accept those flags.

2716-2736: ⚠️ Potential issue | 🟠 Major

Validate --pii-action and --pii-types before type-casting.

Line 2720 / Line 3042 cast raw CLI strings directly, and Line 2721-2735 / Line 3043-3057 cast comma-split tokens without allowlist checks. Invalid values can silently propagate to SDK requests. Add strict runtime parsing/validation once and reuse for both generate and stream paths.

Proposed shared parser pattern
+const ALLOWED_PII_ACTIONS = new Set(["redact", "abort", "warn"] as const);
+const ALLOWED_PII_TYPES = new Set([
+  "email",
+  "phone",
+  "ssn",
+  "creditCard",
+  "ipAddress",
+  "address",
+  "name",
+  "dateOfBirth",
+  "passport",
+  "driversLicense",
+] as const);
+
+private static parsePiiDetection(argv: Record<string, unknown>) {
+  if (!argv.piiRedact) return undefined;
+
+  const actionRaw = String(argv.piiAction ?? "redact");
+  if (!ALLOWED_PII_ACTIONS.has(actionRaw as never)) {
+    throw new Error(`Invalid --pii-action: ${actionRaw}`);
+  }
+
+  const detectTypes = argv.piiTypes
+    ? String(argv.piiTypes)
+        .split(",")
+        .map((t) => t.trim())
+        .filter(Boolean)
+    : undefined;
+
+  const invalid = detectTypes?.filter((t) => !ALLOWED_PII_TYPES.has(t as never));
+  if (invalid?.length) {
+    throw new Error(`Invalid --pii-types: ${invalid.join(", ")}`);
+  }
+
+  return {
+    enabled: true,
+    action: actionRaw,
+    detectTypes,
+  };
+}

Then use CLICommandFactory.parsePiiDetection(argv) in both sdk.generate(...) and sdk.stream(...).

Also applies to: 3038-3058

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/factories/commandFactory.ts` around lines 2716 - 2736, The CLI
currently casts argv.piiAction and argv.piiTypes directly which can let invalid
values pass into SDK calls; implement a single parser method (e.g.,
CLICommandFactory.parsePiiDetection(argv)) that: validates argv.piiAction is one
of "redact" | "abort" | "warn" (default "redact" if missing), splits
argv.piiTypes, trims tokens and filters/validates them against the allowlist
["email","phone","ssn","creditCard","ipAddress","address","name","dateOfBirth","passport","driversLicense"],
and returns either undefined or a well-typed piiDetection object matching the
expected shape; then replace the inline casts where piiDetection is built
(references: piiDetection payload, sdk.generate(...), sdk.stream(...), and
usages of argv.piiAction / argv.piiTypes) to call
CLICommandFactory.parsePiiDetection(argv) so both generate and stream paths
reuse the validated result.
🧹 Nitpick comments (1)
src/lib/utils/tripwireEvaluator.ts (1)

211-246: Repetition-loop detection is reasonable but may produce false positives on prose.

The 20% duplicate-window threshold is a pragmatic heuristic. For highly structured content (lists, repeated headers), this may over-trigger. Consider exposing the threshold as a configurable parameter in a future iteration if users report false positives.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/tripwireEvaluator.ts` around lines 211 - 246, The hard-coded
0.2 duplicate-window threshold in repetitionLoopTripwire's condition can cause
false positives; make this threshold configurable by introducing a named
parameter (e.g., duplicateWindowThreshold) and read it in the condition instead
of the literal 0.2 so callers can adjust sensitivity; update the
repetitionLoopTripwire object to accept the new config (or reference a shared
config/env var) and document that condition uses duplicateWindowThreshold along
with existing symbols like windowSize, totalWindows, duplicates, and the
condition function so tests and callers can override it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/lib/neurolink.ts`:
- Around line 4215-4230: Move the post-processing validation ahead of the
success emits: call validateResponse(...) on textResult before emitting
"generation:end" and "response:end" so observers only see validated outputs; if
validateResponse(...) returns action: "retry" treat it as a retry/failure (e.g.,
throw/return a retry signal or trigger the existing retry path) instead of
proceeding to success emits. Update the block around the
emitter.emit("generation:end", ...) and emitter.emit("response:end", ...) to
perform validation first using the same inputs (textResult, options,
textOptions), map the "retry" branch to your retry mechanism, and only emit
generation:end/response:end when validation indicates success. Ensure the
pipelineAHandled flag is preserved when emitting after validation.
- Around line 3867-3873: originalPrompt is captured before input sanitization so
when piiDetection.action === "redact" the raw PII can be persisted; change the
flow so the value persisted to memory/background is taken from the sanitized
input (i.e., after options.input.text has been sanitized) rather than the
pre-redaction originalPrompt returned by this._extractOriginalPrompt.
Concretely, stop using the early originalPrompt variable for any memory/storage
calls and instead derive the stored prompt from the sanitized options.input.text
(or call _extractOriginalPrompt after sanitization), and update all places that
write originalPrompt to memory/background (the spots referencing originalPrompt
later in this file) to use the post-redaction value. Ensure the same change is
applied consistently to the other affected blocks mentioned (around the
3925–3942, 4403–4405, 7031–7035, 7099–7115, 8154–8158 regions).

---

Duplicate comments:
In `@src/cli/factories/commandFactory.ts`:
- Around line 487-536: The safety flags (piiRedact, piiTypes, piiAction,
inputMaxLength, trimWhitespace, requireContent, outputMaxLength,
outputMinLength) were added to commonOptions causing irrelevant commands (batch,
status, setup, loop) to advertise them; move these option definitions out of
commonOptions and into the option sets actually used by generation/streaming
commands (e.g., generate and stream option objects) or explicitly wire the
safety config through each command handler that supports it; update references
in commandFactory.ts to remove the safety keys from commonOptions and add them
to the specific option objects or handlers so only commands that forward/handle
safety behavior accept those flags.
- Around line 2716-2736: The CLI currently casts argv.piiAction and
argv.piiTypes directly which can let invalid values pass into SDK calls;
implement a single parser method (e.g.,
CLICommandFactory.parsePiiDetection(argv)) that: validates argv.piiAction is one
of "redact" | "abort" | "warn" (default "redact" if missing), splits
argv.piiTypes, trims tokens and filters/validates them against the allowlist
["email","phone","ssn","creditCard","ipAddress","address","name","dateOfBirth","passport","driversLicense"],
and returns either undefined or a well-typed piiDetection object matching the
expected shape; then replace the inline casts where piiDetection is built
(references: piiDetection payload, sdk.generate(...), sdk.stream(...), and
usages of argv.piiAction / argv.piiTypes) to call
CLICommandFactory.parsePiiDetection(argv) so both generate and stream paths
reuse the validated result.

In `@src/lib/neurolink.ts`:
- Around line 3902-3942: The input-validation and PII branches currently throw
generic Errors (inside the options.inputValidation block and after
detectAndRedactPII) which prevents callers from distinguishing guardrail
failures; replace those throw new Error(...) usages with the project’s typed
error path by using ErrorFactory (or constructing a NeuroLinkError via
ErrorFactory.create) and throw that typed error instead, ensuring messages and
any metadata (e.g., type: "InputValidation" or "PIIDetected", relevant fields
like minLength/maxLength or piiResult.feedback) are preserved; update the same
pattern for the other listed regions (around detectAndRedactPII usage and
response-validation areas referenced) so all guardrail failures use
ErrorFactory/NeuroLinkError rather than plain Error.
- Around line 7076-7115: The stream() path currently only runs input validation
and PII redaction but never calls validateResponse(), so responseValidation
options (maxLength, truncationAction, retryOnFailure) are ignored; update the
stream() implementation to accumulate streamed chunks into a final response
string, then before emitting the final "stream:complete" event and before
storing memory call validateResponse(finalText, options.responseValidation) (or
the existing validateResponse function signature used elsewhere), and implement
the same behaviors used by non-streaming code: enforce maxLength (truncate or
throw per truncationAction), honor retryOnFailure by triggering a retry if
validation fails and retries remain, and propagate or surface validation errors
consistently; reference the stream() function, validateResponse(), and the code
locations that emit "stream:complete" and perform memory storage so you insert
the validation step there.

In `@src/lib/types/generate.ts`:
- Around line 543-600: The inline option shapes for piiDetection,
responseValidation, and inputValidation are drifting from the canonical
definitions in ioProcessor; replace these inline types by importing and using
the canonical types (e.g. PiiDetectionConfig, ResponseValidationConfig,
InputValidationConfig or the exact exported names) from ./ioProcessor.ts, ensure
PiiDetectionConfig makes action required (not optional), update the
customValidator return type to include the optional field?: string, and reuse
the exported PiiType literal union instead of duplicating it locally so the
signatures for piiDetection, responseValidation, and inputValidation stay
consistent with ioProcessor.

In `@src/lib/types/stream.ts`:
- Around line 574-622: The inline safety config types in stream.ts diverge from
the canonical types in ioProcessor.ts—replace the inline declarations with
imported types (PiiDetectionConfig, ResponseValidationConfig, and the new
InputValidationConfig you must add to ioProcessor.ts) so the public API matches
the canonical shapes; ensure piiDetection.action uses the same required/optional
signature as PiiDetectionConfig (fix optional vs required mismatch), make the
customValidator return type include the optional field?: string to match
ValidationIssue, and reuse the shared PiiType (do not redeclare the union) so
future changes remain consistent (update the processors?:
ProcessorPipelineConfig deprecation comment to still reference the canonical
types if needed).

In `@src/lib/utils/piiDetector.ts`:
- Around line 81-89: validateCustomPattern currently only checks instance type
and catastrophic backtracking; add a check that the RegExp has the global flag
to prevent infinite loops in scanField's exec() loop. Update
validateCustomPattern to return an error string when !pattern.global (e.g.,
"Custom pattern must include the 'g' flag") and reference validateCustomPattern
and scanField so reviewers see that scanField relies on a global RegExp to
advance lastIndex.
- Around line 99-128: scanField currently mutates and relies on RegExp.lastIndex
allowing races; change it to use String.prototype.matchAll instead: ensure the
incoming pattern has the global "g" flag (or create a new RegExp from
pattern.source + "g" if needed), stop resetting or touching pattern.lastIndex,
iterate over fieldText.matchAll(pattern) and build DetectedPII entries from each
match (use match[0] and match.index for value and position), and remove the
zero-width-match lastIndex++ guard. Update references in scanField and keep
PiiType/"custom" and DetectedPII shape unchanged.

---

Nitpick comments:
In `@src/lib/utils/tripwireEvaluator.ts`:
- Around line 211-246: The hard-coded 0.2 duplicate-window threshold in
repetitionLoopTripwire's condition can cause false positives; make this
threshold configurable by introducing a named parameter (e.g.,
duplicateWindowThreshold) and read it in the condition instead of the literal
0.2 so callers can adjust sensitivity; update the repetitionLoopTripwire object
to accept the new config (or reference a shared config/env var) and document
that condition uses duplicateWindowThreshold along with existing symbols like
windowSize, totalWindows, duplicates, and the condition function so tests and
callers can override it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 18f10346-24d7-487c-957d-00a1859e07f6

📥 Commits

Reviewing files that changed from the base of the PR and between 971bce7 and b3dec87.

📒 Files selected for processing (13)
  • src/cli/factories/commandFactory.ts
  • src/cli/loop/optionsSchema.ts
  • src/lib/index.ts
  • src/lib/neurolink.ts
  • src/lib/processors/index.ts
  • src/lib/types/generate.ts
  • src/lib/types/index.ts
  • src/lib/types/ioProcessor.ts
  • src/lib/types/stream.ts
  • src/lib/utils/piiDetector.ts
  • src/lib/utils/responseValidator.ts
  • src/lib/utils/tripwireEvaluator.ts
  • test/continuous-test-suite-safety.ts

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.

3 participants