Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
Deterministic AST-based codebase intelligence for AI agents.

`no-mistakes` answers structural questions about TypeScript, JavaScript,
React, Next.js, Playwright, queue, server-route, and Rust repository code
React, Next.js, Playwright, queue, server-route, CI-workflow,
Terraform/OpenTofu, and Swift code
without running the application or calling an AI model. It is built for agents
that need small, reliable answers they can feed into follow-up edits and tests.

Expand Down
185 changes: 124 additions & 61 deletions crates/no-mistakes/tests/docs_coverage.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use no_mistakes::codebase::{rules, unique_exports};
use no_mistakes::playwright::rules as playwright_rules;
use std::collections::{BTreeSet, VecDeque};
use std::path::{Path, PathBuf};

fn repo_root() -> PathBuf {
Expand All @@ -10,74 +11,136 @@ fn read(path: &Path) -> String {
std::fs::read_to_string(path).unwrap_or_else(|err| panic!("{}: {err}", path.display()))
}

fn joined_docs(dir: &Path) -> String {
let mut body = String::new();
let mut paths = std::fs::read_dir(dir)
.unwrap()
.map(|entry| entry.unwrap().path())
#[test]
fn cli_commands_have_docs() {
let root = repo_root();
let source = read(&root.join("crates/no-mistakes/src/main.rs"));
let index = read(&root.join("docs/cli/README.md"));
let command_block = source
.split_once("enum Command {")
.and_then(|(_, rest)| rest.split_once("\n}\n"))
.map(|(block, _)| block)
.expect("main.rs must define a closed Command enum");
Comment thread
jonathanong marked this conversation as resolved.
Outdated
assert!(
!command_block.lines().any(|line| line.contains("name =")),
"a clap command name override needs an explicit docs-coverage mapping"
);

let commands = command_block
.lines()
.filter_map(|line| {
let name = line.trim().split_once('(')?.0;
if name.is_empty() || name.starts_with('#') || name.starts_with("///") {
return None;
}
Some(kebab_case(name))
})
.collect::<Vec<_>>();
paths.sort();
for path in paths {
if path.extension().and_then(|ext| ext.to_str()) == Some("md") {
body.push_str(&read(&path));
body.push('\n');
assert!(
!commands.is_empty(),
"Command enum inventory must not be empty"
);

for command in commands {
let file = format!("{command}.md");
let path = root.join("docs/cli").join(&file);
assert!(path.exists(), "missing CLI doc {}", path.display());
assert!(
index.contains(&format!("({file})")),
"docs/cli/README.md must index {file}"
);
}

// Every leaf page must be reachable from the CLI index or its command
// group page. Follow only links rooted under docs/cli so an orphan page
// cannot make itself appear reachable by containing its own filename.
let cli_dir = root.join("docs/cli");
let linked_pages = reachable_cli_pages(&cli_dir);
for entry in std::fs::read_dir(root.join("docs/cli")).unwrap() {
let path = entry.unwrap().path();
if path.extension().and_then(|ext| ext.to_str()) != Some("md")
|| path.file_name().and_then(|name| name.to_str()) == Some("README.md")
{
continue;
}
let file = path.file_name().unwrap().to_string_lossy();
assert!(
linked_pages.contains(file.as_ref()),
"CLI page {file} is not linked by a CLI index or command group"
);
}
}

fn reachable_cli_pages(cli_dir: &Path) -> BTreeSet<String> {
let cli_dir = cli_dir.canonicalize().unwrap();
let mut seen = BTreeSet::new();
let mut pending = VecDeque::from([cli_dir.join("README.md")]);
while let Some(path) = pending.pop_front() {
let Ok(relative) = path.strip_prefix(&cli_dir) else {
continue;
};
let relative = relative.to_string_lossy().into_owned();
if !seen.insert(relative) {
continue;
}
let body = read(&path);
let mut remaining = body.as_str();
while let Some(start) = remaining.find("](") {
remaining = &remaining[start + 2..];
let Some(end) = remaining.find(')') else {
break;
};
let target = remaining[..end].split('#').next().unwrap_or_default();
remaining = &remaining[end + 1..];
if target.is_empty() || target.starts_with("http") {
continue;
}
let target_path = path.parent().unwrap().join(target);
if target_path.extension().and_then(|ext| ext.to_str()) != Some("md") {
continue;
}
let Ok(target_path) = target_path.canonicalize() else {
continue;
};
if target_path.starts_with(&cli_dir) {
pending.push_back(target_path);
}
}
}
seen
}

fn kebab_case(value: &str) -> String {
let mut result = String::new();
for (index, character) in value.chars().enumerate() {
if character.is_uppercase() && index != 0 {
result.push('-');
}
result.extend(character.to_lowercase());
}
body
result
}

#[test]
fn cli_leaf_commands_have_docs() {
fn node_runtime_exports_have_api_docs() {
let root = repo_root();
let cli_docs = joined_docs(&root.join("docs/cli"));
let commands = [
"dependencies",
"dependents",
"related",
"symbols",
"importers",
"exports-of",
"dead-exports",
"call-sites",
"resolve-check",
"fetches",
"flow",
"check",
"tests-plan",
"tests-targets",
"tests-impact",
"tests-why",
"tests-comment",
"tests-graph",
"playwright-check",
"playwright-edges",
"playwright-related",
"playwright-tests",
"react-analyze",
"react-check",
"react-usages",
"queues-edges",
"queues-related",
"queues-check",
"server-routes",
"server-edges",
"server-related",
"server-contracts",
"ci-impact",
"ci-env",
"ci-topology",
"impacted-checks",
"infra-resource-refs",
"infra-outputs",
"infra-test-for",
"swift-importers",
"swift-test-targets",
];
for command in commands {
let file = format!("{command}.md");
let path = root.join("docs/cli").join(&file);
assert!(path.exists(), "missing CLI doc {}", path.display());
assert!(cli_docs.contains(&file), "docs/cli/*.md must link {file}");
let source = read(&root.join("packages/no-mistakes/index.js"));
let docs = read(&root.join("docs/node-api.md"));
let exports = source
.lines()
.filter_map(|line| line.trim().strip_prefix("module.exports."))
.filter_map(|assignment| assignment.split_once(' ').map(|(name, _)| name))
.collect::<Vec<_>>();
assert!(
!exports.is_empty(),
"runtime export inventory must not be empty"
);
for export in exports {
assert!(
docs.lines()
.any(|line| line.starts_with('|') && line.contains(&format!("`{export}`"))),
Comment thread
jonathanong marked this conversation as resolved.
Outdated
"docs/node-api.md must map runtime export `{export}`"
);
}
}

Expand Down
7 changes: 7 additions & 0 deletions docs/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ command name. See [Performance diagnostics](diagnostics.md).
| [`resolve-check`](resolve-check.md) | Whether all imports in a file resolve. |
| [`fetches`](fetches.md) | Next.js routes mapped to static fetch API calls. |
| [`flow`](flow.md) | Compact dependency/symbol flow around one file or export. |
| [`data-pw`](data-pw.md) | Find selector-attribute usages of a value across source and tests. |
| [`effects`](effects.md) | Find configured transitive effect call sites from an entry file. |
| [`rsc-callers`](rsc-callers.md) | Find server components/pages that import a component. |
| [`registry-extension`](registry-extension.md) | Summarize how entries register in a registry file. |
| [`check`](check.md) | Configured project-wide checks. |
| [`lockfile`](lockfile.md) | Show which packages changed between two lockfile versions. |
| [`tests`](tests.md) | Test plan, impact, explanation, comments, and graphs. |
Expand All @@ -60,6 +64,9 @@ command name. See [Performance diagnostics](diagnostics.md).
| [`infra`](infra.md) | Terraform/OpenTofu resource, module, and output relationships. |
| [`swift`](swift.md) | Swift package importers and covering test targets. |

The [graph reference](graph.md) explains shared options and relationship
filters used by the graph commands.

## Shared Output Formats

Most commands accept `--format json|yml|md|paths|human` plus `--json`.
Expand Down
70 changes: 70 additions & 0 deletions docs/node-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,18 @@ const {
| `related` | `related(options)` |
| `symbols` | `symbols(options)` |
| `import-usages` | `importUsages(options)` |
| `importers` | `importers(options)` |
| `exports-of` | `exportsOf(options)` |
| `dead-exports` | `deadExports(options)` |
| `call-sites` | `callSites(options)` |
| `resolve-check` | `resolveCheck(options)` |
| `fetches` | `fetches(options)` |
| `flow` | `flow(options)` |
| `check` | `check(options)` |
| `data-pw` | `dataPw(options)` |
| `effects` | `effects(options)` |
| `rsc-callers` | `rscCallers(options)` |
| `registry-extension` | `registryExtension(options)` |
| `tests plan` | `testsPlan(options)`; `framework` accepts `vitest`, `playwright`, `dotnet`, or `swift` |
| `tests targets` | `testsTargets(options)` |
| `tests impact` | `testsImpact(options)` |
Expand All @@ -77,6 +86,67 @@ const {
| `ci topology` | `ciTopology(options)` |
| `impacted-checks` | `impactedChecks(options)` |

The following inventory is the complete runtime export surface. Keeping this
list exhaustive makes a newly added function visible to agents even when it
does not have a one-to-one CLI command:

| Runtime export | API |
| --- | --- |
| `createWorkflowTopologyIndex` | `createWorkflowTopologyIndex(topology)` |
| `version` | `version()` |
| `analyzeProject` | `analyzeProject(options)` |
| `callSites` | `callSites(options)` |
| `check` | `check(options)` |
| `ciEnv` | `ciEnv(options)` |
| `ciImpact` | `ciImpact(options)` |
| `ciTopology` | `ciTopology(options)` |
| `dataPw` | `dataPw(options)` |
| `deadExports` | `deadExports(options)` |
| `dependencies` | `dependencies(options)` |
| `dependents` | `dependents(options)` |
| `effects` | `effects(options)` |
| `exportsOf` | `exportsOf(options)` |
| `fetches` | `fetches(options)` |
| `flow` | `flow(options)` |
| `impactedChecks` | `impactedChecks(options)` |
| `importUsages` | `importUsages(options)` |
| `importers` | `importers(options)` |
| `infraOutputs` | `infraOutputs(options)` |
| `infraResourceRefs` | `infraResourceRefs(options)` |
| `infraTestFor` | `infraTestFor(options)` |
| `lockfileDiff` | `lockfileDiff(options)` |
| `validateMermaidMarkdown` | `validateMermaidMarkdown(options)` |
| `playwrightCheck` | `playwrightCheck(options)` |
| `playwrightEdges` | `playwrightEdges(options)` |
| `playwrightRelated` | `playwrightRelated(options)` |
| `playwrightTests` | `playwrightTests(options)` |
| `reactAnalyze` | `reactAnalyze(options)` |
| `reactCheck` | `reactCheck(options)` |
| `reactUsages` | `reactUsages(options)` |
| `registryExtension` | `registryExtension(options)` |
| `related` | `related(options)` |
| `resolveCheck` | `resolveCheck(options)` |
| `rscCallers` | `rscCallers(options)` |
| `swiftImporters` | `swiftImporters(options)` |
| `swiftTestTargets` | `swiftTestTargets(options)` |
| `symbols` | `symbols(options)` |
| `testsComment` | `testsComment(options)` |
| `testsGraphMermaid` | `testsGraphMermaid(options)` |
| `queueCheck` | `queueCheck(options)` |
| `queueEdges` | `queueEdges(options)` |
| `queueRelated` | `queueRelated(options)` |
| `queues` | `queues(options)` |
| `serverContracts` | `serverContracts(options)` |
| `serverRouteEdges` | `serverRouteEdges(options)` |
| `serverRouteList` | `serverRouteList(options)` |
| `serverRouteRelated` | `serverRouteRelated(options)` |
| `serverRoutes` | `serverRoutes(options)` |
| `testsGraph` | `testsGraph(options)` |
| `testsImpact` | `testsImpact(options)` |
| `testsPlan` | `testsPlan(options)` |
| `testsTargets` | `testsTargets(options)` |
| `testsWhy` | `testsWhy(options)` |

`testsTargets()` and test-plan targets set `workspace: true` when a Vitest
workspace/project-array source must be passed with `--workspace`; the emitted
`runner_args` already contain the correct flag. This includes configured and
Expand Down
19 changes: 17 additions & 2 deletions skills/no-mistakes/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,19 @@ no-mistakes symbols src/api.mts --include both --format json
no-mistakes import-usages --root . --filter 'src/**' --format json
no-mistakes symbols src/api.mts --mode signature-impact --symbol handler --format json

# Reuse one prepared analysis for related reports
node --input-type=module - <<'NODE'
import { analyzeProject } from "no-mistakes";
const report = await analyzeProject({
root: process.cwd(),
reports: [
{ type: "dependencies", files: ["src/api.mts"] },
{ type: "symbols", files: ["src/api.mts"], include: "both" },
],
});
console.log(report);
NODE

# Playwright coverage gate before finishing Next.js / Playwright work
no-mistakes playwright check --json
no-mistakes playwright related web/app/users/page.tsx --json
Expand Down Expand Up @@ -221,7 +234,7 @@ reports. Note: `analyzeProject` does not support `testsPlan`, `fetches`, or
- `--filter <GLOB>` to include only matching files; repeatable.
- `--target-module <GLOB>` to include only matching external module nodes (useful with `--relationship package`).
- `--test vitest|playwright|cargo|dotnet|swift` to filter to test files.
- `--relationship import|import-static|import-dynamic|import-type|import-require|route-import|workspace|package|test|route|queue|md|ci|workflow|workflow-job|workflow-step|workflow-needs|workflow-uses|workflow-run|workflow-artifact|http|process|asset|react|dotnet|swift|terraform|all`.
- `--relationship import|import-static|import-dynamic|import-type|import-require|route-import|workspace|package|test|route|queue|resource|md|ci|workflow|workflow-job|workflow-step|workflow-needs|workflow-uses|workflow-run|workflow-artifact|http|process|asset|react|dotnet|swift|terraform|all`.
- `--direction deps|dependents|both` for `queues related` and `server related`.
- `--format json|md|yml|paths|human`, `--json`, root-global `--timings` /
`--verbose-timings` (stderr), and `--jobs`.
Expand Down Expand Up @@ -256,7 +269,9 @@ member usage.

## Hard Limits

- `baseUrl`-only imports are not resolved; use `compilerOptions.paths`.
- `baseUrl`-only imports are resolved when `compilerOptions.baseUrl` is set.
Prefer `compilerOptions.paths` for explicit aliases, especially when an
alias should be shared across workspace packages.
- Dynamic `import()` and `require()` are tracked only with literals.
- `route-import` is deliberately conservative: it includes runtime static
imports/re-exports and literal dynamic imports inside functions, but excludes
Expand Down
8 changes: 5 additions & 3 deletions skills/no-mistakes/references/limits-and-fallbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ These patterns need extra care when using the module-graph tools. When you hit a

## baseUrl-only imports

`compilerOptions.baseUrl` resolves bare specifiers not listed in `paths`. The tool only reads `paths`, not `baseUrl`.
`compilerOptions.baseUrl` resolves bare specifiers that are not listed in
`paths`. The resolver follows that setting, including extension and index-file
candidates. Prefer `paths` for explicit aliases, especially when an alias is
shared across workspace packages.

```json
{
"compilerOptions": {
"baseUrl": "./src"
// import 'utils' resolves to './src/utils.ts' via baseUrl — NOT supported
// import 'utils' resolves to './src/utils.ts' via baseUrl
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}
```

**Workaround:** use `rg 'from .utils.' src/` — these imports still appear as literal strings.

## Dynamic import()

Expand Down
Loading