tatara-memory-repo-ingester¶
Stateless Go batch tool that walks a git repository, runs language-aware static analysis, and pushes a deterministic code graph plus enriched semantic chunks to tatara-memory. It is the bulk knowledge loader that seeds the platform's persistent code graph before any agent works on a repository.
Repository: github.com/szymonrychu/tatara-memory-repo-ingester
What it does¶
The ingester is a short-lived binary (tatara-ingest) with no persistent state of its own. Each run:
- Computes the set of files to process - either a
git diffwindow (incremental) or the full tracked file list (git ls-filesfor first or forced full runs). - Routes each file to the first matching language analyzer in a fixed precedence order.
- Runs all matched analyzers in sequence, each producing entities, directed edges, and semantic text chunks.
- Pushes the aggregate code graph to
tatara-memoryviaPOST /code-graph:bulk. - Pushes semantic chunks to
tatara-memoryviaPOST /memories:bulk, with per-file reconciliation so stale chunks for deleted or renamed files are purged. - Optionally runs a best-effort LLM semantic extraction stage (Phase 2) that enriches the graph with concept and rationale nodes for files the server's content-SHA cache marks as unseen.
The ingester is fail-closed on hard analyzer errors: if any analyzer fails for its entire file batch (as opposed to a single-file soft parse error), the run aborts before pushing anything and exits non-zero so the operator retries the same commit window without permanently losing graph coverage.
flowchart LR
subgraph Source
R[(git repo)]
end
subgraph tatara-ingest
W[walk / diff] --> G[group by analyzer]
G --> A1[Go AST\ngo/packages]
G --> A2[Python\ntree-sitter]
G --> A3[JavaScript\ntree-sitter]
G --> A4[Terraform\nHCL parser]
G --> A5[Helm\ntemplate parser]
G --> A6[Docs\nmarkdown chunker]
A1 & A2 & A3 & A4 & A5 & A6 --> AGG[aggregate\nentities / edges / chunks]
AGG --> SEM[semantic stage\nLLM - optional]
end
subgraph tatara-memory
CG[(code graph\nLightRAG)]
MEM[(semantic chunks\nvector store)]
end
R --> W
AGG --> CG
AGG --> MEM
SEM --> CG Language analyzers¶
The analyzer registry has a fixed precedence order. Each file is claimed by the first matching analyzer; files matching none are silently skipped. Helm is registered before Docs so chart YAML is not consumed by the markdown/text path.
| Analyzer | File patterns | Engine | Emits |
|---|---|---|---|
| Go | *.go (non-test by default) | go/packages + go/types | packages, types, funcs, methods; import and call edges with typed confidence scores |
| Python | *.py | tree-sitter | modules, classes, functions; import edges |
| JavaScript | *.js, *.mjs, *.cjs | tree-sitter | modules, classes, functions; import edges |
| Terraform | *.tf | HashiCorp HCL parser | resources, data sources, modules, variables, outputs; module-source and var-ref edges |
| Helm | Chart.yaml, values.yaml, templates/** in charts | Go text/template parser | chart, template, and value entities; contains and value-ref edges |
| Docs | *.md, *.markdown, *.txt, *.rst | plain text + frontmatter | one doc_file entity per file; semantic chunk with optional YAML frontmatter provenance (source_url, author, captured_at) |
Adding a language
The analyzer interface is three methods (Name, Match, Analyze). Adding a language is one new file under internal/analyze/ plus one Register call in registry.go. No other code changes.
Go analyzer - confidence scoring¶
The Go analyzer uses go/packages to perform full type resolution. Call edges carry a numeric confidence score and a tier:
| Resolution level | Score | Tier |
|---|---|---|
| type_resolved | 0.98 | INFERRED |
| scoped_name_match | 0.85 | INFERRED |
| imported_name_match | 0.70 | INFERRED |
| global_name_match | 0.45 | INFERRED |
| ambiguous_multi_def | 0.20 | AMBIGUOUS |
| unresolved | 0.00 | AMBIGUOUS |
TierForScore assigns EXTRACTED only for a score >= 1.0, INFERRED for (0.3, 1.0), and AMBIGUOUS for <= 0.3. The highest-confidence Go call edge (type_resolved, 0.98) still lands in the INFERRED band, so in practice no Go call edge reaches the EXTRACTED tier - that tier is reserved for score-1.0 sources such as a SCIP index. Filter on ?tier=INFERRED (not EXTRACTED) to pick up type-resolved Go calls.
Cross-repo symbol references (packages under a configurable crossRepoPrefix, defaulting to github.com/szymonrychu/) are emitted as SymbolRow records so tatara-memory can link entities across repository boundaries.
Two ingest modes¶
The standard path. The ingester clones (or is given) the repository, computes a file diff, and drives all language analyzers.
tatara-ingest \
--repo-root /path/to/repo \
--repo-name my-service \ # optional; defaults to basename of repo-root
--since <base-commit> \ # incremental: diff since this SHA
--base-url http://tatara-memory:8080
Flags also accept environment variables (REPO_ROOT, REPO_NAME, BASE_URL). Flags take precedence over env.
| Flag | Env | Default | Description |
|---|---|---|---|
--repo-root | REPO_ROOT | - | Path to the git repository root (required) |
--repo-name | REPO_NAME | basename(repo-root) | Logical name written into graph entities |
--since | - | (full ingest) | Base commit SHA for incremental diff |
--full | - | false | Force full re-ingest even when --since is set |
--base-url | BASE_URL | - | tatara-memory base URL |
Output: entities + edges (code graph) and semantic chunks (memories). Both are pushed in the same run.
For repositories where a pre-generated SCIP index (index.scip) is available - typically produced by a language-specific indexer in CI - the ingester can bypass the source walker entirely and consume the protobuf index directly.
tatara-ingest \
--scip /path/to/index.scip \
--scip-repo my-service \ # required
--base-url http://tatara-memory:8080
| Flag | Description |
|---|---|
--scip | Path to the pre-generated SCIP index protobuf |
--scip-repo | Logical repo name (required when --scip is set) |
SCIP mode - code graph only
SCIP ingest pushes entities and edges to the code graph (extractor=scip) but does not produce semantic text chunks. There is no memory (vector store) population in this mode. This is a v1 limitation; semantic chunks from SCIP-indexed repos are planned for a later phase.
SCIP ingest uses the same POST /code-graph:bulk endpoint as walk-based ingest, tagged with extractor=scip so the server's reconcile logic treats the two origins independently.
Incremental re-ingest¶
The ingester does not maintain local state between runs. Incrementality is driven entirely by git:
- The operator records
status.lastIngestedCommiton theRepositoryCR after each successful ingest Job. - On the next ingest trigger the operator passes
--since <lastIngestedCommit>to the Job. - The ingester runs
git diff -M -z --name-status <since>..HEADto produce the change set. - If
<since>is no longer in history (force-push, rebase, GC) the diff command fails; the ingester logs a WARN and falls back to a fullgit ls-filespass automatically. No manual intervention is needed.
Reconcile-per-file (idempotency)¶
For each ingest run the ingester sends a reconcile_files list alongside the chunk push. The tatara-memory server purges prior chunks for every file in that list before inserting the new ones. This means:
- Deleted files: chunks are removed from the vector store.
- Renamed files: old-path chunks are purged; new-path chunks are inserted.
- Re-analyzed files: stale chunks are replaced, not duplicated.
Files where an analyzer produced a per-file soft parse error are excluded from reconcile_files so existing chunks for those files survive until the file is parseable again.
A full re-ingest (--full or first ingest) is insert-only - no reconcile list is sent - so it is safe to run over an already-populated store without a destructive purge pass.
Operator-managed Jobs¶
The tatara operator manages ingest as short-lived Kubernetes Job resources. Operators do not invoke tatara-ingest directly.
Job lifecycle¶
sequenceDiagram
participant SCM as SCM (GitHub/GitLab)
participant OP as tatara-operator
participant JOB as ingest Job (clone + ingest containers)
participant MEM as tatara-memory
SCM->>OP: push webhook
OP->>OP: read Repository.status.lastIngestedCommit
OP->>JOB: create Job (--since <last-sha>)
JOB->>JOB: init: git clone (SCM token in init container)
JOB->>MEM: POST /code-graph:bulk
JOB->>MEM: POST /memories:bulk
JOB->>OP: kubectl patch ConfigMap <repo>-ingest-result sha=<HEAD>
OP->>OP: update status.lastIngestedCommit, phase=Ingested Job structure¶
Each Job has two containers sharing an emptyDir workspace volume:
- init container (
clone): runsgit cloneusing the project's SCM token (sourced viaSecretKeyReffromspec.scmSecretRef). The token is never embedded in the Job spec stored in etcd. - main container (
ingest): runstatara-ingest --repo-root ... --repo-name ... [--since ...], then patches the result ConfigMap with the cloned HEAD SHA.
# Abridged Job spec produced by the operator
apiVersion: batch/v1
kind: Job
metadata:
name: my-service-ingest-x7k2m
labels:
tatara.dev/repository: my-service
tatara.dev/ingest-mode: incremental # or: full
spec:
backoffLimit: 0 # incremental: 0 (fail fast, let operator escalate to full)
ttlSecondsAfterFinished: 600
template:
spec:
restartPolicy: Never
serviceAccountName: tatara-ingest
initContainers:
- name: clone
image: <ingesterImage>
command: ["/bin/sh", "-c"]
args:
- git clone --branch "$GIT_BRANCH" "$GIT_CLONE_URL" "$GIT_REPO_DIR"
env:
- name: SCM_TOKEN
valueFrom:
secretKeyRef:
name: <project.spec.scmSecretRef>
key: token
containers:
- name: ingest
image: <ingesterImage>
env:
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: <oidcSecretName>
key: OPERATOR_OIDC_CLIENT_SECRET
- name: HTTP_TIMEOUT
value: "300s"
- name: SEMANTIC_INGEST
value: "true" # mirrors Repository.spec.semanticIngest
Retry and backoff¶
| Mode | backoffLimit | Rationale |
|---|---|---|
| Incremental | 0 | A missing --since SHA is deterministic; one attempt is enough before the operator escalates to a full ingest. |
| Full | 2 | Transient clone or network failures can self-heal across retries. |
The operator applies exponential backoff between failed Job recreations using status.ingestFailureCount and status.lastIngestFailureTime on the Repository CR.
Repository CR - ingest-relevant fields¶
apiVersion: tatara.dev/v1alpha1
kind: Repository
metadata:
name: my-service
spec:
projectRef: my-project
url: https://github.com/myorg/my-service
defaultBranch: main
ingestEnabled: true # default: true; set false to suspend all ingest
semanticIngest: true # default: true; set false for AST-only (no LLM cost)
reingestSchedule: "0 6 * * *" # cron: daily catch-up ingest at 06:00 UTC
status:
phase: Ingested # Ingesting | Ingested | Failed
lastIngestedCommit: "a1b2c3d"
lastIngestTime: "2026-06-27T06:00:00Z"
ingestFailureCount: 0
jobName: "my-service-ingest-x7k2m"
reingestSchedule is required. It fires a periodic full-history catch-up in addition to push-webhook-triggered incremental ingests, guarding against missed events or webhook delivery failures.
Authentication¶
All HTTP calls to tatara-memory are authenticated with an OIDC client-credentials bearer token. The ingester fetches a short-lived token from the issuer's token endpoint before each run and attaches it to every request. Configuration is via environment variables:
| Env var | Description |
|---|---|
OIDC_ISSUER | Token endpoint base URL (e.g. https://keycloak.example.com/realms/tatara) |
OIDC_CLIENT_ID | Client ID for the ingester service account |
OIDC_CLIENT_SECRET | Client secret (sourced from a Kubernetes Secret in operator-managed Jobs) |
OIDC_AUDIENCE | Expected audience claim on the token |
When OIDC_CLIENT_ID is unset, the ingester makes unauthenticated requests. This is acceptable in local development but should not be used in production.
Observability¶
The ingester is a short-lived Job and cannot be scraped by Prometheus during its run. It pushes gathered metrics to the operator's /internal/metrics/push receiver at Job completion. The operator's tatara.dev/ingest-mode label on the Job distinguishes incremental from full runs for alert routing.
Key metrics emitted:
| Metric | Type | Description |
|---|---|---|
ingest_runs_total | Counter | Total ingest runs started |
ingest_run_result_total{result} | Counter | Terminal results: success or failure |
ingest_stage_duration_seconds{stage} | Histogram | Per-stage latency: push_graph, push_chunks, scip, total |
analyzer_entities_total{analyzer} | Counter | Entities emitted per analyzer |
analyzer_edges_total{analyzer} | Counter | Edges emitted per analyzer |
analyzer_duration_seconds{analyzer} | Histogram | Per-analyzer wall time |
analyzer_parse_errors_total{analyzer} | Counter | Per-file soft parse errors |
push_requests_total{endpoint,result} | Counter | HTTP push requests by endpoint and outcome |
semantic_misses_total | Counter | Files sent to LLM extraction (cache misses) |
llm_calls_total{result} | Counter | LLM completion calls: ok or fail |