diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..1da98ee --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,68 @@ +{ + "permissions": { + "allow": [ + "Bash(pql)", + "Bash(pql *)", + "Bash(/home/jpmschweitzer/.local/bin/pql:*)", + "Bash(git status:*)", + "Bash(git log:*)", + "Bash(git diff:*)", + "Bash(git branch:*)", + "Bash(make test:*)", + "Bash(make test-unit:*)", + "Bash(make test-contracts:*)", + "Bash(make lint:*)", + "Bash(make typecheck:*)", + "Bash(.venv/bin/python -m pytest:*)", + "Bash(.venv/bin/pytest:*)", + "Bash(pytest:*)", + "Bash(ruff check:*)", + "Bash(mypy:*)", + "Bash(docker logs tatlock:*)", + "Bash(curl -s http://localhost:8000/*)", + "Bash(curl -s http://localhost:8777/*)" + ], + "deny": [ + "Bash(sudo *)", + "Bash(su *)", + "Bash(rm -rf /*)", + "Bash(rm -rf ~*)", + "Bash(rm -rf $HOME*)", + "Bash(mkfs*)", + "Bash(dd if=*)", + "Bash(chmod 777 *)", + "Bash(chmod -R 777 *)", + "Bash(find * -exec*)", + "Bash(find * -delete*)", + "Bash(git add -A*)", + "Bash(git * add -A*)", + "Bash(git add --all*)", + "Bash(git * add --all*)", + "Bash(git add .)", + "Bash(git * add .)", + "Bash(git push --force*)", + "Bash(git * push --force*)", + "Bash(git push -f*)", + "Bash(git * push -f*)", + "Bash(git reset --hard*)", + "Bash(git * reset --hard*)", + "Bash(git clean -fd*)", + "Bash(git * clean -fd*)", + "Bash(git clean -fdx*)", + "Bash(git * clean -fdx*)", + "Bash(git branch -D *)", + "Bash(git * branch -D *)", + "Bash(git checkout -- *)", + "Bash(git * checkout -- *)", + "Bash(git restore .*)", + "Bash(git * restore .*)", + "Bash(git commit --no-verify*)", + "Bash(git * commit --no-verify*)", + "Bash(git merge --no-ff*)", + "Bash(git * merge --no-ff*)", + "Bash(redis-cli * FLUSHALL*)", + "Bash(redis-cli * FLUSHDB*)", + "Bash(ollama rm *)" + ] + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..788efb1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.pql/changelog/*.sql merge=union diff --git a/.gitignore b/.gitignore index fcf2970..c6b7e09 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,8 @@ ollama_data/ ehthumbs.db Thumbs.db Desktop.ini + +# Claude Code user-specific settings +.claude/settings.local.json +.pql/* +!.pql/changelog/ diff --git a/.pql/changelog/0000-format.sql b/.pql/changelog/0000-format.sql new file mode 100644 index 0000000..e7d3357 --- /dev/null +++ b/.pql/changelog/0000-format.sql @@ -0,0 +1,11 @@ +-- Changelog format marker, written by pql. Comments only: this file +-- is never executed — Import descends into the per-table directories +-- and does not read the changelog root. +-- +-- A changelog carrying no marker is format 1, the shape that existed +-- before formats were versioned. An older format is migrated forward +-- by `pql plan upgrade` (and automatically from the post-merge hook); +-- a newer one is refused rather than replayed under rules this binary +-- does not know. See D-28 and docs/versions.md. +-- pql:changelog_format: 2.0.0 +-- pql:written_by: 2.2.0 diff --git a/.pql/changelog/ticket_deps/0000-schema.sql b/.pql/changelog/ticket_deps/0000-schema.sql new file mode 100644 index 0000000..e552cd8 --- /dev/null +++ b/.pql/changelog/ticket_deps/0000-schema.sql @@ -0,0 +1,139 @@ +-- Auto-generated by pql init. CREATE TABLE statements +-- for the planning schema; per-table dir keeps the changelog +-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is +-- idempotent so running schema files from each directory in +-- replay order is harmless. +-- +-- Importer parses the markers below to detect schema drift +-- between the producing pql version and the local one — a +-- bumped canonical_version means projection rules changed +-- and replay must refuse rather than silently corrupt state. +-- pql:created_by: 2.2.0 +-- pql:canonical_version: 2 + + +CREATE TABLE IF NOT EXISTS decisions ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')), + domain TEXT NOT NULL, + title TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'active' + CHECK(status IN ('active','superseded','resolved','open')), + date TEXT, + file_path TEXT NOT NULL, + synced_at TEXT NOT NULL DEFAULT (datetime('now')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER +); + +CREATE TABLE IF NOT EXISTS decision_refs ( + source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, + target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, + ref_type TEXT NOT NULL + CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')), + note TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER, + PRIMARY KEY (source_id, target_id, ref_type) +); + +-- Identity split (D-26): a ticket's stable, collision-proof identity is its +-- record_id (a locally-generated ULID, planning.NewRecordID); the friendly +-- T-NNN label lives in ticket_idmap and may be reconciled. Every structural +-- reference (parent, deps, history, labels) targets record_id, so a label +-- clash never corrupts the graph — only ticket_idmap needs a relabel. +CREATE TABLE IF NOT EXISTS tickets ( + record_id TEXT PRIMARY KEY, + type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')), + parent_record_id TEXT REFERENCES tickets(record_id), + title TEXT NOT NULL, + description TEXT, + -- No CHECK enumeration: the ticket status vocabulary is per-vault + -- configurable (ticket_statuses in .pql/config.yaml). Validation lives + -- in Go (planning.StatusSet), so adding/renaming statuses needs no + -- schema change. The DEFAULT is a harmless fallback — CreateTicket + -- always inserts the configured default explicitly. + status TEXT NOT NULL DEFAULT 'backlog', + priority TEXT DEFAULT 'medium' + CHECK(priority IN ('critical','high','medium','low')), + assigned_to TEXT, + team TEXT, + decision_ref TEXT REFERENCES decisions(id), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER +); + +-- ticket_idmap maps a record_id to its current friendly label (T-NNN). +-- ticket_id is intentionally NOT globally unique: two uncoordinated clones +-- can mint the same label, which surfaces as a duplicate-label collision +-- (detected at replay) and is fixed with "pql ticket relabel". +CREATE TABLE IF NOT EXISTS ticket_idmap ( + record_id TEXT PRIMARY KEY REFERENCES tickets(record_id), + ticket_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER +); + +CREATE TABLE IF NOT EXISTS ticket_deps ( + blocker_record_id TEXT NOT NULL REFERENCES tickets(record_id), + blocked_record_id TEXT NOT NULL REFERENCES tickets(record_id), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER, + PRIMARY KEY (blocker_record_id, blocked_record_id) +); + +CREATE TABLE IF NOT EXISTS ticket_history ( + ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id), + field TEXT NOT NULL, + old_value TEXT, + new_value TEXT, + changed_by TEXT, + changed_at TEXT NOT NULL DEFAULT (datetime('now')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT UNIQUE, + canonical_version INTEGER +); + +CREATE TABLE IF NOT EXISTS ticket_labels ( + ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id), + label TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER, + PRIMARY KEY (ticket_record_id, label) +); + +CREATE TABLE IF NOT EXISTS meta ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status); +CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team); +CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref); +CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to); +CREATE INDEX IF NOT EXISTS idx_tickets_parent ON tickets(parent_record_id); +CREATE INDEX IF NOT EXISTS idx_ticket_idmap_label ON ticket_idmap(ticket_id); +CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain); +CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type); +CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id); diff --git a/.pql/changelog/ticket_history/0000-schema.sql b/.pql/changelog/ticket_history/0000-schema.sql new file mode 100644 index 0000000..e552cd8 --- /dev/null +++ b/.pql/changelog/ticket_history/0000-schema.sql @@ -0,0 +1,139 @@ +-- Auto-generated by pql init. CREATE TABLE statements +-- for the planning schema; per-table dir keeps the changelog +-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is +-- idempotent so running schema files from each directory in +-- replay order is harmless. +-- +-- Importer parses the markers below to detect schema drift +-- between the producing pql version and the local one — a +-- bumped canonical_version means projection rules changed +-- and replay must refuse rather than silently corrupt state. +-- pql:created_by: 2.2.0 +-- pql:canonical_version: 2 + + +CREATE TABLE IF NOT EXISTS decisions ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')), + domain TEXT NOT NULL, + title TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'active' + CHECK(status IN ('active','superseded','resolved','open')), + date TEXT, + file_path TEXT NOT NULL, + synced_at TEXT NOT NULL DEFAULT (datetime('now')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER +); + +CREATE TABLE IF NOT EXISTS decision_refs ( + source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, + target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, + ref_type TEXT NOT NULL + CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')), + note TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER, + PRIMARY KEY (source_id, target_id, ref_type) +); + +-- Identity split (D-26): a ticket's stable, collision-proof identity is its +-- record_id (a locally-generated ULID, planning.NewRecordID); the friendly +-- T-NNN label lives in ticket_idmap and may be reconciled. Every structural +-- reference (parent, deps, history, labels) targets record_id, so a label +-- clash never corrupts the graph — only ticket_idmap needs a relabel. +CREATE TABLE IF NOT EXISTS tickets ( + record_id TEXT PRIMARY KEY, + type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')), + parent_record_id TEXT REFERENCES tickets(record_id), + title TEXT NOT NULL, + description TEXT, + -- No CHECK enumeration: the ticket status vocabulary is per-vault + -- configurable (ticket_statuses in .pql/config.yaml). Validation lives + -- in Go (planning.StatusSet), so adding/renaming statuses needs no + -- schema change. The DEFAULT is a harmless fallback — CreateTicket + -- always inserts the configured default explicitly. + status TEXT NOT NULL DEFAULT 'backlog', + priority TEXT DEFAULT 'medium' + CHECK(priority IN ('critical','high','medium','low')), + assigned_to TEXT, + team TEXT, + decision_ref TEXT REFERENCES decisions(id), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER +); + +-- ticket_idmap maps a record_id to its current friendly label (T-NNN). +-- ticket_id is intentionally NOT globally unique: two uncoordinated clones +-- can mint the same label, which surfaces as a duplicate-label collision +-- (detected at replay) and is fixed with "pql ticket relabel". +CREATE TABLE IF NOT EXISTS ticket_idmap ( + record_id TEXT PRIMARY KEY REFERENCES tickets(record_id), + ticket_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER +); + +CREATE TABLE IF NOT EXISTS ticket_deps ( + blocker_record_id TEXT NOT NULL REFERENCES tickets(record_id), + blocked_record_id TEXT NOT NULL REFERENCES tickets(record_id), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER, + PRIMARY KEY (blocker_record_id, blocked_record_id) +); + +CREATE TABLE IF NOT EXISTS ticket_history ( + ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id), + field TEXT NOT NULL, + old_value TEXT, + new_value TEXT, + changed_by TEXT, + changed_at TEXT NOT NULL DEFAULT (datetime('now')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT UNIQUE, + canonical_version INTEGER +); + +CREATE TABLE IF NOT EXISTS ticket_labels ( + ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id), + label TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER, + PRIMARY KEY (ticket_record_id, label) +); + +CREATE TABLE IF NOT EXISTS meta ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status); +CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team); +CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref); +CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to); +CREATE INDEX IF NOT EXISTS idx_tickets_parent ON tickets(parent_record_id); +CREATE INDEX IF NOT EXISTS idx_ticket_idmap_label ON ticket_idmap(ticket_id); +CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain); +CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type); +CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id); diff --git a/.pql/changelog/ticket_idmap/0000-schema.sql b/.pql/changelog/ticket_idmap/0000-schema.sql new file mode 100644 index 0000000..e552cd8 --- /dev/null +++ b/.pql/changelog/ticket_idmap/0000-schema.sql @@ -0,0 +1,139 @@ +-- Auto-generated by pql init. CREATE TABLE statements +-- for the planning schema; per-table dir keeps the changelog +-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is +-- idempotent so running schema files from each directory in +-- replay order is harmless. +-- +-- Importer parses the markers below to detect schema drift +-- between the producing pql version and the local one — a +-- bumped canonical_version means projection rules changed +-- and replay must refuse rather than silently corrupt state. +-- pql:created_by: 2.2.0 +-- pql:canonical_version: 2 + + +CREATE TABLE IF NOT EXISTS decisions ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')), + domain TEXT NOT NULL, + title TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'active' + CHECK(status IN ('active','superseded','resolved','open')), + date TEXT, + file_path TEXT NOT NULL, + synced_at TEXT NOT NULL DEFAULT (datetime('now')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER +); + +CREATE TABLE IF NOT EXISTS decision_refs ( + source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, + target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, + ref_type TEXT NOT NULL + CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')), + note TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER, + PRIMARY KEY (source_id, target_id, ref_type) +); + +-- Identity split (D-26): a ticket's stable, collision-proof identity is its +-- record_id (a locally-generated ULID, planning.NewRecordID); the friendly +-- T-NNN label lives in ticket_idmap and may be reconciled. Every structural +-- reference (parent, deps, history, labels) targets record_id, so a label +-- clash never corrupts the graph — only ticket_idmap needs a relabel. +CREATE TABLE IF NOT EXISTS tickets ( + record_id TEXT PRIMARY KEY, + type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')), + parent_record_id TEXT REFERENCES tickets(record_id), + title TEXT NOT NULL, + description TEXT, + -- No CHECK enumeration: the ticket status vocabulary is per-vault + -- configurable (ticket_statuses in .pql/config.yaml). Validation lives + -- in Go (planning.StatusSet), so adding/renaming statuses needs no + -- schema change. The DEFAULT is a harmless fallback — CreateTicket + -- always inserts the configured default explicitly. + status TEXT NOT NULL DEFAULT 'backlog', + priority TEXT DEFAULT 'medium' + CHECK(priority IN ('critical','high','medium','low')), + assigned_to TEXT, + team TEXT, + decision_ref TEXT REFERENCES decisions(id), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER +); + +-- ticket_idmap maps a record_id to its current friendly label (T-NNN). +-- ticket_id is intentionally NOT globally unique: two uncoordinated clones +-- can mint the same label, which surfaces as a duplicate-label collision +-- (detected at replay) and is fixed with "pql ticket relabel". +CREATE TABLE IF NOT EXISTS ticket_idmap ( + record_id TEXT PRIMARY KEY REFERENCES tickets(record_id), + ticket_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER +); + +CREATE TABLE IF NOT EXISTS ticket_deps ( + blocker_record_id TEXT NOT NULL REFERENCES tickets(record_id), + blocked_record_id TEXT NOT NULL REFERENCES tickets(record_id), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER, + PRIMARY KEY (blocker_record_id, blocked_record_id) +); + +CREATE TABLE IF NOT EXISTS ticket_history ( + ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id), + field TEXT NOT NULL, + old_value TEXT, + new_value TEXT, + changed_by TEXT, + changed_at TEXT NOT NULL DEFAULT (datetime('now')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT UNIQUE, + canonical_version INTEGER +); + +CREATE TABLE IF NOT EXISTS ticket_labels ( + ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id), + label TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER, + PRIMARY KEY (ticket_record_id, label) +); + +CREATE TABLE IF NOT EXISTS meta ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status); +CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team); +CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref); +CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to); +CREATE INDEX IF NOT EXISTS idx_tickets_parent ON tickets(parent_record_id); +CREATE INDEX IF NOT EXISTS idx_ticket_idmap_label ON ticket_idmap(ticket_id); +CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain); +CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type); +CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id); diff --git a/.pql/changelog/ticket_labels/0000-schema.sql b/.pql/changelog/ticket_labels/0000-schema.sql new file mode 100644 index 0000000..e552cd8 --- /dev/null +++ b/.pql/changelog/ticket_labels/0000-schema.sql @@ -0,0 +1,139 @@ +-- Auto-generated by pql init. CREATE TABLE statements +-- for the planning schema; per-table dir keeps the changelog +-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is +-- idempotent so running schema files from each directory in +-- replay order is harmless. +-- +-- Importer parses the markers below to detect schema drift +-- between the producing pql version and the local one — a +-- bumped canonical_version means projection rules changed +-- and replay must refuse rather than silently corrupt state. +-- pql:created_by: 2.2.0 +-- pql:canonical_version: 2 + + +CREATE TABLE IF NOT EXISTS decisions ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')), + domain TEXT NOT NULL, + title TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'active' + CHECK(status IN ('active','superseded','resolved','open')), + date TEXT, + file_path TEXT NOT NULL, + synced_at TEXT NOT NULL DEFAULT (datetime('now')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER +); + +CREATE TABLE IF NOT EXISTS decision_refs ( + source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, + target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, + ref_type TEXT NOT NULL + CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')), + note TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER, + PRIMARY KEY (source_id, target_id, ref_type) +); + +-- Identity split (D-26): a ticket's stable, collision-proof identity is its +-- record_id (a locally-generated ULID, planning.NewRecordID); the friendly +-- T-NNN label lives in ticket_idmap and may be reconciled. Every structural +-- reference (parent, deps, history, labels) targets record_id, so a label +-- clash never corrupts the graph — only ticket_idmap needs a relabel. +CREATE TABLE IF NOT EXISTS tickets ( + record_id TEXT PRIMARY KEY, + type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')), + parent_record_id TEXT REFERENCES tickets(record_id), + title TEXT NOT NULL, + description TEXT, + -- No CHECK enumeration: the ticket status vocabulary is per-vault + -- configurable (ticket_statuses in .pql/config.yaml). Validation lives + -- in Go (planning.StatusSet), so adding/renaming statuses needs no + -- schema change. The DEFAULT is a harmless fallback — CreateTicket + -- always inserts the configured default explicitly. + status TEXT NOT NULL DEFAULT 'backlog', + priority TEXT DEFAULT 'medium' + CHECK(priority IN ('critical','high','medium','low')), + assigned_to TEXT, + team TEXT, + decision_ref TEXT REFERENCES decisions(id), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER +); + +-- ticket_idmap maps a record_id to its current friendly label (T-NNN). +-- ticket_id is intentionally NOT globally unique: two uncoordinated clones +-- can mint the same label, which surfaces as a duplicate-label collision +-- (detected at replay) and is fixed with "pql ticket relabel". +CREATE TABLE IF NOT EXISTS ticket_idmap ( + record_id TEXT PRIMARY KEY REFERENCES tickets(record_id), + ticket_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER +); + +CREATE TABLE IF NOT EXISTS ticket_deps ( + blocker_record_id TEXT NOT NULL REFERENCES tickets(record_id), + blocked_record_id TEXT NOT NULL REFERENCES tickets(record_id), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER, + PRIMARY KEY (blocker_record_id, blocked_record_id) +); + +CREATE TABLE IF NOT EXISTS ticket_history ( + ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id), + field TEXT NOT NULL, + old_value TEXT, + new_value TEXT, + changed_by TEXT, + changed_at TEXT NOT NULL DEFAULT (datetime('now')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT UNIQUE, + canonical_version INTEGER +); + +CREATE TABLE IF NOT EXISTS ticket_labels ( + ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id), + label TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER, + PRIMARY KEY (ticket_record_id, label) +); + +CREATE TABLE IF NOT EXISTS meta ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status); +CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team); +CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref); +CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to); +CREATE INDEX IF NOT EXISTS idx_tickets_parent ON tickets(parent_record_id); +CREATE INDEX IF NOT EXISTS idx_ticket_idmap_label ON ticket_idmap(ticket_id); +CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain); +CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type); +CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id); diff --git a/.pql/changelog/tickets/0000-schema.sql b/.pql/changelog/tickets/0000-schema.sql new file mode 100644 index 0000000..e552cd8 --- /dev/null +++ b/.pql/changelog/tickets/0000-schema.sql @@ -0,0 +1,139 @@ +-- Auto-generated by pql init. CREATE TABLE statements +-- for the planning schema; per-table dir keeps the changelog +-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is +-- idempotent so running schema files from each directory in +-- replay order is harmless. +-- +-- Importer parses the markers below to detect schema drift +-- between the producing pql version and the local one — a +-- bumped canonical_version means projection rules changed +-- and replay must refuse rather than silently corrupt state. +-- pql:created_by: 2.2.0 +-- pql:canonical_version: 2 + + +CREATE TABLE IF NOT EXISTS decisions ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')), + domain TEXT NOT NULL, + title TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'active' + CHECK(status IN ('active','superseded','resolved','open')), + date TEXT, + file_path TEXT NOT NULL, + synced_at TEXT NOT NULL DEFAULT (datetime('now')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER +); + +CREATE TABLE IF NOT EXISTS decision_refs ( + source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, + target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, + ref_type TEXT NOT NULL + CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')), + note TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER, + PRIMARY KEY (source_id, target_id, ref_type) +); + +-- Identity split (D-26): a ticket's stable, collision-proof identity is its +-- record_id (a locally-generated ULID, planning.NewRecordID); the friendly +-- T-NNN label lives in ticket_idmap and may be reconciled. Every structural +-- reference (parent, deps, history, labels) targets record_id, so a label +-- clash never corrupts the graph — only ticket_idmap needs a relabel. +CREATE TABLE IF NOT EXISTS tickets ( + record_id TEXT PRIMARY KEY, + type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')), + parent_record_id TEXT REFERENCES tickets(record_id), + title TEXT NOT NULL, + description TEXT, + -- No CHECK enumeration: the ticket status vocabulary is per-vault + -- configurable (ticket_statuses in .pql/config.yaml). Validation lives + -- in Go (planning.StatusSet), so adding/renaming statuses needs no + -- schema change. The DEFAULT is a harmless fallback — CreateTicket + -- always inserts the configured default explicitly. + status TEXT NOT NULL DEFAULT 'backlog', + priority TEXT DEFAULT 'medium' + CHECK(priority IN ('critical','high','medium','low')), + assigned_to TEXT, + team TEXT, + decision_ref TEXT REFERENCES decisions(id), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER +); + +-- ticket_idmap maps a record_id to its current friendly label (T-NNN). +-- ticket_id is intentionally NOT globally unique: two uncoordinated clones +-- can mint the same label, which surfaces as a duplicate-label collision +-- (detected at replay) and is fixed with "pql ticket relabel". +CREATE TABLE IF NOT EXISTS ticket_idmap ( + record_id TEXT PRIMARY KEY REFERENCES tickets(record_id), + ticket_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER +); + +CREATE TABLE IF NOT EXISTS ticket_deps ( + blocker_record_id TEXT NOT NULL REFERENCES tickets(record_id), + blocked_record_id TEXT NOT NULL REFERENCES tickets(record_id), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER, + PRIMARY KEY (blocker_record_id, blocked_record_id) +); + +CREATE TABLE IF NOT EXISTS ticket_history ( + ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id), + field TEXT NOT NULL, + old_value TEXT, + new_value TEXT, + changed_by TEXT, + changed_at TEXT NOT NULL DEFAULT (datetime('now')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT UNIQUE, + canonical_version INTEGER +); + +CREATE TABLE IF NOT EXISTS ticket_labels ( + ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id), + label TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT, + hash TEXT, + canonical_version INTEGER, + PRIMARY KEY (ticket_record_id, label) +); + +CREATE TABLE IF NOT EXISTS meta ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status); +CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team); +CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref); +CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to); +CREATE INDEX IF NOT EXISTS idx_tickets_parent ON tickets(parent_record_id); +CREATE INDEX IF NOT EXISTS idx_ticket_idmap_label ON ticket_idmap(ticket_id); +CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain); +CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type); +CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id); diff --git a/governance/README.md b/governance/README.md new file mode 100644 index 0000000..0945720 --- /dev/null +++ b/governance/README.md @@ -0,0 +1,54 @@ +# Decisions, Questions, Rejected + +This directory holds structured planning records that pql parses +into pql.db. Each record is a `### [DQR]-N: Title` heading inside +a markdown file. Files live in three per-type subdirectories: + +- `decisions/.md` — confirmed design decisions +- `questions/.md` — open questions that may resolve into + decisions or rejected proposals +- `rejected/.md` — rejected proposals (kept for the audit + trail) + +The parser infers domain from the filename stem and record type +from the parent subdirectory. + +D-records that propose implementation work link to `initiative`-type +tickets via `decision_ref`. Run `pql decisions show +--with-tickets` to inspect implementation status. + +## Recommended domains + +Start with this canonical set; create files as records land in +each domain: + +- **architecture** — structural commitments (storage, layering, + languages, libraries) +- **process** — team workflow (commits, branches, releases, reviews) +- **design** — user-facing surface (UX, UI, public APIs) +- **coding-conventions** — team-internal code shape (style, lint, + file layout) +- **testing** — quality strategy (coverage, layers, gates) + +You might also want, project-permitting: + +- `accessibility` — if you ship user-facing software +- `security` — if you handle user data or network surfaces +- `licensing` — if you release open-source or commercial +- `documentation` — if user-docs are non-trivial +- `deployment` — if shipping is non-trivial +- `performance` — if you have perf budgets / SLOs + + + +## Decisions + +- _(none)_ + +## Open questions + +- _(none)_ + +## Rejected + +- _(none)_