Skip to content

Stephansama/ste 292 create glob based linter#311

Open
stephansama wants to merge 20 commits into
mainfrom
stephansama/ste-292-create-glob-based-linter
Open

Stephansama/ste 292 create glob based linter#311
stephansama wants to merge 20 commits into
mainfrom
stephansama/ste-292-create-glob-based-linter

Conversation

@stephansama

Copy link
Copy Markdown
Owner

Checklist

  • Latest changes from main have been merged
  • Conflicts have been resolved
  • The branch is pointing to main
  • Eslint hasn't reported any issues.
  • All unit tests pass

@changeset-bot

changeset-bot Bot commented Jun 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8a12398

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@stephansama/monorule Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds @stephansama/monorule: new package implementing rule types/schemas, config discovery, dynamic rule loading, file checking and optional fixes, CLI (list/generate), examples/docs, build config, and workspace integration.

Changes

Monorule Core Implementation

Layer / File(s) Summary
Type contracts and schemas
core/monorule/src/type.ts, core/monorule/src/schema.ts, core/monorule/src/paths.ts, core/monorule/__snapshots__/tsnapi/schema.snapshot.d.ts, core/monorule/__snapshots__/tsnapi/schema.snapshot.js
Defines rule/type system (RuleBase, DefaultParserRule, FunctionParserRule), DirtyFile and package context types, Zod schemas (ruleSchema, configSchema, fullConfigSchema, builtinParseEnumSchema), and DEFAULT_IGNORE_LIST.
Public API helpers and entry exports
core/monorule/src/rules/define.ts, core/monorule/src/rules/index.ts, core/monorule/src/rules/load.ts, core/monorule/src/rules/type.ts, core/monorule/src/parse.ts, core/monorule/src/index.ts, core/monorule/__snapshots__/tsnapi/index.snapshot.d.ts, core/monorule/__snapshots__/tsnapi/index.snapshot.js
Adds defineRule overloads, defineConfig, parse/stringify dispatch tables, loadRules dynamic importer, and re-exports for package entrypoint with generated API snapshots.
Configuration discovery and rule loading
core/monorule/src/config.ts
loadConfig integrates cosmiconfig with TOML loader, merges CLI overrides, validates config via fullConfigSchema, loads rules via loadRules, and validates unique rule names.
Rule checking and file application
core/monorule/src/check.ts, core/monorule/src/apply.ts
checkRules globs and parses files per rule, builds DirtyFile contexts including nearest package info, invokes rule.when to collect errors and prints an aligned summary. applyRules runs rule.apply, serializes with stringify, and writes updated files.
CLI arguments, flags, and logging
core/monorule/src/cli/arguments.ts, core/monorule/src/cli/flags.ts, core/monorule/src/log.ts
Defines cliArguments with cleye, typed configFlags, getFlag helper, and debug logging (obug) with namespace instances and verbosity enable.
CLI commands and execution flow
core/monorule/src/commands/generate.ts, core/monorule/src/commands/list.ts, core/monorule/src/commands/index.ts, core/monorule/src/cli/index.ts, core/monorule/src/cli/run.ts, core/monorule/__snapshots__/tsnapi/cli.snapshot.d.ts, core/monorule/__snapshots__/tsnapi/cli.snapshot.js
Adds generate and list commands (colorized or JSON output), command registration, CLI entrypoint, and top-level run flow that dispatches commands or runs check/apply pipeline.
Examples and package documentation
core/monorule/example/index.ts, core/monorule/example/rules/hello.ts, core/monorule/README.md, README.md
Includes example defineConfig and example rules (JSON with Zod validation, TXT passthrough), package README with usage, and workspace README entry for monorule.
Package build, project setup, and workspace integration
core/monorule/package.json, core/monorule/tsconfig.json, core/monorule/tsdown.config.ts, tsconfig.json, tsconfig.base.json, pnpm-workspace.yaml, .config/.cspell.json, .changeset/cold-ants-occur.md
Adds package.json exports/bin, tsdown build config with schema.json post-build hook and tsnapi plugin, TypeScript project reference, pnpm workspace dependencies (empathic, picocolors), tsconfig.base node types, cspell ignores stringifiers, and a changeset for initial patch release.

Sequence Diagram

sequenceDiagram
  participant CLI
  participant LoadConfig
  participant CheckRules
  participant ApplyRules
  participant FS

  CLI->>LoadConfig: loadConfig(cli flags)
  LoadConfig->>CheckRules: return config with rules
  CLI->>CheckRules: checkRules(config)
  CheckRules->>ApplyRules: dirty files (if --fix)
  ApplyRules->>FS: writeFile(path, output)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A rabbit hops with glee, a brand new rule to see,
Configs and commands arranged, parsers dancing free,
Types and schemas stitched so neat, examples in a row,
Monorule hums, it checks and mends — hop, fix, and onward go! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Stephansama/ste 292 create glob based linter' accurately reflects the main objective—creating a glob-based linter (monorule) package with comprehensive implementation.
Description check ✅ Passed The PR description includes all required checklist items from the template (main merge, conflict resolution, branch pointing to main, eslint, and unit tests), fully satisfying the template requirements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch stephansama/ste-292-create-glob-based-linter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces @stephansama/monorule, a monorepo linting and fixing CLI tool that validates and applies custom rules across workspace packages. The review feedback highlights several critical and high-severity issues, including a race condition in applyRules when concurrently writing to the same file, disabled rules still being executed during checks, and potential crashes from unhandled exceptions when parsing malformed package.json files. Additionally, the reviewer suggested avoiding synchronous file system calls to prevent blocking the event loop and removing a debug log that clutters the CLI output.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread core/monorule/src/apply.ts Outdated
Comment on lines +6 to +65
import { stringify } from "./parse";

