Skip to content
GitHub

Lint rules

This appendix lists all lint rules that bino lint and bino build apply to your report manifests. Lint rules check the content of your documents (semantic/business rules), while schema validation checks the structure.

The Severity column below is the severity a rule reports: it picks the colour of the squiggle in the editor. No lint finding blocks a build, and none of them changes the exit code of bino lint on its own — only a severity you set yourself in bino.toml does that, and bino lint prints that one in front of the finding; see Disabling and re-grading rules.

The last five rules only run on a predef project — a project whose bino.toml carries a [package] table (see Predef packages). A project without that table never sees them.

IDNameSeverityDescription
report-artefact-requiredReport Artefact RequiredWarningAt least one ReportArtefact document must be defined.
artefact-layoutpage-requiredArtefact LayoutPage RequiredWarningEach ReportArtefact must have at least one LayoutPage that matches its constraints and format.
text-content-requiredText Content RequiredWarningText components must have a non-empty spec.value field.
dataset-requiredDataset RequiredWarningTable, ChartStructure, ChartTime, ChartScatter, ChartBubble, and ChartBullet components must have a spec.dataset binding.
table-sum-title-unusedTable Sum Title UnusedWarningspec.sumTitle only labels the grand-total row of type: sum / type: opt tables.
missing-required-referenceMissing Required ReferenceErrorRequired child references (ref) must point to existing documents.
page-layout-slots-usedPage Layout Slots UsedWarningLayoutPage children count must match the expected slots for its pageLayout.
card-layout-slots-usedCard Layout Slots UsedWarningLayoutCard children count must match the expected slots for its cardLayout.
asset-reference-undefinedAsset Reference UndefinedWarningImage references must name a declared Asset document or a reachable URL.
asset-source-missingAsset Source MissingWarningAn Asset's spec.source.localPath must point at an existing file.
inline-ref-boundsInline Reference BoundsWarning@inline(N) references must have valid indices within the dependencies array.
dataset-source-exclusiveDataSet Source ExclusiveWarningThe source field is mutually exclusive with query and prql in DataSet specs.
inline-naming-conflictInline Naming ConflictWarningDocument names must not start with _inline_ as this prefix is reserved.
ref-paramsRef ParamsWarningmetadata.params declarations and the parameter values passed to referenced documents must match.
dataset-derive-conflictDataSet Derive ConflictWarningA previous-period slot may be declared in derive or assert, not both.
reserved-name-prefixReserved Name PrefixWarningDataSource and DataSet names must not start with bino_ or _bino_.
i18n-code-unusedInternationalization Code UnusedWarningAn Internationalization spec.code must exactly match an artefact spec.language.
i18n-namespace-unreferencedInternationalization Namespace UnreferencedWarningA named spec.namespace is only read by components whose i18nNamespace points at it.
i18n-title-namespace-deprecatedtitleNamespace DeprecatedWarningtitleNamespace is deprecated; use i18nNamespace, which also inherits to children.
package-config-invalidPackage Config InvalidErrorThe [package] table in bino.toml must satisfy the constraints a registry publish enforces.
predef-name-namespacePredef Name NamespaceErrorDocuments inside the package include set must be named after the package.
predef-forbidden-kindPredef Forbidden KindErrorArtefact kinds and credential kinds must not be inside the package include set.
predef-asset-absolute-pathPredef Asset Absolute PathErrorA packaged Asset's spec.source.localPath must be relative to its manifest.
predef-external-refPredef External ReferenceErrorStructural and presentational references must resolve inside the package or a declared dependency.

Every bino report project needs at least one ReportArtefact document to define what PDF(s) to generate.

Example fix: Add a ReportArtefact manifest to your project:

apiVersion: bino.bi/v1
kind: ReportArtefact
metadata:
  name: my-report
spec:
  title: Monthly Sales Report

Each ReportArtefact must have at least one LayoutPage that:

  1. Passes constraints — The page's metadata.constraints must match the artefact's context (labels, spec, or mode). The linter checks both build and preview modes; a page is valid if it matches in either mode.
  2. Matches format — The page's spec.pageFormat must match the artefact's spec.format. Both default to xga if not specified.

Example problem:

# Artefact with format: a4
kind: ReportArtefact
metadata:
  name: print-report
spec:
  format: a4
