Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/self-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./
with:
mode: verify
Expand Down
28 changes: 27 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,33 @@ dist/
coverage/
*.tsbuildinfo
.DS_Store
Thumbs.db
desktop.ini

# Environment variables
.env
.env.*
!.env.example
!.env.template

# Credential file extensions
*.pem
*.key
*.p12
*.pfx
*.crt

# Editor backups
*~
*.bak
*.orig
*.swp
*.swo

# npm and yarn debug logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Phase 0 data collection cache and output
.cache/
Expand All @@ -26,11 +50,13 @@ tests/fixtures/calibration/payloads/*-payload.json
.ruleprobe/
test-treesitter*.mjs

# Publication prep tooling output (local only)
.pubprep/

# Private project docs (keep local only)
.github/copilot-instructions.md
.github/ruleprobe-e2e-verification-guide.md
docs/ruleprobe-build-guide.md
.ruleprobe-semantic/
.codex
.codex-tmp/
.ruleprobe-semantic/
240 changes: 65 additions & 175 deletions README.md

Large diffs are not rendered by default.

22 changes: 20 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
RuleProbe reads files and produces reports. That is the entire operational scope.

- **No code execution.** ts-morph parses TypeScript into ASTs for structural analysis. It never runs the TypeScript compiler's emit pipeline and never executes scanned code.
- **No network calls by default.** RuleProbe has zero runtime network dependencies. It does not phone home, fetch updates, or transmit any data. Network calls happen only when you explicitly opt in with `--llm-extract`, `--rubric-decompose`, `--semantic`, or `ruleprobe run`.
- **No network calls by default.** RuleProbe has zero runtime network dependencies. It does not phone home, fetch updates, or transmit any data. Network calls happen only when you explicitly opt in with `--llm-extract`, `--rubric-decompose`, or `--semantic`.
- **No file modification.** RuleProbe never writes to the scanned directory. Output goes to stdout or to a user-specified `--output` path, nowhere else.
- **No auth, no database, no state.** Each invocation is stateless. Nothing is persisted between runs.

Expand All @@ -23,6 +23,24 @@ What is NEVER sent:
- Source code, variable names, function names, string literals
- Comments, import paths, module names, file paths, scope names

## LLM Call Budgets

External LLM calls happen only on the opt-in `--llm-extract`,
`--rubric-decompose`, and `--semantic` paths, and each has an upper
bound on calls per invocation:

- `--llm-extract`: at most one OpenAI call per invocation. Unparseable
lines are sent as a single batch (default 50 lines). Transient 429
and 503 responses are retried up to 3 times with exponential
backoff; non-transient errors fail immediately.
- `--rubric-decompose`: at most one OpenAI call per invocation, batch
of 20 unparseable lines.
- `--semantic`: capped by `--max-llm-calls` (default 20). The semantic
engine logs and stops escalation when the budget is hit.

If you supply your own API key, these budgets define the maximum cost
ceiling for a single ruleprobe invocation against your account.

## Path Traversal Protection

User-supplied paths (instruction files and output directories) are resolved and bounded to the current working directory before any filesystem operation.
Expand All @@ -47,7 +65,7 @@ npm run audit

### Current audit status

As of v0.1.0, `npm audit` reports 5 moderate advisories in `esbuild`, a transitive dev dependency of vitest. These affect the vitest development server only and have no impact on RuleProbe's runtime behavior. esbuild is not bundled in the published package.
As of v4.5.0, `npm audit` reports 7 moderate advisories in the `vitest` dev-dependency chain (`vite`, `esbuild`, `postcss`, `brace-expansion`). All four are dev-tooling only. None are reachable at runtime and none are included in the published package; the `files` field in package.json restricts the npm artifact to `dist/`, `action.yml`, and metadata.

## Reporting Security Issues

Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ runs:
using: "composite"
steps:
- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22"

Expand Down
81 changes: 81 additions & 0 deletions assets/cover.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 11 additions & 34 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,6 @@ RuleProbe exports core pipeline functions, project analysis, configuration, and

Semantic analysis runs entirely in-process. Raw source code never leaves the machine; only numeric AST vectors, opaque hashes, boolean flags, and rule text are sent to the Anthropic API for LLM-assisted judgments.

## Agent Invocation

| Function | Purpose |
|----------|---------|
| `buildAgentConfig(options)` | Build an agent invocation configuration |
| `invokeAgent(config)` | Invoke an AI agent via SDK |
| `isAgentSdkAvailable()` | Check if the Claude Agent SDK is installed |
| `hasAgentOutput(dir)` | Check if a directory contains agent output |
| `watchForCompletion(options)` | Watch a directory for agent output |
| `countCodeFiles(dir)` | Count code files in a directory |

---

## Usage Examples
Expand Down Expand Up @@ -129,21 +118,7 @@ export default defineConfig({

### Semantic analysis (v4.0.0+)

```typescript
import { analyzeProject } from 'ruleprobe';
import { analyzeProjectSemantic, integrateSemanticResults } from 'ruleprobe/semantic';
import { resolveSemanticConfig } from 'ruleprobe/semantic/config';

const analysis = analyzeProject('./my-project');
const config = resolveSemanticConfig('./my-project', { anthropicKey: process.env.ANTHROPIC_API_KEY });

if (config) {
const rules = analysis.files.flatMap(f => f.ruleSet.rules);
const semanticResult = await analyzeProjectSemantic('./my-project', config, rules);
const enhanced = integrateSemanticResults(analysis, semanticResult);
console.log(`Semantic verdicts: ${semanticResult.report.verdicts.length}`);
}
```
Semantic analysis runs via the `--semantic` flag on the `analyze` command. The underlying `analyzeProjectSemantic`, `integrateSemanticResults`, and `resolveSemanticConfig` functions live in `src/semantic/` but are not part of the published package's main export. Call semantic analysis through the CLI rather than the programmatic API.

---

Expand Down Expand Up @@ -195,14 +170,16 @@ interface ProjectAnalysis {
}

type RuleCategory =
| 'naming' | 'forbidden-pattern' | 'structure' | 'test-requirement'
| 'import-pattern' | 'error-handling' | 'type-safety' | 'code-style'
| 'dependency' | 'preference' | 'file-structure' | 'tooling'
| 'testing' | 'workflow' | 'agent-behavior';

type VerifierType =
| 'ast' | 'regex' | 'filesystem' | 'treesitter'
| 'preference' | 'tooling' | 'config-file' | 'git-history';
| 'naming'
| 'forbidden-pattern'
| 'structure'
| 'import-pattern'
| 'error-handling'
| 'type-safety'
| 'code-style'
| 'agent-behavior';

type VerifierType = 'ast' | 'regex' | 'filesystem' | 'treesitter';

type QualifierType =
| 'always' | 'prefer' | 'when-possible'
Expand Down
12 changes: 11 additions & 1 deletion src/commands/lint-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { parseInstructionFile } from '../parsers/index.js';
import { mapRuleSetToEslintConfig } from '../mapper/index.js';
import { emitEslintConfig } from '../emitter/eslint.js';
import { emitEslintConfig, formatUnmappableSummary } from '../emitter/eslint.js';
import type { EslintFormat } from '../mapper/types.js';
import { resolveSafePath } from '../utils/safe-path.js';
import { writeFileSync } from 'node:fs';
Expand Down Expand Up @@ -54,4 +54,14 @@ export async function handleLintConfig(
} else {
process.stdout.write(output + '\n');
}

// Legacy JSON output cannot carry unmappable rules inline (no JSON
// comment syntax). Surface them on stderr so the user still sees
// which instructions had no ESLint equivalent.
if (format === 'legacy') {
const summary = formatUnmappableSummary(eslintConfig);
if (summary !== '') {
process.stderr.write(summary + '\n');
}
}
}
63 changes: 35 additions & 28 deletions src/emitter/eslint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
* Serializes an EslintConfig to a runnable ESLint configuration string.
* Supports flat config (default) and legacy .eslintrc format.
*
* Flat config emits valid ES module JavaScript that can be imported by ESLint.
* Legacy config emits valid JSON suitable for .eslintrc.json files.
* Flat config emits valid ES module JavaScript that can be imported by
* ESLint. Unmappable rules are appended as JS comments, which remain
* valid JavaScript.
*
* Unmappable rules are emitted as commented sections with the original
* instruction text and a one-line reason explaining why no ESLint rule
* can enforce them.
* Legacy config emits strictly valid JSON suitable for .eslintrc.json
* files. Unmappable rules are not included in the JSON output because
* JSON has no comment syntax; callers should surface them through a
* sidecar channel via formatUnmappableSummary().
*/

import type { EslintConfig, EslintFormat, EslintRuleEntry } from '../mapper/types.js';
Expand Down Expand Up @@ -159,14 +161,10 @@ function emitLegacyConfig(config: EslintConfig): string {
configObj['plugins'] = config.plugins;
}

// Extends
if (config.plugins.length > 0) {
const extendsList = ['eslint:recommended'];
for (const plugin of config.plugins) {
extendsList.push(`plugin:${plugin}/recommended`);
}
configObj['extends'] = extendsList;
}
// No automatic `extends` block. The flat config emitter does not add
// one and emitting eslint:recommended plus plugin recommended sets
// here would enable hundreds of unrelated rules. Users who want
// recommended sets can extend them in their own config.

// Rules as an object with severity + options
const rulesObj: Record<string, unknown> = {};
Expand All @@ -179,27 +177,36 @@ function emitLegacyConfig(config: EslintConfig): string {
}
configObj['rules'] = rulesObj;

// Serialize to JSON with indentation
const jsonStr = JSON.stringify(configObj, null, 2);
// Legacy output is strictly JSON. Unmappable rules cannot be encoded
// inside the JSON without breaking JSON.parse, so they are omitted
// here. Callers that want to surface them should use
// formatUnmappableSummary() and write to stderr or a sidecar path.
return JSON.stringify(configObj, null, 2);
}

// Append unmappable rules as a comment block after the JSON
/**
* Build a human-readable summary of unmappable rules.
*
* Returns an empty string when there are no unmappable rules. Otherwise
* returns a multi-line block listing each rule's id, reason, and the
* original instruction text. Safe to print to stderr or write alongside
* the legacy JSON output as a sidecar file.
*
* @param config - The mapped ESLint config
* @returns A summary block or empty string when nothing is unmappable
*/
export function formatUnmappableSummary(config: EslintConfig): string {
if (config.unmappable.length === 0) {
return jsonStr;
return '';
}

const commentLines: string[] = [
'',
'// Unmappable rules (not part of the JSON config):',
'// The following rules have no direct ESLint equivalent.',
'',
const lines: string[] = [
'Unmappable rules (not enforceable by ESLint):',
];
for (const rule of config.unmappable) {
commentLines.push(`// [${rule.sourceRuleId}] ${rule.reason}`);
commentLines.push(`// Original: ${rule.sourceText}`);
commentLines.push('');
lines.push(` [${rule.sourceRuleId}] ${rule.reason}`);
lines.push(` Original: ${rule.sourceText}`);
}

return jsonStr + commentLines.join('\n');
return lines.join('\n');
}

/**
Expand Down
Loading
Loading