Skip to content

feat: add GLiNER2 PII detection Rust sidecar + TS client#2

Open
jamon8888 wants to merge 6 commits into
mainfrom
pr-217
Open

feat: add GLiNER2 PII detection Rust sidecar + TS client#2
jamon8888 wants to merge 6 commits into
mainfrom
pr-217

Conversation

@jamon8888

@jamon8888 jamon8888 commented Jun 27, 2026

Copy link
Copy Markdown
Owner

Summary

Implements GLiNER2 PII detection using a Rust HTTP sidecar binary with the SemplificaAI fragmented ONNX model.

Changes

Rust Sidecar (crates/gliner2-server/)

  • HTTP server with axum serving /v1/health and /v1/infer endpoints
  • Lazy Gliner2Engine singleton that auto-downloads model from HuggingFace (~530MB)
  • Port retry logic (3 attempts with random port fallback)
  • Startup JSON protocol for TS client to discover listening port
  • Uses gliner2-inference v0.5.1 crate from SemplificaAI/gliner2-rs

TypeScript Client (packages/anonymize/src/gliner2/)

  • Gliner2Client class: spawns sidecar, polls health, manages lifecycle
  • �uildGliner2Inference() factory: implements NerInferenceFn for pipeline integration
  • Label mapping: pipeline canonical labels → model labels (1:N expansion, N:1 collapse with collision resolution)
  • Excludes crypto and misc (NON_NER_LABELS) — filtered by pipeline before calling NER

Integration

  • Zero changes to pipeline.ts — uses existing NerInferenceFn contract
  • Exports from index-shared.ts: �uildGliner2Inference, Gliner2Client, types

Testing

  • Rust: cargo check --package gliner2-server ✓
  • TS label-map tests: 6/6 passing
  • Full pipeline integration requires binary (Task 6 — deferred)

Configuration

  • Binary path via ANONYMIZE_GLINER2_SERVER_PATH env var
  • Model: SemplificaAI/gliner2-privacy-filter-PII-multi (42 labels, 7 languages)
  • Cargo artifacts: E:\cargo-target\anonymize

Future Work

  • Cross-compilation CI (GitHub Actions) for pre-built binaries
  • Auto-download logic in
    esolveBinary()
  • Pipeline integration tests with running binary

Summary by Sourcery

Introduce a Rust-based GLiNER2 inference sidecar and a TypeScript client that integrates it into the anonymization pipeline via the existing NER inference contract.

New Features:

  • Add gliner2-server Rust crate exposing HTTP /v1/health and /v1/infer endpoints backed by a lazily-initialized GLiNER2 engine and HuggingFace ONNX model.
  • Add a Node/TypeScript Gliner2Client that spawns the Rust sidecar, discovers its port via a startup JSON protocol, polls health, and performs inference requests over HTTP.
  • Add a GLiNER2-based NerInferenceFn factory (buildGliner2Inference) that adapts model entities into pipeline entities, including label expansion/collapse logic between pipeline and model labels.

Enhancements:

  • Add label mapping utilities to translate between pipeline canonical PII labels and the model's label set, handling 1:N expansion and N:1 collisions.
  • Export GLiNER2 client, inference factory, and related types from the shared anonymize package index for downstream consumers.
  • Configure Cargo to place build artifacts in a dedicated E:\cargo-target\anonymize directory and document this in the agent docs.

Documentation:

  • Document the GLiNER2 Rust sidecar implementation plan and reference Cargo build artifact location in both local and shared agent documentation.

Tests:

  • Add unit tests for the label mapping logic used by the GLiNER2 integration.

Summary by CodeRabbit

  • New Features

    • Added a new local GLiNER2-powered inference service for running entity detection.
    • Introduced health and inference endpoints for checking service readiness and extracting entities.
    • Added client-side support to start the service, send inference requests, and shut it down cleanly.
    • Exposed a new inference factory that integrates the service into anonymization workflows.
  • Bug Fixes

    • Improved handling for service startup, model loading, and fallback port binding.
    • Ensured build artifacts are stored outside the default drive to reduce local disk pressure.

