Skip to content

Config (aburi.config.v1) ​

Complete specification of aburi.json, Aburi's per-project configuration file. The JSON Schema aburi.config.v1.json is the single source of truth.

References:


1. File Format and Placement ​

  • File name: aburi.json or aburi.jsonc
  • Placement: workspace root (same level as pnpm-workspace.yaml / package.json, etc.). A config in a subdirectory is also honoured — discovery walks up from cwd (§5.2 of CLI reference) — but every relative path inside it (ignore, components[].roots, relative plugin refs) still resolves against the workspace root, and the scan still covers the whole workspace. aburi scan warns when the two differ.
  • Format: JSONC (comments allowed) or JSON
  • Encoding: UTF-8, LF
  • Indentation: 2 spaces

Why JSONC is allowed: this is a human-authored config, so comments on each section carry value. Generated JSON must be passable to a parser by consumers as-is, so pure JSON is supported too.

aburi.config.ts / aburi.config.yaml are not adopted (on the premise that AI interprets the config, everything is unified as static JSON).

2. Overall Structure ​

jsonc
{
  "$schema": "https://aburi.kage1020.com/schema/aburi.config.v1.json",

  // File exclusion (drop-list Category A)
  "ignore": ["docs/**", "scripts/legacy/**"],
  "respectGitignore": true,

  // Plugin enablement (extension-vocab §4)
  "languages": ["lang-typescript"],
  "frameworks": ["framework-nestjs"],
  "effects": ["effects-prisma", "effects-pino"],

  // Per-plugin options (optional)
  "pluginOptions": {
    "effects-prisma": { "treatExtendsAsTx": true }
  },

  // Component override (autodetect is the default)
  "components": [
    { "id": "billing", "roots": ["apps/billing"] }
  ],

  // drop-list extension (Category D)
  "suppress": ["myLogger", "metrics"],
  "keep": ["myLogger.audit"],

  // Framework hints (Tier 3 plugin, extension-vocab §11.3)
  "frameworkHints": [
    {
      "name": "acme-framework",
      "decorators": {
        "AcmeController": { "boundary": true, "extKind": "framework:acme:controller" }
      }
    }
  ],

  // Output destination
  "output": {
    "dir": "out"
  },

  // Execution mode
  "strict": true,

  // Resource limits for parsing/extraction
  "maxFileSizeBytes": 2097152,        // 2 MB (default); files exceeding this are skipped
  "parseTimeoutMs": 5000,             // per-file parse+extract+walk budget (default 5s); an over-budget file is skipped whole — lang-plugin.md §7.1.2

  // Coverage floor — absent by default
  "minParsedFileRatio": 0.9           // exit 3 when fewer than 90% of discovered files were parsed — cli-spec.md §5.7
}

Every field is optional. Even an empty { "$schema": "..." } works (autodetect does everything).

3. ignore ​

Array of glob patterns appended to drop-list Category A.

jsonc
{
  "ignore": ["docs/**", "scripts/legacy/**", "**/*.fixture.ts"]
}
  • POSIX globs (forward slashes)
  • Relative to the workspace root
  • micromatch-compatible

These are appended to the core standard patterns of drop-list §3.1 (existing patterns are not replaced).

4. respectGitignore ​

jsonc
{ "respectGitignore": true }
  • Default: true
  • Applies every directory's .gitignore with git's own rules — the root's and each nested one, deepest first (see drop-list.md §3.3) — alongside drop-list Category A. $GIT_DIR/info/exclude and core.excludesFile are per-machine and deliberately not read

Setting it to false makes git-ignored files (build artifacts, etc.) extraction targets as well. Intended for special cases such as deliberately inspecting built code in CI.

5. Plugin Enablement ​

5.1 languages / frameworks / effects ​

jsonc
{
  "languages": ["lang-typescript"],
  "frameworks": ["framework-nestjs", "framework-next"],
  "effects": ["effects-prisma", "effects-pino", "effects-fetch"]
}

