Skip to content

Latest commit

 

History

History
2608 lines (1853 loc) · 59.1 KB

File metadata and controls

2608 lines (1853 loc) · 59.1 KB

API Reference

Programmatic API for integrating codepol into your tools and scripts.

@codepol/core

The core package provides policy loading, checking, and formatting utilities.

Installation

pnpm add @codepol/core

Types

import type {
  // Config types
  CodepolConfig,
  CodepolConfigOptions,
  ConfigFileResult,
  // Policy types
  PolicyFile,
  PolicyRule,
  PolicyRuleTarget,
  PolicyTargetMap,
  PolicyRuleTargetContext,
  LoggerConfig,
  LoggerImportConfig,
  LintSeverity,
  PolicyViolation,
  PolicyViolationFix,
  TreeCheckProvider,
  PolicyPluginCapabilities,
  PluginRuleConfig,
  CodepolPluginRule,
  PluginRule,
  PolicyPluginDeclaration,
  PolicyCheckContext,
  LintProvider,
  LintProviderContext,
  EslintProviderConfig,
  FixProvider,
  FixProviderContext,
  RuleMatch,
  PolicyCheckOptions,
  PolicyCheckResult,
  // Adapter types
  LintDiagnostic,
  TreeCheckAdapterOptions,
  TreeCheckLintAdapter,
  // Result
  Result,
  // Parser / Language
  Lang,
  // Plugin loading
  PolicyPluginsMap,
  // Semantic Index types
  SymbolId,
  ScopeId,
  SymbolKind,
  ScopeKind,
  ByteRange,
  SymbolRecord,
  ScopeRecord,
  RelationRecord,
  DefinesRelation,
  ContainsRelation,
  ReferencesRelation,
  ImportsRelation,
  CallsRelation,
  ImportBindingRelation,
  ExportsRelation,
  TypeRelation,
  SymbolFilter,
  IndexCapabilities,
  FlowNodeId,
  FlowNode,
  FlowEdge,
  FlowGraph,
  FlowNodeKind,
  // Module resolution
  ModuleResolveOptions,
  // Index query
  ProjectIndex,
  // Module graph
  ModuleGraph,
  // Index builder
  IndexBuildOptions,
  IndexBuildResult,
  // Index store
  FileIndexDelta,
} from '@codepol/core';

Policy rules split semantics (meaning) from language targets. PolicyRule defines the rule metadata and plugin reference (ruleId), while PolicyRuleTarget declares the language adapter or parser plus its file globs. Rules reference targets by name via rule.targets (an array of strings), pointing to entries in the top-level PolicyFile.targets map. This lets a single rule apply across multiple targets without duplicating configuration.


Config Loading

Codepol uses a unified config file that is auto-discovered from your project root.

defineConfig

Type-safe helper for building codepol config objects programmatically (e.g. in tests or advanced hosts). Runtime config is loaded from codepol.toml; this helper does not replace it.

import { defineConfig } from '@codepol/core';

const config = defineConfig({
  eslintConfigPath: './eslint.config.ts',
  plugins: [
    { id: '@codepol/plugin', source: { kind: 'builtin' } },
  ],
  targets: { /* ... */ },
  rules: [ /* ... */ ],
});

configGet

Discovers and loads the codepol config file by walking up from the current directory.

async function configGet(cwd?: string): Promise<ConfigFileResult>

Parameters:

  • cwd: Working directory to start search from (default: process.cwd())

Returns: Object with config (CodepolConfig) and configPath (string)

Throws: If no config file is found

Example:

import { configGet } from '@codepol/core';

const { config, configPath } = await configGet();
console.log(`Loaded config from: ${configPath}`);
console.log(`Rules: ${config.rules.length}`);

Config file discovery order:

  1. codepol.toml

configGetFromPath

Loads a config file from a specific path. Use this when you have an explicit path (e.g., from --config flag).

async function configGetFromPath(configPath: string): Promise<ConfigFileResult>

Parameters:

  • configPath: Path to the config file (absolute or relative)

Returns: Object with config (CodepolConfig) and configPath (string)

Example:

import { configGetFromPath } from '@codepol/core';

const { config } = await configGetFromPath('./config/codepol.toml');

configFileDiscover

Synchronously walks up from the starting directory to find a config file.

function configFileDiscover(startDir: string): string | null

Parameters:

  • startDir: Directory to start searching from

Returns: Absolute path to the config file, or null if not found


configGetSync

Synchronous version of configGet. Used by the ESLint adapter, which requires sync execution.

function configGetSync(cwd?: string): ConfigFileResult

Parameters:

  • cwd: Working directory to start search from (default: process.cwd())

Returns: Object with config (CodepolConfig) and configPath (string)

Throws: If no config file is found

Example:

import { configGetSync } from '@codepol/core';

const { config, configPath } = configGetSync();

configGetFromPathSync

Synchronous version of configGetFromPath. Used by the ESLint adapter, which requires sync execution.

function configGetFromPathSync(configPath: string): ConfigFileResult

Parameters:

  • configPath: Path to the config file (absolute or relative)

Returns: Object with config (CodepolConfig) and configPath (string)

Throws: If the config file does not exist

Example:

import { configGetFromPathSync } from '@codepol/core';

const { config } = configGetFromPathSync('./config/codepol.toml');

configCacheClear

Clears the in-memory config cache. Useful for testing or when config files change at runtime.

function configCacheClear(): void

Example:

import { configCacheClear } from '@codepol/core';

configCacheClear();

CodepolConfig

Full codepol configuration type combining policy definition and runtime options.

type CodepolConfig = PolicyFile & CodepolConfigOptions;

type CodepolConfigOptions = {
  /** Path to ESLint config (auto-detected if not specified) */
  eslintConfigPath?: string;
};

policyFileGet (deprecated)

Loads and parses a JSON config file. Deprecated: Use configGet() or configGetFromPath() instead, which support TOML config files (codepol.toml).

function policyFileGet(policyPath: string): PolicyFile

Parameters:

  • policyPath: Path to the JSON config file (absolute or relative)

Returns: Parsed PolicyFile object

Example:

// Prefer configGet() for new code:
import { configGet } from '@codepol/core';
const { config } = await configGet();

// Legacy JSON support:
import { policyFileGet } from '@codepol/core';
const policy = policyFileGet('./legacy-config.json');

providerRulesConfigGet

Generates lint provider rules config from the codepol config. Users spread this into their lint config (e.g., eslint.config.js).

async function providerRulesConfigGet(
  provider: 'eslint',
  configPath?: string
): Promise<Record<string, unknown>>

Parameters:

  • provider: The lint provider platform ('eslint')
  • configPath: Path to config file (auto-discovered if not specified)

Returns: Rules config object for the lint provider

Example:

// eslint.config.js
import { eslintPluginCreate } from '@codepol/plugin-eslint';
import {
  pluginBuiltinRegister,
  policyPluginRulesGet,
  providerRulesConfigGet,
} from '@codepol/core';
import codepolBuiltin from '@codepol/plugin';

pluginBuiltinRegister('@codepol/plugin', codepolBuiltin);

const codepol = eslintPluginCreate(await policyPluginRulesGet());

export default [{
  plugins: { codepol },
  rules: {
    ...await providerRulesConfigGet('eslint'),
    'no-console': 'warn',
  },
}];

Severity is read from the config per rule (default: 'error'):

[[rules]]
ruleId = "@codepol/plugin/require-logger-enter-exit"
severity = "warn"
targets = ["typescript"]

Filtering by Provider

Use providers to control which providers a rule applies to:

[[rules]]
ruleId = "@codepol/plugin/require-logger-enter-exit"
providers = ["tree-sitter"]
targets = ["typescript"]

If providers is omitted or empty, the rule applies to all providers. This is useful when you want a rule to only run during codepol check (tree-sitter) but not in ESLint, or vice versa.

Manual ESLint Configuration

providerRulesConfigGet is a convenience helper, not a requirement. You can manually configure codepol rules in your ESLint config:

// eslint.config.js
import codepol from '@codepol/plugin-eslint';

export default [{
  plugins: { codepol },
  rules: {
    'codepol/require-logger-enter-exit': ['error', { /* options */ }],
    'my-plugin/custom-rule': 'warn',
  },
}];

This works when running ESLint directly (eslint .).

::: warning codepol check CLI Override When using codepol check, the CLI generates rules from your config and passes them via ESLint's overrideConfig, which has highest precedence. This means rules defined in codepol.toml will use severity from the config, overriding your eslint.config.js settings for those same rules.

If you want full control over ESLint configuration, run ESLint directly instead of using codepol check. :::


Parser and Language Registration

Tree-sitter WASM parsers must be initialized before any policy checking or index building.

parserInit

Initializes the web-tree-sitter WASM runtime and loads registered language grammars. Must be called before any scanning or indexing operations. Safe to call multiple times (subsequent calls load newly registered languages only).

async function parserInit(): Promise<void>

Example:

import { parserInit, langAdd } from '@codepol/core';

langAdd({ langId: 'typescript', fileExtensions: ['.ts'] });
langAdd({ langId: 'tsx', fileExtensions: ['.tsx'] });
await parserInit();

parserGetForFile

Creates a Tree-sitter parser configured for the given file's language.

function parserGetForFile(filePath: string): Result<Parser, string>

Parameters:

  • filePath: Absolute path to the file

Returns: Ok(Parser) on success, Err(string) if the parser is not initialized or no language is registered for the file extension

Example:

import { parserGetForFile, isOk } from '@codepol/core';

const result = parserGetForFile('/path/to/file.ts');
if (isOk(result)) {
  const tree = result.Ok.parse(sourceCode);
}

Lang

Language registration configuration.

type Lang = {
  langId: string;
  /** Path to WASM file. If omitted, uses bundled wasm/tree-sitter-{langId}.wasm */
  wasmPath?: string;
  fileExtensions: string[];
};

langAdd

Registers a language with its WASM grammar and file extensions. Merges extensions if the language is already registered. Must be called before parserInit().

function langAdd(lang: Lang): void

Parameters:

  • lang: Language registration config

Throws: If langId is empty, no extensions are provided, an extension is already registered to a different language, or the language is re-registered with a different wasmPath

Example:

import { langAdd } from '@codepol/core';

langAdd({ langId: 'typescript', fileExtensions: ['.ts'] });
langAdd({ langId: 'tsx', fileExtensions: ['.tsx'] });

// Custom WASM path
langAdd({
  langId: 'python',
  wasmPath: '/path/to/tree-sitter-python.wasm',
  fileExtensions: ['.py'],
});

langsGet

Returns all registered languages.

function langsGet(): Required<Lang>[]

Returns: Array of language registrations with all fields populated (including resolved wasmPath)


wasmPathGet

Resolves the absolute path to a bundled WASM grammar file.

function wasmPathGet(grammarName: string): string

Parameters:

  • grammarName: Grammar name (e.g., 'tree-sitter-typescript')

Returns: Absolute path to the WASM file


langIdGetForFile

Returns the registered language ID for a file based on its extension.

function langIdGetForFile(filePath: string): string | null

Parameters:

  • filePath: File path (can be relative or absolute)

Returns: Language ID (e.g., 'typescript') or null if the extension is not registered


policyRuleTargetsResolve

Resolves the targets for a policy rule by looking up each target name in the policy's named targets map.

function policyRuleTargetsResolve(
  rule: PolicyRule,
  policy: PolicyFile
): PolicyRuleTarget[]

Parameters:

  • rule: The policy rule to resolve targets for
  • policy: The policy file containing named targets

Returns: Array of resolved PolicyRuleTarget objects

Throws: If any target name in rule.targets doesn't exist in policy.targets

Example:

import { configGet, policyRuleTargetsResolve } from '@codepol/core';

const { config } = await configGet();

for (const rule of config.rules) {
  const targets = policyRuleTargetsResolve(rule, config);
  console.log(`Rule ${rule.id} has ${targets.length} target(s)`);
  for (const target of targets) {
    console.log(`  - ${target.language}: ${target.files.join(', ')}`);
  }
}

API naming note

@codepol/core exports policyFileGet-style names (policyFileGet, ruleMatchesGet, policyViolationsGetFromDir, etc.). If you rely on loadPolicy-style names in existing code, update your imports to the canonical API names listed below.


ruleMatchesGet

Collects all files matching each policy rule.

function ruleMatchesGet(
  policy: PolicyFile,
  cwd: string
): Promise<RuleMatch[]>

Parameters:

  • policy: The loaded policy object
  • cwd: Working directory for resolving glob patterns

Returns: Array of RuleMatch objects

Example:

import { configGet, ruleMatchesGet } from '@codepol/core';

const { config } = await configGet();
const matches = await ruleMatchesGet(config, process.cwd());

for (const match of matches) {
  console.log(`Rule: ${match.rule.id}`);
  console.log(`Target language: ${match.target.language}`);
  console.log(`Files: ${match.files.length}`);
}

policyFileGetChecked

Determines if a file should be checked against the policy.

function policyFileGetChecked(
  policy: PolicyFile,
  filePath: string,
  cwd: string
): boolean

Parameters:

  • policy: The loaded policy object
  • filePath: Absolute path to the file
  • cwd: Working directory for resolving glob patterns

Returns: true if the file should be checked

Example:

import { configGet, policyFileGetChecked } from '@codepol/core';

const { config } = await configGet();
const covered = policyFileGetChecked(
  config,
  '/path/to/file.ts',
  process.cwd()
);

globPatternsGetMatchAny

Checks if any glob pattern in the list matches the given file path. Used internally by policy matching and available for custom tooling.

function globPatternsGetMatchAny(
  patterns: string[] | undefined,
  relativeFile: string
): boolean

Parameters:

  • patterns: Array of glob patterns (returns false if undefined or empty)
  • relativeFile: Relative file path to check

Returns: true if any pattern matches

Example:

import { globPatternsGetMatchAny } from '@codepol/core';

globPatternsGetMatchAny(['src/**/*.ts', 'lib/**/*.ts'], 'src/utils.ts');
// true

globPatternsGetMatchAny(undefined, 'src/utils.ts');
// false

ruleTargetMatchesLanguage

Checks if a file matches the language specified in a rule target.

