Skip to content

File Discovery

llms.txt lists every page for an agent

These utilities find the infrastructure files in a project and resolve the dependencies between entities.

Recursively scans a directory for TypeScript infrastructure files:

import { findInfraFiles } from "@intentius/chant";
const files = await findInfraFiles("./src");
// ["/project/src/resources.ts", "/project/src/config.ts"]

The function:

  • Returns absolute paths to .ts files
  • Excludes test files (*.test.ts, *.spec.ts)
  • Excludes files whose head carries chant’s generated-file marker or the skip marker
  • Walks the tree under the rules every discovery walk shares, below

Source discovery (findInfraFiles, behind build, graph, list and explain), chant lint, component discovery, Op discovery and chant audit all walk the tree the same way (#2527). Each keeps its own file filter (.ts, *.component.ts, *.op.ts, or every file for the audit), and the walk decides which directories and files reach that filter:

RuleWhat it does
node_modules and .gitnever entered
Dot-directoriesskipped, such as .cache/ or a tool’s staging copy of the project. chant audit still reads .github/ and .forgejo/, and records .terraform/ for TF023 without entering it
distskipped
Git-ignored filesskipped, the way git decides: nested .gitignore files, .git/info/exclude and the global excludes file all count. Only paths below the directory being scanned are asked about, so building or linting a directory that is itself ignored still reads it. chant audit leaves Terraform state paths to TF023, which reads .gitignore itself
Child projectswhen the scanned directory is inside a project, a subdirectory holding a project config is not entered. A chant.config.json holding only lint keys is not a project config and does not count; a chant.config.ts always does. When the scanned directory is outside every project, every child project is read
Op discoverythe Op root is still found by walking up from the working directory, and the walk down from it always stops at child projects, so a project with no config of its own no longer sees its siblings’ Ops. The project’s config is read for its globs only when some *.op.ts file sits where an include could reach it, so a project with no Op file has no config evaluated and no tsx loaded for Op discovery (#3976)
exclude and includethe project’s globs. exclude drops what it names, and include re-admits what exclude or any rule above would skip, apart from node_modules and .git
Workspace members and example groupseach member’s directory and each directory an example group matches leave the root project’s walk, and only when a workspace declaration exists. Without one, discovery loads no workspace code. A declaration that fails to read leaves the walk unchanged, and chant workspace ls reports its errors

Up to v0.80.0 the walks disagreed about every rule in this table apart from node_modules, and each walk decided child projects in its own way. Source and component discovery read the first child project they met as the project’s own source and skipped every later one. v0.80.0 printed a warning on stderr for each file whose status this changed, naming the glob that keeps the old behaviour. To keep reading a directory the walk now skips, add it to include:

{ "include": ["src"] }

The change is on the level-0 exception list.

Dynamically imports a TypeScript module and returns its exports:

import { importModule } from "@intentius/chant";
const exports = await importModule("./src/resources.ts");

On import failure, throws a DiscoveryError with type "import".

Collects all Declarable entities from imported modules:

import { collectEntities } from "@intentius/chant";
const entities = collectEntities(modules);
// Map { "MyResource" => {...}, "MyFunction" => {...} }

Detects circular dependencies in an entity graph using DFS:

import { detectCycles } from "@intentius/chant";
const cycles = detectCycles(graph);

Orders entities so dependencies appear before dependents:

import { topologicalSort } from "@intentius/chant";
const order = topologicalSort(deps);
// ["Database", "Cache", "App"]

Builds a dependency graph from entity relationships, detecting dependencies from AttrRef references and direct Declarable property values.

Resolves all AttrRef instances by setting their logical names:

import { resolveAttrRefs } from "@intentius/chant";
resolveAttrRefs(entities);

This enables AttrRef.toJSON() to work correctly during serialization.

The main discovery pipeline that orchestrates all steps:

import { discover } from "@intentius/chant";
const result = await discover("./src/infra");
// result.entities: Map of entity name to Declarable
// result.dependencies: Map of entity to dependency set
// result.sourceFiles: Array of discovered file paths
// result.errors: Array of any errors encountered

The pipeline runs these steps in sequence:

  1. File scan — find all infrastructure TypeScript files
  2. Import — dynamically load modules
  3. Collect — extract declarable entities from exports
  4. Resolve — set logical names and resolve AttrRefs
  5. Graph — build dependency graph

Discovery operations can throw DiscoveryError with different types:

  • "import" — module loading failed
  • "resolution" — entity name conflicts or resolution issues
  • "circular" — circular dependencies detected

chant