Your Name added 3 commits June 27, 2026 00:42
- Register crates/gliner2-server in workspace members
- Add Cargo.toml with axum, tokio, serde, clap dependencies
- Create main.rs with CLI parsing, port retry, startup JSON protocol
- Create health.rs with health endpoint
- Create types.rs with InferRequest/Response types
- Configure cargo target-dir to E:\cargo-target\anonymize
- Add ndarray + half features to ort dependency for gliner2_inference compatibility
- Use node:child_process instead of Bun APIs for portability
- Fix signal type (null instead of undefined)
- Import Entity from types.ts instead of pipeline.ts
- Fix env var access with bracket notation
@sourcery-ai

sourcery-ai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a Rust-based GLiNER2 PII HTTP sidecar and a TypeScript client/inference factory that integrate with the existing anonymize pipeline via the NerInferenceFn contract, including label-mapping logic and workspace/infra tweaks for the new crate and build artifacts.

File-Level Changes

Change Details Files
Introduce gliner2-server Rust HTTP sidecar that serves health and inference endpoints backed by a lazily initialized GLiNER2Engine loaded from HuggingFace.
  • Add new gliner2-server crate to Cargo workspace with dependencies on axum, tokio, clap, tracing, ort, and gliner2-inference v0.5.1
  • Implement CLI parsing for host/port/model/variant and startup logging via a JSON line written to stdout
  • Bind TCP listener with up to three attempts, falling back to an OS-assigned random port and emitting the chosen port in the startup JSON protocol
  • Create AppState shared via axum Router state to carry model configuration into handlers
  • Implement /v1/health handler reporting OK status and whether the model has been initialized
  • Implement /v1/infer handler that initializes/uses a OnceCell-backed singleton Gliner2Engine with HuggingFace model download and runs extraction over provided text/labels/threshold
  • Define InferRequest, EntityOutput, and InferResponse types used by the HTTP API
crates/gliner2-server/Cargo.toml
crates/gliner2-server/src/main.rs
crates/gliner2-server/src/engine.rs
crates/gliner2-server/src/health.rs
crates/gliner2-server/src/infer.rs
crates/gliner2-server/src/types.rs
Cargo.toml
Cargo.lock
Add a Node-based Gliner2Client that manages the sidecar process lifecycle, discovers its port from a startup JSON line, polls health until the model is loaded, and exposes an infer() method over HTTP.
  • Implement Gliner2ClientOptions and Gliner2Client, using node:child_process.spawn to start the sidecar with configured port/model/variant
  • Stream and decode sidecar stdout, parsing newline-delimited JSON to capture the 'listening' event and derive baseUrl/port
  • Poll /v1/health until model_loaded is true or a configurable timeout elapses
  • Implement infer() to POST InferRequest to /v1/infer, propagating AbortSignal and surfacing HTTP errors with body text
  • Implement stop()/dispose() to gracefully terminate the child process with SIGTERM, then SIGKILL on timeout, and reset client state
  • Resolve the sidecar binary path from ANONYMIZE_GLINER2_SERVER_PATH env var, failing with a clear error if missing
packages/anonymize/src/gliner2/client.ts
Implement label-mapping utilities that expand pipeline canonical labels into the model’s label space and collapse model labels back, handling collisions deterministically, with unit tests.
  • Define PIPELINE_TO_MODEL mapping for PII-related labels (person, phone number, address, email address, dates, IDs, cards, etc.) to one or more GLiNER2 model labels
  • Build a reverse MODEL_TO_PIPELINE map preferring the first registered pipeline label for each model label
  • Implement expandLabels() to turn requested pipeline labels into a deduplicated list of model labels, skipping unsupported labels
  • Implement collapseLabel() to map model labels back to canonical pipeline labels, preferring requested labels in collision scenarios and passing through unknown labels
  • Add Bun-based tests validating expansion, deduplication, collision disambiguation, default reverse mapping, and passthrough behavior