function ruleTargetMatchesLanguage(
  target: PolicyRuleTarget,
  filePath: string
): boolean

Parameters:

  • target: The policy rule target containing the language
  • filePath: File path to check (relative or absolute)

Returns: true if the file matches the target language

Language matching rules:

  • "tsx" -- matches .tsx files only
  • "typescript" -- matches .ts and .tsx files
  • Any other language -- always returns true (no extension filtering)

policyViolationsGetForFile

Checks a single file for policy violations using Tree-sitter.

function policyViolationsGetForFile(
  filePath: string,
  rule: PolicyRule,
  target: PolicyRuleTarget,
  policy: PolicyFile,
  pluginsMap: PolicyPluginsMap,
  dir: string
): Result<PolicyViolation[], string>

Parameters:

  • filePath: Absolute path to the file
  • rule: The policy rule being checked
  • target: The rule target (language/parser/glob configuration)
  • policy: The loaded policy object
  • pluginsMap: Loaded policy plugins (from policyPluginsGet)
  • dir: Working directory for resolving paths and plugin context

Returns: Result containing violations or an error message

Example:

import {
  configGet,
  policyPluginsGet,
  policyRuleTargetsResolve,
  policyViolationsGetForFile,
} from '@codepol/core';

const { config } = await configGet();
const pluginsResult = await policyPluginsGet(config, process.cwd());
if ('Err' in pluginsResult) {
  throw new Error(pluginsResult.Err);
}

const rule = config.rules[0];
const targets = policyRuleTargetsResolve(rule, config);
const target = targets[0];
const violationsResult = policyViolationsGetForFile(
  '/path/to/file.ts',
  rule,
  target,
  config,
  pluginsResult.Ok,
  process.cwd()
);

if ('Ok' in violationsResult) {
  console.log(violationsResult.Ok.length);
}

policyViolationsGetFromDir

Checks all files matching the policy for violations.

function policyViolationsGetFromDir(
  policy: PolicyFile,
  cwd: string
): Promise<Result<PolicyViolation[], string>>

Parameters:

  • policy: The loaded policy object
  • cwd: Working directory for resolving patterns

Returns: Result containing all violations or an error message

Example:

import { configGet, policyViolationsGetFromDir } from '@codepol/core';

const { config } = await configGet();
const violationsResult = await policyViolationsGetFromDir(config, process.cwd());

if ('Ok' in violationsResult) {
  for (const v of violationsResult.Ok) {
    console.log(`${v.filePath}:${v.line}:${v.column} - ${v.message}`);
  }
}

policyCheck

Runs complete policy checks (Tree-sitter checking).

function policyCheck(
  options: PolicyCheckOptions
): Promise<Result<PolicyCheckResult, string>>

Parameters:

type PolicyCheckOptions = {
  configPath?: string;  // Path to config file (auto-discovered if not specified)
  cwd?: string;
};

Returns: Result containing the check output or an error message.

The success payload uses:

type PolicyCheckResult = {
  policy: PolicyFile;
  files: string[];
  treeViolations: PolicyViolation[];
};

Example:

import { policyCheck, policyViolationsGetOutputPretty } from '@codepol/core';

// Auto-discovers config from current directory
const result = await policyCheck({});

// Or with explicit config path
const result2 = await policyCheck({
  configPath: './config/codepol.toml',
});

if ('Ok' in result) {
  console.log(`Checked ${result.Ok.files.length} files`);
  console.log(`Found ${result.Ok.treeViolations.length} violations`);

  if (result.Ok.treeViolations.length > 0) {
    console.log(
      policyViolationsGetOutputPretty(result.Ok.treeViolations, process.cwd())
    );
  }
}

policyViolationsGetOutputPretty

Formats violations into a human-readable string.

function policyViolationsGetOutputPretty(
  violations: PolicyViolation[],
  cwd: string
): string

Parameters:

  • violations: Array of violations
  • cwd: Working directory for relative paths

Returns: Formatted string (empty if no violations)

Example:

import { policyViolationsGetOutputPretty } from '@codepol/core';

const output = policyViolationsGetOutputPretty(violations, process.cwd());
if (output) {
  console.log(output);
}
// Output:
// src/utils.ts:15:1 Function foo is missing logger.enter & logger.exit [function-logging]

Plugin Loading

PolicyPluginsMap

Map of loaded plugin rules, keyed by resolved (namespaced) rule ID.

type PolicyPluginsMap = Map<string, PluginRule>;

policyPluginsGet

Loads, validates, and namespaces all plugins declared in the policy. Built-in plugins are resolved from the in-process registry; process plugins are invoked as subprocesses via the JSON protocol. All policy rule references are validated against the loaded plugins.

async function policyPluginsGet(
  policy: PolicyFile,
  cwd: string,
  options?: { configPath?: string }
): Promise<Result<PolicyPluginsMap, string>>

Parameters:

  • policy: The loaded policy definition
  • cwd: Working directory for resolving plugin module specifiers
  • options.configPath: Path to the config file. Process plugins resolve relative command paths from this location; omitting it falls back to cwd.

Returns: Ok(PolicyPluginsMap) on success, Err(string) if a plugin fails to load, a rule references an unknown plugin, or language support validation fails

Example:

import { configGet, policyPluginsGet, isErr } from '@codepol/core';

const { config } = await configGet();
const result = await policyPluginsGet(config, process.cwd());

if (isErr(result)) {
  console.error(result.Err);
  process.exit(1);
}

const pluginsMap = result.Ok;
console.log(`Loaded ${pluginsMap.size} rule(s)`);

pluginGetForRule

Looks up a plugin by rule ID. Supports both fully-qualified IDs (@scope/plugin/rule) and short IDs (rule), matching by suffix.

function pluginGetForRule(
  pluginsMap: PolicyPluginsMap,
  ruleId: string
): { plugin: PluginRule; resolvedId: string } | undefined

Parameters:

  • pluginsMap: Loaded plugins map (from policyPluginsGet)
  • ruleId: Rule ID (full or short)

Returns: The matched plugin and its resolved ID, or undefined if not found or ambiguous (multiple short-ID matches)

Example:

import { pluginGetForRule } from '@codepol/core';

const lookup = pluginGetForRule(pluginsMap, 'require-logger-enter-exit');
if (lookup) {
  console.log(lookup.resolvedId);
  // '@codepol/plugin/require-logger-enter-exit'
}

Tree-Check Adapter Types

The adapter layer enables converting TreeCheckProvider implementations into lint provider rules. This is a provider-agnostic design: ESLint is the first supported platform, with Biome/Ruff/others possible in the future.

LintDiagnostic

Platform-agnostic lint diagnostic that any lint provider can consume.

type PolicyDiagnosticLocation = {
  filePath: string;
  line: number;
  column: number;
  endLine?: number;
  endColumn?: number;
  message?: string;
};

type LintDiagnostic = {
  message: string;       // Human-readable message
  line: number;          // 1-based line number
  column: number;        // 1-based column number
  endLine?: number;      // Optional end line
  endColumn?: number;    // Optional end column
  ruleId: string;        // Rule ID that produced this diagnostic
  severity: 'error' | 'warning' | 'info';
  relatedLocations?: PolicyDiagnosticLocation[]; // Optional extra spans
};

