Programmatic API for integrating codepol into your tools and scripts.
The core package provides policy loading, checking, and formatting utilities.
pnpm add @codepol/coreimport 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.
Codepol uses a unified config file that is auto-discovered from your project root.
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: [ /* ... */ ],
});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:
codepol.toml
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');Synchronously walks up from the starting directory to find a config file.
function configFileDiscover(startDir: string): string | nullParameters:
startDir: Directory to start searching from
Returns: Absolute path to the config file, or null if not found
Synchronous version of configGet. Used by the ESLint adapter, which requires sync execution.
function configGetSync(cwd?: string): ConfigFileResultParameters:
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();Synchronous version of configGetFromPath. Used by the ESLint adapter, which requires sync execution.
function configGetFromPathSync(configPath: string): ConfigFileResultParameters:
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');Clears the in-memory config cache. Useful for testing or when config files change at runtime.
function configCacheClear(): voidExample:
import { configCacheClear } from '@codepol/core';
configCacheClear();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;
};Loads and parses a JSON config file. Deprecated: Use configGet() or configGetFromPath() instead, which support TOML config files (codepol.toml).
function policyFileGet(policyPath: string): PolicyFileParameters:
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');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"]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.
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.
:::
Tree-sitter WASM parsers must be initialized before any policy checking or index building.
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();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);
}Language registration configuration.
type Lang = {
langId: string;
/** Path to WASM file. If omitted, uses bundled wasm/tree-sitter-{langId}.wasm */
wasmPath?: string;
fileExtensions: string[];
};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): voidParameters:
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'],
});Returns all registered languages.
function langsGet(): Required<Lang>[]Returns: Array of language registrations with all fields populated (including resolved wasmPath)
Resolves the absolute path to a bundled WASM grammar file.
function wasmPathGet(grammarName: string): stringParameters:
grammarName: Grammar name (e.g.,'tree-sitter-typescript')
Returns: Absolute path to the WASM file
Returns the registered language ID for a file based on its extension.
function langIdGetForFile(filePath: string): string | nullParameters:
filePath: File path (can be relative or absolute)
Returns: Language ID (e.g., 'typescript') or null if the extension is not registered
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 forpolicy: 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(', ')}`);
}
}@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.
Collects all files matching each policy rule.
function ruleMatchesGet(
policy: PolicyFile,
cwd: string
): Promise<RuleMatch[]>Parameters:
policy: The loaded policy objectcwd: 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}`);
}Determines if a file should be checked against the policy.
function policyFileGetChecked(
policy: PolicyFile,
filePath: string,
cwd: string
): booleanParameters:
policy: The loaded policy objectfilePath: Absolute path to the filecwd: 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()
);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
): booleanParameters:
patterns: Array of glob patterns (returnsfalseif 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');
// falseChecks if a file matches the language specified in a rule target.
function ruleTargetMatchesLanguage(
target: PolicyRuleTarget,
filePath: string
): booleanParameters:
target: The policy rule target containing the languagefilePath: File path to check (relative or absolute)
Returns: true if the file matches the target language
Language matching rules:
"tsx"-- matches.tsxfiles only"typescript"-- matches.tsand.tsxfiles- Any other language -- always returns
true(no extension filtering)
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 filerule: The policy rule being checkedtarget: The rule target (language/parser/glob configuration)policy: The loaded policy objectpluginsMap: Loaded policy plugins (frompolicyPluginsGet)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);
}Checks all files matching the policy for violations.
function policyViolationsGetFromDir(
policy: PolicyFile,
cwd: string
): Promise<Result<PolicyViolation[], string>>Parameters:
policy: The loaded policy objectcwd: 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}`);
}
}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())
);
}
}Formats violations into a human-readable string.
function policyViolationsGetOutputPretty(
violations: PolicyViolation[],
cwd: string
): stringParameters:
violations: Array of violationscwd: 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]Map of loaded plugin rules, keyed by resolved (namespaced) rule ID.
type PolicyPluginsMap = Map<string, PluginRule>;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 definitioncwd: Working directory for resolving plugin module specifiersoptions.configPath: Path to the config file. Process plugins resolve relativecommandpaths from this location; omitting it falls back tocwd.
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)`);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 } | undefinedParameters:
pluginsMap: Loaded plugins map (frompolicyPluginsGet)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'
}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.
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
};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.
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;
};Converts a PolicyViolation to a platform-agnostic LintDiagnostic.
function violationToLintDiagnostic(
violation: PolicyViolation,
severity?: 'error' | 'warning' | 'info'
): LintDiagnosticParameters:
violation: The policy violation to convertseverity: 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' }Batch converts an array of PolicyViolation to LintDiagnostic.
function violationsToLintDiagnostics(
violations: PolicyViolation[],
severity?: 'error' | 'warning' | 'info'
): LintDiagnostic[]Parameters:
violations: Array of policy violations to convertseverity: Severity level to assign (default:'error')
Returns: Array of LintDiagnostic objects
Primitives for creating codepol rule plugins.
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): CodepolPluginRuleParameters:
config: Rule plugin configuration (seePluginRuleConfig)
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];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;
};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;
};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.
Context passed to fix providers when applying fixes.
type FixProviderContext = {
cwd: string;
policy: PolicyFile;
configPath: string;
files: string[];
ruleTargets?: PolicyRuleTargetContext[];
};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[];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;
}): TreeCheckProviderParameters:
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;
},
});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;
}): LintProviderParameters:
config.languages: Languages this provider supportsconfig.pluginName: ESLint plugin name (default:'codepol')config.rules: ESLint rule implementationsconfig.configs: Optional ESLint config presetsconfig.ruleOptions: Optional function to derive rule options from the policy context
Returns: A LintProvider with platform: 'eslint'
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
Default ESLint plugin name used when none is specified in provider config.
const ESLINT_PLUGIN_NAME_DEFAULT = 'codepol';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:
pnpm-workspace.yaml(pnpm)- Root
package.jsonworkspacesfield (npm / yarn)
Entry-point resolution per package:
- Derive source path from
exports["."]ormain(dist/→src/,.js→.ts) - Fallback to
src/index.tsnext to the package'spackage.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.tsThis 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).
Codepol uses a lightweight Result type instead of thrown exceptions for operations that can fail. This makes error handling explicit at the type level.
Discriminated union for success (Ok) or error (Err).
type Result<T, E> = { Ok: T; Err?: never } | { Err: E; Ok?: never };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);
}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);
}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));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()));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.
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;Byte range within a file. Uses byte offsets (not line/column) for precision and performance.
type ByteRange = {
start: number; // Inclusive
end: number; // Exclusive
};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';Language-agnostic scope kinds. Scopes form a tree for name resolution and visibility.
type ScopeKind =
| 'file'
| 'module'
| 'type'
| 'function'
| 'block'
| 'class'
| 'namespace';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 // 512Example:
import { SymbolFlags } from '@codepol/core';
const isExported = (symbol.flags & SymbolFlags.Exported) !== 0;
const isAsyncExport = (symbol.flags & (SymbolFlags.Exported | SymbolFlags.Async)) ===
(SymbolFlags.Exported | SymbolFlags.Async);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
};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
};Filter options for symbol queries.
type SymbolFilter = {
file?: string;
kind?: SymbolKind;
name?: string;
scopeId?: ScopeId;
};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[];
};Relations are append-only facts extracted by language adapters. RelationRecord is a union of all 8 relation types.
A scope declares a symbol.
type DefinesRelation = {
kind: 'Defines';
scopeId: ScopeId;
symbolId: SymbolId;
};A scope contains a child scope.
type ContainsRelation = {
kind: 'Contains';
scopeId: ScopeId;
childScopeId: ScopeId;
};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;
};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;
};A call expression within a scope. resolvedSymbolId is populated during resolution.
type CallsRelation = {
kind: 'Calls';
scopeId: ScopeId;
calleeName: string;
byteRange: ByteRange;
resolvedSymbolId?: SymbolId;
};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;
};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;
};Captures extends/implements relationships between classes and interfaces.
type TypeRelation = {
kind: 'TypeRelation';
symbolId: SymbolId;
targetName: string;
relationKind: 'extends' | 'implements';
byteRange: ByteRange;
resolvedTargetId?: SymbolId;
};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';A node in a control flow graph.
type FlowNode = {
id: FlowNodeId;
kind: FlowNodeKind;
byteRange?: ByteRange; // undefined for synthetic entry/exit
label?: string;
};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';
};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[];
};Options for module resolution.
type ModuleResolveOptions = {
baseDir: string;
extensions: string[];
pathAliases?: Record<string, string[]>;
indexedFiles?: Set<string>;
workspacePackages?: Map<string, string>;
};Resolves a module specifier to an absolute file path.
function moduleResolve(
specifier: string,
fromFile: string,
options: ModuleResolveOptions
): string | undefinedParameters:
specifier: The import specifier (e.g.,'./utils','lodash')fromFile: Absolute path of the importing fileoptions: Resolution options
Returns: Absolute file path if resolved, undefined for external packages or unresolvable specifiers
Resolution strategy:
- Workspace packages: check
workspacePackagesmap first - External packages (
@org/pkg,lodash): returnundefined - Relative imports: resolve relative to importing file, try exact path, then extensions, then directory index
- Path aliases: expand alias and resolve
- Absolute paths: try extensions and directory index
function isRelativeImport(specifier: string): booleanReturns true if the specifier starts with ./ or ../.
function isExternalPackage(specifier: string): booleanReturns true if the specifier is likely an external package (not relative, not absolute).
Default extensions tried when resolving modules.
const DEFAULT_EXTENSIONS = [
'.ts', '.tsx', '.js', '.jsx',
'.mts', '.cts', '.mjs', '.cjs',
];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}`);
}Creates a ProjectIndex from an IndexStore. This wraps the store in a read-only query interface.
function projectIndexCreate(
store: IndexStore,
capabilities: IndexCapabilities
): ProjectIndexParameters:
store: TheIndexStorecontaining indexed datacapabilities: Capabilities descriptor for the index
Returns: A ProjectIndex instance
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
Builds a ModuleGraph from an IndexStore.
function moduleGraphBuild(store: IndexStore): ModuleGraphParameters:
store: TheIndexStorecontaining indexed import/export data
Returns: A ModuleGraph instance
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 resolutionlanguages: 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
Result of building a project index.
type IndexBuildResult = {
index: ProjectIndex;
stats: {
filesIndexed: number;
filesSkipped: number;
errors: string[];
};
};Builds a project index from a list of files (async version).
async function projectIndexBuild(
options: IndexBuildOptions
): Promise<IndexBuildResult>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
): IndexBuildResultIncrementally 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: TheIndexStoreto updatefiles: Absolute file paths to update
Returns: Counts of updated/skipped files and any errors
Updates a single file in the index (synchronous).
function projectIndexUpdateFileSync(
store: IndexStore,
file: string
): booleanReturns: true if the file was re-indexed (content changed), false if unchanged
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
): booleanReturns: true if the file was re-indexed (content changed), false if unchanged
Removes files from the index.
function projectIndexRemoveFiles(
store: IndexStore,
files: string[]
): voidRe-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
): voidParameters:
store: TheIndexStorefile: The file that was updatedresolveOptions: Module resolution options
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
): voidParameters:
languageId: Language identifier (e.g.,'typescript','python')factory: Function that creates anIndexAdapterfrom a Tree-sitterLanguage
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.
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[];
};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(): IndexStoreExample:
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);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.
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 aCodepolPluginRuleto 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',
},
},
];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: TheCodepolPluginRuleto initializepolicy: The loaded policy filecwd: 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);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.
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.
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" }Creates an esbuild plugin for policy enforcement.
function esbuildPluginCreate(options?: PolicyPluginOptions): PluginParameters:
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,
}),
]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