Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
name: Bug report
about: Something isn't working as expected
title: ''
labels: bug
assignees: ''
---

**What happened**
A clear description of the bug.

**What you expected**
What you expected to happen instead.

**Steps to reproduce**
1.
2.
3.

**Environment**
- telos version: <!-- `pip show telos` or the git commit -->
- Python version: <!-- `python --version` -->
- OS: <!-- macOS / Linux / Windows + version -->
- Tax year / form(s) involved: <!-- e.g. TY2025, Form 1040 + Schedule D -->

**Logs / output**
```
paste any error output or traceback here — never paste real tax data, SSNs,
account numbers, or any other PII
```

**Does it reproduce with a synthetic fixture (not your own tax data)?**
<!-- These get prioritized. Please don't paste any real documents or PII. -->
21 changes: 21 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
name: Feature request
about: Suggest an idea or enhancement
title: ''
labels: enhancement
assignees: ''
---

**The problem**
What are you trying to do that Telos doesn't support today?

**Proposed solution**
What you'd like to see.

**Alternatives considered**
Other approaches you've thought about.

**Scope note**
Telos is a deterministic engine: every computed line must be traceable to its inputs and
a primary-source citation. New forms/line items need the citation up front, not derived
from memory. See `CONTRIBUTING.md` for the coverage-guard contract new forms must satisfy.
19 changes: 19 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## What & why

<!-- What does this change and why? Link any related issue. -->

## Checklist

- [ ] Tests added/updated for the behavior change
- [ ] `pytest` passes locally — the coverage floor in `pyproject.toml` is a ratchet, raised as coverage improves and never lowered to make a change pass
- [ ] `ruff check .` is clean for files I touched
- [ ] No secrets, real tax documents, or real PII committed (synthetic fixtures only)
- [ ] The coverage guard (`telos.engine.guard`) still declares every form/line item this change touches — no silent bypass

## Test plan

<!-- How you verified this works. -->

---

