TypeScript SDK Reference

Build deterministic, file-based repository reviewers in TypeScript.

@adversarylabs/sdk 0.1.5

The SDK reads runtime input, locates the source repository, executes registered rules, groups observations, synthesizes and ranks findings, and emits a validated review result. It does not invoke an LLM or hide repository analysis behind a remote service.

pipeline
observe -> group -> synthesize -> rank -> review

Installation

bash
npm install @adversarylabs/sdk@^0.1.5

Requires Node.js 22 or newer and an ESM project.

Your First Adversary

src/index.ts
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { Adversary, Severity } from "@adversarylabs/sdk";

const app = new Adversary({
  name: "adversarylabs/comment-sentences",
  review: { minimumConfidence: "medium" },
});

app.defineRule({
  id: "comments.complete-sentence",
  category: "code-style",
  defaultSeverity: Severity.Info,
  defaultConfidence: "high",
  groupBy: ["ruleId", "subject"],
  aggregate(observations) {
    return {
      title: observations.length === 1
        ? "Comment is a complete sentence"
        : "Comments contain complete sentences",
      summary: observations.length + " comments are written as complete sentences.",
      recommendation:
        "Keep complete-sentence comments only when they explain non-obvious intent.",
    };
  },
});

app.rule("comments.complete-sentence", async (ctx) => {
  for (const file of await ctx.rglob("*.ts")) {
    const content = await readFile(join(ctx.repoPath, file), "utf8");
    content.split(/\r?\n/).forEach((line, index) => {
      const comment = line.match(/^\s*\/\/\s+(.+)/)?.[1];
      if (comment && /^[A-Z][^.!?]*[.!?]$/.test(comment)) {
        ctx.observe({
          ruleId: "comments.complete-sentence",
          subject: file,
          title: "Comment is a complete sentence",
          location: { file, line: index + 1 },
          evidence: { comment },
        });
      }
    });
  }
});

export default app;

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  await app.runFromEnvironment();
}

Detectors report observations. The rule definition owns the repository-specific engineering language used to combine them.

Adversary

ts
const app = new Adversary({
  name: "adversarylabs/example",
  version: "0.1.0",
  review: {
    minimumConfidence: "medium",
    maximumFindings: 10,
    includeInformational: false,
  },
});

Each instance owns its rules and review policy. Defining or replacing a rule on one adversary cannot modify another adversary.

Rules and Definitions

app.rule(id, handler) registers executable detector logic. app.defineRule(definition) registers aggregation and presentation defaults for a stable rule ID.

  • Duplicate rule IDs throw
  • Use app.replaceRule for intentional replacement
  • Definitions are instance-scoped
  • Aggregation receives readonly observations
  • Generic synthesis remains available
  • Rule confidence overrides generic confidence

Rule Context

Every rule handler receives:

RuleContext
ctx.repoPath
ctx.summary.files_scanned
ctx.cache
ctx.relpath(path)
ctx.glob(pattern)
ctx.rglob(pattern)
ctx.observe(observation)
ctx.finding(finding)
ctx.review.assessment(assessment)
ctx.review.positive(note)
ctx.review.observe(note)
ctx.review.score(score)
ctx.review.opinion(opinion)

File contents are read with normal Node.js APIs using ctx.repoPath.

Observations

Prefer ctx.observe for raw detector output. Observations are validated, normalized, deduplicated, grouped, and synthesized after all rule handlers complete.

ts
ctx.observe({
  ruleId: "comments.complete-sentence",
  subject: "src/index.ts",
  groupKey: "complete-sentence-comments",
  title: "Comment is a complete sentence",
  confidence: 0.95,
  location: { file: "src/index.ts", line: 3 },
  evidence: { comment: "This comment explains intent." },
  recommendation: {
    summary: "Keep comments only when they explain non-obvious intent.",
  },
});

Default grouping uses ruleId + subject + category. A rule definition can set groupBy, and an observation can provide an explicit groupKey.

Findings

Use ctx.finding when the adversary has already synthesized a complete issue. Direct findings still pass through validation, ranking, suppression, and rendering.

ts
ctx.finding({
  ruleId: "comments.complete-sentence",
  title: "Comments contain complete sentences",
  category: "code-style",
  severity: "low",
  confidence: "high",
  summary: "Three comments are written as complete sentences.",
  evidence: [
    {
      location: { file: "src/index.ts", line: 3 },
      message: "Explains parser intent.",
      data: { parser: "line-comment" },
    },
  ],
  recommendation:
    "Keep complete-sentence comments only when they explain non-obvious intent.",
  remediation: { complexity: "trivial" },
});

Output evidence uses one canonical shape: nested location and optional structured data. Set deduplicate: false only when findings that share an identity must remain separate.

Review-Level Output

Review-level APIs add concise context that is not itself a finding. Positive signals are limited to the strongest two.

ts
ctx.review.assessment({
  risk: "low",
  summary: "The code is easy to follow. One small style issue remains.",
});

ctx.review.positive({
  key: "intentional-comments",
  summary: "Several comments explain intent rather than restating code.",
  evidence: [{ location: { file: "src/index.ts", line: 3 } }],
});

ctx.review.opinion({
  ship: true,
  summary: "I would ship this as-is.",
});

Review Policy

Policy controls what reaches the primary review:

  • minimumConfidence: low | medium | high
  • maximumFindings: non-negative integer
  • includeInformational: boolean
  • confidenceThresholds: numeric boundaries
  • severityOverrides: rule/group severity map
  • includeSuppressed: run option

Confidence accepts low, medium, high, or a number from 0 to 1. Default numeric thresholds are 0.60 for medium and 0.85 for high.

Execution

Library execution and environment runtime execution are deliberately separate.

ts
// Programmatic: explicit input, no environment overrides or file writes.
const result = await app.run({
  input: { source: { path: "/repo" } },
  includeSuppressed: true,
});

// CLI/container: reads runtime environment and writes the run envelope.
await app.runFromEnvironment();

Timing is omitted from stable results unless includeTiming: trueis explicitly requested.

Result and Rendering

app.run returns a normalized ReviewResult. Use the built-in renderers instead of formatting findings inside rules.

ts
const result = await app.run({
  input: { source: { path: "/repo" } },
});

await new TerminalRenderer().render(result);
await new JsonRenderer().render(result);

Results include assessment, positive signals, review observations, optional scores, ranked findings, opinion, and the count of suppressed findings.

Manifest

The manifest declares execution, permissions, triggers, and the canonical run-envelope format. The SDK owns parse/validate for adversary.yaml (parseAdversaryManifest / validateAdversaryManifest).

adversary.yaml
name: comment-sentences
version: 0.1.0
description: Reports complete-sentence TypeScript comments.

triggers:
  manual: true
  files_changed:
    - "*.ts"
    - "**/*.ts"

runtime:
  name: node
  version: "22"
  command:
    - dist/index.js

permissions:
  enforcement: advisory
  filesystem:
    read:
      - .
    write: []
  network: false
  model: false
  environment:
    allow: []

findings:
  format: adversary.review.v1

Composition (uses)

Language packs and personas can declare other adversaries under uses. The CLI expands composition when you run the entry package; the SDK models and validates the field only.

  • Detection depth lives in each uses member (and the root if it has rules)
  • GitHub comment voice comes from the CLI entry package, not members
  • Transitive expand, dedupe, cycle-safe; --no-compose skips expand
  • Exactly one of name or path per item; version is an exact tag with name only
adversary.yaml (language pack)
name: lang/go
version: 0.0.7
description: Go language pack — specialists under one entrypoint.

# Composition: CLI expands uses transitively when you run this package.
uses:
  - name: go/concurrency
  - name: go/security
    version: "0.0.13"   # optional exact tag (no ^/~ ranges yet)
  - path: ../local-leaf # package-relative; exclusive with name

runtime:
  name: node
  version: "22"
  command: [dist/index.js]

findings:
  format: adversary.review.v1
adversary.yaml (persona)
# Persona entrypoint: voice here, depth from uses
name: local/torvalds-adversary
uses:
  - name: lang/go                 # may expand further
  - name: review/engineering
  - name: review/complexity
  - name: security/secrets
# agent/voice.md (+ section banks) own GitHub rewrite voice for this entry

Example CLI: adversary run lang/go --path ./service or adversary run ./torvalds-adversary --path ./app --github-review. Automatic selection (adversary run with no refs) does not expand uses yet.

Comment voice

GitHub PR wording is owned by the CLI, not the TypeScript rule runtime. Put persona rules in agent/voice.md. With --github-review and a model provider, the CLI rewrites each finding body using that document (template body if no model credentials).

  • Core voice: cadence, bans, length — not detection logic
  • Example bank: real human quotes as style few-shots only
  • Never hard-code banked quotes as finding titles in src/
  • Technical depth comes from findings; voice only dresses them
  • Under composition, the CLI entry package owns rewrite voice
agent/voice.md
# Review voice: blunt maintainer

## Core voice
- Lead with the defect; explain the mechanism.
- Short declarative sentences. No praise sandwiches.

## Length
- 2–5 tight sentences. Stay under ~1,200 characters.

## Example maintainer comments (style only)
These are style few-shots only — re-ground in current evidence; never emit a quote unchanged.

### Ship / OK
> Looks all reasonable to me

### Defects / correctness
> This looks wrong to me. The length is just mb.len.

## Output
Return only the GitHub PR comment body in Markdown.

Train apply issues ask implementers to bank human gold under the example bank. Full CLI guide: composition + voice docs in the adversary repository (docs/voice.md, docs/composition.md).

Train home-built packages

The SDK authors rules; the CLI adversary train command improves local packages from your team’s PR review history. Official catalog packages may run as a jury only—they never receive train drafts.

  • init → edit adversary.train.yaml → train run → results apply
  • Scope (agent/scope.md or docs/scope.md) defines fair misses
  • Apply: docs/train-drafts + optional GitHub issue on the package repo
  • Implement detection class + bank human gold in agent/voice.md
  • Do not bank synthetic draft titles into the voice corpus

Single-package home-built workflow: adversary train init --single-package then adversary train run. See CLI docs and docs/train.md in the adversary repository.

Runtime Contract

runFromEnvironment() reads ADVERSARY_INPUT or /adversary/input.json, then writes ADVERSARY_OUTPUTor /adversary/output.json.

output.json
{
  "protocolVersion": 1,
  "result": {
    "schemaVersion": "adversary.review.v1",
    "adversary": { "name": "adversarylabs/comment-sentences" },
    "target": { "repository": "/repo", "filesScanned": 4 },
    "positives": [],
    "observations": [],
    "findings": [],
    "suppressed": { "findings": 0 }
  }
}

The shipped adversary.run.v1 JSON Schema validates this envelope. The nested review result has schema version adversary.review.v1.

Compatibility

ContractSupport
Node.js22 and 24
ModulesESM
Package@adversarylabs/sdk 0.1.5
Run envelopeadversary.run.v1
Review resultadversary.review.v1
CommonJSNot supported

The SDK remains pre-1.0. Pin a compatible 0.x range and review release notes before upgrading.