---
# Page with default format (xga) won't match!
kind: LayoutPage
metadata:
  name: page1
spec:
  children: [...]

Fix: Set spec.pageFormat: a4 on the LayoutPage.

Text components are meant to display content. An empty spec.value produces blank output.

Example problem:

kind: Text
metadata:
  name: header
spec:
  value: "" # Empty!

Fix: Provide meaningful content in spec.value.

Data visualization components (Table, ChartStructure, ChartTime, ChartScatter, ChartBubble, ChartBullet) need a dataset to display. Without spec.dataset, there's nothing to render.

Example problem:

kind: Table
metadata:
  name: sales-table
spec:
  # Missing dataset!

Fix: Add a dataset reference:

spec:
  dataset: sales-data

Or multiple datasets for charts:

spec:
  dataset:
    - revenue
    - costs

spec.sumTitle labels the grand-total row at the bottom of a table. That row only exists for type: sum and type: opt. On type: list (the default), type: sumnototal and type: optnototal no total row is generated, so the label is silently dropped.

Example problem:

kind: Table
metadata:
  name: sales-table
spec:
  dataset: sales-data
  type: list # renders no total row …
  sumTitle: Total # … so this label is never shown

Fix: render a total row, or drop the label.

# Render the grand-total row and label it
spec:
  dataset: sales-data
  type: sum
  sumTitle: Total # leave empty to use the IBCS ❖ symbol

The rule also covers tables defined inline in LayoutPage, LayoutCard and Grid children, and in Tree nodes. A child that inherits its type from a referenced Table via ref is checked against the effective type, so overriding only sumTitle on a type: sum component does not warn.

Child references (ref) in LayoutPage and LayoutCard must point to documents that exist. This rule catches typos, missing files, and broken references early.

Severity: Error

Example problem:

kind: LayoutPage
metadata:
  name: dashboard
spec:
  pageLayout: 2x2
  children:
    - kind: ChartTime
      ref: revnue_chart  # Typo! Should be "revenue_chart"

Error message:

required reference "revnue_chart" of kind "ChartTime" not found (use optional: true to allow missing refs)

Fixes:

  1. Correct the typo — Fix the reference name to match the target document's metadata.name.

  2. Create the missing document — Add the referenced manifest to your project.

  3. Mark as optional — If the reference is legitimately optional (e.g., debug-only component), add optional: true:

children:
  - kind: ChartTime
    ref: debug_chart
    optional: true  # Won't error if missing

Note: References that exist but are filtered out by constraints (e.g., mode == "preview") do not trigger this error. Only genuinely missing references cause failures.

See also: Optional references

Each LayoutPage must have exactly the number of children that match its pageLayout slot count. This rule is evaluated per artefact, counting only children that:

  1. Pass constraints — Children with metadata.constraints must match the artefact's context.
  2. Have valid refs — Children with ref must point to an existing document (or be marked optional: true). Missing required refs trigger the missing-required-reference error. Optional refs that are missing don't count as slots.
  3. Are renderable — Children must have either a ref or inline spec.

Slot counts for predefined layouts:

LayoutSlots
full1
split-horizontal2
split-vertical2
2x24
3x39
4x416
1-over-23
1-over-34
2-over-13
3-over-14

For custom-template, the slot count is the number of distinct named area tokens in pageCustomTemplate:

# 3 slots: a, b, c
pageLayout: custom-template
pageCustomTemplate: |
  "a a"
  "b c"

# 2 slots: aa, bb
pageLayout: custom-template
pageCustomTemplate: |
  "aa aa"
  "bb bb"

Example problem:

kind: LayoutPage
metadata:
  name: dashboard
spec:
  pageLayout: 2x2 # Expects 4 children
  children:
    - kind: Text
      spec: { value: "Hello" }
    - kind: Table
      ref: sales-table
    # Only 2 children, but 2x2 expects 4!

Fix: Add children to fill all slots, or change pageLayout to match actual children count.

Same as page-layout-slots-used, but for LayoutCard components. Each card's cardLayout defines how many children it expects.

Example problem:

kind: LayoutCard
metadata:
  name: summary-card
spec:
  cardLayout: split-horizontal # Expects 2 children
  children:
    - kind: Text
      spec: { value: "Only one child" }

Fix: Add a second child or change cardLayout: full.