Prepared by: <model-name> via [Claude Code](https://claude.com/claude-code)
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ jobs:

test:
runs-on: ubuntu-latest
# The badge-publish step below writes `coverage.json` on the orphan
# `badges` branch through the contents API. Nothing else in this job
# writes, and the token is scoped no wider than that.
permissions:
contents: write
strategy:
fail-fast: false
matrix:
Expand All @@ -38,6 +43,22 @@ jobs:
- run: pip install -e ".[dev]"
- run: pytest --cov --cov-report=term-missing

# repository-baseline-policy.md §5.1 — a badge whose value is written by
# a human is forbidden: it renders identically to a real one and becomes
# false the moment reality moves without anyone editing a file. This
# repo carried no coverage badge at all. The README badge now renders
# whatever this step last published, derived from the same `.coverage`
# file the pyproject.toml gate reads.
#
# One leg only, pushes to main only: a matrix run on every leg would
# race to publish, and a pull-request run measures a tree that is not
# yet what a reader of the README is looking at.
- name: Publish coverage badge
if: matrix.python-version == '3.13' && github.event_name == 'push' && github.ref == 'refs/heads/main'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bash scripts/publish_coverage_badge.sh

secrets:
# raw gitleaks binary — gitleaks-action@v2 requires a paid license on org repos.
# Runs on GitHub-hosted ubuntu-latest: this is a PUBLIC repo, so GHA-hosted
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
> The tool that completes the year.

[![CI](https://github.com/nousergon/telos/actions/workflows/ci.yml/badge.svg)](https://github.com/nousergon/telos/actions/workflows/ci.yml)
[![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/)
[![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/nousergon/telos/badges/coverage.json)](https://github.com/nousergon/telos/actions/workflows/ci.yml)
[![License](https://img.shields.io/github/license/nousergon/telos)](LICENSE)
[![Status: Pre-Alpha](https://img.shields.io/badge/status-pre--alpha-orange.svg)](#status)

**A deterministic personal tax engine.** LLM document ingestion in front, pure-code
Expand Down Expand Up @@ -71,7 +71,7 @@ git clone https://github.com/nousergon/telos.git
cd telos
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest # 88 tests, coverage gate 90%
pytest # 483 tests, coverage gate 98% (measured over the whole telos package)
ruff check .
```

Expand Down
52 changes: 52 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Security Policy

## Reporting a vulnerability

If you find a security vulnerability in Telos, please report it privately:

- **Preferred:** open a [GitHub Security Advisory](https://github.com/nousergon/telos/security/advisories/new). This keeps the discussion private until a fix ships.
- **Alternative:** email `security@nousergon.ai` with a description and reproduction steps.

Please **do not** open a public issue for security reports. I aim to acknowledge within 72 hours and ship a fix or mitigation within 14 days for high-severity issues.

## Scope

Telos is a personal, self-hosted tax engine: LLM document ingestion in front, pure-code
computation in the middle, official-form PDF output at the back. The sensitive surface is
**tax-document and PII handling**. In scope:

- **PII / document exposure:** any path that leaks a W-2, 1099, or other ingested tax
document — or values extracted from one (SSNs, income, account numbers) — through logs,
error messages, telemetry, or a generated artifact that escapes the operator's own
`TELOS_DATA_DIR`.
- **Injection / escalation:** unsafe handling of LLM-extracted document content that
reaches a shell, filesystem path, or the PDF form-filling layer (`src/telos/forms/`).
- **Determinism / correctness escapes:** any path where the coverage guard
(`telos.engine.guard`) is silently bypassed, letting an unsupported form or line item
compute a number without declaring it.
- **Supply-chain:** a dependency or install path that could execute untrusted code during
`pip install -e .` or ingestion.

Out of scope:

- Issues requiring local filesystem/process access (if your machine is compromised, the
threat model has already failed — `TELOS_DATA_DIR` and your API keys live there).
- Vulnerabilities in upstream dependencies not yet publicly disclosed — report those
upstream first.

## Threat model assumptions

- **Single-user, local-first.** There is no multi-user model in this engine; tax data
stays in the operator's own `TELOS_DATA_DIR`.
- **Credentials for the ingestion path route through the krepis router edge** — no
direct provider API key lives in this repo's runtime config.
- **The model's ingestion output is treated as untrusted structured data**, validated by
Pydantic schemas with `extra="forbid"` before it reaches any downstream computation.
- **HTTPS** is assumed for all router-edge traffic.

## Hardening recommendations for self-hosters

- Keep `TELOS_DATA_DIR` and any `.env` at `600` and never commit them.
- Never place real tax documents or fixtures containing real PII into the repo tree —
synthetic fixtures only (`reportlab`-generated fakes in `tests/fixtures/`).
- Set provider-side spend limits on the ingestion path's key.
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[build-system]
requires = ["setuptools>=84.0.0"]
build-backend = "setuptools.build_meta"
Expand Down Expand Up @@ -68,7 +68,11 @@
source = ["telos"]

[tool.coverage.report]
fail_under = 90
# Raised 90 -> 98 on 2026-09-10 (repository-baseline-policy.md §4.2 C3
# ratchet) against a measured 98.34% — a floor is raised as coverage improves
# and never lowered to make a change pass. tests/test_coverage_scope.py
# protects this value and the measurement scope it applies to.
fail_under = 98
show_missing = true

[tool.ruff]
Expand Down
71 changes: 71 additions & 0 deletions scripts/publish_coverage_badge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Publish the coverage figure CI just measured as a shields.io endpoint document.
#
# repository-baseline-policy.md §5.1: a badge whose value is written by a human
# is forbidden — it renders identically to a real one, so a reader cannot tell
# them apart, and it becomes false the moment reality moves without anyone
# editing a file. This repo carried no coverage badge at all; the README now
# renders whatever this script last wrote to `coverage.json` on the orphan
# `badges` branch. The number therefore comes from the same `.coverage` file
# the gate in pyproject.toml's [tool.coverage.report] fail_under exits
# non-zero on, and it moves on its own.
#
# Runs from .github/workflows/ci.yml on pushes to main only, from the
# python-3.13 leg. The `badges` branch is seeded before this script is ever
# invoked, so the contents API is enough and no branch is ever created here.
set -euo pipefail

: "${GH_TOKEN:?GH_TOKEN is required to publish the badge document}"
: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"

BRANCH="badges"
DOC="coverage.json"

# The same measurement the gate uses — coverage.py's own totals over the
# [tool.coverage.run] source configured in pyproject.toml, not a figure
# re-derived from parsed stdout.
python -m coverage json -o coverage-summary.json --quiet

PCT="$(python -c "import json;print(f\"{json.load(open('coverage-summary.json'))['totals']['percent_covered']:.2f}\")")"

# shields' own convention: red below 60, yellow below 80, green above 90.
COLOR="$(python -c "
pct = float('${PCT}')
print('brightgreen' if pct >= 90 else 'green' if pct >= 80 else 'yellow' if pct >= 60 else 'red')
")"

python - "$PCT" "$COLOR" <<'PY' > badge-endpoint.json
import json, sys
pct, color = sys.argv[1], sys.argv[2]
print(json.dumps({
"schemaVersion": 1,
"label": "coverage",
"message": f"{pct}%",
"color": color,
}, indent=2))
PY

echo "measured coverage: ${PCT}% (${COLOR})"

# The contents API needs the blob sha to replace an existing file. Its absence
# is a hard error rather than a create, because a missing document means the
# `badges` branch is gone — and silently recreating it would hide that the
# README badge has been rendering an error to every reader in the meantime.
SHA="$(gh api "repos/${GITHUB_REPOSITORY}/contents/${DOC}?ref=${BRANCH}" --jq '.sha')"

if [ -z "${SHA}" ]; then
echo "::error::${DOC} not found on the ${BRANCH} branch — the README badge is broken" >&2
exit 1
fi

if [ "$(gh api "repos/${GITHUB_REPOSITORY}/contents/${DOC}?ref=${BRANCH}" --jq '.content' | base64 --decode)" = "$(cat badge-endpoint.json)" ]; then
echo "coverage unchanged at ${PCT}% — nothing to publish"
exit 0
fi

gh api --method PUT "repos/${GITHUB_REPOSITORY}/contents/${DOC}" \
-f message="chore(badges): coverage ${PCT}% from ${GITHUB_SHA:-HEAD}" \
-f branch="${BRANCH}" \
-f sha="${SHA}" \
-f content="$(base64 < badge-endpoint.json | tr -d '\n')" \
--jq '.commit.sha'
103 changes: 103 additions & 0 deletions tests/test_coverage_scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""The coverage gate's *scope* is asserted here, not only its number.

repository-baseline-policy.md §4.2 C5: the way a coverage gate stops being
honest is by narrowing what it measures rather than by lowering the number —
which reads as an improvement in every report. Measured on symposion, removing
one flag moved the reported figure from 34.76% to 92.36% with no new test
code.

So these tests assert what a passing suite cannot otherwise notice:

* the measured source is the WHOLE ``telos`` package (C1), never a path or
submodule narrower than that;
* the floor is enforced by a non-zero exit (C2) and is a ratchet that may be
raised and never lowered (C3);
* no coverage `omit` beyond a pinned, empty, justified list — telos currently
omits nothing;
* every source module under ``src/telos`` is inside the measured package.
"""

from __future__ import annotations

import tomllib
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]
PYPROJECT = REPO_ROOT / "pyproject.toml"
PACKAGE_ROOT = REPO_ROOT / "src" / "telos"

#: The floor may be RAISED here as coverage improves. Lowering it is a policy
#: amendment (repository-baseline-policy.md §4.2 C3), not a code change.
MINIMUM_FLOOR = 98

#: No source is currently omitted from measurement. Widening this set is a
#: scope decision, not a drive-by coverage bump — update this list alongside
#: an [tool.coverage.run] comment justifying each entry if it ever grows.
EXPECTED_OMIT: set[str] = set()


def _pyproject() -> dict:
return tomllib.loads(PYPROJECT.read_text(encoding="utf-8"))


def test_coverage_source_is_the_whole_package() -> None:
"""C1 — ``source`` names the package root, so unimported modules still count."""
sources = _pyproject()["tool"]["coverage"]["run"]["source"]
assert sources == ["telos"], (
f"coverage source must be exactly the telos package, got {sources!r}. "
"Narrowing it to a submodule or a path measures the tested subset and "
"reports it as the repository."
)


def test_coverage_floor_is_enforced_and_never_lowered() -> None:
"""C2 + C3 — the gate exits non-zero below a floor that only ratchets up."""
fail_under = _pyproject()["tool"]["coverage"]["report"]["fail_under"]
assert isinstance(fail_under, int), (
f"fail_under must be a single integer floor, got {fail_under!r}"
)
assert fail_under >= MINIMUM_FLOOR, (
f"coverage floor {fail_under} is below the ratchet {MINIMUM_FLOOR}. "
"A floor is raised as coverage improves and never lowered to make a "
"change pass (repository-baseline-policy.md §4.2 C3)."
)


def test_coverage_omit_matches_the_pinned_justified_set() -> None:
"""A shrunk denominator is a narrowing this test forces into review."""
omit = set(_pyproject()["tool"]["coverage"]["run"].get("omit", []))
added = omit - EXPECTED_OMIT
assert not added, (
f"coverage omit gained unreviewed entries: {sorted(added)}. "
"Each omitted path removes files from the denominator, raising the "
"reported figure without adding a test — update EXPECTED_OMIT here "
"alongside a justification comment in pyproject.toml if this is "
"deliberate."
)


def test_every_source_module_is_inside_the_measured_package() -> None:
"""No source file lives outside what ``--cov=telos`` measures."""
src = REPO_ROOT / "src"
stray = sorted(
p.relative_to(REPO_ROOT).as_posix()
for p in src.rglob("*.py")
if PACKAGE_ROOT not in p.parents and p != PACKAGE_ROOT
)
assert not stray, (
f"source modules outside the measured package are invisible to the "
f"coverage gate: {stray}"
)


def test_no_cov_fail_under_flag_shadows_the_pyproject_gate() -> None:
"""A CI-passed --cov-fail-under could silently override pyproject.toml's."""
import re

for workflow in (REPO_ROOT / ".github" / "workflows").glob("*.yml"):
text = workflow.read_text(encoding="utf-8")
for match in re.findall(r"--cov-fail-under=(\d+)", text):
assert int(match) >= MINIMUM_FLOOR, (
f"{workflow.name} passes --cov-fail-under={match} directly, "
"bypassing the pyproject.toml ratchet this test protects."
)