Metadata-Version: 2.3
Name: vgi-lint-check
Version: 0.47.0
Summary: A pydoclint-style metadata-quality linter for VGI workers.
Keywords: vgi,duckdb,haybarn,linter,metadata,data-quality,documentation,catalog,data-engineering,cli
Author: Rusty Conover
Author-email: Rusty Conover <rusty@query.farm>
License: Query Farm Source-Available License, Version 1.0
         
         Copyright (c) 2025, 2026 Query Farm LLC. All rights reserved.
         
         ## 1. Definitions
         
         "Licensor" means Query Farm LLC (http://query.farm, hello@query.farm) and its
         affiliates under common control.
         
         "VGI" means the Vector Gateway Interface, the DuckDB extension technology developed
         by the Licensor, also referred to by the Licensor as its "Hyperfederation" database
         technology.
         
         "Licensed Work" means VGI, including its source code, object code, and any
         documentation distributed with it, in each version made available by the Licensor
         under this License.
         
         "You" (or "Your") means the individual or legal entity exercising rights under this
         License, together with all affiliates under common control with that entity.
         
         "Production Use" means any use of the Licensed Work other than for development,
         testing, evaluation, experimentation, or other non-production purposes.
         
         "Hyperfederation Services" means services relating to the federation, gateway,
         integration, querying, or interoperation of data sources using VGI or
         functionally equivalent technology, including services that expose, broker, or
         provide access to such federated or gateway capabilities.
         
         "Commercial Marketplace" means any platform, exchange, or intermediary service,
         whether or not operated for a fee, that connects providers and consumers of
         Hyperfederation Services, or that facilitates the offering, discovery, exchange,
         sale, or licensing of Hyperfederation Services among third parties.
         
         "Competing Offering" means a product or service that You make available to third
         parties, on a paid basis (including through paid support, subscription, or hosting
         arrangements), whose capabilities significantly overlap with those of the Licensor's
         version(s) of the Licensed Work.
         
         ## 2. Grant of Rights
         
         Subject to the terms and limitations of this License, the Licensor grants You a
         worldwide, royalty-free, non-exclusive license to:
         
         (a) use, copy, and run the Licensed Work for any non-production purpose;
         
         (b) modify the Licensed Work and create derivative works of it;
         
         (c) redistribute the Licensed Work and Your derivative works, provided You comply
         with Section 5; and
         
         (d) make Production Use of the Licensed Work, except where such use is restricted by
         Section 3 or reserved to the Licensor by Section 4.
         
         ## 3. Production Use Conditions
         
         The grant of Production Use in Section 2(d) does not extend to, and You may not
         without a separate commercial license from the Licensor:
         
         (a) provide a Competing Offering to third parties; or
         
         (b) offer the Licensed Work, or any derivative work of it, to third parties on a
         hosted, embedded, or as-a-service basis where doing so competes with the Licensor's
         commercial interests in the Licensed Work.
         
         "Embedded" includes incorporating the source or object code of the Licensed Work
         into a Competing Offering, and packaging a Competing Offering such that the Licensed
         Work must be accessed or downloaded for that offering to function.
         
         Hosting or using the Licensed Work for Your own internal purposes is not a Competing
         Offering and is permitted, including across Your affiliates under common control.
         
         ## 4. Reserved Rights
         
         Notwithstanding any other provision of this License, the Licensor reserves to itself
         the exclusive right to build, operate, offer, or authorize a Commercial Marketplace
         that incorporates, integrates, is built upon, or otherwise uses the Licensed Work.
         
         This License grants You no right to construct, operate, or enable a Commercial
         Marketplace using the Licensed Work, whether on a commercial or non-commercial basis,
         and any such use requires a separate written agreement with the Licensor.
         
         ## 5. Redistribution
         
         If You redistribute the Licensed Work or any derivative work of it, in original or
         modified form, You must:
         
         (a) include a complete, unmodified copy of this License with each copy; and
         
         (b) cause any recipient to receive the Licensed Work subject to the terms of this
         License.
         
         The conditions in Sections 3 and 4 apply to every recipient of the Licensed Work,
         whether received directly from the Licensor or through a third party.
         
         ## 6. Conversion to Open Source
         
         For each version of the Licensed Work, on the tenth anniversary of the date the
         Licensor first made that version publicly available (the "Change Date" for that
         version), the Licensor additionally grants You the right to use that version under
         the terms of the Apache License, Version 2.0, and on and after that version's Change
         Date the restrictions in Sections 3 and 4 no longer apply to that version.
         
         This License applies separately to each version of the Licensed Work, and the Change
         Date may differ between versions.
         
         ## 7. Commercial Licensing
         
         If Your intended use is not permitted under this License, You may obtain a separate
         commercial license from the Licensor by contacting hello@query.farm. Absent such a
         license, You must refrain from the restricted use.
         
         ## 8. Trademarks
         
         This License does not grant You any right to use the names, trademarks, service
         marks, or logos of the Licensor, including "Vector Gateway Interface," "VGI," and
         "Hyperfederation," except as required for reasonable and customary use in describing
         the origin of the Licensed Work.
         
         ## 9. Termination
         
         Any use of the Licensed Work in violation of this License automatically terminates
         Your rights under this License for the current and all other versions of the Licensed
         Work. Your rights may be reinstated only by a writing signed by the Licensor.
         
         ## 10. Disclaimer of Warranty and Limitation of Liability
         
         TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
         AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EXPRESS OR IMPLIED,
         INCLUDING WITHOUT LIMITATION ANY WARRANTIES OR CONDITIONS OF MERCHANTABILITY, FITNESS
         FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, OR TITLE.
         
         TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL THE LICENSOR BE
         LIABLE TO YOU FOR ANY DAMAGES ARISING OUT OF OR RELATING TO THIS LICENSE OR THE USE
         OF THE LICENSED WORK, WHETHER IN CONTRACT, TORT, OR OTHERWISE.
Classifier: License :: Other/Proprietary License
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Operating System :: OS Independent
Classifier: Typing :: Typed
Requires-Dist: haybarn>=1.5.4rc1,<1.6
Requires-Dist: click>=8.1
Requires-Dist: rich>=13
Requires-Dist: markdown-it-py>=3
Requires-Dist: packaging>=23
Requires-Dist: pytz>=2024.1
Maintainer: Query Farm LLC
Maintainer-email: Query Farm LLC <hello@query.farm>
Requires-Python: >=3.11
Project-URL: Homepage, https://query.farm
Project-URL: Repository, https://github.com/Query-farm/vgi-lint-check
Project-URL: Issues, https://github.com/Query-farm/vgi-lint-check/issues
Description-Content-Type: text/markdown

<p align="center">
  <img src="https://raw.githubusercontent.com/Query-farm/vgi-lint-check/main/assets/vgi-logo.png" alt="Vector Gateway Interface" width="320">
</p>

<p align="center">
  <a href="https://pypi.org/project/vgi-lint-check/"><img src="https://img.shields.io/pypi/v/vgi-lint-check" alt="PyPI version"></a>
  <a href="https://pypi.org/project/vgi-lint-check/"><img src="https://img.shields.io/pypi/pyversions/vgi-lint-check" alt="Python versions"></a>
  <a href="https://github.com/Query-farm/vgi-lint-check/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/Query-farm/vgi-lint-check/ci.yml?branch=main&amp;label=CI" alt="CI"></a>
  <a href="https://github.com/Query-farm/vgi-lint-check/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Source--Available-blue" alt="License"></a>
</p>

# vgi-lint

A `pydoclint`-style **metadata-quality linter for VGI workers**. It attaches to an
arbitrary VGI worker, reads everything the worker contributes through DuckDB
system tables, and reports quality findings — missing descriptions, undocumented
columns/functions, absent or malformed example queries, untagged objects, and
more — with a quality score, per-data-version baselines, and machine output for
coding agents.

It works with **any** VGI worker regardless of implementation language (Python,
Go, Rust, Java, TypeScript, …): it treats the worker as a black box and inspects
only what surfaces post-attach.

## Install / run

```bash
uv sync                      # haybarn is RC-only; prerelease = "allow" is set
uv run vgi-lint --help
```

## Quick start

```bash
# Lint a local subprocess worker
uv run vgi-lint 'uv run volcano_worker.py'

# Lint a no-auth HTTP worker
uv run vgi-lint http://localhost:9009

# Machine output for a coding agent / CI
uv run vgi-lint http://localhost:9009 --format agent
uv run vgi-lint http://localhost:9009 --format json
```

In a worker's own repo, add a `[tool.vgi-lint-check]` block (see `vgi-lint init`)
with a `location`, then just run `vgi-lint` with no arguments.

> v1 supports **local subprocess** and **no-auth HTTP** workers. Authenticated
> (OAuth) workers are not yet supported.

**Findings are grouped by rule** by default, so a rule firing on many objects
reads as one block — the fix stated once, the affected objects listed beneath
(capped at `--max-per-rule`, default 10, with `… +N more`; `0` = list all). Use
`--group-by object` for the per-object layout. The `agent` format groups by rule
too but never truncates, so an LLM gets one fix instruction plus the full object
list (far fewer tokens than repeating the fix per object). `json`/`jsonl` are
unchanged — the complete, ungrouped contract.

## What it checks

Object coverage: the catalog itself, schemas, tables, views, columns,
scalar/aggregate functions, macros, settings, pragmas, and constraints. Rule
families:

| Family | Codes | Examples |
| --- | --- | --- |
| Catalog | VGI0xx | catalog description, `vgi.doc_llm`/`_md`, `source_url`, default schema resolves, `data_version_spec` semver + releases within it, **catalog not empty**, worker advertises 1–N catalogs, `vgi.license` is a valid SPDX id |
| Descriptions | VGI1xx | schema/table/view/function comment, `vgi.doc_llm`, `vgi.doc_md`; **catalog/schema docs must be detailed** (≥300/≥160 chars) |
| Discoverability | VGI12x/13x | duplicate/short/echoed descriptions, **no placeholder text (TODO/TBD/…)**, classifying tag present + **reused (small vocabulary)**, **`vgi.title` required on catalog + schemas** (optional, validated when set, below), keywords present (**JSON array**), **source_url is catalog-only**, join-path docs, release freshness, example richness, column units |
| Content | VGI17x | `vgi.doc_md` is valid Markdown; description links/images & source URLs resolve (no 404) |
| Columns | VGI2xx | column-comment coverage + **every column commented**, comment-not-echo, **naive TIMESTAMP documents its timezone** |
| Functions | VGI3xx | description (+ quality), documented parameters, named arguments, **per-argument descriptions required** (error) that **don't restate the data type** (warning; needs a vgi extension exposing `vgi_function_arguments()`), examples, scalar-function stability (all-VOLATILE smell + per-function VOLATILE flag), **every-parameter-ANY smell**, **parameterless table function should be a table** |
| Tags | VGI4xx | required tag keys (opt-in), reserved-tag validity, **unknown `vgi.*` key is likely a typo (did-you-mean)**, deprecated-key migration, `vgi.category_tags` valid, **`vgi.agent_test_tasks` valid** |
| Examples | VGI5xx | `vgi.example_queries` present, valid JSON, complete, **catalog-qualified**, references its object; `vgi.executable_examples` well-formed + **deterministic (ORDER BY)** |
| Settings | VGI6xx | setting descriptions |
| Pragmas | VGI7xx | pragma descriptions |
| Constraints | VGI8xx | FK/PK/check validity; completeness nudges (no constraints / PKs / NOT NULL anywhere); per-table primary key; **`<table>_id` column with no FK suggests one**; **a key-shaped column shared by several tables with no FK may be a missing relationship** |
| Attach options | VGI10xx | every `vgi_catalogs()` attach option is documented (description present + meaningful) |
| Structure | VGI11x/13x | **schema not empty**; warn on excessive table/function counts and over-long table/function names; **schema object-count cap (>50 by default)** |
| Execution | VGI9xx | example queries **must bind (error)**, runtime/data failures are warnings; **executable examples must run + match expected output + run fast** (slow ones warn, naming the example); CHECK constraints bind; advertised attach options are accepted and advertised catalogs attach (`--execute`, **on by default**); per-query timeout so nothing runs forever |

**Strict by default.** `vgi-lint` ships a strict profile: descriptions on every
table/view/function, classifying/title/keyword/source-url tags, column
documentation, per-table primary keys, and example coverage are all enforced by
default. To run a lighter profile, turn rules off in config — e.g.
`ignore = ["VGI112", "VGI113", "VGI124", "VGI126", "VGI202"]` — or set
`[tool.vgi-lint-check.severity]` per code. Use `vgi-lint rules` to see every rule
and its default.

See **[RULES.md](RULES.md)** for the full per-rule reference (codes, default
severities, and what each checks). Run `vgi-lint rules` to list them from your
installed version, or `vgi-lint explain VGI112` for one.

**Link checking is on by default** (VGI171): URLs and images in descriptions,
and `source_url`/`vgi.source_url` repo links, are resolved over HTTP and flagged
if they 404. Only definitive client errors (4xx) are reported — timeouts, DNS
failures, 5xx, and access-gated codes are skipped so CI isn't flaky. Disable
with `--no-check-links` (or run fully offline).

**Execution is on by default** (`--no-execute` for a static-only lint). Execution
rules run against the live worker under a per-query wall-clock cap
(`execute_timeout`, default 30s) so a runaway query can never hang a lint run.

There are **two tiers of examples**:

- **Illustrative** — `vgi.example_queries` *and* a function's native
  `Meta.examples` (DuckDB's `duckdb_functions().examples`), deduped by SQL across
  tables, views, macros, and scalar/aggregate/table functions. These teach usage
  shape. VGI901 splits the verdict: an example that **doesn't bind** (unknown
  table/column/function, bad types — a real authoring bug) is an **error**; one
  that binds but **fails at runtime** (it may just need data/context) is a
  **warning**.
- **Executable** — `vgi.executable_examples`: self-contained, must-run examples
  that are the contract and the highest-quality material for LLMs. **VGI906**
  runs every statement in order (ERROR if any fails — not filter-skipped, they
  must be self-contained); **VGI907** asserts a statement's output against its
  optional `expected_result` (warning). `expected_result` lives on the
  individual statement, so a multi-statement example can assert any step.

  Write `expected_result` as a **list of row-objects keyed by column name** —
  `[{"class": "strong"}]` — which is self-documenting (a bare scalar or a list of
  rows is also accepted). Comparison stringifies cells (`NULL` → `null`, booleans
  lowercase, numbers as printed — `1.0`, not `1`) and matches rows in order. On a
  mismatch **VGI907 prints the actual output in that exact canonical form**, so
  you can copy it straight into `expected_result` instead of guessing how a value
  is represented.

```jsonc
// vgi.executable_examples on any object (catalog, schema, table, view, function)
[
  {
    "name": "classify a strong quake",
    "description": "magnitude_class buckets a Richter value; 6.2 -> 'strong'.",
    "sql": [                                  // string | [string] | [{description, sql, expected_result?}]
      {"description": "set up a session option", "sql": "SET threads=2"},
      {"description": "Classify magnitude 6.2",
       "sql": "SELECT volcanos.main.magnitude_class(6.2) AS class",
       "expected_result": [{"class": "strong"}]}   // optional; cells compare as strings, rows in order
    ]
  }
]
```

Executable examples should be **re-runnable** (e.g. use `CREATE OR REPLACE`),
since VGI906 and VGI907 each run the statement sequence. Keep the set focused:
**VGI508** warns when one object declares more than
`options.max_executable_examples` (default 10) — each runs against the worker
under `--execute`, and a long list is noise for an LLM. Every executable-example
finding's `fix` is fully self-describing (the JSON shape, `expected_result`
format, and the catalog-qualified/self-contained requirement), so a coding agent
can author or repair the tag straight from `--format agent`/`json` output.

```toml
[tool.vgi-lint-check.execution]
enabled     = true       # default; --no-execute to disable
mode        = "explain"  # explain (bind-only, cheapest) | limit | run — for VGI901
limit       = 1          # row cap for limit/VGI902 modes
timeout     = 30.0       # per-query seconds; 0 disables the guard
concurrency = 1          # run example queries across N cursors in parallel
slow_seconds = 5.0       # VGI908 warns on an executable example slower than this (0 = off)
```

**VGI908 flags slow executable examples** that bloat CI — naming the offending
example and its measured time (`executable example 'heavy-scan' is slow (8.2s >
5s)`). It reuses the timing VGI906 already measures, so detection adds no extra
execution pass.

**Parallel execution.** `concurrency > 1` (or `--execute-concurrency N`) runs
example queries across N cursors that share the attach, so the VGI worker pool
serves them from distinct workers. On a compute-bound worker this is roughly
linear (measured ~3.5× at N=4 on `vgi-units`); on a tiny/local worker it's a
wash, since there's no per-query work to overlap. Multi-statement executable
examples stay ordered on their own cursor; findings remain deterministic.

## Documentation review (LLM-as-judge)

The deterministic linter checks *mechanics* (presence, length, echoes, validity).
`vgi-lint review` adds an **advisory, opt-in** layer that judges what rules can't —
**accuracy, clarity, completeness, audience-fit** — by sending each object's
descriptions **plus its real structure** (columns, types, constraints, examples)
to an LLM with a rubric. Grounding it in the facts is what makes the feedback
reliable (it catches prose that contradicts the schema), not a vibe check.

```bash
vgi-lint review <worker>                 # default backend: the local `claude` CLI
vgi-lint review <worker> --format json   # machine-readable verdicts
```

- **Default backend is the local `claude` CLI** (`claude -p`), so judging runs on
  your **Claude Pro/Max subscription** — no per-token API fees. `--review-backend
  api` uses the Anthropic API instead (needs `ANTHROPIC_API_KEY` + the `anthropic`
  package). Pick a model with `--review-model`.
- **Verdicts are cached by content hash** (`.vgi-review-cache.json`), so unchanged
  docs aren't re-judged — a re-run is free. `--no-review-cache` disables it.
- It's **separate from the lint and never gates** — per-object sub-scores (1–5)
  and concrete suggestions, advisory only. The deterministic lint stays the gate.
- Objects are batched per model call (`--review-batch`, default 8) to stay within
  subscription rate limits.

## Agent-suitability testing (`vgi-lint simulate`)

Documentation review grades *prose*. `simulate` answers the harder question: **can
an agent/SQL-analyst actually accomplish real work here using only what's exposed?**
A worker declares a **fixed** task suite in `vgi.agent_test_tasks`; `simulate` runs
an LLM analyst through each one — it sees only a bounded orientation listing and the
task *prompt* (never the solution) and **discovers the schema through tools**, just
like a real agent: `list_tables`, `describe_table`, `describe_function`, and a guarded
`run_sql` (a local mirror of the production "ask AI" tool contract). It iterates until
it answers. It's a real test, not a vibe check: grading is **execution-based**.

```jsonc
// vgi.agent_test_tasks (catalog tag) — a fixed, version-controlled acceptance suite
[
  {
    "name": "kwh to joules",
    "prompt": "How many joules is 100 kWh? Return one column named joules.",  // ONLY this is shown to the analyst
    "reference_sql": "SELECT units.main.convert(100, 'kWh', 'J') AS joules"    // canonical solution — hidden; re-run to grade
  }
]
```

```bash
vgi-lint simulate <worker>              # run the suite (gates on --min-pass-rate)
vgi-lint simulate <worker> --suggest 5  # authoring: propose candidate tasks as tag JSON
```

- **Grading is layered, strongest wins:** (1) compare the analyst's answer to the
  `reference_sql`'s terminal result (deterministic; stores the *query*, so it
  survives data drift), (2) a `check_sql` assertion over the analyst's post-session
  state, (3) an LLM judge against `success_criteria`. Friction (what metadata was
  missing/confusing) is always surfaced.
- **Tool-mediated discovery** — the analyst is handed only a names-and-one-liners
  listing, then pulls columns/signatures/constraints on demand through the discovery
  tools (no full catalog dump). This scales and mirrors how real agents work, so the
  friction it surfaces reflects real metadata gaps.
- **The path is scored, not just the outcome.** Each task gets a **discoverability
  score** (0–100) from *how* the agent got there — penalizing wasted effort the
  metadata should have spared it (queries that failed to bind, hitting a mandatory
  filter by trial-and-error, re-inspecting an object whose description was too thin,
  looking up something that doesn't exist, or never converging). A task can **pass
  yet score low** — that's the signal the docs need work. Each fault becomes a
  concrete **suggestion** (e.g. "add a `vgi.executable_examples` entry showing this",
  "tighten the column docs — VGI2xx"), deduped across the suite. Raw step count is
  *not* penalized, so an inherently complex task isn't marked down for being complex —
  only for friction. Prefer fixing the worker's metadata over raising `--attempts`:
  needing retries is itself a discoverability finding.
- **The solution is hidden from the analyst** — only `prompt` reaches it, so the
  test measures whether the path is *discoverable* from metadata, not whether the
  agent can copy an answer. Strict result grading is the contract: column names,
  values, and row order must match the reference (per-task `unordered` /
  `ignore_column_names` opt-outs), so prompts should name their output columns.
- **Stateful tasks are supported:** the analyst may build session-local state
  (temp views, `SET`); a guard blocks anything that escapes the disposable session
  (worker writes, `ATTACH`/`INSTALL`/`COPY … TO`).
- **It's a test:** exits non-zero when the pass rate is below `--min-pass-rate`
  (default 1.0); `--advisory` never gates; `--attempts N` retries to tame
  actor non-determinism. Same `claude`-CLI-by-default backend and verdict cache as
  `review`.
- **Object coverage:** the report shows how many of the worker's objects — functions
  **and tables/views** — the suite's `reference_sql` actually exercises
  (`object coverage 16/16 (100%)`) and names the untested ones — so a suite can't
  quietly leave half the API unchecked while scoring 100% pass-rate. (Counting tables
  matters for table-centric workers: a geodata worker whose surface is all tables would
  otherwise read 0/1.) `vgi-lint simulate <worker> --suggest` is
  **coverage-driven**: it proposes a suite sized to cover the worker's functions
  (bare `--suggest` auto-sizes; `--suggest N` caps at N) rather than a fixed count.
- **Tasks run in parallel** (`--concurrency`, default 4): each task is judged on its
  own cursor against the VGI worker pool, so a multi-task suite finishes in roughly
  the time of its slowest task, not the sum (~3.4× on a 4-task suite).
- **Double-duty:** the encoded `reference_sql` doubles as curated **few-shot
  guidance** a worker's MCP server / `suggest_queries` can expose to real agents.

## Attach options

A worker advertises its attach-time options through `vgi_catalogs()` **before**
attach — each option has a `name`, `description`, `type`, and `default_value`.
`vgi-lint` reads them and checks they're documented (**VGI1001/VGI1002**): an
agent choosing the worker relies on those descriptions to know what each option
does. Whether an option is *required* is not flagged on the wire — it's inferred
from the absence of a default. With `--execute`, two live checks also run:

- **VGI904** attaches a throwaway handle passing every advertised option at its
  default and confirms the worker actually accepts each one (options whose type
  can't be reconstructed from a stringified default — `STRUCT`/`MAP`/array/blob —
  are skipped rather than guessed).
- **VGI905** confirms every catalog `vgi_catalogs()` advertises can be attached.

## Reserved tags

VGI workers attach metadata via tags; `vgi-lint` recognizes these reserved keys
(set them on the catalog, a schema, a table/view, or — where noted — a function):

| Tag | Purpose |
| --- | --- |
| `vgi.doc_llm` | LLM-oriented narrative doc — what the object is and when to use it (tool selection). *Complements, doesn't duplicate, the object's own `description`/comment.* |
| `vgi.doc_md` | Richer Markdown narrative doc for human docs / listing pages |
| `vgi.doc_links` | JSON array of links to more docs — URL strings or `{"title","url"}` objects (validated + resolved) |
| `vgi.example_queries` | JSON list of `{"description","sql"}` *illustrative* example queries |
| `vgi.executable_examples` | JSON list of self-contained, **must-run** examples (see below) |
| `vgi.agent_test_tasks` | JSON list of fixed analyst tasks `{name, prompt, reference_sql?, success_criteria?, check_sql?}` — the suite `vgi-lint simulate` runs (see below) |
| `vgi.title` | Human/marketing display name (vs. the machine name) |
| `vgi.keywords` | JSON array of search keywords / synonyms — `["a","b"]` (comma-separated string is now a **VGI138 error**) |
| `vgi.category_tags` | JSON array of category labels for faceting — on any object **except the catalog** |
| `vgi.result_columns_md` | Markdown doc of a table function's returned columns (for dynamic schemas DuckDB can't expose) |
| `vgi.source_url` | Link to where the object is implemented (repo/file) |
| `vgi.author` | Author / maintainer attribution (catalog) |
| `vgi.copyright` | Copyright notice (catalog) |
| `vgi.license` | License name or SPDX identifier (catalog) |
| `vgi.support_contact` | Where to report issues/bugs — email or URL (catalog) |
| `vgi.support_policy_url` | Link to the support / SLA policy (catalog) |

> **Renamed:** `vgi.doc_llm`/`vgi.doc_md` (was `vgi.description_llm`/`_md`) and
> `vgi.result_columns_md` (was `vgi.columns_md`). The old keys still work (dual
> recognition) but **VGI405** nudges you to migrate; they'll stop being
> recognized in a future version.

`vgi.doc_llm`/`vgi.doc_md` are **required on the catalog, every schema, and
(under the strict default) every table, view, and function** — and validated
when set (minimum length, must differ from each other and from the object's own
description). The catalog `source_url` and keywords are enforced by the strict
default; `vgi.title` is required on the **catalog and schemas** only (optional
elsewhere, but validated when set); author/copyright/license are encouraged
(info). Relax any of this (e.g. back to optional docs on tables/views/functions)
via config.

## Data versions

A VGI worker can publish multiple data versions whose metadata differs. The tool
can lint one or all of them and compare quality across versions:

```bash
uv run vgi-lint versions <location>            # list published versions
uv run vgi-lint <location> --data-version 2.0.0
uv run vgi-lint <location> --all-data-versions # per-version report + comparison
```

## Baselines (grandfathering)

Adopt the linter on an existing worker without a wall of failures: record current
findings as a baseline, then fail CI only on **new** findings. Baselines are
per data version (`<prefix>.<version>.json`).

```bash
uv run vgi-lint <location> --baseline vgi-lint-baseline --update-baseline
uv run vgi-lint <location> --baseline vgi-lint-baseline --fail-on warning
```

## Configuration

`[tool.vgi-lint-check]` in `pyproject.toml` (or a dedicated `vgi-lint.toml`):

```toml
[tool.vgi-lint-check]
location = "uv run worker.py"
select = ["ALL"]
ignore = ["VGI113"]
fail_on = "error"

[tool.vgi-lint-check.severity]
VGI201 = "error"

[tool.vgi-lint-check.options]
column_comment_min_ratio = 0.8
# Required tags are opt-in (empty by default) — set them if your workers have a
# tagging convention you want enforced:
# required_schema_tags = ["provider", "domain"]

[tool.vgi-lint-check.per-object]
"volcanos.hans.*" = { ignore = ["VGI112"] }
```

Precedence: defaults < `pyproject.toml` < `vgi-lint.toml` < CLI flags.

## Exit codes

`0` clean (or below `--fail-on`) · `1` config/tool error · `2` findings ≥
`--fail-on` (regressions only when a baseline is set) · `3` connection error.

## Security / trust boundary

A subprocess `LOCATION` is **executed as a command** to launch the worker (the
`vgi` extension spawns it). Treat `location` like any shell command: never pass
an attacker-controlled value, and in CI never derive it from untrusted input
(e.g. a fork PR title/branch). Prefer a fixed path or HTTP URL you control.

## GitHub Action (reusable)

This repo ships a composite action so a worker repo can lint itself in CI with a
single step — it installs `uv`, runs the linter (the signed `vgi` community
extension is installed automatically), gates on `fail-on`, and posts the findings
to the job summary. **Build the worker first**, then point the action at it:

```yaml
# .github/workflows/ci.yml — inside a job that has already built the worker
      - name: VGI metadata quality
        uses: Query-farm/vgi-lint-check@v1
        with:
          location: "$PWD/target/release/units-worker"   # binary, command, or HTTP URL
          fail-on: warning                                 # info | warning | error | never
```

Gate releases harder than everyday CI — e.g. `fail-on: warning` on push/PR while
the worker's quality is being raised, and `fail-on: error` (plus `execute: true`)
in the publish workflow:

```yaml
      - uses: Query-farm/vgi-lint-check@v1
        with:
          location: "$PWD/target/release/units-worker"
          fail-on: error
          # execution rules (VGI9xx) run by default; set execute: false for static-only
```

Key inputs: `location` (required), `fail-on` (default `error`), `version` (pin the
linter, e.g. `0.2.0`), `working-directory`, `data-version` / `all-data-versions`,
`baseline`, `execute`, `spatial`, `format` (`terminal|json|agent|jsonl`),
`config`, `args`, `summary`. The action's `exit-code` is exposed as an output. The action ref `@v1`
tracks the latest v1.x of the action; pin to a tag or SHA for full reproducibility.

