Python
Load catalog bundles, inspect Polars tables, query with DuckDB, and create LanceDB search indexes.
Use chartcoach.open() to inspect Default Catalog tables and run indexed search
from Python.
Run one check without adding the package to a project:
uv run --with chartcoach python - <<'PY'
import chartcoach
cc = chartcoach.open()
print(cc.catalog.guidelines().select("id", "title").head(3))
PYAdd the package from an application root:
uv add chartcoach
uv add 'chartcoach[index]'
uv add 'chartcoach[mcp,index]'import chartcoach
cc = chartcoach.open()
cc.catalog.guidelines().select("id", "title").head(5)chartcoach.open() uses the package-pinned Default Catalog when no path is
provided. Accessing .catalog loads the catalog and returns derived Polars
tables from methods such as guidelines() and sections(). Indexed search
resolves a LanceDB table through .index.
The first .catalog access, cc.search(), or CLI catalog read downloads
package-pinned artifacts. Indexed search downloads an index archive. Set
CHARTCOACH_CACHE_DIR to isolate writes.
Loading
Use the facade when code needs catalog tables and indexed search:
import chartcoach
cc = chartcoach.open("dist/catalog")
catalog = cc.catalogchartcoach.open(source) accepts authored folders, bundle directories,
standalone parquet files, release root URLs, and release metadata URLs. Bundle
reads load MANIFEST.md and entries.parquet.
Use Catalog.open() when code only needs the catalog object:
from chartcoach import Catalog
catalog = Catalog.open("dist/catalog")
rows = catalog.guidelines().select("id", "title")Facade sources and search indexes
| Call | Catalog behavior | Indexed search behavior |
|---|---|---|
chartcoach.open() | Uses the package-pinned Default Catalog | cc.search() can resolve the package-pinned index |
chartcoach.open("dist/catalog") | Loads a custom catalog source | Pass index= or table= before indexed search |
chartcoach.open(catalog=catalog) | Uses a caller-owned catalog | Pass index= or table= before indexed search |
chartcoach.open(catalog=catalog, table=table) | Uses caller-owned catalog and LanceDB table | cc.search() uses the injected table |
Pass either source or catalog, not both.
Tables
Catalog returns derived Polars tables from guidelines(), sections(),
labels(), guideline_labels(), references(), guideline_references(), and
guideline_sources().
Use catalog.table(name) when a caller receives a table name from the CLI, MCP,
or docs. The Cataloging scheme lists table names
and columns.
Entries
entry = catalog.entry("compare-percentages-with-bars-not-pies")
print(entry["id"])
print(entry["title"])
print(entry["sections"][0]["role"])
print(entry["references"])entry(id) returns the catalog record to inspect before using a candidate in
feedback. Use its source references to explain where the recommendation came
from.
Unknown ids raise KeyError.
Use citation_records(...) when an agent needs source references:
from chartcoach.catalog.references import citation_records
citations = citation_records(
catalog,
ids=["compare-percentages-with-bars-not-pies"],
)
print(citations[0]["id"])
print(citations[0]["url"])
print(citations[0]["sources"][0]["reference_id"])Each returned citation object contains id, title, url,
guideline_citation, and sources. Each source includes reference_id,
source_title, doi, url, and a formatted citation.
Catalog API
Use Catalog directly when code owns guideline entries, tables, or local
catalog files.
| API | Behavior |
|---|---|
Catalog.open(path=None) | Loads the Default Catalog, remote metadata, local authored folder, bundle, or parquet file |
Catalog.from_entries(entries, manifest=None) | Builds an in-memory catalog from CatalogEntry, Guideline, or mapping objects |
Catalog.from_frame(df, manifest=None) | Builds a catalog from a serialized Polars dataframe |
Catalog.from_folder(path) | Reads an authored catalog folder |
Catalog.from_parquet(path) | Reads a standalone entries.parquet file |
catalog.select(ids) | Returns a catalog with the requested guideline ids in order and raises KeyError for missing ids |
catalog.merge(other) | Returns a catalog that combines both catalog tables and raises when manifests differ |
catalog.manifest | Returns the loaded CatalogManifest, or None |
catalog.require_manifest() | Returns the manifest or raises when the catalog has none |
catalog.digest() | Returns a stable digest of the serialized catalog table |
CatalogManifest.from_text(markdown) | Parses manifest Markdown |
CatalogManifest.from_path(path) | Reads and parses MANIFEST.md |
parse_label(value) | Splits a catalog label into family, category, and modifier fields |
from chartcoach import Catalog, CatalogManifest, Guideline, Section, parse_label
catalog = Catalog.from_entries(
[
Guideline(
id="direct-labels",
title="Use direct labels",
description="Place labels near the marks they identify.",
sections=[
Section(
role="advice",
title="Advice",
content="Label marks directly when space permits.",
)
],
)
],
manifest=CatalogManifest.from_text("""
## Section Roles
### advice
Actionable guidance for applying the guideline.
## Label Families
### chart
Chart-family labels such as `chart:bar`.
"""),
)
subset = catalog.select(["direct-labels"])
label = parse_label("chart:bar:use")
print(subset.digest())
print(label.family, label.category, label.modifier)Guideline requires either body or sections and derives one from the other
when needed. parse_label() accepts <family>:<category> and
<family>:<category>:<modifier> labels. Pass
manifest=CatalogManifest.from_path("MANIFEST.md") when the catalog should
validate section roles and label families.
DuckDB
Use DuckDB for joins across guideline tables.
conn = catalog.duckdb()
try:
rows = conn.sql("""
select g.id, g.title, s.role, s.content
from guidelines g
join sections s on s.guideline_id = g.id
where s.role = 'advice'
limit 5
""").pl()
finally:
conn.close()The result has id, title, role, and content columns. DuckDB is installed
with the base Python package.
Write a durable database file for another tool:
catalog.write_duckdb("./chartcoach-catalog.duckdb", overwrite=True)LanceDB search
Add the index extra before using search in application code.
import chartcoach
cc = chartcoach.open()
hits = cc.search("overplotted scatter plots").to_dict()["rows"]
print(hits[0]["id"])
print(hits[0]["matched_role"])
print(hits[0]["matched_text"])cc.search() downloads and extracts the package-pinned index on first use.
Later calls reuse the local copy. This default index auto-resolution applies
only to the Default Catalog.
cc.search(text, *, limit=10, candidate_limit=None, where=None, mode="auto")
returns a Result. Result.to_dict() contains query, rows, row_count,
limit, candidate_limit, and where.
Each search hit contains rank, id, title, description, labels,
matched_document_id, matched_role, score, and matched_text. Use hits as
candidate guidelines, then call cc.catalog.entry(hit["id"]) and
citation_records(...) before producing a sourced answer.
Pass index= when application code owns an index path:
cc = chartcoach.open("dist/catalog", index="./chartcoach-index")
hits = cc.search("direct labels", mode="fts")Use cc.index.query() for document-level matches before guideline
deduplication:
documents = cc.index.query(
"direct labels",
limit=5,
where="role = 'overview'",
mode="fts",
)
print(documents[0]["parent_id"])
print(documents[0]["text"])Document rows contain id, parent_id, role, labels, content_hash, and
text, plus LanceDB score fields when returned. Use cc.index.table when
application code needs the native LanceDB table.
Use chartcoach.open(catalog=catalog, table=table) when application code
already owns a catalog and native LanceDB table.
Import index when the application needs to build a caller-owned table:
from chartcoach.search import index
table = index(catalog, "./chartcoach-index")Without an embedding function, index() creates a full-text table over the
text column.
Write formats
catalog.write_folder("dist/authored")
catalog.write_parquet("dist/entries.parquet")
catalog.write_bundle("dist/catalog", overwrite=True)write_bundle() writes MANIFEST.md, entries.parquet, and metadata.json.
It requires a catalog manifest.