Skip to main content

Evidence sources: Git, Jira, docs, deployment

Horologium's Product Truth is only as complete as the evidence feeding it. In the target architecture, four kinds of source feed evidence into the Rust core through the same provider contract (docs/adr/0011-evidence-provider-contract-and-provenance.md):

Source classWhat it's authoritative forTrust tier
Git (vcs)Commit ancestry, tag containment, file contents at a commitsystem_of_record
Jira (tracker)Issue status, fields, and explicit ticket-key referencessystem_of_record for structured fields
Documentation (documentation)Prose describing product behaviorasserted — a docs page states something without being the authority on whether it's still true
Deployment records (deployment)What release is running on what target, over what intervalsystem_of_record

Every provider implements one shared trait (EvidenceProvider) and can only ever emit evidence — never a claim or a finding directly. That boundary is enforced by the type system, not by convention: the batch type a provider returns has no variant for anything except an immutable evidence item with a re-fetchable citation. See Integrity states and the evidence model for what happens to evidence once it's collected.

Egress control is live, not just designed: every outbound network request any provider makes goes through one shared, egress-controlled HTTP client (horologium-providers::egress::EgressClient, HORO-1397) — HTTPS-only by default, an explicit host allowlist with no default wildcard, DNS-pinned resolution (every address a lookup returns is checked against the private/link-local/reserved deny list before anything connects to it), the same check re-run in full on every redirect hop (capped at 3), a credential refused outright rather than followed across a redirect, and a response-size cap enforced by streaming rather than trusting Content-Length. Registration-time validation (see the next section) narrows what a collection run can even be pointed at; this client is what actually enforces it at fetch time. For the local-filesystem-backed Docs, Deployment, Git and Jira sources (HORO-1397 increments 1-2 and 4), an analogous confinement check applies at open time instead — a canonicalized root directory (HOROLOGIUM_SOURCE_ROOT) that every file/repository open is checked against, refusing traversal and symlink escape, since no network request happens for those sources at all. An unconfigured provider fetches or opens nothing — see Security, privacy and data boundaries for the full egress and credential model.

What's actually wired up for operators today​

Being direct about the current state matters more here than anywhere else in this documentation set. Git, Jira, docs, and deployment providers exist as Rust implementations and are exercised end-to-end in this repository's test suite (crates/horologium-providers, crates/horologium-providers-conformance) against the same golden fixture the hosted demo runs. As of HORO-1397, an operator-facing collection entrypoint also exists for real: POST /v1/jobs {"job_kind": "collect"} (or collect_and_reconcile, run by the horologium-runner binary) drives real collection over every registered, enabled source — local Git/docs/deployment/Jira sources by reading real files under HOROLOGIUM_SOURCE_ROOT, and a live Jira project over the egress-controlled HTTP client described above. What does not exist yet in this Public Alpha slice is a live/network Git, docs, or deployment source (only Jira has a live/network implementation so far), or a .env/config-file surface simpler than registering a source through the API.

In concrete terms, as a self-host operator today you can:

  • Register a real local Git/docs/deployment/Jira source, or a live Jira project, and run a real collect/collect_and_reconcile job against it — or seed the golden fixture (seed_golden_fixture, see the self-host quickstart) for a populated instance with no source of your own.
  • Read the full evidence/claim/finding model this repository implements, and verify it against docs/adr/0011-evidence-provider-contract-and-provenance.md and the ontology document, both in the repository.

Registering a source (POST /v1/sources, ADR-0018) is real, live operator-facing functionality today: it validates the source's config/ declared_scope against a typed per-provider schema, checks every URL field against this deployment's HOROLOGIUM_EGRESS_ALLOWED_HOSTS allowlist and refuses private/loopback/link-local IP literals, refuses path traversal in declared_scope, and stores credential_ref (the name of an env var or file path, never the value) without ever resolving it. A Git, Docs, Deployment or Jira source may instead set local_path (HORO-1397): a root-relative filesystem path under this deployment's HOROLOGIUM_SOURCE_ROOT, used at collection time instead of the URL field. When local_path is set, the URL field is still shape-checked (absolute, https) but is not checked against the egress allowlist or refused for naming a private IP — nothing is ever fetched over the network for a local-path source, so it keeps its meaning purely as citation identity. local_path itself is checked for traversal and rejected if it is not root-relative. For Jira specifically, local_path names a directory of Jira-issue-shaped JSON exports (the same trimmed Jira Cloud REST API v3 shape the golden fixture uses) — this exists so a tracker-class source can be exercised end-to-end without a live Jira instance. A Jira source with no local_path is a real, live/network source (HORO-1397 increment 5): credential_ref must be set, and collection calls the configured Jira site over the shared egress-controlled HTTP client. A live/network Git, Docs, or Deployment source (one with no local_path) is still scoped ahead — only Jira has a live implementation so far. GET /v1/sources reports each registered source's origin ("local" or "live") so imported and live evidence are never conflated.

A real collect/collect_and_reconcile job (POST /v1/jobs, run by the horologium-runner binary) now does more than insert evidence rows: after collecting, it normalizes each successfully-collected source's evidence into canonical entities, and — for Jira's tracker.status field — an observed claim, inside the same transaction that runs reconciliation and publishes the snapshot (HORO-1397). Editing a registered local source's underlying file(s) and re-running the job produces new evidence, updated entities/relationships, and (for Jira) a changed claim — visible through GET /v1/entities, GET /v1/entities/{id}/claims and GET /v1/findings. A source that fails to collect on a given run never has its prior evidence or claims deleted; only claims from a source that did collect successfully this run are ever superseded.

Select Markdown sections for local Docs collection​

A local Docs source can declare the exact level-two headings it watches:

{
"source_key": "product-handbook",
"provider_id": "docs",
"display_name": "Product handbook",
"config": {
"base_url": "https://example.invalid/handbook",
"local_path": "handbook"
},
"declared_scope": {
"paths": ["/product.md"],
"watched_sections": [{"path": "/product.md", "heading": "Product Profile"}]
},
"reality_layers": ["documented"]
}

Submit this body through the existing authenticated POST /v1/sources route, then run an ordinary collection job. The selected file is beneath HOROLOGIUM_SOURCE_ROOT/handbook; the URL remains its citation identity. The selector path must exactly match a declared path. Heading matching is case-sensitive and requires one complete ## Product Profile line. The page also needs parseable YAML front matter with last_updated (for example, last_updated: 2026-10-06T00:00:00Z) to retain its evidence timestamp; repeated headings are ambiguous and produce no section evidence. The collector extracts the opening sentence mechanically, retaining its original byte range and file revision. It does not infer a capability or validate what the sentence asserts.

Multiple headings on one file are allowed; use a separate registered source for another file so each citation retains its page identity. Omitted or empty watched_sections preserves the existing behavior and emits no section evidence. Duplicate selectors, paths outside the declared scope, malformed headings and more than 100 selectors are rejected. Existing 2,048-byte string and 8 KiB scope limits still apply. Empty sections remain empty; collection rechecks confinement and refuses a symlink escape even after registration. If every source fails, the job records the failures and publishes no snapshot.

Credential handling, as designed​

Whenever provider configuration does land, the documented shape is already fixed by ADR-0015 and worth knowing in advance:

  • Configuration stores a credential_ref — the name of an environment variable or a file path — never a credential value. The core resolves it at process start.
  • Credentials are never logged, never included in an error message, never returned by any endpoint, and never written to evidence.
  • Least privilege is the documented requirement: read-only tokens, scoped to the specific repository, project, or docs space you're connecting — never a broad admin token.

See Security, privacy and data boundaries for the complete model.