Each array element is a plugin manifest name (the name field).

5.2 Resolution Order ​

For a string <id>, Aburi resolves to exactly one specifier — there is no fallback chain. Paths below are written as the string's value; in aburi.json each backslash is doubled.

  1. <id> is refused → config error (exit 2), raised before any plugin is imported, when it:
    • names a Windows drive in a way the platform cannot load. On Windows that is a path rooted with no drive (/opt/x.mjs, \x.mjs), which would take the workspace root's drive, or a drive with no root (C:x.mjs), which would resolve against whatever directory is current on that drive. On any other platform it is any ref naming a drive (C:/x.mjs, C:\x.mjs)
    • has the shape of a language or framework id (^[a-z][a-z0-9]*$). Rule 3 would look for @aburi/<id>, and the plugins published there are named with their kind as a prefix (lang-, framework-, effects-; a convention the manifest schema does not enforce), so the ref names none of them. The error names the plugin aburi init would have written for that id ("ts" → "lang-typescript", "nestjs" → "framework-nestjs") when there is one, and in effects the name an effects plugin with that xPrefix would have ("prisma" → "effects-prisma"). The schema's PluginRef does not carry this rule, because tightening it breaks v1 (§14.1)
  2. <id> is an absolute filesystem path, or starts with ./ or ../ → normalized and converted to a file: URL. Relative paths resolve against the workspace root; absolute paths do not depend on it. On Windows an absolute path is drive-qualified (C:/x.mjs or C:\x.mjs) or a UNC share (\\server\share\x.mjs). A relative path has to start with ./ or ../; .\x.mjs falls through to rule 3
  3. <id> is scoped or contains / (@myorg/pkg, some-pkg/subpath, a file: URL) → used verbatim
  4. Otherwise → prefixed, becoming @aburi/<id>

Examples:

  • "effects-prisma" → @aburi/effects-prisma
  • "@myorg/aburi-effects" → @myorg/aburi-effects
  • "./aburi-plugins/internal-framework.mjs" → direct relative path
  • "C:/aburi-plugins/internal-framework.mjs" → absolute path on Windows
  • "/opt/aburi-plugins/internal-framework.mjs" → absolute path on POSIX; refused on Windows
  • "file:///opt/aburi-plugins/internal-framework.mjs" → that URL

A bare name is therefore only resolvable under the @aburi scope. Third-party plugins must be listed by their full package name.

5.3 Meaning of Array Order ​

ArrayMeaning of order
languagesOrder is meaningless (unless extensions collide)
frameworksOrder is meaningless (unless decorator names etc. collide; on collision, config order wins)
effectsOrder is priority (effect-plugin §5.1 first-match-wins)

Earlier entries in effects have higher priority. Placing a project-specific plugin first gives it priority over standard plugins.

5.4 pluginOptions ​

Opaque per-plugin configuration.

jsonc
{
  "pluginOptions": {
    "effects-prisma": { "treatExtendsAsTx": true },
    "framework-nestjs": { "considerHttpExceptionAsThrow": false }
  }
}
  • Keys are plugin manifest names
  • Values are arbitrary objects interpreted by the plugin (the Aburi core does not inspect their contents)
  • Each plugin is responsible for documenting the schema of its options

6. components ​

Overrides the logical component boundaries of a monorepo. When not specified explicitly, the core Component autodetect infers them from pnpm-workspace.yaml / turbo.json / go.work / Cargo.toml / pyproject.toml, etc.

jsonc
{
  "components": [
    {
      "id": "billing",
      "name": "Billing",
      "roots": ["apps/billing", "packages/billing-domain"],
      "publicApi": [
        "apps/billing/src/routes/**",
        "ts:packages/billing-domain/src/index.ts#Invoice"
      ],
      "frameworks": ["nestjs"]
    }
  ]
}

Each field has the same shape as ir-schema.md §4. However, languages may be omitted on the config side (when omitted, autodetect fills it in). name may also be omitted on the config side, in which case the autodetect result (package.json#name, etc.) is used. In the schema, both are required on the IR Component; the config Component is lenient.

frameworks holds framework ids (nestjs, what a plugin's provides.frameworks[] or the detector names), not plugin names. A value that is a plugin's own name (framework-nestjs), of a loaded plugin or a first-party one, is kept as written and reported on stderr with the id that plugin provides, or with the advice to remove it when that plugin provides none: the schema admits it, since a framework hint's name is an id that may carry a hyphen. Nothing else is checked. A value in neither set, such as a typo or an id no loaded plugin provides, passes silently into the IR, because the detector itself records ids no plugin provides (vue, fastify), so silence is not confirmation.

6.1 Merge Rules ​

Components explicitly declared in the config replace the autodetect results (no merging):

  • The config contains component A's id → the autodetect result with the same id is discarded and the config is adopted
  • The config does not contain component A's id → the autodetect result is adopted as-is

No ad-hoc merge is provided for "I only want to tweak part of the autodetect result". A component you want to modify must be written out explicitly.

7. suppress / keep (drop-list Extension) ​

jsonc
{
  "suppress": ["myLogger", "metrics", "tracer"],
  "keep": ["myLogger.audit", "@Transaction"]
}
  • suppress[]: identifier prefixes. Excludes all of myLogger.* from effects/calls
  • keep[]: exceptional retention. The @<name> form denotes a decorator; anything else a callee

Follows the evaluation order of drop-list §6.1 / §6.2 / §7 (keep > suppress > plugin > core).

8. frameworkHints (Tier 3 Plugin) ​

A mechanism to declaratively add framework Boundary recognition and extKind mapping with no code at all. The concrete form of the Tier 3 plugin of extension-vocab §11.3.

jsonc
{
  "frameworkHints": [
    {
      "name": "acme-framework",
      "decorators": {
        "AcmeController": {
          "boundary": true,
          "extKind": "framework:acme:controller",
          "derivedBy": "framework-hint:acme:controller"
        },
        "AcmeInternal": {
          "boundary": false,
          "drop": true
        }
      },
      "classNamePatterns": {
        "*Handler": {
          "extKind": "framework:acme:handler"
        }
      }
    }
  ]
}

8.1 decorators ​

Keys are decorator names (the AcmeController part of the decorator @AcmeController). A key matches the decorator's leaf name, so @acme.AcmeController() meets AcmeController too; imports are not read, so a decorator renamed on import is not recognized. A rule applies to any Symbol carrying the decorator, a method as well as a class. Fields of each value:

FieldEffect
boundaryOverrides Decorator.boundary with this value, on the decorator the rule matched
extKindSets Symbol.extKind for Symbols carrying this decorator
derivedByAppended to Symbol.derivedBy[]
dropCategory-B drop of Symbols carrying this decorator

All are optional.

8.2 classNamePatterns ​

Keys are class-name globs (*Handler, *Service, Abstract*): * matches any run of characters, ? exactly one, and every other character itself. A pattern is matched against the whole of a class's own name (Outer.OrderHandler is matched as OrderHandler), and against classes only. Values take the same fields as decorators (extKind/derivedBy/drop; boundary is decorator-only).

8.3 Automatic Ad-hoc Plugin Conversion ​

Each frameworkHints entry is internally registered as a single ad-hoc plugin:

name: hint-<name>
type: framework
provides.extKindPrefixes:  each extKind value's namespace with "hint:" prepended
provides.derivedByPrefixes: each derivedBy value's namespace, as written
provides.frameworks: [<name>]

The registry reserves framework:hint (extension-vocab §5.1), and admits a namespace under it only through the path that registers these manifests; a plugin listed in frameworks still cannot declare one.

8.3.1 Namespace Isolation via Automatic hint: Prefixing ​

The user-written extKind: "framework:acme:controller" is internally transformed into framework:hint:acme:controller. This avoids:

  • Collision accidents on the framework:acme prefix when an @aburi/framework-acme plugin is already installed
  • frameworkHints later breaking an existing plugin

The hint: prefixing is done transparently by the core, so users keep writing extKind: "framework:acme:controller". It appears in the generated IR and Markdown as framework:hint:acme:controller.

Example: extKind: "framework:acme:controller" → internally framework:hint:acme:controller → extKindPrefixes: ["framework:hint:acme"] is inferred automatically.

Users never need to think about prefix declarations.

8.3.2 Where the rules run ​

Each ad-hoc plugin is a framework plugin like any other, placed after every plugin listed in frameworks, in frameworkHints order. Since the first framework plugin to classify a Symbol wins (lang-plugin.md §5.2.1), a Symbol that a configured plugin recognizes keeps that plugin's classification, boundary flags included, and the hint applies to the rest.

Within one entry, decorator rules come first, in the order the decorators are written on the Symbol, then classNamePatterns in config order. The first extKind among the rules that apply is the Symbol's, every derivedBy is appended, and each boundary overrides the decorator it was written for.

drop is not a classification, so it does not depend on who won. Every framework plugin is asked for a drop after the core's shape rules and before the language plugin's (drop-list §6.3), and a Symbol a drop: true rule applies to is dropped with the dropReason frameworkHints "<name>": @<decorator> or frameworkHints "<name>": class <pattern>. A Symbol with a boundary decorator is kept, as it is from every Category-B rule.

8.4 Collisions ​

  • Multiple entries with the same name → config validation error
  • Two entries whose extKinds name the same vendor (framework:acme:*) derive the same framework:hint:acme → the registry refuses the later one at startup (extension-vocab §6.1)
  • A plugin listed in frameworks already owns a namespace an entry derives (a derivedBy prefix) → the registry refuses the entry at startup

A refused entry is a config error (exit 2) naming the entry, since the entry is what the reader has to change.

9. output ​

jsonc
{
  "output": {
    "dir": "out"
  }
}
  • dir (default "out"): output destination for IR/Markdown. It fills the same slot as --output-dir, which wins when both are given, and it resolves against the working directory exactly as that flag does — so writing the default "out" explicitly changes nothing, which is what a schema default has to mean. An absolute value names one directory from wherever the command is run.

Future addition candidates:

  • format: "json" / "md" / "both" (default "both")
  • compact: true for single-line JSON

Currently only dir is supported.

10. strict ​

jsonc
{ "strict": true }
  • Default: true
  • When true, a plugin emitting vocab not declared in its manifest is an extraction-time error: the run stops at the first one and exits 3, naming the plugin, the value and where it was emitted
  • When false (= making the equivalent of aburi scan --discover permanent), it is a warning only and execution continues; aburi scan writes every such value to aburi-vocab-discovered.json (extension-vocab.md §11.5.1), and aburi diff lists them on each scan's incident report

"Declared" means claimed by the plugin that emitted it, directly or through a prefix it owns: an id another plugin owns is as undeclared as one nobody does. The core effect ids (ir-schema.md) belong to no plugin and pass whoever emits them. An extKind is charged to the framework plugin that assigned it, or to the language plugin when no framework replaced its own.

The CI default is true; use false or the CLI's --discover when you want discovery in local development.

When both are specified, the CLI flag overrides the config.

10.1 minParsedFileRatio ​

jsonc
{ "minParsedFileRatio": 0.9 }
  • Absent by default, and absent means no floor rather than a floor of zero
  • Range 0 < r <= 1; a floor of 0 is refused because nothing can fall below it, and a config key that cannot change an outcome is a policy that lies
  • aburi scan exits 3 when parsedFiles / totalFiles is below it — <, not <=, so exactly the floor passes
  • Counts every skip reason, not parse-timeout alone: which reason produced the loss decides the fix, not whether coverage collapsed

Independent of this key, a scan that discovered nothing, or parsed nothing of what it discovered, exits 3 — see cli-spec.md §5.7. The floor is only about the ground between that and a clean run, which is where the answer depends on the repository.

No CLI flag mirrors it. A coverage floor is a property of the workspace rather than of one invocation, and a flag would need the key in the config anyway for the CI run to be reproducible.

11. Overrides from the CLI ​

Config values can be overridden by CLI flags:

CLI flagConfig overridden
--ignore <glob>Appended to ignore[]
--respect-gitignore / --no-respect-gitignorerespectGitignore
--strict / --no-strictstrict
--discoverEquivalent to strict: false
--output-dir <path>output.dir

See cli-spec.md for details.

12. autodetect (Behavior Without a Config) ​

When no config exists at all (aburi.json is absent), aburi scan attempts the following autodetect:

Determining the workspace root:
  - Location of the .git directory (or the nearest one when inside a subdirectory)
  - pnpm-workspace.yaml / package.json workspaces / turbo.json / go.work / Cargo.toml workspace

Monorepo detection:
  - Infer components[].roots from the manifests above

Language detection:
  - Determine languages per component from extension frequency

Framework detection:
  - Look for nestjs / next / react / express etc. in package.json dependencies
  - Suggest auto-enabling the `framework-<name>` plugin matching each detected framework (without actually enabling it)

Autodetect alone is enough to run, but for stability it is recommended to write the results to aburi.json via aburi init.

13. Output of aburi init ​

aburi init writes the autodetect results out as aburi.json. Example:

jsonc
{
  "$schema": "https://aburi.kage1020.com/schema/aburi.config.v1.json",
  "languages": ["lang-typescript"],
  "frameworks": [],
  "effects": [],
  "components": [
    { "id": "billing", "name": "Billing", "roots": ["apps/billing"], "languages": ["ts"] },
    { "id": "shared",  "name": "Shared",  "roots": ["packages/shared"], "languages": ["ts"] }
  ]
}

init fills in the language and framework plugin refs it has a mapping for; effects plugins are outside its scope and are added by hand.

14. Verifiable Properties ​

IDInputExpectation
C1A config that is only {}Runs via autodetect
C2effects is an empty arrayNo effect classification; everything remains in calls
C3effects: ["effects-prisma", "effects-stripe"]Classification is evaluated in order
C4Two entries with the same component idConfig validation error
C5The same callee in both keep and suppresskeep wins (drop-list §7)
C6Two frameworkHints entries with the same nameConfig validation error
C7Prefixes auto-inferred from frameworkHints extKind valuesRegistered in the registry
C7aThe §8 example on a class decorated @AcmeController() and a class named OrderHandlerextKind framework:hint:acme:controller with boundary: true on the decorator, and framework:hint:acme:handler
C7b"AcmeInternal": { "drop": true } on a class decorated @AcmeInternal()dropped: true, dropReason frameworkHints "acme-framework": @AcmeInternal
C7cframeworks: ["framework-nestjs"] and "*Handler" on a NestJS @Controller() class named UsersHandlerNestJS's classification; the hint applies only to classes no configured plugin recognized
C7dTwo entries writing framework:acme:a and framework:acme:bExit 2 naming the second entry
C8Extracting undeclared vocab with strict: falseWarning only; recorded in out/aburi-vocab-discovered.json
C9Passing CLI --strictOverrides config strict: false
C10Specifying an unregistered plugin in pluginOptionsWarning (ignored because the plugin is disabled)
C11minParsedFileRatio omitted, and a scan that parsed some files and lost othersExit 0
C12minParsedFileRatio: 0.9, and a scan that parsed half the files it foundExit 3, naming both counts and the floor
C13minParsedFileRatio equal to the ratio the scan achievedExit 0
C14minParsedFileRatio: 0Config validation error
C15languages: ["<absolute path to a plugin file>"], on any platform, with spaces, # or % in the file nameLoads that file, wherever the workspace root is
C16On Windows, languages: ["./ok.mjs"], effects: ["/opt/plugins/x.mjs"] (no drive)Exit 2, naming the drive to add; ./ok.mjs is not imported either
C17On Windows, languages: ["C:plugins/x.mjs"] (drive, no root)Exit 2, suggesting C:/plugins/x.mjs
C18On POSIX, languages: ["C:/plugins/x.mjs"]Exit 2, saying the platform has no drive C:
C19One object naming a key twice, at any depth — { "ignore": ["a/**"], "ignore": ["b/**"] } — including inside a pluginOptions value, where the first is lost the same way; or naming __proto__ at all, which the parser assigns instead of defining, so it never becomes a key the schema can seeConfig validation error naming the key, the object and the line
C20languages: ["ts"], or frameworks: ["nestjs"]Exit 2 before any plugin is imported, naming lang-typescript or framework-nestjs
C21components: [{ …, "frameworks": ["framework-nestjs"] }]Exit 0; stderr names the component and suggests nestjs; the IR carries framework-nestjs
C21acomponents: [{ …, "frameworks": ["lang-typescript"] }], a plugin that provides no frameworkExit 0; stderr says to remove it or write the component's framework id

14.1 Config Schema Compatibility Policy ​

The compatibility of aburi.config.v1.json follows the same policy as the IR schema (ir-schema.md §15). Particularly important:

  • The contents of pluginOptions values are managed by each plugin's own schema and are outside the compatibility scope of the Aburi core
  • Adding fields to frameworkHints is non-breaking
  • Changing the default values of output.dir / strict is treated as breaking (behavior changes in CI, so it goes to v2)

Deferred to v2, because each tightens a pattern that v1 documents validate against today:

  • A PluginRef pattern that rejects a bare id (§5.2 rule 0 checks it at load time instead)
  • A components[].frameworks pattern that rejects plugin names (§6 warns instead)
  • $refs from components[].languages / .frameworks to branded id types, so the generated Config type stops accepting plain strings there

15. Design Decisions ​

15.1 Why Explicit Plugin Enablement Is Adopted ​

There is an "implicit" alternative — scanning node_modules and auto-loading anything with an aburi-plugin.json — but it is rejected to avoid:

  • A single rogue package influencing Aburi's output
  • Developers struggling to trace "why is this effect appearing"
  • Unpredictable vocab collisions

Explicit enablement is used uniformly. aburi init proposes and writes candidates from the package.json dependencies, so the manual burden is effectively zero.

15.2 Why Plugin Names Are Manifest Names ​

Using npm package names (@aburi/effects-prisma) alongside manifest names (effects-prisma) makes naming redundant. The config is more readable written with short manifest names. Aburi trying @aburi/<name> during npm resolution preserves the DX of official plugins.

15.3 Why pluginOptions Is an Opaque Object ​

Controlling each plugin's behavior is the plugin's responsibility. If the Aburi core enforced a schema, plugin evolution would slow down.

Instead, each plugin documents its own pluginOptions schema, and users consult the plugin docs.

15.4 Why components Merging Is Not Supported ​

Ad-hoc merging causes the accident where "I wrote a config, but a change in autodetect behavior silently changed the values". By standardizing on replacement, the behavior of a written config is predictable independently of autodetect changes.

15.5 Automatic Prefix Inference for frameworkHints ​

Making users hand-write extKindPrefixes: ["framework:acme"] is redundant and error-prone. Auto-inferring from extKind values lets a Tier 3 plugin come together with minimal code.

15.6 Why aburi.config.ts Is Not Adopted ​

  • On the premise that AI consumers read the config, static JSON is overwhelmingly easier to handle
  • TypeScript completion is fully achievable via JSON Schema ($schema) — the biome.json / tsconfig.json worldview
  • The need for dynamic logic (e.g. deciding suppress via a function) is low; when needed, writing a plugin has higher reusability

Released under the Apache License 2.0.