Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

paper-trail

A local-first utility for durable, immutable decision records with explicit supersession. It preserves consequential choices — rejected alternatives, evidence, consequences, owner, review date, and affected projects — so later sessions recover why a decision was made without treating chats or current docs as history.

One decision per tracked Markdown file (YAML frontmatter + Markdown body); a derived JSON index serves fast queries but is never a second source of truth.

Stack

Tool Why
Python 3.12+ Implementation language
uv Environment + dependency management
argparse CLI
pytest Schema, golden, property, and subprocess tests
Ruff Lint + format
mypy (strict) Static typing

Prerequisites

  • Python 3.12+
  • uv

Setup

uv sync --extra dev
uv run pytest -q
uv run ruff check .
uv run mypy --strict src

Usage

paper-trail add        # create a decision record (id = YYYY-MM-DD-<kebab-slug>)
paper-trail list       # list records
paper-trail show <id>  # render one record
paper-trail supersede  # create a successor, linked both directions
paper-trail audit      # validate chains: cycles, forks, broken links
paper-trail index      # rebuild the derived decisions/index.json (deterministic)
paper-trail digest     # concise Markdown/JSON digest

Storage defaults to <git-root>/decisions/ of the enclosing repository; --root <path> overrides. No enclosing repository and no --root is an explicit error (never a heuristic fallback).

A step-by-step walkthrough of the full add → supersede → audit → digest lifecycle lives in docs/decision-authoring-guide.md.

Design decisions

  • Markdown is authoritative. The JSON index (decisions/index.json, gitignored, atomically rebuilt) is derived and may be regenerated; it never becomes a second source of truth.
  • Immutable history. Superseding creates a new record and links both directions through validated IDs. Existing decision meaning is never silently edited or deleted.
  • Supersession fork policy. superseded_by is scalar (at most one successor; a second supersede refuses; audit fails fan-out with both claimant IDs); supersedes is a list (fan-in for consolidation is legal). Cycles and dangling targets are always illegal.
  • High-value threshold. Required consequences and alternatives fields discourage logging trivial choices. The README's qualifying examples: architecture, policy, scope, cross-project contracts.
  • Schema versioning. Every record carries schema_version: 1. Readers tolerate all versions at or below current forever (records are immutable), and refuse newer versions with an explicit error.

Record format (external-consumer contract)

Decision records are YAML-frontmatter Markdown. The frozen frontmatter keys — schema_version, id, title, status, owner, review_date, affected_projects, alternatives, consequences, evidence, supersedes, superseded_by — are the format contract external indexers read (any producer speaking the same format works identically). See plans/plan.md §5.

Schema (v1)

The authoritative schema lives in src/paper_trail/models.py (DecisionRecord); a record is validated on construction, so an invalid record cannot exist. Load untrusted mappings (parsed frontmatter) with DecisionRecord.from_dict, which enforces the schema-version reader policy first.

Field Type Required Rule
schema_version int yes (default 1) >= 1; readers tolerate <= 1, refuse newer with an explicit error
id str yes YYYY-MM-DD-<slug>; real calendar date; slug [a-z0-9-], 1-50 chars, no leading/trailing/double hyphen
title str yes non-empty
status "active" | "superseded" yes one of the two literals
owner str yes non-empty
review_date ISO date (YYYY-MM-DD) yes valid calendar date; a datetime is rejected
affected_projects list[str] no may be empty; entries non-empty
alternatives list[str] yes at least 1 non-empty entry
consequences list[str] yes at least 1 non-empty entry
evidence list[{locator, content_hash?}] no locator non-empty; content_hash, if present, is a 64-char SHA-256 hex digest (format-only; no verification command in v1)
supersedes list[id] no each a valid id; fan-in (many predecessors) allowed
superseded_by id | null no at most one successor; a valid id when set

A record may not supersede or be superseded by itself. Every validation failure raises a specific ValidationError (or UnsupportedSchemaVersionError for a newer schema_version); both derive from SchemaError. The frozen v1 wire shape is pinned by tests/fixtures/golden_v1.json.

Project structure

src/paper_trail/
  models.py   authoritative decision schema
  store.py    atomic record writes + reads
  index.py    deterministic rebuildable JSON index
  graph.py    supersession validation + chain resolution
  render.py   record + digest rendering
  cli.py      add/list/show/supersede/audit/digest
decisions/    tracked records; index.json is derived + gitignored

Qualifying-decision threshold

Record decisions that are architecture, policy, scope, or cross-project contracts — choices a future session would otherwise have to reverse-engineer. Skip minor implementation choices; the required consequences/alternatives fields are the friction that enforces this.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages