Evidence-backed scope/CR triage for PMO teams — fusing contract scope, a code knowledge graph and historical effort into auditable decisions.
Project description
Etki
The only open-source tool that answers "is this in scope?" with contract evidence, code impact and historical effort — support for analysts, leverage in client negotiations.
⚠️ Alpha. APIs, schema and screens may change. Not for production; intended for pilot/evaluation.
Etki is a decision-support application for software teams — analysts, developers and the PMO alike — that answers a single question for every incoming client request: Is this in-scope, out-of-scope, or a Change Request (CR); what is the effort; which code and which contract clause does it touch?
Its defensible value is producing a live, evidence-backed recommendation from the fusion of three sources — it doesn't leave the analyst alone, and it strengthens your hand with evidence in client negotiations:
- Contract scope — clauses extracted into structured
ScopeItems (including the explicitly excluded ones). - Code knowledge graph — modules, dependencies, complexity, churn — for impact analysis and effort.
- Historical effort — real time logged on similar past requests, used by analogy.
Every decision carries an auditable evidence chain (clauses checked, best match, impacted modules, reasoning, confidence, model/version and index-freshness stamp) — it can be reconstructed for a contractual dispute. Copilot, not autopilot: the system recommends, a human makes the final call.
Try it in 5 minutes — no API key
One command starts a fully seeded English demo (sample contract + code repo + past work items). No LLM key, no JVM, no Postgres — the decision path is fully deterministic:
docker compose -f docker-compose.demo.yml up --build
Open http://localhost:8000, log in as demo / demo (hard-coded for local evaluation only), open the Meridian CRM (demo) project → Triage, and try:
- "We need SAML single sign-on with our corporate identity provider" → out of scope, citing the explicit exclusion (Clause 7.1), with the effort such a CR would take — estimated from similar past tickets.
- "Add a date filter to the monthly standard report" → in scope (Clause 4.2.1), impacted modules, and an effort range by analogy.
Every answer shows its full evidence chain: the contract clauses checked, the best match and score, the impacted code modules, and the index-freshness stamp.
What it does
- Triage — enter a request → in-scope / out-of-scope / CR / gray-area / maintenance decision + an effort range (single-point estimates forbidden) + risk + impacted-module cards.
- Automatic pre-analysis — after triage, a developer-oriented technical pre-analysis is generated automatically (LLM if available, otherwise deterministically from the evidence chain) and saved to the case; it can be edited and enriched via chat.
- Approval & living baseline — the reviewer approves / rejects / converts to CR; an approved CR bumps the baseline version by +1. A baseline timeline shows which CR added which clause, when; the history screen filters by decision type, and approved cases live there as analyses.
- Ask (Sor) — a single-input question box over the project's knowledge graph: an instant deterministic answer (scope clauses, modules, dependencies — source-labeled) and, if an LLM is configured, an AI answer grounded in that deterministic result. Every question and answer is appended to the project's process log.
- Flow map (Sankey) — Request → Requirement (scope clause) → Code module interaction; interactive, with node detail. Each triage also has its own flow graph.
- Explorable index — a per-clause detail screen (rulings, memory and pool status per scope item), a per-module code-graph table (repo-scoped), a dependency card with version-compare / OSV lookup, and an index-run history with freshness badges.
- Document management — upload Word/Excel/PDF/CSV (→ text + scope clauses), preview documents inline; attach code repos (git URL / local) and work-item trackers (Jira/GitLab/Redmine/Azure DevOps/Linear/GLPI/file).
- Reports — KPI/scorecard on the project summary (over-reliance rate, agreement, effort pool with per-item consumption breakdown, precedent & disputed-clause counters); every KPI tile links to the list that answers "which ones?", and a client-ready
.docxreport per case. - Decision memory ("decision memory as code") — every triage decision is auto-projected to a per-project, git-versionable markdown wiki (
decisions/, entity backlinks, generated index). Human overrides are promoted to precedents, conflicting rulings on the same clause surface in a disputed page, and aGraphQueryPortretrieves over scope clauses + code modules + past work items (top-k / graph expand / guarded NL query). The wiki is always a projection of the database — regenerable with one command, never a second source of truth. - Multilingual UI (TR/EN/DE) — switch language from the top-right; menus/labels and LLM output follow the chosen language.
- Per-project LLM profile — assign each project an output language (any language) + a selectable domain/skill profile (
config/domains/*.md: integration, enterprise, e-commerce, healthcare…) or free-text instructions; LLM prompts are enriched per project. An optional pivot translation (translate input → working language → reason → translate back) can be enabled per project. - Settings screen (pmo) — the global LLM provider (off / Anthropic / OpenAI-compatible) is configured from the UI with a one-click connection test, and users are managed from the UI too: create/delete, roles, per-user project grants, password reset.
Multi-project, multilingual UI, session/login + role-based authorization: approval = pmo, engineer runs triage/analysis, viewer is read-only; project access via per-user grants. Sessions are hardened (login rate limiting, remember-me lifetimes, password reset drops live sessions). Decisions surface effort ranges only (monetary cost was removed).
How matching works today (honestly): the request↔contract matching is layered, and every layer is measured on a public benchmark (see EtkiBench below):
- Deterministic lexical matching (always on) — tokenization with a Turkish↔English domain-term bridge, prefix-based inflection tolerance, bilingual stopword/boilerplate filtering and a symmetric-normalized overlap score. Fully reproducible; scores 62% on the benchmark.
- Deterministic semantic evidence (optional, local embeddings via Ollama/vLLM) — an
EmbeddingProviderport. We measured that cosine similarity cannot tell "a paraphrase of a clause" from "a new capability near a clause" (0.629 vs 0.630 on the benchmark), so by design the embedding layer only routes clear exclusion matches and otherwise adds an informational "semantically nearest clause" note — it never fabricates an in-scope call. - Guarded LLM assist (optional, off by default) — consulted only when the deterministic match is weak; strength-gated, whitelist-validated, anti-hallucination prompt rules. This is where the paraphrase judgment lives: it lifts the benchmark score to 88% with a local 20B model (94% together with the cross-encoder reranker lane) — and to be blunt, an unguarded small model made results worse, which is exactly why the guards exist.
How it compares
Software-engineering-intelligence platforms measure delivery; estimation plugins guess effort. Neither can defend a scope decision against a contract. That fusion is Etki's whole job:
| Etki | SEI platforms (LinearB, Jellyfish…) | Jira estimation plugins | |
|---|---|---|---|
| Core question | "Is this request in scope, and what effort would it take?" | "How is delivery performing?" | "How many points/hours?" |
| Contract scope as data (incl. excluded clauses) | ✅ first-class | ❌ | ❌ |
| Code knowledge graph for impact analysis | ✅ Joern CPG / Python AST | partial (repo metrics) | ❌ |
| Per-decision evidence chain (clauses, match score, modules, model + index stamp) | ✅ reconstructable for disputes | ❌ | ❌ |
| Estimates | ranges only (three-point/PERT) | n/a | typically single points |
| Human-in-the-loop | built in — a human decides, overrides are tracked | n/a | n/a |
| Deployment | self-hosted, air-gapped capable | SaaS | marketplace app |
| License | Apache-2.0 | commercial | commercial |
EtkiBench — a public benchmark for scope triage
Nothing else measures the scope decision itself, so we ship the benchmark: 66 stratified cases (paraphrases, adversarial exclusions, limit/quota and effort-pool CRs, cross-lingual TR↔EN) anchored to the bundled corpus, every label argued from the contract with a clause-citing rationale. One command reproduces any row; models are scored in the production configuration (assist-on-weak-match, whitelist-validated).
| Mode (2026-07, English prompts) | Agreement |
|---|---|
| gpt-oss:20b + cross-encoder reranker (local) | 94% |
| gpt-oss:20b / gemma3:27b (local, Ollama) | 88% |
| gpt-oss:120b (local, Ollama) | 85% |
| Deterministic + reranker (no LLM at all) | 80% |
| Deterministic (no assists) | 68% |
The answer keys are protected by a CI freeze guard (engine changes and dataset
edits cannot land together), and improvements are validated on pre-registered,
one-shot held-out sets — the methodology, the full scoreboard, and the honest
negative findings live in
eval/datasets/etkibench/.
Quick start
uv sync --dev # venv + dependencies (editable install)
cp .env.example .env # fill in settings (LLM optional)
uv run python -m etki.persistence create-user # first admin user (or ETKI_ADMIN_*)
uv run uvicorn etki.api.app:app --reload # http://localhost:8000 (API docs: /docs)
Only the first admin user needs the CLI/env bootstrap — after logging in, manage users (roles, project grants, password resets) from Settings → Users in the UI.
The LLM is optional — with no API key the system runs deterministically/heuristically.
The easiest way to enable it is Settings → AI Assistant in the UI (pmo-only): pick the
provider, paste the key/endpoint, hit Test connection. Values saved there live in
.etki/llm.json (owner-readable only, git-ignored) and take precedence over env vars.
For production prefer .env:
ETKI_LLM_PROVIDER=anthropic
ETKI_ANTHROPIC_API_KEY=sk-ant-... # Anthropic Claude API key
# ETKI_DEFAULT_LANGUAGE=tr # default UI language (tr|en|de); overridden by session/Accept-Language
Fully air-gapped mode
The data Etki handles — client contracts, code, effort history — is exactly the kind that can't leave your network. Every layer has a local option, including the LLM (one OpenAI-compatible adapter covers Ollama, vLLM, LM Studio, llama.cpp server):
ETKI_LLM_BASE_URL=http://localhost:11434/v1 # Ollama endpoint (enables the local provider)
ETKI_LLM_MODEL=qwen2.5:3b # any model your server hosts
ETKI_EMBED_BASE_URL=http://localhost:11434/v1 # optional: local semantic evidence
ETKI_EMBED_MODEL=qwen3-embedding:0.6b # (deterministic per model — reproducible)
Local LLM + ast code engine + SQLite = zero external dependencies (the UI vendors its own assets — no CDN). And with no LLM configured at all, triage is fully deterministic and reproducible.
Common commands
uv run pytest # all tests (no Joern/JVM needed; uses fakes/AST)
uv run ruff check . && uv run mypy etki # lint + type check
uv run python -m eval.runner # CI gate: retrieval + decision-agreement backtest
uv run python -m eval.runner --dataset my.json # benchmark YOUR labeled cases (add --llm to score a model)
uv run python -m etki.indexing [project_id] # rebuild the index (live Joern; AST alternative)
uv run python -m etki.mcp_server # MCP server: triage_request + 4 index tools —
# ask Claude "is this in scope?" (see docs/MCP.md)
docker compose up -d --build # app + Postgres (ast engine → JVM-free container)
Architecture (the load-bearing decisions)
- Hexagonal (ports & adapters) — the core is vendor-agnostic. Three abstract ports:
WorkItemProvider,CodeRepositoryProvider,DocumentSourceProvider. Adding a new system = writing a new adapter (GLPI/Jira/GitHub/FileSystem/SharePoint…). Which adapter is active is configuration, not code. - Two cadences — Indexing is offline/periodic (code → graph, contract → baseline); Triage is online and answers in seconds by querying the pre-built index.
- EXCLUDED scope is first-class —
ScopeItem.polarity = INCLUDED | EXCLUDED. A match against an excluded clause is the highest-confidence "out-of-scope". - Two-evidence rule — a decision rests on both (a) text similarity (request ↔ contract) and (b) code reality; conflict → gray area → escalate to PMO.
- Estimates are always ranges (cone of uncertainty); three-point/PERT.
flowchart LR
subgraph vendors ["Adapters — selected by config, never code"]
direction TB
A1["Jira / GLPI / file export"]
A2["Joern CPG / Python AST / git churn"]
A3["Filesystem / docx·xlsx·pdf upload"]
A4["Claude API / Ollama / vLLM"]
end
A1 --> P1(["WorkItemProvider"])
A2 --> P2(["CodeRepositoryProvider"])
A3 --> P3(["DocumentSourceProvider"])
A4 -.-> P4(["LLMClient — optional"])
subgraph core ["Vendor-agnostic core"]
direction LR
IDX["Indexing engine<br/>(offline / periodic)"] --> IX[("index.json<br/>baseline + code graph")]
IX --> TRI["Triage engine<br/>(online / seconds)"]
TRI --> HITL["HITL approval<br/>living baseline v+1"]
end
P1 --> core
P2 --> core
P3 --> core
P4 -.-> TRI
The LLM is abstracted behind a single LLMClient port with two providers: the Anthropic Claude API (anthropic SDK; default model claude-opus-4-8) or any OpenAI-compatible endpoint (Ollama, vLLM — see air-gapped mode above). The LLM is optional — with no key the system runs deterministically/heuristically. The code graph is produced by Joern (CPG) or the dependency-free Python ast path (same normalized schema). Persistence is SQLite (default/test) or Postgres (Docker).
etki/
core/ domain models (Pydantic) + ports (Protocol) — vendor-agnostic
adapters/ registry (config→adapter) + fakes + filesystem/jira/glpi/joern/ast/git/llm…
extraction/ scope_extractor (contract → ScopeItem[]) + parsers (docx/xlsx/pdf → text)
indexing/ IndexingEngine + scope↔code mapping + save/load
engine/ triage (decision tree) + understanding + estimation (PERT)
hitl/ ApprovalService (approve/reject/CR + living baseline) + ingest (feedback → precedents/disputed)
persistence/ CaseFileRepository port (SQL / in-memory)
wiki/ decision-wiki CLI (search/show/rebuild) — the wiki is a DB projection
i18n/ multilingual catalog (tr/en/de) + t()/resolve_locale
api/ app (JSON API) + web (HTMX UI) + context + security (login/RBAC) + templates/
kpi.py · graphquery.py · mcp_server.py · agent.py · projects_store.py · config.py
domains.py · llm_profile.py # per-project domain profile + LLM prompt prefix
config/ (connectors + domains/*.md) · samples/ · docs/ · tests/ · eval/ · Dockerfile · docker-compose.yml
The two design documents are the source of truth for the full vision:
Etki_Mimari_Dokuman.md (architecture) and
Etki_Gelistirme_Plani.md (development plan).
Operations/compliance: docs/RUNBOOK.md · docs/KVKK.md (Turkish — KDPL/KVKK compliance notes) · docs/MCP.md (use Etki from Claude via MCP) · docs/writing-an-adapter.md (add your tracker/repo/doc source).
Status
Phases 0–4 are implemented (walking skeleton → data backbone → decision brain → HITL/audit/UI → pilot), plus a hardening pass (baseline rehydration from the DB, per-project access isolation, prompt-injection guards, a frozen 66-case golden eval set with Wilson intervals) and a benchmark-driven engine program (EtkiBench + four measured optimization rounds: guarded LLM assist v2, English maintenance routing, bilingual stopword completion, and an EmbeddingProvider port whose limits are measured and documented — exclusion routing only, by evidence; plus a cross-encoder reranker evidence layer, TEI-compatible and off by default). On top of that sits the GraphRAG decision-memory layer (all four phases implemented): decision wiki as a DB projection, GraphQueryPort retrieval, HITL feedback → precedents/disputed, and rerank-packed context expansion (the live bge-reranker A/B ran: packing stays BFS — the reranker earns its keep on the matching lane instead, see the scoreboard). Not yet done: a real customer pilot on live data; estimation-constant calibration from real pilot data (the constants are config-driven with a suggestion loop, but they have not been validated against real closed work items); ML-regression estimation; multi-customer DB isolation; full OAuth/SSO; a customer-facing portal.
License
Apache-2.0 © 2026 Etki contributors.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file etki-0.1.0a2.tar.gz.
File metadata
- Download URL: etki-0.1.0a2.tar.gz
- Upload date:
- Size: 1.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cc34cbc2876029111b09599badbc6f2417458f561dca35b1a74569e0bba22210
|
|
| MD5 |
d2870586385d0784b1f90d9123d8e512
|
|
| BLAKE2b-256 |
3eff50caf0d82b654d9fd220f2f274699329dbaa0cb1ab3e18eafd7fc6ce5119
|
Provenance
The following attestation bundles were made for etki-0.1.0a2.tar.gz:
Publisher:
release.yml on yasinyaman/etki
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
etki-0.1.0a2.tar.gz -
Subject digest:
cc34cbc2876029111b09599badbc6f2417458f561dca35b1a74569e0bba22210 - Sigstore transparency entry: 2173057222
- Sigstore integration time:
-
Permalink:
yasinyaman/etki@18ceef2aea78838b69ff63687ed47ad6d752c422 -
Branch / Tag:
refs/tags/v0.1.0a2 - Owner: https://github.com/yasinyaman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@18ceef2aea78838b69ff63687ed47ad6d752c422 -
Trigger Event:
push
-
Statement type:
File details
Details for the file etki-0.1.0a2-py3-none-any.whl.
File metadata
- Download URL: etki-0.1.0a2-py3-none-any.whl
- Upload date:
- Size: 324.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a72ee2c683d516c8a85c64c8b37f2f994443ce2dedae8790a90b9bd7ed694f93
|
|
| MD5 |
93977033eaefbdbeae224e7455e134a4
|
|
| BLAKE2b-256 |
79b8113961e299285c82b2470f7a850da3dab80915558d27c1fed092ff7e430d
|
Provenance
The following attestation bundles were made for etki-0.1.0a2-py3-none-any.whl:
Publisher:
release.yml on yasinyaman/etki
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
etki-0.1.0a2-py3-none-any.whl -
Subject digest:
a72ee2c683d516c8a85c64c8b37f2f994443ce2dedae8790a90b9bd7ed694f93 - Sigstore transparency entry: 2173057231
- Sigstore integration time:
-
Permalink:
yasinyaman/etki@18ceef2aea78838b69ff63687ed47ad6d752c422 -
Branch / Tag:
refs/tags/v0.1.0a2 - Owner: https://github.com/yasinyaman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@18ceef2aea78838b69ff63687ed47ad6d752c422 -
Trigger Event:
push
-
Statement type: