Skip to content

Add incremental build support and watch mode to leadtype generate#119

Merged
KayleeWilliams merged 3 commits into
mainfrom
watch-mode-incremental-builds
Jul 6, 2026
Merged

Add incremental build support and watch mode to leadtype generate#119
KayleeWilliams merged 3 commits into
mainfrom
watch-mode-incremental-builds

Conversation

@BurnedChris

Copy link
Copy Markdown
Contributor
  • Introduced --watch option for leadtype generate, enabling automatic rebuilds on changes to documentation sources and configuration files.
  • Implemented incremental builds by caching file inputs and skipping unchanged files, improving performance during repeated runs.
  • Updated the library API to support a new cache option in convertAllMdx, allowing for dependency tracking and cache invalidation.
  • Enhanced documentation to reflect these new features, including detailed explanations of the caching mechanism and watch mode behavior.

These changes significantly enhance the efficiency and usability of the leadtype CLI for documentation generation.

- Introduced `--watch` option for `leadtype generate`, enabling automatic rebuilds on changes to documentation sources and configuration files.
- Implemented incremental builds by caching file inputs and skipping unchanged files, improving performance during repeated runs.
- Updated the library API to support a new `cache` option in `convertAllMdx`, allowing for dependency tracking and cache invalidation.
- Enhanced documentation to reflect these new features, including detailed explanations of the caching mechanism and watch mode behavior.

These changes significantly enhance the efficiency and usability of the `leadtype` CLI for documentation generation.
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e84407d1-e2f5-4f19-9f00-844e3e883c8e

📥 Commits

Reviewing files that changed from the base of the PR and between 95f1767 and 2023a16.

📒 Files selected for processing (6)
  • docs/paths.lock.json
  • docs/reference/cli.mdx
  • packages/leadtype/src/cli/generate.ts
  • packages/leadtype/src/convert/convert.ts
  • packages/leadtype/src/convert/incremental.test.ts
  • packages/leadtype/src/convert/incremental.ts
📝 Walkthrough

Walkthrough

This PR adds --watch and --force to leadtype generate, introduces filesystem watching, and implements incremental MDX conversion caching with dependency tracking, stale output pruning, tests, and documentation updates.

Changes

Watch mode and incremental builds

Layer / File(s) Summary
File watcher module
packages/leadtype/src/cli/watch.ts, packages/leadtype/src/cli/watch.test.ts
watchInputs debounces and deduplicates filesystem events, applies ignore rules, and returns a closeable controller; tests cover batching, ignores, single-file watching, and invalid paths.
CLI watch/force flags and generate command routing
packages/leadtype/src/cli/generate.ts
GenerateArgs adds watch and force; parsing and help text are updated; generate now routes between one-shot and watch mode and resolves incremental cache options.
Incremental cache core module
packages/leadtype/src/convert/incremental.ts
New cache manifest types and helpers hash content, memoize file reads, load/save manifests, and verify dependency freshness.
convertAllMdx caching, dependency capture, and pruning
packages/leadtype/src/convert/convert.ts
Conversion options gain cache and dependency hooks; batch conversion reuses unchanged outputs, records dependency hashes, prunes stale outputs, and writes the updated manifest.
Type-table dependency reporting
packages/leadtype/src/markdown/plugins/type-table.ts
Type-table extraction reports dependency files through a new callback path and forwards host dependency tracking when no callback is provided.
Incremental cache test suite
packages/leadtype/src/convert/incremental.test.ts
Tests cover cache hits, source edits, include-driven rebuilds, dependency recording, pruning, forced rebuilds, fingerprint invalidation, missing outputs, and corrupt manifests.
Documentation and changeset
docs/reference/cli.mdx, docs/paths.lock.json, .changeset/watch-mode-incremental-builds.md
CLI docs, page hashes, and the changeset describe watch mode, incremental builds, pruning, and the cache API update.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant runGenerateCommand
  participant executeGenerate
  participant convertAllMdx
  participant watchInputs

  User->>runGenerateCommand: leadtype generate --watch
  runGenerateCommand->>executeGenerate: initial run
  executeGenerate->>convertAllMdx: convert with cache options
  convertAllMdx-->>executeGenerate: { cached, pruned, outputs }
  executeGenerate->>watchInputs: arm watcher with watchPaths
  watchInputs-->>runGenerateCommand: debounced change batch
  runGenerateCommand->>executeGenerate: rerun
Loading

Possibly related PRs

  • inthhq/leadtype#81: Also changes packages/leadtype/src/cli/generate.ts orchestration and command wiring.
  • inthhq/leadtype#111: Also modifies convertAllMdx and ConversionPrepareOptions plumbing in packages/leadtype/src/convert/convert.ts.
  • inthhq/leadtype#112: Also touches prepareMdxConversion and Git-enrichment-related conversion flow used by the cache fingerprint.

Poem

A rabbit taps the burrow floor,
Watches files and then some more.
Hashes hum and old paths fade,
Fresh builds bloom where changes made.
🐇 --watch wakes, --force can steer,
Incremental carrots, bright and clear.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: incremental builds and watch mode for leadtype generate.
Description check ✅ Passed The description accurately describes the cache-based incremental builds, watch mode, API update, and docs changes in the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch watch-mode-incremental-builds

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

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes — adds --watch/-w and default incremental builds to leadtype generate, with a content-hash manifest cache, dependency tracking for <include> targets and type-table TypeScript sources, and stale-output pruning.

  • CLI flags & docsparseGenerateArgs gains watch and force; help text and docs/reference/cli.mdx document incremental builds and watch mode; .changeset marks a minor bump.
  • Incremental cache (convert/incremental.ts) — per-file manifest under node_modules/.cache/leadtype/, recording sourceHash, per-dependency hashes, serialized git enrichment, and the output path. Every manifest miss (missing, corrupt, version or fingerprint mismatch) degrades to a full rebuild rather than erroring; the save path writes-then-renames to stay crash-safe.
  • convertAllMdx caching — skips a file only when its source hash, git enrichment, and all dependency hashes match and the prior output still exists; pruneStaleOutputs deletes outputs of deleted sources, scoped strictly inside outDir.
  • Dependency protocol — reuses the _compiler.addDependency file-data channel (already emitted by the include plugin) and extends TypeTableOptions with onDependency so type-table TypeScript reads invalidate the cache too.
  • Cache fingerprintresolveConvertCache keys the cache on leadtype version, docs-config content hash, and conversion-relevant flags; caching is skipped entirely when there is no node_modules/.
  • Watch loop (cli/watch.ts, runGenerateWatch) — debounced recursive fs.watch (fine given engines >= 22), ignores outDir/.git/node_modules/.DS_Store, serializes reruns, re-arms when a config edit changes the docs-source set, and stops cleanly on SIGINT/SIGTERM.

I verified the load-bearing seam: downstream site artifacts (search index, llms.txt, agents) read the converted markdown from outDir on disk rather than from convertAllMdx's return value, so a cached skip that leaves the output in place stays consistent — no stale-artifact bug. Dependency paths are recorded as real source paths (mapped back through gitSourcePath), so they survive the temp-mirror churn between runs. Test coverage is strong. The points below are informational only; nothing blocks merge.

ℹ️ --force is sticky across every watch rebuild

In watch mode each rebuild re-invokes generation with the original parsed args, so leadtype generate --watch --force reconverts every file on every change for the life of the session rather than forcing once and then rebuilding incrementally. That is defensible (force means force), but a user reaching for "clean start, then fast iteration" gets a full rebuild on each keystroke-triggered change instead — worth a deliberate decision.

Technical details
# `--force` is sticky across every watch rebuild

## Affected sites
- `packages/leadtype/src/cli/generate.ts``runGenerateWatch``rerun` calls `executeGenerate(args, io)` with the same `args.force` on every change; `resolveConvertCache` sets `force: true` each run, so `previousEntries` is always `{}`.

## Required outcome
- Decide whether `--force` should apply only to the initial watch build (then fall back to incremental) or intentionally to every rebuild. Current behavior is the latter.

## Suggested approach (optional)
- If one-shot force is desired: clear `args.force` (or pass a per-run flag) after the first `executeGenerate` in `runGenerateWatch`, leaving subsequent reruns incremental.

## Open questions for the human
- Is "force every rebuild in watch" intended, or should force be first-build-only? The CLI docs currently say "restart with `--force`", which reads as one-shot.

ℹ️ No incremental test for type-table source invalidation

The changeset calls out type-table TypeScript sources as a newly tracked dependency, and the plugin now reports them through onDependency. The incremental test suite covers source edits, include-target edits, pruning, force, fingerprint, and corrupt-manifest recovery — but not the type-table path, so a regression in getFileDependencyReporter or the extractTypeFromFile dependency reporting would pass CI silently.

Technical details
# No incremental test for type-table source invalidation

## Affected sites
- `packages/leadtype/src/convert/incremental.test.ts` — has "rebuilds a file when its include target changes" but no equivalent for a type-table `.ts` source change.
- `packages/leadtype/src/markdown/plugins/type-table.ts:236-254`, `798-826` — the new dependency-reporting code paths (fresh extraction and cached-extraction branch) are untested for cache invalidation.

## Required outcome
- Add an incremental test asserting that editing a `.ts` file consumed by a type table rebuilds the page that renders it (and leaves an unrelated page cached), mirroring the existing include-target test.

ℹ️ Nitpicks

  • packages/leadtype/src/convert/incremental.ts:125saveConvertCacheManifest uses a fixed temp path ${filePath}.tmp; two concurrent generate processes on one project (e.g. a --watch session plus a manual run) could race on it. Impact is bounded (a corrupt manifest just triggers a rebuild), but a pid- or mkdtemp-suffixed temp name would remove the race.

Pullfrog  | View workflow run | Using Claude Opus𝕏

@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: 6

Caution

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

⚠️ Outside diff range comments (1)
packages/leadtype/src/cli/generate.ts (1)

2629-2722: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Watch session can't detect a docs directory created after startup.

When executeGenerate fails because a configured docs directory doesn't exist yet, the returned watchPaths still includes that nonexistent path. watchInputs (see watch.ts) silently skips watching paths that don't exist at arm-time, so if the user subsequently creates the missing docs directory, no change event fires for it — only edits to the config file (if it resolved) would trigger a re-arm. The watch session is then stuck requiring a manual restart, with no in-app hint.

🩹 Suggested mitigation: fall back to watching the nearest existing ancestor
+function nearestExistingAncestor(target: string): string {
+  let dir = target;
+  while (!existsSync(dir)) {
+    const parent = path.dirname(dir);
+    if (parent === dir) {
+      return dir;
+    }
+    dir = parent;
+  }
+  return dir;
+}

Use nearestExistingAncestor(candidate) when building watchPaths for any docs directory that doesn't yet exist, so its parent's creation events are observed and can trigger a re-check.

🤖 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 `@packages/leadtype/src/cli/generate.ts` around lines 2629 - 2722, The watch
path selection in executeGenerate returns a nonexistent docs directory directly,
so watchInputs never arms a watcher for it and later creation is missed. Update
executeGenerate to detect missing docsDirs and replace each missing path in
watchPaths with nearestExistingAncestor(candidate) so the parent directory
creation can retrigger the generate flow. Keep the change localized around the
docs directory existence check and watchPaths assembly, using
nearestExistingAncestor and executeGenerate as the key symbols.
🤖 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 `@packages/leadtype/src/cli/generate.ts`:
- Around line 2432-2483: The incremental cache is only enabled when
resolveConvertCache finds a node_modules directly under srcDir, which disables
caching for hoisted workspace layouts. Update resolveConvertCache in generate.ts
to locate the nearest usable node_modules by walking up from opts.srcDir
(Node-style resolution) instead of checking only the immediate child, while
keeping the existing cacheId/fingerprint logic and the ConvertCacheOptions file
path generation intact.
- Around line 2547-2607: The watch rerun path in runGenerateWatch keeps reusing
the same args object, so --force remains enabled for every rebuild and bypasses
the cache indefinitely. Update the watch flow so the force flag is only honored
for the initial executeGenerate call, then clear or override args.force before
subsequent reruns triggered by scheduleRerun/rerun. Make the change in
runGenerateWatch and keep the behavior aligned with resolveConvertCache so
incremental watch rebuilds can use the cache after the first pass.

In `@packages/leadtype/src/convert/convert.ts`:
- Around line 1357-1372: The dependency hashing in convert() is still done
sequentially inside the cacheState.nextEntries block, which unnecessarily
serializes I/O for dependencyPaths. Update the hashing logic near deps and
hashFileCached to mirror depsUnchanged by collecting all dependency hash
promises and awaiting them together with Promise.all, then build the deps record
from the resolved results while preserving the empty-string fallback for
unreadable or missing files.
- Around line 1270-1284: The cache manifest handling in convert.ts is dropping
prune history whenever config.cache.force is set or loadConvertCacheManifest
returns null on a fingerprint mismatch, which causes stale outputs to survive
forever. Update the cacheState setup so prune candidates are loaded
independently of force/fingerprint invalidation, and keep a separate history
source for pruneStaleOutputs instead of reusing previousEntries for both reuse
checks and pruning. Add a regression test in incremental.test.ts covering a
deleted source whose output is pruned across a force run or
fingerprint-invalidated run.

In `@packages/leadtype/src/convert/incremental.test.ts`:
- Around line 1-246: Add a test in incremental.test.ts that covers pruning after
a force run or fingerprint change using createCacheProject and runConvert. The
issue is that the current prune test only exercises the normal cache path, so it
misses the case where cacheState.previousEntries is cleared by force/fingerprint
invalidation and deleted outputs may not be removed. Reuse the existing helpers
(runConvert, tamper, existsSync, rm) to delete a source after an initial
conversion, rerun with force or a different fingerprint, and assert the deleted
file’s output is pruned while the remaining output stays intact.

In `@packages/leadtype/src/convert/incremental.ts`:
- Around line 79-116: The manifest validator in isManifestShape only checks that
entries exists as an object, so malformed ConvertCacheEntry data can slip
through and later crash pruneStaleOutputs when it reads entry.output. Tighten
isManifestShape in incremental.ts to validate every entry’s shape, including
required string/object fields such as sourceHash, deps, enrichment, and output,
and only return true when all entries match ConvertCacheEntry. Keep
loadConvertCacheManifest’s behavior unchanged so invalid manifests still fall
back to null and a full rebuild.

---

Outside diff comments:
In `@packages/leadtype/src/cli/generate.ts`:
- Around line 2629-2722: The watch path selection in executeGenerate returns a
nonexistent docs directory directly, so watchInputs never arms a watcher for it
and later creation is missed. Update executeGenerate to detect missing docsDirs
and replace each missing path in watchPaths with
nearestExistingAncestor(candidate) so the parent directory creation can
retrigger the generate flow. Keep the change localized around the docs directory
existence check and watchPaths assembly, using nearestExistingAncestor and
executeGenerate as the key symbols.
🪄 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: 487a745a-9c1b-47a1-b20d-6a660a72251e

📥 Commits

Reviewing files that changed from the base of the PR and between b79b8ca and 558f1f8.

📒 Files selected for processing (9)
  • .changeset/watch-mode-incremental-builds.md
  • docs/reference/cli.mdx
  • packages/leadtype/src/cli/generate.ts
  • packages/leadtype/src/cli/watch.test.ts
  • packages/leadtype/src/cli/watch.ts
  • packages/leadtype/src/convert/convert.ts
  • packages/leadtype/src/convert/incremental.test.ts
  • packages/leadtype/src/convert/incremental.ts
  • packages/leadtype/src/markdown/plugins/type-table.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use explicit types for function parameters and return values when they enhance clarity
Prefer unknown over any when the type is genuinely unknown
Use const assertions (as const) for immutable values and literal types
Leverage TypeScript's type narrowing instead of type assertions

Files:

  • packages/leadtype/src/cli/watch.test.ts
  • packages/leadtype/src/cli/watch.ts
  • packages/leadtype/src/convert/incremental.test.ts
  • packages/leadtype/src/convert/incremental.ts
  • packages/leadtype/src/markdown/plugins/type-table.ts
  • packages/leadtype/src/convert/convert.ts
  • packages/leadtype/src/cli/generate.ts
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use meaningful variable names instead of magic numbers - extract constants with descriptive names
Use arrow functions for callbacks and short functions
Prefer for...of loops over .forEach() and indexed for loops
Use optional chaining (?.) and nullish coalescing (??) for safer property access
Prefer template literals over string concatenation
Use destructuring for object and array assignments
Use const by default, let only when reassignment is needed, never var
Always await promises in async functions - don't forget to use the return value
Use async/await syntax instead of promise chains for better readability
Handle errors appropriately in async code with try-catch blocks
Don't use async functions as Promise executors
Remove console.log, debugger, and alert statements from production code
Throw Error objects with descriptive messages, not strings or other values
Use try-catch blocks meaningfully - don't catch errors just to rethrow them
Prefer early returns over nested conditionals for error cases
Extract complex conditions into well-named boolean variables
Use early returns to reduce nesting
Prefer simple conditionals over nested ternary operators
Don't use eval() or assign directly to document.cookie
Avoid spread syntax in accumulators within loops
Use top-level regex literals instead of creating them in loops
Prefer specific imports over namespace imports
Use descriptive names for functions, variables, and types for meaningful naming
Add comments for complex logic, but prefer self-documenting code

Files:

  • packages/leadtype/src/cli/watch.test.ts
  • packages/leadtype/src/cli/watch.ts
  • packages/leadtype/src/convert/incremental.test.ts
  • packages/leadtype/src/convert/incremental.ts
  • packages/leadtype/src/markdown/plugins/type-table.ts
  • packages/leadtype/src/convert/convert.ts
  • packages/leadtype/src/cli/generate.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: Write assertions inside it() or test() blocks
Avoid done callbacks in async tests - use async/await instead
Don't use .only or .skip in committed code
Keep test suites reasonably flat - avoid excessive describe nesting

Files:

  • packages/leadtype/src/cli/watch.test.ts
  • packages/leadtype/src/convert/incremental.test.ts
🧠 Learnings (1)
📚 Learning: 2026-06-09T18:30:08.038Z
Learnt from: KayleeWilliams
Repo: inthhq/leadtype PR: 97
File: .changeset/search-prototype-safety-and-scaling.md:5-5
Timestamp: 2026-06-09T18:30:08.038Z
Learning: In this repo, `.changeset/*.md` files must not start the body with an H1/first-line heading (`#`) immediately after the YAML frontmatter. The changesets tool inlines the body as bullet entries into `CHANGELOG.md` during release, and a leading `#` heading would break the generated changelog format. As a result, MD041 (`first-line-heading`) warnings for files under `.changeset/` are expected false positives and should be ignored.

Applied to files:

  • .changeset/watch-mode-incremental-builds.md
🪛 ast-grep (0.44.0)
packages/leadtype/src/convert/convert.ts

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🪛 markdownlint-cli2 (0.22.1)
.changeset/watch-mode-incremental-builds.md

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

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

🔇 Additional comments (13)
.changeset/watch-mode-incremental-builds.md (1)

1-12: 📐 Maintainability & Code Quality

MD041 warning is an expected false positive for this repo's changeset files.

Based on learnings, .changeset/*.md files intentionally must not start with an H1 heading immediately after frontmatter, since the changesets tool inlines the body as bullet entries into CHANGELOG.md; a leading # would break that format. The static-analysis MD041 hint here should be ignored.

Source: Learnings

packages/leadtype/src/cli/watch.ts (1)

1-133: LGTM!

packages/leadtype/src/cli/watch.test.ts (1)

1-158: LGTM!

packages/leadtype/src/cli/generate.ts (5)

1-11: LGTM!

Also applies to: 77-77


183-186: LGTM!

Also applies to: 354-366, 396-397, 445-448


2401-2413: LGTM!


2536-2540: LGTM!

Also applies to: 2609-2634, 2694-2699, 2721-2721, 2779-2789, 3161-3165


2414-2430: 🗄️ Data Integrity & Integration

Drop the self-reference warning for leadtype/package.json. packages/leadtype/package.json already exports "./package.json", so createRequire(import.meta.url) can resolve it and this fallback won’t be triggered by package exports alone.

			> Likely an incorrect or invalid review comment.
docs/reference/cli.mdx (1)

60-79: LGTM!

packages/leadtype/src/convert/convert.ts (2)

1458-1497: 🎯 Functional Correctness

Covered by the pruning-history comment above.

The entry.output traversal-guard here is solid, but this function's inputs need the fix proposed on Lines 1270-1284 (using history-preserving prune candidates and per-entry validation) to avoid silently losing prune opportunities and to avoid crashing on malformed manifest entries.


3-3: LGTM!

Also applies to: 37-47, 347-352, 374-379, 842-852, 1286-1356, 1401-1452

packages/leadtype/src/convert/incremental.ts (1)

1-77: LGTM!

Also applies to: 118-147

packages/leadtype/src/markdown/plugins/type-table.ts (1)

192-212: LGTM!

Also applies to: 236-255, 910-932, 1091-1113

Comment thread packages/leadtype/src/cli/generate.ts
Comment thread packages/leadtype/src/cli/generate.ts
Comment thread packages/leadtype/src/convert/convert.ts Outdated
Comment thread packages/leadtype/src/convert/convert.ts
Comment thread packages/leadtype/src/convert/incremental.test.ts
Comment thread packages/leadtype/src/convert/incremental.ts
Resolves conflicts with the recently merged prune (#125) and redirect
tracking (#126) work: incremental-cache writes now go through
writeFileAtomic (concurrent-generate safety from #121), the cache option
composes with prune/pruneKeep on the generate convert call, and the
lock-acquisition failure path returns the new GenerateOutcome shape.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes — the delta since the prior pullfrog review is the merge of main (commit 95f17679), which integrates three main-side features with the incremental cache introduced earlier in this PR.

  • Atomic output writes — the cache write path in convertMdxBatch now uses writeFileAtomic (write-then-rename) instead of writeFile; the existsSync(outputPath) reuse check is unaffected.
  • prune: true alongside cacheconvertAllMdx is now invoked with both the incremental cache and redirect-tracking's prune/pruneKeep when redirectsEnabled.
  • Per-outDir generate lockacquireGenerateLock is taken in executeGenerate and released in its finally, so each watch rerun acquires and releases it fresh rather than holding across the session.
  • docs/paths.lock.json hash refresh — mechanical lockfile updates carried in by the merge.

I traced the load-bearing interaction between the incremental cache and the merged features. pruneAfterBatch builds its keep-set from the full mdxFiles source list rather than the files actually written this run, so a cached (skipped) output is never treated as orphaned. Redirect tracking reads its page set from the on-disk agent-readability manifest (agentReadability.manifest.pages) and reads emitted .md content from disk, so cached skips stay visible to rename/deletion detection while deleted sources still get pruned (by both pruneStaleOutputs and pruneOrphanedOutputs) so detection fires. The two prune paths overlap when both are active, but that is redundant work, not a correctness problem. Nothing here blocks merge.

Pullfrog  | View workflow run | Using Claude Opus𝕏

@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: 2

♻️ Duplicate comments (2)
packages/leadtype/src/convert/convert.ts (2)

1520-1528: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Sequential dependency hashing — still not parallelized (previously flagged).

Each dependency path is hashed one at a time in a for...of loop, unlike depsUnchanged which parallelizes via Promise.all. For pages with many dependencies (e.g. type-table pages), this serializes I/O unnecessarily.

⚡ Suggested fix
       if (cacheState && sourceHash) {
-        const deps: Record<string, string> = {};
-        for (const dependencyPath of dependencyPaths ?? []) {
-          deps[dependencyPath] =
-            (await hashFileCached(dependencyPath, cacheState.hashCache)) ?? "";
-        }
+        const deps = Object.fromEntries(
+          await Promise.all(
+            Array.from(dependencyPaths ?? [], async (dependencyPath) => [
+              dependencyPath,
+              (await hashFileCached(dependencyPath, cacheState.hashCache)) ??
+                "",
+            ])
+          )
+        );
🤖 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 `@packages/leadtype/src/convert/convert.ts` around lines 1520 - 1528, The
dependency hashing in convert.ts is still sequential inside the
cacheState/sourceHash block, which slows pages with many dependency paths.
Update the dependency collection in the `convert` flow to hash all
`dependencyPaths` in parallel, following the `depsUnchanged` pattern with
`Promise.all`, while preserving the current empty-string fallback for unreadable
or missing files. Keep the `deps` object construction tied to the same
`hashFileCached` and `cacheState.hashCache` usage so the rest of the logic
remains unchanged.

1433-1443: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Pruning history is still lost when the cache is invalidated (--force / fingerprint mismatch) — previously flagged, unresolved.

previousEntries is {} whenever config.cache.force is true or the fingerprint mismatches (Line 1436-1443), and this same (now-empty) previousEntries is what pruneStaleOutputs uses at Line 1571-1576 to decide what to prune. Any entry for a source deleted before a force/fingerprint-invalidated run is dropped from the manifest with no chance to prune its output — leaving an orphaned .md file in outDir permanently (e.g. after any leadtype version bump combined with a previously-removed doc).

This is the same issue raised on a prior commit; the code is unchanged.

♻️ Suggested fix: load prune candidates independently of force/fingerprint
+// in incremental.ts
+export async function loadConvertCacheEntriesForPrune(
+  filePath: string
+): Promise<Record<string, ConvertCacheEntry>> {
+  ...
+}
   const cacheState = config.cache
     ? {
         options: config.cache,
         previousEntries: config.cache.force
           ? {}
           : ((
               await loadConvertCacheManifest(
                 config.cache.file,
                 config.cache.fingerprint
               )
             )?.entries ?? {}),
+        pruneEntries: await loadConvertCacheEntriesForPrune(config.cache.file),
         nextEntries: {} as Record<string, ConvertCacheEntry>,
         hashCache: createFileHashCache(),
       }
     : undefined;
     pruned = await pruneStaleOutputs(
-      cacheState.previousEntries,
+      cacheState.pruneEntries,
       cacheState.nextEntries,
       currentKeys,
       outDir
     );

Also applies to: 1571-1576

🤖 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 `@packages/leadtype/src/convert/convert.ts` around lines 1433 - 1443, Pruning
history is still being lost when the cache is invalidated, because
`cacheState.previousEntries` in `convert.ts` is reset to an empty object on
`config.cache.force` or fingerprint mismatch and that same value is later
consumed by `pruneStaleOutputs`. Update the cache handling so stale-prune
candidates are loaded independently of the force/fingerprint invalidation path,
and ensure `pruneStaleOutputs` can still see prior manifest entries even when
the active cache is treated as invalid. Use the existing
`loadConvertCacheManifest`, `cacheState`, and `pruneStaleOutputs` flow to
preserve old entries for pruning while still invalidating cache reuse.
🤖 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 `@packages/leadtype/src/cli/generate.ts`:
- Around line 2228-2274: The pruning logic has a safety issue where the second
fg call that retrieves mirroredFiles can include files from other mounts if
output directories overlap or nest, then incorrectly delete them as stale when
comparing against currentFiles. When calling fg to get mirroredFiles from the
targetDir, add an ignore pattern to exclude other mount paths from the search
result, or alternatively filter the staleFiles computation to only consider
files that could have come from the current mount's source subtree. This ensures
that files belonging to other mounts are never mistakenly pruned as stale.

In `@packages/leadtype/src/convert/convert.ts`:
- Around line 1246-1288: The orphan-pruning and empty-directory cleanup logic in
pruneOrphanedOutputs duplicates the same upward-walk rmdir behavior used by
removeEmptyMirrorDirs. Extract the shared empty-parent cleanup into a common
internal helper (for example in internal/atomic-fs.ts) and have both
pruneOrphanedOutputs and removeEmptyMirrorDirs call it so the behavior stays
consistent and doesn’t drift across files.

---

Duplicate comments:
In `@packages/leadtype/src/convert/convert.ts`:
- Around line 1520-1528: The dependency hashing in convert.ts is still
sequential inside the cacheState/sourceHash block, which slows pages with many
dependency paths. Update the dependency collection in the `convert` flow to hash
all `dependencyPaths` in parallel, following the `depsUnchanged` pattern with
`Promise.all`, while preserving the current empty-string fallback for unreadable
or missing files. Keep the `deps` object construction tied to the same
`hashFileCached` and `cacheState.hashCache` usage so the rest of the logic
remains unchanged.
- Around line 1433-1443: Pruning history is still being lost when the cache is
invalidated, because `cacheState.previousEntries` in `convert.ts` is reset to an
empty object on `config.cache.force` or fingerprint mismatch and that same value
is later consumed by `pruneStaleOutputs`. Update the cache handling so
stale-prune candidates are loaded independently of the force/fingerprint
invalidation path, and ensure `pruneStaleOutputs` can still see prior manifest
entries even when the active cache is treated as invalid. Use the existing
`loadConvertCacheManifest`, `cacheState`, and `pruneStaleOutputs` flow to
preserve old entries for pruning while still invalidating cache reuse.
🪄 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: a7715a88-699e-432c-a4ba-9722f4ea8b8f

📥 Commits

Reviewing files that changed from the base of the PR and between 558f1f8 and 95f1767.

📒 Files selected for processing (3)
  • docs/paths.lock.json
  • packages/leadtype/src/cli/generate.ts
  • packages/leadtype/src/convert/convert.ts
📜 Review details
⚠️ CI failures not shown inline (2)

GitHub Actions: CI / 0_Validate & test.txt: Add incremental build support and watch mode to leadtype generate

Conclusion: failure

View job details

Current runner version: '2.335.1'
 ##[group]Runner Image Provisioner
 Hosted Compute Agent
 Version: 20260624.560
 Commit: 925d229a51159bc391ae97e54a2dd1fe20af789d
 Build Date:
 Worker ID: {84a1cb75-1d54-4cab-9b27-98228a50521a}
 Azure Region: eastus2
 ##[endgroup]
 ##[group]Operating System
 Ubuntu
 24.04.4
 LTS
 ##[endgroup]
 ##[group]Runner Image
 Image: ubuntu-24.04
 Version: 20260628.225.1
 Included Software: https://github.com/actions/runner-images/blob/ubuntu24/20260628.225/images/ubuntu/Ubuntu2404-Readme.md
 Image Release: https://github.com/actions/runner-images/releases/tag/ubuntu24%2F20260628.225
 ##[endgroup]
 ##[group]GITHUB_TOKEN Permissions
 Contents: read
 Metadata: read
 ##[endgroup]
 Secret source: Actions
 Prepare workflow directory
 Prepare all required actions
 Getting action download info
 Download action repository 'actions/checkout@v4' (SHA:34e114876b0b11c390a56381ad16ebd13914f8d5)
 Download action repository 'oven-sh/setup-bun@v2' (SHA:0c5077e51419868618aeaa5fe8019c62421857d6)
 Complete job name: Validate & test
 Node 20 is being deprecated. This workflow is running with Node 24 by default. If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable. For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/
 ##[group]Run actions/checkout@v4
 with:
   repository: inthhq/leadtype
   ***REDACTED***
   ssh-strict: true
   ssh-user: git
   persist-credentials: true
   clean: true
   sparse-checkout-cone-mode: true
   fetch-depth: 1
   fetch-tags: false
   show-progress: true
   lfs: false
   submodules: false
   set-safe-directory: true
 ##[endgroup]
 Syncing repository: inthhq/leadtype
 ##[group]Getting Git version info
 Working directory is '/home/runner/work/leadtype/leadtype'
 [command]/usr/bin/git version
 git version 2.54.0
 ##[endgroup]
 Temporarily overriding HOME='/home/runner/work/_temp/655fcd59-6728-4f10-9224-59414...

GitHub Actions: CI / Validate & test: Add incremental build support and watch mode to leadtype generate

Conclusion: failure

View job details

##[group]Run bun run --filter leadtype test
 �[36;1mbun run --filter leadtype test�[0m
 shell: /usr/bin/bash -e {0}
 ##[endgroup]
 leadtype test:
 leadtype test: �[1m�[30m�[46m RUN �[49m�[39m�[22m �[36mv4.1.5 �[39m�[90m/home/runner/work/leadtype/leadtype/packages/leadtype�[39m
 leadtype test:
 leadtype test:  �[32m✓�[39m src/llm/llm.test.ts �[2m(�[22m�[2m86 tests�[22m�[2m)�[22m�[33m 324�[2mms�[22m�[39m
 leadtype test:  �[32m✓�[39m src/openapi/openapi.test.ts �[2m(�[22m�[2m32 tests�[22m�[2m)�[22m�[33m 421�[2mms�[22m�[39m
 leadtype test: Converted 1 docs in 19 ms
 leadtype test: Converted 2 docs in 7 ms
 leadtype test: Converted 2 docs in 7 ms
 leadtype test: Pruned 1 orphaned .md file(s) from /tmp/leadtype-convert-WAk0Iw/public
 leadtype test: Converted 2 docs in 3 ms
 leadtype test: Converted 1 docs in 2 ms
 leadtype test: Pruned 2 orphaned .md file(s) from /tmp/leadtype-convert-CV8zhC/public
 leadtype test: Converted 1 docs in 3 ms
 leadtype test: Pruned 1 orphaned .md file(s) from /tmp/leadtype-convert-kXjOYm/public
 leadtype test: Converted 1 docs in 1 ms
 leadtype test: Pruned 1 orphaned .md file(s) from /tmp/leadtype-convert-2NrDCJ/public
 leadtype test: Converted 1 docs in 2 ms
 leadtype test: Error: failed to process /tmp/leadtype-convert-v4MQe3/docs/broken.mdx: 3:1: Unexpected character after `<`, expected a valid JSX tag (note: to create a link in MDX, use `[text](url)`) (mdx-jsx:unexpected-character)
 leadtype test: Converted 1 docs in 3 ms (1 failed)
 leadtype test: Warning: prune skipped: 1 file(s) failed to convert, so the expected output set is incomplete.
 leadtype test: Warning: prune skipped: no .mdx sources under /tmp/leadtype-convert-fM7b8H/empty-docs — refusing to treat every output in /tmp/leadtype-convert-fM7b8H/public as orphaned. Check srcDir if this is unexpected.
 leadtype test: Converted 3 docs in 11 ms
 leadtype test: Converted 2 docs in 4 ms
 leadtype test: Converted 1 docs in 1 ms
 leadtype test:  �[32m✓�[39m src/convert/convert.test...
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use explicit types for function parameters and return values when they enhance clarity
Prefer unknown over any when the type is genuinely unknown
Use const assertions (as const) for immutable values and literal types
Leverage TypeScript's type narrowing instead of type assertions

Files:

  • packages/leadtype/src/convert/convert.ts
  • packages/leadtype/src/cli/generate.ts
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use meaningful variable names instead of magic numbers - extract constants with descriptive names
Use arrow functions for callbacks and short functions
Prefer for...of loops over .forEach() and indexed for loops
Use optional chaining (?.) and nullish coalescing (??) for safer property access
Prefer template literals over string concatenation
Use destructuring for object and array assignments
Use const by default, let only when reassignment is needed, never var
Always await promises in async functions - don't forget to use the return value
Use async/await syntax instead of promise chains for better readability
Handle errors appropriately in async code with try-catch blocks
Don't use async functions as Promise executors
Remove console.log, debugger, and alert statements from production code
Throw Error objects with descriptive messages, not strings or other values
Use try-catch blocks meaningfully - don't catch errors just to rethrow them
Prefer early returns over nested conditionals for error cases
Extract complex conditions into well-named boolean variables
Use early returns to reduce nesting
Prefer simple conditionals over nested ternary operators
Don't use eval() or assign directly to document.cookie
Avoid spread syntax in accumulators within loops
Use top-level regex literals instead of creating them in loops
Prefer specific imports over namespace imports
Use descriptive names for functions, variables, and types for meaningful naming
Add comments for complex logic, but prefer self-documenting code

Files:

  • packages/leadtype/src/convert/convert.ts
  • packages/leadtype/src/cli/generate.ts
🪛 ast-grep (0.44.1)
packages/leadtype/src/convert/convert.ts

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🔇 Additional comments (7)
packages/leadtype/src/cli/generate.ts (3)

2974-2992: 🎯 Functional Correctness

Please confirm past-review concerns on resolveConvertCache/watch reruns were addressed.

Two previously-raised concerns on this file's cache/watch wiring aren't visible in the current diff context (the resolveConvertCache and runGenerateWatch bodies fall in the unchanged 2524-2747 range not shown here):

  1. Cache silently disabled for hoisted monorepo node_modules layouts (only checks srcDir/node_modules directly).
  2. --watch --force keeps forcing a full rebuild on every rerun, not just the first, since args is never mutated between reruns.

Since the underlying functions aren't part of this review's visible context, please confirm whether these were addressed in the current implementation.

#!/bin/bash
# Inspect current resolveConvertCache / runGenerateWatch implementations.
rg -nP -A30 'function resolveConvertCache' packages/leadtype/src/cli/generate.ts
rg -nP -A40 'function runGenerateWatch' packages/leadtype/src/cli/generate.ts

1-29: LGTM!

Also applies to: 77-86, 202-263, 305-329, 1281-1411, 1872-1936, 2396-2457, 2520-2523


2748-2862: LGTM!

Also applies to: 3394-3403

packages/leadtype/src/convert/convert.ts (3)

43-53: LGTM!

Also applies to: 370-376, 397-402, 1140-1204


1294-1359: LGTM!

Also applies to: 1564-1591


1433-1443: 🎯 Functional Correctness

No cache fingerprint gap The build-wide fingerprint already hashes the full docs config file, so config-only changes to markdownTransforms, frontmatterSchema, typeTableBasePath/typeTableStrict, and transformers invalidate the cache.

			> Likely an incorrect or invalid review comment.
docs/paths.lock.json (1)

1-218: LGTM!

@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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

♻️ Duplicate comments (2)
packages/leadtype/src/convert/convert.ts (2)

1520-1528: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Sequential dependency hashing — still not parallelized (previously flagged).

Each dependency path is hashed one at a time in a for...of loop, unlike depsUnchanged which parallelizes via Promise.all. For pages with many dependencies (e.g. type-table pages), this serializes I/O unnecessarily.

⚡ Suggested fix
       if (cacheState && sourceHash) {
-        const deps: Record<string, string> = {};
-        for (const dependencyPath of dependencyPaths ?? []) {
-          deps[dependencyPath] =
-            (await hashFileCached(dependencyPath, cacheState.hashCache)) ?? "";
-        }
+        const deps = Object.fromEntries(
+          await Promise.all(
+            Array.from(dependencyPaths ?? [], async (dependencyPath) => [
+              dependencyPath,
+              (await hashFileCached(dependencyPath, cacheState.hashCache)) ??
+                "",
+            ])
+          )
+        );
🤖 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 `@packages/leadtype/src/convert/convert.ts` around lines 1520 - 1528, The
dependency hashing in convert.ts is still sequential inside the
cacheState/sourceHash block, which slows pages with many dependency paths.
Update the dependency collection in the `convert` flow to hash all
`dependencyPaths` in parallel, following the `depsUnchanged` pattern with
`Promise.all`, while preserving the current empty-string fallback for unreadable
or missing files. Keep the `deps` object construction tied to the same
`hashFileCached` and `cacheState.hashCache` usage so the rest of the logic
remains unchanged.

1433-1443: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Pruning history is still lost when the cache is invalidated (--force / fingerprint mismatch) — previously flagged, unresolved.

previousEntries is {} whenever config.cache.force is true or the fingerprint mismatches (Line 1436-1443), and this same (now-empty) previousEntries is what pruneStaleOutputs uses at Line 1571-1576 to decide what to prune. Any entry for a source deleted before a force/fingerprint-invalidated run is dropped from the manifest with no chance to prune its output — leaving an orphaned .md file in outDir permanently (e.g. after any leadtype version bump combined with a previously-removed doc).

This is the same issue raised on a prior commit; the code is unchanged.

♻️ Suggested fix: load prune candidates independently of force/fingerprint
+// in incremental.ts
+export async function loadConvertCacheEntriesForPrune(
+  filePath: string
+): Promise<Record<string, ConvertCacheEntry>> {
+  ...
+}
   const cacheState = config.cache
     ? {
         options: config.cache,
         previousEntries: config.cache.force
           ? {}
           : ((
               await loadConvertCacheManifest(
                 config.cache.file,
                 config.cache.fingerprint
               )
             )?.entries ?? {}),
+        pruneEntries: await loadConvertCacheEntriesForPrune(config.cache.file),
         nextEntries: {} as Record<string, ConvertCacheEntry>,
         hashCache: createFileHashCache(),
       }
     : undefined;
     pruned = await pruneStaleOutputs(
-      cacheState.previousEntries,
+      cacheState.pruneEntries,
       cacheState.nextEntries,
       currentKeys,
       outDir
     );

Also applies to: 1571-1576

🤖 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 `@packages/leadtype/src/convert/convert.ts` around lines 1433 - 1443, Pruning
history is still being lost when the cache is invalidated, because
`cacheState.previousEntries` in `convert.ts` is reset to an empty object on
`config.cache.force` or fingerprint mismatch and that same value is later
consumed by `pruneStaleOutputs`. Update the cache handling so stale-prune
candidates are loaded independently of the force/fingerprint invalidation path,
and ensure `pruneStaleOutputs` can still see prior manifest entries even when
the active cache is treated as invalid. Use the existing
`loadConvertCacheManifest`, `cacheState`, and `pruneStaleOutputs` flow to
preserve old entries for pruning while still invalidating cache reuse.
🤖 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 `@packages/leadtype/src/cli/generate.ts`:
- Around line 2228-2274: The pruning logic has a safety issue where the second
fg call that retrieves mirroredFiles can include files from other mounts if
output directories overlap or nest, then incorrectly delete them as stale when
comparing against currentFiles. When calling fg to get mirroredFiles from the
targetDir, add an ignore pattern to exclude other mount paths from the search
result, or alternatively filter the staleFiles computation to only consider
files that could have come from the current mount's source subtree. This ensures
that files belonging to other mounts are never mistakenly pruned as stale.

In `@packages/leadtype/src/convert/convert.ts`:
- Around line 1246-1288: The orphan-pruning and empty-directory cleanup logic in
pruneOrphanedOutputs duplicates the same upward-walk rmdir behavior used by
removeEmptyMirrorDirs. Extract the shared empty-parent cleanup into a common
internal helper (for example in internal/atomic-fs.ts) and have both
pruneOrphanedOutputs and removeEmptyMirrorDirs call it so the behavior stays
consistent and doesn’t drift across files.

---

Duplicate comments:
In `@packages/leadtype/src/convert/convert.ts`:
- Around line 1520-1528: The dependency hashing in convert.ts is still
sequential inside the cacheState/sourceHash block, which slows pages with many
dependency paths. Update the dependency collection in the `convert` flow to hash
all `dependencyPaths` in parallel, following the `depsUnchanged` pattern with
`Promise.all`, while preserving the current empty-string fallback for unreadable
or missing files. Keep the `deps` object construction tied to the same
`hashFileCached` and `cacheState.hashCache` usage so the rest of the logic
remains unchanged.
- Around line 1433-1443: Pruning history is still being lost when the cache is
invalidated, because `cacheState.previousEntries` in `convert.ts` is reset to an
empty object on `config.cache.force` or fingerprint mismatch and that same value
is later consumed by `pruneStaleOutputs`. Update the cache handling so
stale-prune candidates are loaded independently of the force/fingerprint
invalidation path, and ensure `pruneStaleOutputs` can still see prior manifest
entries even when the active cache is treated as invalid. Use the existing
`loadConvertCacheManifest`, `cacheState`, and `pruneStaleOutputs` flow to
preserve old entries for pruning while still invalidating cache reuse.
🪄 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: a7715a88-699e-432c-a4ba-9722f4ea8b8f

📥 Commits

Reviewing files that changed from the base of the PR and between 558f1f8 and 95f1767.

📒 Files selected for processing (3)
  • docs/paths.lock.json
  • packages/leadtype/src/cli/generate.ts
  • packages/leadtype/src/convert/convert.ts
📜 Review details
🔇 Additional comments (7)
packages/leadtype/src/cli/generate.ts (3)

2974-2992: 🎯 Functional Correctness

Please confirm past-review concerns on resolveConvertCache/watch reruns were addressed.

Two previously-raised concerns on this file's cache/watch wiring aren't visible in the current diff context (the resolveConvertCache and runGenerateWatch bodies fall in the unchanged 2524-2747 range not shown here):

  1. Cache silently disabled for hoisted monorepo node_modules layouts (only checks srcDir/node_modules directly).
  2. --watch --force keeps forcing a full rebuild on every rerun, not just the first, since args is never mutated between reruns.

Since the underlying functions aren't part of this review's visible context, please confirm whether these were addressed in the current implementation.

#!/bin/bash
# Inspect current resolveConvertCache / runGenerateWatch implementations.
rg -nP -A30 'function resolveConvertCache' packages/leadtype/src/cli/generate.ts
rg -nP -A40 'function runGenerateWatch' packages/leadtype/src/cli/generate.ts

1-29: LGTM!

Also applies to: 77-86, 202-263, 305-329, 1281-1411, 1872-1936, 2396-2457, 2520-2523


2748-2862: LGTM!

Also applies to: 3394-3403

packages/leadtype/src/convert/convert.ts (3)

43-53: LGTM!

Also applies to: 370-376, 397-402, 1140-1204


1294-1359: LGTM!

Also applies to: 1564-1591


1433-1443: 🎯 Functional Correctness

No cache fingerprint gap The build-wide fingerprint already hashes the full docs config file, so config-only changes to markdownTransforms, frontmatterSchema, typeTableBasePath/typeTableStrict, and transformers invalidate the cache.

			> Likely an incorrect or invalid review comment.
docs/paths.lock.json (1)

1-218: LGTM!

🛑 Comments failed to post (2)
packages/leadtype/src/cli/generate.ts (1)

2228-2274: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether mount urlPrefix/targetDir exclusivity is validated anywhere.
rg -nP -C5 'function\s+outputDirForUrlPrefix|function\s+validateDocsMounts' packages/leadtype/src

Repository: inthhq/leadtype

Length of output: 1714


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the mount validation and mirror-pruning implementation.
sed -n '867,980p' packages/leadtype/src/cli/generate.ts
printf '\n---\n'
sed -n '2190,2295p' packages/leadtype/src/cli/generate.ts

printf '\n=== search for mount overlap / exclusivity checks ===\n'
rg -n --context 3 'validateDocsMounts|outputDirForUrlPrefix|targetDir|urlPrefix|mirror|staleFiles|removeEmptyMirrorDirs|copyMountedMarkdownMirrors' packages/leadtype/src/cli/generate.ts

Repository: inthhq/leadtype

Length of output: 19856


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect how collections and mounts are resolved and whether overlapping URL prefixes are rejected.
sed -n '1979,2079p' packages/leadtype/src/cli/generate.ts
printf '\n---\n'
sed -n '1200,1465p' packages/leadtype/src/cli/generate.ts

printf '\n=== overlap-related searches ===\n'
rg -n --context 2 'seenUrlPrefixes|seenMounts|multiple collections share URL prefix|mounts|pathPrefixForUrlPrefix|normalizeUrlPrefix|normalizeDocsPath' packages/leadtype/src/cli/generate.ts

Repository: inthhq/leadtype

Length of output: 22305


Pruning needs an ownership boundary. Collections-level uniqueness checks don’t cover source.mounts/metadata.mounts, so fg("**/*.md", { cwd: targetDir }) can sweep in another mount’s files and delete them as stale if output dirs overlap or nest. Add an output-dir exclusivity check or prune from a per-mount manifest instead.

🤖 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 `@packages/leadtype/src/cli/generate.ts` around lines 2228 - 2274, The pruning
logic has a safety issue where the second fg call that retrieves mirroredFiles
can include files from other mounts if output directories overlap or nest, then
incorrectly delete them as stale when comparing against currentFiles. When
calling fg to get mirroredFiles from the targetDir, add an ignore pattern to
exclude other mount paths from the search result, or alternatively filter the
staleFiles computation to only consider files that could have come from the
current mount's source subtree. This ensures that files belonging to other
mounts are never mistakenly pruned as stale.
packages/leadtype/src/convert/convert.ts (1)

1246-1288: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate "prune stale files + remove empty parent dirs" logic vs. generate.ts.

pruneOrphanedOutputs/its empty-dir cleanup loop (Lines 1272-1285) closely mirrors removeEmptyMirrorDirs in packages/leadtype/src/cli/generate.ts (Lines 2276-2303) — same upward-walk-and-rmdir-until-non-empty pattern, duplicated across two files. Consider extracting a shared internal helper (e.g. in internal/atomic-fs.ts) to avoid the two implementations drifting.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 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 `@packages/leadtype/src/convert/convert.ts` around lines 1246 - 1288, The
orphan-pruning and empty-directory cleanup logic in pruneOrphanedOutputs
duplicates the same upward-walk rmdir behavior used by removeEmptyMirrorDirs.
Extract the shared empty-parent cleanup into a common internal helper (for
example in internal/atomic-fs.ts) and have both pruneOrphanedOutputs and
removeEmptyMirrorDirs call it so the behavior stays consistent and doesn’t drift
across files.

…on, hoisted node_modules, watch force semantics

- Stale-output pruning now works from the on-disk manifest even when
  --force or a fingerprint change disables entry reuse; previously a
  source deleted before an invalidated run left its output orphaned
  forever (the rewritten manifest lost the entry). Tests cover both
  invalidation paths and fail against the previous wiring.
- isManifestShape validates every cache entry, so a partially-written
  or hand-edited entry rejects the whole manifest (full rebuild)
  instead of crashing pruning on path.resolve(undefined).
- The cache manifest now lives in the nearest node_modules walking up
  from --src, so hoisted monorepo subpackages get caching too.
- --watch --force forces only the initial build; watch-triggered
  rebuilds stay incremental. Docs updated for both behavior changes.
@KayleeWilliams

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@KayleeWilliams
KayleeWilliams merged commit b5219fd into main Jul 6, 2026
4 checks passed
@KayleeWilliams
KayleeWilliams deleted the watch-mode-incremental-builds branch July 6, 2026 19:19
@KayleeWilliams KayleeWilliams mentioned this pull request Jul 6, 2026
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.

2 participants