Reference

Handles

A handle is the stable identity of a contract — the key Trellis matches to connect what one unit provides to what another consumes. Authoring a handle is interpretive; matching one is deterministic. This is the complete guide: what deserves a handle, how to keep it stable, the strict matching rules, source anchors, and language-by-language naming conventions.

Handles are the most important semantic primitive in Trellis. The graph, the linter, hover, jump-to-definition, and every future analysis are built on them. Everything else in a sidecar is prose or metadata; the handles are the part the tooling actually reasons over.

If handles are vague, inconsistent, overfit to file layout, or unreliably inferred, Trellis’ higher-level capabilities are weakened no matter how good the tooling is. Poor handles produce a poor graph. Good handles make Trellis useful. That makes handle design foundational work, not clerical setup.

What a handle is

A handle is a semantic key for a named contract — the stable identity Trellis uses to connect a Provides: entry in one sidecar to a Consumes: entry in another.

  Provides:
    - Billing.Proration.calculate @source("line:42-68") -> Money

  Consumes:
    - PaymentGateway.charge

The handles here are Billing.Proration.calculate and PaymentGateway.charge. The source anchor (@source(...)) and the description (-> Money) are not part of handle identity.

A handle answers exactly one question:

What named contract is this?

It deliberately does not answer:

Where is the code?
What is the current source symbol?
What file is it in?
What line does it start on?

Those belong to source anchors and descriptions. A handle may correspond to a function, method, class, route, table, paragraph, or component — but it does not have to. It can just as well name a business decision, a workflow step, an event, a record layout, a report, or a policy boundary that is only partly visible in source.

The key distinction

This single idea is central to Trellis:

Handle authoring is interpretive. Handle matching is deterministic.

Trellis makes no claim to magic semantic extraction from source. It asks a codebase to name its important contracts clearly and keep those names stable. A human or agent interprets a durable contract from source hints — file names, symbols, routes, tables, tests, comments, domain vocabulary, neighboring sidecars — and records it as a handle. From that point forward, matching is exact and explainable:

source / code / conventions / domain clues
  → human or agent interprets a stable contract
  → sidecar records that contract as a handle
  → Trellis matches handles exactly
  → graph, lint, CLI, and LSP features rely on the declared handle

The inference is judgment and cannot be made perfectly deterministic by a parser. The matching, once the handle is declared, is pure mechanism. Trellis does not make semantic understanding free — it gives you a structured place to record it, and becomes deterministic the moment you do.

Anatomy of an entry

Every Provides: and Consumes: bullet has up to three parts, in this order:

  Provides:
    - Billing.Proration.calculate @source("line:42-68") -> Money
  #   └── handle ───────────────┘ └── source anchor ──┘ └ description ┘
  • Handle — the leftmost dotted-identifier path (or Prefix:path). The graph identity. The only part matching cares about.
  • Source anchor — an optional @source("kind:target") locator saying where the implementation currently lives. Metadata for tools like jump-to-definition; it never affects matching.
  • Description — free human-readable text (a signature, a note). Ignored by the graph.

Only the first structured token is the handle. This is the rule behind the most common authoring mistake — see Accidental prose below.

What deserves a handle

A handle is warranted when a contract is worth depending on and worth keeping stable. Use this test:

If this behavior changed, would downstream code or reviewers need to know
who consumes it?

Handles commonly name:

  • a public API surface
  • a business rule or policy decision
  • a workflow or batch step
  • a data contract, record layout, or event
  • a user-visible behavior or controller action
  • a component or component contract
  • a migration or report output

Not everything deserves a handle. Private helpers, incidental calls, and pure implementation detail usually should not get one. The goal is a graph of real contracts, not a transcription of every symbol in the file. When in doubt, prefer fewer handles with sharper meaning.

Keep handles stable

A stable handle changes only when the contract identity changes — not when the implementation moves.

  Provides:
    - Decision:ExampleWorkflow.rule @source("label:DECISION-PARAGRAPH")

The paragraph, function, class, or file implementing this rule may be renamed, split, or relocated. The handle should stay the same as long as it still names the same decision. The source anchor absorbs the churn; the handle carries the durable identity. Use this test:

If the implementation moved or the source symbol was renamed, would this
handle still name the same contract?

If yes, the handle is stable.

Matching is strict

Once declared, handle matching is exact, case-sensitive, and unnormalized. These are three different handles and will not resolve to one another:

Subscription.create
subscription.create
Subscription.Create

The current semantics are intentionally conservative:

  • case is preserved
  • matching is exact
  • handles are never normalized
  • aliases are never guessed
  • source anchors do not affect identity
  • descriptions do not affect identity

That strictness is what makes the deterministic operations explainable: a Consumes: entry resolves to a matching Provides: or it is reported as a broken link; two identical handles are flagged as duplicate providers; unmatched consumes surface as unresolved dependencies. There is no fuzzy middle ground to explain away.

Typed prefixes

A handle can carry a typed prefix in the Prefix:path form when the contract is not a plain code symbol:

  Provides:
    - Event:invoice.created
    - Decision:BillingWorkflow.discountEligibility
    - Table:billing.invoices

The prefix names the kind of thing being declared — an event, a decision, a table. It is not a category label. Do not invent prefixes as taxonomy:

  # Anti-pattern: prefixes used as labels
  Provides:
    - Internal:Billing.charge
    - Public:Billing.refund

Properties of a unit — visibility, stability, ownership — belong in frontmatter (@stability: internal), not smuggled into the handle. The handle path is for identity; the prefix is for kind.

Source anchors carry location

Because the handle is identity, location needs its own mechanism: the @source(...) anchor, placed after the handle and before the description.

  Provides:
    - Decision:ExampleWorkflow.rule @source("label:DECISION-PARAGRAPH") determines the outcome
    - Billing.Proration.calculate @source("line:42-68") -> Money
  • label:<name> — file-relative labels, paragraphs, sections, symbols, or other named anchors.
  • line:<n> or line:<start>-<end> — line anchors, when there is no stable label or symbol.
  • Other kind:target forms (symbol:, template:, …) are reserved for project- or language-specific locators.

The anchor is metadata. It changes where tools jump to; it never changes what matches. Keeping location out of the handle is what lets the handle stay stable across refactors.

Handles and Consumes

Every Consumes: handle is a graph edge looking for a Provides: handle to land on. When one sidecar provides PaymentGateway.charge and another consumes it, Trellis draws the edge — and that edge powers dependency queries, broken-link diagnostics, hover, and jump-to-definition.

That is why the Consumes Discipline keys off handles. A handle belongs in Consumes: only when a change to that contract’s name, signature, semantics, or existence would force a corresponding change here. List the real contracts; omit language built-ins, incidental stdlib calls, and framework plumbing. Fewer, sharper edges make the graph trustworthy.

Common failure modes

Most bad handles fall into a handful of recognizable shapes.

Too source-located

A source location masquerading as a contract name:

  Provides:
    - app.billing.proration.go.line42

Better — name the contract, anchor the location:

  Provides:
    - Billing.Proration.calculate @source("line:42-68")

Too vague

A name too generic to be globally unique or durable:

  Provides:
    - Rendered

Better:

  Provides:
    - Component:Billing.InvoiceTable

Accidental prose

The handle is only the first token, so prose silently truncates:

  Provides:
    - Calculates billing discount for renewals

Trellis parses the handle as Calculates and treats the rest as description.

Better:

  Provides:
    - BillingDiscount.renewalEligibility

Source syntax instead of Trellis syntax

Language punctuation does not belong in a handle:

  Provides:
    - Api::V1::FilesController#create

Better — convert to the dotted form:

  Provides:
    - Api.V1.FilesController.create

Anchor in the handle

Folding the implementation location into the handle re-couples identity to layout:

  Provides:
    - Decision:ExampleWorkflow.DECISION-PARAGRAPH

Better:

  Provides:
    - Decision:ExampleWorkflow.rule @source("label:DECISION-PARAGRAPH")