export async function applyRules(dirtyFiles: DirtyFile[], rules: RuleMap) {
await Promise.all(
dirtyFiles.map(async (file) => {
console.log(`applying ${file.rule} to ${file.relativePath}`);

const currentRule = rules[file.rule];
if (currentRule.apply) {
const result = currentRule.apply(
file.content as object | string,
file,
);

if (result instanceof Promise) {
console.info(
`stringifying ${currentRule.name} for ${file.absolutePath}`,
);
const string_ = stringify(
await result,
typeof currentRule.parse === "function"
? "json"
: currentRule.parse,
);

await fs.promises.writeFile(
file.absolutePath,
string_,
"utf8",
);

console.info(
`applied lint rule ${currentRule.name} to ${file.absolutePath}`,
);

return;
}

console.info(
`stringifying ${currentRule.name} for ${file.absolutePath}`,
);

const string_ = stringify(
result,
typeof currentRule.parse === "function"
? "json"
: currentRule.parse,
);

await fs.promises.writeFile(file.absolutePath, string_, "utf8");

console.info(
`applied lint rule ${currentRule.name} to ${file.absolutePath}`,
);
}
}),
);
console.info({ dirty: dirtyFiles });
console.info(rules);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

Race Condition & Redundant Code in Rule Application

There is a critical race condition here. When multiple rules are dirty for the same file, they are processed concurrently via Promise.all. Each rule applies its changes to its own stale copy of file.content and writes to the same file.absolutePath concurrently. This means whichever write finishes last will overwrite the others, leading to lost fixes.

Additionally, the code for handling promises vs. synchronous results is highly duplicated. Since await seamlessly handles both promises and non-promises, we can unify them.

We can resolve both issues by grouping the dirty files by their absolute path and applying their rules sequentially per file.

import { parse, stringify } from "./parse";

export async function applyRules(dirtyFiles: DirtyFile[], rules: RuleMap) {
	const filesGrouped = new Map<string, DirtyFile[]>();
	for (const file of dirtyFiles) {
		const list = filesGrouped.get(file.absolutePath) || [];
		list.push(file);
		filesGrouped.set(file.absolutePath, list);
	}

	for (const [absolutePath, files] of filesGrouped) {
		try {
			let raw = await fs.promises.readFile(absolutePath, "utf8");
			for (const file of files) {
				const currentRule = rules[file.rule];
				if (!currentRule || !currentRule.apply) continue;

				console.log("applying " + currentRule.name + " to " + file.relativePath);
				const parsed = parse(raw, currentRule.parse);
				const result = await currentRule.apply(parsed as any, file);
				raw = stringify(
					result,
					typeof currentRule.parse === "function"
						? "json"
						: currentRule.parse,
				);
			}
			await fs.promises.writeFile(absolutePath, raw, "utf8");
		} catch (error) {
			console.error("Failed to apply rules to " + absolutePath + ":", error);
		}
	}
}

Comment thread core/monorule/src/check.ts Outdated
Comment on lines +20 to +21
Object.values(rules).map(async (rule) => {
const cwd = process.cwd();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Disabled Rules Are Still Executed

Currently, checkRules iterates over all rules in rules without checking if they are enabled. If a rule has enabled: false (like the json rule in the hello example), it will still be executed.

Filter out disabled rules before running the glob and checks.

Suggested change
Object.values(rules).map(async (rule) => {
const cwd = process.cwd();
Object.values(rules)
.filter((rule) => rule.enabled !== false)
.map(async (rule) => {
const cwd = process.cwd();

Comment thread core/monorule/src/check.ts Outdated
Comment on lines +40 to +42
const closestPackageJson = closestPackageFile
? (JSON.parse(closestPackageFile) as PackageJSON)
: undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Unhandled Exception on Malformed package.json

If any package.json file encountered during the traversal is malformed or contains invalid JSON, JSON.parse will throw an unhandled exception and crash the entire linter. Wrapping this in a try-catch block ensures the linter is robust against malformed files.

					let closestPackageJson: PackageJSON | undefined;
					if (closestPackageFile) {
						try {
							closestPackageJson = JSON.parse(closestPackageFile) as PackageJSON;
						} catch {
							console.warn("Failed to parse package.json at " + closestPackagePath);
						}
					}

Comment on lines +12 to +14
if (!fs.existsSync(absolute)) return [];

const files = await fs.promises.readdir(absolute);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Avoid Blocking the Event Loop with Synchronous FS Methods

Using synchronous file system methods like fs.existsSync inside an asynchronous function blocks the Node.js event loop. It is more idiomatic and performant to attempt the asynchronous readdir directly and handle the ENOENT error if the directory does not exist.

	let files: string[];
	try {
		files = await fs.promises.readdir(absolute);
	} catch (error) {
		if ((error as NodeJS.ErrnoException).code === "ENOENT") {
			return [];
		}
		throw error;
	}

Comment thread core/monorule/src/cli.ts Outdated
Comment on lines +64 to +66
export async function run() {
console.info(cliArguments.command);
if (cliArguments.command && cliArguments.command in actions) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Clean Up Debug Log

console.info(cliArguments.command); prints undefined to the console on every normal run of the linter (when no subcommand is specified). This clutters the CLI output and should be removed.

export async function run() {
	if (cliArguments.command && cliArguments.command in actions) {

@codecov

codecov Bot commented Jun 2, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 15

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

Inline comments:
In `@core/monorule/example/monorule.config.ts`:
- Line 1: The example config imports the built module ("import { defineConfig,
defineRule } from \"../dist/index.mjs\"") which breaks if users run the example
before building; update the file (monorule.config.ts) to either import directly
from the source alias used by rules (e.g., replace the "../dist/index.mjs"
import with the project's source alias/import path used elsewhere such as
"`@/src`") or add a clear top-of-file comment explaining this example depends on a
prior tsdown/build step and must import from ../dist only for demonstrating
end-user usage.

In `@core/monorule/example/rules/hello.ts`:
- Around line 39-41: The example txtRule.when currently returns undefined (body
only has a commented line) so the rule is a no-op; change txtRule.when to return
an errors array (empty array when no problems, non-empty array of string
messages when it should flag the input) instead of undefined/boolean — e.g.
implement when(input) to check the condition (like
input.includes("stephansama")) and return [] if fine or ['describe problem
here'] when the rule should fire so check.ts will treat it correctly and allow
txtRule.apply to run.

In `@core/monorule/README.md`:
- Line 1: The README.md starts without a top-level heading and then jumps from
H1 to an H5 at the section around line 16; update the file so the first line
begins with a top-level heading (add a "# " title) and change the heading at the
H5 spot to an appropriate hierarchical level (e.g., "##" or "###") so headings
flow H1 -> H2/ H3 rather than H1 -> H5, ensuring all headings in README.md have
consistent levels.

In `@core/monorule/src/apply.ts`:
- Around line 13-15: The code dereferences rules[file.rule] without checking it;
change the apply logic in apply.ts to first look up const currentRule =
rules[file.rule] and if it's undefined throw or return a clear error (e.g.,
throw new Error(`Unknown rule: ${file.rule}`) or a structured failure) before
using currentRule.apply; alternatively, enforce key/name consistency where
RuleMap is constructed so DirtyFile.rule matches RuleMap keys, but at minimum
add the explicit guard around currentRule and fail with a descriptive message.
- Around line 63-64: Remove the raw debug dumps that print full repository data:
delete the console.info calls that log the variables dirtyFiles and rules and
replace them with minimal structured outputs (e.g., counts or sanitized
identifiers). Specifically, in apply.ts stop logging the full dirtyFiles object
and the full rules map; instead log something like the number of dirty files and
the number of rules or a list of file paths/ rule IDs only (use the variables
dirtyFiles and rules to derive counts/IDs). Ensure no raw file contents or full
rule objects are emitted to stdout.

In `@core/monorule/src/check.ts`:
- Around line 74-83: The code assumes rule.when(...) returns an Error[] but that
can be any truthy value; update the handling around the const errors =
rule.when(parsed, context) call to first check for falsy and then distinguish
array vs non-array: if errors is falsy return false; if Array.isArray(errors)
treat it as before (check length, map id/message, etc.); otherwise treat the
single non-array truthy value as a single error (stringify or log its
message/property) and return true — make these checks where errors is declared
so rule.when, errors, parsed and context are the referenced symbols.

In `@core/monorule/src/cli.ts`:
- Around line 46-51: The CLI parses a dryRun flag (dryRun /
cliArguments.flags.dryRun) but never honors it — the code that applies rule
changes (the write/apply-writes step around line 77) executes unconditionally.
Before calling the function that writes or applies rules, add a branch checking
cliArguments.flags.dryRun: if true, do NOT perform file modifications; instead
gather and print/report the list of impacted files and the rule changes that
would be made (same format currently used after writes), and only perform the
actual apply-writes path when dryRun is false. Ensure the check references
cliArguments.flags.dryRun and keeps existing reporting code so dry-run output
matches real-run output.

In `@core/monorule/src/commands/generate.ts`:
- Around line 9-12: The generate command currently no-ops; update the act
function in core/monorule/src/commands/generate.ts (the exported async function
act(arguments_ : typeof cliArguments)) to either (A) fail-fast by throwing a
clear, non-ambiguous error like "Generate command not implemented" (so the CLI
returns a non-zero exit) or (B) implement the minimal scaffold: use arguments_
(e.g., target path or cwd) to write a default configuration file (starter
config) and create a rules directory scaffold (empty directory plus a sample
rule file and README), ensuring filesystem errors are propagated and logged;
pick one approach and make sure act returns a rejecting Promise when not
implemented or resolves only after the files/directories are created.

In `@core/monorule/src/commands/list.ts`:
- Around line 40-45: The --affected flag is declared but never used in the list
command; update the command handler in core/monorule/src/commands/list.ts to
respect the affected flag (or remove the flag if you prefer to postpone
support). Concretely: in the function that executes the list command, check
options.affected and when true, compute and include the affected files for each
rule (call or implement a helper like getAffectedFilesForRule or reuse existing
affected-file logic) and append that data to the output instead of the current
default listing; if no helper exists, implement a small routine that resolves
affected file paths per rule and integrate it into the list flow. If you choose
to remove support, delete the affected flag definition (alias "a") and any
references so the CLI won’t advertise an unsupported option.

In `@core/monorule/src/config.ts`:
- Around line 19-29: The code incorrectly sets options.searchPlaces =
[arguments_.flags.config] and still calls explorer.search(); instead, when a
--config path is provided (check arguments_.flags.config), call
explorer.load(arguments_.flags.config) to load that explicit filepath and use
its returned config/filepath in the existing merge flow; do not override
options.searchPlaces for this case, keep the default options otherwise, and
replace the await explorer.search() use with the load result when a config flag
is present (referencing variables explorer, arguments_.flags.config, options,
moduleName and the existing merge logic).

In `@core/monorule/src/load.ts`:
- Around line 14-23: The loader currently imports every entry returned by files
= await fs.promises.readdir(absolute), which can include directories or non-rule
files and cause failures; update the logic that builds loadedRules to first
filter directory entries to only regular files with the rule file extensions you
expect (e.g., .js, .ts) — use fs.promises.readdir with { withFileTypes: true }
and Dirent.isFile (or stat) and check path.extname before importing — then
inside the files.map async callback around import(path.join(absolute, file))
wrap the import and ruleMapSchema.parse(...) in a try/catch so a single bad file
doesn’t reject the entire Promise.all, returning an empty array or skipping that
file on error; reference symbols: files, register(), loadedRules,
import(path.join(absolute, file)), and ruleMapSchema.parse.
- Around line 16-27: The current code calls register() and later awaits
unregister(), but if any import or ruleMapSchema.parse in the files.map async
loop throws the unregister() call at the end is skipped; wrap the registration
scope in try/finally: call const unregister = register() then run the
Promise.all(files.map(...)) inside a try block and call await unregister() in
the finally block so unregister() always runs even on import/parse errors
(references: register(), unregister, files.map async import(...), and
ruleMapSchema.parse).

In `@core/monorule/src/schema.ts`:
- Line 10: The schema's when property currently uses z.function() with no return
constraint, which lets any return value slip through and can crash code in
core/monorule/src/check.ts where the result is treated as Array<Error> | void
(used with errors.map). Fix by constraining the when function's return to either
an array of Error instances or undefined — e.g., use z.function().returns(...)
with z.array(z.instanceof(Error)) wrapped as optional/union with z.undefined()
(or z.optional) so callers of when can be safely treated as Array<Error> | void.
- Line 5: The schema currently requires an apply function (apply: z.function())
which rejects valid check-only rules; update the contract in
core/monorule/src/schema.ts to make the apply property optional (e.g., change
the apply schema to an optional function) so that rules without an apply are
accepted, and ensure any related types/usages (e.g., the rule type exported from
schema and consumers like core/monorule/src/apply.ts) handle the possibly
undefined apply correctly.

In `@core/monorule/src/type.ts`:
- Around line 59-61: Remove the accidental public export `t` from the module:
locate the exported symbol `t` (declared as Undefinable<{ test: string }>) in
type.ts and either delete the declaration or change it to a non-exported/local
variable or move it into test code so it does not appear in the package API;
ensure no other code depends on the exported `t` and run the build/tests after
removal.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b5730cda-8508-437c-8246-070fc92e5a5f

📥 Commits

Reviewing files that changed from the base of the PR and between e2595c4 and ae8d80e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (34)
  • .config/.cspell.json
  • README.md
  • core/monorule/README.md
  • core/monorule/__snapshots__/tsnapi/cli.snapshot.d.ts
  • core/monorule/__snapshots__/tsnapi/cli.snapshot.js
  • core/monorule/__snapshots__/tsnapi/index.snapshot.d.ts
  • core/monorule/__snapshots__/tsnapi/index.snapshot.js
  • core/monorule/__snapshots__/tsnapi/schema.snapshot.d.ts
  • core/monorule/__snapshots__/tsnapi/schema.snapshot.js
  • core/monorule/example/data/linted.json
  • core/monorule/example/data/linted.txt
  • core/monorule/example/index.ts
  • core/monorule/example/monorule.config.ts
  • core/monorule/example/rules/hello.ts
  • core/monorule/package.json
  • core/monorule/src/apply.ts
  • core/monorule/src/check.ts
  • core/monorule/src/cli.ts
  • core/monorule/src/commands/generate.ts
  • core/monorule/src/commands/index.ts
  • core/monorule/src/commands/list.ts
  • core/monorule/src/config.ts
  • core/monorule/src/index.ts
  • core/monorule/src/load.ts
  • core/monorule/src/log.ts
  • core/monorule/src/parse.ts
  • core/monorule/src/rule.ts
  • core/monorule/src/schema.ts
  • core/monorule/src/type.ts
  • core/monorule/tsconfig.json
  • core/monorule/tsdown.config.ts
  • pnpm-workspace.yaml
  • tsconfig.base.json
  • tsconfig.json

Comment thread core/monorule/example/monorule.config.ts Outdated
Comment thread core/monorule/example/rules/hello.ts Outdated
Comment thread core/monorule/README.md
@@ -0,0 +1,31 @@
<div align="center">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix heading structure to satisfy markdownlint rules.

Line 1 should start with a top-level heading, and Line 16 jumps from H1 to H5. This can trip markdown linting and reduce doc consistency.

Suggested diff
-<div align="center">
-
-# `@stephansama/monorule`
+ # `@stephansama/monorule`
+
+<div align="center">
@@
-##### Table of contents
+## Table of contents

Also applies to: 16-16

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

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

In `@core/monorule/README.md` at line 1, The README.md starts without a top-level
heading and then jumps from H1 to an H5 at the section around line 16; update
the file so the first line begins with a top-level heading (add a "# " title)
and change the heading at the H5 spot to an appropriate hierarchical level
(e.g., "##" or "###") so headings flow H1 -> H2/ H3 rather than H1 -> H5,
ensuring all headings in README.md have consistent levels.

Comment thread core/monorule/src/apply.ts Outdated
Comment on lines +13 to +15
const currentRule = rules[file.rule];
if (currentRule.apply) {
const result = currentRule.apply(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Guard missing rule lookups before dereferencing.

At Line 13, rules[file.rule] can be undefined (the RuleMap key is not type-coupled to rule.name), and Line 14 will throw on currentRule.apply. Add an explicit guard and fail with a clear error, and/or enforce key/name consistency when rules are built so DirtyFile.rule and RuleMap keys use the same identifier.

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

In `@core/monorule/src/apply.ts` around lines 13 - 15, The code dereferences
rules[file.rule] without checking it; change the apply logic in apply.ts to
first look up const currentRule = rules[file.rule] and if it's undefined throw
or return a clear error (e.g., throw new Error(`Unknown rule: ${file.rule}`) or
a structured failure) before using currentRule.apply; alternatively, enforce
key/name consistency where RuleMap is constructed so DirtyFile.rule matches
RuleMap keys, but at minimum add the explicit guard around currentRule and fail
with a descriptive message.

Comment thread core/monorule/src/apply.ts Outdated
Comment on lines +14 to +23
const files = await fs.promises.readdir(absolute);

const unregister = register();

const loadedRules = await Promise.all(
files.map(async (file) => {
console.info(`loading ${file}`);
// convert to object to remove module namespace key
const imported = (await import(path.join(absolute, file))) as {};
return Object.values(ruleMapSchema.parse({ ...imported }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Filter directory entries before dynamic import.

Line 14-23 imports every entry from readdir, including non-rule files and subdirectories. One unsupported file can fail the entire rules load path.

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

In `@core/monorule/src/load.ts` around lines 14 - 23, The loader currently imports
every entry returned by files = await fs.promises.readdir(absolute), which can
include directories or non-rule files and cause failures; update the logic that
builds loadedRules to first filter directory entries to only regular files with
the rule file extensions you expect (e.g., .js, .ts) — use fs.promises.readdir
with { withFileTypes: true } and Dirent.isFile (or stat) and check path.extname
before importing — then inside the files.map async callback around
import(path.join(absolute, file)) wrap the import and ruleMapSchema.parse(...)
in a try/catch so a single bad file doesn’t reject the entire Promise.all,
returning an empty array or skipping that file on error; reference symbols:
files, register(), loadedRules, import(path.join(absolute, file)), and
ruleMapSchema.parse.

Comment on lines +16 to +27
const unregister = register();

const loadedRules = await Promise.all(
files.map(async (file) => {
console.info(`loading ${file}`);
// convert to object to remove module namespace key
const imported = (await import(path.join(absolute, file))) as {};
return Object.values(ruleMapSchema.parse({ ...imported }));
}),
);

await unregister();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Always unregister tsx hooks via try/finally.

If any import in Line 18-25 throws, unregister() at Line 27 is skipped. Wrap registration scope in try/finally to avoid leaking runtime hooks.

Suggested diff
 	const unregister = register();
-
-	const loadedRules = await Promise.all(
-		files.map(async (file) => {
-			console.info(`loading ${file}`);
-			// convert to object to remove module namespace key
-			const imported = (await import(path.join(absolute, file))) as {};
-			return Object.values(ruleMapSchema.parse({ ...imported }));
-		}),
-	);
-
-	await unregister();
+	try {
+		const loadedRules = await Promise.all(
+			files.map(async (file) => {
+				console.info(`loading ${file}`);
+				const imported = (await import(path.join(absolute, file))) as {};
+				return Object.values(ruleMapSchema.parse({ ...imported }));
+			}),
+		);
+		return loadedRules.flat();
+	} finally {
+		await unregister();
+	}
-
-	return loadedRules.flat();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/monorule/src/load.ts` around lines 16 - 27, The current code calls
register() and later awaits unregister(), but if any import or
ruleMapSchema.parse in the files.map async loop throws the unregister() call at
the end is skipped; wrap the registration scope in try/finally: call const
unregister = register() then run the Promise.all(files.map(...)) inside a try
block and call await unregister() in the finally block so unregister() always
runs even on import/parse errors (references: register(), unregister, files.map
async import(...), and ruleMapSchema.parse).

Comment thread core/monorule/src/schema.ts Outdated
Comment thread core/monorule/src/schema.ts Outdated
Comment thread core/monorule/src/type.ts Outdated
@pkg-pr-new

pkg-pr-new Bot commented Jun 3, 2026

Copy link
Copy Markdown

Open in StackBlitz

@stephansama/ai-commit-msg

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/ai-commit-msg@311

@stephansama/alfred-kaomoji

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/alfred-kaomoji@311

@stephansama/astro-iconify-svgmap

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/astro-iconify-svgmap@311

@stephansama/auto-readme

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/auto-readme@311

@stephansama/catppuccin-jsonresume-theme

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/catppuccin-jsonresume-theme@311

@stephansama/catppuccin-opml

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/catppuccin-opml@311

@stephansama/catppuccin-rss

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/catppuccin-rss@311

@stephansama/catppuccin-typedoc

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/catppuccin-typedoc@311

@stephansama/catppuccin-xsl

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/catppuccin-xsl@311

@stephansama/eslint-config

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/eslint-config@311

create-stephansama-example

pnpm add https://pkg.pr.new/stephansama/packages/create-stephansama-example@311

@stephansama/find-makefile-targets

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/find-makefile-targets@311

@stephansama/github-env

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/github-env@311

@stephansama/inline-image

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/inline-image@311

@stephansama/monorule

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/monorule@311

@stephansama/multipublish

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/multipublish@311

@stephansama/pnpm-hooks

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/pnpm-hooks@311

@stephansama/prettier-plugin-handlebars

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/prettier-plugin-handlebars@311

@stephansama/remark-asciinema

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/remark-asciinema@311

@stephansama/single-file

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/single-file@311

@stephansama/svelte-social-share-links

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/svelte-social-share-links@311

@stephansama/typed-env

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/typed-env@311

@stephansama/typed-events

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/typed-events@311

@stephansama/typed-nocodb-api

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/typed-nocodb-api@311

@stephansama/typed-templates

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/typed-templates@311

@stephansama/types-github-action-env

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/types-github-action-env@311

@stephansama/types-lhci

pnpm add https://pkg.pr.new/stephansama/packages/@stephansama/types-lhci@311

commit: 8a12398

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
core/monorule/example/rules/hello.ts (1)

3-3: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix the unresolved example import.

@/src is not resolvable in the example lint/test context, and CI is already failing with import-x/no-unresolved. Import the package entry through a path that is valid from this file instead of the internal alias.

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

In `@core/monorule/example/rules/hello.ts` at line 3, The import using the
internal alias (import { defineRule } from "`@/src`") is unresolved in the example
context; replace it with an import from the package entry point that is
resolvable from this file (i.e., import defineRule from the published package
name or a relative path that reaches the package root) so the symbol defineRule
is imported from a valid module instead of the "`@/src`" alias.
core/monorule/src/apply.ts (1)

25-29: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Custom parsers are always reserialized as JSON.

When currentRule.parse is a function, this code hardcodes stringify(..., "json"). But core/monorule/src/schema.ts explicitly allows function parsers for any rule shape, not just JSON-backed ones. A rule that parses YAML/TOML/text through a custom function will be rewritten as JSON here, which breaks the file format. The rule contract needs an explicit output format/stringifier, or fix mode needs to reject function-based parsers.

Also applies to: 49-53

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

In `@core/monorule/src/apply.ts` around lines 25 - 29, When applying fixes, do not
unconditionally stringify outputs from function-based parsers as JSON; detect
when currentRule.parse is a function and refuse/fail fix-mode instead of calling
stringify(..., "json"). Update the logic around the stringify call (the use of
result and currentRule.parse in apply.ts) to throw or skip with a clear error
message instructing that function-based parsers need an explicit output
stringifier (e.g., a new currentRule.stringify or outputFormat property) or
cannot be auto-fixed; apply the same guard where stringify is used later (the
same pattern around lines 49-53) so function parsers are not silently
reserialized to JSON.
♻️ Duplicate comments (3)
core/monorule/example/rules/hello.ts (1)

36-38: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make txtRule.when return actual violations.

This still returns undefined, so checkRules() treats every .txt file as clean and apply never runs. The example rule is effectively a no-op.

Possible fix
-	when() {
-		// return !input.includes("stephansama");
+	when(input) {
+		if (input.startsWith("stephansama")) return;
+		return [
+			{
+				id: "missing_prefix",
+				message: "text has not been prefixed",
+			},
+		];
 	},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/monorule/example/rules/hello.ts` around lines 36 - 38, txtRule.when
currently returns undefined so checkRules treats files as clean; update
txtRule.when to return actual violation(s) when the rule matches (e.g., return a
non-empty array/object describing the violation or a boolean true depending on
the rule contract) and an empty value when clean. Locate the txtRule.when
function in hello.ts and replace the commented stub with a real return that
checks input (e.g., if !input.includes("stephansama") then return a violation
descriptor like { message: "missing stephansama", location: ... } or an array
containing that object) so checkRules sees a violation and txtRule.apply will
run. Ensure the returned shape matches what checkRules expects.
core/monorule/README.md (1)

1-16: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the README heading hierarchy.

The file still starts with a <div> instead of an H1, and the next section jumps straight to H5. That keeps the markdown heading issue from the earlier review unresolved.

Suggested diff
-<div align="center">
-
-# `@stephansama/monorule`
+# `@stephansama/monorule`
+
+<div align="center">
@@
-##### Table of contents
+## Table of contents
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/monorule/README.md` around lines 1 - 16, Replace the top-level HTML
block "<div align="center">" and the centered title with a proper Markdown H1
header (e.g., "# `@stephansama/monorule`") and adjust the following "##### Table
of contents" to a sensible next-level heading such as "## Table of contents" so
heading hierarchy is correct; ensure any remaining decorative HTML is removed or
moved below the H1 and that subsequent section headings follow a logical H2/H3
sequence instead of jumping to H5.
core/monorule/src/check.ts (1)

96-100: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Normalize rule.when(...) before treating it as RuleError[].

This still crashes if a dynamically loaded rule returns any truthy non-array value: .length and the spread both assume RuleError[]. Tightening the TypeScript type in core/monorule/src/type.ts does not protect this runtime boundary.

Suggested fix
- // `@ts-expect-error` works
- const errors = rule.when(parsed, context);
- if (!errors || errors.length === 0) return false;
-
- checkedErrors.push(...errors);
+ // `rule.when` is user code loaded at runtime, so normalize before using it.
+ const result = rule.when(parsed, context as never);
+ if (!result) return false;
+
+ if (!Array.isArray(result)) {
+   throw new TypeError(
+     `Rule "${rule.name}" must return RuleError[] or void from when()`,
+   );
+ }
+
+ if (result.length === 0) return false;
+ checkedErrors.push(...result);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/monorule/src/check.ts` around lines 96 - 100, Normalize the return value
of rule.when(parsed, context) before assuming it's a RuleError[]: call rule.when
and store its result (e.g., const result = rule.when(parsed, context)); if
result is falsy skip, if Array.isArray(result) push its elements into
checkedErrors, otherwise coerce a single truthy value into an array (e.g.,
[result]) before spreading; use the symbols rule.when, parsed, context, and
checkedErrors to locate and update the code path so non-array truthy returns
won't cause .length or spread runtime errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/monorule/src/apply.ts`:
- Around line 10-12: The code currently uses Promise.all over dirtyFiles which
can run multiple fixes concurrently against the same file; group dirtyFiles by
file.absolutePath so each unique path's entries are processed sequentially
(e.g., build a map from absolutePath to array of DirtyFile entries), then for
each group iterate with a for...of and apply rules one-by-one (reparsing the
file state between each rule application) instead of running them in parallel;
use the DirtyFile.rule identifier and the existing apply logic to ensure you
serialize writes per absolutePath and avoid concurrent writes to the same file.

In `@core/monorule/src/check.ts`:
- Around line 26-27: The aggregated errors currently push only RuleError into
the checkedErrors array (symbol: checkedErrors) which loses file/rule context;
change the collection to store an object like { error: RuleError, relativePath:
string, rule: string } or group by DirtyFile before formatting (affecting the
logic around where checkedErrors is populated and where the summary is printed,
also lines around 100-121). Update the type of checkedErrors, adjust all push
sites (where RuleError instances are added) to include relativePath and rule (or
attach the DirtyFile reference), and then modify the summary/formatting code to
use these fields so output shows the originating file and rule for each error;
update any related imports/types to match the new structure.

In `@core/monorule/src/cli/run.ts`:
- Around line 24-25: The CLI currently ignores the dryRun flag; update run() to
read getFlag(cliArguments, "dryRun") and branch accordingly: when getFlag(...,
"fix") is true call applyRules(dirty, config.rules) as before, but when
getFlag(..., "dryRun") is true do not apply changes—either call an existing
non-mutating function to compute/report fixes or extend applyRules to accept a
dryRun boolean (e.g., applyRules(dirty, config.rules, { dryRun: true })) and
ensure it only reports the fixes without writing them; reference getFlag,
cliArguments, "fix", "dryRun", applyRules, config.rules and the run function
when making the change.
- Around line 20-26: The CLI currently returns exit code 0 when
checkRules(config) finds violations and --fix is not provided; modify the run
logic in run.ts to emit a non-zero exit when dirty.length > 0 and the fix flag
is not set: log a clear error (e.g., list/count of dirty files) and call
process.exit(1) (or throw an Error) to fail CI. Locate the block using
checkRules, getFlag(cliArguments, "fix"), applyRules and ensure the new
early-return path triggers process.exit(1) when dirty exists and fix is false.

In `@core/monorule/src/commands/list.ts`:
- Around line 72-85: The current JSON branch mutates the live rule objects by
deleting fields on each rule in rules.map; instead create and return a derived
DTO per rule so config.rules is not modified and function-valued fields are
normalized. In the rules.map callback (the anonymous mapper that currently
deletes when/apply/context), construct a new object (e.g., const dto = { ...rule
}) and delete dto.when/dto.apply/dto.context from that DTO; if dto.parse (or
other parser fields) is a function, replace it with a serializable placeholder
or normalized value (e.g., a string) rather than letting JSON.stringify drop it,
then return dto for JSON.stringify.

---

Outside diff comments:
In `@core/monorule/example/rules/hello.ts`:
- Line 3: The import using the internal alias (import { defineRule } from
"`@/src`") is unresolved in the example context; replace it with an import from
the package entry point that is resolvable from this file (i.e., import
defineRule from the published package name or a relative path that reaches the
package root) so the symbol defineRule is imported from a valid module instead
of the "`@/src`" alias.

In `@core/monorule/src/apply.ts`:
- Around line 25-29: When applying fixes, do not unconditionally stringify
outputs from function-based parsers as JSON; detect when currentRule.parse is a
function and refuse/fail fix-mode instead of calling stringify(..., "json").
Update the logic around the stringify call (the use of result and
currentRule.parse in apply.ts) to throw or skip with a clear error message
instructing that function-based parsers need an explicit output stringifier
(e.g., a new currentRule.stringify or outputFormat property) or cannot be
auto-fixed; apply the same guard where stringify is used later (the same pattern
around lines 49-53) so function parsers are not silently reserialized to JSON.

---

Duplicate comments:
In `@core/monorule/example/rules/hello.ts`:
- Around line 36-38: txtRule.when currently returns undefined so checkRules
treats files as clean; update txtRule.when to return actual violation(s) when
the rule matches (e.g., return a non-empty array/object describing the violation
or a boolean true depending on the rule contract) and an empty value when clean.
Locate the txtRule.when function in hello.ts and replace the commented stub with
a real return that checks input (e.g., if !input.includes("stephansama") then
return a violation descriptor like { message: "missing stephansama", location:
... } or an array containing that object) so checkRules sees a violation and
txtRule.apply will run. Ensure the returned shape matches what checkRules
expects.

In `@core/monorule/README.md`:
- Around line 1-16: Replace the top-level HTML block "<div align="center">" and
the centered title with a proper Markdown H1 header (e.g., "#
`@stephansama/monorule`") and adjust the following "##### Table of contents" to a
sensible next-level heading such as "## Table of contents" so heading hierarchy
is correct; ensure any remaining decorative HTML is removed or moved below the
H1 and that subsequent section headings follow a logical H2/H3 sequence instead
of jumping to H5.

In `@core/monorule/src/check.ts`:
- Around line 96-100: Normalize the return value of rule.when(parsed, context)
before assuming it's a RuleError[]: call rule.when and store its result (e.g.,
const result = rule.when(parsed, context)); if result is falsy skip, if
Array.isArray(result) push its elements into checkedErrors, otherwise coerce a
single truthy value into an array (e.g., [result]) before spreading; use the
symbols rule.when, parsed, context, and checkedErrors to locate and update the
code path so non-array truthy returns won't cause .length or spread runtime
errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1aa215b6-f77a-457d-abef-911018c85cc0

📥 Commits

Reviewing files that changed from the base of the PR and between ae8d80e and 9459c20.

📒 Files selected for processing (25)
  • .changeset/cold-ants-occur.md
  • core/monorule/README.md
  • core/monorule/__snapshots__/tsnapi/cli.snapshot.d.ts
  • core/monorule/__snapshots__/tsnapi/cli.snapshot.js
  • core/monorule/__snapshots__/tsnapi/index.snapshot.d.ts
  • core/monorule/__snapshots__/tsnapi/index.snapshot.js
  • core/monorule/example/index.ts
  • core/monorule/example/rules/hello.ts
  • core/monorule/package.json
  • core/monorule/src/apply.ts
  • core/monorule/src/check.ts
  • core/monorule/src/cli/arguments.ts
  • core/monorule/src/cli/flags.ts
  • core/monorule/src/cli/index.ts
  • core/monorule/src/cli/run.ts
  • core/monorule/src/commands/generate.ts
  • core/monorule/src/commands/list.ts
  • core/monorule/src/config.ts
  • core/monorule/src/index.ts
  • core/monorule/src/load.ts
  • core/monorule/src/log.ts
  • core/monorule/src/paths.ts
  • core/monorule/src/type.ts
  • core/monorule/tsconfig.json
  • core/monorule/tsdown.config.ts

Comment on lines +10 to +12
await Promise.all(
dirtyFiles.map(async (file) => {
info(`applying ${file.rule} to ${file.relativePath}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Serialize fixes per file instead of writing them concurrently.

DirtyFile.rule in core/monorule/src/type.ts is the violated rule identifier, so the same absolutePath can appear multiple times when one file fails multiple rules. Promise.all then writes competing outputs to the same path in parallel, and the last writer wins. Group by file.absolutePath and apply that file’s fixes sequentially, reparsing between steps if later rules depend on earlier mutations.

Also applies to: 32-56

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

In `@core/monorule/src/apply.ts` around lines 10 - 12, The code currently uses
Promise.all over dirtyFiles which can run multiple fixes concurrently against
the same file; group dirtyFiles by file.absolutePath so each unique path's
entries are processed sequentially (e.g., build a map from absolutePath to array
of DirtyFile entries), then for each group iterate with a for...of and apply
rules one-by-one (reparsing the file state between each rule application)
instead of running them in parallel; use the DirtyFile.rule identifier and the
existing apply logic to ensure you serialize writes per absolutePath and avoid
concurrent writes to the same file.

Comment thread core/monorule/src/check.ts Outdated
Comment on lines +20 to +26
const dirty = await checkRules(config);

if (dirty.length === 0) return console.info("no files to change");

if (getFlag(cliArguments, "fix")) {
await applyRules(dirty, config.rules);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return a failing exit code when violations are found.

When checkRules(config) returns dirty files and --fix is not set, this function falls off the end with exit code 0. That makes CI/hooks pass even though the linter found violations.

Suggested fix
 	const dirty = await checkRules(config);

 	if (dirty.length === 0) return console.info("no files to change");

 	if (getFlag(cliArguments, "fix")) {
 		await applyRules(dirty, config.rules);
+		return;
 	}
+
+	process.exitCode = 1;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const dirty = await checkRules(config);
if (dirty.length === 0) return console.info("no files to change");
if (getFlag(cliArguments, "fix")) {
await applyRules(dirty, config.rules);
}
const dirty = await checkRules(config);
if (dirty.length === 0) return console.info("no files to change");
if (getFlag(cliArguments, "fix")) {
await applyRules(dirty, config.rules);
return;
}
process.exitCode = 1;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/monorule/src/cli/run.ts` around lines 20 - 26, The CLI currently returns
exit code 0 when checkRules(config) finds violations and --fix is not provided;
modify the run logic in run.ts to emit a non-zero exit when dirty.length > 0 and
the fix flag is not set: log a clear error (e.g., list/count of dirty files) and
call process.exit(1) (or throw an Error) to fail CI. Locate the block using
checkRules, getFlag(cliArguments, "fix"), applyRules and ensure the new
early-return path triggers process.exit(1) when dirty exists and fix is false.

Comment on lines +24 to +25
if (getFlag(cliArguments, "fix")) {
await applyRules(dirty, config.rules);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

--dryRun is currently a no-op.

core/monorule/src/cli/arguments.ts declares dryRun with user-facing behavior, but run() never reads that flag. Right now --dryRun is indistinguishable from the normal non---fix path, so the advertised CLI contract is not implemented.

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

In `@core/monorule/src/cli/run.ts` around lines 24 - 25, The CLI currently ignores
the dryRun flag; update run() to read getFlag(cliArguments, "dryRun") and branch
accordingly: when getFlag(..., "fix") is true call applyRules(dirty,
config.rules) as before, but when getFlag(..., "dryRun") is true do not apply
changes—either call an existing non-mutating function to compute/report fixes or
extend applyRules to accept a dryRun boolean (e.g., applyRules(dirty,
config.rules, { dryRun: true })) and ensure it only reports the fixes without
writing them; reference getFlag, cliArguments, "fix", "dryRun", applyRules,
config.rules and the run function when making the change.

Comment on lines +72 to +85
if (json) {
return console.info(
JSON.stringify(
rules.map((rule) => {
// @ts-expect-error need to delete anyway
delete rule.when;
delete rule.apply;
delete rule.context;
return rule;
}),
undefined,
2,
),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Serialize a derived rule DTO instead of the live rule object.

This branch mutates config.rules with delete, and any rule whose parse is a function will lose that field entirely because JSON.stringify drops functions. The text path already normalizes function values at Lines 93-95, so list --json currently emits incomplete rule data for custom parsers.

Suggested fix
 	if (json) {
 		return console.info(
 			JSON.stringify(
-				rules.map((rule) => {
-					// `@ts-expect-error` need to delete anyway
-					delete rule.when;
-					delete rule.apply;
-					delete rule.context;
-					return rule;
-				}),
+				rules.map(({ when, apply, context, ...rule }) => ({
+					...rule,
+					parse:
+						typeof rule.parse === "function" ? "function" : rule.parse,
+				})),
 				undefined,
 				2,
 			),
 		);
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (json) {
return console.info(
JSON.stringify(
rules.map((rule) => {
// @ts-expect-error need to delete anyway
delete rule.when;
delete rule.apply;
delete rule.context;
return rule;
}),
undefined,
2,
),
);
if (json) {
return console.info(
JSON.stringify(
rules.map(({ when, apply, context, ...rule }) => ({
...rule,
parse:
typeof rule.parse === "function" ? "function" : rule.parse,
})),
undefined,
2,
),
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/monorule/src/commands/list.ts` around lines 72 - 85, The current JSON
branch mutates the live rule objects by deleting fields on each rule in
rules.map; instead create and return a derived DTO per rule so config.rules is
not modified and function-valued fields are normalized. In the rules.map
callback (the anonymous mapper that currently deletes when/apply/context),
construct a new object (e.g., const dto = { ...rule }) and delete
dto.when/dto.apply/dto.context from that DTO; if dto.parse (or other parser
fields) is a function, replace it with a serializable placeholder or normalized
value (e.g., a string) rather than letting JSON.stringify drop it, then return
dto for JSON.stringify.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
core/monorule/example/index.ts (1)

1-3: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move zod to dependencies (or remove runtime zod imports from the shipped examples).

core/monorule/example/index.ts imports zod, and the same runtime import pattern exists in core/monorule/example/rules/hello.ts. However, core/monorule/package.json declares zod only under devDependencies ("zod": "catalog:schema") and not under dependencies, even though the package ships the example/ directory ("files": ["dist", "example"]). Add zod to dependencies, or rewrite the examples to avoid runtime zod imports (e.g., inline schema / type-only usage where possible).

import * as z from "zod";

import { defineConfig, defineRule, parsers } from "../dist/index.mjs";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/monorule/example/index.ts` around lines 1 - 3, The example files import
zod at runtime (e.g., the `import * as z from "zod";` lines in
core/monorule/example/index.ts and core/monorule/example/rules/hello.ts) while
core/monorule/package.json lists zod only as a devDependency; either add zod to
package.json "dependencies" or remove the runtime import by switching to
type-only usage or inlining validation. Concretely: if you want examples to ship
without adding a runtime dependency, change runtime `import * as z from "zod"`
to type-only imports (e.g., `import type { ZodSchema } from "zod"`) and rewrite
any runtime schema validation in those example modules (used by `defineConfig`,
`defineRule`, `parsers`) to be pure types or simple inline checks; otherwise add
`"zod": \"catalog:schema\"` (or the appropriate version) to
core/monorule/package.json dependencies so the shipped example imports resolve
at runtime.
core/monorule/README.md (1)

27-37: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document the zod dependency used in the usage example.

Line 34 imports zod, but the Installation section only documents @stephansama/monorule. Add zod to the install instructions or use only built-in parsers in the example.

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

In `@core/monorule/README.md` around lines 27 - 37, The README usage imports zod
(import * as z from "zod") but the install instructions only list
`@stephansama/monorule`; update the installation line so it installs zod as well
(e.g., add zod to the pnpm install command) or alternatively remove the zod
import and adjust the example to use only built-in parsers (references: the
import line for zod and the usage of defineConfig, defineRule, parsers) so the
example and the Installation section remain consistent.
core/monorule/package.json (1)

70-70: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Lower engines.node requirement (currently >=24) to avoid unnecessary user restriction.

core/monorule’s source/build output imports only standard built-ins (node:fs, node:path, node:url, node:tty, node:util) and does not reference Node-24+ specific APIs (e.g., node:test, node:timers/promises, node:stream/web, node:worker_threads). If there’s no other dependency/runtime requirement, set engines.node to >=18 (or >=20) instead of >=24.

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

In `@core/monorule/package.json` at line 70, The package.json in core/monorule
currently pins engines.node to ">=24", which needlessly restricts users; update
the "engines.node" field in core/monorule's package.json (the engines.node
entry) to a lower version such as ">=18" or ">=20" after confirming no runtime
APIs require Node 24+ (the package only imports node:fs, node:path, node:url,
node:tty, node:util). Change the value of the engines.node property accordingly
and run a quick local install/build to verify compatibility.
♻️ Duplicate comments (2)
core/monorule/src/schema.ts (1)

13-14: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Constrain parse/when function contracts in schema validation.

With unconstrained z.function() on Line 13 and Line 14, invalid rule signatures pass schema validation, but check.ts consumes when as Array<Error> | void and can break at runtime.

Proposed fix
 export const ruleSchema = z.object({
 	apply: z.function().optional(),
 	enabled: z.boolean().default(true),
 	exclude: z.array(z.string().trim()).or(z.string().trim()).optional(),
 	include: z.union([z.array(z.string().trim()), z.string().trim()]),
 	name: z.string().trim(),
-	parse: z.function().or(builtinParseEnumSchema),
-	when: z.function(),
+	parse: z
+		.function({
+			input: [z.string()],
+			output: z.unknown(),
+		})
+		.or(builtinParseEnumSchema),
+	when: z.function({
+		input: [z.unknown(), z.unknown().optional()],
+		output: z.union([
+			z.array(
+				z.object({
+					id: z.string(),
+					message: z.string(),
+					fixable: z.boolean().optional(),
+				}),
+			),
+			z.void(),
+		]),
+	}),
 });
#!/bin/bash
# Verify runtime expectations vs schema function constraints
rg -nP 'parse\(raw,\s*rule\.parse\)|rule\.when\(|errors\.length|push\(\.\.\.errors\)' core/monorule/src/check.ts
rg -nP 'parse:\s*z\.function|when:\s*z\.function' core/monorule/src/schema.ts
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/monorule/src/schema.ts` around lines 13 - 14, The schema's parse and
when are currently unconstrained z.function() which allows invalid signatures;
update both in schema.ts to explicitly require a function taking (raw: unknown)
and returning Array<Error> | void so runtime consumers in check.ts (which treat
rule.when/parse results as Array<Error> | void) don't break: replace parse:
z.function() and when: z.function() with e.g.
z.function().args(z.unknown()).returns(z.union([z.array(z.instanceof(Error)),
z.undefined()])), ensuring the function signature matches what check.ts expects.
core/monorule/src/rules/load.ts (1)

15-29: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Always filter import candidates and guarantee unregister() via finally.

Line 15 reads all directory entries (files + directories), and any throw in Lines 19-25 currently bypasses cleanup because unregister() is outside a finally. One bad entry can fail rule loading and leave the tsx hook registered.

Suggested fix
-	const files = await fs.promises.readdir(absolute);
+	const entries = await fs.promises.readdir(absolute, { withFileTypes: true });
+	const files = entries
+		.filter((entry) => entry.isFile())
+		.map((entry) => entry.name)
+		.filter((file) =>
+			[".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"].includes(
+				path.extname(file),
+			),
+		);

 	const unregister = register();

-	const loadedRules = await Promise.all(
-		files.map(async (file) => {
-			info(`loading ${file}`);
-			// convert to object to remove module namespace key
-			const imported = (await import(path.join(absolute, file))) as {};
-			return Object.values(ruleMapSchema.parse({ ...imported }));
-		}),
-	);
-
-	await unregister();
-
-	return loadedRules.flat();
+	try {
+		const loadedRules = await Promise.all(
+			files.map(async (file) => {
+				info(`loading ${file}`);
+				// convert to object to remove module namespace key
+				const imported = (await import(path.join(absolute, file))) as {};
+				return Object.values(ruleMapSchema.parse({ ...imported }));
+			}),
+		);
+
+		return loadedRules.flat();
+	} finally {
+		await unregister();
+	}
#!/bin/bash
# Verify the current implementation reads all entries and does not protect unregister with finally.
rg -n -C3 'readdir\(|register\(|unregister\(|Promise\.all|import\(' core/monorule/src/rules/load.ts
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/monorule/src/rules/load.ts` around lines 15 - 29, The code currently
reads all directory entries and registers a hook but may skip cleanup if an
import throws; change readdir to use fs.promises.readdir(absolute, {
withFileTypes: true }) and filter to regular files with allowed extensions
(e.g., .js/.cjs/.mjs/.ts/.tsx) before mapping, so only valid import candidates
are passed to import(path.join(absolute, file)). Move the unregister() call into
a finally block that surrounds the await Promise.all(...) so
register()/unregister() are always paired, and inside the Promise.all mapping
catch and handle per-file import/parse errors (so a single bad module doesn't
reject the whole Promise.all) while still returning parsed rule objects to be
flattened via ruleMapSchema.parse and Object.values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/monorule/package.json`:
- Around line 36-37: Remove the TS-only example from the published package by
deleting "example" from the package.json "files" array (leaving only "dist"), or
alternatively replace it with a documented JS/docs build of the examples; update
package.json to remove "example" from "files" and, if those examples are still
needed for local dev scripts (cli:config, cli:list, cli:rules), keep them in
repo but exclude them from publishing or add a build step that emits a
consumer-ready JS/docs copy to be published instead.

In `@core/monorule/src/commands/generate.ts`:
- Around line 34-39: The exported helper fileExists is unused and its sole call
is incorrect; either remove the export (make fileExists internal) or delete the
function entirely and fix any remaining references in the generate command
implementation (remove or replace the incorrect call). Locate the fileExists
function and either change "export async function fileExists" to a non-exported
local function or remove the function and ensure generate (and any other code)
no longer relies on it.
- Around line 11-24: The flags object defines two flags, config and
ruleDirectory, that currently both use the alias "c" and the ruleDirectory
description is wrong; update ruleDirectory to use a unique alias (e.g., alias:
"r") and change its description to something like "rules directory location" (or
"rules directory") so it no longer conflicts with config and accurately
describes the flag; keep the existing defaults and types and only modify the
alias and description for the ruleDirectory entry.
- Around line 28-32: In act (function act) remove the incorrect
fileExists(arguments_.flags.help + "") call and replace it with the real
behavior: either remove the placeholder entirely or check a proper path flag
(e.g., arguments_.flags.path or arguments_.flags.out) if such exists; if the
command should generate defaults, implement the TODOs to create a default config
and rules directory (use fileExists only when validating a real path flag) and
ensure you reference the correct flag name instead of arguments_.flags.help
(help is a boolean from Cleye).
- Around line 8-9: Remove the unused constant _dirname (declared as const
_dirname = path.dirname(url.fileURLToPath(import.meta.url))); from generate.ts;
delete that line and, if doing so renders the imported symbols path or url
unused in this module, remove their imports as well to keep the file free of
unused variables/imports.

In `@core/monorule/src/parse.ts`:
- Around line 10-12: The txt parser currently uses a blind TypeScript cast
("txt: (input) => input as string") which can leak non-string values later;
change the txt handler in the builtins map to actually convert/validate input:
if typeof input === "string" return it, if input == null return empty string (or
a safe default), otherwise return a deterministic string representation (e.g.,
JSON.stringify(input) or String(input)) so non-string rule outputs don't break
the apply pipeline — update the txt entry in the same map (the
BuiltinParseEnumSchema record) accordingly while preserving the function
signature.

In `@core/monorule/src/rules/type.ts`:
- Around line 3-4: The current Rule type uses ReturnType<typeof defineRule>,
which collapses to the last overload and drops the FunctionParserRule shape;
change Rule to explicitly represent both overload returns (e.g. export type Rule
= FunctionParserRule | DefaultParserRule) and update RuleMap accordingly so maps
accept rules that implement either the functional parse signature (parse(input)
{...}) or the default parser shape; reference the defineRule overloads and the
FunctionParserRule and DefaultParserRule types when making the change.

In `@core/monorule/src/type.ts`:
- Around line 15-16: DefaultRuleParseTypeExtract currently maps non-"txt" parse
types to object, which is too narrow for parsers that can return primitives
(e.g., JSON/YAML); change the mapped type to a wider safe type (e.g., unknown)
so callbacks like when and apply receive a correct, non-unsafe type. Update the
type alias DefaultRuleParseTypeExtract<T extends BuiltinParseEnumSchema> to use
unknown (or a union of primitive|object) for the non-"txt" branch and adjust any
dependent signatures (when/apply) to accept that widened type so the compiler no
longer assumes object for non-txt parsers.

---

Outside diff comments:
In `@core/monorule/example/index.ts`:
- Around line 1-3: The example files import zod at runtime (e.g., the `import *
as z from "zod";` lines in core/monorule/example/index.ts and
core/monorule/example/rules/hello.ts) while core/monorule/package.json lists zod
only as a devDependency; either add zod to package.json "dependencies" or remove
the runtime import by switching to type-only usage or inlining validation.
Concretely: if you want examples to ship without adding a runtime dependency,
change runtime `import * as z from "zod"` to type-only imports (e.g., `import
type { ZodSchema } from "zod"`) and rewrite any runtime schema validation in
those example modules (used by `defineConfig`, `defineRule`, `parsers`) to be
pure types or simple inline checks; otherwise add `"zod": \"catalog:schema\"`
(or the appropriate version) to core/monorule/package.json dependencies so the
shipped example imports resolve at runtime.

In `@core/monorule/package.json`:
- Line 70: The package.json in core/monorule currently pins engines.node to
">=24", which needlessly restricts users; update the "engines.node" field in
core/monorule's package.json (the engines.node entry) to a lower version such as
">=18" or ">=20" after confirming no runtime APIs require Node 24+ (the package
only imports node:fs, node:path, node:url, node:tty, node:util). Change the
value of the engines.node property accordingly and run a quick local
install/build to verify compatibility.

In `@core/monorule/README.md`:
- Around line 27-37: The README usage imports zod (import * as z from "zod") but
the install instructions only list `@stephansama/monorule`; update the
installation line so it installs zod as well (e.g., add zod to the pnpm install
command) or alternatively remove the zod import and adjust the example to use
only built-in parsers (references: the import line for zod and the usage of
defineConfig, defineRule, parsers) so the example and the Installation section
remain consistent.

---

Duplicate comments:
In `@core/monorule/src/rules/load.ts`:
- Around line 15-29: The code currently reads all directory entries and
registers a hook but may skip cleanup if an import throws; change readdir to use
fs.promises.readdir(absolute, { withFileTypes: true }) and filter to regular
files with allowed extensions (e.g., .js/.cjs/.mjs/.ts/.tsx) before mapping, so
only valid import candidates are passed to import(path.join(absolute, file)).
Move the unregister() call into a finally block that surrounds the await
Promise.all(...) so register()/unregister() are always paired, and inside the
Promise.all mapping catch and handle per-file import/parse errors (so a single
bad module doesn't reject the whole Promise.all) while still returning parsed
rule objects to be flattened via ruleMapSchema.parse and Object.values.

In `@core/monorule/src/schema.ts`:
- Around line 13-14: The schema's parse and when are currently unconstrained
z.function() which allows invalid signatures; update both in schema.ts to
explicitly require a function taking (raw: unknown) and returning Array<Error> |
void so runtime consumers in check.ts (which treat rule.when/parse results as
Array<Error> | void) don't break: replace parse: z.function() and when:
z.function() with e.g.
z.function().args(z.unknown()).returns(z.union([z.array(z.instanceof(Error)),
z.undefined()])), ensuring the function signature matches what check.ts expects.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a1e7d592-342d-4c48-8fce-94c629cf272e

📥 Commits

Reviewing files that changed from the base of the PR and between 9459c20 and 6824e41.

📒 Files selected for processing (22)
  • core/monorule/README.md
  • core/monorule/__snapshots__/tsnapi/index.snapshot.d.ts
  • core/monorule/__snapshots__/tsnapi/index.snapshot.js
  • core/monorule/__snapshots__/tsnapi/schema.snapshot.d.ts
  • core/monorule/__snapshots__/tsnapi/schema.snapshot.js
  • core/monorule/example/index.ts
  • core/monorule/example/rules/hello.ts
  • core/monorule/package.json
  • core/monorule/src/apply.ts
  • core/monorule/src/check.ts
  • core/monorule/src/commands/generate.ts
  • core/monorule/src/commands/list.ts
  • core/monorule/src/config.ts
  • core/monorule/src/index.ts
  • core/monorule/src/parse.ts
  • core/monorule/src/paths.ts
  • core/monorule/src/rules/define.ts
  • core/monorule/src/rules/index.ts
  • core/monorule/src/rules/load.ts
  • core/monorule/src/rules/type.ts
  • core/monorule/src/schema.ts
  • core/monorule/src/type.ts

Comment thread core/monorule/package.json Outdated
Comment on lines +8 to +9
const _dirname = path.dirname(url.fileURLToPath(import.meta.url));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Remove unused _dirname variable.

The _dirname variable is declared but never referenced in this file.

♻️ Proposed fix
-const _dirname = path.dirname(url.fileURLToPath(import.meta.url));
-
 export const meta = command({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const _dirname = path.dirname(url.fileURLToPath(import.meta.url));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/monorule/src/commands/generate.ts` around lines 8 - 9, Remove the unused
constant _dirname (declared as const _dirname =
path.dirname(url.fileURLToPath(import.meta.url))); from generate.ts; delete that
line and, if doing so renders the imported symbols path or url unused in this
module, remove their imports as well to keep the file free of unused
variables/imports.

Comment on lines +11 to +24
flags: {
config: {
alias: "c",
default: "monorule.config.ts",
description: "config file location",
type: String,
},
ruleDirectory: {
alias: "c",
default: "rules",
description: "config file location",
type: String,
},
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix duplicate flag alias and incorrect description.

Both config and ruleDirectory flags use alias "c", which creates a conflict. Additionally, Line 21 incorrectly describes ruleDirectory as "config file location" instead of describing the rules directory.

🐛 Proposed fix
 	flags: {
 		config: {
 			alias: "c",
 			default: "monorule.config.ts",
 			description: "config file location",
 			type: String,
 		},
 		ruleDirectory: {
-			alias: "c",
+			alias: "r",
 			default: "rules",
-			description: "config file location",
+			description: "rules directory location",
 			type: String,
 		},
 	},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
flags: {
config: {
alias: "c",
default: "monorule.config.ts",
description: "config file location",
type: String,
},
ruleDirectory: {
alias: "c",
default: "rules",
description: "config file location",
type: String,
},
},
flags: {
config: {
alias: "c",
default: "monorule.config.ts",
description: "config file location",
type: String,
},
ruleDirectory: {
alias: "r",
default: "rules",
description: "rules directory location",
type: String,
},
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/monorule/src/commands/generate.ts` around lines 11 - 24, The flags
object defines two flags, config and ruleDirectory, that currently both use the
alias "c" and the ruleDirectory description is wrong; update ruleDirectory to
use a unique alias (e.g., alias: "r") and change its description to something
like "rules directory location" (or "rules directory") so it no longer conflicts
with config and accurately describes the flag; keep the existing defaults and
types and only modify the alias and description for the ruleDirectory entry.

Comment on lines +28 to +32
export async function act(arguments_: CliArguments) {
await fileExists(arguments_.flags.help + "");
// TODO: generate default configuration
// TODO: generate rules directory
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Incorrect usage of arguments_.flags.help as a file path.

Line 29 calls fileExists(arguments_.flags.help + ""), but help is a boolean flag from Cleye, so this will check for a file named "true" or "false" rather than a meaningful path. This appears to be placeholder code that should be removed or replaced with the actual implementation logic.

🐛 Proposed fix to remove the incorrect call
 export async function act(arguments_: CliArguments) {
-	await fileExists(arguments_.flags.help + "");
 	// TODO: generate default configuration
 	// TODO: generate rules directory
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function act(arguments_: CliArguments) {
await fileExists(arguments_.flags.help + "");
// TODO: generate default configuration
// TODO: generate rules directory
}
export async function act(arguments_: CliArguments) {
// TODO: generate default configuration
// TODO: generate rules directory
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/monorule/src/commands/generate.ts` around lines 28 - 32, In act
(function act) remove the incorrect fileExists(arguments_.flags.help + "") call
and replace it with the real behavior: either remove the placeholder entirely or
check a proper path flag (e.g., arguments_.flags.path or arguments_.flags.out)
if such exists; if the command should generate defaults, implement the TODOs to
create a default config and rules directory (use fileExists only when validating
a real path flag) and ensure you reference the correct flag name instead of
arguments_.flags.help (help is a boolean from Cleye).

Comment on lines +34 to +39
export async function fileExists(file: string) {
return await fs.promises
.access(file)
.then(() => true)
.catch(() => false);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider removing or internalizing fileExists helper.

The fileExists helper is exported but not used anywhere in the codebase (its only call on Line 29 is incorrect). If this helper is not needed for the public API, consider removing the export keyword or removing the function entirely until the generate command is implemented.

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

In `@core/monorule/src/commands/generate.ts` around lines 34 - 39, The exported
helper fileExists is unused and its sole call is incorrect; either remove the
export (make fileExists internal) or delete the function entirely and fix any
remaining references in the generate command implementation (remove or replace
the incorrect call). Locate the fileExists function and either change "export
async function fileExists" to a non-exported local function or remove the
function and ensure generate (and any other code) no longer relies on it.

Comment on lines +10 to +12
txt: (input) => input as string,
yaml: yaml.stringify,
} as const satisfies Record<BuiltinParseEnumSchema, (input: unknown) => string>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate txt stringify input instead of blind casting.

Line 10 uses a TypeScript cast, not conversion. If a rule returns a non-string for txt, this can surface later as a write failure in the apply pipeline.

Proposed fix
 export const stringifiers = {
 	json: JSON.stringify,
 	toml: toml.stringify,
-	txt: (input) => input as string,
+	txt: (input) => {
+		if (typeof input !== "string") {
+			throw new TypeError("txt stringify expects a string input");
+		}
+		return input;
+	},
 	yaml: yaml.stringify,
 } as const satisfies Record<BuiltinParseEnumSchema, (input: unknown) => string>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/monorule/src/parse.ts` around lines 10 - 12, The txt parser currently
uses a blind TypeScript cast ("txt: (input) => input as string") which can leak
non-string values later; change the txt handler in the builtins map to actually
convert/validate input: if typeof input === "string" return it, if input == null
return empty string (or a safe default), otherwise return a deterministic string
representation (e.g., JSON.stringify(input) or String(input)) so non-string rule
outputs don't break the apply pipeline — update the txt entry in the same map
(the BuiltinParseEnumSchema record) accordingly while preserving the function
signature.

Comment thread core/monorule/src/rules/type.ts Outdated
Comment thread core/monorule/src/type.ts
Comment on lines +15 to +16
export type DefaultRuleParseTypeExtract<T extends BuiltinParseEnumSchema> =
T extends "txt" ? string : object;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Widen non-text parse output typing to avoid unsafe assumptions.

Line 16 forces every non-"txt" parse result to object, but built-in parsers (notably JSON/YAML) can return primitives. That makes when/apply callbacks type-safe on paper and unsafe at runtime.

Proposed fix
 export type DefaultRuleParseTypeExtract<T extends BuiltinParseEnumSchema> =
-	T extends "txt" ? string : object;
+	T extends "txt" ? string : unknown;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type DefaultRuleParseTypeExtract<T extends BuiltinParseEnumSchema> =
T extends "txt" ? string : object;
export type DefaultRuleParseTypeExtract<T extends BuiltinParseEnumSchema> =
T extends "txt" ? string : unknown;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/monorule/src/type.ts` around lines 15 - 16, DefaultRuleParseTypeExtract
currently maps non-"txt" parse types to object, which is too narrow for parsers
that can return primitives (e.g., JSON/YAML); change the mapped type to a wider
safe type (e.g., unknown) so callbacks like when and apply receive a correct,
non-unsafe type. Update the type alias DefaultRuleParseTypeExtract<T extends
BuiltinParseEnumSchema> to use unknown (or a union of primitive|object) for the
non-"txt" branch and adjust any dependent signatures (when/apply) to accept that
widened type so the compiler no longer assumes object for non-txt parsers.

@socket-security

socket-security Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedempathic@​2.0.0 ⏵ 2.0.11001009384100

View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant