Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.6.0] - 2026-08-16

### Added
- `find <term...>` — identifier search across all node kinds (tables, columns, enums, enum values incl. overlay value-sets, domains). Exact → glob → fuzzy precedence, `--strict`, `--format json`.
- `install claude [--global]` — generate a Claude Code skill file with the schema directory baked in.
- `install agents` — marker-delimited AGENTS.md section (covers Codex, opencode, and other AGENTS.md-standard agents). Idempotent re-install.
- `install --list`, `uninstall claude|agents`.
- `-i/--input` now accepts DBML embedded in a Markdown file — extracts ```` ```dbml ```` fences (or, for `.md`/`.markdown` files, generic fences whose body looks like DBML), so a `.dbml` schema living inside an Obsidian/docs note can be pointed at directly. Parse errors are cleaner: file path, line number, and a hint when a markdown input has no fence to extract.
- `query` neighbors now carry a `keys:` line (PK flag, FK target table(s), enum type) so a solver can join through a neighbor without opening its file, and the default `--budget` is adaptive — sized to fit every depth-1 neighbor in full — so a hub table's direct partners no longer degrade on a plain invocation. Under pressure, neighbors degrade (lose their `keys:` line) before they're dropped, and depth-1 direct FK partners are never dropped even if the pack ships oversize.

## [0.5.0] - 2026-08-15

### Added
- `dbmlgraph query <table...>` — context pack for agents: full nodes for the queried tables, one-line neighbor summaries, deduped merged rules, `--depth`/`--budget`/`--columns`/`--format`/`--strict`.
- `generate` now always writes `AGENTS.md` at the output root, a navigation landing doc for AI agents dropped into the directory.

## [0.4.0] - 2026-08-03

### Added
Expand Down
78 changes: 69 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ its whole point is the business-context layer a live connection can't provide.
| Output split by grain (index / domain / table) | Yes | Per-table pages | No |
| Lint for documentation gaps | Yes (`W001`–`W003`) | Yes (column comments) | No |
| ER diagrams | Per-domain (Mermaid) | Yes (many formats) | No |
| Identifier search across the whole schema | Yes (`find`) | No | No |
| One-command agent wiring (Claude Code, AGENTS.md) | Yes (`install`) | No | No |
| Primary job | Feed schema *meaning* to AI agents | Document a live DB in CI | Convert DBML ↔ SQL |

Use `tbls` when you have a running database and want rich human documentation
Expand Down Expand Up @@ -270,23 +272,29 @@ built for pasting into an agent, so it never has to guess which files to open:
dbmlgraph query order_items orders -i schema.dbml --overlays overlays/
```

Full nodes for the queried tables, then one-line summaries of their neighbors,
then merged rules — deduped across the whole pack. A queried table never shows up
as its own neighbor, a neighbor shared by two queried tables prints once, and
repeated FKs between the same pair collapse with a `(2 refs collapsed)` note.
The footer names the tables just outside the pack and the command that fetches them.
Full nodes for the queried tables, then a one-line hook per neighbor plus its
`keys:` line (PK flag, FK target table(s), enum type — enough to join through
the neighbor without opening its file), then merged rules — deduped across the
whole pack. A queried table never shows up as its own neighbor, a neighbor
shared by two queried tables prints once, and repeated FKs between the same
pair collapse with a `(2 refs collapsed)` note. The footer names the tables
just outside the pack and the command that fetches them.

| flag | default | meaning |
|---|---|---|
| `--depth <n>` | `1` | neighbor hops, capped at 2. `0` drops the Neighbors section |
| `--budget <tokens>` | adaptive | by default the cap is sized to fit the queried nodes and every depth-1 neighbor in full (min `4000`), so a hub table is never degraded on a plain invocation; pass a number for a hard cap. Neighbors are ranked (distance, then FK degree, then name) and cut from the tail; queried nodes are never cut |
| `--budget <tokens>` | adaptive | by default the cap is sized to fit the queried nodes and every depth-1 neighbor's hook line + `keys:` line in full (min `4000`), so a hub table's direct partners are never degraded on a plain invocation; pass a number for a hard cap. Under pressure, neighbors degrade before they drop: depth-2+ entries lose their `keys:` line first (a direct depth-1 FK partner never does under the adaptive default), and only once nothing more can be trimmed does the tail get cut, ranked by distance then FK degree then name — queried nodes are never cut or degraded |
| `--columns key\|all` | `key` | `all` gives each neighbor a compact PK/FK/enum column table |
| `--format md\|json` | `md` | `json` mirrors the same sections as keys |
| `--format md\|json` | `md` | `json` mirrors the same sections as keys, plus `resolvedBudget` (the adaptive value actually used) |
| `--strict` | off | exact names only. Without it, a close name (case, or edit distance ≤2) is accepted when unambiguous |

Truncation is never silent — a cut list always ends in a counted
`… and N more (raise --budget or --depth)` label. An unresolvable name exits `1`
after printing the three closest table names.
`… and N more (raise --budget or --depth)` label, and a run of key-column
degradation ends in `(N neighbors shown without columns — raise --budget)`.
If even that isn't enough room for every direct relationship, the pack ships
oversize rather than dropping one, with `(budget exceeded to preserve direct
relationships)` noting why. An unresolvable name exits `1` after printing the
three closest table names.

Does the pack actually work? We benchmarked it blind against a 70-table
production schema: fresh agent sessions wrote SQL from either a pack (4–8k
Expand All @@ -295,6 +303,58 @@ accuracy at 3.5–6x less context, and on the hardest rule-dependent task the
pack arm produced the only fully-correct answer. Method, results, and the two
design changes the benchmark forced: [docs/benchmark.md](docs/benchmark.md).

## Find an identifier

`find` locates, `query` explains. Reach for `find` first when an agent doesn't yet know
the exact table/column/enum name it needs — it's a wide, cheap lookup across every node
kind (tables, columns, enums, enum values including overlay `values:` sets, domains), not
a deep dump of one table's context.

```bash
dbmlgraph find cycle_id # every column named cycle_id, across every table
dbmlgraph find KICKOFF # an enum or enum-value hit — DBML enum or overlay values: set
dbmlgraph find planning_cycle # a table hit (plus anything else matching the name)
dbmlgraph find "ratio_*" # glob — quote it so the shell doesn't expand *
```

Matching runs in precedence order and stops at the first level that finds something:
exact name (case-insensitive counts as exact) → glob (a term containing `*`) → fuzzy
(edit distance ≤ 2), unless `--strict` is set. Multiple terms in one invocation are
matched independently and their results concatenated.

| flag | default | meaning |
|---|---|---|
| `--format md\|json` | `md` | `json` mirrors the same hit groups as keys |
| `--strict` | off | exact + glob only — skip the fuzzy fallback |

## Install AI-agent integration

`install` wires an agent up to `find`/`query` so it reaches for the schema graph
instead of grepping markdown or DDL by hand.

```bash
dbmlgraph install claude [--global] # writes .claude/skills/dbmlgraph/SKILL.md
dbmlgraph install agents # writes/updates a marker-delimited AGENTS.md section
dbmlgraph install --list # show install status for all targets
dbmlgraph uninstall claude|agents [--global]
```

`install claude` writes a Claude Code skill file — local by default (`./.claude/skills/`),
or `~/.claude/skills/` with `--global`. `install agents` writes a
`<!-- dbmlgraph:start -->` … `<!-- dbmlgraph:end -->` section into `AGENTS.md` at the
project root, covering Codex, opencode, and any other agent that reads the AGENTS.md
standard. Both installs bake in the resolved schema directory so the agent never has to
guess `-i`/`--overlays` — one absolute `cd` in the generated instructions.

Schema directory resolution: `--schema-dir <path>` if given, otherwise walk up from the
current directory looking for a `.dbmlgraph.yml`; no match in either case is an error.

`install agents` is idempotent — re-running it replaces only the content between the
markers, so re-install after a schema move never duplicates the section or disturbs
anything else in `AGENTS.md`. `uninstall claude` removes the skill directory;
`uninstall agents` removes the marked section (and the whole file if nothing else is
in it).

## Roadmap

Ideas for future versions, roughly ordered by leverage. None are promises —
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dbmlgraph",
"version": "0.5.0",
"version": "0.6.0",
"description": "DBML → LLM-ready markdown knowledge graph (index, per-table, per-domain) + RAG export",
"type": "module",
"license": "MIT",
Expand Down
55 changes: 55 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { exportJsonl } from './export.js';
import { generate } from './generate.js';
import { init } from './init.js';
import { query, QueryResolveError, MAX_DEPTH, DEFAULT_DEPTH, DEFAULT_BUDGET } from './query.js';
import { find } from './find.js';
import { loadConfig, pick, require_, ConfigFormatError } from './config.js';
import { installClaude, installAgents, uninstall, installStatus, resolveSchemaDir, InstallError } from './install.js';

// comma-string flag → array, matching config's excludeColumns shape
const asCols = (v: string | undefined): string[] | undefined => v?.split(',');
Expand Down Expand Up @@ -120,11 +122,64 @@ program.command('query')
}));
});

program.command('find')
.description('locate identifiers — tables, columns, enums, enum values, domains — across the schema')
.argument('<term...>', 'identifier names or globs (e.g. cycle_id, "ratio_*")')
.option('-i, --input <file>', 'DBML file')
.option('--overlays <dir>', 'overlay YAML directory')
.addOption(new Option('--format <fmt>', 'md | json').choices(['md', 'json']))
.option('--strict', 'exact + glob only, no fuzzy match')
.option('-c, --config <file>', 'config file (default .dbmlgraph.yml)')
.action((terms: string[], o) => {
const cfg = loadConfig(o.config);
const input = require_(pick(o.input, cfg.input), 'input');
const overlays = pick(o.overlays, cfg.overlays);
const ir = parseDbmlFile(input);
if (overlays) mergeOverlays(ir, loadOverlays(overlays));
console.log(find(ir, { terms, format: o.format, strict: !!o.strict }));
});

program.command('install')
.description('install AI-agent integration (claude | agents)')
.argument('[agent]', 'claude | agents')
.option('--schema-dir <path>', 'directory containing .dbmlgraph.yml (default: walk up from cwd)')
.option('--global', 'claude only: install to ~/.claude/skills instead of ./.claude/skills')
.option('--list', 'show install status for all targets')
.action((agent: string | undefined, o) => {
if (o.list) {
const s = installStatus({ cwd: process.cwd() });
console.log(`claude ${s.claude ? 'installed' : (s.claudeGlobal ? 'installed (global)' : 'not installed')}`);
console.log(`agents ${s.agents ? 'installed' : 'not installed'}`);
return;
}
if (agent !== 'claude' && agent !== 'agents') {
throw new InstallError(`unknown agent "${agent ?? ''}": expected claude | agents`);
}
const schemaDir = resolveSchemaDir(o.schemaDir, process.cwd());
const file = agent === 'claude'
? installClaude({ schemaDir, cwd: process.cwd(), global: !!o.global })
: installAgents({ schemaDir, cwd: process.cwd() });
console.log(`wrote ${file}\nschema dir baked: ${schemaDir}`);
});

program.command('uninstall')
.description('remove AI-agent integration (claude | agents)')
.argument('<agent>', 'claude | agents')
.option('--global', 'claude only: remove from ~/.claude/skills instead of ./.claude/skills')
.action((agent: string, o) => {
if (agent !== 'claude' && agent !== 'agents') {
throw new InstallError(`unknown agent "${agent}": expected claude | agents`);
}
const removed = uninstall(agent, { cwd: process.cwd(), global: !!o.global });
console.log(removed ? `removed ${removed}` : 'nothing installed');
});

try {
program.parse();
} catch (e: any) {
if (e instanceof ConfigFormatError) { console.error(`error: ${e.message}`); process.exit(1); }
if (e instanceof QueryResolveError) { console.error(e.message); process.exit(1); }
if (e instanceof InstallError) { console.error(`error: ${e.message}`); process.exit(1); }
if (e instanceof DbmlParseError) {
const loc = [e.file, e.line != null ? `line ${e.line}` : null].filter(Boolean).join(', ');
const hint = e.markdownNoFence ? ' — input looks like markdown without a ```dbml fence?' : '';
Expand Down
Loading
Loading