TreeCheckAdapterOptions

Options for adapting a TreeCheckProvider to a lint provider rule.

type TreeCheckAdapterOptions = {
  ruleName?: string;              // Rule name for the generated lint rule
  ruleUrl?: string;               // URL to rule documentation
  severity?: 'error' | 'warning'; // Default severity (default: 'error')
};

Note: Config path is now auto-discovered via configFileDiscover(). Pass explicit paths through ESLint rule options if needed.

TreeCheckLintAdapter

Adapter contract for converting TreeCheckProvider to lint provider rules.

type TreeCheckLintAdapter<TRule> = {
  platform: string;  // Platform identifier (e.g., 'eslint', 'biome')
  adapt: (provider: CodepolPluginRule, options?: TreeCheckAdapterOptions) => TRule;
};

violationToLintDiagnostic

Converts a PolicyViolation to a platform-agnostic LintDiagnostic.

function violationToLintDiagnostic(
  violation: PolicyViolation,
  severity?: 'error' | 'warning' | 'info'
): LintDiagnostic

Parameters:

  • violation: The policy violation to convert
  • severity: Severity level to assign (default: 'error')

Returns: A LintDiagnostic representing the violation

Example:

import { violationToLintDiagnostic } from '@codepol/core';

const violation = {
  ruleId: 'require-logger',
  filePath: '/src/foo.ts',
  message: 'Missing logger.enter()',
  line: 10,
  column: 5,
};

const diagnostic = violationToLintDiagnostic(violation);
// { message: 'Missing logger.enter()', line: 10, column: 5, ruleId: 'require-logger', severity: 'error' }

violationsToLintDiagnostics

Batch converts an array of PolicyViolation to LintDiagnostic.

function violationsToLintDiagnostics(
  violations: PolicyViolation[],
  severity?: 'error' | 'warning' | 'info'
): LintDiagnostic[]

Parameters:

  • violations: Array of policy violations to convert
  • severity: Severity level to assign (default: 'error')

Returns: Array of LintDiagnostic objects


Plugin Authoring

Primitives for creating codepol rule plugins.

pluginRuleNew

Factory that validates and creates a CodepolPluginRule. This is the only way to create a rule plugin -- direct object literals will not type-check due to internal branding.

function pluginRuleNew(config: PluginRuleConfig): CodepolPluginRule

Parameters:

  • config: Rule plugin configuration (see PluginRuleConfig)

Returns: A branded CodepolPluginRule

Throws: If the rule ID contains / (reserved for namespacing)

Example:

import { pluginRuleNew, treeCheckProviderNew } from '@codepol/core';

const myProvider = treeCheckProviderNew({
  languages: ['typescript', 'tsx'],
  check: (rule, context) => {
    const violations = [];
    // ... check logic ...
    return violations;
  },
});

export const myRule = pluginRuleNew({
  id: 'no-todo-comments',
  capabilities: {
    treeCheckProvider: myProvider,
  },
});

export default [myRule];

PluginRuleConfig

Input configuration for creating a rule plugin.

type PluginRuleConfig = {
  /** Must NOT contain '/' -- reserved for namespacing (e.g., "my-rule" becomes "@scope/plugin/my-rule") */
  id: string;
  /** Capability bundle for this rule */
  capabilities: PolicyPluginCapabilities;
};

PolicyViolation

A single policy finding. Primary position uses line / column; optional endLine / endColumn close the range; relatedLocations lists additional spans (see PolicyDiagnosticLocation under Tree-Check Adapter Types).

type PolicyViolation = {
  ruleId: string;
  filePath: string;
  message: string;
  line: number;
  column: number;
  endLine?: number;
  endColumn?: number;
  relatedLocations?: PolicyDiagnosticLocation[];
  fix?: PolicyViolationFix;
};

PolicyViolationFix

Fix data for an auto-fixable violation. Provides the byte range and replacement text for ESLint/IDE inline fixes.

type PolicyViolationFix = {
  /** Byte offset range [start, end) in the source text to replace */
  byteRange: ByteRange;
  /** Replacement text (can be empty to delete the range) */
  text: string;
};

Violations with a fix field are auto-fixable when running through ESLint with --fix or via IDE quick-fix actions.


FixProviderContext

Context passed to fix providers when applying fixes.

type FixProviderContext = {
  cwd: string;
  policy: PolicyFile;
  configPath: string;
  files: string[];
  ruleTargets?: PolicyRuleTargetContext[];
};

TreeCheckFn

Consumer-facing check function type. Returns a plain violations array; errors are thrown as exceptions (the treeCheckProviderNew factory wraps this with resultFrom to produce Result).

type TreeCheckFn = (
  rule: PolicyRule,
  context: PolicyCheckContext
) => PolicyViolation[];

treeCheckProviderNew

Factory for creating a TreeCheckProvider from a plain check function. Wraps the function with resultFrom so that thrown exceptions are converted to Result.Err instead of propagating.

function treeCheckProviderNew(config: {
  languages: string[];
  check: TreeCheckFn;
}): TreeCheckProvider

Parameters:

  • config.languages: Languages this provider supports (e.g., ['typescript', 'tsx'])
  • config.check: Check function that receives a rule and context, returns violations

Returns: A TreeCheckProvider suitable for use in PluginRuleConfig.capabilities

Example:

import { treeCheckProviderNew } from '@codepol/core';

const provider = treeCheckProviderNew({
  languages: ['typescript', 'tsx'],
  check: (rule, context) => {
    const violations = [];
    // ... tree-sitter analysis ...
    return violations;
  },
});

eslintProviderCreate

Factory for creating LintProvider objects with platform: 'eslint'.

function eslintProviderCreate(config: {
  languages: string[];
  pluginName?: string;
  rules: Record<string, unknown>;
  configs?: Record<string, unknown>;
  ruleOptions?: (ctx: LintProviderContext) => unknown;
}): LintProvider

Parameters:

  • config.languages: Languages this provider supports
  • config.pluginName: ESLint plugin name (default: 'codepol')
  • config.rules: ESLint rule implementations
  • config.configs: Optional ESLint config presets
  • config.ruleOptions: Optional function to derive rule options from the policy context

Returns: A LintProvider with platform: 'eslint'


rulePluginLanguagesGet

Derives all supported languages from a plugin's lint providers and tree-check provider.

function rulePluginLanguagesGet(plugin: CodepolPluginRule): string[]

Parameters:

  • plugin: The rule plugin to inspect

Returns: Deduplicated array of language IDs


ESLINT_PLUGIN_NAME_DEFAULT

Default ESLint plugin name used when none is specified in provider config.

const ESLINT_PLUGIN_NAME_DEFAULT = 'codepol';

Workspace Package Discovery

workspacePackageMapDiscover

Discovers all workspace packages in a monorepo and maps each package name to the absolute path of its source entry-point file. Supports pnpm, npm, and yarn workspace layouts.

function workspacePackageMapDiscover(rootDir: string): Map<string, string>

Parameters:

  • rootDir: Absolute path to the monorepo root directory

Returns: Map of package name to absolute source entry-point file path

Discovery order:

  1. pnpm-workspace.yaml (pnpm)
  2. Root package.json workspaces field (npm / yarn)

Entry-point resolution per package:

  1. Derive source path from exports["."] or main (dist/src/, .js.ts)
  2. Fallback to src/index.ts next to the package's package.json

Example:

import { workspacePackageMapDiscover } from '@codepol/core';

const packages = workspacePackageMapDiscover('/path/to/monorepo');

for (const [name, entryPoint] of packages) {
  console.log(`${name}${entryPoint}`);
}
// @codepol/core → /path/to/monorepo/packages/core/src/index.ts
// @codepol/plugin → /path/to/monorepo/packages/plugin/src/index.ts

This is used internally by the semantic index for monorepo-aware module resolution (resolving bare specifiers like import { foo } from '@codepol/core' to their source files).


Result Utilities

Codepol uses a lightweight Result type instead of thrown exceptions for operations that can fail. This makes error handling explicit at the type level.

Result

Discriminated union for success (Ok) or error (Err).

type Result<T, E> = { Ok: T; Err?: never } | { Err: E; Ok?: never };

Ok / Err

Constructors for creating Result values.

function Ok<T>(value: T): Result<T, never>
function Err<E>(error: E): Result<never, E>

Example:

import { Ok, Err, type Result } from '@codepol/core';

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) return Err('Division by zero');
  return Ok(a / b);
}

isOk / isErr

Type guards for narrowing Result values.

function isOk<T, E>(result: Result<T, E>): result is { Ok: T; Err?: never }
function isErr<T, E>(result: Result<T, E>): result is { Err: E; Ok?: never }

Example:

import { isOk, isErr } from '@codepol/core';

const result = await policyViolationsGetFromDir(config, cwd);

if (isOk(result)) {
  console.log(`${result.Ok.length} violations`);
}
if (isErr(result)) {
  console.error(result.Err);
}

resultFrom / resFrom

Wraps a synchronous function, converting thrown exceptions to Result.Err. resFrom is an alias.

function resultFrom<T, E>(fn: () => T): Result<T, E>
const resFrom = resultFrom;

Example:

import { resultFrom } from '@codepol/core';

const result = resultFrom(() => JSON.parse(rawJson));

resFromAsync

Async version of resultFrom. Wraps an async function, converting rejections to Result.Err.

async function resFromAsync<T, E>(
  fn: () => Promise<T>
): Promise<Result<T, E>>

Example:

import { resFromAsync } from '@codepol/core';

const result = await resFromAsync(() => fetch(url).then(r => r.json()));

Semantic Index (Cross-File Analysis)

The semantic index provides project-wide cross-file analysis. It extracts symbols, scopes, and relations from source files using Tree-sitter, resolves cross-file imports/exports, and exposes a read-only query API for plugins.

Core Types

SymbolId / ScopeId / FlowNodeId

Stable string identifiers for index entities. IDs are deterministic and stable across re-indexing when content is unchanged.

type SymbolId = string;
type ScopeId = string;
type FlowNodeId = string;

ByteRange

Byte range within a file. Uses byte offsets (not line/column) for precision and performance.

type ByteRange = {
  start: number;  // Inclusive
  end: number;    // Exclusive
};

SymbolKind

Language-agnostic symbol kinds. Adapters map language-specific node types to these canonical kinds.

type SymbolKind =
  | 'module'
  | 'namespace'
  | 'class'
  | 'interface'
  | 'type'
  | 'function'
  | 'method'
  | 'variable'
  | 'const'
  | 'field'
  | 'parameter'
  | 'enum'
  | 'enumMember';

ScopeKind

Language-agnostic scope kinds. Scopes form a tree for name resolution and visibility.

type ScopeKind =
  | 'file'
  | 'module'
  | 'type'
  | 'function'
  | 'block'
  | 'class'
  | 'namespace';

SymbolFlags

Symbol attribute flags as a bitset. Combine with bitwise OR.

import { SymbolFlags } from '@codepol/core';

SymbolFlags.None        // 0
SymbolFlags.Exported    // 1
SymbolFlags.Async       // 2
SymbolFlags.Generator   // 4
SymbolFlags.Static      // 8
SymbolFlags.Abstract    // 16
SymbolFlags.Readonly    // 32
SymbolFlags.Optional    // 64
SymbolFlags.Private     // 128
SymbolFlags.Protected   // 256
SymbolFlags.Public      // 512

Example:

import { SymbolFlags } from '@codepol/core';

const isExported = (symbol.flags & SymbolFlags.Exported) !== 0;
const isAsyncExport = (symbol.flags & (SymbolFlags.Exported | SymbolFlags.Async)) ===
  (SymbolFlags.Exported | SymbolFlags.Async);

SymbolRecord

A symbol (declaration) in the semantic index.

type SymbolRecord = {
  id: SymbolId;
  kind: SymbolKind;
  name: string;          // Declared name (local, not qualified)
  file: string;          // Absolute file path
  byteRange: ByteRange;
  scopeId: ScopeId;      // Scope that contains this symbol
  qualName: string;       // Qualified name for disambiguation
  flags: number;          // SymbolFlags bitset
};

ScopeRecord

A scope (lexical/semantic boundary). Scopes form a tree via the parent field.

type ScopeRecord = {
  id: ScopeId;
  kind: ScopeKind;
  file: string;
  byteRange: ByteRange;
  parent?: ScopeId;       // undefined for file scope
};

SymbolFilter

Filter options for symbol queries.

type SymbolFilter = {
  file?: string;
  kind?: SymbolKind;
  name?: string;
  scopeId?: ScopeId;
};

IndexCapabilities

Declares what capabilities the index supports. Plugins should check this before attempting advanced queries.

type IndexCapabilities = {
  crossFileResolution: boolean;
  callGraph: 'none' | 'heuristic' | 'precise';
  controlFlowGraph: boolean;
  supportedLanguages: string[];
};

Relation Types

Relations are append-only facts extracted by language adapters. RelationRecord is a union of all 8 relation types.

DefinesRelation

A scope declares a symbol.

type DefinesRelation = {
  kind: 'Defines';
  scopeId: ScopeId;
  symbolId: SymbolId;
};

ContainsRelation

A scope contains a child scope.

type ContainsRelation = {
  kind: 'Contains';
  scopeId: ScopeId;
  childScopeId: ScopeId;
};

ReferencesRelation

An identifier refers to a symbol. resolvedSymbolId is populated during file-local resolution; undefined if resolution failed.

type ReferencesRelation = {
  kind: 'References';
  scopeId: ScopeId;
  name: string;
  byteRange: ByteRange;
  resolvedSymbolId?: SymbolId;
};

ImportsRelation

A scope imports from a module specifier. resolvedModulePath is populated during cross-file resolution.

type ImportsRelation = {
  kind: 'Imports';
  scopeId: ScopeId;
  spec: string;
  byteRange: ByteRange;
  resolvedModulePath?: string;
};

CallsRelation

A call expression within a scope. resolvedSymbolId is populated during resolution.

type CallsRelation = {
  kind: 'Calls';
  scopeId: ScopeId;
  calleeName: string;
  byteRange: ByteRange;
  resolvedSymbolId?: SymbolId;
};

ImportBindingRelation

Links an imported name to its source module. This is the key relation for cross-file symbol resolution.