Language conventions

The canonical rules above are language-agnostic. Each language then has its own shape for good handles — these conventions are advisory; the rules own the semantics. Two threads run through all of them: handles trend toward dotted identifiers built from the language’s contract boundaries, and typed prefixes appear precisely where the contract is not a direct source symbol (events, tasks, components, tables, decisions). The source anchor, meanwhile, carries the language-native locator kind (symbol:, label:, template:, line:).

Go

Package-oriented handles. Use exported names when they are the contract; skip unexported helpers unless they are the meaningful unit of intent.

  Provides:
    - parser.ParseFile @source("symbol:ParseFile")
    - graph.Graph.Dependents @source("symbol:Dependents")
    - lint.SourceAnchorShape @source("symbol:SourceAnchorShape")

Omit stdlib incidentals (fmt.Errorf, strings.TrimSpace, len) from Consumes:.

Ruby and Rails

Convert :: and # into dots, and include enough namespace to be globally unique.

Ruby / Rails unit Preferred handle
Controller action Api.V1.FilesController.create
Service object method BillingCheckoutService.create_session
Model contract Team.current_bandwidth_usage
Concern method Api.ErrorHandling.render_not_found_error
ViewComponent class method Ui.CardComponent.card_classes
Helper method FilesHelper.file_size_label
Job FormSubmissionDigestJob.perform
  Provides:
    - Api.V1.FilesController.create @source("symbol:create")
    - BillingCheckoutService.create_session @source("symbol:create_session")

Omit route helpers, assigns and instance variables, partial names, ApplicationController, and Active Record query helpers from Consumes:.

Python

Follow module / class / function boundaries; reach for typed prefixes when the contract is an event, task, or data shape rather than a symbol.

  Provides:
    - billing.proration.calculate @source("symbol:calculate")
    - billing.InvoiceService.create_invoice @source("symbol:create_invoice")
    - Task:billing.reconcile_accounts @source("symbol:reconcile_accounts")
    - Event:invoice.created

Django views, DRF serializers, FastAPI route handlers, and Celery tasks all take symbol: anchors. Omit stdlib incidentals (pathlib.Path, datetime, json.loads, logging) from Consumes:.

TypeScript and JavaScript

Match exported modules, functions, classes, and types; use typed prefixes for runtime contracts.

  Provides:
    - billing.calculateProration @source("symbol:calculateProration")
    - billing.InvoiceService.createInvoice @source("symbol:createInvoice")
    - Type:Billing.InvoiceDTO @source("symbol:InvoiceDTO")
    - Event:invoice.created
    - ApiClient:Billing.fetchInvoices

Omit language built-ins, DOM APIs, and incidental utility imports. Bare default, index, and handleClick are not handles.

React

Describe component contracts, hooks, user-visible actions, and data-loading boundaries — not every helper in the file.

  Provides:
    - Component:Billing.InvoiceTable @source("symbol:InvoiceTable")
    - Hook:useInvoiceFilters @source("symbol:useInvoiceFilters")
    - Action:InvoiceTable.selectInvoice @source("symbol:handleSelectInvoice")
    - Route:BillingInvoicesPage

Note that the handle (Action:InvoiceTable.selectInvoice) names the user-facing contract while the anchor (symbol:handleSelectInvoice) points at the current implementation. Omit imported UI primitives, CSS modules, and React built-ins like useState.

Java

Follow package / class / member boundaries, but avoid dragging in the full package path when the workspace has a clearer convention.

  Provides:
    - billing.InvoiceService.createInvoice @source("symbol:createInvoice")
    - api.InvoiceController.create @source("symbol:create")
    - domain.InvoiceCreated @source("symbol:InvoiceCreated")

For interfaces, name the contract, not one implementation — PaymentGateway.charge. When several implementations each matter, distinguish ownership: StripePaymentGateway.charge. Omit JDK incidentals (List, Optional, String).

.NET

Namespace / type / member handles, with framework-aware prefixes.

  Provides:
    - Billing.InvoiceService.CreateInvoice @source("symbol:CreateInvoice")
    - Api.InvoicesController.Create @source("symbol:Create")
    - Command:CreateInvoice @source("symbol:Handle")
    - Query:GetOpenInvoices
    - Job:Billing.ReconcileAccounts

MediatR-style handlers map cleanly to Command: / Query: / Handler: prefixes. Omit BCL incidentals (string, DateTime, IEnumerable, Task, LINQ) and DI wiring unless the registration is itself the contract.

SQL

Name durable data contracts. Typed prefixes are usually clearer than plain dotted paths, because SQL objects can share names across object kinds.

  Provides:
    - Table:billing.invoices @source("label:create_invoices")
    - View:analytics.monthly_revenue @source("symbol:monthly_revenue")
    - Procedure:billing.apply_payment @source("symbol:apply_payment")
    - Migration:create_invoices_table
    - Report:finance.monthly_revenue

In Consumes:, list the tables and views whose schema or semantics this unit depends on; omit individual column references, built-in functions, and internal CTE names.

COBOL

Describe stable business contracts rather than raw paragraph names. Paragraphs and sections are usually best represented as source anchors, not handles.

  Provides:
    - Decision:BillingWorkflow.rateSelection @source("label:CALC-RATE")
    - Program:BillingWorkflow.main @source("label:PROCEDURE-DIVISION")
    - Record:CustomerAccount @source("label:CUSTOMER-ACCOUNT-RECORD")
    - Copybook:CustomerAccountLayout
    - Batch:NightlyBilling.applyPayments

The handle names the decision; the anchor names where the current code implements it. In Consumes:, list copybooks, called programs, transaction boundaries, and file/record formats that are part of the contract — not every performed paragraph or working-storage field.

What Trellis deliberately leaves open

The handle rules are intentionally conservative for v0.1, and the project is honest that they will feel strict in large or legacy systems. These are known pressures, deliberately not yet solved:

  • Global-uniqueness pressure. One global handle namespace is simple and trustworthy, but big systems carry many similarly named contracts. Authors may reach for ever-longer namespaces; a project-level namespace policy is a likely future answer.
  • Drift from code symbols. A handle is contract identity, not a symbol, so it can drift from implementation names. Some drift is healthy (the contract outlives the code); too much hurts discoverability.
  • Case sensitivity. Exact case keeps resolution deterministic but is unforgiving — Subscription.create and Subscription.Create are simply different. Typos become real authoring mistakes.
  • No alias or rename history. Trellis does not yet model previous names, so intentional renames are abrupt. A mature workflow may need rename metadata that preserves one canonical identity.
  • Prefix-taxonomy abuse. Typed prefixes are useful for events and the like, but can be abused as arbitrary categories; a recognized-prefix policy may follow.
  • Dotted-identifier limits. The dotted form fits many languages but can be awkward for legacy, domain, or route-like names whose natural identifiers fall outside the grammar.

The deliberate choice for now is advisory diagnostics over implicit normalization: keep handles exact and explicit, and warn about suspicious shapes (case-only differences, accidental first-token prose) rather than silently “fixing” them. Normalization would make authoring feel forgiving at the cost of making identity — and every diagnostic built on it — harder to trust.

A handle, summarized

Good handles are:

  • stable across routine refactors
  • specific enough to be globally unique
  • familiar to the people who work in the codebase
  • aligned with real review and change boundaries
  • separate from source locations
  • consistent with language and framework convention
  • short enough to scan, precise enough to depend on

Bad handles are generic, prose-like, source-location-shaped, overfit to current symbol names, inconsistent with neighboring sidecars, or invented without first searching for an existing one.

Handles are authored semantic keys. They may be inferred from source, but they are not source facts until declared in a sidecar. Trellis becomes deterministic after that declaration.

  • The Get Started guide writes a first sidecar end to end.
  • The glossary defines Provides, Consumes, Invariants, and the rest of the vocabulary.
  • The whitepaper makes the full architectural case.
  • The Trellis repository holds the format spec and the per-language skill addendums these conventions are drawn from.