## Development

```bash
uv run pytest               # unit tests (offline)
uv run pytest --run-live    # also run live tests against real workers
uv build                    # build sdist + wheel into dist/
```

## Releasing (GitHub Actions → PyPI)

Publishing is automated via GitHub Actions using **PyPI Trusted Publishing**
(OIDC — no API token secret to store):

- `.github/workflows/ci.yml` runs the offline test suite (Python 3.11–3.13) and
  a smoke build on every push/PR.
- `.github/workflows/publish.yml` builds, validates (`twine check`), and uploads
  to PyPI when a **GitHub Release is published**. It first checks that the
  release tag matches the `version` in `pyproject.toml`.

One-time setup on PyPI (Trusted Publisher), under the project's *Publishing*
settings (use a "pending publisher" before the first release):

| Field | Value |
| --- | --- |
| Owner | `Query-farm` |
| Repository | `vgi-lint-check` |
| Workflow | `publish.yml` |
| Environment | `pypi` |

Also create a GitHub Environment named `pypi` in the repo settings (it gates the
publish job and is referenced for the OIDC claim).

To cut a release:

```bash
# bump version in pyproject.toml, commit, then tag + create the release
git tag v0.1.0 && git push origin v0.1.0
gh release create v0.1.0 --generate-notes
```

The release publishing event triggers the workflow. (Prefer a token instead of
OIDC? Replace the `publish` job's trusted-publishing step with
`pypa/gh-action-pypi-publish` configured with `password: ${{ secrets.PYPI_API_TOKEN }}`
and add that repository secret.)