packages/anonymize/src/gliner2/label-map.ts
packages/anonymize/src/gliner2/__test__/label-map.test.ts
Provide a GLiNER2-based NerInferenceFn implementation and export surface so the existing pipeline can use the sidecar transparently.
  • Implement buildGliner2Inference() factory that creates a Gliner2Client and returns a NerInferenceFn-compatible function
  • Within the inference function, expand pipeline labels to model labels, call client.infer(), and adapt EntityOutput to pipeline Entity with source set to NER and labels collapsed via collapseLabel()
  • Export buildGliner2Inference, Gliner2Client, and associated types from the shared index to make them available to consumers without touching pipeline.ts
packages/anonymize/src/gliner2/inference.ts
packages/anonymize/src/gliner2/types.ts
packages/anonymize/src/index-shared.ts
Adjust workspace configuration and documentation to account for the new crate and custom Cargo target directory on E:.
  • Register gliner2-server as a member in the root Cargo workspace so CI and workspace-wide commands include it
  • Configure Cargo build target directory to E:\cargo-target\anonymize to keep build artifacts off the system drive
  • Update local agent documentation files to mention the Cargo target-dir location for contributors and agent tooling
Cargo.toml
.cargo/config.toml
.ai/local/agents.md
AGENTS.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jamon8888, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 29 minutes and 32 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 57333d75-2bf9-4f6f-8fd9-5121a014919e

📥 Commits

Reviewing files that changed from the base of the PR and between 696823a and 3d0f5f9.

📒 Files selected for processing (19)
  • .cargo/config.toml
  • .claude/skills/gitnexus/gitnexus-cli/SKILL.md
  • .claude/skills/gitnexus/gitnexus-debugging/SKILL.md
  • .claude/skills/gitnexus/gitnexus-exploring/SKILL.md
  • .claude/skills/gitnexus/gitnexus-guide/SKILL.md
  • .claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md
  • .claude/skills/gitnexus/gitnexus-refactoring/SKILL.md
  • crates/gliner2-inference/Cargo.toml
  • crates/gliner2-inference/src/error.rs
  • crates/gliner2-inference/src/processor.rs
  • crates/gliner2-server/src/engine.rs
  • crates/gliner2-server/src/health.rs
  • crates/gliner2-server/src/infer.rs
  • crates/gliner2-server/src/main.rs
  • docs/superpowers/plans/2026-06-26-gliner2-pii-rust-implementation.md
  • packages/anonymize/src/__test__/slow/gliner2-pipeline.test.ts
  • packages/anonymize/src/gliner2/__test__/label-map.test.ts
  • packages/anonymize/src/gliner2/client.ts
  • packages/anonymize/src/gliner2/label-map.ts
📝 Walkthrough

Walkthrough

Added a new gliner2-server Rust workspace crate with health and inference endpoints, plus TypeScript client, label mapping, and inference wiring. Updated local Cargo build output configuration, repository docs, ignore rules, and an implementation plan document.

Changes

GLiNER2 Sidecar Integration

Layer / File(s) Summary
Workspace and crate setup
Cargo.toml, crates/gliner2-server/Cargo.toml, .cargo/config.toml, .ai/local/agents.md, AGENTS.md, .gitignore
Adds the new Rust workspace member and crate manifest, routes Cargo outputs to E:\cargo-target\anonymize, ignores .gitnexus, and updates repository docs with the local build path.
Rust engine, types, and health
crates/gliner2-server/src/types.rs, crates/gliner2-server/src/engine.rs, crates/gliner2-server/src/health.rs
Defines Rust inference and health payloads, a lazily initialized Gliner2Engine, and a health response that reports model_loaded.
Rust inference route and server entrypoint
crates/gliner2-server/src/infer.rs, crates/gliner2-server/src/main.rs
Adds shared app state, the /v1/infer handler, CLI parsing, startup logging, listener retries, and Axum route wiring.
TypeScript label mapping and tests
packages/anonymize/src/gliner2/types.ts, packages/anonymize/src/gliner2/label-map.ts, packages/anonymize/src/gliner2/__test__/label-map.test.ts
Adds GLiNER2 request and response types, pipeline-to-model label mapping helpers, and Bun tests for expansion and collapse behavior.
TypeScript client and inference flow
packages/anonymize/src/gliner2/client.ts, packages/anonymize/src/gliner2/inference.ts, packages/anonymize/src/index-shared.ts
Adds the local server client, health polling, inference HTTP calls, process shutdown handling, the buildGliner2Inference() wrapper, and shared re-exports.
Implementation plan document
docs/superpowers/plans/2026-06-26-gliner2-pii-rust-implementation.md
Adds the multi-task implementation plan, the future cross-compilation follow-up, and the review checklist.