Images are referenced by the name of an Asset document, not by file path. This rule checks every image reference and warns when it cannot resolve, which otherwise fails silently: the name is passed through to the browser as if it were a URL, and you get a broken image with no error.

It covers messageImage (LayoutPage), titleImage (LayoutCard), source (Image) — including components defined inline in children or nodes — and asset: image references inside Markdown (messageText, titleBusinessUnit, and a Text component's value).

Example problem:

kind: LayoutPage
metadata:
  name: dashboard
spec:
  pageLayout: full
  messageImage: logo # No Asset document is named "logo"
  messageText: "Report by ![logo](asset:compnay) " # Typo! Should be "company"

Fix: declare the Asset and reference it by its metadata.name.

apiVersion: bino.bi/v1alpha1
kind: Asset
metadata:
  name: logo
spec:
  type: image
  mediaType: image/png
  source:
    localPath: images/logo.png # Relative to this manifest

A reference resolves when it is one of:

  • the metadata.name of an Asset document (messageImage: logo), optionally with the asset: prefix (messageImage: asset:logo);
  • an absolute URL or data URI the browser can fetch (https://…, data:image/png;base64,…).

A value that looks like a file path (images/logo.png) is checked against the filesystem, relative to the manifest, and warns when the file is absent. Note that bino serves only declared Assets — a bare path is not served even when the file exists, so prefer declaring an Asset.

Font Assets do not count: they are not available to image references.

See also: Asset

An Asset whose spec.source.localPath points at a file that is not on disk. The path is resolved relative to the manifest that declares it, the same way the renderer resolves it.

This is a build error at render time; the rule surfaces it while you are still editing, instead of at the next build.

Example problem:

kind: Asset
metadata:
  name: logo
spec:
  type: image
  mediaType: image/png
  source:
    localPath: images/logo.png # File was moved or deleted

Fix: restore the file, or correct localPath. Assets sourced from remoteURL or inlineBase64 are not checked.

See also: Asset

When using inline DataSource definitions in a DataSet's dependencies, the @inline(N) references in queries must point to valid indices.

Example problem:

kind: DataSet
metadata:
  name: my_dataset
spec:
  dependencies:
    - type: csv
      path: ./data/sales.csv
  query: |
    SELECT * FROM @inline(5)  # Index 5 doesn't exist!

Error message:

@inline(5) out of bounds (have 1 inline dependencies)

Fix: Use @inline(0) to reference the first (and only) inline dependency.

See also: Inline DataSet definitions

The source field in DataSet provides direct pass-through to a DataSource without transformation. It cannot be used together with query or prql.

Example problem:

kind: DataSet
metadata:
  name: conflicting_dataset
spec:
  source: sales_csv
  query: |  # Can't have both source and query!
    SELECT * FROM sales_csv

Fix: Choose one approach:

# Option 1: Use source for pass-through
spec:
  source: sales_csv

# Option 2: Use query for transformation
spec:
  query: SELECT * FROM sales_csv
  dependencies:
    - sales_csv

See also: Direct source pass-through

Document names starting with _inline_ are reserved for generated inline definitions. User-defined documents must not use this prefix.

Example problem:

kind: DataSource
metadata:
  name: _inline_my_data  # Reserved prefix!
spec:
  type: csv
  path: ./data.csv

Fix: Choose a different name that doesn't start with _inline_:

metadata:
  name: my_data

Validates metadata.params declarations on documents that accept parameters (LayoutPage, ReportArtefact and the other param-capable kinds) and the values passed to them where they are referenced: a layout child, a grid child or a tree node with ref and params. A parameter that is passed but not declared, a required parameter that is missing, or a value that does not fit the declared type is reported.

Example problem:

kind: LayoutPage
metadata:
  name: region_page
  params:
    - name: region
      type: string
      required: true
---
kind: ReportArtefact
metadata:
  name: report
spec:
  layoutPages:
    - ref: region_page
      params:
        regoin: North   # misspelled, and the required 'region' is missing

Fix: Pass exactly the declared parameters:

  layoutPages:
    - ref: region_page
      params:
        region: North

A DataSet declares what a previous-period slot means either with derive (the CLI produces the column) or with assert (the query supplies the column and the CLI checks it). The same slot cannot be in both maps: the executor rejects the dataset before running it.

Example problem:

kind: DataSet
metadata:
  name: sales
spec:
  query: SELECT ... AS ac1, ... AS pp1, ... AS date FROM sales_csv
  derive:
    pp1: { from: ac1, shift: 1 year, grain: month }
  assert:
    pp1: { from: ac1, shift: 1 year, grain: month }  # pp1 is also derived!

Fix: Keep the slot in one map. Use assert when the query already returns the column, derive when it does not:

spec:
  query: SELECT ... AS ac1, ... AS pp1, ... AS date FROM sales_csv
  assert:
    pp1: { from: ac1, shift: 1 year, grain: month }

Names starting with bino_ are reserved for the functions and macros the CLI registers in every DuckDB session (op, iop, bino_shift), and names starting with _bino_ for the views it creates while executing a dataset with derive or assert. A DataSource or DataSet with such a name can shadow one of them. The executor also refuses to run a project with a _bino_ name.

Example problem:

kind: DataSource
metadata:
  name: bino_sales  # Reserved prefix!
spec:
  type: csv
  path: ./sales.csv

Fix: Choose a name without the prefix:

metadata:
  name: sales

Locale lookup is an exact string match against the artefact's spec.language. An Internationalization document whose spec.code matches no artefact language is loaded into the browser store and then never read — most often because the code carries a region subtag.

Example problem:

kind: ReportArtefact
metadata:
  name: sales_report
spec:
  language: de # …the report renders in 'de'…
---
kind: Internationalization
metadata:
  name: labels_de
spec:
  code: de-DE # …but this bundle is filed under 'de-DE' and never consulted
  content:
    global.ac1: Ist

Fix: Use the plain language code, exactly as written on the artefact. There is no BCP 47 normalization.

spec:
  code: de

See also: Internationalization

spec.namespace files a translation set under a named bundle instead of _system. A named bundle is only consulted by components whose i18nNamespace (or a page/card titleNamespace) points at it, so a namespace nothing references is dead weight.

Example problem:

kind: Internationalization
metadata:
  name: audited_labels_de
spec:
  code: de
  namespace: audited # nothing in the bundle sets i18nNamespace: audited
  content:
    global.ac1: Ist (geprüft)

Fix: Point a component, card, page or the artefact at it — or drop spec.namespace so the content merges into _system, the bundle every component reads.

kind: LayoutPage
metadata:
  name: audited_page
spec:
  i18nNamespace: audited

See also: Scoped overrides with i18nNamespace

titleNamespace on LayoutPage / LayoutCard only ever applied to that page or card's own title; it never reached the children. i18nNamespace supersedes it, takes precedence when both are set, and inherits to every descendant.

Example problem:

kind: LayoutPage
metadata:
  name: audited_page
spec:
  titleNamespace: audited # the title is translated, the table below is not
  children:
    - kind: Table
      ref: sales_table

Fix: Rename it to i18nNamespace.

spec:
  i18nNamespace: audited # title and children both resolve against 'audited'

See also: Scoped overrides with i18nNamespace

The [package] table is not checked when bino.toml is loaded — an unrelated command must not fail over a field only bino publish reads. bino lint checks it instead and reports the first problem it finds as one finding on bino.toml.

Example problem:

[package]
name = "acme/starter-kit" # no scope
visibility = "secret" # not "public" or "private"
compat-engine = "not-a-range"

Fix: Use a scoped name, one of the two visibilities, and a valid semver range.

[package]
name = "@acme/starter-kit"
visibility = "private"
compat-engine = ">=1.0.0"

When this rule fires, the four content rules below do not run: the include set and the name prefix both derive from the table, so their findings would be meaningless.

Every document a package publishes carries the package name, so a consumer who installs two kits never gets a name collision. One document — the package's main definition — may be named exactly like the package; every other one needs a <package>/<definition> name.

Example problem:

# components/revenue_table.yaml, inside the include set of @acme/starter-kit
kind: Table
metadata:
  name: revenue_table # not namespaced

Fix: Prefix it with the package name, or move the manifest out of the include set.

metadata:
  name: "@acme/starter-kit/revenue_table"

A DataSource is the exception: its name becomes a DuckDB view name and is limited to two segments (@scope/name), so it cannot carry a package segment at all. Either name it exactly like the package (as the main definition) or keep it out of the include set — mocks/ is the conventional place.

Two groups of kinds must never be published.

  • Artefacts (ReportArtefact, LiveReportArtefact, ScreenshotArtefact, DocumentArtefact) render a report. They are the harness that previews your package, not package content. Keep them in reports/ or mocks/, both of which are excluded from every package.
  • Credentials (ConnectionSecret, SigningProfile) must never leave your machine. secrets/ and signing/ are part of the default include set, on purpose: if they were excluded the rule could never fire on the one layout where credentials actually live.

Fix: Move the manifest out of the include set, or write an explicit include list in [package] that leaves its folder out.

A published package is unpacked on someone else's machine, so an Asset inside it must find its file relative to its own manifest. An absolute path — /home/you/logo.svg, or a C:\ drive path — resolves to nothing there.

Example problem:

kind: Asset
metadata:
  name: "@acme/starter-kit/logo"
spec:
  source:
    localPath: /home/you/assets/logo.svg

Fix: Put the file next to the manifest and reference it relatively.

spec:
  source:
    localPath: logo.svg

Values containing ${ are skipped (missing-env-var owns those), as are empty ones (inlineBase64 / remoteURL forms).

A packaged document may only point at documents that travel with it: something else inside the include set, or a package listed under [dependencies] in bino.toml. Anything else breaks the moment a consumer installs the package.

The rule inspects structural and presentational references only:

  • {kind, ref} children anywhere in the document
  • spec.selectedStyle (a ComponentStyle)
  • spec.ruleset (a RuleSet)

Data bindings are deliberately exempt. spec.dataset is the seam a predef exists for: a packaged Table binds to a dataset the consumer supplies, so it points outside the package by construction. Flagging it would make the primary use case unlintable. The same reasoning covers a DataSet's spec.dependencies and spec.source, and spec.i18nNamespace names a namespace rather than a document. Raw SQL in spec.query is never parsed.

Example problem:

# components/revenue_table.yaml, inside @acme/starter-kit
kind: Table
metadata:
  name: "@acme/starter-kit/revenue_table"
spec:
  dataset: ${DATASET} # fine — the consumer binds this
  selectedStyle: mock_theme # not fine — mock_theme lives in mocks/

Fix: Move the style into the package and namespace it, or — when it belongs to someone else's package — declare that package as a dependency.

[dependencies]
"@other/kit" = "^1.0.0"

References containing ${ (a parameter or environment placeholder), glob characters, or the reserved _inline_ prefix are skipped. A name that matches no document at all is left to missing-required-reference, which words it better.

The bino linter is designed for easy rule addition. Each rule has:

  • ID: A unique identifier (e.g., naming-lowercase, reference-undefined-dataset).
  • Name: A short human-readable name.
  • Description: An explanation of what the rule checks.
  • Check function: The logic that inspects documents and returns findings.

Rules are registered in internal/report/lint/rules.go.

The [lint] table in bino.toml switches rules off and changes the severity they report:

[lint]
disable = ["table-sum-title-unused"]

[lint.severity]
text-content-required = "error"
i18n-code-unused = "info"

disable hides a rule's findings everywhere — CLI output, lint log and editor. severity re-grades them everywhere: bino lint prints the finding under its new severity and the editor squiggles it in the matching colour.

Only a severity you set in bino.toml affects an exit code: a rule you raise to "error" fails bino lint on its own, a rule you lower to "info" no longer counts for --fail-on-warnings. The Severity column above changes no exit code by itself, and no lint finding fails bino build[lint] changes neither.

Besides the rule IDs above you can name four more IDs, which findings carry although no rule of that name declares them:

IDCovers
schema-validationA manifest that failed schema validation.
manifest-loadA manifest that could not be loaded at all.
engine-version-incompatibleThe engine-version pin check in bino.toml.
missing-required-referenceThe dangling-reference finding above, which page-layout-slots-used emits while it walks the children.

Disabling schema-validation or manifest-load hides the lines only — bino lint still exits non-zero on a bundle it could not load, because the load really failed. Disabling engine-version-incompatible, in contrast, drops the finding and the exit code it forced together.

Those three accept disable only: their weight follows the state of the bundle, so bino lint rejects a [lint.severity] entry on them with a warning rather than honouring nothing.

There is still no per-file or per-line suppression: [lint] works on rule IDs for the whole project.

See Lint rules in bino.toml for the full semantics, plugin rule IDs and how unknown entries are reported.