tarsier-graph 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. tarsier_graph-0.0.1/.claude/agents/reviewer.md +41 -0
  2. tarsier_graph-0.0.1/.gitattributes +2 -0
  3. tarsier_graph-0.0.1/.github/workflows/ci.yml +117 -0
  4. tarsier_graph-0.0.1/.github/workflows/release.yml +49 -0
  5. tarsier_graph-0.0.1/.gitignore +13 -0
  6. tarsier_graph-0.0.1/CLAUDE.md +35 -0
  7. tarsier_graph-0.0.1/DECISIONS.md +136 -0
  8. tarsier_graph-0.0.1/Makefile +17 -0
  9. tarsier_graph-0.0.1/PKG-INFO +84 -0
  10. tarsier_graph-0.0.1/README.md +71 -0
  11. tarsier_graph-0.0.1/STATE.md +81 -0
  12. tarsier_graph-0.0.1/docker-compose.yml +20 -0
  13. tarsier_graph-0.0.1/docs/DESIGN.md +273 -0
  14. tarsier_graph-0.0.1/docs/HANDOFF.md +251 -0
  15. tarsier_graph-0.0.1/docs/PRD.md +374 -0
  16. tarsier_graph-0.0.1/docs/SETUP-WINDOWS.md +181 -0
  17. tarsier_graph-0.0.1/docs/TRD.md +475 -0
  18. tarsier_graph-0.0.1/docs/USER-TESTING.md +94 -0
  19. tarsier_graph-0.0.1/docs/audits/01-wikimedia-terms.md +36 -0
  20. tarsier_graph-0.0.1/docs/audits/02-eventstreams.md +26 -0
  21. tarsier_graph-0.0.1/docs/audits/03-seed-qids.md +27 -0
  22. tarsier_graph-0.0.1/docs/audits/05-pypi-name.md +21 -0
  23. tarsier_graph-0.0.1/docs/testing-setup.md +111 -0
  24. tarsier_graph-0.0.1/packs/communication-history.lock.json +102 -0
  25. tarsier_graph-0.0.1/packs/communication-history.yaml +25 -0
  26. tarsier_graph-0.0.1/pyproject.toml +87 -0
  27. tarsier_graph-0.0.1/tarsier/__init__.py +3 -0
  28. tarsier_graph-0.0.1/tarsier/__main__.py +3 -0
  29. tarsier_graph-0.0.1/tarsier/api/__init__.py +1 -0
  30. tarsier_graph-0.0.1/tarsier/api/server.py +273 -0
  31. tarsier_graph-0.0.1/tarsier/api/static/__init__.py +1 -0
  32. tarsier_graph-0.0.1/tarsier/api/static/app.css +189 -0
  33. tarsier_graph-0.0.1/tarsier/api/static/app.js +236 -0
  34. tarsier_graph-0.0.1/tarsier/api/static/index.html +62 -0
  35. tarsier_graph-0.0.1/tarsier/cli/__init__.py +1 -0
  36. tarsier_graph-0.0.1/tarsier/cli/app.py +781 -0
  37. tarsier_graph-0.0.1/tarsier/cli/render.py +100 -0
  38. tarsier_graph-0.0.1/tarsier/enrich/__init__.py +1 -0
  39. tarsier_graph-0.0.1/tarsier/ingest/__init__.py +1 -0
  40. tarsier_graph-0.0.1/tarsier/ingest/backfill.py +187 -0
  41. tarsier_graph-0.0.1/tarsier/ingest/deep.py +102 -0
  42. tarsier_graph-0.0.1/tarsier/ingest/differ.py +32 -0
  43. tarsier_graph-0.0.1/tarsier/ingest/statemachine.py +200 -0
  44. tarsier_graph-0.0.1/tarsier/ingest/windows.py +49 -0
  45. tarsier_graph-0.0.1/tarsier/model/__init__.py +1 -0
  46. tarsier_graph-0.0.1/tarsier/model/entity.py +174 -0
  47. tarsier_graph-0.0.1/tarsier/model/errors.py +93 -0
  48. tarsier_graph-0.0.1/tarsier/packs/__init__.py +1 -0
  49. tarsier_graph-0.0.1/tarsier/packs/membership.py +31 -0
  50. tarsier_graph-0.0.1/tarsier/packs/schema.py +120 -0
  51. tarsier_graph-0.0.1/tarsier/packs/validate.py +253 -0
  52. tarsier_graph-0.0.1/tarsier/providers/__init__.py +1 -0
  53. tarsier_graph-0.0.1/tarsier/query/__init__.py +1 -0
  54. tarsier_graph-0.0.1/tarsier/query/asof.py +109 -0
  55. tarsier_graph-0.0.1/tarsier/query/bitemporal.py +217 -0
  56. tarsier_graph-0.0.1/tarsier/query/format.py +62 -0
  57. tarsier_graph-0.0.1/tarsier/query/payload.py +149 -0
  58. tarsier_graph-0.0.1/tarsier/store/__init__.py +1 -0
  59. tarsier_graph-0.0.1/tarsier/store/base.py +150 -0
  60. tarsier_graph-0.0.1/tarsier/store/migrations/0001_init.sql +121 -0
  61. tarsier_graph-0.0.1/tarsier/store/migrations/0002_empty_record_time.sql +9 -0
  62. tarsier_graph-0.0.1/tarsier/store/migrations/__init__.py +1 -0
  63. tarsier_graph-0.0.1/tarsier/store/postgres.py +412 -0
  64. tarsier_graph-0.0.1/tarsier/time/__init__.py +1 -0
  65. tarsier_graph-0.0.1/tarsier/time/precision.py +173 -0
  66. tarsier_graph-0.0.1/tarsier/upstream/__init__.py +1 -0
  67. tarsier_graph-0.0.1/tarsier/upstream/client.py +223 -0
  68. tarsier_graph-0.0.1/tarsier/upstream/entitydata.py +141 -0
  69. tarsier_graph-0.0.1/tarsier/upstream/eventstreams.py +64 -0
  70. tarsier_graph-0.0.1/tarsier/upstream/sparql.py +116 -0
  71. tarsier_graph-0.0.1/tests/__init__.py +0 -0
  72. tarsier_graph-0.0.1/tests/adversarial/__init__.py +0 -0
  73. tarsier_graph-0.0.1/tests/adversarial/test_11_rate_limit.py +80 -0
  74. tarsier_graph-0.0.1/tests/adversarial/test_15_observed_honesty.py +81 -0
  75. tarsier_graph-0.0.1/tests/conftest.py +38 -0
  76. tarsier_graph-0.0.1/tests/contract/__init__.py +0 -0
  77. tarsier_graph-0.0.1/tests/contract/cassettes/sparql_hop.json +338 -0
  78. tarsier_graph-0.0.1/tests/contract/cassettes/wbgetentities.json +4175 -0
  79. tarsier_graph-0.0.1/tests/contract/test_upstream_contract.py +143 -0
  80. tarsier_graph-0.0.1/tests/e2e/__init__.py +0 -0
  81. tarsier_graph-0.0.1/tests/e2e/test_pack_validate_offline.py +45 -0
  82. tarsier_graph-0.0.1/tests/fixtures/vocabulary.json +34 -0
  83. tarsier_graph-0.0.1/tests/helpers.py +69 -0
  84. tarsier_graph-0.0.1/tests/integration/__init__.py +0 -0
  85. tarsier_graph-0.0.1/tests/integration/test_bitemporal.py +218 -0
  86. tarsier_graph-0.0.1/tests/integration/test_deep_and_api.py +248 -0
  87. tarsier_graph-0.0.1/tests/integration/test_ingest.py +415 -0
  88. tarsier_graph-0.0.1/tests/integration/test_migrations.py +183 -0
  89. tarsier_graph-0.0.1/tests/property/__init__.py +0 -0
  90. tarsier_graph-0.0.1/tests/property/test_m1_properties.py +249 -0
  91. tarsier_graph-0.0.1/tests/property/test_properties.py +77 -0
  92. tarsier_graph-0.0.1/tests/smoke/__init__.py +0 -0
  93. tarsier_graph-0.0.1/tests/smoke/test_upstream_smoke.py +128 -0
  94. tarsier_graph-0.0.1/tests/unit/__init__.py +0 -0
  95. tarsier_graph-0.0.1/tests/unit/test_asof.py +74 -0
  96. tarsier_graph-0.0.1/tests/unit/test_backfill.py +314 -0
  97. tarsier_graph-0.0.1/tests/unit/test_client.py +168 -0
  98. tarsier_graph-0.0.1/tests/unit/test_entity_differ.py +206 -0
  99. tarsier_graph-0.0.1/tests/unit/test_entitydata.py +83 -0
  100. tarsier_graph-0.0.1/tests/unit/test_lint_no_clock.py +70 -0
  101. tarsier_graph-0.0.1/tests/unit/test_migration_files.py +36 -0
  102. tarsier_graph-0.0.1/tests/unit/test_pack_schema.py +80 -0
  103. tarsier_graph-0.0.1/tests/unit/test_pack_validate.py +140 -0
  104. tarsier_graph-0.0.1/tests/unit/test_precision.py +169 -0
  105. tarsier_graph-0.0.1/tests/unit/test_render_cli.py +184 -0
  106. tarsier_graph-0.0.1/tests/unit/test_sse.py +72 -0
  107. tarsier_graph-0.0.1/tests/unit/test_values_deep.py +163 -0
  108. tarsier_graph-0.0.1/tests/unit/test_windows_sparql.py +189 -0
  109. tarsier_graph-0.0.1/tools/lint_no_clock.py +121 -0
  110. tarsier_graph-0.0.1/tools/record_cassettes.py +71 -0
  111. tarsier_graph-0.0.1/tools/tasks.py +41 -0
  112. tarsier_graph-0.0.1/uv.lock +625 -0
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: reviewer
3
+ description: Spec-conformance reviewer for Tarsier. Reads a staged diff plus named spec sections and returns a fixed-format verdict. Cannot write code.
4
+ tools: Read, Grep, Glob
5
+ ---
6
+
7
+ You are the Reviewer. You receive (1) a git diff and (2) the spec sections the task cites. You read nothing else. You never write or edit files. You return exactly the format below.
8
+
9
+ ## Checklist (evaluate in order; each item has a fixed severity)
10
+
11
+ 1. BLOCKER · HANDOFF §5.1 · Any clock call in tarsier/core, ingest, query, time, packs.
12
+ 2. BLOCKER · HANDOFF §5.2 / TRD §5.3 · Any float written toward canonical tables.
13
+ 3. BLOCKER · HANDOFF §5.3 / TRD Principle 4 · Any UPDATE of a claims row or in-place mutation of a claim object after insert.
14
+ 4. BLOCKER · HANDOFF §5.4 / TRD Principle 6 · providers/ or enrich/ importing store write methods; or SQL granting public write to tarsier_enrich.
15
+ 5. BLOCKER · HANDOFF §5.5 / TRD §8.1 · A second HTTP client, thread, asyncio task, or concurrency primitive in ingest or upstream.
16
+ 6. BLOCKER · HANDOFF §5.10 · A change that contradicts a cited spec section with no ADR in the diff.
17
+ 7. BLOCKER · HANDOFF §5.11–12 · Any company/product/person reference as inspiration; any secret, key, or .env content.
18
+ 7b. BLOCKER · HANDOFF §5.13–14 · A workflow using a GitHub-hosted runner label, missing `timeout-minutes`, installing packages, binding a fixed port, or any Actions config aimed at the public mirror.
19
+ 8. MAJOR · TRD §4.2 · State-machine transition outside the documented sequence, or APPLIED/LOGGED not in one transaction.
20
+ 9. MAJOR · TRD §4.3 / PRD §11.2–3 · Idempotency or out-of-order guard missing or bypassable.
21
+ 10. MAJOR · TRD §7 · Canonical hash includes excluded tables/columns, or serialization not canonical (unsorted keys, float, non-UTC, variable precision).
22
+ 11. MAJOR · TRD §5.2 · Time/range math missing a precision case, BCE handling, or before/after widening.
23
+ 12. MAJOR · TRD §11 · Pack schema accepting URLs or free-form query text; f-string SQL; unvalidated identifiers reaching SQL or SPARQL.
24
+ 13. MAJOR · TRD §12 · New behavior without a test at the layer TRD §12 assigns; adversarial case listed for this milestone missing.
25
+ 14. MAJOR · DESIGN §1.2 / §1.5 · Command without --json; color used as sole signal; NO_COLOR ignored.
26
+ 15. MINOR · DESIGN §8 / §9 · Vocabulary drift; emoji; box-drawing; relative timestamps in query output; print() in library code.
27
+ 16. MINOR · TRD §3 · Dependency added without a one-line reason in pyproject comments.
28
+ 17. MINOR · General · Dead code, unused imports, missing type on a public function.
29
+
30
+ ## Output format (exact)
31
+
32
+ VERDICT: PASS | PASS w/ MINOR | FAIL
33
+ BLOCKERS:
34
+ - [#<item>] <file:line> — <one sentence> — cites <spec §>
35
+ MAJORS:
36
+ - [#<item>] <file:line> — <one sentence> — cites <spec §>
37
+ MINORS:
38
+ - [#<item>] <file:line> — <one sentence> — cites <spec §>
39
+ SPEC CONFLICT: none | <section A> vs <section B>: <one sentence>
40
+
41
+ Rules: cite line numbers from the diff; one sentence per finding; no praise; no suggestions beyond the finding; if a finding needs a section you were not given, say "not given — cannot evaluate" under that item rather than guessing.
@@ -0,0 +1,2 @@
1
+ # LF everywhere so hashes, lock files and migration checksums match across Linux and Windows.
2
+ * text=auto eol=lf
@@ -0,0 +1,117 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: ci-${{ github.workflow }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ env:
17
+ CI: "true"
18
+ NO_COLOR: "1"
19
+ UV_PROJECT_ENVIRONMENT: .venv-py312
20
+ TARSIER_USER_AGENT: "tarsier-graph/ci (ci@invalid)" # never hits the network in CI (cassettes)
21
+
22
+ jobs:
23
+ linux:
24
+ name: linux (lint, unit, property, integration, adversarial, e2e)
25
+ runs-on: [self-hosted, linux, macworker1]
26
+ timeout-minutes: 60 # includes waiting behind the shared per-machine job queue (ADR-10)
27
+ services:
28
+ postgres:
29
+ image: postgres:16
30
+ env:
31
+ POSTGRES_USER: tarsier
32
+ POSTGRES_PASSWORD: tarsier
33
+ POSTGRES_DB: tarsier_test
34
+ ports:
35
+ - 5432 # random host port; read back below. Never a fixed port on this machine.
36
+ options: >-
37
+ --health-cmd "pg_isready -U tarsier"
38
+ --health-interval 5s
39
+ --health-timeout 5s
40
+ --health-retries 10
41
+ steps:
42
+ - uses: actions/checkout@v7
43
+ # The owner-installed uv lives in ~/.local/bin, which the runner service's PATH lacks (ADR-19).
44
+ - name: Put uv on PATH
45
+ shell: bash
46
+ run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"
47
+ - name: Sync (locked)
48
+ shell: bash
49
+ run: uv sync --locked --all-groups
50
+ - name: Lint and type
51
+ shell: bash
52
+ run: make check-static
53
+ - name: Unit and property
54
+ shell: bash
55
+ run: make test-unit
56
+ - name: Contract (recorded cassettes)
57
+ shell: bash
58
+ run: make test-contract
59
+ - name: Integration
60
+ shell: bash
61
+ env:
62
+ TARSIER_TEST_DSN: postgresql://tarsier:tarsier@127.0.0.1:${{ job.services.postgres.ports['5432'] }}/tarsier_test
63
+ run: make test-int
64
+ - name: Adversarial
65
+ shell: bash
66
+ env:
67
+ TARSIER_TEST_DSN: postgresql://tarsier:tarsier@127.0.0.1:${{ job.services.postgres.ports['5432'] }}/tarsier_test
68
+ run: make test-adv
69
+ - name: End-to-end (offline cassettes)
70
+ shell: bash
71
+ env:
72
+ TARSIER_TEST_DSN: postgresql://tarsier:tarsier@127.0.0.1:${{ job.services.postgres.ports['5432'] }}/tarsier_test
73
+ run: make e2e
74
+
75
+ windows:
76
+ name: windows (lint, unit, property)
77
+ runs-on: [self-hosted, windows, deezcomputer]
78
+ timeout-minutes: 45 # includes waiting behind the shared per-machine job queue (ADR-10)
79
+ steps:
80
+ - uses: actions/checkout@v7
81
+ - name: Use Git Bash on Windows
82
+ shell: powershell
83
+ run: Add-Content -Path $env:GITHUB_PATH -Value 'C:\Program Files\Git\bin'
84
+ - name: Sync (locked)
85
+ shell: bash
86
+ run: uv sync --locked --all-groups
87
+ - name: Lint and type
88
+ shell: bash
89
+ run: uv run python tools/tasks.py check-static # no make on this runner (ADR-9)
90
+ - name: Unit and property
91
+ shell: bash
92
+ run: uv run python tools/tasks.py test-unit
93
+ - name: Contract (recorded cassettes)
94
+ shell: bash
95
+ run: uv run python tools/tasks.py test-contract
96
+
97
+ probe:
98
+ # Manual only. Runs the capability probe against the laptop's local model (M4+).
99
+ if: github.event_name == 'workflow_dispatch'
100
+ name: probe (local model, windows)
101
+ runs-on: [self-hosted, windows, deezcomputer]
102
+ timeout-minutes: 30
103
+ steps:
104
+ - uses: actions/checkout@v7
105
+ - name: Use Git Bash on Windows
106
+ shell: powershell
107
+ run: Add-Content -Path $env:GITHUB_PATH -Value 'C:\Program Files\Git\bin'
108
+ - name: Sync (locked)
109
+ shell: bash
110
+ run: uv sync --locked --all-groups
111
+ - name: Probe phi4-mini
112
+ shell: bash
113
+ run: uv run tarsier probe --model ollama:phi4-mini --json > probe-phi4-mini.json
114
+ - uses: actions/upload-artifact@v4
115
+ with:
116
+ name: probe-phi4-mini
117
+ path: probe-phi4-mini.json
@@ -0,0 +1,49 @@
1
+ name: Release
2
+
3
+ # Tag v<version> -> build -> PyPI trusted publishing for tarsier-graph (TRD §15, Audit 05).
4
+ on:
5
+ push:
6
+ tags: ["v*"]
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: release-${{ github.ref }}
13
+ cancel-in-progress: false
14
+
15
+ env:
16
+ UV_PROJECT_ENVIRONMENT: .venv-py312
17
+
18
+ jobs:
19
+ publish:
20
+ name: publish to PyPI
21
+ runs-on: [self-hosted, linux, macworker1]
22
+ timeout-minutes: 45
23
+ environment: pypi
24
+ permissions:
25
+ contents: read
26
+ id-token: write
27
+ steps:
28
+ - uses: actions/checkout@v7
29
+ # The owner-installed uv lives in ~/.local/bin (ADR-19); the CI workflow does the same.
30
+ - name: Put uv on PATH
31
+ shell: bash
32
+ run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"
33
+ - name: Tag matches package version
34
+ shell: bash
35
+ run: |
36
+ version=$(uv version --short)
37
+ test "v${version}" = "${GITHUB_REF_NAME}" || { echo "tag ${GITHUB_REF_NAME} != v${version}"; exit 1; }
38
+ - name: Build
39
+ shell: bash
40
+ run: |
41
+ rm -rf dist
42
+ uv build --no-sources
43
+ - name: Publish
44
+ shell: bash
45
+ run: uv publish --trusted-publishing always
46
+ - name: Clean
47
+ if: always()
48
+ shell: bash
49
+ run: rm -rf dist
@@ -0,0 +1,13 @@
1
+ .venv*/
2
+ __pycache__/
3
+ *.pyc
4
+ .mypy_cache/
5
+ .ruff_cache/
6
+ .pytest_cache/
7
+ .hypothesis/
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+ .env
12
+ packs/*.qids.json
13
+ probe-*.json
@@ -0,0 +1,35 @@
1
+ # Tarsier
2
+
3
+ Deterministic, bitemporal knowledge graph of events. Python 3.12, Postgres 16, Apache-2.0.
4
+ Specs: docs/PRD.md (what), docs/TRD.md (how), docs/DESIGN.md (output), docs/audits/ (verified constraints), docs/HANDOFF.md (rules).
5
+
6
+ ## You are the Builder
7
+ - Read docs/HANDOFF.md §0, §2, §3, §5 first. Then STATE.md. Then only the sections STATE.md names.
8
+ - Never redesign. Deviations → DECISIONS.md ADR with section number.
9
+ - Replies ≤ 5 lines. Ask only on blocking spec conflicts.
10
+
11
+ ## Commands
12
+ - `make check` → ruff, mypy --strict, import-linter, wall-clock lint, pytest (unit+property+contract)
13
+ - `make test-contract` → contract tests against recorded cassettes (offline)
14
+ - `make test-int` → integration tests (needs Postgres from docker compose)
15
+ - `make test-adv` → adversarial suite (tests/adversarial/)
16
+ - `make e2e` → offline cassette end-to-end + replay --verify on fixture pack
17
+
18
+ ## Layout (TRD §2)
19
+ tarsier/packs · tarsier/upstream · tarsier/model · tarsier/time (L0)
20
+ tarsier/store (L1)
21
+ tarsier/ingest (L2)
22
+ tarsier/query (L3)
23
+ tarsier/providers (L4)
24
+ tarsier/enrich (L5)
25
+ tarsier/cli · tarsier/api · ui/ (L6)
26
+ Import direction is downward only; import-linter enforces it.
27
+
28
+ ## Hard rules
29
+ See docs/HANDOFF.md §5. Violations are review BLOCKERs.
30
+
31
+ ## Vocabulary (only these words in user-facing text)
32
+ asserted observed present unknown absent retracted checkpoint reconcile replay pack card
33
+
34
+ ## Review
35
+ Every staged diff goes to the `reviewer` subagent with the spec sections the task cites. Fix BLOCKER/MAJOR before commit.
@@ -0,0 +1,136 @@
1
+ # DECISIONS
2
+
3
+ Format: ADR-<n> · <date> · <status: accepted|superseded> · cites <spec §>
4
+ One paragraph: context, decision, consequence. Never edit a past ADR; supersede it.
5
+
6
+ ADR-0 · bootstrap · accepted · cites HANDOFF §1
7
+ The Builder operates under docs/HANDOFF.md. Spec precedence: Audits > TRD > PRD > DESIGN when they conflict, because audits are verified against primary sources and later documents refine earlier ones. Any such conflict is still reported to the Owner.
8
+
9
+ ADR-1 · 2026-09-16 · accepted · cites TRD §3
10
+ uv manages the environment and the hash-locked `uv.lock`; CI installs with `uv sync --locked`. The pip fallback is not maintained. Consequence: `uv` must be preinstalled on both runners (HANDOFF §6).
11
+
12
+ ADR-2 · 2026-09-16 · accepted · cites TRD §3, §5.1
13
+ Plain-SQL migrations in `tarsier/store/migrations/NNNN_*.sql`, applied by a ~40-line runner in `tarsier/store/postgres.py`. Each file runs in one transaction under a Postgres advisory lock and is recorded in `schema_migrations(name, checksum)`; the checksum is sha256 of the LF-normalized file, and a changed applied migration raises `StoreConflict`. `schema_migrations` is bookkeeping and is excluded from the canonical hash (TRD §7).
14
+
15
+ ADR-3 · 2026-09-16 · accepted · cites TRD §3, §8.4
16
+ Hand-written SSE parser in `tarsier/upstream/eventstreams.py` (WHATWG rules: data lines join, id persists, dispatch only on a blank line). A truncated trailing event is never yielded, which serves adversarial #10.
17
+
18
+ ADR-4 · 2026-09-16 · accepted · cites TRD §11
19
+ Append-only trigger on `ingest_log`, on by default: BEFORE UPDATE/DELETE (row) and BEFORE TRUNCATE (statement) raise. Consequence: `replay --verify` (M2) must rebuild into a separate database or schema, never truncate the live log.
20
+
21
+ ADR-5 · 2026-09-16 · accepted · cites TRD §7
22
+ Entity disk cache location is `~/.cache/tarsier/entities/`. Adopted now, implemented with replay in M2.
23
+
24
+ ADR-6 · 2026-09-16 · accepted · cites Audit 02 §Design rules 2, TRD §8.4
25
+ `RETENTION_SAFE = 6 days`. Adopted now, implemented with the stream gap check in M2.
26
+
27
+ ADR-7 · 2026-09-16 · accepted · cites TRD §5.1
28
+ Three mechanical deviations in `0001_init.sql` from the abridged DDL: (a) `item_windows.window` is written `"window"` because WINDOW is a reserved word and the unquoted DDL does not parse; (b) roles are created inside a `DO` block guarded by `pg_roles` because roles are cluster-wide and a second database on the same cluster would otherwise fail; (c) added `GRANT USAGE ON SCHEMA enrich TO tarsier_enrich` and `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO tarsier_core`, without which the specified table grants are unusable. No column, type, constraint or grant on `public` for `tarsier_enrich` changed; the integration suite checks that `tarsier_enrich` cannot insert into `public.items`.
29
+
30
+ ADR-8 · 2026-09-16 · accepted · cites Audit 02 §2.3, §2.4, §M0 smoke tests; TRD §8.4
31
+ Both smoke tests passed live on 2026-09-17 UTC. `recentchange` filtered on wiki=wikidatawiki, namespace=0, type in {edit,new} carries wiki, namespace, type, title, revision.new/old, timestamp, meta.dt, and an SSE id array of {topic, partition, timestamp}. `mediawiki.revision-create` filtered on database=wikidatawiki, page_namespace=0 carries database, page_namespace, page_title, rev_id, rev_parent_id, rev_timestamp, meta.dt with the same id shape. Per Audit 02 §2.4 the stream used from M2 is `mediawiki.revision-create`; `recentchange` stays as the documented fallback. Audit 01 §1.8 is also confirmed (`Special:EntityData/Q42.json?revision=2543789771` returned lastrevid 2543789771), so the `action=query` fallback is not implemented.
32
+
33
+ ADR-9 · 2026-09-16 · accepted · cites HANDOFF §1.4, TRD §15, docs/testing-setup.md
34
+ Neither the Owner's laptop nor the Windows runner has `make` (Git Bash does not ship it, and jobs cannot install packages). The command lists live in the stdlib-only `tools/tasks.py`; every Makefile target delegates to it with the same target names, the Linux job keeps calling `make`, and the Windows job calls `uv run python tools/tasks.py <target>`. A `smoke` target runs the live smoke tests, which CI never runs.
35
+
36
+ ADR-10 · 2026-09-16 · accepted · cites TRD §15
37
+ The Owner approved a shared first-come-first-served job queue across this repo and another project on both runner machines, and waiting in that queue counts against `timeout-minutes`. The linux job timeout goes from 30 to 60 minutes and the windows job from 20 to 45. Job contents are unchanged.
38
+
39
+ ADR-11 · 2026-09-16 · accepted · cites PRD §10.1, §13; Audit 03 §Resolution; TRD §11, §15
40
+ `tarsier pack validate <path>` has three modes. With no lock file it resolves live and writes `<name>.lock.json`. With a lock file it checks offline that the pack's identifier set, name and language match the lock, and exits 2 with a `--live` instruction on mismatch. With `--live` (the flag TRD §15's nightly job names) it resolves live, rewrites the lock only if the bytes change, and reports label drift. Live resolution rejects missing, redirected, wrong-kind and disambiguation (P31 contains Q4167410) identifiers, and identifiers with no label in the pack language. It also requires `traverse` properties to have datatype wikibase-item and `temporal` properties to have datatype time. Pack `name` must equal the file stem, list entries must be unique, and `description` rejects URLs. The lock holds only kind, label and datatype per identifier, keyed in (letter, number) order, LF, with no timestamps or revision ids, so it changes only on real drift. The Owner commits the lock after confirming labels (HANDOFF §6).
41
+
42
+ ADR-12 · 2026-09-16 · accepted · cites DESIGN §9 item 2, TRD §3
43
+ Typer requires rich, whose help panels draw boxes. The app sets `rich_markup_mode=None` and `pretty_exceptions_enable=False`, so help renders as plain click text; a unit test checks for box-drawing characters. `pyyaml` is added (safe_load only) for pack parsing, which TRD §3 does not name.
44
+
45
+ ADR-13 · 2026-09-16 · accepted · cites TRD §2
46
+ The import-linter layers contract orders L0 internally as `packs` > `upstream : time` > `model`. This is stricter than TRD §2, which lists them as one layer, and matches the actual dependencies: packs use model errors, upstream uses model errors, and nothing in L0 imports packs. A second contract forbids `providers` and `enrich` from importing `store` (Principle 6).
47
+
48
+ ADR-14 · 2026-09-16 · accepted · cites TRD §15, Audit 05
49
+ `.github/workflows/release.yml` publishes `tarsier-graph` from a `v*` tag on the Linux runner via PyPI trusted publishing (environment `pypi`, `id-token: write`) after checking that the tag equals the package version. The name is reserved by tagging `v0.0.1` once the Owner has configured the pending publisher. If PyPI refuses OIDC from a self-hosted runner, the Owner publishes 0.0.1 from their own machine instead; the Builder never handles credentials.
50
+
51
+ ADR-15 · 2026-09-16 · accepted · cites TRD Principle 6, §5.1, §11
52
+ The reviewer flagged a spec conflict: Principle 6 said the enrichment role has "no grants on `public`", but §5.1 and §11 grant it SELECT on `public` with no write grant. The Owner resolved it in favor of §5.1 and amended the Principle 6 text in docs/TRD.md to match: `tarsier_enrich` has SELECT only on `public`, no write grants there, and full grants on `enrich`, because enrichment has to read claims (§2). The integration suite checks that `tarsier_enrich` can SELECT `public.claims` and cannot INSERT into it or UPDATE or DELETE it.
53
+
54
+ ADR-16 · 2026-09-16 · accepted · cites TRD §5.1, §5.2, §12.1 #6
55
+ §5.1's literal CHECK `(value_kind = 'time') = (event_time IS NOT NULL)` would reject the pre-4713 BC sentinel that §5.2 and adversarial #6 require. 0001 implements the check as §5.2 relaxes it: a `time` claim has a non-NULL `event_time` exactly when `value.out_of_range` is not `true`, and a non-time claim always has a NULL `event_time`. An integration test covers the range case, the sentinel case, and the rejected case where both are set.
56
+
57
+ ADR-17 · 2026-09-16 · accepted · cites TRD §8.1, §12.1 #11, Principle 2
58
+ `Retry-After` is honored in both RFC 9110 forms: delta-seconds, and an HTTP-date converted to a delay against an injectable `now` (a past date means no wait). An unparseable value falls back to jittered exponential backoff. This is the one wall-clock read in `tarsier.upstream`, a package the no-clock lint does not guard. It only sizes a sleep and never reaches stored data.
59
+
60
+ ADR-18 · 2026-09-17 · accepted · cites DESIGN §6.6, §8, §9 item 11
61
+ The reviewer flagged a spec conflict: the README tagline in DESIGN §6.6 says "honest about what it hasn't seen", but §9 item 11 bans "seen" as a synonym for `observed`. The Owner resolved it in favor of the fixed vocabulary. The tagline now reads "honest about what it hasn't observed", and DESIGN §6.6 was amended in place to match.
62
+
63
+ ADR-19 · 2026-09-17 · accepted · cites TRD §15, docs/testing-setup.md rule 5
64
+ The first PR run failed on macworker1 at `uv sync` with `uv: command not found`: the Owner's uv is installed in `~/.local/bin`, and the runner service's PATH does not include it. The linux job now adds `$HOME/.local/bin` to `$GITHUB_PATH` before syncing, the same way the windows job adds Git Bash. The step installs nothing and changes no machine settings.
65
+
66
+ ADR-20 · 2026-09-17 · accepted · supersedes ADR-13 · cites TRD §2, §8.2
67
+ M1 puts the SPARQL generator in `tarsier.upstream.sparql` (TRD §2 L0), and it reads the pack, so `tarsier.upstream` now sits above `tarsier.packs` in the import-linter layering instead of below it. `tarsier.packs` never imports `tarsier.upstream`: `pack validate` still takes its resolver by injection (ADR-11). Both are L0 in TRD §2, so the order between them is ours to pick.
68
+
69
+ ADR-21 · 2026-09-17 · accepted · cites TRD §4.4, §5.1
70
+ TRD §4.4 signs a claim with Wikidata's own `mainsnak.hash`, but `claims` (TRD §5.1) has no column for it, so a claim read back from the store could never reproduce that signature and every revision would diff as changed. The signature is instead sha256 of the canonical JSON of `{value_kind, value, rank, qualifiers, references}` — the same fields, sourced from what is stored. Wikidata's snak hash changes exactly when those fields do, so change detection is unaffected, and the property tests `diff(a, a) == empty` and `apply(diff(a, b), a) == b` still hold.
71
+
72
+ ADR-22 · 2026-09-17 · accepted · cites TRD §8.2, PRD §10.1, Audit 03 §Property-set note
73
+ The first compile of the default pack returned 20 items, because each hop both expanded the frontier and filtered it by `require_temporal`: traversal could not pass through intermediate classes, which rarely carry dates (`telephone` Q11035 has no P571 at all). Each hop now expands through every neighbour and admits only the neighbours that carry a temporal property, with the temporal check as an OPTIONAL binding in the same query, so it stays one query per hop per page. The admitted set is what reaches `packs/<name>.qids.json`; frontier-only items are never ingested. Membership (PRD §11.4) is unchanged and still decides per entity at ingest.
74
+
75
+ ADR-23 · 2026-09-17 · accepted · cites TRD §8.2, §8.1
76
+ A hop query carrying a frontier of thousands of QIDs exceeds the URL length WDQS accepts (HTTP 414), so SPARQL goes out as a POST form instead of a GET. `UpstreamClient.post_json` shares the single-flight guard, retries and backoff with `get_json`; the client is still one instance making one request at a time. The frontier is also sent in chunks of 250 seeds per query so each query stays inside the 60 s server cap (Audit 01 §1.6).
77
+
78
+ ADR-24 · 2026-09-17 · accepted · cites TRD §5.1, §4.2, PRD §9.2
79
+ Upstream revision timestamps have one-second resolution, so two revisions of one item can share a timestamp, and closing a claim at the instant it was asserted yields the empty record interval `[t, t)`. 0001's `lower_inc AND NOT upper_inc` check rejects that and aborts the transaction. Migration 0002 relaxes it to `isempty(record_time) OR (lower_inc AND NOT upper_inc)`. The row is kept rather than deleted: it is part of what the ingest log replays, and an empty interval matches no `as-known-at` instant, which is the honest answer. 0001 itself is untouched, because the runner's checksum forbids editing an applied migration (ADR-2).
80
+
81
+ ADR-25 · 2026-09-17 · accepted · cites TRD §8.2, §13, Audit 01 §1.6-1.7
82
+ Hop expansion through every neighbour (ADR-22) makes the frontier grow without bound: at depth 3 from the `communication` seeds, hop 2 had not finished after 25 minutes of serial WDQS queries, against a §13 target that assumes compile is a preliminary step. Each hop now carries at most 2,000 items into the next hop, taken in QID order so the choice is deterministic, and `pack compile` warns which hops were truncated. Nothing already admitted is dropped: the cut applies only to what is expanded from. A pack that needs deeper coverage lowers `depth` and narrows its seeds instead, which is also what Audit 03 §Property-set note recommends.
83
+
84
+ ADR-26 · 2026-09-17 · accepted · cites PRD §10.1, TRD §8.2, §13, Audit 03 §Property-set note
85
+ The shipped pack's depth goes from 3 (PRD §10.1's example value) to 2. At depth 3 the third hop expands from 2,000 frontier items into the general Wikidata ontology: individual queries stayed under two seconds, but the hop as a whole ran past ten minutes and hit WDQS timeouts, and nothing it reached was about the history of communication any more. Depth 2 compiles in seconds and admits about 420 items. `depth` is a pack field, not a spec constant, and it is not part of the lock file, so this needs no re-confirmation of labels. Compile also stopped paging with OFFSET: WDQS deep paging times out rather than returning, so each chunk of 50 frontier items gets one page of up to 5,000 rows and a full page marks the hop truncated.
86
+
87
+ ADR-27 · 2026-09-17 · accepted · cites PRD §9.2, TRD §5.3, ADR-0
88
+ The reviewer flagged a spec conflict: PRD §9.2 keeps qualifiers and references "verbatim", but a globe-coordinate or quantity snak nested in one carries JSON floats, which TRD §5.3 forbids anywhere in canonical data because floats are not hash-stable across a Postgres round-trip. The Owner resolved it in favor of §5.3 and amended §9.2 in place. Upstream JSON is now decoded with `json.loads(..., parse_float=str)` in `tarsier.upstream.client`, so every float is kept as the exact token upstream sent, never a re-formatted float, everywhere it appears: main values, qualifiers and references. Structure is untouched.
89
+
90
+ ADR-28 · 2026-09-17 · accepted · cites TRD §6, §5.4
91
+ `as-of --include-deprecated` could not do anything, because `item_windows` is derived only from non-deprecated claims (§5.4), and the flag only reached the optional `--property` filter. With the flag, `as-of` now takes the window from the claims themselves — any current claim whose `event_time` contains the instant, deprecated ranks included — one row per item, preferred rank first, then property, then guid. Without it the derived window is used exactly as before, so the default path and the rank-flip adversarial case are unchanged.
92
+
93
+ ADR-29 · 2026-09-17 · accepted · cites PRD §8, TRD §4.2, §5.2, §4.3
94
+ Three corrections to `apply_event`. (a) Every time value is converted before any write, so an unparsable one quarantines the event without leaving closes and inserts applied; QUARANTINED now means nothing was applied. (b) CHECKPOINTED is implemented for backfill and reconcile as a cursor row in `checkpoints` named `<source>:<pack>` holding the last QID, so an interrupted run resumes instead of restarting; `--restart` ignores it. (c) Out-of-range time values are counted in the result and reported by the CLI rather than printed from library code (DESIGN §9 item 6), and a `log_ingest` conflict after the writes now raises `StoreConflict` instead of returning SKIPPED with the writes committed.
95
+
96
+ ADR-30 · 2026-09-17 · accepted · cites DESIGN §1.2, §9 item 4, PRD §13
97
+ `backfill --json` streams JSON Lines: one `{"event": "item", …}` object per item as it is applied, then one `{"event": "summary", …}`. A single document at the end would hide the per-item progress a human sees, which DESIGN §9 item 4 forbids. Other commands still emit one JSON document. `pack compile` also gained `--force` to compile above the dump threshold (Audit 03 §Resolution 3).
98
+
99
+ ADR-31 · 2026-09-18 · accepted · cites TRD §8.2, Principle 2
100
+ TRD §8.2 says the QID cache carries the compile timestamp, but `tarsier.ingest` may not read a clock (Principle 2, and the wall-clock lint enforces it). `compile_qids` takes `compiled_at` as an argument and the CLI supplies it, so the timestamp is in the cache without a clock in the ingest layer. The cache is not a canonical table and is excluded from the replay hash, so the timestamp cannot affect determinism. `pack compile` also refuses an over-threshold slice before the cache is written, so a refused compile leaves no QID set behind for `backfill` (Audit 03 §Resolution 3).
101
+
102
+ ADR-32 · 2026-09-18 · accepted · cites TRD §12 (Contract), §3
103
+ The contract layer records real upstream responses as JSON in `tests/contract/cassettes/`, written by `tools/record_cassettes.py` and replayed through an `httpx.MockTransport`, instead of using `vcrpy` as TRD §12 names. vcrpy's httpx support is a compatibility risk for the one client the whole project depends on (TRD §8.1), and a recorded JSON document plus the project's own transport needs no dependency and asserts the request as well as the response. The suite checks that the recorded query still matches what the generator produces, so a stale cassette fails rather than passing quietly. `make test-contract` runs it and `make check` includes it; it never touches the network.
104
+
105
+ ADR-33 · 2026-09-18 · accepted · cites PRD §9.1, §13, DESIGN §6.1, §9 item 11
106
+ The reviewer flagged a spec conflict: DESIGN §6.1's `as-of` header carries `pack <name>`, but PRD §13 gives `as-of` no pack argument and one store may hold several packs (PRD §9.1). The Owner resolved it: `as-of` gains an optional `--pack`, which filters on `items.pack_id`, and the header always carries a pack segment — the filter name when given, otherwise the distinct packs present in the result, comma-separated. PRD §13 was amended to show the option. `as-known-at`, `diff` and `history` take the same option and the same header rule when they are built in M3.
107
+
108
+ ADR-34 · 2026-09-18 · accepted · cites TRD §5.2, §4.4, §12.1 #6, ADR-21, ADR-23
109
+ Two corrections found by review. (a) A coarse precision floors the range start and `before`/`after` widen it, so a date well inside the Postgres domain could produce a bound outside it; those bounds are now clamped to the domain, as §5.2's precision table says ("clamped to Postgres range"), and the `out_of_range` sentinel is reserved for a value whose own date Postgres cannot hold. (b) That sentinel is written by us into the claim's stored `value`, so a claim read back from the store signed differently from the same claim read from upstream and every later revision would have re-diffed it as changed; `claim_signature` now ignores the `out_of_range` key. Also noted: ADR-23 recorded a frontier chunk of 250 seeds per query, which ADR-26 later lowered to 50; the code uses 50. And `as-of --category` is refused as a `QueryError` (exit 1, user-facing) rather than a `ConfigError` (exit 2, fatal at start), per the taxonomy in TRD §10.
110
+
111
+ ADR-35 · 2026-09-18 · accepted · cites PRD §15, HANDOFF §4, §2
112
+ Milestone order changed with the Owner's goal: M3 (bitemporal queries) and M6 (read-only API and timeline UI) come before M2 (stream and replay), because the Owner asked for a version testable on their computer and their phone, and M2's acceptance needs a daemon running for 24 hours against the live stream, which would block that for a day. Nothing in M3 or M6 depends on M2: both read the canonical store that M1 fills. M2 remains required before 1.0 and is the next milestone after the UI.
113
+
114
+ ADR-36 · 2026-09-18 · accepted · cites TRD §6, DESIGN §6.2, PRD §9.2
115
+ `as-known-at` reports one row per claim, plus an `absent` row for every pack property with nothing to say at that instant, which is how DESIGN §6.2 shows P576. Without a `--pack` the answer covers only stored claims, because there is no property list to be absent against. Status comes from the predicates Postgres evaluates: `present` when the record interval contains the instant; `unknown` when the claim is `observed` and its record starts later, with reason `observed_after_instant`; `absent` otherwise. An `asserted` claim outside its interval is `absent`, never `unknown`: the stream tells us the record genuinely lacked it.
116
+
117
+ ADR-37 · 2026-09-18 · accepted · cites PRD §8, Audit 01 consequence, TRD §4.2
118
+ `backfill --deep --qids` walks each item's revisions oldest first, one serial fetch per revision, capped at 500 items (Audit 01's default) and 500 revisions per item (`--max-revisions`). Deep claims are `asserted`. Because the out-of-order guard skips any revision at or below the newest already applied, `--deep` has to run before a plain backfill of the same item; run afterwards it reports every revision as skipped, which is the guard working, not a failure. The CLI help says so.
119
+
120
+ ADR-38 · 2026-09-18 · accepted · cites DESIGN §4, §9 item 4
121
+ Table cells are clipped with an ellipsis: labels at 32 characters, rendered values at 48. One Wikidata label 200 characters long otherwise pads every row in the table to its width and destroys the dense, scannable layout DESIGN §4 asks for. `--json` carries the full value, so nothing a script needs is lost.
122
+
123
+ ADR-39 · 2026-09-18 · accepted · cites PRD §15 M6, TRD §2, §3
124
+ The HTTP API is `http.server.ThreadingHTTPServer` from the standard library, not a web framework. The surface is four read-only GET endpoints and three static files; a framework would be a dependency, a lockfile entry and a supply-chain surface for something the stdlib already does. Threading is per request and stays in L6: the ingest path's serial rule (TRD §8.1) is about the upstream client, and each request opens and closes its own store connection. Anything but GET and HEAD is refused with 405 and a drained body.
125
+
126
+ ADR-40 · 2026-09-18 · accepted · cites PRD §13, TRD §6, DESIGN §6.2
127
+ `as-known-at` takes the qid PRD §13 marks optional. With one, the answer is that item's claims plus an absent row for each pack property with nothing to say. Without one, it is every stored claim with a status, ordered by item, bounded by `--limit` (500), and the result says when it stopped early; pack properties are not filled in there, because a property absent on one item is present on another and the rows would say nothing. `--json` reports `pack_applied` so a caller can tell which pack's property list produced the absent rows, separately from the pack that admitted the item.
128
+
129
+ ADR-41 · 2026-09-18 · accepted · cites PRD §15 M6, DESIGN §6.7
130
+ `tarsier serve` binds 127.0.0.1 by default and takes `--host`. With a host other than loopback it prints the address a phone on the same network should open, found by opening a UDP socket toward a reserved TEST-NET address and reading back the local end — no packet leaves the machine and no name is resolved. The API has no authentication because it is read-only over data that is already public (Audit 01 §1.1, CC0), and the help says exactly who can reach it. The page is served from the same origin as the API, so it needs no cross-origin headers.
131
+
132
+ ADR-42 · 2026-09-18 · accepted · cites DESIGN §10 M6, §1.2
133
+ Both surfaces build their JSON in `tarsier/query/payload.py`, and `tarsier/query/format.py` renders values for both, so "API response shape mirrors `--json` exactly" is structural rather than a promise: an integration test asserts the API document equals the document the CLI prints for the same query. Value rendering moved out of `tarsier/cli` into the query layer for that reason; only the CLI prints (DESIGN §9 item 6).
134
+
135
+ ADR-43 · 2026-09-18 · accepted · cites DESIGN §1.4, §1.5, §9 item 1
136
+ Review found the page carrying a claim's origin by colour alone: the bar beside each item in the list was coloured `asserted` or `observed` with no text. Every item now names its origin in the line beneath its label, the bar is `aria-hidden` because it only repeats that word, and an item whose window came from a claim that is no longer open reads "unknown origin" rather than guessing. The `as-of` origin lookup also orders by asserting revision instead of taking an arbitrary row, and returns nothing rather than defaulting to `observed`.
@@ -0,0 +1,17 @@
1
+ # Targets the Builder fills in during M0. Names are fixed (CI and HANDOFF cite them).
2
+ .PHONY: check check-static test-unit test-contract test-int test-adv e2e smoke mirror
3
+
4
+ # Each target delegates to tools/tasks.py so hosts without make run the same commands (ADR-9).
5
+ check check-static test-unit test-contract test-int test-adv e2e smoke:
6
+ uv run python tools/tasks.py $@
7
+
8
+ # Push main to the public mirror with CI config removed. Private repo only.
9
+ mirror:
10
+ git fetch origin main
11
+ git checkout -B mirror-tmp origin/main
12
+ git rm -r -q --cached .github 2>/dev/null || true
13
+ rm -rf .github
14
+ git commit -q -m "mirror: strip CI config" || true
15
+ git push -f public mirror-tmp:main
16
+ git checkout -q -
17
+ git branch -q -D mirror-tmp
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.5
2
+ Name: tarsier-graph
3
+ Version: 0.0.1
4
+ Summary: Deterministic, bitemporal knowledge graph of events.
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: <3.13,>=3.12
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: psycopg[binary]>=3.3.5
9
+ Requires-Dist: pydantic>=2.13.5
10
+ Requires-Dist: pyyaml>=6.0.3
11
+ Requires-Dist: typer>=0.27.2
12
+ Description-Content-Type: text/markdown
13
+
14
+ # [• Tarsier
15
+
16
+ A deterministic, bitemporal knowledge graph of events —
17
+ built from Wikidata, kept current from its change stream,
18
+ and honest about what it hasn't observed.
19
+
20
+ pip install tarsier-graph
21
+ export TARSIER_USER_AGENT="tarsier-graph/0.1 (you@example.com)"
22
+ tarsier backfill --pack communication-history
23
+ tarsier query as-of 1876-03-10
24
+
25
+ as-of 1876-03-10T00:00:00Z pack communication-history rank preferred>normal
26
+
27
+ QID label window via
28
+ Q1843318 telephony in the 19th century [1800-01-01, 1900-01-01) P585
29
+ Q2916210 Mephisto [1876-01-01, 1877-01-01) P571
30
+
31
+ A window of `[-infinity, …)` means the record gives an end but no start. The engine says what
32
+ it has, not what it guesses.
33
+
34
+ [Why this exists](#why-this-exists) · [The honesty model](#the-honesty-model) · [Writing a pack](#writing-a-pack) · [Probe cards](#probe-cards) · [Limitations](#limitations)
35
+
36
+ ## Why this exists
37
+
38
+ Status: M0. Written at release.
39
+
40
+ ## The honesty model
41
+
42
+ Backfill writes `observed` claims; the change stream writes `asserted` claims. A query about a time
43
+ before an `observed` claim's lower bound answers `unknown`, never `present`.
44
+
45
+ ## Running it
46
+
47
+ Postgres comes from `docker compose up -d postgres`. Point the CLI at it with `TARSIER_DSN`
48
+ when it is not on the default port:
49
+
50
+ export TARSIER_DSN="postgresql://tarsier:tarsier@127.0.0.1:5432/tarsier"
51
+ tarsier pack compile packs/communication-history.yaml --count
52
+ tarsier backfill --pack communication-history
53
+ tarsier query as-of 1876-03-10 --json
54
+
55
+ `backfill` records every claim as `observed`: it reads the current revision of each item, so it
56
+ knows nothing about what the record said earlier. The change stream (M2) writes `asserted`.
57
+
58
+ ### The page
59
+
60
+ tarsier serve --pack communication-history # http://127.0.0.1:8080/
61
+ tarsier serve --host 0.0.0.0 --pack communication-history # also your phone, same Wi-Fi
62
+
63
+ One page, two time axes: a record-time scrubber across the top (what the record said then) and
64
+ an event-time slider down the left (what was happening then). The same four endpoints answer
65
+ JSON: `/as-of`, `/as-known-at`, `/history`, `/diff`, in the shape `--json` prints.
66
+
67
+ See [docs/USER-TESTING.md](docs/USER-TESTING.md) for what is built today and what to try.
68
+
69
+ ## Writing a pack
70
+
71
+ A pack is YAML: identifiers and configuration only. Validate it once live, confirm the printed
72
+ labels, and commit the lock file:
73
+
74
+ tarsier pack validate packs/communication-history.yaml
75
+
76
+ ## Probe cards
77
+
78
+ Optional enrichment, measured per model. Cards land with M4.
79
+
80
+ ## Limitations
81
+
82
+ Written at release.
83
+
84
+ Wikidata structured data is CC0; see Wikidata:Copyright.
@@ -0,0 +1,71 @@
1
+ # [• Tarsier
2
+
3
+ A deterministic, bitemporal knowledge graph of events —
4
+ built from Wikidata, kept current from its change stream,
5
+ and honest about what it hasn't observed.
6
+
7
+ pip install tarsier-graph
8
+ export TARSIER_USER_AGENT="tarsier-graph/0.1 (you@example.com)"
9
+ tarsier backfill --pack communication-history
10
+ tarsier query as-of 1876-03-10
11
+
12
+ as-of 1876-03-10T00:00:00Z pack communication-history rank preferred>normal
13
+
14
+ QID label window via
15
+ Q1843318 telephony in the 19th century [1800-01-01, 1900-01-01) P585
16
+ Q2916210 Mephisto [1876-01-01, 1877-01-01) P571
17
+
18
+ A window of `[-infinity, …)` means the record gives an end but no start. The engine says what
19
+ it has, not what it guesses.
20
+
21
+ [Why this exists](#why-this-exists) · [The honesty model](#the-honesty-model) · [Writing a pack](#writing-a-pack) · [Probe cards](#probe-cards) · [Limitations](#limitations)
22
+
23
+ ## Why this exists
24
+
25
+ Status: M0. Written at release.
26
+
27
+ ## The honesty model
28
+
29
+ Backfill writes `observed` claims; the change stream writes `asserted` claims. A query about a time
30
+ before an `observed` claim's lower bound answers `unknown`, never `present`.
31
+
32
+ ## Running it
33
+
34
+ Postgres comes from `docker compose up -d postgres`. Point the CLI at it with `TARSIER_DSN`
35
+ when it is not on the default port:
36
+
37
+ export TARSIER_DSN="postgresql://tarsier:tarsier@127.0.0.1:5432/tarsier"
38
+ tarsier pack compile packs/communication-history.yaml --count
39
+ tarsier backfill --pack communication-history
40
+ tarsier query as-of 1876-03-10 --json
41
+
42
+ `backfill` records every claim as `observed`: it reads the current revision of each item, so it
43
+ knows nothing about what the record said earlier. The change stream (M2) writes `asserted`.
44
+
45
+ ### The page
46
+
47
+ tarsier serve --pack communication-history # http://127.0.0.1:8080/
48
+ tarsier serve --host 0.0.0.0 --pack communication-history # also your phone, same Wi-Fi
49
+
50
+ One page, two time axes: a record-time scrubber across the top (what the record said then) and
51
+ an event-time slider down the left (what was happening then). The same four endpoints answer
52
+ JSON: `/as-of`, `/as-known-at`, `/history`, `/diff`, in the shape `--json` prints.
53
+
54
+ See [docs/USER-TESTING.md](docs/USER-TESTING.md) for what is built today and what to try.
55
+
56
+ ## Writing a pack
57
+
58
+ A pack is YAML: identifiers and configuration only. Validate it once live, confirm the printed
59
+ labels, and commit the lock file:
60
+
61
+ tarsier pack validate packs/communication-history.yaml
62
+
63
+ ## Probe cards
64
+
65
+ Optional enrichment, measured per model. Cards land with M4.
66
+
67
+ ## Limitations
68
+
69
+ Written at release.
70
+
71
+ Wikidata structured data is CC0; see Wikidata:Copyright.