Sequence Diagram(s)

sequenceDiagram
  participant Gliner2Client
  participant gliner2ServerMain as gliner2-server main
  participant healthHandler as health_handler
  participant inferHandler as infer_handler
  participant Gliner2Engine
  Gliner2Client->>gliner2ServerMain: start()
  gliner2ServerMain-->>Gliner2Client: stdout {"event":"listening"}
  loop until model_loaded
    Gliner2Client->>healthHandler: GET /v1/health
    healthHandler-->>Gliner2Client: HealthResponse
  end
  Gliner2Client->>inferHandler: POST /v1/infer
  inferHandler->>Gliner2Engine: get_or_init(...)
  inferHandler->>Gliner2Engine: extract(...)
  Gliner2Engine-->>inferHandler: entities
  inferHandler-->>Gliner2Client: InferResponse
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I hopped through ports and watched them start,
With health checks thumping, brave and smart.
I mapped the labels, neat and new,
Then piped the inference through and through.
Now bunny burrows hum with "ok" too! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a GLiNER2 PII Rust sidecar and a TypeScript client.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-217

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.

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown

Dependency Review

The following issues were found:

  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 20 package(s) with unknown licenses.
  • ⚠️ 18 packages with OpenSSF Scorecard issues.

View full job summary

@sourcery-ai sourcery-ai 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.

Hey - I've found 3 issues, and left some high level feedback:

  • The .cargo/config.toml target-dir is hardcoded to E:\cargo-target\anonymize, which will break on CI and other developers' environments; consider using a relative path or environment-based configuration instead.
  • In Gliner2Client.infer, you're passing signal: signal ?? null to fetch; signal should be an AbortSignal | undefined, so drop the property when undefined instead of passing null to avoid relying on implicit coercion.
  • In Gliner2Client.start, if the sidecar process exits before emitting the listening JSON line, you'll fall through to the generic 'no listening event' error; it would be helpful to detect premature process exit and surface the exit code or error to make startup failures easier to debug.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `.cargo/config.toml` `target-dir` is hardcoded to `E:\cargo-target\anonymize`, which will break on CI and other developers' environments; consider using a relative path or environment-based configuration instead.
- In `Gliner2Client.infer`, you're passing `signal: signal ?? null` to `fetch`; `signal` should be an `AbortSignal | undefined`, so drop the property when undefined instead of passing `null` to avoid relying on implicit coercion.
- In `Gliner2Client.start`, if the sidecar process exits before emitting the `listening` JSON line, you'll fall through to the generic 'no listening event' error; it would be helpful to detect premature process exit and surface the exit code or error to make startup failures easier to debug.

## Individual Comments

### Comment 1
<location path=".cargo/config.toml" line_range="6-7" />
<code_context>
 protocol = "sparse"

+# Build artifacts to E: (fast NVMe, keeps C: free)
+[build]
+target-dir = "E:\\cargo-target\\anonymize"
+
 [alias]
</code_context>
<issue_to_address>
**issue (bug_risk):** Hard-coded Windows-specific target-dir is likely to break on other machines and CI environments

Using `E:\cargo-target\anonymize` assumes that drive and path exist, which will break builds on most non-Windows machines and many Windows/CI environments. Please move this to a local-only config (outside the repo) or make it configurable (e.g., via an env var) so the workspace stays portable.
</issue_to_address>