type ImportBindingRelation = {
  kind: 'ImportBinding';
  localSymbolId: SymbolId;
  importedName: string;
  moduleSpec: string;
  resolvedModulePath?: string;
  resolvedExportId?: SymbolId;
  isDefault: boolean;
  isNamespace: boolean;
  byteRange: ByteRange;
};

ExportsRelation

Marks a symbol as exported from its module.

type ExportsRelation = {
  kind: 'Exports';
  symbolId: SymbolId;
  exportedName: string;
  isDefault: boolean;
  sourceModule?: string;     // For re-exports
  sourceName?: string;       // For re-exports
  byteRange: ByteRange;
};

TypeRelation

Captures extends/implements relationships between classes and interfaces.

type TypeRelation = {
  kind: 'TypeRelation';
  symbolId: SymbolId;
  targetName: string;
  relationKind: 'extends' | 'implements';
  byteRange: ByteRange;
  resolvedTargetId?: SymbolId;
};

Control Flow Graph Types

FlowNodeKind

Language-agnostic control flow node kinds.

type FlowNodeKind =
  | 'entry'      // Function entry point (exactly one per CFG)
  | 'exit'       // Function exit point (exactly one per CFG)
  | 'statement'  // Basic statement
  | 'branch'     // Decision point (if, loop condition)
  | 'merge'      // Where branches rejoin
  | 'loop'       // Loop header (back-edge target)
  | 'return'
  | 'throw';

FlowNode

A node in a control flow graph.

type FlowNode = {
  id: FlowNodeId;
  kind: FlowNodeKind;
  byteRange?: ByteRange;    // undefined for synthetic entry/exit
  label?: string;
};

FlowEdge

An edge representing a possible transition between control flow points.

type FlowEdge = {
  from: FlowNodeId;
  to: FlowNodeId;
  label?: 'true' | 'false' | 'loop-back' | 'unconditional'
    | 'break' | 'continue' | 'case' | 'default'
    | 'exception' | 'finally';
};

FlowGraph

A control flow graph for a single function/method scope. Contains exactly one entry node and one exit node.

type FlowGraph = {
  scopeId: ScopeId;
  nodes: FlowNode[];
  edges: FlowEdge[];
};

Module Resolution

ModuleResolveOptions

Options for module resolution.

type ModuleResolveOptions = {
  baseDir: string;
  extensions: string[];
  pathAliases?: Record<string, string[]>;
  indexedFiles?: Set<string>;
  workspacePackages?: Map<string, string>;
};

moduleResolve

Resolves a module specifier to an absolute file path.

function moduleResolve(
  specifier: string,
  fromFile: string,
  options: ModuleResolveOptions
): string | undefined

Parameters:

  • specifier: The import specifier (e.g., './utils', 'lodash')
  • fromFile: Absolute path of the importing file
  • options: Resolution options

Returns: Absolute file path if resolved, undefined for external packages or unresolvable specifiers

Resolution strategy:

  1. Workspace packages: check workspacePackages map first
  2. External packages (@org/pkg, lodash): return undefined
  3. Relative imports: resolve relative to importing file, try exact path, then extensions, then directory index
  4. Path aliases: expand alias and resolve
  5. Absolute paths: try extensions and directory index

isRelativeImport

function isRelativeImport(specifier: string): boolean

Returns true if the specifier starts with ./ or ../.


isExternalPackage

function isExternalPackage(specifier: string): boolean

Returns true if the specifier is likely an external package (not relative, not absolute).


DEFAULT_EXTENSIONS

Default extensions tried when resolving modules.

const DEFAULT_EXTENSIONS = [
  '.ts', '.tsx', '.js', '.jsx',
  '.mts', '.cts', '.mjs', '.cjs',
];

Query API

ProjectIndex

Read-only query interface for the semantic index. This is the stable API exposed to plugins for cross-file analysis. All queries are total (return empty arrays, never null).

type ProjectIndex = {
  // Symbol Queries
  symbolsGet(filter?: SymbolFilter): SymbolRecord[];
  symbolGet(id: SymbolId): SymbolRecord | undefined;
  symbolsInFileGet(file: string): SymbolRecord[];
  symbolsGetByName(name: string): SymbolRecord[];

  // Reference Queries
  referencesGet(symbolId: SymbolId): ReferencesRelation[];
  referencesInFileGet(file: string): ReferencesRelation[];

  // Call Graph Queries (heuristic)
  callersGet(symbolId: SymbolId): SymbolId[];
  calleesGet(symbolId: SymbolId): SymbolId[];

  // Scope Queries
  scopeGet(id: ScopeId): ScopeRecord | undefined;
  scopesInFileGet(file: string): ScopeRecord[];
  symbolsInScopeGet(scopeId: ScopeId): SymbolRecord[];

  // Import/Export Queries
  importsGet(file: string): ImportsRelation[];
  importBindingsGet(file: string): ImportBindingRelation[];
  importBindingGetForSymbol(symbolId: SymbolId): ImportBindingRelation | undefined;
  exportedSymbolsGet(filter?: { file?: string; name?: string }): SymbolRecord[];
  exportersGet(symbolName: string): SymbolRecord[];
  fileExportsGet(file: string): ExportsRelation[];
  exportLocationsGet(symbolId: SymbolId): { file: string; exportedName: string }[];
  importResolve(fromFile: string, specifier: string, name: string): SymbolId | undefined;

  // Type Relation Queries
  typeRelationsGet(symbolId: SymbolId): TypeRelation[];
  subTypesGet(symbolId: SymbolId): TypeRelation[];
  typeRelationsInFileGet(file: string): TypeRelation[];

  // Module Graph Queries
  moduleImportersGet(file: string): string[];
  moduleImporteesGet(file: string): string[];
  moduleDependencyOrderGet(): string[];
  moduleCyclesGet(): string[][];
  moduleEntryPointsGet(): string[];

  // Control Flow Graph Queries
  cfgGet(scopeId: ScopeId): FlowGraph | undefined;
  cyclomaticComplexityGet(symbolId: SymbolId): number | undefined;

  // Metadata
  readonly capabilities: IndexCapabilities;
  filesGet(): string[];
  statsGet(): { files: number; symbols: number; scopes: number; relations: number };
};

Example:

import {
  parserInit,
  langAdd,
  projectIndexBuild,
  SymbolFlags,
} from '@codepol/core';

langAdd({ langId: 'typescript', fileExtensions: ['.ts'] });
langAdd({ langId: 'tsx', fileExtensions: ['.tsx'] });
await parserInit();

const { index, stats } = await projectIndexBuild({
  files: ['/src/index.ts', '/src/utils.ts'],
  dir: '/project',
});

console.log(`Indexed ${stats.filesIndexed} files`);

// Find all exported functions
const exported = index.symbolsGet({ kind: 'function' })
  .filter(s => (s.flags & SymbolFlags.Exported) !== 0);

// Check for circular dependencies
const cycles = index.moduleCyclesGet();
if (cycles.length > 0) {
  console.warn(`Found ${cycles.length} circular dependency cycle(s)`);
}

// Get cyclomatic complexity
const fn = index.symbolsGet({ name: 'processData', kind: 'function' })[0];
if (fn) {
  const complexity = index.cyclomaticComplexityGet(fn.id);
  console.log(`Complexity: ${complexity}`);
}

projectIndexCreate

Creates a ProjectIndex from an IndexStore. This wraps the store in a read-only query interface.

function projectIndexCreate(
  store: IndexStore,
  capabilities: IndexCapabilities
): ProjectIndex

Parameters:

  • store: The IndexStore containing indexed data
  • capabilities: Capabilities descriptor for the index

Returns: A ProjectIndex instance


Module Graph

ModuleGraph

Module-level dependency graph. All file paths are absolute, matching the paths in IndexStore.

type ModuleGraph = {
  moduleGraphImportersGet(file: string): string[];
  moduleGraphImporteesGet(file: string): string[];
  moduleGraphDependencyOrderGet(): string[];
  moduleGraphCyclesGet(): string[][];
  moduleGraphEntryPointsGet(): string[];
};

Methods:

  • moduleGraphImportersGet(file) -- files that import the given file (reverse edges)
  • moduleGraphImporteesGet(file) -- files that the given file imports (forward edges)
  • moduleGraphDependencyOrderGet() -- topological sort (dependencies first)
  • moduleGraphCyclesGet() -- circular dependency cycles (Tarjan's SCC algorithm)
  • moduleGraphEntryPointsGet() -- files with no importers (root files), sorted alphabetically

moduleGraphBuild

Builds a ModuleGraph from an IndexStore.

function moduleGraphBuild(store: IndexStore): ModuleGraph

Parameters:

  • store: The IndexStore containing indexed import/export data

Returns: A ModuleGraph instance


Index Builder

IndexBuildOptions

Options for building a project index.

type IndexBuildOptions = {
  files: string[];
  dir: string;
  languages?: string[];
  store?: IndexStore;
  crossFileResolution?: boolean;
  pathAliases?: Record<string, string[]>;
  workspacePackages?: Map<string, string>;
};

Fields:

  • files: Files to index (absolute paths)
  • dir: Working directory for relative paths and module resolution
  • languages: Filter to specific languages (optional, indexes all supported languages if omitted)
  • store: Existing index store to update (creates new if not provided)
  • crossFileResolution: Enable cross-file symbol resolution (default: true)
  • pathAliases: Path aliases from tsconfig (e.g., { "@/*": ["src/*"] })
  • workspacePackages: Workspace package name to source entry file map

IndexBuildResult

Result of building a project index.

type IndexBuildResult = {
  index: ProjectIndex;
  stats: {
    filesIndexed: number;
    filesSkipped: number;
    errors: string[];
  };
};

projectIndexBuild

Builds a project index from a list of files (async version).

async function projectIndexBuild(
  options: IndexBuildOptions
): Promise<IndexBuildResult>

projectIndexBuildSync

Builds a project index from a list of files (sync version). Use this in ESLint rules where async is not supported.

function projectIndexBuildSync(
  options: IndexBuildOptions
): IndexBuildResult

projectIndexUpdate

Incrementally updates the index for specific files. Only re-indexes files whose content has changed.

async function projectIndexUpdate(
  store: IndexStore,
  files: string[]
): Promise<{ updated: number; skipped: number; errors: string[] }>

Parameters:

  • store: The IndexStore to update
  • files: Absolute file paths to update

Returns: Counts of updated/skipped files and any errors


projectIndexUpdateFileSync

Updates a single file in the index (synchronous).

function projectIndexUpdateFileSync(
  store: IndexStore,
  file: string
): boolean

Returns: true if the file was re-indexed (content changed), false if unchanged


projectIndexUpdateFileFromSource

Updates a single file using provided source content instead of reading from disk. Important when the in-memory source (e.g., from ESLint) may differ from the on-disk version.

function projectIndexUpdateFileFromSource(
  store: IndexStore,
  file: string,
  source: string
): boolean

Returns: true if the file was re-indexed (content changed), false if unchanged


projectIndexRemoveFiles

Removes files from the index.

function projectIndexRemoveFiles(
  store: IndexStore,
  files: string[]
): void

crossFileResolveForFile

Re-resolves cross-file imports and exports for a single file. Call this after updating a file to refresh its import binding resolutions.

function crossFileResolveForFile(
  store: IndexStore,
  file: string,
  resolveOptions: ModuleResolveOptions
): void

Parameters:

  • store: The IndexStore
  • file: The file that was updated
  • resolveOptions: Module resolution options

adapterRegister

Registers a language adapter factory for indexing. Built-in adapters for TypeScript, TSX, and Python are registered automatically.

function adapterRegister(
  languageId: string,
  factory: (language: Language) => IndexAdapter
): void

Parameters:

  • languageId: Language identifier (e.g., 'typescript', 'python')
  • factory: Function that creates an IndexAdapter from a Tree-sitter Language

Index Store (Advanced Use)

The IndexStore is the mutable backing store for the semantic index. Most users should use the builder functions (projectIndexBuild, etc.) and query through ProjectIndex. Direct store access is for advanced use cases like custom adapters or incremental tooling.

FileIndexDelta

Result of indexing a single file. Produced by language adapters, consumed by IndexStore.

type FileIndexDelta = {
  file: string;
  revision: string;
  symbols: SymbolRecord[];
  scopes: ScopeRecord[];
  relations: RelationRecord[];
  cfgs?: FlowGraph[];
};

IndexStore / indexStoreNew

In-memory store with secondary indexes for efficient lookups.

class IndexStore {
  filePut(delta: FileIndexDelta): void;
  fileRemove(file: string): void;
  clear(): void;

  symbolGet(id: SymbolId): SymbolRecord | undefined;
  symbolsGet(filter?: SymbolFilter): SymbolRecord[];
  scopeGet(id: ScopeId): ScopeRecord | undefined;
  scopesInFileGet(file: string): ScopeRecord[];
  symbolsInScopeGet(scopeId: ScopeId): SymbolRecord[];

  referencesGet(symbolId: SymbolId): ReferencesRelation[];
  referencesInFileGet(file: string): ReferencesRelation[];
  callsInScopeGet(scopeId: ScopeId): CallsRelation[];
  callsGet(): CallsRelation[];

  importsInFileGet(file: string): ImportsRelation[];
  importBindingsInFileGet(file: string): ImportBindingRelation[];
  importBindingForSymbolGet(symbolId: SymbolId): ImportBindingRelation | undefined;
  importBindingsGet(): ImportBindingRelation[];

  exportsInFileGet(file: string): ExportsRelation[];
  exportsGet(): ExportsRelation[];
  exportMapBuild(): Map<string, Map<string, SymbolId>>;

  typeRelationsForSymbolGet(symbolId: SymbolId): TypeRelation[];
  typeRelationsInFileGet(file: string): TypeRelation[];

  relationUpdate<R extends RelationRecord>(oldRelation: R, newRelation: R): void;

  fileHasRevision(file: string, revision: string): boolean;
  filesGet(): string[];
  cfgGet(scopeId: string): FlowGraph | undefined;
  statsGet(): { files: number; symbols: number; scopes: number; relations: number };
}

function indexStoreNew(): IndexStore

Example:

import { indexStoreNew, projectIndexCreate } from '@codepol/core';

const store = indexStoreNew();

// Use store with builder functions
const { index } = await projectIndexBuild({
  files: myFiles,
  dir: projectRoot,
  store,
});

// Later, incrementally update
await projectIndexUpdate(store, changedFiles);

// Rebuild query interface after updates
const updatedIndex = projectIndexCreate(store, index.capabilities);

@codepol/plugin-eslint

eslintPluginCreate

import { eslintPluginCreate } from '@codepol/plugin-eslint';
import pluginRules from '@codepol/plugin';

const plugin = eslintPluginCreate(pluginRules);
// plugin.rules['require-logger-enter-exit']

The ESLint plugin is a thin adapter that aggregates rules from capability plugins like @codepol/plugin.


eslintAdapter

Converts a TreeCheckProvider into an ESLint rule module. This enables tree-sitter based checks to run within ESLint's infrastructure without duplicating the check logic.

import { eslintAdapter } from '@codepol/plugin-eslint';

const eslintAdapter: TreeCheckLintAdapter<TSESLint.RuleModule<string, unknown[]>>

Properties:

  • platform: 'eslint'
  • adapt(provider, options?): Converts a CodepolPluginRule to an ESLint rule

Example:

import { eslintAdapter } from '@codepol/plugin-eslint';
import { loggerEnterExitRule } from '@codepol/plugin';

// Convert tree-check provider to ESLint rule
const eslintRule = eslintAdapter.adapt(loggerEnterExitRule, {
  ruleName: 'require-logger-enter-exit',
});

// Use in ESLint flat config
export default [
  {
    plugins: {
      codepol: { rules: { 'require-logger-enter-exit': eslintRule } },
    },
    rules: {
      'codepol/require-logger-enter-exit': 'error',
    },
  },
];

eslintAdapterInit

Pre-initializes a TreeCheckProvider for use with ESLint. Call this before running ESLint to ensure async initialization completes (e.g., loading Tree-sitter WASM parsers).

async function eslintAdapterInit(
  provider: CodepolPluginRule,
  policy: PolicyFile,
  cwd: string
): Promise<void>

Parameters:

  • provider: The CodepolPluginRule to initialize
  • policy: The loaded policy file
  • cwd: Current working directory

Example:

import { eslintAdapterInit, eslintAdapter } from '@codepol/plugin-eslint';
import { configGet, parserInit, langAdd } from '@codepol/core';
import { loggerEnterExitRule } from '@codepol/plugin';

// Initialize tree-sitter languages
langAdd({ langId: 'typescript', fileExtensions: ['.ts'] });
await parserInit();

// Initialize the provider
const { config } = await configGet();
await eslintAdapterInit(loggerEnterExitRule, config, process.cwd());

// Now the adapted rule will work without async init warnings
const rule = eslintAdapter.adapt(loggerEnterExitRule);

policyCacheClear / providerInitStateClear

Utility functions for clearing cached state (useful for testing).

import { policyCacheClear, providerInitStateClear } from '@codepol/plugin-eslint';

// Clear cached policy files
policyCacheClear();

// Clear provider initialization state
providerInitStateClear();

Note: The old names clearPolicyCache and clearProviderInitState are still available but deprecated.


@codepol/plugin

Rule Plugins

import pluginRules, { loggerEnterExitRule, loggerLintProvider } from '@codepol/plugin';

// loggerEnterExitRule.id === '@codepol/plugin/require-logger-enter-exit'
// loggerEnterExitRule.capabilities.lintProviders contains loggerLintProvider
// loggerLintProvider.platform === 'eslint'
// loggerLintProvider.languages === ['typescript', 'tsx']
// pluginRules (array for convenience)

Use (lintProvider.config as EslintProviderConfig).ruleOptions?.({ policy, configPath, cwd, ruleId, ruleArgs }) to get the ESLint rule options. Severity is defined in codepol.toml per rule.

policyCacheClear

Clears the policy file cache used by the logger plugin.

import { policyCacheClear } from '@codepol/plugin';

policyCacheClear();

Config file example:

[[plugins]]
id = "@codepol/plugin"
source = { kind = "builtin" }

[targets.typescript-src]
language = "typescript"
files = ["src/**/*.ts"]

[[rules]]
ruleId = "@codepol/plugin/require-logger-enter-exit"
targets = ["typescript-src"]

[rules.args.logger]
identifier = "logger"
enterMethod = "enter"
exitMethod = "exit"
import = { module = "@org/logger", named = "logger" }

@codepol/plugin-esbuild

esbuildPluginCreate

Creates an esbuild plugin for policy enforcement.

function esbuildPluginCreate(options?: PolicyPluginOptions): Plugin

Parameters:

type PolicyPluginOptions = {
  configPath?: string;       // Path to config file (auto-discovered if not specified)
  eslintConfigPath?: string; // Path to ESLint config (uses config value or auto-detects)
  fix?: boolean;             // Default: false
  cwd?: string;              // Default: esbuild's absWorkingDir or cwd
};

Returns: esbuild Plugin

Example (zero-config):

import { build } from 'esbuild';
import { esbuildPluginCreate } from '@codepol/plugin-esbuild';

await build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  outdir: 'dist',
  plugins: [
    // Auto-discovers codepol.toml from project root
    esbuildPluginCreate(),
  ],
});

