Replaces the labels-as-milestones pattern (carried over from the retired SQLite milestone subsystem) with pql's native initiative/epic hierarchy, which already modelled this: initiative T-745 (Development Cascade) holds the six phases as epics. A ticket is in a phase by being parented under that phase epic — self-maintaining, no label to apply or forget. The `phase:4` label had already drifted (66 tickets under the Phase-4 epic T-750 but only 42 labelled). Fixes: - T-750 (Phase 4) -> in_progress to mark the active phase (sequential per D-166). - Re-parented the 2 strays (T-974 Atlas-to-tile epic, T-1008 economy task) under T-750. - Dropped all 42 phase:4 labels (tombstoned in the changelog). - /whats-next, /ticket, ticket-cli.md, CLAUDE.md now gate on `pql ticket list --under <active-phase-epic> --unblocked` instead of `--label phase:4`. - Fixed the label-action verb in the docs: pql uses `add|rm`, not `add|remove`. - pql-requirements #9 flipped: no milestone entity / labels-as-milestones needed — the initiative/epic tree is the answer. Verified: `pql plan rebuild` reconstructs the mutations; `--under T-750 --unblocked` returns the active phase's ready work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10 KiB
title, description, type, status, created, updated, tags
| title | description | type | status | created | updated | tags | |||
|---|---|---|---|---|---|---|---|---|---|
| pql Requirements — gaps surfaced by the Settled Reach migration | Feature gaps and friction points found while migrating a 1013-ticket + 358-decision project onto pql, for the pql team | requirements | draft | 2026-06-06 | 2026-06-06 |
|
pql Requirements — gaps for the pql team
Compiled while planning a migration of The Settled Reach (a Godot+Rust game) off a SQLite-backed ticket CLI + markdown-decisions-sync onto pql. Scale: 1013 tickets, 481 dependency edges, 48 labels, 16 history rows; 358 decision records (237 D / 109 Q / 12 R) with 1040 cross-refs; a ~4385-file markdown vault (decisions, docs, 22 workshops, ~3500 wiki pages).
pql is a strong fit and most of our system maps cleanly — unsurprisingly, since pql was
developed off the lessons of this repo's planning structures (the SQLite ticket+decision
schema, the decisions/ domain split, the kanban/whats-next flow). The enums, the DQR tree,
and the kanban model match almost exactly; pql's deliberate departures (TEXT ids over our
integer ids, a git-tracked text changelog over our binary-DB-plus-backup ritual, and no
milestone entity) are the productized lessons. So the items below are best read as feedback to
a descendant, not gaps in a foreign tool — and a few are deliberate simplifications we should
adopt rather than ask pql to re-add. Each lists why, a suggested shape, and a
priority; viable workarounds are noted.
1. Seeded / explicit ticket IDs + id-counter advance — RESOLVED (non-issue)
Why it looked critical. Our 1013 tickets have integer ids referenced as #440 in thousands of
immutable git commits, in decision "Ticket: #N" lines, and by PR automation. We need the pql id to
preserve the number — T-440 ≡ old #440. The fear was that, since pql ticket new has no --id
flag and auto-assigns T-N sequentially, a post-seed native create would mint T-1 and collide.
Why it's actually fine (verified against pql 1.6.2). Bulk seeding goes through direct-INSERT
into pql.db + pql plan export, not ticket new. And pql's id counter is derived from
max(id) — exactly the behaviour our own integer-id CLI had. After seeding T-1…T-1021, the next
pql ticket new minted T-1022, not T-1. No collision, no recycling, no held creation. The
--id gap only bites the (rare) case of hand-creating a specific id via the CLI — irrelevant to
a bulk numbered migration.
Optional nicety (low priority). A pql ticket new --id T-1022 flag would still be convenient
for the occasional manual back-fill, but it is not required for a clean migration.
2. Duplicate-ID detection in pql decisions sync / validate — HIGH
Why. Our markdown had a genuine integrity bug: D-035 defined in two domain files, R-011
defined three times. pql decisions sync silently last-writer-wins-collapsed them (reported
broken: 0), so the duplication was invisible. Our legacy Python check-dupes errors on it. A
duplicate canonical-ID is almost always a data bug, not an intended merge.
Suggested shape. pql decisions validate (and sync --strict) should warn or error on
duplicate D/Q/R ids across the vault, reporting file:line for each occurrence.
Workaround. Keep our Python check-dupes wired into pre-commit alongside pql — extra tooling
we'd like to retire.
3. core.hooksPath awareness in pql init — HIGH
Why. Our repo sets core.hooksPath = .config/hooks (so hooks are version-controlled). pql init
installs its pre-commit/post-merge/post-checkout hooks into .git/hooks/, which git ignores when
core.hooksPath is set. pql's changelog-staging + replay hooks would be silently dead — the whole
durable-versioning model depends on them. This is a quiet, dangerous failure (data appears to work, but
.pql/changelog/ is never staged).
Suggested shape. pql init/pql doctor should detect core.hooksPath and either install into the
configured dir or emit a loud warning with the manual fold-in snippet.
Workaround. Manually fold pql's hook logic into .config/hooks/* (we will), but pql doctor
should flag the mismatch so others don't get bitten.
4. Document the changelog seed format + hash/canonical_version algorithm — HIGH
Why. pql plan import --legacy is documented as retired; the real durable artifact is
.pql/changelog/<table>/<YYYY-MM>.sql. To migrate 1013 tickets we must emit those SQL rows directly.
We reverse-engineered the UPSERT shape, but the per-row hash and canonical_version columns +
the LWW guard (WHERE excluded.updated_at > … OR (… AND excluded.hash > …)) are opaque. If our seed
hashes don't match pql's scheme, later native edits still win by updated_at (so correctness holds),
but the LWW tie-break becomes unpredictable.
Suggested shape. Document (a) the canonical changelog row format per table, (b) the hash
input + algorithm, (c) how canonical_version increments — OR provide a supported one-shot
pql plan seed <json|csv> import that computes them.
Workaround. Emit our own stable per-row hash; rely on updated_at for correctness. Works, but
under-specified.
5. Bare-text reference detection / auto-link for D-NNN / T-NNN — MEDIUM
Why. Across ~4385 vault files (esp. ~3500 generated wiki pages), decision/ticket references are
mostly bare text (D-117, #440) rather than markdown/wikilinks. pql search finds them, but
pql backlinks/related only follow real links — so "what references D-117?" is incomplete. Rewriting
3500+ generated files to add links is impractical (they're regenerated from a pipeline).
Suggested shape. Optional reference recognizers (configurable regex → entity, e.g.
D-\d+→decision, T-\d+→ticket) so backlinks/related treat bare-text mentions as edges without
requiring link syntax in source.
Workaround. Adopt a link convention in authored files only; accept that generated files are search-only.
Related gap — frontmatter list-membership in the DSL. Our Phase-5 provenance puts the decisions
each workshop touches in a decision_refs: [D-010, …] frontmatter list. pql indexes the list and
SELECT fm.decision_refs round-trips it, but we found no DSL operator that filters by membership:
fm.decision_refs ~ 'D-010' and … contains … are lex/parse errors, and 'D-010' in fm.decision_refs
parses but matches nothing. So "which workshops touch D-010?" needs a SELECT + client-side filter or
a pql search. A documented list-membership operator (or making in work against frontmatter arrays)
would close this.
6. WIP-limit enforcement (advisory) on ticket status … in_progress — MEDIUM
Why. Our kanban flow enforces a WIP limit (max 5 in_progress); our activation step relied on the CLI warning when exceeded. pql has no forced state machine / WIP concept.
Suggested shape. An optional config (wip_limit: 5) that makes pql ticket status <id> in_progress
emit an advisory warning (not a hard block) when the in_progress count would exceed it.
Workaround. Compute pql ticket list --status in_progress | count in our skill and warn there.
7. Fan-out / downstream-unblock count on ticket show — MEDIUM
Why. Our batch-selection ranks ready tickets partly by fan-out — how many downstream tickets a
ticket unblocks. We currently compute it from the dependency graph. pql has --unblocked (great — it
replaced our manual blocker walk) but no "how many does this unblock" signal.
Suggested shape. A blocks_count / unblocks_count field on pql ticket show/list, or a
pql ticket list --orders-by fanout.
Workaround. Compute client-side from ticket show --with-context dep data.
8. Multi-team / array team field — LOW
Why. One ticket carries a comma-separated team (client,server). pql treats team as a single
string. (1 of 1013 — genuinely low.)
Suggested shape. Accept an array/comma team, or document the single-team constraint.
Workaround. Pick the primary team for the one affected ticket.
9. No milestone entity needed — the ticket hierarchy IS the milestone — RESOLVED (no ask)
Why it looked like a gap. Our old schema had a milestones subsystem (milestones,
ticket_milestones, milestone_deps, cascade_phase) — but it was vestigial (2 milestones,
milestone_deps empty, 42/1013 linked, cascade ordering never exercised). The migration first
mapped the active milestone to a label (phase:4).
What we landed on — and it's better. The labels-as-milestones pattern immediately drifted
(66 tickets lived under the Phase-4 epic but only 42 carried the label). The fix wasn't a milestone
entity or a label convention: it was to use pql's native initiative/epic hierarchy, which
already modelled this. The repo has an initiative T-745 (Development Cascade) with the six phases
as epics under it; a ticket belongs to a phase by being parented under that phase epic
(pql ticket list --under <phase-epic>), and the active phase is the in_progress phase epic. This
is self-maintaining — parent a ticket and it's in the phase; no label to apply, forget, or let rot.
Net for the pql team: dropping the milestone entity was the right call — don't add one back, and
don't bless labels-as-milestones either. The initiative→epic→story→task tree already expresses phase
gating cleanly. The only nice-to-have is a documented "active node" convention (status in_progress
on a grouping ticket) and maybe a --under done/total rollup. Not blocking; no entity needed.
Summary
| # | Requirement | Priority | Have workaround? |
|---|---|---|---|
| 1 | Seeded/explicit ticket id (--id flag) |
N/A — direct-INSERT + max-id counter already works | |
| 2 | Duplicate-ID detection in sync/validate | High | Yes (keep our checker) |
| 3 | core.hooksPath awareness |
High | Yes (manual fold-in) |
| 4 | Documented changelog seed format + hash algo | High | Partial |
| 5 | Bare-text ref detection for backlinks | Medium | Partial (authored files only) |
| 6 | WIP-limit advisory | Medium | Yes (skill-side) |
| 7 | Fan-out / unblocks count | Medium | Yes (client-side) |
| 8 | Multi-team field | Low | Yes |
| 9 | Milestone entity / labels-as-milestones | N/A — use the initiative/epic hierarchy |