### Comment 2
<location path="packages/anonymize/src/gliner2/client.ts" line_range="154-159" />
<code_context>
+    this.stop().catch(() => {});
+  }
+
+  private async resolveBinary(): Promise<string> {
+    const envPath = process.env.ANONYMIZE_GLINER2_SERVER_PATH;
+    if (envPath) return envPath;
+    // TODO: check bundled binary in node_modules, fallback to download
+    throw new Error(
+      "gliner2-server binary not found. " +
+      "Set ANONYMIZE_GLINER2_SERVER_PATH or use a bundled installation."
+    );
</code_context>
<issue_to_address>
**issue:** The `binaryPath` option is currently unused; `resolveBinary` only consults the environment variable

`Gliner2ClientOptions` exposes `binaryPath`, but `resolveBinary` never uses it and only reads `ANONYMIZE_GLINER2_SERVER_PATH`, which makes the option misleading and forces env-based configuration. Consider preferring `opts.binaryPath`, then falling back to the env var, and finally throwing if neither is provided.
</issue_to_address>

### Comment 3
<location path="packages/anonymize/src/gliner2/client.ts" line_range="28-29" />
<code_context>
+    };
+  }
+
+  get isRunning(): boolean {
+    return this.process !== null && this.port !== null;
+  }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** `isRunning` does not detect a crashed or exited child process

Because this only checks for non-null `process`/`port`, a crashed or exited child will still be reported as running, and `infer` will continue to target a dead sidecar. Please also check `this.process.exitCode` / `this.process.killed`, and consider clearing `process`/`port` on `exit`/`error` so callers see an accurate liveness signal.

Suggested implementation:

```typescript
  get isRunning(): boolean {
    if (!this.process || this.port == null) {
      return false;
    }

    const { exitCode, killed } = this.process;

    // If the process has an exit code or was marked as killed, it's not running anymore
    if (exitCode !== null || killed) {
      return false;
    }

    return true;
  }

```

To fully implement the suggestion:
1. In the code where the child process is spawned (e.g. where `this.process = spawn(...)`), register `exit` and `error` handlers that clear the state:
   - `this.process.on("exit", () => { this.process = null; this.port = null; });`
   - `this.process.on("error", () => { this.process = null; this.port = null; });`
2. If you maintain any additional liveness-related state (e.g. a "ready" flag), also reset it in those handlers so `isRunning` plus that state reflect accurate liveness for callers like `infer`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread .cargo/config.toml Outdated
Comment thread packages/anonymize/src/gliner2/client.ts
Comment thread packages/anonymize/src/gliner2/client.ts Outdated

@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: 12

🧹 Nitpick comments (3)
docs/superpowers/plans/2026-06-26-gliner2-pii-rust-implementation.md (1)

15-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fix heading level to satisfy markdownlint.

The ### Prerequisite at Line 15 skips h2. Add an ## section before it, or change to ## Prerequisite: Register crate in workspace.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-06-26-gliner2-pii-rust-implementation.md` at line
15, The markdown heading hierarchy is skipping a level in the plan document.
Update the “Prerequisite: Register crate in workspace” heading so it uses the
correct heading level, either by introducing an `##` section before it or by
changing the heading itself to `## Prerequisite: Register crate in workspace`,
and keep the surrounding section structure consistent.

Source: Linters/SAST tools

.cargo/config.toml (1)

5-7: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Move the target-dir override out of shared Cargo config. .cargo/config.toml hardcodes E:\cargo-target\anonymize, which makes this repo config machine-specific; keep it in local untracked config or switch to a portable setup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.cargo/config.toml around lines 5 - 7, The shared Cargo config is hardcoding
a machine-specific target directory, which makes the repo non-portable. Remove
the `build.target-dir` override from `.cargo/config.toml` and move it to a local
untracked Cargo config or another developer-specific setup; if you keep it
configurable, reference the `build` section so the repo defaults stay portable.
packages/anonymize/src/gliner2/__test__/label-map.test.ts (1)

4-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an invariant over the full mapping table.

These examples only sample a few labels, so adding or reordering entries in PIPELINE_TO_MODEL can break expand/collapse behavior without touching the tests. Please add a table-driven invariant that iterates the whole map and asserts each expanded model label collapses back into one of the requested pipeline labels, plus a collision case for multi-label requests.

Suggested test shape
+import { PIPELINE_TO_MODEL, expandLabels, collapseLabel } from "../label-map";
-import { expandLabels, collapseLabel } from "../label-map";
+
+it("round-trips every mapped label into one of the requested pipeline labels", () => {
+  for (const [pipelineLabel, modelLabels] of Object.entries(PIPELINE_TO_MODEL)) {
+    for (const modelLabel of modelLabels) {
+      expect(
+        collapseLabel(modelLabel, new Set([pipelineLabel])),
+      ).toBe(pipelineLabel);
+    }
+  }
+});

As per coding guidelines, in test files favor invariant-based tests; based on learnings, prefer invariants over examples when the input space is large.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/anonymize/src/gliner2/__test__/label-map.test.ts` around lines 4 -
51, The current tests in expandLabels and collapseLabel only cover a few
examples, so changes to PIPELINE_TO_MODEL could slip through unnoticed. In
label-map.test.ts, add a table-driven invariant that iterates the full mapping
table and verifies each expanded model label collapses back to one of the
originally requested pipeline labels using expandLabels and collapseLabel, and
include a collision case covering multiple pipeline labels that share the same
model label to confirm the preferred label is selected.

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/gliner2-server/src/engine.rs`:
- Around line 13-23: Move the synchronous cold-start work in the ENGINE
initializer off the Tokio worker thread. In the async OnceCell setup around
Gliner2Engine::from_pretrained and ort::init().commit(), wrap the blocking model
load/download in a blocking task so the runtime thread is not stalled, then
await that task before returning the Arc< Gliner2Engine >. Keep the
ENGINE.get_or_try_init entrypoint and the Gliner2Engine::from_pretrained call as
the key locations to update.

In `@crates/gliner2-server/src/health.rs`:
- Around line 19-22: The HealthResponse in the health endpoint is hardcoding an
outdated version string instead of reporting the crate’s actual version. Update
the response construction in the health handler to use env!("CARGO_PKG_VERSION")
for the version field so it always matches the workspace-inherited crate
version, keeping the rest of the health payload unchanged.

In `@crates/gliner2-server/src/infer.rs`:
- Around line 32-39: The synchronous engine.extract call inside the infer
handler is blocking the async runtime thread; move the inference work into a
blocking task so /v1/infer does not stall other requests. Keep the existing
request parsing and error mapping in infer.rs, but run the engine.extract
invocation for req.text, tasks, and params via a blocking-thread helper and
return its result back into the handler before building the response.

In `@crates/gliner2-server/src/main.rs`:
- Around line 51-52: The startup handshake in main should be flushed immediately
after writing the JSON readiness line so the parent process does not hang
waiting for discovery. Update the stdout write in main to ensure the startup
event is emitted and flushed right away, using the startup handshake path around
writeln!(std::io::stdout(), ...) and the local binding information before
serving begins.