Example (with explicit config):

plugins: [
  esbuildPluginCreate({
    configPath: './config/codepol.toml',
    fix: true,
  }),
]

Complete Example

Custom policy checker script:

// scripts/check-policy.ts
import {
  parserInit,
  configGet,
  configGetFromPath,
  policyViolationsGetFromDir,
  policyViolationsGetOutputPretty,
  type PolicyViolation,
} from '@codepol/core';

async function main() {
  const configPath = process.argv[2];
  
  // Load config: explicit path or auto-discover
  const { config } = configPath
    ? await configGetFromPath(configPath)
    : await configGet();

  await parserInit();

  console.log(`Checking policy: ${config.rules.map(r => r.id).join(', ')}`);

  const violationsResult = await policyViolationsGetFromDir(
    config,
    process.cwd()
  );

  if ('Err' in violationsResult) {
    console.error(violationsResult.Err);
    process.exit(1);
  }

  if (violationsResult.Ok.length === 0) {
    console.log('✔ All checks passed!');
    return;
  }

  console.log(`\n✖ Found ${violationsResult.Ok.length} violation(s):\n`);
  console.log(
    policyViolationsGetOutputPretty(violationsResult.Ok, process.cwd())
  );

  // Group by rule
  const byRule = violationsResult.Ok.reduce((acc, v) => {
    acc[v.ruleId] = acc[v.ruleId] || [];
    acc[v.ruleId].push(v);
    return acc;
  }, {} as Record<string, PolicyViolation[]>);

  console.log('\nSummary:');
  for (const [ruleId, ruleViolations] of Object.entries(byRule)) {
    console.log(`  ${ruleId}: ${ruleViolations.length} violation(s)`);
  }

  process.exit(1);
}

main().catch(console.error);

Run with:

# Auto-discovers codepol.toml
npx ts-node scripts/check-policy.ts

# Or with explicit config path
npx ts-node scripts/check-policy.ts ./config/codepol.toml