In `@docs/superpowers/plans/2026-06-26-gliner2-pii-rust-implementation.md`:
- Around line 449-457: The import-time reverse-map initialization for
MODEL_TO_PIPELINE is a module-level side effect in a shared module. Move the
construction logic into a lazy accessor such as getModelToPipeline() or a
lazySingleton, and have callers use that accessor instead of relying on eager
initialization. Keep the first-registration-wins behavior while ensuring the
mapping is built only on demand.
- Around line 862-864: The integration test cleanup currently relies on
GC/process exit, but the Rust sidecar client needs explicit shutdown to avoid
leaking child processes. Update the test setup around nerInference so the
created client is reachable at teardown (for example, attach it to the returned
function or keep it in a module-level variable), then change the afterAll
cleanup to call client.stop() if present. Make sure the fix uses the
nerInference/client symbols so the sidecar is always disposed in watch mode and
repeated runs.
- Around line 960-968: The release asset path in the cross-compilation workflow
is likely pointing at the wrong Cargo output location. Check whether
`.cargo/config.toml` or the build setup for `gliner2-server` sets a crate-local
`target-dir`; if not, update the `softprops/action-gh-release` files path to
match the workspace root `target/${{ matrix.target }}/release/...` produced by
`cargo build` in the `gliner2-server` build step.
- Around line 688-699: The stop() logic in the process manager has a stale
fallback race: the Bun.sleep(5000) SIGKILL callback can fire after a later
start() and kill the new process. Update stop() to capture the current process
in a local variable and ensure the fallback only targets that instance, or
replace the sleep-based fallback with a cancellable timeout/AbortController so
it is cleared when the process exits; use the stop() and process fields to
locate the fix. Also update dispose() to stop swallowing errors silently by
logging failures from stop() so shutdown issues are observable.

In `@packages/anonymize/src/gliner2/client.ts`:
- Around line 32-79: The gliner2 sidecar startup in start() only waits for
stdout and can hang or leave a stale child behind on failure. Update start() in
gliner2/client.ts to race the stdout “listening” parse against the child process
error/exit events and a startup timeout, and make sure any failure path tears
down this.process before throwing so later calls don’t reuse a half-started
process.
- Around line 109-114: The fetch in the GLiNER client’s inference path has no
built-in deadline, so add an internal timeout around the `/v1/infer` request in
the client’s inference method. Update the relevant method in `client.ts` (the
one that builds the `fetch` call) to create a default abort timeout and compose
it with any caller-provided `AbortSignal`, rather than passing `signal ?? null`
directly. Make sure the timeout is owned by the client and applies even when
callers do not supply a signal.
- Around line 154-161: Gliner2Client.resolveBinary currently ignores the
constructor-provided binaryPath and only checks ANONYMIZE_GLINER2_SERVER_PATH.
Update Gliner2Client and its resolveBinary method to prefer the configured
Gliner2ClientOptions.binaryPath first, then fall back to the env var, and only
throw if neither is present. Make sure the binary resolution logic still works
with the existing constructor options and preserves the current error path when
no valid path is available.

In `@packages/anonymize/src/gliner2/label-map.ts`:
- Around line 19-26: The label collision handling in collapseLabel should
preserve the caller’s requested label order instead of relying on the first
pipeline entry in PIPELINE_TO_MODEL. Update the mapping logic in label-map.ts so
each model label keeps all matching pipeline labels, then have collapseLabel
pick the first candidate based on the original input order. Use the existing
collapseLabel helper and the PIPELINE_TO_MODEL / MODEL_TO_PIPELINE lookup code
to locate and adjust the resolution path.

---

Nitpick comments:
In @.cargo/config.toml:
- Around line 5-7: The shared Cargo config is hardcoding a machine-specific
target directory, which makes the repo non-portable. Remove the
`build.target-dir` override from `.cargo/config.toml` and move it to a local
untracked Cargo config or another developer-specific setup; if you keep it
configurable, reference the `build` section so the repo defaults stay portable.

In `@docs/superpowers/plans/2026-06-26-gliner2-pii-rust-implementation.md`:
- Line 15: The markdown heading hierarchy is skipping a level in the plan
document. Update the “Prerequisite: Register crate in workspace” heading so it
uses the correct heading level, either by introducing an `##` section before it
or by changing the heading itself to `## Prerequisite: Register crate in
workspace`, and keep the surrounding section structure consistent.

In `@packages/anonymize/src/gliner2/__test__/label-map.test.ts`:
- Around line 4-51: The current tests in expandLabels and collapseLabel only
cover a few examples, so changes to PIPELINE_TO_MODEL could slip through
unnoticed. In label-map.test.ts, add a table-driven invariant that iterates the
full mapping table and verifies each expanded model label collapses back to one
of the originally requested pipeline labels using expandLabels and
collapseLabel, and include a collision case covering multiple pipeline labels
that share the same model label to confirm the preferred label is selected.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 029079d0-7b1c-4577-be90-5ed92ded149c

📥 Commits

Reviewing files that changed from the base of the PR and between f53920d and 696823a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • .ai/local/agents.md
  • .cargo/config.toml
  • .gitignore
  • AGENTS.md
  • Cargo.toml
  • crates/gliner2-server/Cargo.toml
  • crates/gliner2-server/src/engine.rs
  • crates/gliner2-server/src/health.rs
  • crates/gliner2-server/src/infer.rs
  • crates/gliner2-server/src/main.rs
  • crates/gliner2-server/src/types.rs
  • docs/superpowers/plans/2026-06-26-gliner2-pii-rust-implementation.md
  • packages/anonymize/src/gliner2/__test__/label-map.test.ts
  • packages/anonymize/src/gliner2/client.ts
  • packages/anonymize/src/gliner2/inference.ts
  • packages/anonymize/src/gliner2/label-map.ts
  • packages/anonymize/src/gliner2/types.ts
  • packages/anonymize/src/index-shared.ts

Comment thread crates/gliner2-server/src/engine.rs
Comment thread crates/gliner2-server/src/health.rs Outdated
Comment thread crates/gliner2-server/src/infer.rs Outdated
Comment thread crates/gliner2-server/src/main.rs Outdated
Comment on lines +449 to +457
const MODEL_TO_PIPELINE: Record<string, string> = {};
for (const [pipeline, models] of Object.entries(PIPELINE_TO_MODEL)) {
for (const model of models) {
// First registration wins (earliest pipeline label takes priority)
if (!(model in MODEL_TO_PIPELINE)) {
MODEL_TO_PIPELINE[model] = pipeline;
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Avoid module-level side effects in shared modules.

The for...of loop at Lines 450-457 runs at import time to populate MODEL_TO_PIPELINE. As per coding guidelines, do not introduce module-level side effects in shared modules. Move the reverse-map construction into a lazy initializer (e.g., getModelToPipeline() or lazySingleton) so utilities can be imported without triggering initialization.

// Replace with lazy singleton
let _modelToPipeline: Record<string, string> | undefined;
function getModelToPipeline(): Record<string, string> {
  if (_modelToPipeline) return _modelToPipeline;
  _modelToPipeline = {};
  for (const [pipeline, models] of Object.entries(PIPELINE_TO_MODEL)) {
    for (const model of models) {
      if (!(model in _modelToPipeline)) {
        _modelToPipeline[model] = pipeline;
      }
    }
  }
  return _modelToPipeline;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-06-26-gliner2-pii-rust-implementation.md` around
lines 449 - 457, The import-time reverse-map initialization for
MODEL_TO_PIPELINE is a module-level side effect in a shared module. Move the
construction logic into a lazy accessor such as getModelToPipeline() or a
lazySingleton, and have callers use that accessor instead of relying on eager
initialization. Keep the first-registration-wins behavior while ensuring the
mapping is built only on demand.

Source: Coding guidelines

Comment thread packages/anonymize/src/gliner2/client.ts
Comment thread packages/anonymize/src/gliner2/client.ts Outdated
Comment thread packages/anonymize/src/gliner2/client.ts
Comment thread packages/anonymize/src/gliner2/label-map.ts Outdated
Your Name added 3 commits June 27, 2026 17:04
- Move blocking model init to spawn_blocking in engine.rs
- Use CARGO_PKG_VERSION in health.rs
- Move extract call to spawn_blocking in infer.rs
- Flush stdout immediately in main.rs
- Add timeout and abort controller in client.ts infer()
- Fix label collision handling to preserve PIPELINE_TO_MODEL order
- Add invariant tests in label-map.test.ts
- Make cargo target-dir configurable via env var
- Fix heading hierarchy in plan doc
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.

1 participant